feat(userscript): archived tab, archive button, and site nav chips
This commit is contained in:
@@ -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 @@
|
||||
<span>Manga Bookmarks</span>
|
||||
<button id="closeBtn" aria-label="Close">✕</button>
|
||||
</header>
|
||||
<div id="nav">
|
||||
<a class="chip" href="${WEB_BASE}" target="_blank" rel="noopener">🌐 Web</a>
|
||||
<a class="chip" href="https://asurascans.com" target="_blank" rel="noopener">Asura</a>
|
||||
<a class="chip" href="https://demonicscans.org" target="_blank" rel="noopener">Demonic</a>
|
||||
</div>
|
||||
<section id="context"></section>
|
||||
<div id="tabs" role="tablist">
|
||||
<button id="tabAll" class="tab active" role="tab">All</button>
|
||||
<button id="tabFav" class="tab" role="tab">★ Favourites</button>
|
||||
<button id="tabArc" class="tab" role="tab">Archived</button>
|
||||
</div>
|
||||
<div id="list"></div>
|
||||
</aside>
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user