Feat: Library Manga Updates Display

This commit is contained in:
Youwes09
2026-04-11 19:17:44 -05:00
parent af29cffdff
commit de397f2462
3 changed files with 29 additions and 16 deletions
+7 -3
View File
@@ -858,12 +858,12 @@
class="icon-btn refresh-btn" class="icon-btn refresh-btn"
class:icon-btn-active={refreshing} class:icon-btn-active={refreshing}
class:refresh-btn-done={refreshDone} class:refresh-btn-done={refreshDone}
title={refreshing ? `Checking… ${refreshProgress.finished}/${refreshProgress.total}` : refreshDone ? "Library updated" : "Check for updates"} title={refreshing ? (refreshProgress.finished > 0 ? `Checking… ${refreshProgress.finished}/${refreshProgress.total}` : "Checking…") : refreshDone ? "Library updated" : "Check for updates"}
disabled={refreshing} disabled={refreshing}
onclick={startLibraryRefresh} onclick={startLibraryRefresh}
> >
<ArrowsClockwise size={15} weight="bold" class={refreshing ? "anim-spin" : ""} /> <ArrowsClockwise size={15} weight="bold" class={refreshing ? "anim-spin" : ""} />
{#if refreshing && refreshProgress.total > 0} {#if refreshing && refreshProgress.finished > 0}
<span class="refresh-progress">{refreshProgress.finished}/{refreshProgress.total}</span> <span class="refresh-progress">{refreshProgress.finished}/{refreshProgress.total}</span>
{/if} {/if}
</button> </button>
@@ -971,7 +971,7 @@
</div> </div>
<!-- ── Refresh progress bar ──────────────────────────────────────────────── --> <!-- ── Refresh progress bar ──────────────────────────────────────────────── -->
{#if refreshing && refreshProgress.total > 0} {#if refreshing && refreshProgress.finished > 0}
{@const pct = Math.round((refreshProgress.finished / refreshProgress.total) * 100)} {@const pct = Math.round((refreshProgress.finished / refreshProgress.total) * 100)}
<div class="refresh-bar-wrap" aria-hidden="true"> <div class="refresh-bar-wrap" aria-hidden="true">
<div class="refresh-bar-fill" style="width:{pct}%"></div> <div class="refresh-bar-fill" style="width:{pct}%"></div>
@@ -1059,6 +1059,9 @@
<Thumbnail src={m.thumbnailUrl} alt={m.title} class="cover" style="object-fit:{store.settings.libraryCropCovers ? 'cover' : 'contain'}" draggable="false" /> <Thumbnail src={m.thumbnailUrl} alt={m.title} class="cover" style="object-fit:{store.settings.libraryCropCovers ? 'cover' : 'contain'}" draggable="false" />
{#if m.downloadCount}<span class="badge-dl">{m.downloadCount}</span>{/if} {#if m.downloadCount}<span class="badge-dl">{m.downloadCount}</span>{/if}
{#if m.unreadCount}<span class="badge-unread">{m.unreadCount}</span>{/if} {#if m.unreadCount}<span class="badge-unread">{m.unreadCount}</span>{/if}
{#if store.libraryUpdates.some(u => u.mangaId === m.id) && !store.acknowledgedUpdates.has(m.id)}
<span class="badge-new" aria-label="New chapters"></span>
{/if}
{#if selectMode} {#if selectMode}
<div class="select-overlay" aria-hidden="true"> <div class="select-overlay" aria-hidden="true">
<div class="select-check" class:checked={isSelected}> <div class="select-check" class:checked={isSelected}>
@@ -1186,6 +1189,7 @@
.cover { width: 100%; height: 100%; transition: filter var(--t-base); will-change: filter; } .cover { width: 100%; height: 100%; transition: filter var(--t-base); will-change: filter; }
.badge-dl { position: absolute; bottom: var(--sp-1); right: var(--sp-1); min-width: 18px; height: 18px; padding: 0 3px; display: flex; align-items: center; justify-content: center; font-size: 10px; font-weight: bold; background: var(--accent-dim); color: var(--accent-fg); border-radius: var(--radius-sm); border: 1px solid var(--accent-muted); } .badge-dl { position: absolute; bottom: var(--sp-1); right: var(--sp-1); min-width: 18px; height: 18px; padding: 0 3px; display: flex; align-items: center; justify-content: center; font-size: 10px; font-weight: bold; background: var(--accent-dim); color: var(--accent-fg); border-radius: var(--radius-sm); border: 1px solid var(--accent-muted); }
.badge-unread { position: absolute; top: var(--sp-1); left: var(--sp-1); min-width: 18px; height: 18px; padding: 0 4px; display: flex; align-items: center; justify-content: center; font-size: 10px; font-weight: bold; background: var(--bg-void); color: var(--text-primary); border-radius: var(--radius-sm); border: 1px solid var(--border-strong); } .badge-unread { position: absolute; top: var(--sp-1); left: var(--sp-1); min-width: 18px; height: 18px; padding: 0 4px; display: flex; align-items: center; justify-content: center; font-size: 10px; font-weight: bold; background: var(--bg-void); color: var(--text-primary); border-radius: var(--radius-sm); border: 1px solid var(--border-strong); }
.badge-new { position: absolute; top: 6px; right: 6px; width: 8px; height: 8px; border-radius: 50%; background: var(--accent-fg); border: 2px solid var(--bg-base); box-shadow: 0 0 6px var(--accent); }
/* Select overlay (checkbox) */ /* Select overlay (checkbox) */
.select-overlay { position: absolute; inset: 0; background: rgba(0,0,0,0.18); display: flex; align-items: flex-start; justify-content: flex-end; padding: 6px; pointer-events: none; } .select-overlay { position: absolute; inset: 0; background: rgba(0,0,0,0.18); display: flex; align-items: flex-start; justify-content: flex-end; padding: 6px; pointer-events: none; }
+2 -2
View File
@@ -6,7 +6,7 @@
import { GET_MANGA, GET_CHAPTERS, FETCH_CHAPTERS, ENQUEUE_DOWNLOAD, UPDATE_MANGA, MARK_CHAPTER_READ, MARK_CHAPTERS_READ, DELETE_DOWNLOADED_CHAPTERS, ENQUEUE_CHAPTERS_DOWNLOAD, GET_ALL_MANGA, GET_CATEGORIES, CREATE_CATEGORY, UPDATE_MANGA_CATEGORIES } from "../../lib/queries"; import { GET_MANGA, GET_CHAPTERS, FETCH_CHAPTERS, ENQUEUE_DOWNLOAD, UPDATE_MANGA, MARK_CHAPTER_READ, MARK_CHAPTERS_READ, DELETE_DOWNLOADED_CHAPTERS, ENQUEUE_CHAPTERS_DOWNLOAD, GET_ALL_MANGA, GET_CATEGORIES, CREATE_CATEGORY, UPDATE_MANGA_CATEGORIES } from "../../lib/queries";
import { cache, CACHE_KEYS, recordSourceAccess } from "../../lib/cache"; import { cache, CACHE_KEYS, recordSourceAccess } from "../../lib/cache";
import { dedupeMangaById, dedupeMangaByTitle } from "../../lib/util"; import { dedupeMangaById, dedupeMangaByTitle } from "../../lib/util";
import { store, addToast, updateSettings, openReader, setActiveManga, setGenreFilter, setNavPage, linkManga, unlinkManga, setPreviewManga, checkAndMarkCompleted as storeCheckAndMarkCompleted, clearMarkersForManga, addBookmark } from "../../store/state.svelte"; import { store, addToast, updateSettings, openReader, setActiveManga, setGenreFilter, setNavPage, linkManga, unlinkManga, setPreviewManga, checkAndMarkCompleted as storeCheckAndMarkCompleted, clearMarkersForManga, addBookmark, acknowledgeUpdate } from "../../store/state.svelte";
import type { MangaPrefs } from "../../store/state.svelte"; import type { MangaPrefs } from "../../store/state.svelte";
import { DEFAULT_MANGA_PREFS } from "../../store/state.svelte"; import { DEFAULT_MANGA_PREFS } from "../../store/state.svelte";
import type { Manga, Chapter, Category } from "../../lib/types"; import type { Manga, Chapter, Category } from "../../lib/types";
@@ -326,7 +326,7 @@
$effect(() => { $effect(() => {
const m = store.activeManga; const m = store.activeManga;
if (m) untrack(() => { loadManga(m.id); loadChapters(m.id); loadCategories(m.id); }); if (m) untrack(() => { acknowledgeUpdate(m.id); loadManga(m.id); loadChapters(m.id); loadCategories(m.id); });
}); });
let prevChapterId: number | null = null; let prevChapterId: number | null = null;
+20 -11
View File
@@ -449,20 +449,22 @@ class Store {
readerSessionId: number = $state(0); readerSessionId: number = $state(0);
libraryUpdates: LibraryUpdateEntry[] = $state(saved?.libraryUpdates ?? []); libraryUpdates: LibraryUpdateEntry[] = $state(saved?.libraryUpdates ?? []);
lastLibraryRefresh: number = $state(saved?.lastLibraryRefresh ?? 0); lastLibraryRefresh: number = $state(saved?.lastLibraryRefresh ?? 0);
acknowledgedUpdates: Set<number> = $state(new Set(saved?.acknowledgedUpdateIds ?? []));
constructor() { constructor() {
$effect.root(() => { $effect.root(() => {
$effect(() => { $effect(() => {
persist({ persist({
settings: this.settings, settings: this.settings,
history: this.history, history: this.history,
bookmarks: this.bookmarks, bookmarks: this.bookmarks,
markers: this.markers, markers: this.markers,
readLog: this.readLog, readLog: this.readLog,
readingStats: this.readingStats, readingStats: this.readingStats,
libraryUpdates: this.libraryUpdates, libraryUpdates: this.libraryUpdates,
lastLibraryRefresh: this.lastLibraryRefresh, lastLibraryRefresh: this.lastLibraryRefresh,
storeVersion: STORE_VERSION, acknowledgedUpdateIds: [...this.acknowledgedUpdates],
storeVersion: STORE_VERSION,
}); });
}); });
}); });
@@ -692,8 +694,14 @@ class Store {
} }
clearLibraryUpdates() { clearLibraryUpdates() {
this.libraryUpdates = []; this.libraryUpdates = [];
this.lastLibraryRefresh = 0; this.lastLibraryRefresh = 0;
this.acknowledgedUpdates = new Set();
}
acknowledgeUpdate(mangaId: number) {
if (this.acknowledgedUpdates.has(mangaId)) return;
this.acknowledgedUpdates = new Set([...this.acknowledgedUpdates, mangaId]);
} }
bumpReaderSession() { bumpReaderSession() {
@@ -733,6 +741,7 @@ export function resetKeybinds() { sto
export function clearDiscoverCache() { store.clearDiscoverCache(); } export function clearDiscoverCache() { store.clearDiscoverCache(); }
export function setLibraryUpdates(entries: LibraryUpdateEntry[]) { store.setLibraryUpdates(entries); } export function setLibraryUpdates(entries: LibraryUpdateEntry[]) { store.setLibraryUpdates(entries); }
export function clearLibraryUpdates() { store.clearLibraryUpdates(); } export function clearLibraryUpdates() { store.clearLibraryUpdates(); }
export function acknowledgeUpdate(mangaId: number) { store.acknowledgeUpdate(mangaId); }
export function bumpReaderSession() { store.bumpReaderSession(); } export function bumpReaderSession() { store.bumpReaderSession(); }
export function addBookmark(entry: Omit<BookmarkEntry, "savedAt">, label?: string) { store.addBookmark(entry, label); } export function addBookmark(entry: Omit<BookmarkEntry, "savedAt">, label?: string) { store.addBookmark(entry, label); }
export function removeBookmark(chapterId: number) { store.removeBookmark(chapterId); } export function removeBookmark(chapterId: number) { store.removeBookmark(chapterId); }