From 3d6b6430ed5a509971a395355990afd12818e57f Mon Sep 17 00:00:00 2001 From: Zerebos Date: Sat, 23 May 2026 16:21:09 -0400 Subject: [PATCH] Reader route migration --- src/lib/core/reader/session.ts | 101 ++++ src/lib/request-manager/chapters.ts | 62 ++- src/lib/server-adapters/moku/index.ts | 61 +-- src/lib/server-adapters/suwayomi/index.ts | 183 +++---- src/lib/server-adapters/types.ts | 111 ++--- .../[mangaId]/[chapterId]/+layout.svelte | 18 + .../reader/[mangaId]/[chapterId]/+layout.ts | 2 + .../reader/[mangaId]/[chapterId]/+page.svelte | 455 ++++++++++++++++++ src/routes/series/[mangaId]/+page.ts | 86 ++-- 9 files changed, 846 insertions(+), 233 deletions(-) create mode 100644 src/lib/core/reader/session.ts create mode 100644 src/routes/reader/[mangaId]/[chapterId]/+layout.svelte create mode 100644 src/routes/reader/[mangaId]/[chapterId]/+layout.ts create mode 100644 src/routes/reader/[mangaId]/[chapterId]/+page.svelte diff --git a/src/lib/core/reader/session.ts b/src/lib/core/reader/session.ts new file mode 100644 index 0000000..a802a35 --- /dev/null +++ b/src/lib/core/reader/session.ts @@ -0,0 +1,101 @@ +import {getAdapter} from '$lib/request-manager'; +import {loadChapterPages, updateProgress} from '$lib/request-manager/chapters'; +import {readerState} from '$lib/state/reader.svelte'; +import type {Chapter} from '$lib/types'; + +function sortChapters(chapters: Chapter[]): Chapter[] { + return [...chapters].sort((left, right) => left.sourceOrder - right.sourceOrder); +} + +function currentChapterIndex(): number { + if (!readerState.chapter) return -1; + + return sortChapters(readerState.chapters).findIndex( + (chapter) => String(chapter.id) === String(readerState.chapter?.id) + ); +} + +function clampPageIndex(index: number): number { + if (readerState.pages.length === 0) return 0; + return Math.min(Math.max(index, 0), readerState.pages.length - 1); +} + +export function getAdjacentChapters() { + const chapters = sortChapters(readerState.chapters); + const index = currentChapterIndex(); + + return { + previous: index > 0 ? chapters[index - 1] : null, + next: index >= 0 && index < chapters.length - 1 ? chapters[index + 1] : null, + }; +} + +export async function ensureReaderSession(mangaId: string, chapterId: string) { + const adapter = getAdapter(); + + const mangaPromise = + readerState.manga && String(readerState.manga.id) === mangaId + ? Promise.resolve(readerState.manga) + : adapter.getManga(mangaId); + + const chaptersPromise = + readerState.chapters.length > 0 && String(readerState.chapters[0]?.mangaId) === mangaId + ? Promise.resolve(readerState.chapters) + : adapter.getChapters(mangaId); + + const [manga, chapters] = await Promise.all([mangaPromise, chaptersPromise]); + const chapter = + chapters.find((entry) => String(entry.id) === chapterId) ?? + (String(readerState.chapter?.id) === chapterId ? readerState.chapter : null) ?? + (await adapter.getChapter(chapterId)); + + readerState.manga = manga; + readerState.chapters = chapters; + readerState.chapter = chapter; + readerState.pages = []; + readerState.currentPage = 0; + readerState.pagesError = null; + + await loadChapterPages(chapterId); + + if (readerState.pages.length > 0) { + readerState.currentPage = clampPageIndex((chapter.lastPageRead ?? 1) - 1); + } +} + +export async function setCurrentReaderPage(index: number) { + const nextIndex = clampPageIndex(index); + readerState.currentPage = nextIndex; + + if (!readerState.chapter || readerState.pages.length === 0) return; + + const lastPageRead = nextIndex + 1; + const completed = lastPageRead >= readerState.pages.length; + + if ( + readerState.chapter.lastPageRead === lastPageRead && + readerState.chapter.read === completed + ) { + return; + } + + try { + await updateProgress(String(readerState.chapter.id), lastPageRead, completed); + } catch (error) { + readerState.pagesError = error instanceof Error ? error.message : String(error); + } +} + +export async function goToNextReaderPage(): Promise { + if (readerState.currentPage >= readerState.pages.length - 1) return false; + + await setCurrentReaderPage(readerState.currentPage + 1); + return true; +} + +export async function goToPreviousReaderPage(): Promise { + if (readerState.currentPage <= 0) return false; + + await setCurrentReaderPage(readerState.currentPage - 1); + return true; +} \ No newline at end of file diff --git a/src/lib/request-manager/chapters.ts b/src/lib/request-manager/chapters.ts index bd25178..64a347b 100644 --- a/src/lib/request-manager/chapters.ts +++ b/src/lib/request-manager/chapters.ts @@ -1,40 +1,66 @@ -import { getAdapter } from '$lib/request-manager' -import { seriesState } from '$lib/state/series.svelte' -import { readerState } from '$lib/state/reader.svelte' +import {getAdapter} from '$lib/request-manager'; +import {seriesState} from '$lib/state/series.svelte'; +import {readerState} from '$lib/state/reader.svelte'; export async function loadChapters(mangaId: string) { - seriesState.chaptersLoading = true - seriesState.chaptersError = null + seriesState.chaptersLoading = true; + seriesState.chaptersError = null; try { - seriesState.chapters = await getAdapter().getChapters(mangaId) + seriesState.chapters = await getAdapter().getChapters(mangaId); } catch (e) { - seriesState.chaptersError = String(e) + seriesState.chaptersError = String(e); } finally { - seriesState.chaptersLoading = false + seriesState.chaptersLoading = false; } } export async function loadChapterPages(chapterId: string) { - readerState.pagesLoading = true - readerState.pagesError = null + readerState.pagesLoading = true; + readerState.pagesError = null; try { - readerState.pages = await getAdapter().getChapterPages(chapterId) + readerState.pages = await getAdapter().getChapterPages(chapterId); } catch (e) { - readerState.pagesError = String(e) + readerState.pagesError = String(e); } finally { - readerState.pagesLoading = false + readerState.pagesLoading = false; + } +} + +export async function updateProgress(chapterId: string, lastPageRead: number, read = false) { + await getAdapter().updateChapterProgress(chapterId, lastPageRead, read); + + const chapterIds = new Set([chapterId]); + const nextRead = read || false; + + for (const chapter of seriesState.chapters) { + if (chapterIds.has(String(chapter.id))) { + chapter.lastPageRead = lastPageRead; + chapter.read = nextRead; + } + } + + for (const chapter of readerState.chapters) { + if (chapterIds.has(String(chapter.id))) { + chapter.lastPageRead = lastPageRead; + chapter.read = nextRead; + } + } + + if (readerState.chapter && String(readerState.chapter.id) === chapterId) { + readerState.chapter.lastPageRead = lastPageRead; + readerState.chapter.read = nextRead; } } export async function markRead(id: string, read: boolean) { - await getAdapter().markChapterRead(id, read) - const chapter = seriesState.chapters.find(c => c.id === id) - if (chapter) chapter.read = read + await getAdapter().markChapterRead(id, read); + const chapter = seriesState.chapters.find(c => c.id === id); + if (chapter) chapter.read = read; } export async function markManyRead(ids: string[], read: boolean) { - await getAdapter().markChaptersRead(ids, read) + await getAdapter().markChaptersRead(ids, read); for (const c of seriesState.chapters) { - if (ids.includes(c.id)) c.read = read + if (ids.includes(c.id)) c.read = read; } } diff --git a/src/lib/server-adapters/moku/index.ts b/src/lib/server-adapters/moku/index.ts index f74bdf7..1172f1f 100644 --- a/src/lib/server-adapters/moku/index.ts +++ b/src/lib/server-adapters/moku/index.ts @@ -8,46 +8,47 @@ import type { Page, DownloadItem, UpdateResult, -} from '$lib/server-adapters/types' -import type { Manga, Chapter, Extension, Source, Tracker } from '$lib/types' +} from '$lib/server-adapters/types'; +import type {Manga, Chapter, Extension, Source, Tracker} from '$lib/types'; function notImplemented(): never { - throw new Error('MokuAdapter: not implemented') + throw new Error('MokuAdapter: not implemented'); } export class MokuAdapter implements ServerAdapter { - async connect(_config: ServerConfig): Promise { notImplemented() } - async getStatus(): Promise { return notImplemented() } + async connect(_config: ServerConfig): Promise {notImplemented();} + async getStatus(): Promise {return notImplemented();} - async getManga(_id: string): Promise { return notImplemented() } - async getMangaList(_filters: MangaFilters): Promise> { return notImplemented() } - async searchManga(_query: string, _sourceId?: string): Promise { return notImplemented() } - async addToLibrary(_mangaId: string): Promise { notImplemented() } - async removeFromLibrary(_mangaId: string): Promise { notImplemented() } - async updateMangaMeta(_id: string, _meta: Partial): Promise { notImplemented() } + async getManga(_id: string): Promise {return notImplemented();} + async getMangaList(_filters: MangaFilters): Promise> {return notImplemented();} + async searchManga(_query: string, _sourceId?: string): Promise {return notImplemented();} + async addToLibrary(_mangaId: string): Promise {notImplemented();} + async removeFromLibrary(_mangaId: string): Promise {notImplemented();} + async updateMangaMeta(_id: string, _meta: Partial): Promise {notImplemented();} - async getChapters(_mangaId: string): Promise { return notImplemented() } - async getChapter(_id: string): Promise { return notImplemented() } - async getChapterPages(_id: string): Promise { return notImplemented() } - async markChapterRead(_id: string, _read: boolean): Promise { notImplemented() } - async markChaptersRead(_ids: string[], _read: boolean): Promise { notImplemented() } + async getChapters(_mangaId: string): Promise {return notImplemented();} + async getChapter(_id: string): Promise {return notImplemented();} + async getChapterPages(_id: string): Promise {return notImplemented();} + async markChapterRead(_id: string, _read: boolean): Promise {notImplemented();} + async updateChapterProgress(_id: string, _lastPageRead: number, _read?: boolean): Promise {notImplemented();} + async markChaptersRead(_ids: string[], _read: boolean): Promise {notImplemented();} - async getDownloads(): Promise { return notImplemented() } - async enqueueDownload(_chapterId: string): Promise { notImplemented() } - async dequeueDownload(_chapterId: string): Promise { notImplemented() } - async clearDownloads(): Promise { notImplemented() } + async getDownloads(): Promise {return notImplemented();} + async enqueueDownload(_chapterId: string): Promise {notImplemented();} + async dequeueDownload(_chapterId: string): Promise {notImplemented();} + async clearDownloads(): Promise {notImplemented();} - async getExtensions(): Promise { return notImplemented() } - async installExtension(_id: string): Promise { notImplemented() } - async uninstallExtension(_id: string): Promise { notImplemented() } - async updateExtension(_id: string): Promise { notImplemented() } + async getExtensions(): Promise {return notImplemented();} + async installExtension(_id: string): Promise {notImplemented();} + async uninstallExtension(_id: string): Promise {notImplemented();} + async updateExtension(_id: string): Promise {notImplemented();} - async getSources(): Promise { return notImplemented() } - async browseSource(_sourceId: string, _page: number): Promise> { return notImplemented() } + async getSources(): Promise {return notImplemented();} + async browseSource(_sourceId: string, _page: number): Promise> {return notImplemented();} - async getTrackers(): Promise { return notImplemented() } - async linkTracker(_mangaId: string, _trackerId: string, _remoteId: string): Promise { notImplemented() } - async syncTracking(_mangaId: string): Promise { notImplemented() } + async getTrackers(): Promise {return notImplemented();} + async linkTracker(_mangaId: string, _trackerId: string, _remoteId: string): Promise {notImplemented();} + async syncTracking(_mangaId: string): Promise {notImplemented();} - async checkForUpdates(_mangaIds?: string[]): Promise { return notImplemented() } + async checkForUpdates(_mangaIds?: string[]): Promise {return notImplemented();} } \ No newline at end of file diff --git a/src/lib/server-adapters/suwayomi/index.ts b/src/lib/server-adapters/suwayomi/index.ts index c6f534e..53743c2 100644 --- a/src/lib/server-adapters/suwayomi/index.ts +++ b/src/lib/server-adapters/suwayomi/index.ts @@ -8,8 +8,8 @@ import type { Page, DownloadItem, UpdateResult, -} from '$lib/server-adapters/types' -import type { Manga, Chapter, Extension, Source, Tracker } from '$lib/types' +} from '$lib/server-adapters/types'; +import type {Manga, Chapter, Extension, Source, Tracker} from '$lib/types'; import { GET_LIBRARY, GET_MANGA, @@ -19,38 +19,39 @@ import { SET_MANGA_META, UPDATE_LIBRARY, FETCH_SOURCE_MANGA, -} from './manga' +} from './manga'; import { GET_CHAPTERS, FETCH_CHAPTERS, FETCH_CHAPTER_PAGES, MARK_CHAPTER_READ, MARK_CHAPTERS_READ, -} from './chapters' + UPDATE_CHAPTERS_PROGRESS, +} from './chapters'; import { GET_DOWNLOAD_STATUS, ENQUEUE_DOWNLOAD, DEQUEUE_DOWNLOAD, CLEAR_DOWNLOADER, -} from './downloads' +} from './downloads'; import { GET_EXTENSIONS, GET_SOURCES, FETCH_EXTENSIONS, UPDATE_EXTENSION, -} from './extensions' +} from './extensions'; import { GET_TRACKERS, BIND_TRACK, TRACK_PROGRESS, -} from './tracking' +} from './tracking'; import { GQLResponse, mapManga, mapChapter, mapExtension, mapDownloadItem, -} from './types' +} from './types'; const GET_CHAPTER = ` query GetChapter($id: Int!) { @@ -59,17 +60,17 @@ const GET_CHAPTER = ` pageCount mangaId uploadDate realUrl lastPageRead lastReadAt scanlator } } -` +`; export class SuwayomiAdapter implements ServerAdapter { - private baseUrl = 'http://127.0.0.1:4567' - private authHeader: string | null = null + private baseUrl = 'http://127.0.0.1:4567'; + private authHeader: string | null = null; async connect(config: ServerConfig) { - this.baseUrl = config.baseUrl.replace(/\/$/, '') + this.baseUrl = config.baseUrl.replace(/\/$/, ''); if (config.credentials) { - const { username, password } = config.credentials - this.authHeader = 'Basic ' + btoa(`${username}:${password}`) + const {username, password} = config.credentials; + this.authHeader = 'Basic ' + btoa(`${username}:${password}`); } } @@ -78,154 +79,162 @@ export class SuwayomiAdapter implements ServerAdapter { const res = await fetch(`${this.baseUrl}/api/graphql`, { method: 'POST', headers: this.headers(), - body: JSON.stringify({ query: '{ aboutServer { name } }' }), - }) - return res.ok ? 'connected' : 'error' + body: JSON.stringify({query: '{ aboutServer { name } }'}), + }); + return res.ok ? 'connected' : 'error'; } catch { - return 'disconnected' + return 'disconnected'; } } private headers(): Record { - const h: Record = { 'Content-Type': 'application/json' } - if (this.authHeader) h['Authorization'] = this.authHeader - return h + const h: Record = {'Content-Type': 'application/json'}; + if (this.authHeader) h['Authorization'] = this.authHeader; + return h; } private async gql(query: string, variables?: Record): Promise { const res = await fetch(`${this.baseUrl}/api/graphql`, { method: 'POST', headers: this.headers(), - body: JSON.stringify({ query, variables }), - }) - if (!res.ok) throw new Error(`Suwayomi HTTP ${res.status}`) - const json: GQLResponse = await res.json() - if (json.errors?.length) throw new Error(json.errors[0].message) - return json.data + body: JSON.stringify({query, variables}), + }); + if (!res.ok) throw new Error(`Suwayomi HTTP ${res.status}`); + const json: GQLResponse = await res.json(); + if (json.errors?.length) throw new Error(json.errors[0].message); + return json.data; } async getManga(id: string): Promise { - const data = await this.gql<{ manga: Record }>(GET_MANGA, { id: Number(id) }) - return mapManga(data.manga) + const data = await this.gql<{manga: Record;}>(GET_MANGA, {id: Number(id)}); + return mapManga(data.manga); } async getMangaList(filters: MangaFilters): Promise> { - const data = await this.gql<{ mangas: { nodes: Record[] } }>(GET_LIBRARY) - let items = data.mangas.nodes.map(mapManga) - if (filters.status) items = items.filter(m => m.status === filters.status) - if (filters.tags?.length) items = items.filter(m => filters.tags!.every(t => m.tags?.includes(t))) - if (filters.unread) items = items.filter(m => (m.unreadCount ?? 0) > 0) - if (filters.sourceId) items = items.filter(m => String(m.source?.id) === filters.sourceId) - return { items, hasNextPage: false } + const data = await this.gql<{mangas: {nodes: Record[];};}>(GET_LIBRARY); + let items = data.mangas.nodes.map(mapManga); + if (filters.status) items = items.filter(m => m.status === filters.status); + if (filters.tags?.length) items = items.filter(m => filters.tags!.every(t => m.tags?.includes(t))); + if (filters.unread) items = items.filter(m => (m.unreadCount ?? 0) > 0); + if (filters.sourceId) items = items.filter(m => String(m.source?.id) === filters.sourceId); + return {items, hasNextPage: false}; } async searchManga(query: string, sourceId?: string): Promise { - if (!sourceId) return [] + if (!sourceId) return []; const data = await this.gql<{ - fetchSourceManga: { mangas: Record[] } - }>(FETCH_SOURCE_MANGA, { source: sourceId, type: 'SEARCH', page: 1, query }) - return data.fetchSourceManga.mangas.map(mapManga) + fetchSourceManga: {mangas: Record[];}; + }>(FETCH_SOURCE_MANGA, {source: sourceId, type: 'SEARCH', page: 1, query}); + return data.fetchSourceManga.mangas.map(mapManga); } async addToLibrary(mangaId: string) { - await this.gql(UPDATE_MANGA, { id: Number(mangaId), inLibrary: true }) + await this.gql(UPDATE_MANGA, {id: Number(mangaId), inLibrary: true}); } async removeFromLibrary(mangaId: string) { - await this.gql(UPDATE_MANGA, { id: Number(mangaId), inLibrary: false }) + await this.gql(UPDATE_MANGA, {id: Number(mangaId), inLibrary: false}); } async updateMangaMeta(id: string, meta: Partial) { for (const [key, value] of Object.entries(meta)) { - if (value === undefined) continue - await this.gql(SET_MANGA_META, { mangaId: Number(id), key, value: String(value) }) + if (value === undefined) continue; + await this.gql(SET_MANGA_META, {mangaId: Number(id), key, value: String(value)}); } } async getChapters(mangaId: string): Promise { - const data = await this.gql<{ chapters: { nodes: Record[] } }>( - GET_CHAPTERS, { mangaId: Number(mangaId) } - ) - return data.chapters.nodes.map(mapChapter) + const data = await this.gql<{chapters: {nodes: Record[];};}>( + GET_CHAPTERS, {mangaId: Number(mangaId)} + ); + return data.chapters.nodes.map(mapChapter); } async getChapter(id: string): Promise { - const data = await this.gql<{ chapter: Record }>( - GET_CHAPTER, { id: Number(id) } - ) - return mapChapter(data.chapter) + const data = await this.gql<{chapter: Record;}>( + GET_CHAPTER, {id: Number(id)} + ); + return mapChapter(data.chapter); } async getChapterPages(id: string): Promise { - const data = await this.gql<{ fetchChapterPages: { pages: string[] } }>( - FETCH_CHAPTER_PAGES, { chapterId: Number(id) } - ) - return data.fetchChapterPages.pages.map((url, index) => ({ index, url })) + const data = await this.gql<{fetchChapterPages: {pages: string[];};}>( + FETCH_CHAPTER_PAGES, {chapterId: Number(id)} + ); + return data.fetchChapterPages.pages.map((url, index) => ({index, url})); } async markChapterRead(id: string, read: boolean) { - await this.gql(MARK_CHAPTER_READ, { id: Number(id), isRead: read }) + await this.gql(MARK_CHAPTER_READ, {id: Number(id), isRead: read}); + } + + async updateChapterProgress(id: string, lastPageRead: number, read?: boolean) { + await this.gql(UPDATE_CHAPTERS_PROGRESS, { + ids: [Number(id)], + lastPageRead, + isRead: read, + }); } async markChaptersRead(ids: string[], read: boolean) { - await this.gql(MARK_CHAPTERS_READ, { ids: ids.map(Number), isRead: read }) + await this.gql(MARK_CHAPTERS_READ, {ids: ids.map(Number), isRead: read}); } async getDownloads(): Promise { - const data = await this.gql<{ downloadStatus: { queue: Record[] } }>( + const data = await this.gql<{downloadStatus: {queue: Record[];};}>( GET_DOWNLOAD_STATUS - ) - return data.downloadStatus.queue.map(mapDownloadItem) + ); + return data.downloadStatus.queue.map(mapDownloadItem); } async enqueueDownload(chapterId: string) { - await this.gql(ENQUEUE_DOWNLOAD, { chapterId: Number(chapterId) }) + await this.gql(ENQUEUE_DOWNLOAD, {chapterId: Number(chapterId)}); } async dequeueDownload(chapterId: string) { - await this.gql(DEQUEUE_DOWNLOAD, { chapterId: Number(chapterId) }) + await this.gql(DEQUEUE_DOWNLOAD, {chapterId: Number(chapterId)}); } async clearDownloads() { - await this.gql(CLEAR_DOWNLOADER) + await this.gql(CLEAR_DOWNLOADER); } async getExtensions(): Promise { - await this.gql(FETCH_EXTENSIONS) - const data = await this.gql<{ extensions: { nodes: Record[] } }>(GET_EXTENSIONS) - return data.extensions.nodes.map(mapExtension) + await this.gql(FETCH_EXTENSIONS); + const data = await this.gql<{extensions: {nodes: Record[];};}>(GET_EXTENSIONS); + return data.extensions.nodes.map(mapExtension); } async installExtension(id: string) { - await this.gql(UPDATE_EXTENSION, { id, install: true }) + await this.gql(UPDATE_EXTENSION, {id, install: true}); } async uninstallExtension(id: string) { - await this.gql(UPDATE_EXTENSION, { id, uninstall: true }) + await this.gql(UPDATE_EXTENSION, {id, uninstall: true}); } async updateExtension(id: string) { - await this.gql(UPDATE_EXTENSION, { id, update: true }) + await this.gql(UPDATE_EXTENSION, {id, update: true}); } async getSources(): Promise { - const data = await this.gql<{ sources: { nodes: Source[] } }>(GET_SOURCES) - return data.sources.nodes + const data = await this.gql<{sources: {nodes: Source[];};}>(GET_SOURCES); + return data.sources.nodes; } async browseSource(sourceId: string, page: number): Promise> { const data = await this.gql<{ - fetchSourceManga: { mangas: Record[]; hasNextPage: boolean } - }>(FETCH_SOURCE_MANGA, { source: sourceId, type: 'LATEST', page }) + fetchSourceManga: {mangas: Record[]; hasNextPage: boolean;}; + }>(FETCH_SOURCE_MANGA, {source: sourceId, type: 'LATEST', page}); return { items: data.fetchSourceManga.mangas.map(mapManga), hasNextPage: data.fetchSourceManga.hasNextPage, - } + }; } async getTrackers(): Promise { - const data = await this.gql<{ trackers: { nodes: Tracker[] } }>(GET_TRACKERS) - return data.trackers.nodes + const data = await this.gql<{trackers: {nodes: Tracker[];};}>(GET_TRACKERS); + return data.trackers.nodes; } async linkTracker(mangaId: string, trackerId: string, remoteId: string) { @@ -233,25 +242,25 @@ export class SuwayomiAdapter implements ServerAdapter { mangaId: Number(mangaId), trackerId: Number(trackerId), remoteId, - }) + }); } async syncTracking(mangaId: string) { - await this.gql(TRACK_PROGRESS, { mangaId: Number(mangaId) }) + await this.gql(TRACK_PROGRESS, {mangaId: Number(mangaId)}); } async checkForUpdates(mangaIds?: string[]): Promise { if (mangaIds?.length) { - const results: UpdateResult[] = [] + const results: UpdateResult[] = []; for (const id of mangaIds) { - const before = await this.getChapters(id) - await this.gql(FETCH_CHAPTERS, { mangaId: Number(id) }) - const after = await this.getChapters(id) - results.push({ mangaId: id, newChapters: after.length - before.length }) + const before = await this.getChapters(id); + await this.gql(FETCH_CHAPTERS, {mangaId: Number(id)}); + const after = await this.getChapters(id); + results.push({mangaId: id, newChapters: after.length - before.length}); } - return results + return results; } - await this.gql(UPDATE_LIBRARY) - return [] + await this.gql(UPDATE_LIBRARY); + return []; } } \ No newline at end of file diff --git a/src/lib/server-adapters/types.ts b/src/lib/server-adapters/types.ts index 75d8865..00bd74a 100644 --- a/src/lib/server-adapters/types.ts +++ b/src/lib/server-adapters/types.ts @@ -4,91 +4,92 @@ import type { Extension, Source, Tracker, -} from '$lib/types' +} from '$lib/types'; export interface ServerConfig { - baseUrl: string - credentials?: { username: string; password: string } + baseUrl: string; + credentials?: {username: string; password: string;}; } -export type ServerStatus = 'connected' | 'disconnected' | 'error' +export type ServerStatus = 'connected' | 'disconnected' | 'error'; export interface MangaFilters { - inLibrary?: boolean - status?: MangaStatus - tags?: string[] - unread?: boolean - sourceId?: string + inLibrary?: boolean; + status?: MangaStatus; + tags?: string[]; + unread?: boolean; + sourceId?: string; } -export type MangaStatus = 'ONGOING' | 'COMPLETED' | 'LICENSED' | 'PUBLISHING_FINISHED' | 'CANCELLED' | 'ON_HIATUS' +export type MangaStatus = 'ONGOING' | 'COMPLETED' | 'LICENSED' | 'PUBLISHING_FINISHED' | 'CANCELLED' | 'ON_HIATUS'; export interface PaginatedResult { - items: T[] - hasNextPage: boolean - total?: number + items: T[]; + hasNextPage: boolean; + total?: number; } export interface MangaMeta { - customTitle?: string - customCover?: string - notes?: string - [key: string]: unknown + customTitle?: string; + customCover?: string; + notes?: string; + [key: string]: unknown; } export interface Page { - index: number - url: string - imageData?: string + index: number; + url: string; + imageData?: string; } export interface DownloadItem { - chapterId: string - mangaId: string - chapterName: string - mangaTitle: string - progress: number - state: 'queued' | 'downloading' | 'finished' | 'error' + chapterId: string; + mangaId: string; + chapterName: string; + mangaTitle: string; + progress: number; + state: 'queued' | 'downloading' | 'finished' | 'error'; } export interface UpdateResult { - mangaId: string - newChapters: number + mangaId: string; + newChapters: number; } export interface ServerAdapter { - connect(config: ServerConfig): Promise - getStatus(): Promise + connect(config: ServerConfig): Promise; + getStatus(): Promise; - getManga(id: string): Promise - getMangaList(filters: MangaFilters): Promise> - searchManga(query: string, sourceId?: string): Promise - addToLibrary(mangaId: string): Promise - removeFromLibrary(mangaId: string): Promise - updateMangaMeta(id: string, meta: Partial): Promise + getManga(id: string): Promise; + getMangaList(filters: MangaFilters): Promise>; + searchManga(query: string, sourceId?: string): Promise; + addToLibrary(mangaId: string): Promise; + removeFromLibrary(mangaId: string): Promise; + updateMangaMeta(id: string, meta: Partial): Promise; - getChapters(mangaId: string): Promise - getChapter(id: string): Promise - getChapterPages(id: string): Promise - markChapterRead(id: string, read: boolean): Promise - markChaptersRead(ids: string[], read: boolean): Promise + getChapters(mangaId: string): Promise; + getChapter(id: string): Promise; + getChapterPages(id: string): Promise; + markChapterRead(id: string, read: boolean): Promise; + updateChapterProgress(id: string, lastPageRead: number, read?: boolean): Promise; + markChaptersRead(ids: string[], read: boolean): Promise; - getDownloads(): Promise - enqueueDownload(chapterId: string): Promise - dequeueDownload(chapterId: string): Promise - clearDownloads(): Promise + getDownloads(): Promise; + enqueueDownload(chapterId: string): Promise; + dequeueDownload(chapterId: string): Promise; + clearDownloads(): Promise; - getExtensions(): Promise - installExtension(id: string): Promise - uninstallExtension(id: string): Promise - updateExtension(id: string): Promise + getExtensions(): Promise; + installExtension(id: string): Promise; + uninstallExtension(id: string): Promise; + updateExtension(id: string): Promise; - getSources(): Promise - browseSource(sourceId: string, page: number): Promise> + getSources(): Promise; + browseSource(sourceId: string, page: number): Promise>; - getTrackers(): Promise - linkTracker(mangaId: string, trackerId: string, remoteId: string): Promise - syncTracking(mangaId: string): Promise + getTrackers(): Promise; + linkTracker(mangaId: string, trackerId: string, remoteId: string): Promise; + syncTracking(mangaId: string): Promise; - checkForUpdates(mangaIds?: string[]): Promise + checkForUpdates(mangaIds?: string[]): Promise; } diff --git a/src/routes/reader/[mangaId]/[chapterId]/+layout.svelte b/src/routes/reader/[mangaId]/[chapterId]/+layout.svelte new file mode 100644 index 0000000..0bcea35 --- /dev/null +++ b/src/routes/reader/[mangaId]/[chapterId]/+layout.svelte @@ -0,0 +1,18 @@ + + +
+ {@render children()} +
+ + \ No newline at end of file diff --git a/src/routes/reader/[mangaId]/[chapterId]/+layout.ts b/src/routes/reader/[mangaId]/[chapterId]/+layout.ts new file mode 100644 index 0000000..bf2831b --- /dev/null +++ b/src/routes/reader/[mangaId]/[chapterId]/+layout.ts @@ -0,0 +1,2 @@ +export const ssr = false; +export const prerender = false; \ No newline at end of file diff --git a/src/routes/reader/[mangaId]/[chapterId]/+page.svelte b/src/routes/reader/[mangaId]/[chapterId]/+page.svelte new file mode 100644 index 0000000..8fc8736 --- /dev/null +++ b/src/routes/reader/[mangaId]/[chapterId]/+page.svelte @@ -0,0 +1,455 @@ + + + + +
+
+
+ + +
+

{readerState.manga?.title ?? 'Reader'}

+

{readerState.chapter?.name ?? 'Loading chapter'}

+

{chapterLabel} ยท {pageLabel}

+
+
+ +
+
+ + +
+ + +
+
+ +
+
+ {progressPercent}% read + {pageLabel} +
+ +
+ +
+ {#if initializing && readerState.pages.length === 0} +
+ +

Loading chapter pages...

+
+ {:else if routeError || readerState.pagesError} +
+

{routeError ?? readerState.pagesError}

+ +
+ {:else if totalPages === 0} +
+

No pages were returned for this chapter.

+
+ {:else if readerState.mode === 'strip'} +
+ {#each readerState.pages as pageData, index (pageData.index)} + + {/each} +
+ {:else} +
+ + + {#if currentPageData} + {`Page + {/if} + + +
+ {/if} +
+ +
+ + + +
+
+ + \ No newline at end of file diff --git a/src/routes/series/[mangaId]/+page.ts b/src/routes/series/[mangaId]/+page.ts index a6135ef..7979c8a 100644 --- a/src/routes/series/[mangaId]/+page.ts +++ b/src/routes/series/[mangaId]/+page.ts @@ -1,48 +1,48 @@ -import { error } from '@sveltejs/kit' -import type { PageLoad } from './$types' -import { getAdapter } from '$lib/request-manager' -import { seriesState } from '$lib/state/series.svelte' -import { readerState } from '$lib/state/reader.svelte' +import {error} from '@sveltejs/kit'; +import type {PageLoad} from './$types'; +import {getAdapter} from '$lib/request-manager'; +import {seriesState} from '$lib/state/series.svelte'; +import {readerState} from '$lib/state/reader.svelte'; -export const load: PageLoad = async ({ params }) => { - const mangaId = params.mangaId +export const load: PageLoad = async ({params}) => { + const mangaId = params.mangaId; - if (!mangaId) { - throw error(400, 'Missing manga id') - } - - try { - seriesState.loading = true - seriesState.error = null - seriesState.chaptersLoading = true - seriesState.chaptersError = null - - const adapter = getAdapter() - const [manga, chapters] = await Promise.all([ - adapter.getManga(mangaId), - adapter.getChapters(mangaId), - ]) - - seriesState.current = manga - seriesState.chapters = chapters - - readerState.manga = manga - readerState.chapters = chapters - - return { - manga, - chapters, - mangaId, + if (!mangaId) { + throw error(400, 'Missing manga id'); } - } catch (err) { - const message = err instanceof Error ? err.message : String(err) - seriesState.error = message - seriesState.chaptersError = message + try { + seriesState.loading = true; + seriesState.error = null; + seriesState.chaptersLoading = true; + seriesState.chaptersError = null; - throw error(500, message) - } finally { - seriesState.loading = false - seriesState.chaptersLoading = false - } -} \ No newline at end of file + const adapter = getAdapter(); + const [manga, chapters] = await Promise.all([ + adapter.getManga(mangaId), + adapter.getChapters(mangaId), + ]); + + seriesState.current = manga; + seriesState.chapters = chapters; + + readerState.manga = manga; + readerState.chapters = chapters; + + return { + manga, + chapters, + mangaId, + }; + } catch (err) { + const message = err instanceof Error ? err.message : String(err); + + seriesState.error = message; + seriesState.chaptersError = message; + + throw error(500, message); + } finally { + seriesState.loading = false; + seriesState.chaptersLoading = false; + } +}; \ No newline at end of file