From a9c442dc2c404b91a5ddb06a21c59df770694c8c Mon Sep 17 00:00:00 2001 From: Robonau <30987265+Robonau@users.noreply.github> Date: Fri, 25 Oct 2024 00:38:51 +0100 Subject: [PATCH 1/3] versionToServerVersionMapping --- versionToServerVersionMapping.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/versionToServerVersionMapping.json b/versionToServerVersionMapping.json index 0cbef2b..58117e7 100644 --- a/versionToServerVersionMapping.json +++ b/versionToServerVersionMapping.json @@ -5,7 +5,7 @@ }, { "tag": "v1.1.0", - "uiVersion": "r1162", + "uiVersion": "r1165", "serverVersion": "r1502", "comment": "the server version is between 1.0.0 and 1.1.0 release", "comment2": "because its the preview version that implemented the breaking changes" From fa72fa679df9157321e82537252d8a74d48eb7b7 Mon Sep 17 00:00:00 2001 From: Robonau <30987265+Robonau@users.noreply.github> Date: Tue, 29 Oct 2024 21:54:09 +0000 Subject: [PATCH 2/3] update complex stores to state --- src/lib/components/TriStateSlide.svelte | 2 +- src/lib/components/lightswitch.svelte | 16 +- src/lib/gql/graphqlClient.ts | 11 +- src/lib/simpleStores.svelte.ts | 568 ++++++++++++++++++ src/lib/simpleStores.ts | 283 --------- src/lib/{util.ts => util.svelte.ts} | 86 ++- src/routes/(app)/(library)/+page.svelte | 104 ++-- .../(app)/(library)/LibraryFilterModal.svelte | 23 +- .../(library)/LibraryMassCategoryModal.svelte | 20 +- .../(app)/(library)/libraryActions.svelte | 2 +- .../browse/HorisontalmangaElement.svelte | 14 +- .../(app)/browse/extensions/+page.svelte | 41 +- .../browse/extensions/ExtensionCard.svelte | 2 +- .../extensions/ExtensionsActions.svelte | 2 +- src/routes/(app)/browse/globalSearch.svelte | 16 +- src/routes/(app)/browse/migrate/+page.svelte | 13 +- .../migrate/manga/[MangaID]/+page.svelte | 22 +- .../manga/[MangaID]/migrateModal.svelte | 2 +- .../migrate/source/[SourceID]/+page.svelte | 42 +- .../browse/source/[sourceID]/+layout.svelte | 9 +- .../browse/source/[sourceID]/Grid.svelte | 45 +- .../source/[sourceID]/PreferencesModal.svelte | 28 +- .../source/[sourceID]/filter/+page.svelte | 24 +- .../[sourceID]/filter/FilterModal.svelte | 31 +- .../filter/filters/TriStateFilter.svelte | 2 +- src/routes/(app)/browse/sources/+page.svelte | 21 +- src/routes/(app)/history/+page.svelte | 67 ++- .../manga/[MangaID]/(manga)/+page.svelte | 26 +- .../(manga)/ChaptersFilterModal.svelte | 35 +- .../manga/[MangaID]/(manga)/InfoSide.svelte | 73 +-- .../[MangaID]/(manga)/MangaActions.svelte | 8 +- .../(manga)/MangaCatagoryModal.svelte | 19 +- .../manga/[MangaID]/(manga)/NotesModal.svelte | 12 +- .../[MangaID]/(manga)/TrackingModal.svelte | 106 ++-- .../[MangaID]/(manga)/chaptersSide.svelte | 70 +-- .../[MangaID]/(manga)/mangaStores.svelte.ts | 44 ++ .../manga/[MangaID]/(manga)/mangaStores.ts | 15 - .../chapter/[ChapterID]/+layout@.svelte | 2 +- .../chapter/[ChapterID]/+page.svelte | 112 ++-- .../chapter/[ChapterID]/chapterDrawer.svelte | 130 ++-- ...apterStores.ts => chapterStores.svelte.ts} | 30 + src/routes/(app)/manga/[MangaID]/util.ts | 22 +- src/routes/(app)/settings/+page.svelte | 16 +- src/routes/(app)/settings/BackupModal.svelte | 38 +- .../(app)/settings/LibrarySettings.svelte | 5 +- .../(app)/settings/MangaSettingsModal.svelte | 36 +- .../(app)/settings/ReaderDefaultsModal.svelte | 32 +- .../(app)/settings/TrackingModal.svelte | 17 +- src/routes/(app)/settings/about/+page.svelte | 30 +- .../(app)/settings/categories/+page.svelte | 44 +- src/routes/(app)/settings/server/+page.svelte | 123 ++-- .../components/extensionReposModal.svelte | 2 +- src/routes/(app)/updates/+page.svelte | 97 +-- .../(app)/updates/UpdatesActions.svelte | 2 +- src/routes/+layout.svelte | 21 +- src/routes/+layout.ts | 4 +- src/routes/QuickSearchModal.svelte | 82 +-- 57 files changed, 1586 insertions(+), 1163 deletions(-) create mode 100644 src/lib/simpleStores.svelte.ts delete mode 100644 src/lib/simpleStores.ts rename src/lib/{util.ts => util.svelte.ts} (78%) create mode 100644 src/routes/(app)/manga/[MangaID]/(manga)/mangaStores.svelte.ts delete mode 100644 src/routes/(app)/manga/[MangaID]/(manga)/mangaStores.ts rename src/routes/(app)/manga/[MangaID]/chapter/[ChapterID]/{chapterStores.ts => chapterStores.svelte.ts} (56%) diff --git a/src/lib/components/TriStateSlide.svelte b/src/lib/components/TriStateSlide.svelte index dcdc434..c576e9b 100644 --- a/src/lib/components/TriStateSlide.svelte +++ b/src/lib/components/TriStateSlide.svelte @@ -11,7 +11,7 @@ // Types import type { CssClasses, SvelteEvent } from '@skeletonlabs/skeleton'; - import type { TriState } from '$lib/util'; + import type { TriState } from '$lib/util.svelte'; // Base Styles const cBase = 'inline-block'; diff --git a/src/lib/components/lightswitch.svelte b/src/lib/components/lightswitch.svelte index 2edd87a..279a4e2 100644 --- a/src/lib/components/lightswitch.svelte +++ b/src/lib/components/lightswitch.svelte @@ -7,7 +7,7 @@ --> diff --git a/src/lib/gql/graphqlClient.ts b/src/lib/gql/graphqlClient.ts index 7445d30..be0bc17 100644 --- a/src/lib/gql/graphqlClient.ts +++ b/src/lib/gql/graphqlClient.ts @@ -44,10 +44,9 @@ import type { updateTrack } from './Mutations'; import type { ResultOf, VariablesOf } from '$lib/gql/graphql'; -import { get } from 'svelte/store'; import { lastFetched } from '../../../src/routes/(app)/browse/extensions/ExtensionsStores'; -import { Meta } from '$lib/simpleStores'; import { introspection } from '../../graphql-env'; +import { gmState } from '$lib/simpleStores.svelte'; // import type { downloadChanged } from './Subscriptions'; export const client = new Client({ @@ -752,11 +751,11 @@ function fetchExtensionsUpdater( ) { if (!data?.fetchExtensions) return; let filteredExtensions = data.fetchExtensions.extensions; - if (!get(Meta).nsfw) + if (!gmState.value.nsfw) filteredExtensions = filteredExtensions.filter((e) => !e.isNsfw); filteredExtensions.forEach((e) => { cache.writeFragment(ExtensionTypeFragment, e, { - isNsfw: get(Meta).nsfw ? null : false + isNsfw: gmState.value.nsfw ? null : false }); }); lastFetched.set(new Date()); @@ -875,7 +874,7 @@ function updateExtentionsList( cache.updateQuery( { query: getExtensions, - variables: { isNsfw: get(Meta).nsfw ? null : false } + variables: { isNsfw: gmState.value.nsfw ? null : false } }, (extensionsData) => { if (!extensionsData) return extensionsData; @@ -901,7 +900,7 @@ function updateSourcesList( cache.updateQuery( { query: getSources, - variables: { isNsfw: get(Meta).nsfw ? null : false } + variables: { isNsfw: gmState.value.nsfw ? null : false } }, (sourcesData) => { if (!sourcesData) return sourcesData; diff --git a/src/lib/simpleStores.svelte.ts b/src/lib/simpleStores.svelte.ts new file mode 100644 index 0000000..1777535 --- /dev/null +++ b/src/lib/simpleStores.svelte.ts @@ -0,0 +1,568 @@ +// Copyright (c) 2023 Contributors to the Suwayomi project +// +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. + +// import { localStorageStore } from '@skeletonlabs/skeleton'; +import { + queryStore, + type OperationResult, + type OperationResultSource, + type OperationResultState, + type OperationResultStore +} from '@urql/svelte'; +import type { ResultOf, VariablesOf } from '$lib/gql/graphql'; +import { get, writable } from 'svelte/store'; +import type { ToastStore } from './components/Toast/types'; +import { + deleteGlobalMeta, + deleteMangaMeta, + setGlobalMeta, + setMangaMeta +} from './gql/Mutations'; +import { getManga, metas } from './gql/Queries'; +import { client } from './gql/graphqlClient'; +import type { presetConst } from './presets'; +import type { TriState } from './util.svelte'; +import { browser } from '$app/environment'; +import { untrack } from 'svelte'; + +// function getObjectEntries(obj: T): [keyof T, T[keyof T]][] { +// return Object.entries(obj) as [keyof T, T[keyof T]][]; +// } +function getObjectKeys(obj: T): (keyof T)[] { + return Object.keys(obj) as (keyof T)[]; +} + +export const toastStore = writable(null); + +type Themes = (typeof presetConst)[number]['name']; + +export enum ChapterTitle { + 'Source Title' = 'Source Title', + 'Chapter Number' = 'Chapter Number' +} + +export enum ChapterSort { + Source = 'Source', + 'Fetched Date' = 'Fetched Date', + 'Upload Date' = 'Upload Date' +} + +export enum Mode { + Vertical = 'Vertical', + single = 'single', + RTL = 'RTL', + LTR = 'LTR' +} + +export enum Layout { + L = 'L', + RAL = 'RAL', + Kindle = 'Kindle', + Edge = 'Edge' +} + +export enum sort { + Unread = 'Unread', + Alphabetical = 'Alphabetical', + ID = 'ID', + 'Latest Read' = 'Latest Read', + 'Latest Fetched' = 'Latest Fetched', + 'Latest Uploaded' = 'Latest Uploaded', + Random = 'Random' +} + +export enum display { + Compact = 'Compact', + Comfortable = 'Comfortable' + // List = 'List' // WIP +} + +const mangaMetaDefaults = { + ChapterUnread: 0 as TriState, + ChapterDownloaded: 0 as TriState, + ChapterBookmarked: 0 as TriState, + ChapterSort: ChapterSort.Source, + ChapterAsc: false, + ChapterFetchUpload: false, + ChapterTitle: ChapterTitle['Source Title'], + Margins: false, + Scale: false, + Offset: false, + SmoothScroll: true, + ReaderMode: Mode.Vertical, + NavLayout: Layout.L, + preLoadNextChapter: true, + mobileFullScreenOnChapterPage: true, + doPageIndicator: false as boolean | undefined, + notes: '' as string | undefined, + showMissingChapters: false as boolean | undefined, + groupPartials: [] as string[] | undefined +}; + +const trueDefaults = { + nsfw: true, + ignoreFiltersWhenSearching: false, + theme: 'skeleton' as Themes, + dark: true, + Display: display.Compact, + Sort: sort.ID, + Asc: true, + Unread: 0 as TriState, + Downloaded: 0 as TriState, + Tracked: 0 as TriState, + mangaMetaDefaults, + downloadsBadge: true, + unreadBadge: true, + mangaUpdatesTracking: { + enabled: false, + username: '', + password: '', + Authorization: '' + }, + libraryCategoryTotalCounts: false, + DownloadAllChaptersOnAddToLibrary: false, + DeleteAllChaptersOnRemoveFromLibrary: false, + RemoveChaptersFromDownloadQueueOnRemoveFromLibrary: false +}; + +// function GlobalMeta() { +// const Meta = queryStore({ +// client, +// query: metas +// }); +// const store = localStorageStore('GlobalMeta', trueDefaults); + +// if (get(store).mangaUpdatesTracking === undefined) { +// store.update((newStore) => { +// newStore.mangaUpdatesTracking = trueDefaults.mangaUpdatesTracking; +// return newStore; +// }); +// } + +// Meta.subscribe((queryResult) => { +// store.update((value) => { +// return extractGlobalMeta(value, queryResult); +// }); +// }); + +// function extractGlobalMeta( +// value: typeof trueDefaults, +// queryResult: OperationResultState> +// ): typeof trueDefaults { +// const globalMetaCopy = { ...get(store) } as typeof trueDefaults; +// const metas = queryResult.data?.metas?.nodes || []; +// getObjectKeys(value).forEach( +// (key: T) => { +// const foundMeta = metas.find( +// (node) => node.key.replace('VUI3_', '') === key +// ); +// if (foundMeta) { +// globalMetaCopy[key] = JSON.parse( +// foundMeta.value +// ) as (typeof trueDefaults)[T]; +// } +// } +// ); +// return globalMetaCopy; +// } + +// async function set(val: typeof trueDefaults) { +// for (const [key, value] of getObjectEntries(val)) { +// const stringValue = JSON.stringify(value); +// const metaKey = `VUI3_${key}`; +// const existingValue = get(Meta).data?.metas?.nodes.find( +// (e) => e.key === metaKey +// )?.value; + +// if (stringValue !== existingValue) { +// try { +// //update early for instant UI updates +// store.update((vall) => { +// (vall[key] as unknown) = value; +// return vall; +// }); + +// const variables = { key: metaKey, value: stringValue }; +// if (stringValue !== JSON.stringify(trueDefaults[key])) { +// await client.mutation(setGlobalMeta, variables).toPromise(); +// } else if (existingValue !== undefined) { +// await client.mutation(deleteGlobalMeta, variables).toPromise(); +// } +// } catch {} +// } +// } +// } + +// async function update( +// func: (value: typeof trueDefaults) => typeof trueDefaults +// ) { +// const currentStore = get(store); +// const updatedStore = func(currentStore); +// set(updatedStore); +// } + +// return { +// subscribe: store.subscribe, +// set, +// update +// }; +// } + +// export const Meta = GlobalMeta(); + +// getObjectKeys(mangaMetaDefaults).forEach((key) => { +// if (get(Meta).mangaMetaDefaults[key] === undefined) { +// Meta.update((old) => { +// const tmp = old; +// tmp.mangaMetaDefaults[key] = mangaMetaDefaults[key] as never; +// return tmp; +// }); +// } +// }); + +// export function MangaMeta(id: number) { +// const MMeta = queryStore({ +// client, +// query: getManga, +// variables: { id } +// }); +// const store = writable(get(Meta).mangaMetaDefaults); + +// MMeta.subscribe((queryResult) => { +// store.update((value) => { +// return extractMangaMeta(value, queryResult); +// }); +// }); + +// function extractMangaMeta( +// newMeta: typeof mangaMetaDefaults, +// queryResult: OperationResultState> +// ): typeof mangaMetaDefaults { +// const clonedStore = { +// ...get(Meta).mangaMetaDefaults +// } as typeof mangaMetaDefaults; +// const metas = queryResult.data?.manga.meta || []; +// getObjectKeys(newMeta).forEach( +// (key: T) => { +// const matchedMeta = metas.find( +// (meta) => meta.key.replace('VUI3_', '') === key +// ); +// if (!matchedMeta) return; +// clonedStore[key] = JSON.parse( +// matchedMeta.value +// ) as (typeof mangaMetaDefaults)[T]; +// } +// ); +// return clonedStore; +// } + +// async function set(value: typeof mangaMetaDefaults) { +// for (const [key, val] of getObjectEntries(value)) { +// const jsonValue = JSON.stringify(val); +// const cacheKey = `VUI3_${key}`; +// const cachedValue = get(MMeta).data?.manga?.meta.find( +// (e) => e.key === cacheKey +// )?.value; + +// if (jsonValue !== cachedValue) { +// try { +// //update early for instant UI updates +// store.update((vall) => { +// (vall[key] as unknown) = val; +// return vall; +// }); + +// const variables = { key: cacheKey, value: jsonValue, id }; +// if (val !== get(Meta).mangaMetaDefaults[key]) { +// await client.mutation(setMangaMeta, variables); +// } else if (cachedValue !== undefined) { +// await client.mutation(deleteMangaMeta, variables); +// } +// } catch {} +// } +// } +// } + +// async function update( +// func: (value: typeof mangaMetaDefaults) => typeof mangaMetaDefaults +// ) { +// const value = get(store); +// const updatedValue = func(value); +// set(updatedValue); +// } + +// return { +// subscribe: store.subscribe, +// set, +// update +// }; +// } + +class LocalStore { + value = $state() as T; + cleanup = () => {}; + + constructor(key: string, value: T) { + this.value = value; + + if (browser) { + const item = localStorage.getItem(key); + if (item) this.value = this.#deserialize(item); + } + this.cleanup = $effect.root(() => { + $effect(() => { + localStorage.setItem(key, this.#serialize(this.value)); + }); + }); + } + + #serialize(value: T): string { + return JSON.stringify(value); + } + + #deserialize(item: string): T { + return JSON.parse(item); + } +} + +function localStore(key: string, value: T) { + return new LocalStore(key, value); +} + +class GMState { + private store = localStore('GlobalMeta', trueDefaults); + private runningMutations = false; + private mutations: OperationResultSource< + | OperationResult< + ResultOf, + VariablesOf + > + | OperationResult< + ResultOf, + VariablesOf + > + >[] = []; + private unSub = () => {}; + constructor() { + $effect.root(() => { + const Meta = queryStore({ + client, + query: metas + }); + + this.unSub = Meta.subscribe((queryResult) => { + this.store.value = this.extractGlobalMeta(queryResult); + if (queryResult.fetching) return; + this.unSub(); + }); + $effect(() => { + for (const key of getObjectKeys(trueDefaults)) { + const stringValue = JSON.stringify(this.store.value[key]); + const metaKey = `VUI3_g2_${key}`; + const existingValue = get(Meta).data?.metas?.nodes.find( + (e) => e.key === metaKey + )?.value; + + if (stringValue !== existingValue) { + const variables = { key: metaKey, value: stringValue }; + if (stringValue !== JSON.stringify(trueDefaults[key])) { + this.mutations.push(client.mutation(setGlobalMeta, variables)); + continue; + } + if (existingValue !== undefined) { + this.mutations.push(client.mutation(deleteGlobalMeta, variables)); + continue; + } + if ( + this.store.value[key] === undefined && + trueDefaults[key] !== undefined + ) { + (this.store.value[key] as (typeof trueDefaults)[typeof key]) = + trueDefaults[key]; + } + } + } + this.startRunMutations(); + }); + }); + } + + private async startRunMutations() { + if (this.runningMutations) return; + this.runningMutations = true; + while (this.mutations.length > 0) { + const mutation = this.mutations.shift(); + await mutation?.toPromise(); + } + this.runningMutations = false; + } + + private extractGlobalMeta( + queryResult: OperationResultState> + ): typeof trueDefaults { + const globalMetaCopy = $state.snapshot(this.store.value); + const metas = queryResult.data?.metas?.nodes || []; + getObjectKeys(trueDefaults).forEach( + (key: T) => { + const foundMeta = metas.find( + (node) => node.key.replace('VUI3_', '') === key + ); + if (!foundMeta) { + globalMetaCopy[key] = trueDefaults[key]; + return; + } + globalMetaCopy[key] = JSON.parse( + foundMeta.value + ) as (typeof trueDefaults)[T]; + } + ); + return globalMetaCopy; + } + + get value() { + return this.store.value; + } + + set value(val: typeof trueDefaults) { + this.store.value = val; + } +} + +export const gmState = new GMState(); + +class MMState { + private _id: number = -1; + private store = $state( + gmState.value.mangaMetaDefaults + ); + private unSub = () => {}; + private MMeta: OperationResultStore> | null = null; + private isRunningMutations = false; + private pendingMutations: OperationResultSource< + | OperationResult< + ResultOf, + VariablesOf + > + | OperationResult< + ResultOf, + VariablesOf + > + >[] = []; + cleanup = () => {}; + + constructor(id: number) { + if (id != -1) this.id = id; + this.cleanup = $effect.root(() => { + $effect(() => { + for (const key of getObjectKeys( + untrack(() => { + return $state.snapshot(gmState.value.mangaMetaDefaults); + }) + )) { + const serializedValue = JSON.stringify(this.store[key]); + const metaKey = `VUI3_${key}`; + const storedValue = untrack(() => { + if (!this.MMeta) return; + return get(this.MMeta).data?.manga?.meta.find( + (e) => e.key === metaKey + )?.value; + }); + if (serializedValue !== storedValue) { + const mutationVariables = { + key: metaKey, + value: serializedValue, + id: this._id + }; + untrack(() => { + if ( + serializedValue !== + JSON.stringify(gmState.value.mangaMetaDefaults[key]) + ) { + this.pendingMutations.push( + client.mutation(setMangaMeta, mutationVariables) + ); + return; + } + if (storedValue !== undefined) { + this.pendingMutations.push( + client.mutation(deleteMangaMeta, mutationVariables) + ); + return; + } + if ( + this.store[key] === undefined && + gmState.value.mangaMetaDefaults[key] !== undefined + ) { + (this.store[key] as (typeof this.store)[typeof key]) = + gmState.value.mangaMetaDefaults[key]; + } + }); + } + } + this.runPendingMutations(); + }); + }); + } + + get value() { + return this.store; + } + + set value(val: typeof mangaMetaDefaults) { + this.store = val; + } + + set id(val: number) { + this._id = val; + this.unSub(); + this.MMeta = queryStore({ + client, + query: getManga, + variables: { id: val } + }); + this.unSub = this.MMeta.subscribe((queryResult) => { + this.store = this.extractMangaMeta(queryResult); + if (queryResult.fetching) return; + this.unSub(); + }); + } + + private async runPendingMutations() { + if (this.isRunningMutations) return; + this.isRunningMutations = true; + while (this.pendingMutations.length > 0) { + const mutation = this.pendingMutations.shift(); + await mutation?.toPromise(); + } + this.isRunningMutations = false; + } + + private extractMangaMeta( + queryResult: OperationResultState> + ): typeof mangaMetaDefaults { + const clonedStore = $state.snapshot(this.store); + const metas = queryResult.data?.manga.meta || []; + getObjectKeys($state.snapshot(gmState.value.mangaMetaDefaults)).forEach( + (key: T) => { + const matchedMeta = metas.find( + (meta) => meta.key.replace('VUI3_', '') === key + ); + if (!matchedMeta) { + clonedStore[key] = $state.snapshot(gmState.value.mangaMetaDefaults)[ + key + ]; + return; + } + clonedStore[key] = JSON.parse( + matchedMeta.value + ) as (typeof mangaMetaDefaults)[T]; + } + ); + return clonedStore; + } +} + +export const mmState = new MMState(-1); diff --git a/src/lib/simpleStores.ts b/src/lib/simpleStores.ts deleted file mode 100644 index 0194c62..0000000 --- a/src/lib/simpleStores.ts +++ /dev/null @@ -1,283 +0,0 @@ -// Copyright (c) 2023 Contributors to the Suwayomi project -// -// This Source Code Form is subject to the terms of the Mozilla Public -// License, v. 2.0. If a copy of the MPL was not distributed with this -// file, You can obtain one at http://mozilla.org/MPL/2.0/. - -import { localStorageStore } from '@skeletonlabs/skeleton'; -import { queryStore, type OperationResultState } from '@urql/svelte'; -import type { ResultOf } from '$lib/gql/graphql'; -import { get, writable } from 'svelte/store'; -import type { ToastStore } from './components/Toast/types'; -import { - deleteGlobalMeta, - deleteMangaMeta, - setGlobalMeta, - setMangaMeta -} from './gql/Mutations'; -import { getManga, metas } from './gql/Queries'; -import { client } from './gql/graphqlClient'; -import type { presetConst } from './presets'; -import type { TriState } from './util'; - -function getObjectEntries(obj: T): [keyof T, T[keyof T]][] { - return Object.entries(obj) as [keyof T, T[keyof T]][]; -} -function getObjectKeys(obj: T): (keyof T)[] { - return Object.keys(obj) as (keyof T)[]; -} - -export const toastStore = writable(null); - -type Themes = (typeof presetConst)[number]['name']; - -export enum ChapterTitle { - 'Source Title' = 'Source Title', - 'Chapter Number' = 'Chapter Number' -} - -export enum ChapterSort { - Source = 'Source', - 'Fetched Date' = 'Fetched Date', - 'Upload Date' = 'Upload Date' -} - -export enum Mode { - Vertical = 'Vertical', - single = 'single', - RTL = 'RTL', - LTR = 'LTR' -} - -export enum Layout { - L = 'L', - RAL = 'RAL', - Kindle = 'Kindle', - Edge = 'Edge' -} - -const mangaMetaDefaults = { - ChapterUnread: 0 as TriState, - ChapterDownloaded: 0 as TriState, - ChapterBookmarked: 0 as TriState, - ChapterSort: ChapterSort.Source, - ChapterAsc: false, - ChapterFetchUpload: false, - ChapterTitle: ChapterTitle['Source Title'], - Margins: false, - Scale: false, - Offset: false, - SmoothScroll: true, - ReaderMode: Mode.Vertical, - NavLayout: Layout.L, - preLoadNextChapter: true, - mobileFullScreenOnChapterPage: true, - doPageIndicator: false as boolean | undefined, - notes: '' as string | undefined, - showMissingChapters: false as boolean | undefined, - groupPartials: [] as string[] | undefined -}; -type mangaMeta = typeof mangaMetaDefaults; - -export enum sort { - Unread = 'Unread', - Alphabetical = 'Alphabetical', - ID = 'ID', - 'Latest Read' = 'Latest Read', - 'Latest Fetched' = 'Latest Fetched', - 'Latest Uploaded' = 'Latest Uploaded', - Random = 'Random' -} - -export enum display { - Compact = 'Compact', - Comfortable = 'Comfortable' - // List = 'List' // WIP -} - -const trueDefaults = { - nsfw: true, - ignoreFiltersWhenSearching: false, - theme: 'skeleton' as Themes, - dark: true, - Display: display.Compact, - Sort: sort.ID, - Asc: true, - Unread: 0 as TriState, - Downloaded: 0 as TriState, - Tracked: 0 as TriState, - mangaMetaDefaults, - downloadsBadge: true, - unreadBadge: true, - mangaUpdatesTracking: { - enabled: false, - username: '', - password: '', - Authorization: '' - }, - libraryCategoryTotalCounts: false, - DownloadAllChaptersOnAddToLibrary: false, - DeleteAllChaptersOnRemoveFromLibrary: false, - RemoveChaptersFromDownloadQueueOnRemoveFromLibrary: false -}; - -type globalMeta = typeof trueDefaults; - -function GlobalMeta() { - const Meta = queryStore({ - client, - query: metas - }); - const store = localStorageStore('GlobalMeta', trueDefaults); - - if (get(store).mangaUpdatesTracking === undefined) { - store.update((newStore) => { - newStore.mangaUpdatesTracking = trueDefaults.mangaUpdatesTracking; - return newStore; - }); - } - - Meta.subscribe((queryResult) => { - store.update((value) => { - return extractGlobalMeta(value, queryResult); - }); - }); - - function extractGlobalMeta( - value: typeof trueDefaults, - queryResult: OperationResultState> - ): globalMeta { - const globalMetaCopy = { ...get(store) } as globalMeta; - const metas = queryResult.data?.metas?.nodes || []; - getObjectKeys(value).forEach((key: T) => { - const foundMeta = metas.find( - (node) => node.key.replace('VUI3_', '') === key - ); - if (foundMeta) { - globalMetaCopy[key] = JSON.parse(foundMeta.value) as globalMeta[T]; - } - }); - return globalMetaCopy; - } - - async function set(val: globalMeta) { - for (const [key, value] of getObjectEntries(val)) { - const stringValue = JSON.stringify(value); - const metaKey = `VUI3_${key}`; - const existingValue = get(Meta).data?.metas?.nodes.find( - (e) => e.key === metaKey - )?.value; - - if (stringValue !== existingValue) { - try { - //update early for instant UI updates - store.update((vall) => { - (vall[key] as unknown) = value; - return vall; - }); - - const variables = { key: metaKey, value: stringValue }; - if (stringValue !== JSON.stringify(trueDefaults[key])) { - await client.mutation(setGlobalMeta, variables).toPromise(); - } else if (existingValue !== undefined) { - await client.mutation(deleteGlobalMeta, variables).toPromise(); - } - } catch {} - } - } - } - - async function update(func: (value: globalMeta) => globalMeta) { - const currentStore = get(store); - const updatedStore = func(currentStore); - set(updatedStore); - } - - return { - subscribe: store.subscribe, - set, - update - }; -} - -export const Meta = GlobalMeta(); - -getObjectKeys(mangaMetaDefaults).forEach((key) => { - if (get(Meta).mangaMetaDefaults[key] === undefined) { - Meta.update((old) => { - const tmp = old; - tmp.mangaMetaDefaults[key] = mangaMetaDefaults[key] as never; - return tmp; - }); - } -}); - -export function MangaMeta(id: number) { - const MMeta = queryStore({ - client, - query: getManga, - variables: { id } - }); - const store = writable(get(Meta).mangaMetaDefaults); - - MMeta.subscribe((queryResult) => { - store.update((value) => { - return extractMangaMeta(value, queryResult); - }); - }); - - function extractMangaMeta( - newMeta: mangaMeta, - queryResult: OperationResultState> - ): mangaMeta { - const clonedStore = { ...get(Meta).mangaMetaDefaults } as mangaMeta; - const metas = queryResult.data?.manga.meta || []; - getObjectKeys(newMeta).forEach((key: T) => { - const matchedMeta = metas.find( - (meta) => meta.key.replace('VUI3_', '') === key - ); - if (!matchedMeta) return; - clonedStore[key] = JSON.parse(matchedMeta.value) as mangaMeta[T]; - }); - return clonedStore; - } - - async function set(value: mangaMeta) { - for (const [key, val] of getObjectEntries(value)) { - const jsonValue = JSON.stringify(val); - const cacheKey = `VUI3_${key}`; - const cachedValue = get(MMeta).data?.manga?.meta.find( - (e) => e.key === cacheKey - )?.value; - - if (jsonValue !== cachedValue) { - try { - //update early for instant UI updates - store.update((vall) => { - (vall[key] as unknown) = val; - return vall; - }); - - const variables = { key: cacheKey, value: jsonValue, id }; - if (val !== get(Meta).mangaMetaDefaults[key]) { - await client.mutation(setMangaMeta, variables); - } else if (cachedValue !== undefined) { - await client.mutation(deleteMangaMeta, variables); - } - } catch {} - } - } - } - - async function update(func: (value: mangaMeta) => mangaMeta) { - const value = get(store); - const updatedValue = func(value); - set(updatedValue); - } - - return { - subscribe: store.subscribe, - set, - update - }; -} diff --git a/src/lib/util.ts b/src/lib/util.svelte.ts similarity index 78% rename from src/lib/util.ts rename to src/lib/util.svelte.ts index 75bfb10..9011b45 100644 --- a/src/lib/util.ts +++ b/src/lib/util.svelte.ts @@ -6,7 +6,7 @@ import { get, type Writable } from 'svelte/store'; -import { toastStore } from './simpleStores'; +import { toastStore } from './simpleStores.svelte'; import { client } from './gql/graphqlClient'; import { deleteDownloadedChapters, @@ -15,10 +15,26 @@ import { updateChapters } from './gql/Mutations'; import type { VariablesOf } from '$lib/gql/graphql'; -import type { OperationResult } from '@urql/svelte'; +import { + mutationStore, + queryStore, + type AnyVariables, + type MutationArgs, + type OperationContext, + type OperationResult, + type OperationResultState, + type QueryArgs +} from '@urql/svelte'; import { introspection } from '../graphql-env'; export type TriState = 0 | 1 | 2; +export type OperationResultF< + /* eslint-disable-next-line @typescript-eslint/no-explicit-any */ + Data = any, + Variables extends AnyVariables = AnyVariables +> = OperationResult & { + fetching: boolean; +}; export function HelpDoSelect( update: T, @@ -316,3 +332,69 @@ export function formatDate(date: Date) { return date.toLocaleString(); } + +export type queryStateReturn< + // eslint-disable-next-line @typescript-eslint/no-explicit-any + Data = any, + Variables extends AnyVariables = AnyVariables +> = { + get value(): OperationResultState; + get isPaused$(): boolean; + pause(): void; + resume(): void; + reexecute(context: Partial): void; +}; + +export function queryState< + // eslint-disable-next-line @typescript-eslint/no-explicit-any + Data = any, + Variables extends AnyVariables = AnyVariables +>(args: QueryArgs): queryStateReturn { + const store = queryStore(args); + let queryState = $state(get(store)); + let isPaused = $state(get(store.isPaused$)); + + store.subscribe((value) => { + queryState = value; + }); + store.isPaused$.subscribe((value) => { + isPaused = value; + }); + + return { + get value() { + return queryState; + }, + get isPaused$() { + return isPaused; + }, + pause: store.pause, + resume: store.resume, + reexecute: store.reexecute + }; +} + +export type mutationStateReturn< + // eslint-disable-next-line @typescript-eslint/no-explicit-any + Data = any, + Variables extends AnyVariables = AnyVariables +> = { + get value(): OperationResultState; +}; + +export function mutationState< + // eslint-disable-next-line @typescript-eslint/no-explicit-any + Data = any, + Variables extends AnyVariables = AnyVariables +>(args: MutationArgs): mutationStateReturn { + const store = mutationStore(args); + let mutationState = $state(get(store)); + store.subscribe((value) => { + mutationState = value; + }); + return { + get value() { + return mutationState; + } + }; +} diff --git a/src/routes/(app)/(library)/+page.svelte b/src/routes/(app)/(library)/+page.svelte index 6eb94bc..4ef0e63 100644 --- a/src/routes/(app)/(library)/+page.svelte +++ b/src/routes/(app)/(library)/+page.svelte @@ -10,27 +10,28 @@ import IntersectionObserver from '$lib/components/IntersectionObserver.svelte'; import MangaCard from '$lib/components/MangaCard.svelte'; import { longPress } from '$lib/press'; - import { display, Meta, sort } from '$lib/simpleStores'; + import { display, gmState, sort } from '$lib/simpleStores.svelte'; import { Tab, TabGroup } from '@skeletonlabs/skeleton'; import { queryParam, ssp } from 'sveltekit-search-params'; import LibraryActions from './libraryActions.svelte'; import { selected, selectMode, type MangaType } from './LibraryStores'; - import { onMount } from 'svelte'; + import { onMount, untrack } from 'svelte'; import { AppBarData } from '$lib/MountTitleAction'; import { errortoast, gridValues, HelpDoSelect, - HelpSelectAll - } from '$lib/util'; + HelpSelectAll, + queryState + } from '$lib/util.svelte'; import IconWrapper from '$lib/components/IconWrapper.svelte'; import { parseQuery, type ANO, type parsedQueryType } from './queryParse'; import { getCategories, getCategory } from '$lib/gql/Queries'; - import { getContextClient, queryStore } from '@urql/svelte'; + import { getContextClient } from '@urql/svelte'; const client = getContextClient(); - const categories = queryStore({ + const categories = queryState({ client, query: getCategories }); @@ -212,7 +213,7 @@ validateParsedQuery(parsedQuery); }); let orderedCategories = $derived( - ($categories.data?.categories?.nodes ?? []) + (categories.value.data?.categories?.nodes ?? []) .filter((e) => e.mangas.totalCount) .sort((a, b) => { return a.order > b.order ? 1 : -1; @@ -228,23 +229,27 @@ }); } }); - let mangas = $derived( - queryStore({ - client, - query: getCategory, - variables: { id: $tab ?? 0 }, - requestPolicy: 'cache-first' - }) - ); + + let mangas = $derived.by(() => { + const _ = [$tab]; + return untrack(() => + queryState({ + client, + query: getCategory, + variables: { id: $tab ?? 0 }, + requestPolicy: 'cache-first' + }) + ); + }); $effect(() => { if ($selectMode === false) { $selected = []; } }); let filteredMangas = $derived( - $mangas.data?.category?.mangas.nodes.filter((ele) => { + mangas.value.data?.category?.mangas.nodes.filter((ele) => { if (!ele.inLibrary) return false; - if ($Meta.ignoreFiltersWhenSearching) { + if (gmState.value.ignoreFiltersWhenSearching) { if ( parsedQuery !== null && specificSearch(ele, parsedQuery).findIndex((e) => e === false) === -1 @@ -253,15 +258,17 @@ } } - if ($Meta.Downloaded === 1 && ele.downloadCount === 0) return false; - if ($Meta.Downloaded === 2 && ele.downloadCount !== 0) return false; + if (gmState.value.Downloaded === 1 && ele.downloadCount === 0) + return false; + if (gmState.value.Downloaded === 2 && ele.downloadCount !== 0) + return false; - if ($Meta.Unread === 1 && ele.unreadCount === 0) return false; - if ($Meta.Unread === 2 && ele.unreadCount !== 0) return false; + if (gmState.value.Unread === 1 && ele.unreadCount === 0) return false; + if (gmState.value.Unread === 2 && ele.unreadCount !== 0) return false; - if ($Meta.Tracked === 1 && ele.trackRecords.nodes.length === 0) + if (gmState.value.Tracked === 1 && ele.trackRecords.nodes.length === 0) return false; - if ($Meta.Tracked === 2 && ele.trackRecords.nodes.length !== 0) + if (gmState.value.Tracked === 2 && ele.trackRecords.nodes.length !== 0) return false; if ( @@ -290,11 +297,11 @@ }); let sortedMangas = $derived( filteredMangas - ? $Meta.Sort === sort.Random + ? gmState.value.Sort === sort.Random ? shuffle([...filteredMangas]) : [...filteredMangas].sort((a, b) => { let tru = true; - switch ($Meta.Sort) { + switch (gmState.value.Sort) { case sort.ID: tru = a.id > b.id; break; @@ -321,14 +328,14 @@ break; } - if ($Meta.Asc) tru = !tru; + if (gmState.value.Asc) tru = !tru; return tru ? -1 : 1; }) : undefined ); -{#if $categories.fetching} +{#if categories.value.fetching}
@@ -339,10 +346,10 @@
- {#if $Meta.Display === display.Comfortable} + {#if gmState.value.Display === display.Comfortable}
@@ -350,9 +357,9 @@
{/each}
-{:else if $categories.error} +{:else if categories.value.error}
- Error loading categories: {JSON.stringify($categories.error, null, 4)} + Error loading categories: {JSON.stringify(categories.value.error, null, 4)}
{:else} @@ -361,7 +368,7 @@ {#snippet lead()} {cat.name} - {#if $Meta.libraryCategoryTotalCounts} + {#if gmState.value.libraryCategoryTotalCounts} @@ -373,16 +380,16 @@ {/each} {/if} - {#if $mangas.fetching} + {#if mangas.value.fetching}
{#each new Array(orderedCategories.find((e) => e.id === $tab)?.mangas.totalCount ?? 10) as _}
- {#if $Meta.Display === display.Comfortable} + {#if gmState.value.Display === display.Comfortable}
@@ -390,9 +397,9 @@
{/each}
- {:else if $mangas.error} + {:else if mangas.value.error}
- Error loading mangas: {JSON.stringify($mangas.error, null, 4)} + Error loading mangas: {JSON.stringify(mangas.value.error, null, 4)}
{:else if sortedMangas}
@@ -432,14 +439,15 @@ thumbnailUrl={manga.thumbnailUrl ?? ''} title={manga.title} class="select-none {$selectMode && 'opacity-80'}" - rounded="{$Meta.Display === display.Compact && + rounded="{gmState.value.Display === display.Compact && 'rounded-lg'} - {$Meta.Display === display.Comfortable && 'rounded-none rounded-t-lg'}" + {gmState.value.Display === display.Comfortable && 'rounded-none rounded-t-lg'}" >
- {#if manga.downloadCount && $Meta.downloadsBadge} + {#if manga.downloadCount && gmState.value.downloadsBadge}
{/if} - {#if manga.unreadCount && $Meta.unreadBadge} + {#if manga.unreadCount && gmState.value.unreadBadge}
{/if} - {#if $Meta.Display === display.Compact} + {#if gmState.value.Display === display.Compact}
@@ -484,7 +492,7 @@
{/if} - {#if $Meta.Display === display.Comfortable} + {#if gmState.value.Display === display.Comfortable}
{/if}
- {#if !intersecting && $Meta.Display === display.Comfortable} + {#if !intersecting && gmState.value.Display === display.Comfortable}
{/if} {/snippet} diff --git a/src/routes/(app)/(library)/LibraryFilterModal.svelte b/src/routes/(app)/(library)/LibraryFilterModal.svelte index 426453a..dc0be0f 100644 --- a/src/routes/(app)/(library)/LibraryFilterModal.svelte +++ b/src/routes/(app)/(library)/LibraryFilterModal.svelte @@ -17,8 +17,9 @@ getModalStore, localStorageStore } from '@skeletonlabs/skeleton'; - import { Meta, display, sort } from '$lib/simpleStores'; - import { enumKeys } from '$lib/util'; + import { display, gmState, sort } from '$lib/simpleStores.svelte'; + + import { enumKeys } from '$lib/util.svelte'; import Slide from '$lib/components/Slide.svelte'; const modalStore = getModalStore(); let tabSet = localStorageStore('libraryModalTabs', 0); @@ -78,7 +79,7 @@
{#if $tabSet === 0} Unread Downloaded {#each enumKeys(sort) as value} {:else if $tabSet === 2} Category Total Counts Downloads Badge Unread Badge @@ -157,7 +158,7 @@ > {#each enumKeys(display) as value} import TriStateSlide from '$lib/components/TriStateSlide.svelte'; - import { ErrorHelp } from '$lib/util'; + import { ErrorHelp, queryState } from '$lib/util.svelte'; import { getModalStore } from '@skeletonlabs/skeleton'; import { selected } from './LibraryStores'; - import { getContextClient, queryStore } from '@urql/svelte'; + import { getContextClient } from '@urql/svelte'; import { updateMangasCategories } from '$lib/gql/Mutations'; import { getCategories } from '$lib/gql/Queries'; import { type ResultOf } from '$lib/gql/graphql'; @@ -22,7 +22,7 @@ let selectedCategories: number[] = []; - const categories = queryStore({ + const categories = queryState({ client, query: getCategories }); @@ -63,14 +63,18 @@ {#if $modalStore[0]} {#snippet children()} - {#if $categories.fetching} + {#if categories.value.fetching} loading... - {:else if $categories.error} + {:else if categories.value.error}
- Error loading categories: {JSON.stringify($categories.error, null, 4)} + Error loading categories: {JSON.stringify( + categories.value.error, + null, + 4 + )}
- {:else if $categories.data} - {@const nodes = $categories.data.categories.nodes} + {:else if categories.value.data} + {@const nodes = categories.value.data.categories.nodes} {#each nodes .filter((e) => e.id !== 0) .sort((a, b) => (a.order > b.order ? 1 : -1)) as category} diff --git a/src/routes/(app)/(library)/libraryActions.svelte b/src/routes/(app)/(library)/libraryActions.svelte index 8d065de..a04ed41 100644 --- a/src/routes/(app)/(library)/libraryActions.svelte +++ b/src/routes/(app)/(library)/libraryActions.svelte @@ -21,7 +21,7 @@ import { screens } from '$lib/screens'; import IconWrapper from '$lib/components/IconWrapper.svelte'; import { selected, selectMode } from './LibraryStores'; - import { ErrorHelp } from '$lib/util'; + import { ErrorHelp } from '$lib/util.svelte'; import { getContextClient } from '@urql/svelte'; import { ConditionalChaptersOfGivenManga } from '$lib/gql/Queries'; import { enqueueChapterDownloads, updateMangas } from '$lib/gql/Mutations'; diff --git a/src/routes/(app)/browse/HorisontalmangaElement.svelte b/src/routes/(app)/browse/HorisontalmangaElement.svelte index 4883fad..ffbb95c 100644 --- a/src/routes/(app)/browse/HorisontalmangaElement.svelte +++ b/src/routes/(app)/browse/HorisontalmangaElement.svelte @@ -9,7 +9,8 @@