Repos / pytaku / fc7236d595
commit fc7236d59518fc11b75d12b307cdb978ea7fe879
Author: Bùi Thành Nhân <hi@imnhan.com>
Date: Sun Aug 23 22:52:51 2020 +0700
implement "read all", tweak follows page
- "read all" button on Title page allows to mark all chapters as read
- Follows page now shows all unread chapters instead of just chapters
that are newer than latest read chapter.
diff --git a/src/pytaku/database/migrations/latest_schema.sql b/src/pytaku/database/migrations/latest_schema.sql
index c9bf35d..fc65604 100644
--- a/src/pytaku/database/migrations/latest_schema.sql
+++ b/src/pytaku/database/migrations/latest_schema.sql
@@ -50,6 +50,15 @@ CREATE TABLE IF NOT EXISTS "chapter" (
unique(site, title_id, id),
unique(site, title_id, num_major, num_minor)
);
+CREATE TABLE token (
+ user_id integer not null,
+ token text unique not null,
+ created_at text not null default (datetime('now')),
+ last_accessed_at text not null default (datetime('now')),
+ lifespan text not null, -- '+1 day', '+365 days', etc.
+
+ foreign key (user_id) references user (id)
+);
CREATE TABLE IF NOT EXISTS "read" (
user_id integer not null,
site text not null,
@@ -58,15 +67,5 @@ CREATE TABLE IF NOT EXISTS "read" (
updated_at text default (datetime('now')),
foreign key (user_id) references user (id),
- foreign key (site, title_id, chapter_id) references chapter (site, title_id, id),
unique(user_id, site, title_id, chapter_id)
);
-CREATE TABLE token (
- user_id integer not null,
- token text unique not null,
- created_at text not null default (datetime('now')),
- last_accessed_at text not null default (datetime('now')),
- lifespan text not null, -- '+1 day', '+365 days', etc.
-
- foreign key (user_id) references user (id)
-);
diff --git a/src/pytaku/database/migrations/m0005.sql b/src/pytaku/database/migrations/m0005.sql
new file mode 100644
index 0000000..b4adb73
--- /dev/null
+++ b/src/pytaku/database/migrations/m0005.sql
@@ -0,0 +1,24 @@
+-- Remove foreign key from "read" table pointing to "chapter".
+-- So we can, say, mark all chapters of a title as read even if some of those
+-- chapters haven't been created.
+
+pragma foreign_keys = off; -- to let us do anything at all
+begin transaction;
+
+create table new_read (
+ user_id integer not null,
+ site text not null,
+ title_id text, -- nullable to accomodate existing mangadex rows, urgh.
+ chapter_id text not null,
+ updated_at text default (datetime('now')),
+
+ foreign key (user_id) references user (id),
+ unique(user_id, site, title_id, chapter_id)
+);
+insert into new_read select * from read;
+drop table read;
+alter table new_read rename to read;
+
+pragma foreign_key_check;
+commit;
+pragma foreign_keys = on;
diff --git a/src/pytaku/main.py b/src/pytaku/main.py
index d934a5e..95b7721 100644
--- a/src/pytaku/main.py
+++ b/src/pytaku/main.py
@@ -542,12 +542,13 @@ def api_read():
if reads:
for r in reads:
+ print(">> reading", request.user_id, r)
read(
request.user_id, r["site"], r["title_id"], r["chapter_id"],
)
if unreads:
for u in unreads:
- read(
+ unread(
request.user_id, u["site"], u["title_id"], u["chapter_id"],
)
# TODO: rewrite read/unread to do bulk updates instead of n+1 queries like these.
diff --git a/src/pytaku/persistence.py b/src/pytaku/persistence.py
index 7cb0a81..2a246e0 100644
--- a/src/pytaku/persistence.py
+++ b/src/pytaku/persistence.py
@@ -80,10 +80,9 @@ def load_title(site, title_id, user_id=None):
"""
SELECT r.chapter_id
FROM read r
- INNER JOIN chapter c ON c.id = r.chapter_id AND c.site = r.site
WHERE r.user_id = ?
- AND c.title_id = ?
- AND c.site = ?
+ AND r.title_id = ?
+ AND r.site = ?
ORDER BY r.updated_at;
""",
(user_id, title["id"], title["site"]),
@@ -248,27 +247,18 @@ def get_followed_titles(user_id):
# n+1 queries cuz I don't give a f- actually I do, but sqlite's cool with it:
# https://www.sqlite.org/np1queryprob.html
- chapters_i_finished = run_sql_on_demand(
+ chapters_i_finished = run_sql(
"""
- SELECT r.chapter_id
- FROM read r
- INNER JOIN chapter c ON c.id = r.chapter_id AND c.site = r.site
- WHERE r.user_id = ?
- AND c.title_id = ?
- AND c.site = ?
- ORDER BY c.num_major desc, c.num_minor desc;
+ SELECT chapter_id
+ FROM read
+ WHERE user_id = ?
+ AND title_id = ?
+ AND site = ?;
""",
(user_id, t["id"], t["site"]),
)
- # Cut off chapter list:
- # only show chapters newer than the latest chapter that user has finished.
- # Running a loop here instead of just picking the one latest finished chapter
- # because source site may have deleted said chapter.
- for finished_chapter_id in chapters_i_finished:
- for i, ch in enumerate(chapters):
- if finished_chapter_id == ch["id"]:
- chapters = chapters[:i]
- break
+ # Only show chapters that user hasn't read
+ chapters = [ch for ch in chapters if ch["id"] not in chapters_i_finished]
t["chapters"] = chapters
diff --git a/src/pytaku/static/js/routes/title.js b/src/pytaku/static/js/routes/title.js
index 529bceb..c5a368e 100644
--- a/src/pytaku/static/js/routes/title.js
+++ b/src/pytaku/static/js/routes/title.js
@@ -4,7 +4,9 @@ import { LoadingMessage, Button, fullChapterName, Chapter } from "../utils.js";
function Title(initialVNode) {
let isLoading = false;
let isTogglingFollow = false;
+ let isMarkingAllAsRead = false;
let title = {};
+ let allAreRead;
return {
oninit: (vnode) => {
@@ -29,6 +31,15 @@ function Title(initialVNode) {
});
},
view: (vnode) => {
+ if (!isLoading && Auth.isLoggedIn()) {
+ allAreRead = true;
+ for (let chap of title.chapters) {
+ if (!chap.is_read) {
+ allAreRead = false;
+ break;
+ }
+ }
+ }
return m(
"div.content",
isLoading
@@ -37,40 +48,82 @@ function Title(initialVNode) {
m("h1", title.name),
m("div.title--details", [
Auth.isLoggedIn()
- ? m(Button, {
- icon: "bookmark",
- disabled: isTogglingFollow ? "disabled" : null,
- text: isTogglingFollow
- ? "submitting..."
- : title.is_following
- ? "following"
- : "follow",
- color: title.is_following ? "red" : "green",
- title: title.is_following
- ? "Click to unfollow"
- : "Click to follow",
- onclick: (ev) => {
- isTogglingFollow = true;
- m.redraw();
- Auth.request({
- method: "POST",
- url: "/api/follow",
- body: {
- site: title.site,
- title_id: title.id,
- follow: !title.is_following,
- },
- })
- .then((resp) => {
- title.is_following = resp.follow;
+ ? [
+ m(Button, {
+ icon: "bookmark",
+ disabled: isTogglingFollow ? "disabled" : null,
+ text: isTogglingFollow
+ ? "submitting..."
+ : title.is_following
+ ? "following"
+ : "follow",
+ color: title.is_following ? "red" : "green",
+ title: title.is_following
+ ? "Click to unfollow"
+ : "Click to follow",
+ onclick: (ev) => {
+ isTogglingFollow = true;
+ m.redraw();
+ Auth.request({
+ method: "POST",
+ url: "/api/follow",
+ body: {
+ site: title.site,
+ title_id: title.id,
+ follow: !title.is_following,
+ },
})
- .finally(() => {
- isTogglingFollow = false;
- });
- },
- })
+ .then((resp) => {
+ title.is_following = resp.follow;
+ })
+ .finally(() => {
+ isTogglingFollow = false;
+ });
+ },
+ }),
+ m(Button, {
+ icon: "eye",
+ disabled:
+ isMarkingAllAsRead || allAreRead ? "disabled" : null,
+ text: isMarkingAllAsRead
+ ? "submitting..."
+ : allAreRead
+ ? "no new chapters"
+ : "read all",
+ color: "green",
+ title: allAreRead
+ ? null
+ : "Click to mark all chapters as read",
+ onclick: (ev) => {
+ isMarkingAllAsRead = true;
+ m.redraw();
+ Auth.request({
+ method: "POST",
+ url: "/api/read",
+ body: {
+ read: title.chapters
+ .filter((ch) => !ch.is_read)
+ .map((ch) => {
+ return {
+ site: title.site,
+ title_id: title.id,
+ chapter_id: ch.id,
+ };
+ }),
+ },
+ })
+ .then((resp) => {
+ title.chapters.forEach((chap) => {
+ chap.is_read = true;
+ });
+ })
+ .finally(() => {
+ isMarkingAllAsRead = false;
+ });
+ },
+ }),
+ ]
: null,
- " ",
m(
"a.touch-friendly[title=Go to source site][target=_blank]",
{ href: title.source_url },
diff --git a/src/pytaku/static/spa.css b/src/pytaku/static/spa.css
index 9d7bc59..f2bebf5 100644
--- a/src/pytaku/static/spa.css
+++ b/src/pytaku/static/spa.css
@@ -287,6 +287,9 @@ .title--cover {
.title--details {
margin: 1rem 0;
}
+.title--details > * {
+ margin-right: 0.3rem;
+}
/* Chapter route */
.chapter.content {
@@ -327,7 +330,7 @@ .utils--chapter {
.utils--chapter.read > a:before {
content: "🗹";
color: green;
- margin-right: 0.2rem;
+ margin-right: 0.4rem;
font-weight: bold;
}
.utils--chapter > a {