mirror of
https://github.com/moku-project/Moku.git
synced 2026-06-13 09:19:56 -05:00
Chore: Port over Home & Fix Suwayomi-Server Detection on Web
This commit is contained in:
@@ -9,6 +9,15 @@ export const GET_CHAPTERS = `
|
||||
}
|
||||
`
|
||||
|
||||
export const GET_CHAPTER = `
|
||||
query GetChapter($id: Int!) {
|
||||
chapter(id: $id) {
|
||||
id name chapterNumber sourceOrder isRead isDownloaded isBookmarked
|
||||
pageCount mangaId uploadDate realUrl lastPageRead lastReadAt scanlator
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
export const GET_RECENTLY_UPDATED = `
|
||||
query GetRecentlyUpdated {
|
||||
chapters(orderBy: FETCHED_AT, orderByType: DESC, first: 300) {
|
||||
|
||||
@@ -8,29 +8,51 @@ import type {
|
||||
Page,
|
||||
DownloadItem,
|
||||
UpdateResult,
|
||||
LibraryUpdateProgress,
|
||||
} from '$lib/server-adapters/types'
|
||||
import type { Manga, Chapter, Extension, Source, Tracker } from '$lib/types'
|
||||
import type { Manga, Chapter, Extension, Source, Tracker, Category } from '$lib/types'
|
||||
import {
|
||||
GET_LIBRARY,
|
||||
GET_MANGA,
|
||||
GET_CATEGORIES,
|
||||
FETCH_MANGA,
|
||||
UPDATE_MANGA,
|
||||
SET_MANGA_META,
|
||||
UPDATE_MANGAS,
|
||||
UPDATE_MANGA_CATEGORIES,
|
||||
UPDATE_MANGAS_CATEGORIES,
|
||||
CREATE_CATEGORY,
|
||||
DELETE_CATEGORY,
|
||||
UPDATE_CATEGORY_ORDER,
|
||||
UPDATE_CATEGORY_MANGA,
|
||||
UPDATE_LIBRARY,
|
||||
UPDATE_LIBRARY_MANGA,
|
||||
UPDATE_STOP,
|
||||
SET_MANGA_META,
|
||||
DELETE_MANGA_META,
|
||||
FETCH_SOURCE_MANGA,
|
||||
LIBRARY_UPDATE_STATUS,
|
||||
} from './manga'
|
||||
import {
|
||||
GET_CHAPTERS,
|
||||
GET_CHAPTER,
|
||||
GET_RECENTLY_UPDATED,
|
||||
FETCH_CHAPTERS,
|
||||
FETCH_CHAPTER_PAGES,
|
||||
MARK_CHAPTER_READ,
|
||||
MARK_CHAPTERS_READ,
|
||||
UPDATE_CHAPTERS_PROGRESS,
|
||||
DELETE_DOWNLOADED_CHAPTERS,
|
||||
SET_CHAPTER_META,
|
||||
DELETE_CHAPTER_META,
|
||||
} from './chapters'
|
||||
import {
|
||||
GET_DOWNLOAD_STATUS,
|
||||
ENQUEUE_DOWNLOAD,
|
||||
ENQUEUE_CHAPTERS_DOWNLOAD,
|
||||
DEQUEUE_DOWNLOAD,
|
||||
DEQUEUE_CHAPTERS_DOWNLOAD,
|
||||
START_DOWNLOADER,
|
||||
STOP_DOWNLOADER,
|
||||
CLEAR_DOWNLOADER,
|
||||
} from './downloads'
|
||||
import {
|
||||
@@ -38,34 +60,32 @@ import {
|
||||
GET_SOURCES,
|
||||
FETCH_EXTENSIONS,
|
||||
UPDATE_EXTENSION,
|
||||
UPDATE_EXTENSIONS,
|
||||
INSTALL_EXTERNAL_EXTENSION,
|
||||
} from './extensions'
|
||||
import {
|
||||
GET_TRACKERS,
|
||||
GET_MANGA_TRACK_RECORDS,
|
||||
SEARCH_TRACKER,
|
||||
BIND_TRACK,
|
||||
UNLINK_TRACK,
|
||||
TRACK_PROGRESS,
|
||||
UPDATE_TRACK,
|
||||
} from './tracking'
|
||||
import {
|
||||
GQLResponse,
|
||||
type GQLResponse,
|
||||
mapManga,
|
||||
mapChapter,
|
||||
mapExtension,
|
||||
mapDownloadItem,
|
||||
mapCategory,
|
||||
} from './types'
|
||||
|
||||
const GET_CHAPTER = `
|
||||
query GetChapter($id: Int!) {
|
||||
chapter(id: $id) {
|
||||
id name chapterNumber sourceOrder isRead isDownloaded isBookmarked
|
||||
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
|
||||
|
||||
async connect(config: ServerConfig) {
|
||||
async connect(config: ServerConfig): Promise<void> {
|
||||
this.baseUrl = config.baseUrl.replace(/\/$/, '')
|
||||
if (config.credentials) {
|
||||
const { username, password } = config.credentials
|
||||
@@ -73,6 +93,10 @@ export class SuwayomiAdapter implements ServerAdapter {
|
||||
}
|
||||
}
|
||||
|
||||
getServerUrl(): string {
|
||||
return this.baseUrl
|
||||
}
|
||||
|
||||
async getStatus(): Promise<ServerStatus> {
|
||||
try {
|
||||
const res = await fetch(`${this.baseUrl}/api/graphql`, {
|
||||
@@ -92,11 +116,16 @@ export class SuwayomiAdapter implements ServerAdapter {
|
||||
return h
|
||||
}
|
||||
|
||||
private async gql<T>(query: string, variables?: Record<string, unknown>): Promise<T> {
|
||||
private async gql<T>(
|
||||
query: string,
|
||||
variables?: Record<string, unknown>,
|
||||
signal?: AbortSignal,
|
||||
): Promise<T> {
|
||||
const res = await fetch(`${this.baseUrl}/api/graphql`, {
|
||||
method: 'POST',
|
||||
headers: this.headers(),
|
||||
body: JSON.stringify({ query, variables }),
|
||||
signal,
|
||||
})
|
||||
if (!res.ok) throw new Error(`Suwayomi HTTP ${res.status}`)
|
||||
const json: GQLResponse<T> = await res.json()
|
||||
@@ -104,44 +133,63 @@ export class SuwayomiAdapter implements ServerAdapter {
|
||||
return json.data
|
||||
}
|
||||
|
||||
// ─── Manga ───────────────────────────────────────────────────────────────
|
||||
|
||||
async getManga(id: string): Promise<Manga> {
|
||||
const data = await this.gql<{ manga: Record<string, unknown> }>(GET_MANGA, { id: Number(id) })
|
||||
return mapManga(data.manga)
|
||||
}
|
||||
|
||||
async fetchManga(id: string): Promise<Manga> {
|
||||
const data = await this.gql<{ fetchManga: { manga: Record<string, unknown> } }>(
|
||||
FETCH_MANGA, { id: Number(id) }
|
||||
)
|
||||
return mapManga(data.fetchManga.manga)
|
||||
}
|
||||
|
||||
async getMangaList(filters: MangaFilters): Promise<PaginatedResult<Manga>> {
|
||||
const data = await this.gql<{ mangas: { nodes: Record<string, unknown>[] } }>(GET_LIBRARY)
|
||||
let items = data.mangas.nodes.map(mapManga)
|
||||
if (filters.status) items = items.filter(m => m.status === filters.status)
|
||||
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)
|
||||
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[]> {
|
||||
if (!sourceId) return []
|
||||
const data = await this.gql<{
|
||||
fetchSourceManga: { mangas: Record<string, unknown>[] }
|
||||
}>(FETCH_SOURCE_MANGA, { source: sourceId, type: 'SEARCH', page: 1, query })
|
||||
const data = await this.gql<{ fetchSourceManga: { mangas: Record<string, unknown>[] } }>(
|
||||
FETCH_SOURCE_MANGA, { source: sourceId, type: 'SEARCH', page: 1, query }
|
||||
)
|
||||
return data.fetchSourceManga.mangas.map(mapManga)
|
||||
}
|
||||
|
||||
async addToLibrary(mangaId: string) {
|
||||
async addToLibrary(mangaId: string): Promise<void> {
|
||||
await this.gql(UPDATE_MANGA, { id: Number(mangaId), inLibrary: true })
|
||||
}
|
||||
|
||||
async removeFromLibrary(mangaId: string) {
|
||||
async removeFromLibrary(mangaId: string): Promise<void> {
|
||||
await this.gql(UPDATE_MANGA, { id: Number(mangaId), inLibrary: false })
|
||||
}
|
||||
|
||||
async updateMangaMeta(id: string, meta: Partial<MangaMeta>) {
|
||||
async updateMangas(ids: string[], patch: { inLibrary?: boolean }): Promise<void> {
|
||||
await this.gql(UPDATE_MANGAS, { ids: ids.map(Number), ...patch })
|
||||
}
|
||||
|
||||
async updateMangaMeta(id: string, meta: Partial<MangaMeta>): Promise<void> {
|
||||
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) })
|
||||
}
|
||||
}
|
||||
|
||||
async deleteMangaMeta(id: string, key: string): Promise<void> {
|
||||
await this.gql(DELETE_MANGA_META, { mangaId: Number(id), key })
|
||||
}
|
||||
|
||||
// ─── Chapters ────────────────────────────────────────────────────────────
|
||||
|
||||
async getChapters(mangaId: string): Promise<Chapter[]> {
|
||||
const data = await this.gql<{ chapters: { nodes: Record<string, unknown>[] } }>(
|
||||
GET_CHAPTERS, { mangaId: Number(mangaId) }
|
||||
@@ -156,21 +204,56 @@ export class SuwayomiAdapter implements ServerAdapter {
|
||||
return mapChapter(data.chapter)
|
||||
}
|
||||
|
||||
async getChapterPages(id: string): Promise<Page[]> {
|
||||
async getChapterPages(id: string, signal?: AbortSignal): Promise<Page[]> {
|
||||
const data = await this.gql<{ fetchChapterPages: { pages: string[] } }>(
|
||||
FETCH_CHAPTER_PAGES, { chapterId: Number(id) }
|
||||
FETCH_CHAPTER_PAGES, { chapterId: Number(id) }, signal
|
||||
)
|
||||
return data.fetchChapterPages.pages.map((url, index) => ({ index, url }))
|
||||
}
|
||||
|
||||
async markChapterRead(id: string, read: boolean) {
|
||||
async fetchChapters(mangaId: string): Promise<Chapter[]> {
|
||||
const data = await this.gql<{ fetchChapters: { chapters: Record<string, unknown>[] } }>(
|
||||
FETCH_CHAPTERS, { mangaId: Number(mangaId) }
|
||||
)
|
||||
return data.fetchChapters.chapters.map(mapChapter)
|
||||
}
|
||||
|
||||
async getRecentlyUpdated(): Promise<Chapter[]> {
|
||||
const data = await this.gql<{ chapters: { nodes: Record<string, unknown>[] } }>(
|
||||
GET_RECENTLY_UPDATED
|
||||
)
|
||||
return data.chapters.nodes.map(mapChapter)
|
||||
}
|
||||
|
||||
async markChapterRead(id: string, read: boolean): Promise<void> {
|
||||
await this.gql(MARK_CHAPTER_READ, { id: Number(id), isRead: read })
|
||||
}
|
||||
|
||||
async markChaptersRead(ids: string[], read: boolean) {
|
||||
async markChaptersRead(ids: string[], read: boolean): Promise<void> {
|
||||
await this.gql(MARK_CHAPTERS_READ, { ids: ids.map(Number), isRead: read })
|
||||
}
|
||||
|
||||
async updateChaptersProgress(
|
||||
ids: string[],
|
||||
patch: { isRead?: boolean; isBookmarked?: boolean; lastPageRead?: number },
|
||||
): Promise<void> {
|
||||
await this.gql(UPDATE_CHAPTERS_PROGRESS, { ids: ids.map(Number), ...patch })
|
||||
}
|
||||
|
||||
async deleteDownloadedChapters(ids: string[]): Promise<void> {
|
||||
await this.gql(DELETE_DOWNLOADED_CHAPTERS, { ids: ids.map(Number) })
|
||||
}
|
||||
|
||||
async setChapterMeta(chapterId: string, key: string, value: string): Promise<void> {
|
||||
await this.gql(SET_CHAPTER_META, { chapterId: Number(chapterId), key, value })
|
||||
}
|
||||
|
||||
async deleteChapterMeta(chapterId: string, key: string): Promise<void> {
|
||||
await this.gql(DELETE_CHAPTER_META, { chapterId: Number(chapterId), key })
|
||||
}
|
||||
|
||||
// ─── Downloads ───────────────────────────────────────────────────────────
|
||||
|
||||
async getDownloads(): Promise<DownloadItem[]> {
|
||||
const data = await this.gql<{ downloadStatus: { queue: Record<string, unknown>[] } }>(
|
||||
GET_DOWNLOAD_STATUS
|
||||
@@ -178,36 +261,62 @@ export class SuwayomiAdapter implements ServerAdapter {
|
||||
return data.downloadStatus.queue.map(mapDownloadItem)
|
||||
}
|
||||
|
||||
async enqueueDownload(chapterId: string) {
|
||||
async enqueueDownload(chapterId: string): Promise<void> {
|
||||
await this.gql(ENQUEUE_DOWNLOAD, { chapterId: Number(chapterId) })
|
||||
}
|
||||
|
||||
async dequeueDownload(chapterId: string) {
|
||||
async enqueueDownloads(chapterIds: string[]): Promise<void> {
|
||||
await this.gql(ENQUEUE_CHAPTERS_DOWNLOAD, { chapterIds: chapterIds.map(Number) })
|
||||
}
|
||||
|
||||
async dequeueDownload(chapterId: string): Promise<void> {
|
||||
await this.gql(DEQUEUE_DOWNLOAD, { chapterId: Number(chapterId) })
|
||||
}
|
||||
|
||||
async clearDownloads() {
|
||||
async dequeueDownloads(chapterIds: string[]): Promise<void> {
|
||||
await this.gql(DEQUEUE_CHAPTERS_DOWNLOAD, { chapterIds: chapterIds.map(Number) })
|
||||
}
|
||||
|
||||
async clearDownloads(): Promise<void> {
|
||||
await this.gql(CLEAR_DOWNLOADER)
|
||||
}
|
||||
|
||||
async startDownloader(): Promise<void> {
|
||||
await this.gql(START_DOWNLOADER)
|
||||
}
|
||||
|
||||
async stopDownloader(): Promise<void> {
|
||||
await this.gql(STOP_DOWNLOADER)
|
||||
}
|
||||
|
||||
// ─── Extensions ──────────────────────────────────────────────────────────
|
||||
|
||||
async getExtensions(): Promise<Extension[]> {
|
||||
await this.gql(FETCH_EXTENSIONS)
|
||||
const data = await this.gql<{ extensions: { nodes: Record<string, unknown>[] } }>(GET_EXTENSIONS)
|
||||
return data.extensions.nodes.map(mapExtension)
|
||||
}
|
||||
|
||||
async installExtension(id: string) {
|
||||
async installExtension(id: string): Promise<void> {
|
||||
await this.gql(UPDATE_EXTENSION, { id, install: true })
|
||||
}
|
||||
|
||||
async uninstallExtension(id: string) {
|
||||
async uninstallExtension(id: string): Promise<void> {
|
||||
await this.gql(UPDATE_EXTENSION, { id, uninstall: true })
|
||||
}
|
||||
|
||||
async updateExtension(id: string) {
|
||||
async updateExtension(id: string): Promise<void> {
|
||||
await this.gql(UPDATE_EXTENSION, { id, update: true })
|
||||
}
|
||||
|
||||
async updateExtensions(ids: string[]): Promise<void> {
|
||||
await this.gql(UPDATE_EXTENSIONS, { ids, update: true })
|
||||
}
|
||||
|
||||
async installExternalExtension(url: string): Promise<void> {
|
||||
await this.gql(INSTALL_EXTERNAL_EXTENSION, { url })
|
||||
}
|
||||
|
||||
async getSources(): Promise<Source[]> {
|
||||
const data = await this.gql<{ sources: { nodes: Source[] } }>(GET_SOURCES)
|
||||
return data.sources.nodes
|
||||
@@ -223,12 +332,65 @@ export class SuwayomiAdapter implements ServerAdapter {
|
||||
}
|
||||
}
|
||||
|
||||
// ─── Categories ──────────────────────────────────────────────────────────
|
||||
|
||||
async getCategories(): Promise<Category[]> {
|
||||
const data = await this.gql<{ categories: { nodes: Record<string, unknown>[] } }>(GET_CATEGORIES)
|
||||
return data.categories.nodes.map(mapCategory)
|
||||
}
|
||||
|
||||
async createCategory(name: string): Promise<Category> {
|
||||
const data = await this.gql<{ createCategory: { category: Record<string, unknown> } }>(
|
||||
CREATE_CATEGORY, { name }
|
||||
)
|
||||
return mapCategory(data.createCategory.category)
|
||||
}
|
||||
|
||||
async deleteCategory(id: number): Promise<void> {
|
||||
await this.gql(DELETE_CATEGORY, { id })
|
||||
}
|
||||
|
||||
async updateCategoryOrder(id: number, position: number): Promise<Category[]> {
|
||||
const data = await this.gql<{ updateCategoryOrder: { categories: Record<string, unknown>[] } }>(
|
||||
UPDATE_CATEGORY_ORDER, { id, position }
|
||||
)
|
||||
return data.updateCategoryOrder.categories.map(mapCategory)
|
||||
}
|
||||
|
||||
async updateMangaCategories(mangaId: string, addTo: number[], removeFrom: number[]): Promise<void> {
|
||||
await this.gql(UPDATE_MANGA_CATEGORIES, { mangaId: Number(mangaId), addTo, removeFrom })
|
||||
}
|
||||
|
||||
async updateMangasCategories(mangaIds: string[], addTo: number[], removeFrom: number[]): Promise<void> {
|
||||
await this.gql(UPDATE_MANGAS_CATEGORIES, { ids: mangaIds.map(Number), addTo, removeFrom })
|
||||
}
|
||||
|
||||
async updateCategoryManga(categoryId: number): Promise<void> {
|
||||
await this.gql(UPDATE_CATEGORY_MANGA, { categoryId })
|
||||
}
|
||||
|
||||
// ─── Tracking ────────────────────────────────────────────────────────────
|
||||
|
||||
async getTrackers(): Promise<Tracker[]> {
|
||||
const data = await this.gql<{ trackers: { nodes: Tracker[] } }>(GET_TRACKERS)
|
||||
return data.trackers.nodes
|
||||
}
|
||||
|
||||
async linkTracker(mangaId: string, trackerId: string, remoteId: string) {
|
||||
async getMangaTrackRecords(mangaId: string): Promise<unknown[]> {
|
||||
const data = await this.gql<{
|
||||
manga: { trackRecords: { nodes: unknown[] } }
|
||||
}>(GET_MANGA_TRACK_RECORDS, { mangaId: Number(mangaId) })
|
||||
return data.manga.trackRecords.nodes
|
||||
}
|
||||
|
||||
async searchTracker(trackerId: string, query: string): Promise<unknown[]> {
|
||||
const data = await this.gql<{
|
||||
searchTracker: { trackSearches: unknown[] }
|
||||
}>(SEARCH_TRACKER, { trackerId: Number(trackerId), query })
|
||||
return data.searchTracker.trackSearches
|
||||
}
|
||||
|
||||
async linkTracker(mangaId: string, trackerId: string, remoteId: string): Promise<void> {
|
||||
await this.gql(BIND_TRACK, {
|
||||
mangaId: Number(mangaId),
|
||||
trackerId: Number(trackerId),
|
||||
@@ -236,16 +398,26 @@ export class SuwayomiAdapter implements ServerAdapter {
|
||||
})
|
||||
}
|
||||
|
||||
async syncTracking(mangaId: string) {
|
||||
async unlinkTracker(recordId: string): Promise<void> {
|
||||
await this.gql(UNLINK_TRACK, { trackRecordId: Number(recordId) })
|
||||
}
|
||||
|
||||
async fetchTrackRecord(recordId: string): Promise<void> {
|
||||
await this.gql(UPDATE_TRACK, { recordId: Number(recordId) })
|
||||
}
|
||||
|
||||
async syncTracking(mangaId: string): Promise<void> {
|
||||
await this.gql(TRACK_PROGRESS, { mangaId: Number(mangaId) })
|
||||
}
|
||||
|
||||
// ─── Library updates ─────────────────────────────────────────────────────
|
||||
|
||||
async checkForUpdates(mangaIds?: string[]): Promise<UpdateResult[]> {
|
||||
if (mangaIds?.length) {
|
||||
const results: UpdateResult[] = []
|
||||
for (const id of mangaIds) {
|
||||
const before = await this.getChapters(id)
|
||||
await this.gql(FETCH_CHAPTERS, { mangaId: Number(id) })
|
||||
await this.gql(UPDATE_LIBRARY_MANGA, { mangaId: Number(id) })
|
||||
const after = await this.getChapters(id)
|
||||
results.push({ mangaId: id, newChapters: after.length - before.length })
|
||||
}
|
||||
@@ -254,4 +426,18 @@ export class SuwayomiAdapter implements ServerAdapter {
|
||||
await this.gql(UPDATE_LIBRARY)
|
||||
return []
|
||||
}
|
||||
|
||||
async stopLibraryUpdate(): Promise<void> {
|
||||
await this.gql(UPDATE_STOP)
|
||||
}
|
||||
|
||||
async getLibraryUpdateStatus(): Promise<LibraryUpdateProgress> {
|
||||
const data = await this.gql<{
|
||||
libraryUpdateStatus: {
|
||||
jobsInfo: { isRunning: boolean; finishedJobs: number; totalJobs: number }
|
||||
}
|
||||
}>(LIBRARY_UPDATE_STATUS)
|
||||
const { isRunning, finishedJobs, totalJobs } = data.libraryUpdateStatus.jobsInfo
|
||||
return { isRunning, finishedJobs, totalJobs }
|
||||
}
|
||||
}
|
||||
@@ -3,9 +3,12 @@ export const GET_LIBRARY = `
|
||||
mangas(condition: { inLibrary: true }) {
|
||||
nodes {
|
||||
id title thumbnailUrl inLibrary downloadCount unreadCount bookmarkCount
|
||||
description status author artist genre inLibraryAt lastFetchedAt
|
||||
description status author artist genre
|
||||
inLibraryAt lastFetchedAt chaptersLastFetchedAt thumbnailUrlLastFetched
|
||||
source { id name displayName }
|
||||
chapters { totalCount }
|
||||
latestFetchedChapter { id uploadDate }
|
||||
latestUploadedChapter { id uploadDate }
|
||||
lastReadChapter { id chapterNumber }
|
||||
firstUnreadChapter { id chapterNumber }
|
||||
}
|
||||
@@ -17,7 +20,7 @@ export const GET_MANGA = `
|
||||
query GetManga($id: Int!) {
|
||||
manga(id: $id) {
|
||||
id title description thumbnailUrl status author artist genre inLibrary realUrl
|
||||
inLibraryAt lastFetchedAt updateStrategy
|
||||
inLibraryAt lastFetchedAt thumbnailUrlLastFetched updateStrategy
|
||||
source { id name displayName }
|
||||
lastReadChapter { id chapterNumber lastPageRead }
|
||||
firstUnreadChapter { id chapterNumber }
|
||||
@@ -39,6 +42,21 @@ export const GET_CATEGORIES = `
|
||||
}
|
||||
`
|
||||
|
||||
export const LIBRARY_UPDATE_STATUS = `
|
||||
query LibraryUpdateStatus {
|
||||
libraryUpdateStatus {
|
||||
jobsInfo {
|
||||
isRunning finishedJobs totalJobs skippedMangasCount skippedCategoriesCount
|
||||
}
|
||||
mangaUpdates {
|
||||
status
|
||||
manga { id title thumbnailUrl unreadCount }
|
||||
}
|
||||
}
|
||||
lastUpdateTimestamp { timestamp }
|
||||
}
|
||||
`
|
||||
|
||||
export const MANGAS_BY_GENRE = `
|
||||
query MangasByGenre($filter: MangaFilterInput, $first: Int, $offset: Int) {
|
||||
mangas(filter: $filter, first: $first, offset: $offset, orderBy: IN_LIBRARY_AT, orderByType: DESC) {
|
||||
@@ -52,18 +70,9 @@ export const MANGAS_BY_GENRE = `
|
||||
}
|
||||
`
|
||||
|
||||
export const LIBRARY_UPDATE_STATUS = `
|
||||
query LibraryUpdateStatus {
|
||||
libraryUpdateStatus {
|
||||
jobsInfo {
|
||||
isRunning finishedJobs totalJobs skippedMangasCount skippedCategoriesCount
|
||||
}
|
||||
mangaUpdates {
|
||||
status
|
||||
manga { id title thumbnailUrl unreadCount }
|
||||
}
|
||||
}
|
||||
lastUpdateTimestamp { timestamp }
|
||||
export const GET_DOWNLOADS_PATH = `
|
||||
query GetDownloadsPath {
|
||||
settings { downloadsPath localSourcePath }
|
||||
}
|
||||
`
|
||||
|
||||
@@ -142,6 +151,14 @@ export const UPDATE_CATEGORY_ORDER = `
|
||||
}
|
||||
`
|
||||
|
||||
export const UPDATE_CATEGORY_MANGA = `
|
||||
mutation UpdateCategoryManga($categoryId: Int!) {
|
||||
updateCategoryManga(input: { categoryId: $categoryId }) {
|
||||
updateStatus { jobsInfo { isRunning finishedJobs totalJobs } }
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
export const UPDATE_LIBRARY = `
|
||||
mutation UpdateLibrary {
|
||||
updateLibrary(input: {}) {
|
||||
@@ -158,6 +175,14 @@ export const UPDATE_LIBRARY_MANGA = `
|
||||
}
|
||||
`
|
||||
|
||||
export const UPDATE_STOP = `
|
||||
mutation UpdateStop {
|
||||
updateStop(input: {}) {
|
||||
updateStatus { jobsInfo { isRunning finishedJobs totalJobs } }
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
export const SET_MANGA_META = `
|
||||
mutation SetMangaMeta($mangaId: Int!, $key: String!, $value: String!) {
|
||||
setMangaMeta(input: { meta: { mangaId: $mangaId, key: $key, value: $value } }) {
|
||||
@@ -189,8 +214,11 @@ export const RESTORE_BACKUP = `
|
||||
}
|
||||
`
|
||||
|
||||
export const GET_RESTORE_STATUS = `
|
||||
query GetRestoreStatus($id: String!) {
|
||||
restoreStatus(id: $id) { mangaProgress state totalManga }
|
||||
export const FETCH_SOURCE_MANGA = `
|
||||
mutation FetchSourceManga($source: LongString!, $type: FetchSourceMangaType!, $page: Int!, $query: String, $filters: [FilterChangeInput!]) {
|
||||
fetchSourceManga(input: { source: $source, type: $type, page: $page, query: $query, filters: $filters }) {
|
||||
mangas { id title thumbnailUrl inLibrary }
|
||||
hasNextPage
|
||||
}
|
||||
}
|
||||
`
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { Manga, Chapter, Extension } from '$lib/types'
|
||||
import type { Manga, Chapter, Extension, Category } from '$lib/types'
|
||||
import type { DownloadItem } from '$lib/server-adapters/types'
|
||||
|
||||
export interface GQLResponse<T> {
|
||||
@@ -7,33 +7,46 @@ export interface GQLResponse<T> {
|
||||
}
|
||||
|
||||
export 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,
|
||||
id: raw.id as number,
|
||||
title: raw.title as string,
|
||||
description: raw.description as string | null | undefined,
|
||||
thumbnailUrl: raw.thumbnailUrl as string | null | undefined,
|
||||
status: raw.status as string | undefined,
|
||||
author: raw.author as string | null | undefined,
|
||||
artist: raw.artist as string | null | undefined,
|
||||
tags: raw.genre as string[] | undefined,
|
||||
inLibrary: raw.inLibrary as boolean,
|
||||
realUrl: raw.realUrl as string | null | undefined,
|
||||
source: raw.source as Manga['source'],
|
||||
unreadCount: raw.unreadCount as number | undefined,
|
||||
downloadCount: raw.downloadCount as number | undefined,
|
||||
bookmarkCount: raw.bookmarkCount as number | undefined,
|
||||
lastReadChapter: raw.lastReadChapter as Manga['lastReadChapter'],
|
||||
firstUnreadChapter: raw.firstUnreadChapter as Manga['firstUnreadChapter'],
|
||||
addedAt: raw.inLibraryAt ? new Date(raw.inLibraryAt as string).getTime() : undefined,
|
||||
lastReadAt: raw.lastReadChapter ? Date.now() : undefined,
|
||||
}
|
||||
}
|
||||
|
||||
export function mapChapter(raw: Record<string, unknown>): Chapter {
|
||||
return {
|
||||
id: raw.id as number,
|
||||
name: raw.name as string,
|
||||
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'],
|
||||
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'],
|
||||
}
|
||||
}
|
||||
|
||||
@@ -46,14 +59,14 @@ export function mapExtension(raw: Record<string, unknown>): Extension {
|
||||
|
||||
export function mapDownloadItem(raw: Record<string, unknown>): DownloadItem {
|
||||
const chapter = raw.chapter as Record<string, unknown>
|
||||
const manga = chapter?.manga as Record<string, unknown>
|
||||
const manga = chapter?.manga as Record<string, unknown>
|
||||
return {
|
||||
chapterId: String(chapter?.id),
|
||||
mangaId: String(chapter?.mangaId ?? manga?.id),
|
||||
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),
|
||||
mangaTitle: manga?.title as string,
|
||||
progress: (raw.progress as number) ?? 0,
|
||||
state: mapDownloadState(raw.state as string),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -64,4 +77,16 @@ function mapDownloadState(state: string): DownloadItem['state'] {
|
||||
case 'ERROR': return 'error'
|
||||
default: return 'queued'
|
||||
}
|
||||
}
|
||||
|
||||
export function mapCategory(raw: Record<string, unknown>): Category {
|
||||
return {
|
||||
id: raw.id as number,
|
||||
name: raw.name as string,
|
||||
order: raw.order as number,
|
||||
default: raw.default as boolean,
|
||||
includeInUpdate: raw.includeInUpdate as boolean,
|
||||
includeInDownload: raw.includeInDownload as boolean,
|
||||
mangas: (raw.mangas as { nodes: Record<string, unknown>[] })?.nodes?.map(mapManga),
|
||||
}
|
||||
}
|
||||
@@ -1,10 +1,4 @@
|
||||
import type {
|
||||
Manga,
|
||||
Chapter,
|
||||
Extension,
|
||||
Source,
|
||||
Tracker,
|
||||
} from '$lib/types'
|
||||
import type { Manga, Chapter, Extension, Source, Tracker, Category } from '$lib/types'
|
||||
|
||||
export interface ServerConfig {
|
||||
baseUrl: string
|
||||
@@ -21,7 +15,13 @@ export interface MangaFilters {
|
||||
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<T> {
|
||||
items: T[]
|
||||
@@ -47,6 +47,7 @@ export interface DownloadItem {
|
||||
mangaId: string
|
||||
chapterName: string
|
||||
mangaTitle: string
|
||||
thumbnailUrl?: string
|
||||
progress: number
|
||||
state: 'queued' | 'downloading' | 'finished' | 'error'
|
||||
}
|
||||
@@ -56,39 +57,75 @@ export interface UpdateResult {
|
||||
newChapters: number
|
||||
}
|
||||
|
||||
export interface LibraryUpdateProgress {
|
||||
isRunning: boolean
|
||||
finishedJobs: number
|
||||
totalJobs: number
|
||||
}
|
||||
|
||||
export interface ServerAdapter {
|
||||
connect(config: ServerConfig): Promise<void>
|
||||
getStatus(): Promise<ServerStatus>
|
||||
getServerUrl(): string
|
||||
|
||||
getManga(id: string): Promise<Manga>
|
||||
getMangaList(filters: MangaFilters): Promise<PaginatedResult<Manga>>
|
||||
searchManga(query: string, sourceId?: string): Promise<Manga[]>
|
||||
fetchManga(id: string): Promise<Manga>
|
||||
addToLibrary(mangaId: string): Promise<void>
|
||||
removeFromLibrary(mangaId: string): Promise<void>
|
||||
updateMangas(ids: string[], patch: { inLibrary?: boolean }): Promise<void>
|
||||
updateMangaMeta(id: string, meta: Partial<MangaMeta>): Promise<void>
|
||||
deleteMangaMeta(id: string, key: string): Promise<void>
|
||||
|
||||
getChapters(mangaId: string): Promise<Chapter[]>
|
||||
getChapter(id: string): Promise<Chapter>
|
||||
getChapterPages(id: string): Promise<Page[]>
|
||||
fetchChapters(mangaId: string): Promise<Chapter[]>
|
||||
getRecentlyUpdated(): Promise<Chapter[]>
|
||||
markChapterRead(id: string, read: boolean): Promise<void>
|
||||
markChaptersRead(ids: string[], read: boolean): Promise<void>
|
||||
updateChaptersProgress(ids: string[], patch: { isRead?: boolean; isBookmarked?: boolean; lastPageRead?: number }): Promise<void>
|
||||
deleteDownloadedChapters(ids: string[]): Promise<void>
|
||||
setChapterMeta(chapterId: string, key: string, value: string): Promise<void>
|
||||
deleteChapterMeta(chapterId: string, key: string): Promise<void>
|
||||
|
||||
getDownloads(): Promise<DownloadItem[]>
|
||||
enqueueDownload(chapterId: string): Promise<void>
|
||||
enqueueDownloads(chapterIds: string[]): Promise<void>
|
||||
dequeueDownload(chapterId: string): Promise<void>
|
||||
dequeueDownloads(chapterIds: string[]): Promise<void>
|
||||
clearDownloads(): Promise<void>
|
||||
startDownloader(): Promise<void>
|
||||
stopDownloader(): Promise<void>
|
||||
|
||||
getExtensions(): Promise<Extension[]>
|
||||
installExtension(id: string): Promise<void>
|
||||
uninstallExtension(id: string): Promise<void>
|
||||
updateExtension(id: string): Promise<void>
|
||||
updateExtensions(ids: string[]): Promise<void>
|
||||
installExternalExtension(url: string): Promise<void>
|
||||
|
||||
getSources(): Promise<Source[]>
|
||||
browseSource(sourceId: string, page: number): Promise<PaginatedResult<Manga>>
|
||||
|
||||
getCategories(): Promise<Category[]>
|
||||
createCategory(name: string): Promise<Category>
|
||||
deleteCategory(id: number): Promise<void>
|
||||
updateCategoryOrder(id: number, position: number): Promise<Category[]>
|
||||
updateMangaCategories(mangaId: string, addTo: number[], removeFrom: number[]): Promise<void>
|
||||
updateMangasCategories(mangaIds: string[], addTo: number[], removeFrom: number[]): Promise<void>
|
||||
updateCategoryManga(categoryId: number): Promise<void>
|
||||
|
||||
getTrackers(): Promise<Tracker[]>
|
||||
getMangaTrackRecords(mangaId: string): Promise<unknown[]>
|
||||
searchTracker(trackerId: string, query: string): Promise<unknown[]>
|
||||
linkTracker(mangaId: string, trackerId: string, remoteId: string): Promise<void>
|
||||
unlinkTracker(recordId: string): Promise<void>
|
||||
fetchTrackRecord(recordId: string): Promise<void>
|
||||
syncTracking(mangaId: string): Promise<void>
|
||||
|
||||
checkForUpdates(mangaIds?: string[]): Promise<UpdateResult[]>
|
||||
}
|
||||
stopLibraryUpdate(): Promise<void>
|
||||
getLibraryUpdateStatus(): Promise<LibraryUpdateProgress>
|
||||
}
|
||||
Reference in New Issue
Block a user