Repos / pytaku / b316a044c8
commit b316a044c8228c3579789e64bc1dadcc06415f52
Author: Bùi Thành Nhân <hi@imnhan.com>
Date:   Sun Sep 5 17:42:32 2021 +0700

    fix race condition when finishing title
    
    When viewing latest chapter of a title, make sure to only transition to
    next route (title details) after the "mark chapter as read" request is
    done, so that we don't end up showing the last chapter as still unread
    in the title details route.

diff --git a/src/pytaku/js-src/routes/authentication.js b/src/pytaku/js-src/routes/authentication.js
index ecdaa13..30cc850 100644
--- a/src/pytaku/js-src/routes/authentication.js
+++ b/src/pytaku/js-src/routes/authentication.js
@@ -64,7 +64,6 @@ function Authentication(initialVNode) {
             m("input[placeholder=username][name=username][required]", {
               value: loginUsername,
               oninput: (e) => {
-                console.log("username onInput:", e.target.value);
                 loginUsername = e.target.value;
               },
             }),
@@ -73,7 +72,6 @@ function Authentication(initialVNode) {
               {
                 value: loginPassword,
                 oninput: (e) => {
-                  console.log("password onInput: size", e.target.value.length);
                   loginPassword = e.target.value;
                 },
               }
diff --git a/src/pytaku/js-src/routes/chapter.js b/src/pytaku/js-src/routes/chapter.js
index ea4d77a..fa150ec 100644
--- a/src/pytaku/js-src/routes/chapter.js
+++ b/src/pytaku/js-src/routes/chapter.js
@@ -60,6 +60,7 @@ function FallbackableImg(initialVNode) {
 
 function Chapter(initialVNode) {
   let isLoading = false;
+  let isMarkingLastChapterAsRead = false;
   let chapter = {};
   let loadedPages = [];
   let pendingPages = [];
@@ -112,6 +113,94 @@ function Chapter(initialVNode) {
     }
   }
 
+  function markChapterAsRead(site, titleId, chapterId) {
+    return Auth.request({
+      method: "POST",
+      url: "/api/read",
+      body: {
+        read: [
+          {
+            site,
+            title_id: titleId,
+            chapter_id: chapterId,
+          },
+        ],
+      },
+    });
+  }
+
+  function buttonsView(site, prev, next) {
+    const nextRoute = next
+      ? `/m/${site}/${titleId}/${next.id}`
+      : `/m/${site}/${titleId}`;
+
+    return m("div.chapter--buttons", [
+      prev
+        ? m(
+            m.route.Link,
+            {
+              class: "touch-friendly",
+              href: `/m/${site}/${titleId}/${prev.id}`,
+            },
+            [m("i.icon.icon-chevrons-left"), m("span", "prev")]
+          )
+        : m(Button, {
+            text: "prev",
+            icon: "chevrons-left",
+            disabled: true,
+          }),
+      m(
+        m.route.Link,
+        {
+          class: "touch-friendly",
+          href: `/m/${site}/${titleId}`,
+        },
+        [m("i.icon.icon-list"), m("span", " chapter list")]
+      ),
+      m(
+        m.route.Link,
+        {
+          class:
+            "touch-friendly" + (isMarkingLastChapterAsRead ? " disabled" : ""),
+          href: nextRoute,
+          disabled: isMarkingLastChapterAsRead,
+          onclick: (ev) => {
+            if (Auth.isLoggedIn()) {
+              if (next) {
+                markChapterAsRead(site, titleId, chapter.id);
+              } else {
+                // If this is the last chapter, make sure to only transition
+                // to next route (title details) after the "mark chapter as
+                // read" request is done, so that we don't end up showing the
+                // last chapter as still unread in the title details route.
+                ev.preventDefault();
+                isMarkingLastChapterAsRead = true;
+                m.redraw();
+                markChapterAsRead(site, titleId, chapter.id).finally(() => {
+                  isMarkingLastChapterAsRead = false; // proly unnecessary
+                  m.route.set(nextRoute);
+                });
+              }
+            }
+          },
+        },
+        [
+          m(
+            "span",
+            next
+              ? "next"
+              : isMarkingLastChapterAsRead
+              ? "finishing..."
+              : "finish"
+          ),
+          isMarkingLastChapterAsRead
+            ? null
+            : m("i.icon.icon-" + (next ? "chevrons-right" : "check-circle")),
+        ]
+      ),
+    ]);
+  }
+
   return {
     oninit: (vnode) => {
       document.title = "Manga chapter";
@@ -158,64 +247,10 @@ function Chapter(initialVNode) {
       const { site, titleId } = vnode.attrs;
       const prev = chapter.prev_chapter;
       const next = chapter.next_chapter;
-      const buttons = m("div.chapter--buttons", [
-        prev
-          ? m(
-              m.route.Link,
-              {
-                class: "touch-friendly",
-                href: `/m/${site}/${titleId}/${prev.id}`,
-              },
-              [m("i.icon.icon-chevrons-left"), m("span", "prev")]
-            )
-          : m(Button, {
-              text: "prev",
-              icon: "chevrons-left",
-              disabled: true,
-            }),
-        m(
-          m.route.Link,
-          {
-            class: "touch-friendly",
-            href: `/m/${site}/${titleId}`,
-          },
-          [m("i.icon.icon-list"), m("span", " chapter list")]
-        ),
-        m(
-          m.route.Link,
-          {
-            class: "touch-friendly",
-            href: next
-              ? `/m/${site}/${titleId}/${next.id}`
-              : `/m/${site}/${titleId}`,
-            onclick: (ev) => {
-              if (Auth.isLoggedIn()) {
-                Auth.request({
-                  method: "POST",
-                  url: "/api/read",
-                  body: {
-                    read: [
-                      {
-                        site,
-                        title_id: titleId,
-                        chapter_id: chapter.id,
-                      },
-                    ],
-                  },
-                });
-              }
-              return true;
-            },
-          },
-          [
-            m("span", next ? "next" : "finish"),
-            m("i.icon.icon-" + (next ? "chevrons-right" : "check-circle")),
-          ]
-        ),
-      ]);
+
       return m("div.chapter.content", [
         m("h1", fullChapterName(chapter)),
-        buttons,
+        buttonsView(site, prev, next),
         m(
           "div",
           {
@@ -257,7 +292,7 @@ function Chapter(initialVNode) {
             pendingPages.map(() => m(PendingPlaceholder)),
           ]
         ),
-        buttons,
+        buttonsView(site, prev, next),
         nextChapterLoadedPages.map((page) =>
           m(FallbackableImg, {
             style: { display: "none" },
diff --git a/src/pytaku/static/lookandfeel.css b/src/pytaku/static/lookandfeel.css
index 5435d9f..a904c38 100644
--- a/src/pytaku/static/lookandfeel.css
+++ b/src/pytaku/static/lookandfeel.css
@@ -116,6 +116,9 @@ button.blue {
   background-color: var(--btn-blue);
   border-bottom: 4px solid var(--btn-blue-bottom);
 }
+a.touch-friendly.disabled,
+a.touch-friendly.disabled:hover,
+a.touch-friendly.disabled:active,
 button[disabled],
 button[disabled]:hover,
 button[disabled]:active {