diff --git a/src/app.d.ts b/src/app.d.ts index 1a3a8f8..32fa55b 100644 --- a/src/app.d.ts +++ b/src/app.d.ts @@ -1,4 +1,5 @@ declare global { namespace App {} + const __APP_VERSION__: string } -export {}; \ No newline at end of file +export {} \ No newline at end of file diff --git a/src/lib/platform-adapters/capacitor/index.ts b/src/lib/platform-adapters/capacitor/index.ts new file mode 100644 index 0000000..2789f06 --- /dev/null +++ b/src/lib/platform-adapters/capacitor/index.ts @@ -0,0 +1,93 @@ +import type { + PlatformAdapter, + PlatformFeature, + ServerLaunchConfig, + DiscordPresence, + AppUpdateInfo, +} from '$lib/platform-adapters/types' + +export class CapacitorAdapter implements PlatformAdapter { + async init() {} + + isSupported(feature: PlatformFeature): boolean { + const supported: PlatformFeature[] = ['biometric-auth', 'filesystem'] + return supported.includes(feature) + } + + async launchServer(_config: ServerLaunchConfig) {} + async stopServer() {} + async getServerStatus(): Promise<'running' | 'stopped' | 'error'> { + return 'stopped' + } + + async readFile(path: string): Promise { + const { Filesystem, Directory } = await import('@capacitor/filesystem') + const result = await Filesystem.readFile({ path, directory: Directory.Data }) + const base64 = result.data as string + const binary = atob(base64) + const bytes = new Uint8Array(binary.length) + for (let i = 0; i < binary.length; i++) bytes[i] = binary.charCodeAt(i) + return bytes + } + + async writeFile(path: string, data: Uint8Array): Promise { + const { Filesystem, Directory } = await import('@capacitor/filesystem') + const binary = String.fromCharCode(...data) + const base64 = btoa(binary) + await Filesystem.writeFile({ path, data: base64, directory: Directory.Data }) + } + + async pickFolder(): Promise { + return null + } + + async authenticateBiometric(): Promise { + try { + const { NativeBiometric } = await import('capacitor-native-biometric') + await NativeBiometric.verifyIdentity({ reason: 'Authenticate to access Moku', title: 'Biometric Auth' }) + return true + } catch { + return false + } + } + + async storeCredential(key: string, value: string): Promise { + const { NativeBiometric } = await import('capacitor-native-biometric') + await NativeBiometric.setCredentials({ username: key, password: value, server: 'moku' }) + } + + async getCredential(key: string): Promise { + try { + const { NativeBiometric } = await import('capacitor-native-biometric') + const result = await NativeBiometric.getCredentials({ server: 'moku' }) + return result.username === key ? result.password : null + } catch { + return null + } + } + + async setTitle(_title: string) {} + async minimize() {} + async maximize() {} + async close() {} + + async setDiscordPresence(_presence: DiscordPresence) {} + async clearDiscordPresence() {} + + async getVersion(): Promise { + const { App } = await import('@capacitor/app') + const info = await App.getInfo() + return info.version + } + + async openExternal(url: string): Promise { + const { Browser } = await import('@capacitor/browser') + await Browser.open({ url }) + } + + async checkForAppUpdate(): Promise { + return null + } + + async installAppUpdate(): Promise {} +} \ No newline at end of file diff --git a/src/lib/platform-adapters/tauri/index.ts b/src/lib/platform-adapters/tauri/index.ts new file mode 100644 index 0000000..5c30343 --- /dev/null +++ b/src/lib/platform-adapters/tauri/index.ts @@ -0,0 +1,119 @@ +import { invoke } from '@tauri-apps/api/core' +import { open } from '@tauri-apps/plugin-dialog' +import { readFile, writeFile } from '@tauri-apps/plugin-fs' +import { open as openUrl } from '@tauri-apps/plugin-shell' +import { getVersion } from '@tauri-apps/api/app' +import { check } from '@tauri-apps/plugin-updater' +import { relaunch } from '@tauri-apps/plugin-process' +import type { + PlatformAdapter, + PlatformFeature, + ServerLaunchConfig, + DiscordPresence, + AppUpdateInfo, +} from '$lib/platform-adapters/types' + +export class TauriAdapter implements PlatformAdapter { + async init() { + await invoke('init_app') + } + + isSupported(feature: PlatformFeature): boolean { + const supported: PlatformFeature[] = [ + 'server-management', + 'biometric-auth', + 'native-window', + 'filesystem', + 'app-updates', + 'discord-rpc', + ] + return supported.includes(feature) + } + + async launchServer(config: ServerLaunchConfig) { + await invoke('launch_server', { config }) + } + + async stopServer() { + await invoke('stop_server') + } + + async getServerStatus(): Promise<'running' | 'stopped' | 'error'> { + return invoke('get_server_status') + } + + async readFile(path: string): Promise { + return readFile(path) + } + + async writeFile(path: string, data: Uint8Array) { + await writeFile(path, data) + } + + async pickFolder(): Promise { + const result = await open({ directory: true, multiple: false }) + return typeof result === 'string' ? result : null + } + + async authenticateBiometric(): Promise { + return invoke('authenticate_biometric') + } + + async storeCredential(key: string, value: string) { + await invoke('store_credential', { key, value }) + } + + async getCredential(key: string): Promise { + return invoke('get_credential', { key }) + } + + async setTitle(title: string) { + await invoke('set_window_title', { title }) + } + + async minimize() { + await invoke('minimize_window') + } + + async maximize() { + await invoke('maximize_window') + } + + async close() { + await invoke('close_window') + } + + async setDiscordPresence(presence: DiscordPresence) { + await invoke('set_discord_presence', { presence }) + } + + async clearDiscordPresence() { + await invoke('clear_discord_presence') + } + + async getVersion(): Promise { + return getVersion() + } + + async openExternal(url: string) { + await openUrl(url) + } + + async checkForAppUpdate(): Promise { + const update = await check() + if (!update?.available) return null + return { + version: update.version, + url: update.body ?? '', + notes: update.body, + } + } + + async installAppUpdate() { + const update = await check() + if (update?.available) { + await update.downloadAndInstall() + await relaunch() + } + } +} \ No newline at end of file diff --git a/src/lib/platform-adapters/types.ts b/src/lib/platform-adapters/types.ts new file mode 100644 index 0000000..dafb7c4 --- /dev/null +++ b/src/lib/platform-adapters/types.ts @@ -0,0 +1,55 @@ +export type PlatformFeature = + | 'server-management' + | 'biometric-auth' + | 'native-window' + | 'filesystem' + | 'app-updates' + | 'discord-rpc' + +export interface ServerLaunchConfig { + jarPath: string + port: number + dataPath: string +} + +export interface DiscordPresence { + title: string + chapter: string + startTimestamp?: number +} + +export interface AppUpdateInfo { + version: string + url: string + notes?: string +} + +export interface PlatformAdapter { + init(): Promise + isSupported(feature: PlatformFeature): boolean + + launchServer(config: ServerLaunchConfig): Promise + stopServer(): Promise + getServerStatus(): Promise<'running' | 'stopped' | 'error'> + + readFile(path: string): Promise + writeFile(path: string, data: Uint8Array): Promise + pickFolder(): Promise + + authenticateBiometric(): Promise + storeCredential(key: string, value: string): Promise + getCredential(key: string): Promise + + setTitle(title: string): Promise + minimize(): Promise + maximize(): Promise + close(): Promise + + setDiscordPresence(presence: DiscordPresence): Promise + clearDiscordPresence(): Promise + + getVersion(): Promise + openExternal(url: string): Promise + checkForAppUpdate(): Promise + installAppUpdate(): Promise +} \ No newline at end of file diff --git a/src/lib/platform-adapters/web/index.ts b/src/lib/platform-adapters/web/index.ts new file mode 100644 index 0000000..a55aa15 --- /dev/null +++ b/src/lib/platform-adapters/web/index.ts @@ -0,0 +1,66 @@ +import type { + PlatformAdapter, + PlatformFeature, + ServerLaunchConfig, + DiscordPresence, + AppUpdateInfo, +} from '$lib/platform-adapters/types' + +export class WebAdapter implements PlatformAdapter { + async init() {} + + isSupported(_feature: PlatformFeature): boolean { + return false + } + + async launchServer(_config: ServerLaunchConfig) {} + async stopServer() {} + async getServerStatus(): Promise<'running' | 'stopped' | 'error'> { + return 'stopped' + } + + async readFile(_path: string): Promise { + return new Uint8Array() + } + + async writeFile(_path: string, _data: Uint8Array) {} + + async pickFolder(): Promise { + return null + } + + async authenticateBiometric(): Promise { + return false + } + + async storeCredential(_key: string, _value: string) {} + + async getCredential(_key: string): Promise { + return null + } + + async setTitle(title: string) { + document.title = title + } + + async minimize() {} + async maximize() {} + async close() {} + + async setDiscordPresence(_presence: DiscordPresence) {} + async clearDiscordPresence() {} + + async getVersion(): Promise { + return __APP_VERSION__ + } + + async openExternal(url: string) { + window.open(url, '_blank', 'noopener,noreferrer') + } + + async checkForAppUpdate(): Promise { + return null + } + + async installAppUpdate() {} +} \ No newline at end of file diff --git a/src/lib/platform-service/index.ts b/src/lib/platform-service/index.ts new file mode 100644 index 0000000..fc8079b --- /dev/null +++ b/src/lib/platform-service/index.ts @@ -0,0 +1,98 @@ +import type { + PlatformAdapter, + PlatformFeature, + ServerLaunchConfig, + DiscordPresence, + AppUpdateInfo, +} from '$lib/platform-adapters/types' + +let adapter: PlatformAdapter + +export function initPlatformService(a: PlatformAdapter) { + adapter = a +} + +function getAdapter(): PlatformAdapter { + if (!adapter) throw new Error('PlatformService not initialized') + return adapter +} + +export function isSupported(feature: PlatformFeature): boolean { + return getAdapter().isSupported(feature) +} + +export function launchServer(config: ServerLaunchConfig) { + return getAdapter().launchServer(config) +} + +export function stopServer() { + return getAdapter().stopServer() +} + +export function getServerStatus() { + return getAdapter().getServerStatus() +} + +export function readFile(path: string) { + return getAdapter().readFile(path) +} + +export function writeFile(path: string, data: Uint8Array) { + return getAdapter().writeFile(path, data) +} + +export function pickFolder() { + return getAdapter().pickFolder() +} + +export function authenticateBiometric() { + return getAdapter().authenticateBiometric() +} + +export function storeCredential(key: string, value: string) { + return getAdapter().storeCredential(key, value) +} + +export function getCredential(key: string) { + return getAdapter().getCredential(key) +} + +export function setTitle(title: string) { + return getAdapter().setTitle(title) +} + +export function minimize() { + return getAdapter().minimize() +} + +export function maximize() { + return getAdapter().maximize() +} + +export function close() { + return getAdapter().close() +} + +export function setDiscordPresence(presence: DiscordPresence) { + return getAdapter().setDiscordPresence(presence) +} + +export function clearDiscordPresence() { + return getAdapter().clearDiscordPresence() +} + +export function getVersion() { + return getAdapter().getVersion() +} + +export function openExternal(url: string) { + return getAdapter().openExternal(url) +} + +export function checkForAppUpdate(): Promise { + return getAdapter().checkForAppUpdate() +} + +export function installAppUpdate() { + return getAdapter().installAppUpdate() +} \ No newline at end of file diff --git a/src/lib/request-manager/index.ts b/src/lib/request-manager/index.ts new file mode 100644 index 0000000..bdac274 --- /dev/null +++ b/src/lib/request-manager/index.ts @@ -0,0 +1,12 @@ +import type { ServerAdapter } from '$lib/server-adapters/types' + +let adapter: ServerAdapter + +export function initRequestManager(a: ServerAdapter) { + adapter = a +} + +export function getAdapter(): ServerAdapter { + if (!adapter) throw new Error('RequestManager not initialized') + return adapter +} \ No newline at end of file diff --git a/src/lib/request-manager/manga.ts b/src/lib/request-manager/manga.ts index dcbcec2..624488a 100644 --- a/src/lib/request-manager/manga.ts +++ b/src/lib/request-manager/manga.ts @@ -47,12 +47,12 @@ export async function addToLibrary(mangaId: string) { export async function removeFromLibrary(mangaId: string) { await getAdapter().removeFromLibrary(mangaId) - libraryState.items = libraryState.items.filter(m => m.id !== mangaId) + libraryState.items = libraryState.items.filter(m => String(m.id) !== mangaId) } export async function updateMangaMeta(id: string, meta: Partial) { await getAdapter().updateMangaMeta(id, meta) - if (seriesState.current?.id === id) { + if (String(seriesState.current?.id) === id) { await loadManga(id) } -} +} \ No newline at end of file diff --git a/src/lib/server-adapters/moku/index.ts b/src/lib/server-adapters/moku/index.ts new file mode 100644 index 0000000..f74bdf7 --- /dev/null +++ b/src/lib/server-adapters/moku/index.ts @@ -0,0 +1,53 @@ +import type { + ServerAdapter, + ServerConfig, + ServerStatus, + MangaFilters, + MangaMeta, + PaginatedResult, + Page, + DownloadItem, + UpdateResult, +} from '$lib/server-adapters/types' +import type { Manga, Chapter, Extension, Source, Tracker } from '$lib/types' + +function notImplemented(): never { + throw new Error('MokuAdapter: not implemented') +} + +export class MokuAdapter implements ServerAdapter { + 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 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 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 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 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 f4bc504..c6f534e 100644 --- a/src/lib/server-adapters/suwayomi/index.ts +++ b/src/lib/server-adapters/suwayomi/index.ts @@ -10,299 +10,57 @@ import type { UpdateResult, } from '$lib/server-adapters/types' import type { Manga, Chapter, Extension, Source, Tracker } from '$lib/types' +import { + GET_LIBRARY, + GET_MANGA, + GET_CATEGORIES, + FETCH_MANGA, + UPDATE_MANGA, + SET_MANGA_META, + UPDATE_LIBRARY, + FETCH_SOURCE_MANGA, +} from './manga' +import { + GET_CHAPTERS, + FETCH_CHAPTERS, + FETCH_CHAPTER_PAGES, + MARK_CHAPTER_READ, + MARK_CHAPTERS_READ, +} from './chapters' +import { + GET_DOWNLOAD_STATUS, + ENQUEUE_DOWNLOAD, + DEQUEUE_DOWNLOAD, + CLEAR_DOWNLOADER, +} from './downloads' +import { + GET_EXTENSIONS, + GET_SOURCES, + FETCH_EXTENSIONS, + UPDATE_EXTENSION, +} from './extensions' +import { + GET_TRACKERS, + BIND_TRACK, + TRACK_PROGRESS, +} from './tracking' +import { + GQLResponse, + mapManga, + mapChapter, + mapExtension, + mapDownloadItem, +} from './types' -interface GQLResponse { - data: T - errors?: { message: string }[] -} - -const GET_LIBRARY = ` - query GetLibrary { - mangas(condition: { inLibrary: true }) { - nodes { - id title thumbnailUrl inLibrary downloadCount unreadCount bookmarkCount - description status author artist genre inLibraryAt lastFetchedAt - source { id name displayName } - chapters { totalCount } - lastReadChapter { id chapterNumber } - firstUnreadChapter { id chapterNumber } - } +const GET_CHAPTER = ` + query GetChapter($id: Int!) { + chapter(id: $id) { + id name chapterNumber sourceOrder isRead isDownloaded isBookmarked + pageCount mangaId uploadDate realUrl lastPageRead lastReadAt scanlator } } ` -const GET_MANGA = ` - query GetManga($id: Int!) { - manga(id: $id) { - id title description thumbnailUrl status author artist genre inLibrary realUrl - inLibraryAt lastFetchedAt updateStrategy - source { id name displayName } - lastReadChapter { id chapterNumber lastPageRead } - firstUnreadChapter { id chapterNumber } - highestNumberedChapter { id chapterNumber } - } - } -` - -const GET_CHAPTERS = ` - query GetChapters($mangaId: Int!) { - chapters(condition: { mangaId: $mangaId }) { - nodes { - id name chapterNumber sourceOrder isRead isDownloaded isBookmarked - pageCount mangaId uploadDate realUrl lastPageRead lastReadAt scanlator - } - } - } -` - -const GET_DOWNLOAD_STATUS = ` - query GetDownloadStatus { - downloadStatus { - state - queue { - progress state tries - chapter { - id name pageCount mangaId - manga { id title thumbnailUrl } - } - } - } - } -` - -const GET_EXTENSIONS = ` - query GetExtensions { - extensions { - nodes { - apkName pkgName name lang versionName - isInstalled isObsolete hasUpdate iconUrl - } - } - } -` - -const GET_SOURCES = ` - query GetSources { - sources { - nodes { - id name lang displayName iconUrl isNsfw - isConfigurable supportsLatest - } - } - } -` - -const GET_TRACKERS = ` - query GetTrackers { - trackers { - nodes { - id name icon isLoggedIn isTokenExpired authUrl - supportsPrivateTracking supportsReadingDates supportsTrackDeletion - scores - statuses { value name } - } - } - } -` - -const FETCH_MANGA = ` - mutation FetchManga($id: Int!) { - fetchManga(input: { id: $id }) { - manga { - id title description thumbnailUrl status author artist genre inLibrary realUrl - source { id name displayName } - } - } - } -` - -const FETCH_SOURCE_MANGA = ` - mutation FetchSourceManga($source: LongString!, $type: FetchSourceMangaType!, $page: Int!, $query: String) { - fetchSourceManga(input: { source: $source, type: $type, page: $page, query: $query }) { - mangas { id title thumbnailUrl inLibrary } - hasNextPage - } - } -` - -const UPDATE_MANGA = ` - mutation UpdateManga($id: Int!, $inLibrary: Boolean) { - updateManga(input: { id: $id, patch: { inLibrary: $inLibrary } }) { - manga { id inLibrary } - } - } -` - -const SET_MANGA_META = ` - mutation SetMangaMeta($mangaId: Int!, $key: String!, $value: String!) { - setMangaMeta(input: { meta: { mangaId: $mangaId, key: $key, value: $value } }) { - meta { key value } - } - } -` - -const FETCH_CHAPTERS = ` - mutation FetchChapters($mangaId: Int!) { - fetchChapters(input: { mangaId: $mangaId }) { - chapters { - id name chapterNumber sourceOrder isRead isDownloaded isBookmarked - pageCount mangaId uploadDate realUrl lastPageRead lastReadAt scanlator - } - } - } -` - -const FETCH_CHAPTER_PAGES = ` - mutation FetchChapterPages($chapterId: Int!) { - fetchChapterPages(input: { chapterId: $chapterId }) { pages } - } -` - -const MARK_CHAPTER_READ = ` - mutation MarkChapterRead($id: Int!, $isRead: Boolean!) { - updateChapter(input: { id: $id, patch: { isRead: $isRead } }) { - chapter { id isRead } - } - } -` - -const MARK_CHAPTERS_READ = ` - mutation MarkChaptersRead($ids: [Int!]!, $isRead: Boolean!) { - updateChapters(input: { ids: $ids, patch: { isRead: $isRead } }) { - chapters { id isRead } - } - } -` - -const ENQUEUE_DOWNLOAD = ` - mutation EnqueueDownload($chapterId: Int!) { - enqueueChapterDownload(input: { id: $chapterId }) { - downloadStatus { state } - } - } -` - -const DEQUEUE_DOWNLOAD = ` - mutation DequeueDownload($chapterId: Int!) { - dequeueChapterDownload(input: { id: $chapterId }) { - downloadStatus { state } - } - } -` - -const CLEAR_DOWNLOADER = ` - mutation ClearDownloader { - clearDownloader(input: {}) { - downloadStatus { state } - } - } -` - -const FETCH_EXTENSIONS = ` - mutation FetchExtensions { - fetchExtensions(input: {}) { - extensions { - apkName pkgName name lang versionName - isInstalled isObsolete hasUpdate iconUrl - } - } - } -` - -const UPDATE_EXTENSION = ` - mutation UpdateExtension($id: String!, $install: Boolean, $uninstall: Boolean, $update: Boolean) { - updateExtension(input: { id: $id, patch: { install: $install, uninstall: $uninstall, update: $update } }) { - extension { apkName pkgName name isInstalled hasUpdate } - } - } -` - -const BIND_TRACK = ` - mutation BindTrack($mangaId: Int!, $trackerId: Int!, $remoteId: LongString!) { - bindTrack(input: { mangaId: $mangaId, trackerId: $trackerId, remoteId: $remoteId }) { - trackRecord { id trackerId remoteId } - } - } -` - -const TRACK_PROGRESS = ` - mutation TrackProgress($mangaId: Int!) { - trackProgress(input: { mangaId: $mangaId }) { - trackRecords { id trackerId lastChapterRead status } - } - } -` - -const UPDATE_LIBRARY = ` - mutation UpdateLibrary { - updateLibrary(input: {}) { - updateStatus { jobsInfo { isRunning finishedJobs totalJobs } } - } - } -` - -function mapChapter(raw: Record): Chapter { - return { - id: raw.id as number, - name: raw.name as string, - chapterNumber: raw.chapterNumber as number, - sourceOrder: raw.sourceOrder as number, - read: (raw.isRead as boolean) ?? false, - downloaded: (raw.isDownloaded as boolean) ?? false, - bookmarked: (raw.isBookmarked as boolean) ?? false, - pageCount: (raw.pageCount as number) ?? 0, - mangaId: raw.mangaId as number, - fetchedAt: raw.fetchedAt as string | undefined, - uploadDate: raw.uploadDate as string | null | undefined, - realUrl: raw.realUrl as string | null | undefined, - lastPageRead: raw.lastPageRead as number | undefined, - lastReadAt: raw.lastReadAt as string | undefined, - scanlator: raw.scanlator as string | null | undefined, - manga: raw.manga as Chapter['manga'], - } -} - -function mapManga(raw: Record): Manga { - const inLibraryAt = raw.inLibraryAt as string | null | undefined - return { - ...(raw as unknown as Manga), - tags: raw.genre as string[] | undefined, - addedAt: inLibraryAt ? new Date(inLibraryAt).getTime() : undefined, - lastReadAt: raw.lastReadChapter - ? Date.now() - : undefined, - } -} - -function mapExtension(raw: Record): Extension { - return { - ...(raw as unknown as Extension), - id: raw.pkgName as string, - } -} - -function mapDownloadItem(raw: Record): DownloadItem { - const chapter = raw.chapter as Record - const manga = chapter?.manga as Record - return { - chapterId: String(chapter?.id), - mangaId: String(chapter?.mangaId ?? manga?.id), - chapterName: chapter?.name as string, - mangaTitle: manga?.title as string, - progress: (raw.progress as number) ?? 0, - state: mapDownloadState(raw.state as string), - } -} - -function mapDownloadState(state: string): DownloadItem['state'] { - switch (state) { - case 'DOWNLOADING': return 'downloading' - case 'FINISHED': return 'finished' - case 'ERROR': return 'error' - default: return 'queued' - } -} - export class SuwayomiAdapter implements ServerAdapter { private baseUrl = 'http://127.0.0.1:4567' private authHeader: string | null = null @@ -347,31 +105,25 @@ export class SuwayomiAdapter implements ServerAdapter { } async getManga(id: string): Promise { - const data = await this.gql<{ manga: Record }>( - GET_MANGA, { id: Number(id) } - ) + const data = await this.gql<{ manga: Record }>(GET_MANGA, { id: Number(id) }) return mapManga(data.manga) } async getMangaList(filters: MangaFilters): Promise> { - if (filters.inLibrary) { - const data = await this.gql<{ mangas: { nodes: Record[] } }>(GET_LIBRARY) - return { items: data.mangas.nodes.map(mapManga), hasNextPage: false } - } const data = await this.gql<{ mangas: { nodes: Record[] } }>(GET_LIBRARY) - return { items: data.mangas.nodes.map(mapManga), hasNextPage: false } + 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 [] const data = await this.gql<{ fetchSourceManga: { mangas: Record[] } - }>(FETCH_SOURCE_MANGA, { - source: sourceId, - type: 'SEARCH', - page: 1, - query, - }) + }>(FETCH_SOURCE_MANGA, { source: sourceId, type: 'SEARCH', page: 1, query }) return data.fetchSourceManga.mangas.map(mapManga) } @@ -386,11 +138,7 @@ export class SuwayomiAdapter implements ServerAdapter { 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), - }) + await this.gql(SET_MANGA_META, { mangaId: Number(id), key, value: String(value) }) } } @@ -402,12 +150,10 @@ export class SuwayomiAdapter implements ServerAdapter { } async getChapter(id: string): Promise { - const chapters = await this.gql<{ chapters: { nodes: Record[] } }>( - GET_CHAPTERS, { mangaId: 0 } + const data = await this.gql<{ chapter: Record }>( + GET_CHAPTER, { id: Number(id) } ) - const found = chapters.chapters.nodes.find(c => String(c.id) === id) - if (!found) throw new Error(`Chapter ${id} not found`) - return mapChapter(found) + return mapChapter(data.chapter) } async getChapterPages(id: string): Promise { @@ -426,9 +172,9 @@ export class SuwayomiAdapter implements ServerAdapter { } async getDownloads(): Promise { - const data = await this.gql<{ - downloadStatus: { queue: Record[] } - }>(GET_DOWNLOAD_STATUS) + const data = await this.gql<{ downloadStatus: { queue: Record[] } }>( + GET_DOWNLOAD_STATUS + ) return data.downloadStatus.queue.map(mapDownloadItem) } @@ -446,9 +192,7 @@ export class SuwayomiAdapter implements ServerAdapter { async getExtensions(): Promise { await this.gql(FETCH_EXTENSIONS) - const data = await this.gql<{ extensions: { nodes: Record[] } }>( - GET_EXTENSIONS - ) + const data = await this.gql<{ extensions: { nodes: Record[] } }>(GET_EXTENSIONS) return data.extensions.nodes.map(mapExtension) } @@ -472,11 +216,7 @@ export class SuwayomiAdapter implements ServerAdapter { 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, - }) + }>(FETCH_SOURCE_MANGA, { source: sourceId, type: 'LATEST', page }) return { items: data.fetchSourceManga.mangas.map(mapManga), hasNextPage: data.fetchSourceManga.hasNextPage, diff --git a/src/lib/server-adapters/suwayomi/tracking.ts b/src/lib/server-adapters/suwayomi/tracking.ts new file mode 100644 index 0000000..3b08d95 --- /dev/null +++ b/src/lib/server-adapters/suwayomi/tracking.ts @@ -0,0 +1,92 @@ +export const GET_TRACKERS = ` + query GetTrackers { + trackers { + nodes { + id name icon isLoggedIn isTokenExpired authUrl + supportsPrivateTracking supportsReadingDates supportsTrackDeletion + scores + statuses { value name } + } + } + } +` + +export const GET_MANGA_TRACK_RECORDS = ` + query GetMangaTrackRecords($mangaId: Int!) { + manga(id: $mangaId) { + trackRecords { + nodes { + id trackerId remoteId title status score displayScore + lastChapterRead totalChapters remoteUrl startDate finishDate private libraryId + } + } + } + } +` + +export const SEARCH_TRACKER = ` + query SearchTracker($trackerId: Int!, $query: String!) { + searchTracker(input: { trackerId: $trackerId, query: $query }) { + trackSearches { + id trackerId remoteId title coverUrl summary + publishingStatus publishingType startDate totalChapters trackingUrl + } + } + } +` + +export const BIND_TRACK = ` + mutation BindTrack($mangaId: Int!, $trackerId: Int!, $remoteId: LongString!) { + bindTrack(input: { mangaId: $mangaId, trackerId: $trackerId, remoteId: $remoteId }) { + trackRecord { id trackerId remoteId } + } + } +` + +export const TRACK_PROGRESS = ` + mutation TrackProgress($mangaId: Int!) { + trackProgress(input: { mangaId: $mangaId }) { + trackRecords { id trackerId lastChapterRead status } + } + } +` + +export const UPDATE_TRACK = ` + mutation UpdateTrack($recordId: Int!, $status: Int, $score: Float, $lastChapterRead: Float, $startDate: LongString, $finishDate: LongString, $private: Boolean) { + updateTrack(input: { + recordId: $recordId + status: $status + score: $score + lastChapterRead: $lastChapterRead + startDate: $startDate + finishDate: $finishDate + private: $private + }) { + trackRecord { id status score lastChapterRead } + } + } +` + +export const UNLINK_TRACK = ` + mutation UnlinkTrack($trackRecordId: Int!) { + unlinkTrack(input: { trackRecordId: $trackRecordId }) { + trackRecord { id } + } + } +` + +export const LOGIN_TRACKER_CREDENTIALS = ` + mutation LoginTrackerCredentials($trackerId: Int!, $username: String!, $password: String!) { + loginTrackerCredentials(input: { trackerId: $trackerId, username: $username, password: $password }) { + isLoggedIn + } + } +` + +export const LOGOUT_TRACKER = ` + mutation LogoutTracker($trackerId: Int!) { + logoutTracker(input: { trackerId: $trackerId }) { + isLoggedIn + } + } +` \ No newline at end of file diff --git a/vite.config.ts b/vite.config.ts index 6832917..ebcb9c2 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -1,9 +1,12 @@ -import { sveltekit } from '@sveltejs/kit/vite'; -import { defineConfig } from 'vite'; +import { sveltekit } from '@sveltejs/kit/vite' +import { defineConfig } from 'vite' export default defineConfig({ plugins: [sveltekit()], clearScreen: false, + define: { + __APP_VERSION__: JSON.stringify(process.env.npm_package_version ?? '0.0.0'), + }, server: { port: 1420, strictPort: true, @@ -17,4 +20,4 @@ export default defineConfig({ minify: !process.env.TAURI_DEBUG ? 'oxc' : false, sourcemap: !!process.env.TAURI_DEBUG, }, -}); \ No newline at end of file +}) \ No newline at end of file