diff --git a/userscript/manga-bookmark.user.js b/userscript/manga-bookmark.user.js index a50c6f6..791799b 100644 --- a/userscript/manga-bookmark.user.js +++ b/userscript/manga-bookmark.user.js @@ -1,7 +1,7 @@ // ==UserScript== // @name Manga Bookmark Sync // @namespace mangabm -// @version 1.2.0 +// @version 1.3.0 // @description Track read progress on Asura & Demonic and sync to a self-hosted backend. Bromite-compatible (no GM_* APIs). // @author you // @match https://asuracomic.net/* @@ -19,6 +19,7 @@ // ============================================================ const API_BASE = "https://manga-api.violetcrown.my.id"; // your backend origin, no trailing slash const API_TOKEN = "40d79969b5442f90df4fe306a092c7c50e7b4a7a98099f98cc398f4fb374b1df"; // must equal backend API_TOKEN + const WEB_BASE = "https://manga.violetcrown.my.id"; // the browser UI, for the panel's nav chips // Safe in Bromite's isolated world: the page's own JS cannot read these. const CACHE_KEY = "mangabm:cache"; @@ -325,6 +326,12 @@ saveCache(state.list); } + // A list cached by an older version has no status field, and a row the + // server defaulted has "reading" — both mean the same thing here. + function statusOf(b) { + return b.status || "reading"; + } + // ============================================================ // Mutations (optimistic: update UI/cache first, then sync) // ============================================================ @@ -415,6 +422,26 @@ } } + // Archive parks a series: it leaves All and Favourites but the server keeps + // polling it for new chapters. "finished" is deliberately not reachable from + // here — the API rejects that value, it is a web-UI decision. + async function toggleArchive(key) { + const existing = state.byKey[key]; + if (!existing) return; + const next = statusOf(existing) === "archived" ? "reading" : "archived"; + const bm = Object.assign({}, existing, { status: next }); + upsertLocal(bm); + render(); + try { + const saved = await apiPut(bm.key, bm); + upsertLocal(saved); // adopt the stored row: the server owns updated_at + render(); + toast(next === "archived" ? "Archived" : "Back in your list"); + } catch (e) { + toast("Archive failed — retry when online", true); + } + } + // Records the newest chapter a site has published. Silent: this fires from // page visits and background checks the user did not ask for, and it never // reorders the list — updated_at is a candidate the server discards unless @@ -551,7 +578,7 @@ // ============================================================ let root, panelOpen = false; - let activeTab = "all"; // "all" | "favorites"; resets each page, by design + let activeTab = "all"; // "all" | "favorites" | "archived"; resets each page, by design function buildUI() { const host = document.createElement("div"); @@ -580,7 +607,7 @@ root.getElementById("backdrop").addEventListener("click", togglePanel); root.getElementById("closeBtn").addEventListener("click", togglePanel); - for (const [id, tab] of [["tabAll", "all"], ["tabFav", "favorites"]]) { + for (const [id, tab] of [["tabAll", "all"], ["tabFav", "favorites"], ["tabArc", "archived"]]) { root.getElementById(id).addEventListener("click", () => { activeTab = tab; render(); @@ -752,22 +779,28 @@ ctx.appendChild(el("div", { class: "ctx-sub", text: "Open a series or chapter to bookmark it." })); } - // Bookmark list. Favorites only narrows what is drawn — state.list keeps - // every bookmark, so favouriting one never removes it from "All". - const favorites = activeTab === "favorites"; - root.getElementById("tabAll").classList.toggle("active", !favorites); - root.getElementById("tabFav").classList.toggle("active", favorites); + // Tabs narrow what is drawn; state.list always holds every bookmark. + // Archived rows are hidden from All and Favourites, and finished ones — + // which only the web UI can set — are hidden from every tab. + for (const [id, tab] of [["tabAll", "all"], ["tabFav", "favorites"], ["tabArc", "archived"]]) { + root.getElementById(id).classList.toggle("active", activeTab === tab); + } const listEl = root.getElementById("list"); listEl.innerHTML = ""; - const items = favorites ? state.list.filter((b) => b.favorite) : state.list; + const items = state.list.filter((b) => { + const s = statusOf(b); + if (activeTab === "archived") return s === "archived"; + if (s !== "reading") return false; + return activeTab !== "favorites" || b.favorite; + }); if (items.length === 0) { - listEl.appendChild( - el("div", { - class: "empty", - text: favorites ? "No favourites yet." : "No bookmarks yet.", - }) - ); + const empty = { + favorites: "No favourites yet.", + archived: "Nothing archived.", + all: "No bookmarks yet.", + }[activeTab]; + listEl.appendChild(el("div", { class: "empty", text: empty })); } for (const b of items) { listEl.appendChild(renderItem(b)); @@ -798,6 +831,11 @@ title: b.favorite ? "Remove from favourites" : "Add to favourites", onclick: () => toggleFavorite(b.key), }), + el("button", { + class: "btn small", + text: statusOf(b) === "archived" ? "Unarchive" : "Archive", + onclick: () => toggleArchive(b.key), + }), el("a", { class: "btn small", href: cont, text: "Continue" }), el("button", { class: "btn small", text: "Edit", onclick: () => editChapter(b) }), el("button", { class: "btn small danger", text: "Remove", onclick: () => removeBookmark(b.key) }), @@ -909,10 +947,16 @@ Manga Bookmarks +
+
@@ -960,6 +1004,13 @@ padding: 14px 16px; border-bottom: 1px solid #33333d; font-weight: 600; font-size: 16px; } #closeBtn { background: none; border: none; color: #aaa; font-size: 18px; cursor: pointer; } + #nav { display: flex; gap: 8px; padding: 10px 16px; border-bottom: 1px solid #33333d; } + .chip { + background: #2a2a33; color: #c4b5fd; text-decoration: none; + padding: 6px 11px; border-radius: 999px; font-size: 12px; font-weight: 600; + white-space: nowrap; + } + .chip:active { opacity: .8; } #context { padding: 12px 16px; border-bottom: 1px solid #33333d; display: flex; flex-direction: column; gap: 8px;