Repos / pytaku / 3ec0908268
commit 3ec090826828e14a3d4004dfb2c5b764910b632e
Author: Bùi Thành Nhân <hi@imnhan.com>
Date:   Sun Aug 23 16:58:01 2020 +0700

    implement chapter route

diff --git a/src/pytaku/main.py b/src/pytaku/main.py
index 5ebcdfe..c155e13 100644
--- a/src/pytaku/main.py
+++ b/src/pytaku/main.py
@@ -19,7 +19,7 @@
 )
 
 from .conf import config
-from .decorators import process_token, require_login, toggle_has_read
+from .decorators import process_token, require_login
 from .persistence import (
     create_token,
     delete_token,
@@ -55,6 +55,20 @@
 )
 
 
+def _chapter_name(chapter: dict):
+    result = ""
+    if chapter.get("num_major") is not None:
+        result += "Ch. " if chapter.get("volume") else "Chapter "
+        result += str(chapter["num_major"])
+    if chapter.get("num_minor"):
+        result += '.{chapter["num_minor"]}'
+    if chapter.get("volume"):
+        result += f"Vol. {chapter['volume']}"
+    if chapter.get("name"):
+        result += f" - {chapter['name']}"
+    return result
+
+
 @app.route("/following", methods=["GET"])
 @require_login
 def follows_view():
@@ -71,14 +85,14 @@ def follows_view():
 @require_login
 def follow_view(site, title_id):
     follow(session["user"]["id"], site, title_id)
-    return redirect(url_for("title_view", site=site, title_id=title_id))
+    return redirect(url_for("spa_title_view", site=site, title_id=title_id))
 
 
 @app.route("/unfollow/<site>/<title_id>", methods=["POST"])
 @require_login
 def unfollow_view(site, title_id):
     unfollow(session["user"]["id"], site, title_id)
-    return redirect(url_for("title_view", site=site, title_id=title_id))
+    return redirect(url_for("spa_title_view", site=site, title_id=title_id))
 
 
 @app.route("/logout", methods=["POST"])
@@ -170,38 +184,6 @@ def auth_view():
     return render_template("old/auth.html")
 
 
-@app.route("/m/<site>/<title_id>/<chapter_id>")
-@toggle_has_read
-def chapter_view(site, title_id, chapter_id):
-    chapter = load_chapter(site, title_id, chapter_id)
-    if not chapter:
-        print("Getting chapter", chapter_id)
-        chapter = get_chapter(site, title_id, chapter_id)
-        save_chapter(chapter)
-    else:
-        print("Loading chapter", chapter_id, "from db")
-
-    if site in ("mangadex", "mangasee"):
-        chapter["pages"] = [
-            url_for("proxy_view", b64_url=_encode_proxy_url(p))
-            for p in chapter["pages"]
-        ]
-
-    # YIIIIKES
-    title = load_title(site, title_id)
-    title["cover"] = title_cover(site, title_id, title["cover_ext"])
-    if site == "mangadex":
-        title["cover"] = url_for(
-            "proxy_view", b64_url=_encode_proxy_url(title["cover"])
-        )
-    prev_chapter, next_chapter = get_prev_next_chapters(title, chapter)
-    chapter["prev_chapter"] = prev_chapter
-    chapter["next_chapter"] = next_chapter
-
-    chapter["site"] = site
-    return render_template("old/chapter.html", title=title, **chapter)
-
-
 @app.route("/search")
 def search_view():
     query = request.args.get("q", "").strip()
@@ -381,6 +363,35 @@ def spa_title_view(site, title_id):
     )
 
 
+@app.route("/m/<site>/<title_id>/<chapter_id>")
+def spa_chapter_view(site, title_id, chapter_id):
+    chapter = load_chapter(site, title_id, chapter_id)
+    if not chapter:
+        print("Getting chapter", chapter_id)
+        chapter = get_chapter(site, title_id, chapter_id)
+        save_chapter(chapter)
+    else:
+        print("Loading chapter", chapter_id, "from db")
+
+    # YIIIIKES
+    title = load_title(site, title_id)
+    title["cover"] = title_cover(site, title_id, title["cover_ext"])
+    if site == "mangadex":
+        title["cover"] = url_for(
+            "proxy_view", b64_url=_encode_proxy_url(title["cover"])
+        )
+
+    chapter["site"] = site
+    return render_template(
+        "spa.html",
+        open_graph={
+            "title": f'{_chapter_name(chapter)} - {title["name"]}',
+            "image": title["cover"],
+            "description": "\n".join(title["descriptions"]),
+        },
+    )
+
+
 @app.route("/api/title/<site>/<title_id>", methods=["GET"])
 @process_token(required=False)
 def api_title(site, title_id):
@@ -388,6 +399,37 @@ def api_title(site, title_id):
     return title
 
 
+@app.route("/api/chapter/<site>/<title_id>/<chapter_id>", methods=["GET"])
+@process_token(required=False)
+def api_chapter(site, title_id, chapter_id):
+    chapter = load_chapter(site, title_id, chapter_id)
+    if not chapter:
+        print("Getting chapter", chapter_id)
+        chapter = get_chapter(site, title_id, chapter_id)
+        save_chapter(chapter)
+    else:
+        print("Loading chapter", chapter_id, "from db")
+
+    if site in ("mangadex", "mangasee"):
+        chapter["pages"] = [
+            url_for("proxy_view", b64_url=_encode_proxy_url(p))
+            for p in chapter["pages"]
+        ]
+
+    # YIIIIKES
+    title = load_title(site, title_id)
+    title["cover"] = title_cover(site, title_id, title["cover_ext"])
+    if site == "mangadex":
+        title["cover"] = url_for(
+            "proxy_view", b64_url=_encode_proxy_url(title["cover"])
+        )
+    prev_chapter, next_chapter = get_prev_next_chapters(title, chapter)
+    chapter["prev_chapter"] = prev_chapter
+    chapter["next_chapter"] = next_chapter
+    chapter["site"] = site
+    return chapter
+
+
 @app.route("/api/register", methods=["POST"])
 def api_register():
     username = request.json["username"].strip()
diff --git a/src/pytaku/static/js/layout.js b/src/pytaku/static/js/layout.js
index cbd56db..7a566e5 100644
--- a/src/pytaku/static/js/layout.js
+++ b/src/pytaku/static/js/layout.js
@@ -72,7 +72,7 @@ function Navbar(initialVNode) {
 
 const Layout = {
   view: (vnode) => {
-    return m("div.main", [m(Navbar), vnode.children]);
+    return [m(Navbar), vnode.children];
   },
 };
 
diff --git a/src/pytaku/static/js/main.js b/src/pytaku/static/js/main.js
index 68e84ef..427f052 100644
--- a/src/pytaku/static/js/main.js
+++ b/src/pytaku/static/js/main.js
@@ -5,6 +5,7 @@ import Home from "./routes/home.js";
 import Follows from "./routes/follows.js";
 import Search from "./routes/search.js";
 import Title from "./routes/title.js";
+import Chapter from "./routes/chapter.js";
 
 Auth.init().then(() => {
   const root = document.getElementById("spa-root");
@@ -63,5 +64,17 @@ Auth.init().then(() => {
           })
         ),
     },
+    "/m/:site/:titleId/:chapterId": {
+      render: (vnode) =>
+        m(
+          Layout,
+          m(Chapter, {
+            site: vnode.attrs.site,
+            titleId: vnode.attrs.titleId,
+            chapterId: vnode.attrs.chapterId,
+            key: vnode.attrs.chapterId,
+          })
+        ),
+    },
   });
 });
diff --git a/src/pytaku/static/js/routes/chapter.js b/src/pytaku/static/js/routes/chapter.js
new file mode 100644
index 0000000..b48fbcd
--- /dev/null
+++ b/src/pytaku/static/js/routes/chapter.js
@@ -0,0 +1,95 @@
+import { Auth } from "../models.js";
+import { LoadingMessage, fullChapterName, Button } from "../utils.js";
+
+function Chapter(initialVNode) {
+  let isLoading = false;
+  let chapter = {};
+
+  return {
+    oninit: (vnode) => {
+      document.title = "Manga chapter";
+
+      isLoading = true;
+      m.redraw();
+
+      Auth.request({
+        method: "GET",
+        url: "/api/chapter/:site/:titleId/:chapterId",
+        params: {
+          site: vnode.attrs.site,
+          titleId: vnode.attrs.titleId,
+          chapterId: vnode.attrs.chapterId,
+        },
+      })
+        .then((resp) => {
+          chapter = resp;
+          document.title = fullChapterName(chapter);
+        })
+        .finally(() => {
+          isLoading = false;
+        });
+    },
+    view: (vnode) => {
+      if (isLoading) {
+        return m("div.chapter.content", m(LoadingMessage));
+      }
+
+      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")]
+        ),
+        next
+          ? m(
+              m.route.Link,
+              {
+                class: "touch-friendly",
+                href: `/m/${site}/${titleId}/${next.id}`,
+              },
+              [m("span", "next"), m("i.icon.icon-chevrons-right")]
+            )
+          : m(Button, {
+              text: "next",
+              icon: "chevrons-right",
+              disabled: true,
+            }),
+      ]);
+      return m("div.chapter.content", [
+        m("h1", fullChapterName(chapter)),
+        buttons,
+        m(
+          "div",
+          {
+            class:
+              "chapter--pages" + chapter.is_webtoon ? " chapter--webtoon" : "",
+          },
+          [chapter.pages.map((page) => m("img", { src: page }))]
+        ),
+        buttons,
+      ]);
+    },
+  };
+}
+
+export default Chapter;
diff --git a/src/pytaku/static/lookandfeel.css b/src/pytaku/static/lookandfeel.css
index 018294e..b874b45 100644
--- a/src/pytaku/static/lookandfeel.css
+++ b/src/pytaku/static/lookandfeel.css
@@ -28,6 +28,7 @@ h4 {
 
 h1 {
   font-size: 2rem;
+  line-height: 2rem;
 }
 
 h2 {
@@ -60,7 +61,6 @@ p {
 
 button,
 a.touch-friendly {
-  display: inline-block;
   user-select: none;
   margin: 0;
   display: inline-flex;
@@ -77,10 +77,12 @@ button {
   background-color: var(--btn-gray);
   border-bottom: 4px solid var(--btn-gray-bottom);
 }
-button > i {
+button > *,
+a.touch-friendly > * {
   margin-right: 0.3rem;
 }
-button > i:last-child {
+button > *:last-child,
+a.touch-friendly > *:last-child {
   margin-right: 0;
 }
 button:hover {
@@ -115,7 +117,7 @@ button[disabled]:active {
 }
 a.touch-friendly {
   background-color: white;
-  color: inherit;
+  color: black;
   border: 1px solid #aaa;
   text-decoration: none;
   border-bottom: 4px solid grey;
diff --git a/src/pytaku/static/spa.css b/src/pytaku/static/spa.css
index 6fbb584..f772a66 100644
--- a/src/pytaku/static/spa.css
+++ b/src/pytaku/static/spa.css
@@ -94,8 +94,13 @@ .nav--link i {
 }
 
 /* Route content common styling */
+#spa-root {
+  display: flex;
+  flex-direction: column;
+}
 .content {
   padding: var(--body-padding);
+  flex-grow: 1;
 }
 .content > * {
   display: block;
@@ -279,6 +284,38 @@ .title--details {
   margin: 1rem 0;
 }
 
+/* Chapter route */
+.chapter.content {
+  padding: var(--body-padding) 0;
+  text-align: center;
+  background-color: #444;
+  color: white;
+}
+
+.chapter--pages img {
+  display: block;
+  margin: 0 auto 0.7rem auto;
+}
+.chapter--pages.chapter--webtoon img {
+  margin: 0 auto;
+}
+
+.chapter--buttons {
+  display: flex;
+  flex-direction: row;
+  justify-content: center;
+}
+.chapter--buttons > a,
+.chapter--buttons > button {
+  margin-right: 0.5rem;
+  margin-top: 0.2rem;
+  margin-bottom: 0.2rem;
+}
+.chapter--buttons > a:last-child,
+.chapter--buttons > button:last-child {
+  margin-right: 0;
+}
+
 /* Components defined in utils */
 .utils--chapter {
   margin-bottom: 0.5rem;