Repos / pytaku / 9f2b32c335
commit 9f2b32c3358f29b2780fd78c7f9707c219293dda
Author: Bùi Thành Nhân <hi@imnhan.com>
Date: Tue Aug 18 23:02:46 2020 +0700
mithril "follows" route
diff --git a/src/pytaku/main.py b/src/pytaku/main.py
index b73dc0e..0791098 100644
--- a/src/pytaku/main.py
+++ b/src/pytaku/main.py
@@ -435,4 +435,10 @@ def api_logout():
@app.route("/api/follows", methods=["GET"])
@require_token
def api_follows():
- return jsonify({"message": "TODO"})
+ titles = get_followed_titles(request.user_id)
+ for title in titles:
+ thumbnail = title_thumbnail(title["site"], title["id"])
+ if title["site"] == "mangadex":
+ thumbnail = url_for("proxy_view", b64_url=_encode_proxy_url(thumbnail))
+ title["thumbnail"] = thumbnail
+ return jsonify({"titles": titles})
diff --git a/src/pytaku/static/js/routes/follows.js b/src/pytaku/static/js/routes/follows.js
index cd4e18f..06679fc 100644
--- a/src/pytaku/static/js/routes/follows.js
+++ b/src/pytaku/static/js/routes/follows.js
@@ -1,29 +1,112 @@
import { Auth } from "../models.js";
+const truncate = (input) =>
+ input.length > 20 ? `${input.substring(0, 20)}...` : input;
+
+function fullChapterName(chapter) {
+ let result = "Chapter " + chapter.num_major;
+ if (chapter.num_minor) {
+ result += "." + chapter.num_minor;
+ }
+ if (chapter.volume) {
+ result += " Volume " + chapter.volume;
+ }
+ if (chapter.name) {
+ result += " - " + chapter.name;
+ }
+ return result;
+}
+
+const Title = {
+ view: (vnode) => {
+ const title = vnode.attrs.title;
+ const numChaptersToDisplay = 4;
+
+ return m(
+ "div.follows--title" + (title.chapters.length === 0 ? ".empty" : ""),
+ [
+ m("div", [
+ m(m.route.Link, { href: `/m/${title.site}/${title.id}` }, [
+ m("img.follows--cover", { src: title.thumbnail, alt: title.name }),
+ ]),
+ ]),
+ m("div.follows--chapters", [
+ title.chapters.length > numChaptersToDisplay
+ ? m(
+ m.route.Link,
+ {
+ href: `/m/${title.site}/${title.id}`,
+ class: "follows--chapter follows--more",
+ },
+ `and ${title.chapters.length - numChaptersToDisplay} more...`
+ )
+ : "",
+ title.chapters.slice(-numChaptersToDisplay).map((chapter) =>
+ m(
+ m.route.Link,
+ {
+ href: `/m/${title.site}/${title.id}/${chapter.id}`,
+ class: "follows--chapter",
+ },
+ [
+ fullChapterName(chapter),
+ chapter.groups.map((group) => {
+ m("span.follows--group", truncate(group));
+ }),
+ ]
+ )
+ ),
+ ]),
+ ]
+ );
+ },
+};
+
function Follows(initialVNode) {
let titles = [];
+ let isLoading = false;
+
return {
oninit: () => {
+ isLoading = true;
Auth.request({
method: "GET",
url: "/api/follows",
- });
+ })
+ .then((resp) => {
+ titles = resp.titles;
+ })
+ .catch((err) => {
+ alert("TODO");
+ console.log(err);
+ })
+ .finally(() => {
+ isLoading = false;
+ });
},
+
oncreate: (vnode) => {
document.title = "Stuff I follow - Pytaku";
},
+
view: (vnode) => {
- return m("div.content", [
- titles.map((title) =>
- m("div.title", [
- m("div", [
- m("a", { href: "TODO" }, [
- m("img.cover", { src: title.thumbnail, alt: title.name }),
- ]),
- ]),
- ])
- ),
- ]);
+ let content = "";
+
+ if (isLoading) {
+ return m(
+ "div.content",
+ m("h2.blink", [m("i.icon.icon-loader.spin"), " loading..."])
+ );
+ }
+
+ if (titles.length === 0) {
+ return m(
+ "div.content",
+ "You're not following any title yet. Try searching for some."
+ );
+ }
+
+ return m("div.content", [titles.map((title) => m(Title, { title }))]);
},
};
}
diff --git a/src/pytaku/static/spa.css b/src/pytaku/static/spa.css
index ec92e72..4d9eec0 100644
--- a/src/pytaku/static/spa.css
+++ b/src/pytaku/static/spa.css
@@ -1,3 +1,41 @@
+/* --- Utilities --- */
+
+/* spinner */
+.spin {
+ display: inline-block;
+ animation-name: spin;
+ animation-duration: 1s;
+ animation-iteration-count: infinite;
+ animation-timing-function: linear;
+}
+@keyframes spin {
+ from {
+ transform: rotate(0deg);
+ }
+ to {
+ transform: rotate(360deg);
+ }
+}
+
+/* blinker */
+.blink {
+ display: inline-block;
+ animation-name: blink;
+ animation-duration: 700ms;
+ animation-iteration-count: infinite;
+ animation-timing-function: linear;
+}
+@keyframes blink {
+ from {
+ opacity: 0.2;
+ }
+ to {
+ opacity: 1;
+ }
+}
+
+/* --- End Utilities --- */
+
/* Navbar */
nav {
background-color: var(--bg-black);
@@ -121,3 +159,64 @@ .auth--form--message-success {
.auth--form--message-error {
color: red;
}
+
+/* Follows route */
+
+.follows--title {
+ display: flex;
+ flex-direction: row;
+ flex-wrap: wrap;
+ margin-bottom: var(--body-padding);
+ background-color: #efefef;
+}
+.follows--title.empty {
+ display: inline-flex;
+}
+.follows--title.empty .chapters {
+ display: none;
+}
+
+.follows--cover {
+ border: 1px solid #777;
+ margin-right: 0.5rem;
+ max-width: 150px;
+}
+.follows--cover:hover {
+ box-shadow: 0 0 3px black;
+}
+
+.follows--chapters {
+ padding: 0.5rem 0.5rem 0.5rem 0;
+}
+
+.follows--chapter {
+ display: block;
+ margin-bottom: 0.5rem;
+ background-color: white;
+ border: 1px solid #999;
+ padding: 6px;
+ border-radius: 5px;
+ text-decoration: none;
+ color: black;
+}
+.follows--chapter:hover {
+ background-color: #eee;
+}
+.follows--chapter:last-child::after {
+ content: "← resume here";
+ background-color: cornsilk;
+ white-space: nowrap;
+}
+
+.follows--group {
+ font-size: 0.9em;
+ background-color: #ddd;
+ border-radius: 3px;
+ white-space: nowrap;
+ padding: 2px 5px;
+}
+
+.follows--more {
+ display: inline-block;
+ font-style: italic;
+}