M src/pytaku/js-src/main.js => src/pytaku/js-src/main.js +2 -1
@@ 6,6 6,7 @@ import Follows from "./routes/follows.js";
import Search from "./routes/search.js";
import Title from "./routes/title.js";
import Chapter from "./routes/chapter.js";
+import EInkChapter from "./routes/eink-chapter.js";
import Importer from "./routes/importer.js";
Auth.init().then(() => {
@@ 69,7 70,7 @@ Auth.init().then(() => {
render: (vnode) =>
m(
Layout,
- m(Chapter, {
+ m(EInkChapter, {
site: vnode.attrs.site,
titleId: vnode.attrs.titleId,
chapterId: vnode.attrs.chapterId,
M src/pytaku/js-src/routes/chapter.js => src/pytaku/js-src/routes/chapter.js +8 -50
@@ 1,5 1,12 @@
import { Auth, ChapterModel } from "../models.js";
-import { LoadingMessage, fullChapterName, Button } from "../utils.js";
+import {
+ LoadingMessage,
+ FallbackableImg,
+ fullChapterName,
+ Button,
+ RetryImgButton,
+ ImgStatus,
+} from "../utils.js";
const KEYCODE_PLUS = 43;
const KEYCODE_MINUS = 45;
@@ 14,55 21,6 @@ const PendingPlaceholder = {
view: () => m("h2", [m("i.icon.icon-loader")]),
};
-const RetryImgButton = {
- view: (vnode) => {
- return m(Button, {
- text: "Errored. Try again?",
- color: "red",
- onclick: (ev) => {
- const { page } = vnode.attrs;
- page.status = ImgStatus.LOADING;
- // Cheat: append to src so the element's key is
- // different, forcing mithril to redraw.
- // Chose `?` here because it will just be stripped by
- // flask's path parser.
- page.src = page.src.endsWith("?")
- ? page.src.slice(0, -1)
- : page.src + "?";
- },
- });
- },
-};
-
-const ImgStatus = {
- LOADING: "loading",
- SUCCEEDED: "succeeded",
- FAILED: "failed",
-};
-
-function FallbackableImg(initialVNode) {
- let currentSrc;
- return {
- oninit: (vnode) => {
- currentSrc = vnode.attrs.src;
- },
- view: (vnode) => {
- return m("img", {
- src: currentSrc,
- style: vnode.attrs.style,
- onload: vnode.attrs.onload,
- onerror: (ev) => {
- if (currentSrc === vnode.attrs.src && vnode.attrs.altsrc !== null) {
- currentSrc = vnode.attrs.altsrc;
- } else {
- vnode.attrs.onerror(ev);
- }
- },
- });
- },
- };
-}
-
function Chapter(initialVNode) {
let isLoading = false;
let isMarkingLastChapterAsRead = false;
A src/pytaku/js-src/routes/eink-chapter.js => src/pytaku/js-src/routes/eink-chapter.js +176 -0
@@ 0,0 1,176 @@
+import { Auth, ChapterModel } from "../models.js";
+import {
+ fullChapterName,
+ Button,
+ FallbackableImg,
+ RetryImgButton,
+ ImgStatus,
+} from "../utils.js";
+
+const LoadingPlaceholder = {
+ view: () => m("h2", [m("i.icon.icon-loader")]),
+};
+
+function EInkChapter(initialVNode) {
+ let isLoading = false;
+ let isMarkingLastChapterAsRead = false;
+ let chapter = {};
+ let pendingPages = [];
+ let loadedPages = [];
+ let site, titleId; // these are written on init
+ let currentPageIndex = 0;
+
+ let eInkRefreshTimeoutId = null;
+ let isInverted = false;
+
+ function loadNextPage() {
+ if (pendingPages.length > 0) {
+ let [src, altsrc] = pendingPages.splice(0, 1)[0];
+ loadedPages.push({
+ status: ImgStatus.LOADING,
+ src,
+ altsrc,
+ });
+ }
+ }
+
+ return {
+ oninit: (vnode) => {
+ document.title = "Manga chapter";
+ site = vnode.attrs.site;
+ titleId = vnode.attrs.titleId;
+
+ isLoading = true;
+ m.redraw();
+
+ ChapterModel.get({
+ site: vnode.attrs.site,
+ titleId: vnode.attrs.titleId,
+ chapterId: vnode.attrs.chapterId,
+ })
+ .then((resp) => {
+ chapter = resp;
+ document.title = fullChapterName(chapter);
+
+ // "zip" pages & pages_alt into pendingPages
+ if (chapter.pages_alt.length > 0) {
+ pendingPages = chapter.pages.map((page, i) => {
+ return [page, chapter.pages_alt[i]];
+ });
+ } else {
+ pendingPages = chapter.pages.map((page) => {
+ return [page, null];
+ });
+ }
+
+ loadNextPage();
+ loadNextPage();
+ loadNextPage();
+ })
+ .finally(() => {
+ isLoading = false;
+ });
+ },
+
+ view: (vnode) => {
+ if (isLoading) {
+ return m(
+ "div.chapter.content",
+ m("h2.blink", [m("i.icon.icon-loader.spin"), " loading..."])
+ );
+ }
+
+ const { site, titleId } = vnode.attrs;
+ const prev = chapter.prev_chapter;
+ const next = chapter.next_chapter;
+
+ return m(
+ ".e-ink-chapter",
+ {
+ style: {
+ position: "absolute",
+ backgroundColor: "white",
+ top: 0,
+ left: 0,
+ right: 0,
+ bottom: 0,
+ zIndex: 1,
+ textAlign: "center",
+ },
+ },
+ [
+ loadedPages.map((page, pageIndex) =>
+ m("div", { key: page.src }, [
+ m(FallbackableImg, {
+ src: page.src,
+ altsrc: page.altsrc,
+ style: {
+ display:
+ page.status === ImgStatus.SUCCEEDED &&
+ pageIndex === currentPageIndex
+ ? "block"
+ : "none",
+ filter: isInverted ? "invert(1)" : null,
+ position: "absolute",
+ top: 0,
+ left: 0,
+ right: 0,
+ margin: "auto",
+ maxHeight: "100%",
+ zIndex: 2,
+ },
+ onload: (ev) => {
+ page.status = ImgStatus.SUCCEEDED;
+ loadNextPage();
+ },
+ onerror: (ev) => {
+ page.status = ImgStatus.FAILED;
+ loadNextPage();
+ },
+ }),
+ page.status === ImgStatus.LOADING &&
+ pageIndex === currentPageIndex
+ ? m(LoadingPlaceholder)
+ : null,
+ page.status === ImgStatus.FAILED && pageIndex === currentPageIndex
+ ? m(
+ "div",
+ { style: { "margin-bottom": ".5rem" } },
+ m(RetryImgButton, { page })
+ )
+ : null,
+ ])
+ ),
+ m(".e-ink-chapter--prev-page", {
+ onclick: (ev) => {
+ if (currentPageIndex > 0) currentPageIndex--;
+ if (eInkRefreshTimeoutId !== null) {
+ clearTimeout(eInkRefreshTimeoutId);
+ }
+ isInverted = true;
+ eInkRefreshTimeoutId = setTimeout(() => {
+ isInverted = false;
+ m.redraw();
+ }, 450);
+ },
+ }),
+ m(".e-ink-chapter--next-page", {
+ onclick: (ev) => {
+ if (currentPageIndex < loadedPages.length - 1) currentPageIndex++;
+ if (eInkRefreshTimeoutId !== null) {
+ clearTimeout(eInkRefreshTimeoutId);
+ }
+ isInverted = true;
+ eInkRefreshTimeoutId = setTimeout(() => {
+ isInverted = false;
+ m.redraw();
+ }, 450);
+ },
+ }),
+ ]
+ );
+ },
+ };
+}
+
+export default EInkChapter;
M src/pytaku/js-src/utils.js => src/pytaku/js-src/utils.js +59 -1
@@ 57,4 57,62 @@ function fullChapterName(chapter) {
return result;
}
-export { LoadingMessage, Button, Chapter, truncate, fullChapterName };
+function FallbackableImg(initialVNode) {
+ let currentSrc;
+ return {
+ oninit: (vnode) => {
+ currentSrc = vnode.attrs.src;
+ },
+ view: (vnode) => {
+ return m("img", {
+ src: currentSrc,
+ style: vnode.attrs.style,
+ onload: vnode.attrs.onload,
+ onerror: (ev) => {
+ if (currentSrc === vnode.attrs.src && vnode.attrs.altsrc !== null) {
+ currentSrc = vnode.attrs.altsrc;
+ } else {
+ vnode.attrs.onerror(ev);
+ }
+ },
+ });
+ },
+ };
+}
+
+const RetryImgButton = {
+ view: (vnode) => {
+ return m(Button, {
+ text: "Errored. Try again?",
+ color: "red",
+ onclick: (ev) => {
+ const { page } = vnode.attrs;
+ page.status = ImgStatus.LOADING;
+ // Cheat: append to src so the element's key is
+ // different, forcing mithril to redraw.
+ // Chose `?` here because it will just be stripped by
+ // flask's path parser.
+ page.src = page.src.endsWith("?")
+ ? page.src.slice(0, -1)
+ : page.src + "?";
+ },
+ });
+ },
+};
+
+const ImgStatus = {
+ LOADING: "loading",
+ SUCCEEDED: "succeeded",
+ FAILED: "failed",
+};
+
+export {
+ LoadingMessage,
+ Button,
+ Chapter,
+ truncate,
+ fullChapterName,
+ FallbackableImg,
+ RetryImgButton,
+ ImgStatus,
+};
M src/pytaku/static/spa.css => src/pytaku/static/spa.css +18 -0
@@ 434,6 434,24 @@ footer {
}
}
+/* E-ink Chapter route */
+
+.e-ink-chapter--prev-page,
+.e-ink-chapter--next-page {
+ position: fixed;
+ z-index: 3;
+ top: 0;
+ bottom: 0;
+}
+.e-ink-chapter--prev-page {
+ left: 0;
+ right: 50%;
+}
+.e-ink-chapter--next-page {
+ right: 0;
+ left: 50%;
+}
+
/* Components defined in utils */
.utils--chapter {
margin-bottom: 0.5rem;