mirror of
https://github.com/moku-project/Moku.git
synced 2026-06-14 01:39:56 -05:00
Feat: Recent Tab (Unread State) + Bug Fixes
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
<script lang="ts">
|
||||
import { Download, CheckCircle, Circle, CircleNotch, Trash } from 'phosphor-svelte'
|
||||
import { Download, CheckSquare, Square, CircleNotch, Trash } from 'phosphor-svelte'
|
||||
import ContextMenu from '$lib/components/shared/ui/ContextMenu.svelte'
|
||||
import type { MenuEntry } from '$lib/components/shared/ui/ContextMenu.svelte'
|
||||
import { longPress } from '$lib/core/ui/touchscreen'
|
||||
@@ -14,27 +14,39 @@
|
||||
enqueueing: Set<number>
|
||||
chapterPage: number
|
||||
totalPages: number
|
||||
scrollEl?: HTMLDivElement | null
|
||||
onOpen: (ch: Chapter, inProgress: boolean) => void
|
||||
onToggleSelect: (id: number, e: MouseEvent | KeyboardEvent) => void
|
||||
onEnqueue: (ch: Chapter, e: MouseEvent) => void
|
||||
onDeleteDownload:(id: number) => void
|
||||
onPageChange: (page: number) => void
|
||||
onPageSizeChange:(n: number) => void
|
||||
buildCtxItems: (ch: Chapter, idx: number) => MenuEntry[]
|
||||
}
|
||||
|
||||
let {
|
||||
pageChapters, sortedChapters, viewMode, loadingChapters,
|
||||
selectedIds, enqueueing, chapterPage, totalPages,
|
||||
scrollEl = $bindable(null),
|
||||
onOpen, onToggleSelect, onEnqueue, onDeleteDownload,
|
||||
onPageChange, buildCtxItems,
|
||||
onPageChange, onPageSizeChange, buildCtxItems,
|
||||
}: Props = $props()
|
||||
|
||||
let ctx: { x: number; y: number; chapter: Chapter; idx: number } | null = $state(null)
|
||||
let listEl: HTMLDivElement | null = $state(null)
|
||||
|
||||
const hasSelection = $derived(selectedIds.size > 0)
|
||||
|
||||
$effect(() => {
|
||||
if (!listEl || viewMode !== 'list') return
|
||||
const ro = new ResizeObserver(([entry]) => {
|
||||
const firstRow = listEl!.querySelector('.ch-row') as HTMLElement | null
|
||||
const rowH = firstRow ? firstRow.offsetHeight : 37
|
||||
const n = Math.max(1, Math.floor(entry.contentRect.height / rowH))
|
||||
onPageSizeChange(n)
|
||||
})
|
||||
ro.observe(listEl)
|
||||
return () => ro.disconnect()
|
||||
})
|
||||
|
||||
function chapterLongPress(node: HTMLElement, param: [Chapter, number]) {
|
||||
const [ch, idx] = param
|
||||
return longPress(node, {
|
||||
@@ -50,7 +62,7 @@
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class={viewMode === 'grid' ? 'ch-grid' : 'ch-list'} bind:this={scrollEl}>
|
||||
<div class={viewMode === 'grid' ? 'ch-grid' : 'ch-list'} bind:this={listEl}>
|
||||
{#if loadingChapters && sortedChapters.length === 0}
|
||||
{#if viewMode === 'grid'}
|
||||
{#each Array(24) as _}<div class="grid-cell-skeleton skeleton"></div>{/each}
|
||||
@@ -100,7 +112,7 @@
|
||||
oncontextmenu={(e) => { e.preventDefault(); ctx = { x: e.clientX, y: e.clientY, chapter: ch, idx: idxInSorted } }}
|
||||
>
|
||||
<button class="ch-check" class:ch-check-visible={hasSelection} onclick={(e) => onToggleSelect(ch.id, e)} title="Select">
|
||||
{#if isSelected}<CheckCircle size={15} weight="fill" />{:else}<Circle size={15} weight="light" />{/if}
|
||||
{#if isSelected}<CheckSquare size={15} weight="fill" />{:else}<Square size={15} weight="light" />{/if}
|
||||
</button>
|
||||
<div class="ch-left">
|
||||
<span class="ch-name">{ch.name}</span>
|
||||
@@ -111,7 +123,7 @@
|
||||
</div>
|
||||
</div>
|
||||
<div class="ch-right">
|
||||
{#if ch.read}<CheckCircle size={14} weight="light" class="read-icon" />{/if}
|
||||
{#if ch.read}<CheckSquare size={14} weight="light" class="read-icon" />{/if}
|
||||
{#if ch.downloaded}
|
||||
<div class="ch-dl-wrap">
|
||||
<Download size={13} weight="fill" class="ch-dl-icon" />
|
||||
@@ -145,38 +157,42 @@
|
||||
{/if}
|
||||
|
||||
<style>
|
||||
.ch-list { flex: 1; overflow-y: auto; }
|
||||
.ch-grid { flex: 1; overflow-y: auto; display: grid; grid-template-columns: repeat(auto-fill, minmax(42px, 1fr)); gap: 4px; padding: var(--sp-3); align-content: start; }
|
||||
.ch-list { flex: 1; overflow: hidden; }
|
||||
.ch-grid { flex: 1; overflow: hidden; display: grid; grid-template-columns: repeat(auto-fill, minmax(42px, 1fr)); gap: 4px; padding: var(--sp-3); align-content: start; }
|
||||
|
||||
.ch-row { display: flex; align-items: center; padding: 10px var(--sp-4); border-bottom: 1px solid var(--border-dim); cursor: pointer; transition: background var(--t-fast); gap: var(--sp-3); }
|
||||
.ch-row { display: flex; align-items: center; padding: 8px var(--sp-4); border-bottom: 1px solid var(--border-dim); cursor: pointer; transition: background var(--t-fast); gap: var(--sp-3); }
|
||||
.ch-row:hover { background: var(--bg-raised); }
|
||||
.ch-row.read { opacity: 0.45; }
|
||||
.ch-left { flex: 1; min-width: 0; display: flex; flex-direction: column; gap: 3px; }
|
||||
.ch-row.read { opacity: 0.5; }
|
||||
.ch-left { flex: 1; min-width: 0; display: flex; flex-direction: column; gap: 2px; }
|
||||
.ch-name { font-size: var(--text-sm); color: var(--text-secondary); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
||||
.ch-meta { display: flex; align-items: center; gap: var(--sp-2); flex-wrap: wrap; }
|
||||
.ch-meta { display: flex; align-items: center; gap: var(--sp-2); }
|
||||
.ch-meta-item { font-family: var(--font-ui); font-size: var(--text-2xs); color: var(--text-faint); letter-spacing: var(--tracking-wide); }
|
||||
.ch-right { display: flex; align-items: center; gap: var(--sp-1); flex-shrink: 0; }
|
||||
:global(.read-icon) { color: var(--text-faint); }
|
||||
:global(.enqueue-icon) { color: var(--text-faint); }
|
||||
|
||||
.dl-btn { display: flex; align-items: center; justify-content: center; width: 24px; height: 24px; border-radius: var(--radius-sm); color: var(--text-faint); transition: color var(--t-base), background var(--t-base); opacity: 0; }
|
||||
.ch-row:hover .dl-btn { opacity: 1; }
|
||||
.dl-btn { display: flex; align-items: center; justify-content: center; width: 24px; height: 24px; border-radius: var(--radius-sm); color: var(--text-faint); transition: color var(--t-base), background var(--t-base); }
|
||||
.dl-btn:hover { color: var(--text-muted); background: var(--bg-overlay); }
|
||||
.dl-btn-delete { color: var(--color-error) !important; opacity: 0; }
|
||||
.ch-row:hover .dl-btn-delete { opacity: 1; }
|
||||
.dl-btn-delete { color: var(--color-error) !important; }
|
||||
.dl-btn-delete:hover { background: var(--color-error-bg) !important; }
|
||||
|
||||
.ch-dl-wrap { position: relative; display: flex; align-items: center; justify-content: center; width: 24px; height: 24px; }
|
||||
:global(.ch-dl-icon) { color: var(--text-faint); transition: opacity var(--t-fast); }
|
||||
.ch-row:hover .ch-dl-wrap :global(.ch-dl-icon) { opacity: 0; }
|
||||
.ch-dl-wrap .dl-btn-delete { position: absolute; inset: 0; opacity: 0; }
|
||||
.ch-row:hover .ch-dl-wrap .dl-btn-delete { opacity: 1; }
|
||||
.ch-dl-wrap { display: flex; align-items: center; gap: var(--sp-1); }
|
||||
:global(.ch-dl-icon) { color: var(--text-faint); }
|
||||
|
||||
.ch-check { display: flex; align-items: center; justify-content: center; width: 20px; height: 20px; flex-shrink: 0; border-radius: var(--radius-sm); border: none; background: none; color: var(--text-faint); cursor: pointer; opacity: 0; transition: opacity var(--t-fast), color var(--t-fast); padding: 0; }
|
||||
.ch-row:hover .ch-check { opacity: 1; }
|
||||
.ch-check-visible { opacity: 1 !important; }
|
||||
.ch-check {
|
||||
display: flex; align-items: center; justify-content: center;
|
||||
width: 20px; height: 20px; flex-shrink: 0;
|
||||
border-radius: var(--radius-sm); border: none; background: none;
|
||||
color: var(--text-faint); cursor: pointer; padding: 0;
|
||||
opacity: 0;
|
||||
transform: translateX(-6px);
|
||||
transition: opacity var(--t-fast), transform var(--t-fast), color var(--t-fast);
|
||||
margin-right: -20px;
|
||||
}
|
||||
.ch-row:hover .ch-check { opacity: 1; transform: translateX(0); margin-right: 0; }
|
||||
.ch-check-visible { opacity: 1 !important; transform: translateX(0) !important; margin-right: 0 !important; }
|
||||
.ch-selected .ch-check { color: var(--accent-fg); }
|
||||
.ch-selected { background: color-mix(in srgb, var(--accent) 8%, transparent) !important; }
|
||||
.ch-selected .ch-check { color: var(--accent-fg); opacity: 1; }
|
||||
|
||||
.row-skeleton { display: flex; flex-direction: column; gap: var(--sp-2); padding: 12px var(--sp-4); border-bottom: 1px solid var(--border-dim); }
|
||||
|
||||
@@ -184,8 +200,8 @@
|
||||
.grid-cell:hover { background: var(--bg-overlay); border-color: var(--border-strong); }
|
||||
.grid-cell.read { background: var(--color-read); color: var(--text-faint); border-color: transparent; }
|
||||
.grid-cell-num { font-size: 10px; }
|
||||
.grid-cell-dot { position: absolute; bottom: 3px; right: 3px; width: 4px; height: 4px; border-radius: 50%; background: var(--text-faint); }
|
||||
.grid-cell-dl { position: absolute; top: 3px; left: 3px; width: 4px; height: 4px; border-radius: 50%; background: var(--accent-fg); }
|
||||
.grid-cell-dot { position: absolute; bottom: 3px; right: 3px; width: 4px; height: 4px; border-radius: var(--radius-sm); background: var(--text-faint); }
|
||||
.grid-cell-dl { position: absolute; top: 3px; left: 3px; width: 4px; height: 4px; border-radius: var(--radius-sm); background: var(--accent-fg); }
|
||||
.grid-cell-spinner { position: absolute; top: 2px; right: 2px; }
|
||||
.grid-cell-skeleton { aspect-ratio: 1; border-radius: var(--radius-sm); }
|
||||
.grid-selected { background: var(--accent-muted) !important; border-color: var(--accent-dim) !important; }
|
||||
|
||||
@@ -19,8 +19,6 @@
|
||||
sortMode: ChapterSortMode
|
||||
sortDir: ChapterSortDir
|
||||
viewMode: 'list' | 'grid'
|
||||
chapterPage: number
|
||||
totalPages: number
|
||||
downloadedCount: number
|
||||
totalCount: number
|
||||
deletingAll: boolean
|
||||
@@ -57,7 +55,7 @@
|
||||
|
||||
let {
|
||||
chapters, sortedChapters, sortMode, sortDir, viewMode,
|
||||
chapterPage, totalPages, downloadedCount, totalCount, deletingAll,
|
||||
downloadedCount, totalCount, deletingAll,
|
||||
hasSelection, selectedCount, continueChapter,
|
||||
availableScanlators, scanlatorFilter, scanlatorBlacklist, scanlatorForce,
|
||||
allCategories, mangaCategories, catsLoading, refreshing,
|
||||
@@ -277,9 +275,11 @@
|
||||
<ArrowsClockwise size={14} weight="light" class={refreshing ? 'anim-spin' : ''} />
|
||||
</button>
|
||||
|
||||
<button class="icon-btn" onclick={onOpenFolder} title="Open manga folder">
|
||||
<FolderOpen size={14} weight="light" />
|
||||
</button>
|
||||
{#if downloadedCount > 0}
|
||||
<button class="icon-btn" onclick={onOpenFolder} title="Open manga folder">
|
||||
<FolderOpen size={14} weight="light" />
|
||||
</button>
|
||||
{/if}
|
||||
|
||||
<div class="fp-wrap" bind:this={folderPickerRef}>
|
||||
<button class="icon-btn" class:active={hasFolders} onclick={() => folderPickerOpen = !folderPickerOpen}>
|
||||
@@ -377,13 +377,6 @@
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#if totalPages > 1}
|
||||
<div class="pagination">
|
||||
<button class="page-btn" onclick={() => onPageChange(Math.max(1, chapterPage - 1))} disabled={chapterPage === 1}>←</button>
|
||||
<span class="page-num">{chapterPage} / {totalPages}</span>
|
||||
<button class="page-btn" onclick={() => onPageChange(Math.min(totalPages, chapterPage + 1))} disabled={chapterPage === totalPages}>→</button>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -572,17 +565,6 @@
|
||||
.dl-unified-btn.dl-has-count:hover { background: var(--accent-muted); border-color: var(--accent); opacity: 0.9; }
|
||||
.dl-unified-btn.active { color: var(--accent-fg); border-color: var(--accent-dim); background: var(--accent-muted); }
|
||||
|
||||
.pagination { display: flex; align-items: center; gap: var(--sp-2); }
|
||||
.page-btn {
|
||||
font-family: var(--font-ui); font-size: var(--text-xs); letter-spacing: var(--tracking-wide);
|
||||
padding: 4px 10px; border-radius: var(--radius-sm); border: 1px solid var(--border-dim);
|
||||
color: var(--text-faint); background: none; cursor: pointer;
|
||||
transition: color var(--t-base), border-color var(--t-base);
|
||||
}
|
||||
.page-btn:hover:not(:disabled) { color: var(--text-muted); border-color: var(--border-strong); }
|
||||
.page-btn:disabled { opacity: 0.3; cursor: default; }
|
||||
.page-num { font-family: var(--font-ui); font-size: var(--text-xs); color: var(--text-faint); letter-spacing: var(--tracking-wide); }
|
||||
|
||||
.sel-count {
|
||||
font-family: var(--font-ui); font-size: var(--text-xs); color: var(--text-muted);
|
||||
letter-spacing: var(--tracking-wide); padding: 0 var(--sp-1);
|
||||
|
||||
@@ -8,13 +8,17 @@
|
||||
CheckCircle, Circle, ArrowFatLinesUp, ArrowFatLinesDown,
|
||||
ArrowFatLineUp, ArrowFatLineDown, Download, Trash, DownloadSimple, CheckSquare,
|
||||
} from 'phosphor-svelte'
|
||||
import type { MenuEntry } from '$lib/components/shared/ui/ContextMenu.svelte'
|
||||
|
||||
type MenuSeparator = { separator: true }
|
||||
type MenuItem = { label: string; icon?: any; onClick: () => void; danger?: boolean; disabled?: boolean; separator?: never; children?: MenuEntry[] }
|
||||
type MenuEntry = MenuItem | MenuSeparator
|
||||
import { getManga, getMangaList } from '$lib/request-manager/manga'
|
||||
import { markChapterRead, markChaptersRead, deleteDownloadedChapters, fetchChapters } from '$lib/request-manager/chapters'
|
||||
import { downloadStore } from '$lib/state/downloads.svelte'
|
||||
import { getCategories, updateMangaCategories, createCategory as createCategoryReq, updateManga } from '$lib/request-manager/manga'
|
||||
import { saveScroll, getScroll } from '$lib/state/app.svelte'
|
||||
import { seriesState, openReaderForChapter, acknowledgeUpdate, addBookmark, clearMarkersForManga } from '$lib/state/series.svelte'
|
||||
import { updateSettings } from '$lib/state/settings.svelte'
|
||||
import { DEFAULT_MANGA_PREFS } from '$lib/state/series.svelte'
|
||||
import type { MangaPrefs } from '$lib/types/settings'
|
||||
import { addToast } from '$lib/state/notifications.svelte'
|
||||
@@ -33,8 +37,8 @@
|
||||
interface Props { mangaId: number }
|
||||
let { mangaId }: Props = $props()
|
||||
|
||||
const CHAPTERS_PER_PAGE = 25
|
||||
const MANGA_TTL_MS = 5 * 60 * 1000
|
||||
let chaptersPerPage: number = $state(25)
|
||||
const MANGA_TTL_MS = 5 * 60 * 1000
|
||||
|
||||
const mangaCache: Map<number, { data: Manga; fetchedAt: number }> = new Map()
|
||||
|
||||
@@ -80,8 +84,8 @@
|
||||
const scanlatorBlacklist = $derived(get('scanlatorBlacklist') as string[])
|
||||
const scanlatorForce = $derived(get('scanlatorForce') as boolean)
|
||||
|
||||
const totalPages = $derived(Math.ceil(sortedChapters.length / CHAPTERS_PER_PAGE))
|
||||
const pageChapters = $derived(sortedChapters.slice((chapterPage - 1) * CHAPTERS_PER_PAGE, chapterPage * CHAPTERS_PER_PAGE))
|
||||
const totalPages = $derived(Math.ceil(sortedChapters.length / chaptersPerPage))
|
||||
const pageChapters = $derived(sortedChapters.slice((chapterPage - 1) * chaptersPerPage, chapterPage * chaptersPerPage))
|
||||
const readCount = $derived(sortedChapters.filter(c => c.read).length)
|
||||
const totalCount = $derived(sortedChapters.length)
|
||||
const progressPct = $derived(totalCount > 0 ? (readCount / totalCount) * 100 : 0)
|
||||
@@ -94,11 +98,11 @@
|
||||
const bookmark = seriesState.bookmarks.find(b => b.mangaId === mangaId)
|
||||
const bookmarkedCh = bookmark ? asc.find(c => c.id === bookmark.chapterId) : null
|
||||
if (bookmarkedCh && !bookmarkedCh.read)
|
||||
return { chapter: bookmarkedCh, type: (anyRead ? 'continue' : 'start') as const, resumePage: bookmark!.pageNumber }
|
||||
return { chapter: bookmarkedCh, type: (anyRead ? 'continue' : 'start') as 'continue' | 'start', resumePage: bookmark!.pageNumber }
|
||||
const inProgress = asc.find(c => !c.read && (c.lastPageRead ?? 0) > 0)
|
||||
const firstUnread = asc.find(c => !c.read)
|
||||
const target = inProgress ?? firstUnread
|
||||
if (target) return { chapter: target, type: (anyRead ? 'continue' : 'start') as const, resumePage: null }
|
||||
if (target) return { chapter: target, type: (anyRead ? 'continue' : 'start') as 'continue' | 'start', resumePage: null }
|
||||
return { chapter: asc[0], type: 'reread' as const, resumePage: null }
|
||||
})())
|
||||
|
||||
@@ -140,8 +144,13 @@
|
||||
const completed = allCategories.find(c => c.name === 'Completed')
|
||||
if (!completed) return
|
||||
const inCompleted = mangaCategories.some(c => c.id === completed.id)
|
||||
if (allRead && !inCompleted) mangaCategories = [...mangaCategories, completed]
|
||||
else if (!allRead && inCompleted) mangaCategories = mangaCategories.filter(c => c.id !== completed.id)
|
||||
if (allRead && !inCompleted) {
|
||||
await updateMangaCategories(String(id), [completed.id], []).catch(console.error)
|
||||
mangaCategories = [...mangaCategories, completed]
|
||||
} else if (!allRead && inCompleted) {
|
||||
await updateMangaCategories(String(id), [], [completed.id]).catch(console.error)
|
||||
mangaCategories = mangaCategories.filter(c => c.id !== completed.id)
|
||||
}
|
||||
}
|
||||
|
||||
function loadMangaData(id: number) {
|
||||
@@ -154,7 +163,6 @@
|
||||
loadingManga = false
|
||||
seriesState.setActiveManga(cached.data)
|
||||
if (Date.now() - cached.fetchedAt < MANGA_TTL_MS) return
|
||||
// stale-while-revalidate: update cache + store in background
|
||||
getManga(id, ctrl.signal)
|
||||
.then(m => {
|
||||
if (ctrl.signal.aborted) return
|
||||
@@ -224,8 +232,8 @@
|
||||
const records = trackingState.recordsFor(id)
|
||||
if (!records.length) return
|
||||
const prefs = {
|
||||
sortMode: get('sortMode'),
|
||||
sortDir: get('sortDir'),
|
||||
sortMode: seriesState.settings.chapterSortMode,
|
||||
sortDir: seriesState.settings.chapterSortDir,
|
||||
preferredScanlator: get('preferredScanlator') as string,
|
||||
scanlatorFilter: scanlatorFilter,
|
||||
scanlatorBlacklist: scanlatorBlacklist,
|
||||
@@ -278,7 +286,7 @@
|
||||
checkAndMarkCompleted(mangaId, seriesState.chaptersFor(mangaId))
|
||||
const ch = seriesState.chaptersFor(mangaId).find(c => c.id === chapterId)
|
||||
const currentPrefs = {
|
||||
sortMode: get('sortMode'), sortDir: get('sortDir'),
|
||||
sortMode: seriesState.settings.chapterSortMode, sortDir: seriesState.settings.chapterSortDir,
|
||||
preferredScanlator: get('preferredScanlator') as string,
|
||||
scanlatorFilter, scanlatorBlacklist, scanlatorForce,
|
||||
}
|
||||
@@ -310,7 +318,7 @@
|
||||
seriesState.patchChapters(mangaId, chaps => chaps.map(c => idSet.has(c.id) ? { ...c, read: isRead } : c))
|
||||
checkAndMarkCompleted(mangaId, seriesState.chaptersFor(mangaId))
|
||||
const currentPrefs = {
|
||||
sortMode: get('sortMode'), sortDir: get('sortDir'),
|
||||
sortMode: seriesState.settings.chapterSortMode, sortDir: seriesState.settings.chapterSortDir,
|
||||
preferredScanlator: get('preferredScanlator') as string,
|
||||
scanlatorFilter, scanlatorBlacklist, scanlatorForce,
|
||||
}
|
||||
@@ -431,21 +439,8 @@
|
||||
openReaderForChapter(ch, manga)
|
||||
}
|
||||
|
||||
function handleContinue(cc: typeof continueChapter) {
|
||||
if (!cc) return
|
||||
if (cc.type === 'continue' && cc.resumePage && cc.resumePage > 1) {
|
||||
const existing = seriesState.bookmarks.find(b => b.chapterId === cc.chapter.id)
|
||||
if (!existing || existing.pageNumber < cc.resumePage) {
|
||||
addBookmark({
|
||||
mangaId,
|
||||
mangaTitle: manga!.title,
|
||||
thumbnailUrl: manga!.thumbnailUrl,
|
||||
chapterId: cc.chapter.id,
|
||||
chapterName: cc.chapter.name,
|
||||
pageNumber: cc.resumePage,
|
||||
})
|
||||
}
|
||||
}
|
||||
interface ContinueChapter { chapter: Chapter; type: 'start' | 'continue' | 'reread'; resumePage: number | null }
|
||||
function handleContinue(cc: ContinueChapter) {
|
||||
openReaderForChapter(cc.chapter, manga)
|
||||
}
|
||||
|
||||
@@ -472,7 +467,7 @@
|
||||
async function toggleCategory(cat: Category) {
|
||||
const inCat = mangaCategories.some(c => c.id === cat.id)
|
||||
try {
|
||||
await updateMangaCategories(mangaId, inCat ? [] : [cat.id], inCat ? [cat.id] : [])
|
||||
await updateMangaCategories(String(mangaId), inCat ? [] : [cat.id], inCat ? [cat.id] : [])
|
||||
if (!inCat && !manga?.inLibrary) {
|
||||
await updateManga(mangaId, { inLibrary: true }).catch(console.error)
|
||||
if (manga) { manga = { ...manga, inLibrary: true }; seriesState.setActiveManga(manga) }
|
||||
@@ -485,7 +480,7 @@
|
||||
if (!name) return
|
||||
try {
|
||||
const cat = await createCategoryReq(name)
|
||||
await updateMangaCategories(mangaId, [cat.id], [])
|
||||
await updateMangaCategories(String(mangaId), [cat.id], [])
|
||||
if (!manga?.inLibrary) {
|
||||
await updateManga(mangaId, { inLibrary: true }).catch(console.error)
|
||||
if (manga) { manga = { ...manga, inLibrary: true }; seriesState.setActiveManga(manga) }
|
||||
@@ -514,7 +509,7 @@
|
||||
{loadingLinkList}
|
||||
{mangaCategories}
|
||||
{togglingLibrary}
|
||||
onRead={handleContinue}
|
||||
onRead={(ch) => handleContinue(ch)}
|
||||
onToggleLibrary={toggleLibrary}
|
||||
onDeleteAll={deleteAllDownloads}
|
||||
onMigrateOpen={() => migrateOpen = true}
|
||||
@@ -526,15 +521,13 @@
|
||||
onGenreClick={(genre) => goto(`/browse?genre=${encodeURIComponent(genre)}`)}
|
||||
/>
|
||||
|
||||
<div class="list-wrap">
|
||||
<div class="list-wrap" bind:this={chapterListEl}>
|
||||
<SeriesActions
|
||||
{chapters}
|
||||
{sortedChapters}
|
||||
sortMode={get('sortMode')}
|
||||
sortDir={get('sortDir')}
|
||||
sortMode={seriesState.settings.chapterSortMode}
|
||||
sortDir={seriesState.settings.chapterSortDir}
|
||||
{viewMode}
|
||||
{chapterPage}
|
||||
{totalPages}
|
||||
{downloadedCount}
|
||||
{totalCount}
|
||||
{deletingAll}
|
||||
@@ -564,8 +557,8 @@
|
||||
onSetScanlatorFilter={(v) => set('scanlatorFilter', v)}
|
||||
onSetScanlatorBlacklist={(v) => set('scanlatorBlacklist', v)}
|
||||
onSetScanlatorForce={(v) => set('scanlatorForce', v)}
|
||||
onSortModeChange={(v) => set('sortMode', v)}
|
||||
onSortDirChange={(v) => set('sortDir', v)}
|
||||
onSortModeChange={(v) => updateSettings({ chapterSortMode: v })}
|
||||
onSortDirChange={(v) => updateSettings({ chapterSortDir: v })}
|
||||
onOpenFolder={() => manga && openMangaFolder(manga)}
|
||||
/>
|
||||
|
||||
@@ -578,12 +571,12 @@
|
||||
{enqueueing}
|
||||
{chapterPage}
|
||||
{totalPages}
|
||||
bind:scrollEl={chapterListEl}
|
||||
onOpen={openReaderWithAhead}
|
||||
onToggleSelect={toggleSelect}
|
||||
onEnqueue={enqueue}
|
||||
onDeleteDownload={deleteDownloaded}
|
||||
onPageChange={(p) => chapterPage = p}
|
||||
onPageSizeChange={(n) => { chaptersPerPage = n; chapterPage = Math.min(chapterPage, Math.ceil(sortedChapters.length / n) || 1) }}
|
||||
{buildCtxItems}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -163,7 +163,7 @@
|
||||
<Play size={12} weight="fill" />
|
||||
{continueChapter.type === 'reread' ? 'Read again'
|
||||
: continueChapter.type === 'start' ? 'Start reading'
|
||||
: `Continue · Ch.${continueChapter.chapter.chapterNumber}${continueChapter.resumePage ? ` p.${continueChapter.resumePage}` : ''}`}
|
||||
: `Continue · Ch.${continueChapter.chapter.chapterNumber}`}
|
||||
</button>
|
||||
{/if}
|
||||
<div class="actions">
|
||||
|
||||
@@ -1,19 +0,0 @@
|
||||
import { settingsState, updateSettings } from '$lib/state/settings.svelte'
|
||||
import { DEFAULT_MANGA_PREFS } from '$lib/types/settings'
|
||||
import type { MangaPrefs } from '$lib/types/settings'
|
||||
|
||||
export { DEFAULT_MANGA_PREFS } from '$lib/types/settings'
|
||||
|
||||
export function getPref<K extends keyof MangaPrefs>(mangaId: number, key: K): MangaPrefs[K] {
|
||||
const prefs = settingsState.settings.mangaPrefs?.[mangaId] ?? {}
|
||||
return (prefs[key] ?? DEFAULT_MANGA_PREFS[key]) as MangaPrefs[K]
|
||||
}
|
||||
|
||||
export function setPref<K extends keyof MangaPrefs>(mangaId: number, key: K, value: MangaPrefs[K]) {
|
||||
updateSettings({
|
||||
mangaPrefs: {
|
||||
...settingsState.settings.mangaPrefs,
|
||||
[mangaId]: { ...(settingsState.settings.mangaPrefs?.[mangaId] ?? {}), [key]: value },
|
||||
},
|
||||
})
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
<script lang="ts">
|
||||
import { X } from "phosphor-svelte";
|
||||
import { getPref, setPref } from "$lib/components/series/lib/mangaPrefs";
|
||||
import { getPref, setPref } from "$lib/state/series.svelte";
|
||||
import { settingsState } from "$lib/state/settings.svelte";
|
||||
import { libraryState } from "$lib/state/library.svelte";
|
||||
import { resolvedCover } from "$lib/core/cover/coverResolver";
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<script lang="ts">
|
||||
import { X, CaretLeft, CaretRight, CircleNotch } from "phosphor-svelte";
|
||||
import { setPref } from "$lib/components/series/lib/mangaPrefs";
|
||||
import { setPref } from "$lib/state/series.svelte";
|
||||
import { coverCandidatesSync, dedupeByImage } from "$lib/core/cover/coverResolver";
|
||||
import Thumbnail from "$lib/components/shared/manga/Thumbnail.svelte";
|
||||
import type { Manga } from "$lib/types";
|
||||
|
||||
Reference in New Issue
Block a user