diff --git a/src/api/queries/chapters.ts b/src/api/queries/chapters.ts index 4c22666..91a4920 100644 --- a/src/api/queries/chapters.ts +++ b/src/api/queries/chapters.ts @@ -2,6 +2,12 @@ export const GET_RECENTLY_UPDATED = ` query GetRecentlyUpdated { chapters(orderBy: FETCHED_AT, orderByType: DESC, first: 300) { nodes { + id + name + chapterNumber + sourceOrder + isRead + lastPageRead mangaId fetchedAt manga { id title thumbnailUrl inLibrary } @@ -19,4 +25,4 @@ export const GET_CHAPTERS = ` } } } -`; \ No newline at end of file +`; diff --git a/src/api/queries/manga.ts b/src/api/queries/manga.ts index d2656eb..ac89539 100644 --- a/src/api/queries/manga.ts +++ b/src/api/queries/manga.ts @@ -75,6 +75,9 @@ export const LIBRARY_UPDATE_STATUS = ` manga { id title thumbnailUrl unreadCount } } } + lastUpdateTimestamp { + timestamp + } } `; diff --git a/src/api/queries/queries.md b/src/api/queries/queries.md index e8dadef..7c441a8 100644 --- a/src/api/queries/queries.md +++ b/src/api/queries/queries.md @@ -10,7 +10,7 @@ | `GET_CATEGORIES` | — | All categories with order/settings and their assigned manga (minimal fields) | | `GET_DOWNLOADED_CHAPTERS_PAGES` | — | Page counts for all downloaded chapters — used for storage stats | | `GET_DOWNLOADS_PATH` | — | `downloadsPath` and `localSourcePath` from settings | -| `LIBRARY_UPDATE_STATUS` | — | Current library update job — `jobsInfo` progress and `mangaUpdates` list with new chapters | +| `LIBRARY_UPDATE_STATUS` | — | Current library update job (`jobsInfo`, `mangaUpdates`) plus `lastUpdateTimestamp` for server-side update timing | | `GET_RESTORE_STATUS` | `id: String!` | Backup restore job status by job ID — `mangaProgress`, `state`, `totalManga` | | `VALIDATE_BACKUP` | `backup: Upload!` | Validate a backup file before restore — returns missing sources and trackers | | `MANGAS_BY_GENRE` | `filter: MangaFilterInput`, `first: Int`, `offset: Int` | Paginated manga filtered by genre, ordered by `IN_LIBRARY_AT DESC` | diff --git a/src/core/cache/queryCache.ts b/src/core/cache/queryCache.ts index 4eae1cf..1baae89 100644 --- a/src/core/cache/queryCache.ts +++ b/src/core/cache/queryCache.ts @@ -147,6 +147,7 @@ export const CACHE_GROUPS = { export const CACHE_KEYS = { LIBRARY: "library", + RECENT_UPDATES: "recent_updates", ALL_MANGA: "all_manga_unfiltered", CATEGORIES: "categories", SEARCH: "search_all_manga", diff --git a/src/features/downloads/components/DownloadQueue.svelte b/src/features/downloads/components/DownloadQueue.svelte index f9f235c..a543f33 100644 --- a/src/features/downloads/components/DownloadQueue.svelte +++ b/src/features/downloads/components/DownloadQueue.svelte @@ -23,8 +23,28 @@ {#if loading} -
- +
+ {#each Array(5) as _, i (i)} +
+
+ +
+
+
+
+
+
+
+
+ +
+
+
+
+
+
+
+ {/each}
{:else if queue.length === 0}
Queue is empty.
@@ -49,4 +69,30 @@ \ No newline at end of file diff --git a/src/features/extensions/components/ExtensionFilters.svelte b/src/features/extensions/components/ExtensionFilters.svelte index 48f7920..e0901fc 100644 --- a/src/features/extensions/components/ExtensionFilters.svelte +++ b/src/features/extensions/components/ExtensionFilters.svelte @@ -1,5 +1,5 @@
- -
-
- - History -
-
-
- - - {#if search} - - {/if} -
- {#if store.history.length > 0} - - {/if} -
-
- - {#if store.readingStats.totalChaptersRead > 0} -
-
-
- -
-
- {store.readingStats.currentStreakDays} - day streak -
-
-
-
- -
-
- {store.readingStats.totalChaptersRead} - chapters -
-
-
-
- -
-
- {formatReadTime(store.readingStats.totalMinutesRead)} - read time -
-
-
-
- -
-
- {store.readingStats.totalMangaRead} - series -
-
-
- {/if} - {#if store.history.length === 0}
@@ -184,6 +114,44 @@
{:else}
+ {#if store.readingStats.totalChaptersRead > 0} +
+
+ Reading Stats +
+
+
+
+
+ {store.readingStats.currentStreakDays} + Day streak +
+
+
+
+
+ {store.readingStats.totalChaptersRead} + Chapters read +
+
+
+
+
+ {formatReadTime(store.readingStats.totalMinutesRead)} + Read time +
+
+
+
+
+ {store.readingStats.totalMangaRead} + Series read +
+
+
+
+ {/if} + {#each groups as { label, items }}
@@ -230,180 +198,105 @@ overflow: hidden; } - .header { + .stats-header { display: flex; align-items: center; - justify-content: space-between; - padding: var(--sp-4) var(--sp-6); - border-bottom: 1px solid var(--border-dim); - flex-shrink: 0; + padding-bottom: var(--sp-2); } - .heading-group { - display: flex; + .stats-title { + display: inline-flex; align-items: center; gap: var(--sp-2); - } - - :global(.heading-icon) { color: var(--text-faint); } - - .heading { font-family: var(--font-ui); - font-size: var(--text-xs); - font-weight: var(--weight-medium); - color: var(--text-muted); + font-size: var(--text-2xs); + color: var(--text-faint); letter-spacing: var(--tracking-wider); text-transform: uppercase; } - .header-right { - display: flex; - align-items: center; - gap: var(--sp-2); - } - - .search-wrap { - position: relative; - display: flex; - align-items: center; - } - - .search-wrap :global(.search-icon) { - position: absolute; - left: 8px; - color: var(--text-faint); - pointer-events: none; - } - - .search { - background: var(--bg-raised); - border: 1px solid var(--border-dim); - border-radius: var(--radius-md); - padding: 4px 26px; - color: var(--text-primary); - font-size: var(--text-xs); - width: 148px; - outline: none; - transition: border-color var(--t-base), width var(--t-base), background var(--t-base); - } - - .search::placeholder { color: var(--text-faint); } - - .search:focus { - border-color: var(--border-strong); - background: var(--bg-elevated); - width: 200px; - } - - .search-clear { - position: absolute; - right: 8px; - color: var(--text-faint); - font-size: 13px; - line-height: 1; - background: none; - border: none; - cursor: pointer; - padding: 2px; - transition: color var(--t-base); - } - - .search-clear:hover { color: var(--text-muted); } - - .clear-btn { - display: flex; align-items: center; gap: 4px; - height: 30px; padding: 0 var(--sp-2); - border-radius: var(--radius-md); border: 1px solid var(--border-dim); - background: var(--bg-raised); color: var(--text-faint); - cursor: pointer; font-family: var(--font-ui); font-size: var(--text-2xs); - letter-spacing: var(--tracking-wide); flex-shrink: 0; - transition: color var(--t-base), background var(--t-base), border-color var(--t-base); - } - .clear-btn:hover { color: var(--color-error); background: var(--color-error-bg); border-color: color-mix(in srgb, var(--color-error) 30%, transparent); } - .clear-btn.confirm { color: var(--color-error); background: var(--color-error-bg); border-color: var(--color-error); } - - .clear-label { font-size: var(--text-2xs); } - .stats-grid { display: grid; - grid-template-columns: repeat(4, 1fr); - gap: 1px; - background: var(--border-dim); - border-bottom: 1px solid var(--border-dim); - flex-shrink: 0; + grid-template-columns: repeat(auto-fit, minmax(100px, 1fr)); + gap: var(--sp-2); } .stat-card { display: flex; align-items: center; - gap: var(--sp-2); - padding: var(--sp-3) var(--sp-4); - background: var(--bg-base); - transition: background var(--t-base); + gap: var(--sp-3); + background: var(--bg-raised); + border: 1px solid var(--border-dim); + border-radius: var(--radius-md); + padding: var(--sp-3); + transition: border-color var(--t-fast); } - .stat-card.streak .stat-icon-wrap { background: color-mix(in srgb, #f97316 12%, transparent); } - .stat-card.streak .stat-val { color: #f97316; } + .stat-card:hover { border-color: var(--border-base); } .stat-icon-wrap { display: flex; align-items: center; justify-content: center; - width: 24px; - height: 24px; + width: 32px; + height: 32px; border-radius: var(--radius-sm); - background: var(--bg-raised); - color: var(--text-faint); flex-shrink: 0; } - .stat-icon-wrap.fire { color: #f97316; } + .fire { background: rgba(251, 146, 60, 0.15); color: #fb923c; } + .accent { background: var(--accent-muted); color: var(--accent-fg); } + .neutral { background: var(--bg-overlay); color: var(--text-faint); } .stat-body { display: flex; flex-direction: column; - gap: 1px; + gap: 2px; min-width: 0; } .stat-val { font-family: var(--font-ui); - font-size: var(--text-sm); - font-weight: var(--weight-semibold); + font-size: var(--text-lg, 1.05rem); + font-weight: var(--weight-medium); color: var(--text-secondary); line-height: 1; - letter-spacing: -0.01em; } - .stat-unit { + .stat-label { font-family: var(--font-ui); - font-size: 9px; + font-size: var(--text-2xs); color: var(--text-faint); letter-spacing: var(--tracking-wide); - text-transform: uppercase; white-space: nowrap; } .timeline { flex: 1; overflow-y: auto; - padding: var(--sp-4) var(--sp-5) var(--sp-6); + padding: var(--sp-4) var(--sp-6) var(--sp-6); + display: flex; + flex-direction: column; + gap: var(--sp-5); scrollbar-width: thin; scrollbar-color: var(--border-dim) transparent; } - .day-group { margin-bottom: var(--sp-5); } + .day-group { + display: flex; + flex-direction: column; + gap: var(--sp-3); + } .day-header { display: flex; align-items: center; gap: var(--sp-3); - padding-bottom: var(--sp-2); } .day-label { font-family: var(--font-ui); - font-size: 9px; + font-size: var(--text-2xs); color: var(--text-faint); letter-spacing: var(--tracking-wider); text-transform: uppercase; @@ -414,12 +307,12 @@ flex: 1; height: 1px; background: var(--border-dim); - opacity: 0.5; } .session-list { display: flex; flex-direction: column; + gap: var(--sp-2); } .session-row { @@ -427,17 +320,16 @@ align-items: center; gap: var(--sp-3); width: 100%; - padding: var(--sp-2) var(--sp-2); + padding: var(--sp-3); border-radius: var(--radius-md); - border: none; - background: none; + border: 1px solid var(--border-dim); + background: var(--bg-raised); text-align: left; cursor: pointer; - transition: background var(--t-fast); + transition: border-color var(--t-fast), background var(--t-fast); } - .session-row:hover { background: var(--bg-raised); } - .session-row:active { background: var(--bg-elevated); } + .session-row:hover { border-color: var(--border-strong); background: var(--bg-elevated); } .thumb-wrap { position: relative; @@ -494,8 +386,8 @@ align-items: center; gap: 4px; font-family: var(--font-ui); - font-size: var(--text-2xs); - color: var(--text-faint); + font-size: var(--text-xs); + color: var(--text-muted); letter-spacing: var(--tracking-wide); white-space: nowrap; overflow: hidden; diff --git a/src/features/recent/components/Recent.svelte b/src/features/recent/components/Recent.svelte new file mode 100644 index 0000000..cb9e716 --- /dev/null +++ b/src/features/recent/components/Recent.svelte @@ -0,0 +1,234 @@ + + +
+
+ Recent + +
+ + +
+ +
+ {#if tab === "updates"} + + {:else} +
+ + historySearch = (e.target as HTMLInputElement).value} + /> + {#if historySearch} + + {/if} +
+ {#if store.history.length > 0} + + {/if} + {/if} +
+
+ +
+ {#if tab === "updates"} + updatesRefreshFn = fn} + /> + {:else} + + {/if} +
+
+ + diff --git a/src/features/recent/components/UpdatesPanel.svelte b/src/features/recent/components/UpdatesPanel.svelte new file mode 100644 index 0000000..4539637 --- /dev/null +++ b/src/features/recent/components/UpdatesPanel.svelte @@ -0,0 +1,631 @@ + + +
+
+
+
+ + {#if loading}Checking for updates…{:else if error}Update check failed{:else if updaterRunning}Library update in progress...{#if updaterProgressLabel} ({updaterProgressLabel}){/if}{:else}Up to date{/if} + +
+ {#if !loading && lastUpdatedLabel} + Last updated: {lastUpdatedLabel} +
+ {/if} + {#if !loading && updates.length > 0} + {updates.length} chapter{updates.length === 1 ? "" : "s"} + {/if} +
+
+
+ + {#if loading && updates.length === 0} + + {:else if error} +
+
+ +
+

Couldn't load updates

+

{error}

+
+ {:else if updates.length === 0} +
+
+ +
+

No recent library updates

+

Run a library update to populate this page.

+
+ {:else} +
+ {#each groups as { label, items } (label)} +
+
+ {label} +
+
+ +
+ {#each items as item (item.id)} +
+ + + +
+ {/each} +
+
+ {/each} +
+ {/if} +
+ + diff --git a/src/shared/chrome/Layout.svelte b/src/shared/chrome/Layout.svelte index 6c4ec74..a91bd61 100644 --- a/src/shared/chrome/Layout.svelte +++ b/src/shared/chrome/Layout.svelte @@ -1,7 +1,6 @@
@@ -27,7 +27,7 @@ {:else if store.navPage === "search"} {:else if store.navPage === "history"} - + {:else if store.navPage === "downloads"} {:else if store.navPage === "extensions"} @@ -45,4 +45,4 @@ .frame { display: flex; padding: 6px 15px 15px; width: 100%; height: 100%; box-sizing: border-box; overflow: hidden; } .shell { display: flex; flex: 1; border-radius: 14px; overflow: hidden; border: 1px solid var(--border-dim); background: var(--bg-base); background-image: var(--bg-image); min-height: 0; min-width: 0; } .main { flex: 1; overflow: hidden; background: var(--bg-surface); transform: translateZ(0); contain: layout style; min-width: 0; } - \ No newline at end of file + diff --git a/src/shared/chrome/Sidebar.svelte b/src/shared/chrome/Sidebar.svelte index 0fc72d6..1ba045b 100644 --- a/src/shared/chrome/Sidebar.svelte +++ b/src/shared/chrome/Sidebar.svelte @@ -7,7 +7,7 @@ { id: "home", label: "Home", icon: House }, { id: "library", label: "Library", icon: Books }, { id: "search", label: "Search", icon: MagnifyingGlass }, - { id: "history", label: "History", icon: ClockCounterClockwise }, + { id: "history", label: "Recent", icon: ClockCounterClockwise }, { id: "downloads", label: "Downloads", icon: DownloadSimple }, { id: "extensions", label: "Extensions", icon: PuzzlePiece }, { id: "tracking", label: "Tracking", icon: ChartLineUp }, @@ -93,4 +93,4 @@ .settings-btn:focus-visible { outline: 2px solid var(--accent); outline-offset: -2px; } .settings-btn.anims { transition: color var(--t-base), background var(--t-base), transform var(--t-slow); } .settings-btn.anims:hover { transform: rotate(30deg); } - \ No newline at end of file + diff --git a/src/store/app.svelte.ts b/src/store/app.svelte.ts index f22bd06..99cf479 100644 --- a/src/store/app.svelte.ts +++ b/src/store/app.svelte.ts @@ -31,4 +31,4 @@ export function setSearchPrefill(next: string) { app.setSearchPrefill(next); export function setSearchQuery(next: string) { app.setSearchQuery(next); } export function setGenreFilter(next: string) { app.setGenreFilter(next); } export function saveScroll(key: string, top: number) { app.saveScroll(key, top); } -export function getScroll(key: string): number { return app.getScroll(key); } \ No newline at end of file +export function getScroll(key: string): number { return app.getScroll(key); }