feat: card body is the continue target; drop Continue and Edit

Cover, title, and subtitle become one link to the last-read chapter. The action
row is a sibling of that link, so the destructive button is never also a
navigation target, and Remove now confirms. Manual chapter entry is dropped
entirely — the web UI has a real form, and in the field the existing
'Update to Chapter N' button covers it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-07-28 15:32:15 +07:00
parent 383dbaa7eb
commit d93a94f1aa
+43 -44
View File
@@ -677,19 +677,6 @@
await syncUpsert(bm, "Progress updated"); await syncUpsert(bm, "Progress updated");
} }
// Manual override from the panel input — forces any value.
async function setChapterManual(key, label, num, url) {
const existing = state.byKey[key];
if (!existing) return;
const bm = Object.assign({}, existing, {
last_chapter: label,
last_chapter_num: isNaN(num) ? existing.last_chapter_num : num,
last_chapter_url: url || existing.last_chapter_url,
updated_at: Date.now(),
});
await syncUpsert(bm, "Progress set");
}
async function syncUpsert(bm, okMsg) { async function syncUpsert(bm, okMsg) {
upsertLocal(bm); // optimistic upsertLocal(bm); // optimistic
render(); render();
@@ -1108,40 +1095,47 @@
const sub = behind const sub = behind
? "Read: " + (b.last_chapter || "—") + " · Latest: " + b.latest_chapter ? "Read: " + (b.last_chapter || "—") + " · Latest: " + b.latest_chapter
: (b.last_chapter || "—") + " · " + b.site; : (b.last_chapter || "—") + " · " + b.site;
const row = el("div", { class: "item" }, [ // Cover and text are one link: the card body *is* the continue button, so
b.cover // there is no separate one. The action row is a sibling of that link, never
? el("img", { class: "cover", src: b.cover, loading: "lazy", alt: "" }) // a child — a thumb that misses ★ must not land on a navigation target, and
: el("div", { class: "cover ph" }), // Remove must never be one.
el("div", { class: "meta" }, [ return el("div", { class: "item" }, [
el("div", { class: "t", text: b.title || b.series_id }), el("a", { class: "go", href: cont }, [
el("div", { class: "c" + (behind ? " behind" : ""), text: sub }), b.cover
el("div", { class: "actions" }, [ ? el("img", { class: "cover", src: b.cover, loading: "lazy", alt: "" })
el("button", { : el("div", { class: "cover ph" }),
class: "btn small star" + (b.favorite ? " on" : ""), el("div", { class: "meta" }, [
text: b.favorite ? "★" : "☆", el("div", { class: "t", text: b.title || b.series_id }),
title: b.favorite ? "Remove from favourites" : "Add to favourites", el("div", { class: "c" + (behind ? " behind" : ""), text: sub }),
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) }),
]), ]),
]), ]),
el("div", { class: "actions" }, [
el("button", {
class: "btn small star" + (b.favorite ? " on" : ""),
text: b.favorite ? "★" : "☆",
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("button", {
class: "btn small danger",
text: "Remove",
onclick: () => confirmRemove(b),
}),
]),
]); ]);
return row;
} }
function editChapter(b) { // Removal syncs to every device, so it asks first. confirm() is the page's
const raw = prompt("Set last-read chapter for \"" + (b.title || b.series_id) + "\":", b.last_chapter || ""); // own dialog, not a shadow-root one — modal, and the site cannot style it.
if (raw == null) return; function confirmRemove(b) {
const label = raw.trim(); if (confirm('Remove "' + (b.title || b.series_id) + '" from your bookmarks?')) {
const num = parseFloat((label.match(/[\d.]+/) || [])[0]); removeBookmark(b.key);
setChapterManual(b.key, label, num, b.last_chapter_url); }
} }
// Toast inside the shadow root. // Toast inside the shadow root.
@@ -1325,12 +1319,17 @@
.tab.active { color: #eee; border-bottom-color: #6d28d9; } .tab.active { color: #eee; border-bottom-color: #6d28d9; }
#list { overflow-y: auto; flex: 1; padding: 8px 0; } #list { overflow-y: auto; flex: 1; padding: 8px 0; }
.empty { color: #9a9aa5; text-align: center; padding: 30px 16px; font-size: 14px; } .empty { color: #9a9aa5; text-align: center; padding: 30px 16px; font-size: 14px; }
.item { display: flex; gap: 10px; padding: 10px 16px; border-bottom: 1px solid #2a2a33; } .item {
display: flex; flex-direction: column; gap: 8px;
padding: 10px 16px; border-bottom: 1px solid #2a2a33;
}
.go { display: flex; gap: 10px; text-decoration: none; color: inherit; }
.go:active { opacity: .7; }
.cover { width: 46px; height: 62px; object-fit: cover; border-radius: 4px; flex: none; background: #333; } .cover { width: 46px; height: 62px; object-fit: cover; border-radius: 4px; flex: none; background: #333; }
.cover.ph { display: block; } .cover.ph { display: block; }
.meta { min-width: 0; flex: 1; } .meta { min-width: 0; flex: 1; }
.t { font-weight: 600; font-size: 14px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } .t { font-weight: 600; font-size: 14px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.c { color: #9a9aa5; font-size: 12px; margin: 2px 0 8px; } .c { color: #9a9aa5; font-size: 12px; margin: 2px 0 0; }
.c.behind { color: #c4b5fd; } .c.behind { color: #c4b5fd; }
.actions { display: flex; gap: 6px; flex-wrap: wrap; } .actions { display: flex; gap: 6px; flex-wrap: wrap; }
.btn { .btn {