mirror of
https://github.com/moku-project/Moku.git
synced 2026-06-13 09:19:56 -05:00
Chore: Implement Server Adapters & Request Manager
This commit is contained in:
Vendored
+2
-1
@@ -1,4 +1,5 @@
|
|||||||
declare global {
|
declare global {
|
||||||
namespace App {}
|
namespace App {}
|
||||||
|
const __APP_VERSION__: string
|
||||||
}
|
}
|
||||||
export {};
|
export {}
|
||||||
@@ -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<Uint8Array> {
|
||||||
|
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<void> {
|
||||||
|
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<string | null> {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
async authenticateBiometric(): Promise<boolean> {
|
||||||
|
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<void> {
|
||||||
|
const { NativeBiometric } = await import('capacitor-native-biometric')
|
||||||
|
await NativeBiometric.setCredentials({ username: key, password: value, server: 'moku' })
|
||||||
|
}
|
||||||
|
|
||||||
|
async getCredential(key: string): Promise<string | null> {
|
||||||
|
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<string> {
|
||||||
|
const { App } = await import('@capacitor/app')
|
||||||
|
const info = await App.getInfo()
|
||||||
|
return info.version
|
||||||
|
}
|
||||||
|
|
||||||
|
async openExternal(url: string): Promise<void> {
|
||||||
|
const { Browser } = await import('@capacitor/browser')
|
||||||
|
await Browser.open({ url })
|
||||||
|
}
|
||||||
|
|
||||||
|
async checkForAppUpdate(): Promise<AppUpdateInfo | null> {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
async installAppUpdate(): Promise<void> {}
|
||||||
|
}
|
||||||
@@ -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<Uint8Array> {
|
||||||
|
return readFile(path)
|
||||||
|
}
|
||||||
|
|
||||||
|
async writeFile(path: string, data: Uint8Array) {
|
||||||
|
await writeFile(path, data)
|
||||||
|
}
|
||||||
|
|
||||||
|
async pickFolder(): Promise<string | null> {
|
||||||
|
const result = await open({ directory: true, multiple: false })
|
||||||
|
return typeof result === 'string' ? result : null
|
||||||
|
}
|
||||||
|
|
||||||
|
async authenticateBiometric(): Promise<boolean> {
|
||||||
|
return invoke('authenticate_biometric')
|
||||||
|
}
|
||||||
|
|
||||||
|
async storeCredential(key: string, value: string) {
|
||||||
|
await invoke('store_credential', { key, value })
|
||||||
|
}
|
||||||
|
|
||||||
|
async getCredential(key: string): Promise<string | null> {
|
||||||
|
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<string> {
|
||||||
|
return getVersion()
|
||||||
|
}
|
||||||
|
|
||||||
|
async openExternal(url: string) {
|
||||||
|
await openUrl(url)
|
||||||
|
}
|
||||||
|
|
||||||
|
async checkForAppUpdate(): Promise<AppUpdateInfo | null> {
|
||||||
|
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()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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<void>
|
||||||
|
isSupported(feature: PlatformFeature): boolean
|
||||||
|
|
||||||
|
launchServer(config: ServerLaunchConfig): Promise<void>
|
||||||
|
stopServer(): Promise<void>
|
||||||
|
getServerStatus(): Promise<'running' | 'stopped' | 'error'>
|
||||||
|
|
||||||
|
readFile(path: string): Promise<Uint8Array>
|
||||||
|
writeFile(path: string, data: Uint8Array): Promise<void>
|
||||||
|
pickFolder(): Promise<string | null>
|
||||||
|
|
||||||
|
authenticateBiometric(): Promise<boolean>
|
||||||
|
storeCredential(key: string, value: string): Promise<void>
|
||||||
|
getCredential(key: string): Promise<string | null>
|
||||||
|
|
||||||
|
setTitle(title: string): Promise<void>
|
||||||
|
minimize(): Promise<void>
|
||||||
|
maximize(): Promise<void>
|
||||||
|
close(): Promise<void>
|
||||||
|
|
||||||
|
setDiscordPresence(presence: DiscordPresence): Promise<void>
|
||||||
|
clearDiscordPresence(): Promise<void>
|
||||||
|
|
||||||
|
getVersion(): Promise<string>
|
||||||
|
openExternal(url: string): Promise<void>
|
||||||
|
checkForAppUpdate(): Promise<AppUpdateInfo | null>
|
||||||
|
installAppUpdate(): Promise<void>
|
||||||
|
}
|
||||||
@@ -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<Uint8Array> {
|
||||||
|
return new Uint8Array()
|
||||||
|
}
|
||||||
|
|
||||||
|
async writeFile(_path: string, _data: Uint8Array) {}
|
||||||
|
|
||||||
|
async pickFolder(): Promise<string | null> {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
async authenticateBiometric(): Promise<boolean> {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
async storeCredential(_key: string, _value: string) {}
|
||||||
|
|
||||||
|
async getCredential(_key: string): Promise<string | null> {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
async setTitle(title: string) {
|
||||||
|
document.title = title
|
||||||
|
}
|
||||||
|
|
||||||
|
async minimize() {}
|
||||||
|
async maximize() {}
|
||||||
|
async close() {}
|
||||||
|
|
||||||
|
async setDiscordPresence(_presence: DiscordPresence) {}
|
||||||
|
async clearDiscordPresence() {}
|
||||||
|
|
||||||
|
async getVersion(): Promise<string> {
|
||||||
|
return __APP_VERSION__
|
||||||
|
}
|
||||||
|
|
||||||
|
async openExternal(url: string) {
|
||||||
|
window.open(url, '_blank', 'noopener,noreferrer')
|
||||||
|
}
|
||||||
|
|
||||||
|
async checkForAppUpdate(): Promise<AppUpdateInfo | null> {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
async installAppUpdate() {}
|
||||||
|
}
|
||||||
@@ -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<AppUpdateInfo | null> {
|
||||||
|
return getAdapter().checkForAppUpdate()
|
||||||
|
}
|
||||||
|
|
||||||
|
export function installAppUpdate() {
|
||||||
|
return getAdapter().installAppUpdate()
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
}
|
||||||
@@ -47,12 +47,12 @@ export async function addToLibrary(mangaId: string) {
|
|||||||
|
|
||||||
export async function removeFromLibrary(mangaId: string) {
|
export async function removeFromLibrary(mangaId: string) {
|
||||||
await getAdapter().removeFromLibrary(mangaId)
|
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<MangaMeta>) {
|
export async function updateMangaMeta(id: string, meta: Partial<MangaMeta>) {
|
||||||
await getAdapter().updateMangaMeta(id, meta)
|
await getAdapter().updateMangaMeta(id, meta)
|
||||||
if (seriesState.current?.id === id) {
|
if (String(seriesState.current?.id) === id) {
|
||||||
await loadManga(id)
|
await loadManga(id)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -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<void> { notImplemented() }
|
||||||
|
async getStatus(): Promise<ServerStatus> { return notImplemented() }
|
||||||
|
|
||||||
|
async getManga(_id: string): Promise<Manga> { return notImplemented() }
|
||||||
|
async getMangaList(_filters: MangaFilters): Promise<PaginatedResult<Manga>> { return notImplemented() }
|
||||||
|
async searchManga(_query: string, _sourceId?: string): Promise<Manga[]> { return notImplemented() }
|
||||||
|
async addToLibrary(_mangaId: string): Promise<void> { notImplemented() }
|
||||||
|
async removeFromLibrary(_mangaId: string): Promise<void> { notImplemented() }
|
||||||
|
async updateMangaMeta(_id: string, _meta: Partial<MangaMeta>): Promise<void> { notImplemented() }
|
||||||
|
|
||||||
|
async getChapters(_mangaId: string): Promise<Chapter[]> { return notImplemented() }
|
||||||
|
async getChapter(_id: string): Promise<Chapter> { return notImplemented() }
|
||||||
|
async getChapterPages(_id: string): Promise<Page[]> { return notImplemented() }
|
||||||
|
async markChapterRead(_id: string, _read: boolean): Promise<void> { notImplemented() }
|
||||||
|
async markChaptersRead(_ids: string[], _read: boolean): Promise<void> { notImplemented() }
|
||||||
|
|
||||||
|
async getDownloads(): Promise<DownloadItem[]> { return notImplemented() }
|
||||||
|
async enqueueDownload(_chapterId: string): Promise<void> { notImplemented() }
|
||||||
|
async dequeueDownload(_chapterId: string): Promise<void> { notImplemented() }
|
||||||
|
async clearDownloads(): Promise<void> { notImplemented() }
|
||||||
|
|
||||||
|
async getExtensions(): Promise<Extension[]> { return notImplemented() }
|
||||||
|
async installExtension(_id: string): Promise<void> { notImplemented() }
|
||||||
|
async uninstallExtension(_id: string): Promise<void> { notImplemented() }
|
||||||
|
async updateExtension(_id: string): Promise<void> { notImplemented() }
|
||||||
|
|
||||||
|
async getSources(): Promise<Source[]> { return notImplemented() }
|
||||||
|
async browseSource(_sourceId: string, _page: number): Promise<PaginatedResult<Manga>> { return notImplemented() }
|
||||||
|
|
||||||
|
async getTrackers(): Promise<Tracker[]> { return notImplemented() }
|
||||||
|
async linkTracker(_mangaId: string, _trackerId: string, _remoteId: string): Promise<void> { notImplemented() }
|
||||||
|
async syncTracking(_mangaId: string): Promise<void> { notImplemented() }
|
||||||
|
|
||||||
|
async checkForUpdates(_mangaIds?: string[]): Promise<UpdateResult[]> { return notImplemented() }
|
||||||
|
}
|
||||||
@@ -10,299 +10,57 @@ import type {
|
|||||||
UpdateResult,
|
UpdateResult,
|
||||||
} from '$lib/server-adapters/types'
|
} from '$lib/server-adapters/types'
|
||||||
import type { Manga, Chapter, Extension, Source, Tracker } from '$lib/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<T> {
|
const GET_CHAPTER = `
|
||||||
data: T
|
query GetChapter($id: Int!) {
|
||||||
errors?: { message: string }[]
|
chapter(id: $id) {
|
||||||
}
|
id name chapterNumber sourceOrder isRead isDownloaded isBookmarked
|
||||||
|
pageCount mangaId uploadDate realUrl lastPageRead lastReadAt scanlator
|
||||||
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_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<string, unknown>): 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<string, unknown>): 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<string, unknown>): Extension {
|
|
||||||
return {
|
|
||||||
...(raw as unknown as Extension),
|
|
||||||
id: raw.pkgName as string,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function mapDownloadItem(raw: Record<string, unknown>): DownloadItem {
|
|
||||||
const chapter = raw.chapter as Record<string, unknown>
|
|
||||||
const manga = chapter?.manga as Record<string, unknown>
|
|
||||||
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 {
|
export class SuwayomiAdapter implements ServerAdapter {
|
||||||
private baseUrl = 'http://127.0.0.1:4567'
|
private baseUrl = 'http://127.0.0.1:4567'
|
||||||
private authHeader: string | null = null
|
private authHeader: string | null = null
|
||||||
@@ -347,31 +105,25 @@ export class SuwayomiAdapter implements ServerAdapter {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async getManga(id: string): Promise<Manga> {
|
async getManga(id: string): Promise<Manga> {
|
||||||
const data = await this.gql<{ manga: Record<string, unknown> }>(
|
const data = await this.gql<{ manga: Record<string, unknown> }>(GET_MANGA, { id: Number(id) })
|
||||||
GET_MANGA, { id: Number(id) }
|
|
||||||
)
|
|
||||||
return mapManga(data.manga)
|
return mapManga(data.manga)
|
||||||
}
|
}
|
||||||
|
|
||||||
async getMangaList(filters: MangaFilters): Promise<PaginatedResult<Manga>> {
|
async getMangaList(filters: MangaFilters): Promise<PaginatedResult<Manga>> {
|
||||||
if (filters.inLibrary) {
|
|
||||||
const data = await this.gql<{ mangas: { nodes: Record<string, unknown>[] } }>(GET_LIBRARY)
|
|
||||||
return { items: data.mangas.nodes.map(mapManga), hasNextPage: false }
|
|
||||||
}
|
|
||||||
const data = await this.gql<{ mangas: { nodes: Record<string, unknown>[] } }>(GET_LIBRARY)
|
const data = await this.gql<{ mangas: { nodes: Record<string, unknown>[] } }>(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<Manga[]> {
|
async searchManga(query: string, sourceId?: string): Promise<Manga[]> {
|
||||||
if (!sourceId) return []
|
if (!sourceId) return []
|
||||||
const data = await this.gql<{
|
const data = await this.gql<{
|
||||||
fetchSourceManga: { mangas: Record<string, unknown>[] }
|
fetchSourceManga: { mangas: Record<string, unknown>[] }
|
||||||
}>(FETCH_SOURCE_MANGA, {
|
}>(FETCH_SOURCE_MANGA, { source: sourceId, type: 'SEARCH', page: 1, query })
|
||||||
source: sourceId,
|
|
||||||
type: 'SEARCH',
|
|
||||||
page: 1,
|
|
||||||
query,
|
|
||||||
})
|
|
||||||
return data.fetchSourceManga.mangas.map(mapManga)
|
return data.fetchSourceManga.mangas.map(mapManga)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -386,11 +138,7 @@ export class SuwayomiAdapter implements ServerAdapter {
|
|||||||
async updateMangaMeta(id: string, meta: Partial<MangaMeta>) {
|
async updateMangaMeta(id: string, meta: Partial<MangaMeta>) {
|
||||||
for (const [key, value] of Object.entries(meta)) {
|
for (const [key, value] of Object.entries(meta)) {
|
||||||
if (value === undefined) continue
|
if (value === undefined) continue
|
||||||
await this.gql(SET_MANGA_META, {
|
await this.gql(SET_MANGA_META, { mangaId: Number(id), key, value: String(value) })
|
||||||
mangaId: Number(id),
|
|
||||||
key,
|
|
||||||
value: String(value),
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -402,12 +150,10 @@ export class SuwayomiAdapter implements ServerAdapter {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async getChapter(id: string): Promise<Chapter> {
|
async getChapter(id: string): Promise<Chapter> {
|
||||||
const chapters = await this.gql<{ chapters: { nodes: Record<string, unknown>[] } }>(
|
const data = await this.gql<{ chapter: Record<string, unknown> }>(
|
||||||
GET_CHAPTERS, { mangaId: 0 }
|
GET_CHAPTER, { id: Number(id) }
|
||||||
)
|
)
|
||||||
const found = chapters.chapters.nodes.find(c => String(c.id) === id)
|
return mapChapter(data.chapter)
|
||||||
if (!found) throw new Error(`Chapter ${id} not found`)
|
|
||||||
return mapChapter(found)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async getChapterPages(id: string): Promise<Page[]> {
|
async getChapterPages(id: string): Promise<Page[]> {
|
||||||
@@ -426,9 +172,9 @@ export class SuwayomiAdapter implements ServerAdapter {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async getDownloads(): Promise<DownloadItem[]> {
|
async getDownloads(): Promise<DownloadItem[]> {
|
||||||
const data = await this.gql<{
|
const data = await this.gql<{ downloadStatus: { queue: Record<string, unknown>[] } }>(
|
||||||
downloadStatus: { queue: Record<string, unknown>[] }
|
GET_DOWNLOAD_STATUS
|
||||||
}>(GET_DOWNLOAD_STATUS)
|
)
|
||||||
return data.downloadStatus.queue.map(mapDownloadItem)
|
return data.downloadStatus.queue.map(mapDownloadItem)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -446,9 +192,7 @@ export class SuwayomiAdapter implements ServerAdapter {
|
|||||||
|
|
||||||
async getExtensions(): Promise<Extension[]> {
|
async getExtensions(): Promise<Extension[]> {
|
||||||
await this.gql(FETCH_EXTENSIONS)
|
await this.gql(FETCH_EXTENSIONS)
|
||||||
const data = await this.gql<{ extensions: { nodes: Record<string, unknown>[] } }>(
|
const data = await this.gql<{ extensions: { nodes: Record<string, unknown>[] } }>(GET_EXTENSIONS)
|
||||||
GET_EXTENSIONS
|
|
||||||
)
|
|
||||||
return data.extensions.nodes.map(mapExtension)
|
return data.extensions.nodes.map(mapExtension)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -472,11 +216,7 @@ export class SuwayomiAdapter implements ServerAdapter {
|
|||||||
async browseSource(sourceId: string, page: number): Promise<PaginatedResult<Manga>> {
|
async browseSource(sourceId: string, page: number): Promise<PaginatedResult<Manga>> {
|
||||||
const data = await this.gql<{
|
const data = await this.gql<{
|
||||||
fetchSourceManga: { mangas: Record<string, unknown>[]; hasNextPage: boolean }
|
fetchSourceManga: { mangas: Record<string, unknown>[]; hasNextPage: boolean }
|
||||||
}>(FETCH_SOURCE_MANGA, {
|
}>(FETCH_SOURCE_MANGA, { source: sourceId, type: 'LATEST', page })
|
||||||
source: sourceId,
|
|
||||||
type: 'LATEST',
|
|
||||||
page,
|
|
||||||
})
|
|
||||||
return {
|
return {
|
||||||
items: data.fetchSourceManga.mangas.map(mapManga),
|
items: data.fetchSourceManga.mangas.map(mapManga),
|
||||||
hasNextPage: data.fetchSourceManga.hasNextPage,
|
hasNextPage: data.fetchSourceManga.hasNextPage,
|
||||||
|
|||||||
@@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`
|
||||||
+6
-3
@@ -1,9 +1,12 @@
|
|||||||
import { sveltekit } from '@sveltejs/kit/vite';
|
import { sveltekit } from '@sveltejs/kit/vite'
|
||||||
import { defineConfig } from 'vite';
|
import { defineConfig } from 'vite'
|
||||||
|
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
plugins: [sveltekit()],
|
plugins: [sveltekit()],
|
||||||
clearScreen: false,
|
clearScreen: false,
|
||||||
|
define: {
|
||||||
|
__APP_VERSION__: JSON.stringify(process.env.npm_package_version ?? '0.0.0'),
|
||||||
|
},
|
||||||
server: {
|
server: {
|
||||||
port: 1420,
|
port: 1420,
|
||||||
strictPort: true,
|
strictPort: true,
|
||||||
@@ -17,4 +20,4 @@ export default defineConfig({
|
|||||||
minify: !process.env.TAURI_DEBUG ? 'oxc' : false,
|
minify: !process.env.TAURI_DEBUG ? 'oxc' : false,
|
||||||
sourcemap: !!process.env.TAURI_DEBUG,
|
sourcemap: !!process.env.TAURI_DEBUG,
|
||||||
},
|
},
|
||||||
});
|
})
|
||||||
Reference in New Issue
Block a user