diff --git a/src/gql/Mutations.gql b/src/gql/Mutations.gql index 89e6bab3..1084c6b3 100644 --- a/src/gql/Mutations.gql +++ b/src/gql/Mutations.gql @@ -135,6 +135,7 @@ mutation updateExtension( isNsfw extension { pkgName + repo } } } @@ -412,3 +413,21 @@ mutation clearCachedImages { clientMutationId } } + +mutation installExternalExtension($extensionFile: Upload!) { + installExternalExtension(input: { extensionFile: $extensionFile }) { + extension { + ...ExtensionTypeFragment + source { + nodes { + ...SourceTypeFragment + isNsfw + extension { + pkgName + repo + } + } + } + } + } +} diff --git a/src/gql/Queries.gql b/src/gql/Queries.gql index 4f59fdc2..4b3df9ef 100644 --- a/src/gql/Queries.gql +++ b/src/gql/Queries.gql @@ -69,6 +69,7 @@ query sources($isNsfw: Boolean = null) { isNsfw extension { pkgName + repo } } } diff --git a/src/lib/generated.ts b/src/lib/generated.ts index 387f75e6..13143eff 100644 --- a/src/lib/generated.ts +++ b/src/lib/generated.ts @@ -2232,7 +2232,7 @@ export type UpdateExtensionMutationVariables = Exact<{ }>; -export type UpdateExtensionMutation = { __typename?: 'Mutation', updateExtension: { __typename?: 'UpdateExtensionPayload', extension?: { __typename?: 'ExtensionType', name: string, repo?: string | null, versionName: string, pkgName: string, lang: string, iconUrl: string, isNsfw: boolean, isInstalled: boolean, isObsolete: boolean, hasUpdate: boolean, source: { __typename?: 'SourceNodeList', nodes: Array<{ __typename?: 'SourceType', isNsfw: boolean, id: any, displayName: string, iconUrl: string, lang: string, extension: { __typename?: 'ExtensionType', pkgName: string } }> } } | null } }; +export type UpdateExtensionMutation = { __typename?: 'Mutation', updateExtension: { __typename?: 'UpdateExtensionPayload', extension?: { __typename?: 'ExtensionType', name: string, repo?: string | null, versionName: string, pkgName: string, lang: string, iconUrl: string, isNsfw: boolean, isInstalled: boolean, isObsolete: boolean, hasUpdate: boolean, source: { __typename?: 'SourceNodeList', nodes: Array<{ __typename?: 'SourceType', isNsfw: boolean, id: any, displayName: string, iconUrl: string, lang: string, extension: { __typename?: 'ExtensionType', pkgName: string, repo?: string | null } }> } } | null } }; export type FetchSourceMangaMutationVariables = Exact<{ page: Scalars['Int']['input']; @@ -2400,6 +2400,13 @@ export type ClearCachedImagesMutationVariables = Exact<{ [key: string]: never; } export type ClearCachedImagesMutation = { __typename?: 'Mutation', clearCachedImages: { __typename?: 'ClearCachedImagesPayload', clientMutationId?: string | null } }; +export type InstallExternalExtensionMutationVariables = Exact<{ + extensionFile: Scalars['Upload']['input']; +}>; + + +export type InstallExternalExtensionMutation = { __typename?: 'Mutation', installExternalExtension: { __typename?: 'InstallExternalExtensionPayload', extension: { __typename?: 'ExtensionType', name: string, repo?: string | null, versionName: string, pkgName: string, lang: string, iconUrl: string, isNsfw: boolean, isInstalled: boolean, isObsolete: boolean, hasUpdate: boolean, source: { __typename?: 'SourceNodeList', nodes: Array<{ __typename?: 'SourceType', isNsfw: boolean, id: any, displayName: string, iconUrl: string, lang: string, extension: { __typename?: 'ExtensionType', pkgName: string, repo?: string | null } }> } } } }; + export type CategoriesQueryVariables = Exact<{ notEqualTo?: InputMaybe; }>; @@ -2440,7 +2447,7 @@ export type SourcesQueryVariables = Exact<{ }>; -export type SourcesQuery = { __typename?: 'Query', sources: { __typename?: 'SourceNodeList', nodes: Array<{ __typename?: 'SourceType', isNsfw: boolean, id: any, displayName: string, iconUrl: string, lang: string, extension: { __typename?: 'ExtensionType', pkgName: string } }> } }; +export type SourcesQuery = { __typename?: 'Query', sources: { __typename?: 'SourceNodeList', nodes: Array<{ __typename?: 'SourceType', isNsfw: boolean, id: any, displayName: string, iconUrl: string, lang: string, extension: { __typename?: 'ExtensionType', pkgName: string, repo?: string | null } }> } }; export type SourcesMigrationQueryVariables = Exact<{ [key: string]: never; }>; @@ -2749,6 +2756,7 @@ export const UpdateExtensionDoc = gql` isNsfw extension { pkgName + repo } } } @@ -3018,6 +3026,26 @@ export const ClearCachedImagesDoc = gql` } } `; +export const InstallExternalExtensionDoc = gql` + mutation installExternalExtension($extensionFile: Upload!) { + installExternalExtension(input: {extensionFile: $extensionFile}) { + extension { + ...ExtensionTypeFragment + source { + nodes { + ...SourceTypeFragment + isNsfw + extension { + pkgName + repo + } + } + } + } + } +} + ${ExtensionTypeFragmentFragmentDoc} +${SourceTypeFragmentFragmentDoc}`; export const CategoriesDoc = gql` query categories($notEqualTo: Int = null) { categories(filter: {id: {notEqualTo: $notEqualTo}}) { @@ -3087,6 +3115,7 @@ export const SourcesDoc = gql` isNsfw extension { pkgName + repo } } } @@ -3830,6 +3859,18 @@ export const clearCachedImages = ( }); return m; } +export const installExternalExtension = ( + options: Omit< + MutationOptions, + "mutation" + > + ) => { + const m = client.mutate({ + mutation: InstallExternalExtensionDoc, + ...options, + }); + return m; + } export const categories = ( options: Omit< WatchQueryOptions, diff --git a/src/routes/(app)/browse/extensions/+page.svelte b/src/routes/(app)/browse/extensions/+page.svelte index 4ce68e76..86ea4395 100644 --- a/src/routes/(app)/browse/extensions/+page.svelte +++ b/src/routes/(app)/browse/extensions/+page.svelte @@ -9,12 +9,10 @@
+ + + + + +
diff --git a/src/routes/(app)/browse/extensions/ExtensionsStores.ts b/src/routes/(app)/browse/extensions/ExtensionsStores.ts index ac7d29b3..dad4942f 100644 --- a/src/routes/(app)/browse/extensions/ExtensionsStores.ts +++ b/src/routes/(app)/browse/extensions/ExtensionsStores.ts @@ -6,6 +6,18 @@ import { localStorageStore } from '@skeletonlabs/skeleton'; import * as devalue from 'devalue'; +import { + ExtensionsDoc, + SourcesDoc, + type ExtensionsQuery, + type FetchExtensionsMutation, + type InstallExternalExtensionMutation, + type SourcesQuery, + type UpdateExtensionMutation +} from '$lib/generated'; +import { Meta } from '$lib/simpleStores'; +import type { ApolloCache, FetchResult } from '@apollo/client'; +import { get } from 'svelte/store'; export const lastFetched = localStorageStore('lastFetchedExtensions', new Date(0), { serializer: devalue @@ -18,3 +30,117 @@ export const langFilter = localStorageStore>( serializer: devalue } ); + +export function fetchExtensionsUpdater( + cache: ApolloCache, + { data }: FetchResult +) { + if (!data) return; + let filteredExtensions = data.fetchExtensions.extensions; + if (!get(Meta).nsfw) filteredExtensions = filteredExtensions.filter((e) => !e.isNsfw); + cache.writeQuery({ + query: ExtensionsDoc, + data: { extensions: { nodes: filteredExtensions } }, + variables: { isNsfw: get(Meta).nsfw ? null : false } + }); + lastFetched.set(new Date()); +} + +function updateExtentionsList( + cache: ApolloCache, + extension: UpdateExtensionMutation['updateExtension']['extension'], + ext: ExtensionsQuery['extensions']['nodes'][0] +) { + const extensionsData = structuredClone( + cache.readQuery({ + query: ExtensionsDoc, + variables: { isNsfw: get(Meta).nsfw ? null : false } + }) + ); + if (!extensionsData) throw new Error('failed to read extensions'); + const { extensions } = extensionsData; + + if (extension) { + const index = extensions.nodes.findIndex( + (extension) => extension.pkgName === ext.pkgName && extension.repo === ext.repo + ); + if (index) extensions.nodes[index] = extension; + else extensions.nodes.push(extension); + } + + cache.writeQuery({ + query: ExtensionsDoc, + data: { extensions }, + variables: { isNsfw: get(Meta).nsfw ? null : false } + }); +} + +function updateSourcesList( + cache: ApolloCache, + extension: UpdateExtensionMutation['updateExtension']['extension'], + ext: ExtensionsQuery['extensions']['nodes'][0] +) { + const sourcesData = structuredClone( + cache.readQuery({ + query: SourcesDoc, + variables: { isNsfw: get(Meta).nsfw ? null : false } + }) + ); + if (!sourcesData) throw new Error('failed to read sources'); + const { sources } = sourcesData; + + if (extension?.isInstalled) { + extension.source.nodes.forEach((source) => { + if (!sources.nodes.find((existingSource) => existingSource.id === source.id)) { + sources.nodes.push(source); + } + }); + } else { + sources.nodes = sources.nodes.filter( + (existingSource) => + existingSource.extension.repo !== ext.repo || + existingSource.extension.pkgName !== ext.pkgName + ); + } + + cache.writeQuery({ + query: SourcesDoc, + variables: { isNsfw: get(Meta).nsfw ? null : false }, + data: { sources } + }); +} + +export function UpdateExtensionUpdater( + cache: ApolloCache, + { data }: Omit, 'context'>, + ext: ExtensionsQuery['extensions']['nodes'][0] +): void { + if (!data) return; + try { + updateExtentionsList(cache, data.updateExtension.extension, ext); + } catch {} + try { + updateSourcesList(cache, data.updateExtension.extension, ext); + } catch {} +} + +export function installExternalExtensionUpdater( + cache: ApolloCache, + { data }: FetchResult +) { + if (!data) return; + try { + updateExtentionsList( + cache, + data.installExternalExtension.extension, + data.installExternalExtension.extension + ); + } catch {} + try { + updateSourcesList( + cache, + data.installExternalExtension.extension, + data.installExternalExtension.extension + ); + } catch {} +}