diff --git a/src/components/ExtensionCard.tsx b/src/components/ExtensionCard.tsx index 3257dd42..0d202a61 100644 --- a/src/components/ExtensionCard.tsx +++ b/src/components/ExtensionCard.tsx @@ -22,6 +22,7 @@ import { makeToast } from '@/components/util/Toast.tsx'; interface IProps { extension: PartialExtension; handleUpdate: () => void; + usesCustomRepos: boolean; } enum ExtensionAction { @@ -91,8 +92,9 @@ export function ExtensionCard(props: IProps) { const { t } = useTranslation(); const { - extension: { name, lang, versionName, isInstalled, hasUpdate, isObsolete, pkgName, iconUrl, isNsfw }, + extension: { name, lang, versionName, isInstalled, hasUpdate, isObsolete, pkgName, iconUrl, isNsfw, repo }, handleUpdate, + usesCustomRepos, } = props; const [installedState, setInstalledState] = useState( getInstalledState(isInstalled, isObsolete, hasUpdate), @@ -184,6 +186,11 @@ export function ExtensionCard(props: IProps) { )} + {usesCustomRepos && ( + + {repo} + + )} diff --git a/src/components/settings/MutableListSetting.tsx b/src/components/settings/MutableListSetting.tsx new file mode 100644 index 00000000..8555727d --- /dev/null +++ b/src/components/settings/MutableListSetting.tsx @@ -0,0 +1,131 @@ +/* + * Copyright (C) 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 https://mozilla.org/MPL/2.0/. + */ + +import { Button, Dialog, DialogTitle, ListItemButton, ListItemText, Stack, Tooltip } from '@mui/material'; +import { useEffect, useState } from 'react'; +import DialogContent from '@mui/material/DialogContent'; +import DialogActions from '@mui/material/DialogActions'; +import { useTranslation } from 'react-i18next'; +import List from '@mui/material/List'; +import DeleteIcon from '@mui/icons-material/Delete'; +import IconButton from '@mui/material/IconButton'; +import DialogContentText from '@mui/material/DialogContentText'; +import { TextSetting, TextSettingProps } from '@/components/settings/TextSetting.tsx'; + +const MutableListItem = ({ + handleDelete, + ...textSettingProps +}: Omit & { handleDelete: () => void }) => { + const { t } = useTranslation(); + + return ( + + + + + + + + + ); +}; + +export const MutableListSetting = ({ + settingName, + description, + values, + handleChange, + addItemButtonTitle, +}: { + settingName: string; + description?: string; + values?: string[]; + handleChange: (values: string[]) => void; + addItemButtonTitle?: string; +}) => { + const { t } = useTranslation(); + + const [isDialogOpen, setIsDialogOpen] = useState(false); + const [dialogValues, setDialogValues] = useState(values ?? []); + + useEffect(() => { + if (!values) { + return; + } + + setDialogValues(values); + }, [values]); + + const closeDialog = (resetValue: boolean = true) => { + if (resetValue) { + setDialogValues(values ?? []); + } + + setIsDialogOpen(false); + }; + + const updateSetting = (index: number, newValue: string | undefined) => { + const deleteValue = newValue === undefined; + if (deleteValue) { + setDialogValues(dialogValues.toSpliced(index, 1)); + return; + } + + setDialogValues(dialogValues.toSpliced(index, 1, newValue.trim())); + }; + + const saveChanges = () => { + closeDialog(); + handleChange(dialogValues.filter((dialogValue) => dialogValue !== '')); + }; + + return ( + <> + setIsDialogOpen(true)}> + + + + closeDialog()} fullWidth> + {settingName} + {!!description && ( + + {description} + + )} + + + {dialogValues.map((dialogValue, index) => ( + updateSetting(index, newValue)} + handleDelete={() => updateSetting(index, undefined)} + value={dialogValue} + /> + ))} + + + + + + + + + + + + + + ); +}; diff --git a/src/components/settings/TextSetting.tsx b/src/components/settings/TextSetting.tsx index f325d6eb..bfd461a2 100644 --- a/src/components/settings/TextSetting.tsx +++ b/src/components/settings/TextSetting.tsx @@ -17,23 +17,27 @@ import DialogContentText from '@mui/material/DialogContentText'; import IconButton from '@mui/material/IconButton'; import { Visibility, VisibilityOff } from '@mui/icons-material'; -export const TextSetting = ({ - settingName, - dialogDescription, - value, - handleChange, - isPassword = false, - placeholder = '', - disabled = false, -}: { +export type TextSettingProps = { settingName: string; + dialogTitle?: string; dialogDescription?: string; value?: string; handleChange: (value: string) => void; isPassword?: boolean; placeholder?: string; disabled?: boolean; -}) => { +}; + +export const TextSetting = ({ + settingName, + dialogTitle = settingName, + dialogDescription, + value, + handleChange, + isPassword = false, + placeholder = '', + disabled = false, +}: TextSettingProps) => { const { t } = useTranslation(); const [isDialogOpen, setIsDialogOpen] = useState(false); @@ -70,13 +74,15 @@ export const TextSetting = ({ closeDialog()} fullWidth> - {settingName} + {dialogTitle} {!!dialogDescription && ( {dialogDescription} )} diff --git a/src/i18n/locale/en.json b/src/i18n/locale/en.json index 126dfd9b..734c703a 100644 --- a/src/i18n/locale/en.json +++ b/src/i18n/locale/en.json @@ -233,6 +233,23 @@ "all": "All", "other": "Other" }, + "settings": { + "repositories": { + "custom": { + "dialog": { + "action": { + "button": { + "add": "Add repository" + } + } + }, + "label": { + "description": "Add custom repositories from which extensions can be installed", + "title": "Custom repositories" + } + } + } + }, "state": { "label": { "installed": "Installed", @@ -247,6 +264,7 @@ }, "global": { "button": { + "add": "Add", "browse": "Browse", "cancel": "Cancel", "clear": "Clear", @@ -326,6 +344,7 @@ "none": "None", "other": "Other", "password": "Password", + "placeholder": "Placeholder", "sort": "Sort", "unknown": "Unknown", "username": "Username" diff --git a/src/lib/graphql/Fragments.ts b/src/lib/graphql/Fragments.ts index 98e2d026..64a7db65 100644 --- a/src/lib/graphql/Fragments.ts +++ b/src/lib/graphql/Fragments.ts @@ -302,6 +302,7 @@ export const UPDATER_MANGA_FIELDS = gql` export const FULL_EXTENSION_FIELDS = gql` fragment FULL_EXTENSION_FIELDS on ExtensionType { apkName + repo hasUpdate iconUrl isInstalled @@ -473,6 +474,9 @@ export const SERVER_SETTINGS = gql` excludeEntryWithUnreadChapters autoDownloadAheadLimit + # extensions + extensionRepos + # requests maxSourcesInParallel diff --git a/src/lib/graphql/generated/apollo-helpers.ts b/src/lib/graphql/generated/apollo-helpers.ts index 7ab312c2..6dcd5864 100644 --- a/src/lib/graphql/generated/apollo-helpers.ts +++ b/src/lib/graphql/generated/apollo-helpers.ts @@ -254,7 +254,7 @@ export type ExtensionNodeListFieldPolicy = { pageInfo?: FieldPolicy | FieldReadFunction, totalCount?: FieldPolicy | FieldReadFunction }; -export type ExtensionTypeKeySpecifier = ('apkName' | 'hasUpdate' | 'iconUrl' | 'isInstalled' | 'isNsfw' | 'isObsolete' | 'lang' | 'name' | 'pkgName' | 'source' | 'versionCode' | 'versionName' | ExtensionTypeKeySpecifier)[]; +export type ExtensionTypeKeySpecifier = ('apkName' | 'hasUpdate' | 'iconUrl' | 'isInstalled' | 'isNsfw' | 'isObsolete' | 'lang' | 'name' | 'pkgName' | 'repo' | 'source' | 'versionCode' | 'versionName' | ExtensionTypeKeySpecifier)[]; export type ExtensionTypeFieldPolicy = { apkName?: FieldPolicy | FieldReadFunction, hasUpdate?: FieldPolicy | FieldReadFunction, @@ -265,6 +265,7 @@ export type ExtensionTypeFieldPolicy = { lang?: FieldPolicy | FieldReadFunction, name?: FieldPolicy | FieldReadFunction, pkgName?: FieldPolicy | FieldReadFunction, + repo?: FieldPolicy | FieldReadFunction, source?: FieldPolicy | FieldReadFunction, versionCode?: FieldPolicy | FieldReadFunction, versionName?: FieldPolicy | FieldReadFunction @@ -356,7 +357,7 @@ export type MangaNodeListFieldPolicy = { pageInfo?: FieldPolicy | FieldReadFunction, totalCount?: FieldPolicy | FieldReadFunction }; -export type MangaTypeKeySpecifier = ('age' | 'artist' | 'author' | 'categories' | 'chapters' | 'chaptersAge' | 'chaptersLastFetchedAt' | 'description' | 'downloadCount' | 'genre' | 'id' | 'inLibrary' | 'inLibraryAt' | 'initialized' | 'lastFetchedAt' | 'lastReadChapter' | 'meta' | 'realUrl' | 'source' | 'sourceId' | 'status' | 'thumbnailUrl' | 'title' | 'unreadCount' | 'url' | MangaTypeKeySpecifier)[]; +export type MangaTypeKeySpecifier = ('age' | 'artist' | 'author' | 'categories' | 'chapters' | 'chaptersAge' | 'chaptersLastFetchedAt' | 'description' | 'downloadCount' | 'genre' | 'id' | 'inLibrary' | 'inLibraryAt' | 'initialized' | 'lastFetchedAt' | 'lastReadChapter' | 'meta' | 'realUrl' | 'source' | 'sourceId' | 'status' | 'thumbnailUrl' | 'title' | 'unreadCount' | 'updateStrategy' | 'url' | MangaTypeKeySpecifier)[]; export type MangaTypeFieldPolicy = { age?: FieldPolicy | FieldReadFunction, artist?: FieldPolicy | FieldReadFunction, @@ -382,6 +383,7 @@ export type MangaTypeFieldPolicy = { thumbnailUrl?: FieldPolicy | FieldReadFunction, title?: FieldPolicy | FieldReadFunction, unreadCount?: FieldPolicy | FieldReadFunction, + updateStrategy?: FieldPolicy | FieldReadFunction, url?: FieldPolicy | FieldReadFunction }; export type MetaEdgeKeySpecifier = ('cursor' | 'node' | MetaEdgeKeySpecifier)[]; @@ -473,7 +475,7 @@ export type PageInfoFieldPolicy = { hasPreviousPage?: FieldPolicy | FieldReadFunction, startCursor?: FieldPolicy | FieldReadFunction }; -export type PartialSettingsTypeKeySpecifier = ('autoDownloadAheadLimit' | 'autoDownloadNewChapters' | 'backupInterval' | 'backupPath' | 'backupTTL' | 'backupTime' | 'basicAuthEnabled' | 'basicAuthPassword' | 'basicAuthUsername' | 'debugLogsEnabled' | 'downloadAsCbz' | 'downloadsPath' | 'electronPath' | 'excludeCompleted' | 'excludeEntryWithUnreadChapters' | 'excludeNotStarted' | 'excludeUnreadChapters' | 'globalUpdateInterval' | 'gqlDebugLogsEnabled' | 'initialOpenInBrowserEnabled' | 'ip' | 'localSourcePath' | 'maxSourcesInParallel' | 'port' | 'socksProxyEnabled' | 'socksProxyHost' | 'socksProxyPort' | 'systemTrayEnabled' | 'updateMangas' | 'webUIChannel' | 'webUIFlavor' | 'webUIInterface' | 'webUIUpdateCheckInterval' | PartialSettingsTypeKeySpecifier)[]; +export type PartialSettingsTypeKeySpecifier = ('autoDownloadAheadLimit' | 'autoDownloadNewChapters' | 'backupInterval' | 'backupPath' | 'backupTTL' | 'backupTime' | 'basicAuthEnabled' | 'basicAuthPassword' | 'basicAuthUsername' | 'debugLogsEnabled' | 'downloadAsCbz' | 'downloadsPath' | 'electronPath' | 'excludeCompleted' | 'excludeEntryWithUnreadChapters' | 'excludeNotStarted' | 'excludeUnreadChapters' | 'extensionRepos' | 'globalUpdateInterval' | 'gqlDebugLogsEnabled' | 'initialOpenInBrowserEnabled' | 'ip' | 'localSourcePath' | 'maxSourcesInParallel' | 'port' | 'socksProxyEnabled' | 'socksProxyHost' | 'socksProxyPort' | 'systemTrayEnabled' | 'updateMangas' | 'webUIChannel' | 'webUIFlavor' | 'webUIInterface' | 'webUIUpdateCheckInterval' | PartialSettingsTypeKeySpecifier)[]; export type PartialSettingsTypeFieldPolicy = { autoDownloadAheadLimit?: FieldPolicy | FieldReadFunction, autoDownloadNewChapters?: FieldPolicy | FieldReadFunction, @@ -492,6 +494,7 @@ export type PartialSettingsTypeFieldPolicy = { excludeEntryWithUnreadChapters?: FieldPolicy | FieldReadFunction, excludeNotStarted?: FieldPolicy | FieldReadFunction, excludeUnreadChapters?: FieldPolicy | FieldReadFunction, + extensionRepos?: FieldPolicy | FieldReadFunction, globalUpdateInterval?: FieldPolicy | FieldReadFunction, gqlDebugLogsEnabled?: FieldPolicy | FieldReadFunction, initialOpenInBrowserEnabled?: FieldPolicy | FieldReadFunction, @@ -585,7 +588,7 @@ export type SetSettingsPayloadFieldPolicy = { clientMutationId?: FieldPolicy | FieldReadFunction, settings?: FieldPolicy | FieldReadFunction }; -export type SettingsKeySpecifier = ('autoDownloadAheadLimit' | 'autoDownloadNewChapters' | 'backupInterval' | 'backupPath' | 'backupTTL' | 'backupTime' | 'basicAuthEnabled' | 'basicAuthPassword' | 'basicAuthUsername' | 'debugLogsEnabled' | 'downloadAsCbz' | 'downloadsPath' | 'electronPath' | 'excludeCompleted' | 'excludeEntryWithUnreadChapters' | 'excludeNotStarted' | 'excludeUnreadChapters' | 'globalUpdateInterval' | 'gqlDebugLogsEnabled' | 'initialOpenInBrowserEnabled' | 'ip' | 'localSourcePath' | 'maxSourcesInParallel' | 'port' | 'socksProxyEnabled' | 'socksProxyHost' | 'socksProxyPort' | 'systemTrayEnabled' | 'updateMangas' | 'webUIChannel' | 'webUIFlavor' | 'webUIInterface' | 'webUIUpdateCheckInterval' | SettingsKeySpecifier)[]; +export type SettingsKeySpecifier = ('autoDownloadAheadLimit' | 'autoDownloadNewChapters' | 'backupInterval' | 'backupPath' | 'backupTTL' | 'backupTime' | 'basicAuthEnabled' | 'basicAuthPassword' | 'basicAuthUsername' | 'debugLogsEnabled' | 'downloadAsCbz' | 'downloadsPath' | 'electronPath' | 'excludeCompleted' | 'excludeEntryWithUnreadChapters' | 'excludeNotStarted' | 'excludeUnreadChapters' | 'extensionRepos' | 'globalUpdateInterval' | 'gqlDebugLogsEnabled' | 'initialOpenInBrowserEnabled' | 'ip' | 'localSourcePath' | 'maxSourcesInParallel' | 'port' | 'socksProxyEnabled' | 'socksProxyHost' | 'socksProxyPort' | 'systemTrayEnabled' | 'updateMangas' | 'webUIChannel' | 'webUIFlavor' | 'webUIInterface' | 'webUIUpdateCheckInterval' | SettingsKeySpecifier)[]; export type SettingsFieldPolicy = { autoDownloadAheadLimit?: FieldPolicy | FieldReadFunction, autoDownloadNewChapters?: FieldPolicy | FieldReadFunction, @@ -604,6 +607,7 @@ export type SettingsFieldPolicy = { excludeEntryWithUnreadChapters?: FieldPolicy | FieldReadFunction, excludeNotStarted?: FieldPolicy | FieldReadFunction, excludeUnreadChapters?: FieldPolicy | FieldReadFunction, + extensionRepos?: FieldPolicy | FieldReadFunction, globalUpdateInterval?: FieldPolicy | FieldReadFunction, gqlDebugLogsEnabled?: FieldPolicy | FieldReadFunction, initialOpenInBrowserEnabled?: FieldPolicy | FieldReadFunction, @@ -621,7 +625,7 @@ export type SettingsFieldPolicy = { webUIInterface?: FieldPolicy | FieldReadFunction, webUIUpdateCheckInterval?: FieldPolicy | FieldReadFunction }; -export type SettingsTypeKeySpecifier = ('autoDownloadAheadLimit' | 'autoDownloadNewChapters' | 'backupInterval' | 'backupPath' | 'backupTTL' | 'backupTime' | 'basicAuthEnabled' | 'basicAuthPassword' | 'basicAuthUsername' | 'debugLogsEnabled' | 'downloadAsCbz' | 'downloadsPath' | 'electronPath' | 'excludeCompleted' | 'excludeEntryWithUnreadChapters' | 'excludeNotStarted' | 'excludeUnreadChapters' | 'globalUpdateInterval' | 'gqlDebugLogsEnabled' | 'initialOpenInBrowserEnabled' | 'ip' | 'localSourcePath' | 'maxSourcesInParallel' | 'port' | 'socksProxyEnabled' | 'socksProxyHost' | 'socksProxyPort' | 'systemTrayEnabled' | 'updateMangas' | 'webUIChannel' | 'webUIFlavor' | 'webUIInterface' | 'webUIUpdateCheckInterval' | SettingsTypeKeySpecifier)[]; +export type SettingsTypeKeySpecifier = ('autoDownloadAheadLimit' | 'autoDownloadNewChapters' | 'backupInterval' | 'backupPath' | 'backupTTL' | 'backupTime' | 'basicAuthEnabled' | 'basicAuthPassword' | 'basicAuthUsername' | 'debugLogsEnabled' | 'downloadAsCbz' | 'downloadsPath' | 'electronPath' | 'excludeCompleted' | 'excludeEntryWithUnreadChapters' | 'excludeNotStarted' | 'excludeUnreadChapters' | 'extensionRepos' | 'globalUpdateInterval' | 'gqlDebugLogsEnabled' | 'initialOpenInBrowserEnabled' | 'ip' | 'localSourcePath' | 'maxSourcesInParallel' | 'port' | 'socksProxyEnabled' | 'socksProxyHost' | 'socksProxyPort' | 'systemTrayEnabled' | 'updateMangas' | 'webUIChannel' | 'webUIFlavor' | 'webUIInterface' | 'webUIUpdateCheckInterval' | SettingsTypeKeySpecifier)[]; export type SettingsTypeFieldPolicy = { autoDownloadAheadLimit?: FieldPolicy | FieldReadFunction, autoDownloadNewChapters?: FieldPolicy | FieldReadFunction, @@ -640,6 +644,7 @@ export type SettingsTypeFieldPolicy = { excludeEntryWithUnreadChapters?: FieldPolicy | FieldReadFunction, excludeNotStarted?: FieldPolicy | FieldReadFunction, excludeUnreadChapters?: FieldPolicy | FieldReadFunction, + extensionRepos?: FieldPolicy | FieldReadFunction, globalUpdateInterval?: FieldPolicy | FieldReadFunction, gqlDebugLogsEnabled?: FieldPolicy | FieldReadFunction, initialOpenInBrowserEnabled?: FieldPolicy | FieldReadFunction, diff --git a/src/lib/graphql/generated/graphql.ts b/src/lib/graphql/generated/graphql.ts index aacede82..647020ea 100644 --- a/src/lib/graphql/generated/graphql.ts +++ b/src/lib/graphql/generated/graphql.ts @@ -511,6 +511,7 @@ export type ExtensionConditionInput = { lang?: InputMaybe; name?: InputMaybe; pkgName?: InputMaybe; + repo?: InputMaybe; versionCode?: InputMaybe; versionName?: InputMaybe; }; @@ -534,6 +535,7 @@ export type ExtensionFilterInput = { not?: InputMaybe; or?: InputMaybe>; pkgName?: InputMaybe; + repo?: InputMaybe; versionCode?: InputMaybe; versionName?: InputMaybe; }; @@ -563,6 +565,7 @@ export type ExtensionType = { lang: Scalars['String']['output']; name: Scalars['String']['output']; pkgName: Scalars['String']['output']; + repo?: Maybe; source: SourceNodeList; versionCode: Scalars['Int']['output']; versionName: Scalars['String']['output']; @@ -880,6 +883,7 @@ export type MangaType = { thumbnailUrl?: Maybe; title: Scalars['String']['output']; unreadCount: Scalars['Int']['output']; + updateStrategy: UpdateStrategy; url: Scalars['String']['output']; }; @@ -1263,6 +1267,7 @@ export type PartialSettingsType = Settings & { excludeEntryWithUnreadChapters?: Maybe; excludeNotStarted?: Maybe; excludeUnreadChapters?: Maybe; + extensionRepos?: Maybe>; globalUpdateInterval?: Maybe; gqlDebugLogsEnabled?: Maybe; initialOpenInBrowserEnabled?: Maybe; @@ -1299,6 +1304,7 @@ export type PartialSettingsTypeInput = { excludeEntryWithUnreadChapters?: InputMaybe; excludeNotStarted?: InputMaybe; excludeUnreadChapters?: InputMaybe; + extensionRepos?: InputMaybe>; globalUpdateInterval?: InputMaybe; gqlDebugLogsEnabled?: InputMaybe; initialOpenInBrowserEnabled?: InputMaybe; @@ -1583,6 +1589,7 @@ export type Settings = { excludeEntryWithUnreadChapters?: Maybe; excludeNotStarted?: Maybe; excludeUnreadChapters?: Maybe; + extensionRepos?: Maybe>; globalUpdateInterval?: Maybe; gqlDebugLogsEnabled?: Maybe; initialOpenInBrowserEnabled?: Maybe; @@ -1620,6 +1627,7 @@ export type SettingsType = Settings & { excludeEntryWithUnreadChapters: Scalars['Boolean']['output']; excludeNotStarted: Scalars['Boolean']['output']; excludeUnreadChapters: Scalars['Boolean']['output']; + extensionRepos: Array; globalUpdateInterval: Scalars['Float']['output']; gqlDebugLogsEnabled: Scalars['Boolean']['output']; initialOpenInBrowserEnabled: Scalars['Boolean']['output']; @@ -1918,7 +1926,7 @@ export type UpdateExtensionPatchInput = { export type UpdateExtensionPayload = { __typename?: 'UpdateExtensionPayload'; clientMutationId?: Maybe; - extension: ExtensionType; + extension?: Maybe; }; export type UpdateExtensionsInput = { @@ -2052,6 +2060,11 @@ export type UpdateStopPayload = { clientMutationId?: Maybe; }; +export enum UpdateStrategy { + AlwaysUpdate = 'ALWAYS_UPDATE', + OnlyFetchOnce = 'ONLY_FETCH_ONCE' +} + export type ValidateBackupInput = { backup: Scalars['Upload']['input']; }; @@ -2135,7 +2148,7 @@ export type FullMangaFieldsFragment = { __typename?: 'MangaType', unreadCount: n export type UpdaterMangaFieldsFragment = { __typename?: 'MangaType', id: number, title: string, thumbnailUrl?: string | null }; -export type FullExtensionFieldsFragment = { __typename?: 'ExtensionType', apkName: string, hasUpdate: boolean, iconUrl: string, isInstalled: boolean, isNsfw: boolean, isObsolete: boolean, lang: string, name: string, pkgName: string, versionCode: number, versionName: string }; +export type FullExtensionFieldsFragment = { __typename?: 'ExtensionType', apkName: string, repo?: string | null, hasUpdate: boolean, iconUrl: string, isInstalled: boolean, isNsfw: boolean, isObsolete: boolean, lang: string, name: string, pkgName: string, versionCode: number, versionName: string }; export type FullDownloadStatusFragment = { __typename?: 'DownloadStatus', state: DownloaderState, queue: Array<{ __typename?: 'DownloadType', progress: number, state: DownloadState, tries: number, chapter: { __typename?: 'ChapterType', id: number, name: string, sourceOrder: number, isDownloaded: boolean, manga: { __typename?: 'MangaType', id: number, title: string, downloadCount: number } } }> }; @@ -2151,7 +2164,7 @@ export type WebuiUpdateInfoFragment = { __typename?: 'WebUIUpdateInfo', channel: export type WebuiUpdateStatusFragment = { __typename?: 'WebUIUpdateStatus', progress: number, state: UpdateState, info: { __typename?: 'WebUIUpdateInfo', channel: string, tag: string } }; -export type ServerSettingsFragment = { __typename?: 'SettingsType', ip: string, port: number, socksProxyEnabled: boolean, socksProxyHost: string, socksProxyPort: string, webUIFlavor: WebUiFlavor, initialOpenInBrowserEnabled: boolean, webUIInterface: WebUiInterface, electronPath: string, webUIChannel: WebUiChannel, webUIUpdateCheckInterval: number, downloadAsCbz: boolean, downloadsPath: string, autoDownloadNewChapters: boolean, excludeEntryWithUnreadChapters: boolean, autoDownloadAheadLimit: number, maxSourcesInParallel: number, excludeUnreadChapters: boolean, excludeNotStarted: boolean, excludeCompleted: boolean, globalUpdateInterval: number, updateMangas: boolean, basicAuthEnabled: boolean, basicAuthUsername: string, basicAuthPassword: string, debugLogsEnabled: boolean, gqlDebugLogsEnabled: boolean, systemTrayEnabled: boolean, backupPath: string, backupTime: string, backupInterval: number, backupTTL: number, localSourcePath: string }; +export type ServerSettingsFragment = { __typename?: 'SettingsType', ip: string, port: number, socksProxyEnabled: boolean, socksProxyHost: string, socksProxyPort: string, webUIFlavor: WebUiFlavor, initialOpenInBrowserEnabled: boolean, webUIInterface: WebUiInterface, electronPath: string, webUIChannel: WebUiChannel, webUIUpdateCheckInterval: number, downloadAsCbz: boolean, downloadsPath: string, autoDownloadNewChapters: boolean, excludeEntryWithUnreadChapters: boolean, autoDownloadAheadLimit: number, extensionRepos: Array, maxSourcesInParallel: number, excludeUnreadChapters: boolean, excludeNotStarted: boolean, excludeCompleted: boolean, globalUpdateInterval: number, updateMangas: boolean, basicAuthEnabled: boolean, basicAuthUsername: string, basicAuthPassword: string, debugLogsEnabled: boolean, gqlDebugLogsEnabled: boolean, systemTrayEnabled: boolean, backupPath: string, backupTime: string, backupInterval: number, backupTTL: number, localSourcePath: string }; export type CreateBackupMutationVariables = Exact<{ input: CreateBackupInput; @@ -2362,28 +2375,28 @@ export type GetExtensionsFetchMutationVariables = Exact<{ }>; -export type GetExtensionsFetchMutation = { __typename?: 'Mutation', fetchExtensions: { __typename?: 'FetchExtensionsPayload', clientMutationId?: string | null, extensions: Array<{ __typename?: 'ExtensionType', apkName: string, hasUpdate: boolean, iconUrl: string, isInstalled: boolean, isNsfw: boolean, isObsolete: boolean, lang: string, name: string, pkgName: string, versionCode: number, versionName: string }> } }; +export type GetExtensionsFetchMutation = { __typename?: 'Mutation', fetchExtensions: { __typename?: 'FetchExtensionsPayload', clientMutationId?: string | null, extensions: Array<{ __typename?: 'ExtensionType', apkName: string, repo?: string | null, hasUpdate: boolean, iconUrl: string, isInstalled: boolean, isNsfw: boolean, isObsolete: boolean, lang: string, name: string, pkgName: string, versionCode: number, versionName: string }> } }; export type UpdateExtensionMutationVariables = Exact<{ input: UpdateExtensionInput; }>; -export type UpdateExtensionMutation = { __typename?: 'Mutation', updateExtension: { __typename?: 'UpdateExtensionPayload', clientMutationId?: string | null, extension: { __typename?: 'ExtensionType', pkgName: string, apkName: string, versionName: string, versionCode: number, isInstalled: boolean, isObsolete: boolean, hasUpdate: boolean } } }; +export type UpdateExtensionMutation = { __typename?: 'Mutation', updateExtension: { __typename?: 'UpdateExtensionPayload', clientMutationId?: string | null, extension?: { __typename?: 'ExtensionType', pkgName: string, apkName: string, repo?: string | null, versionName: string, versionCode: number, isInstalled: boolean, isObsolete: boolean, hasUpdate: boolean } | null } }; export type UpdateExtensionsMutationVariables = Exact<{ input: UpdateExtensionsInput; }>; -export type UpdateExtensionsMutation = { __typename?: 'Mutation', updateExtensions: { __typename?: 'UpdateExtensionsPayload', clientMutationId?: string | null, extensions: Array<{ __typename?: 'ExtensionType', pkgName: string, apkName: string, versionName: string, versionCode: number, isInstalled: boolean, isObsolete: boolean, hasUpdate: boolean }> } }; +export type UpdateExtensionsMutation = { __typename?: 'Mutation', updateExtensions: { __typename?: 'UpdateExtensionsPayload', clientMutationId?: string | null, extensions: Array<{ __typename?: 'ExtensionType', pkgName: string, apkName: string, repo?: string | null, versionName: string, versionCode: number, isInstalled: boolean, isObsolete: boolean, hasUpdate: boolean }> } }; export type InstallExternalExtensionMutationVariables = Exact<{ file: Scalars['Upload']['input']; }>; -export type InstallExternalExtensionMutation = { __typename?: 'Mutation', installExternalExtension: { __typename?: 'InstallExternalExtensionPayload', clientMutationId?: string | null, extension: { __typename?: 'ExtensionType', apkName: string, hasUpdate: boolean, iconUrl: string, isInstalled: boolean, isNsfw: boolean, isObsolete: boolean, lang: string, name: string, pkgName: string, versionCode: number, versionName: string } } }; +export type InstallExternalExtensionMutation = { __typename?: 'Mutation', installExternalExtension: { __typename?: 'InstallExternalExtensionPayload', clientMutationId?: string | null, extension: { __typename?: 'ExtensionType', apkName: string, repo?: string | null, hasUpdate: boolean, iconUrl: string, isInstalled: boolean, isNsfw: boolean, isObsolete: boolean, lang: string, name: string, pkgName: string, versionCode: number, versionName: string } } }; export type DeleteGlobalMetadataMutationVariables = Exact<{ input: DeleteGlobalMetaInput; @@ -2472,14 +2485,14 @@ export type ResetServerSettingsMutationVariables = Exact<{ }>; -export type ResetServerSettingsMutation = { __typename?: 'Mutation', resetSettings: { __typename?: 'ResetSettingsPayload', clientMutationId?: string | null, settings: { __typename?: 'SettingsType', ip: string, port: number, socksProxyEnabled: boolean, socksProxyHost: string, socksProxyPort: string, webUIFlavor: WebUiFlavor, initialOpenInBrowserEnabled: boolean, webUIInterface: WebUiInterface, electronPath: string, webUIChannel: WebUiChannel, webUIUpdateCheckInterval: number, downloadAsCbz: boolean, downloadsPath: string, autoDownloadNewChapters: boolean, excludeEntryWithUnreadChapters: boolean, autoDownloadAheadLimit: number, maxSourcesInParallel: number, excludeUnreadChapters: boolean, excludeNotStarted: boolean, excludeCompleted: boolean, globalUpdateInterval: number, updateMangas: boolean, basicAuthEnabled: boolean, basicAuthUsername: string, basicAuthPassword: string, debugLogsEnabled: boolean, gqlDebugLogsEnabled: boolean, systemTrayEnabled: boolean, backupPath: string, backupTime: string, backupInterval: number, backupTTL: number, localSourcePath: string } } }; +export type ResetServerSettingsMutation = { __typename?: 'Mutation', resetSettings: { __typename?: 'ResetSettingsPayload', clientMutationId?: string | null, settings: { __typename?: 'SettingsType', ip: string, port: number, socksProxyEnabled: boolean, socksProxyHost: string, socksProxyPort: string, webUIFlavor: WebUiFlavor, initialOpenInBrowserEnabled: boolean, webUIInterface: WebUiInterface, electronPath: string, webUIChannel: WebUiChannel, webUIUpdateCheckInterval: number, downloadAsCbz: boolean, downloadsPath: string, autoDownloadNewChapters: boolean, excludeEntryWithUnreadChapters: boolean, autoDownloadAheadLimit: number, extensionRepos: Array, maxSourcesInParallel: number, excludeUnreadChapters: boolean, excludeNotStarted: boolean, excludeCompleted: boolean, globalUpdateInterval: number, updateMangas: boolean, basicAuthEnabled: boolean, basicAuthUsername: string, basicAuthPassword: string, debugLogsEnabled: boolean, gqlDebugLogsEnabled: boolean, systemTrayEnabled: boolean, backupPath: string, backupTime: string, backupInterval: number, backupTTL: number, localSourcePath: string } } }; export type UpdateServerSettingsMutationVariables = Exact<{ input: SetSettingsInput; }>; -export type UpdateServerSettingsMutation = { __typename?: 'Mutation', setSettings: { __typename?: 'SetSettingsPayload', clientMutationId?: string | null, settings: { __typename?: 'SettingsType', ip: string, port: number, socksProxyEnabled: boolean, socksProxyHost: string, socksProxyPort: string, webUIFlavor: WebUiFlavor, initialOpenInBrowserEnabled: boolean, webUIInterface: WebUiInterface, electronPath: string, webUIChannel: WebUiChannel, webUIUpdateCheckInterval: number, downloadAsCbz: boolean, downloadsPath: string, autoDownloadNewChapters: boolean, excludeEntryWithUnreadChapters: boolean, autoDownloadAheadLimit: number, maxSourcesInParallel: number, excludeUnreadChapters: boolean, excludeNotStarted: boolean, excludeCompleted: boolean, globalUpdateInterval: number, updateMangas: boolean, basicAuthEnabled: boolean, basicAuthUsername: string, basicAuthPassword: string, debugLogsEnabled: boolean, gqlDebugLogsEnabled: boolean, systemTrayEnabled: boolean, backupPath: string, backupTime: string, backupInterval: number, backupTTL: number, localSourcePath: string } } }; +export type UpdateServerSettingsMutation = { __typename?: 'Mutation', setSettings: { __typename?: 'SetSettingsPayload', clientMutationId?: string | null, settings: { __typename?: 'SettingsType', ip: string, port: number, socksProxyEnabled: boolean, socksProxyHost: string, socksProxyPort: string, webUIFlavor: WebUiFlavor, initialOpenInBrowserEnabled: boolean, webUIInterface: WebUiInterface, electronPath: string, webUIChannel: WebUiChannel, webUIUpdateCheckInterval: number, downloadAsCbz: boolean, downloadsPath: string, autoDownloadNewChapters: boolean, excludeEntryWithUnreadChapters: boolean, autoDownloadAheadLimit: number, extensionRepos: Array, maxSourcesInParallel: number, excludeUnreadChapters: boolean, excludeNotStarted: boolean, excludeCompleted: boolean, globalUpdateInterval: number, updateMangas: boolean, basicAuthEnabled: boolean, basicAuthUsername: string, basicAuthPassword: string, debugLogsEnabled: boolean, gqlDebugLogsEnabled: boolean, systemTrayEnabled: boolean, backupPath: string, backupTime: string, backupInterval: number, backupTTL: number, localSourcePath: string } } }; export type GetSourceMangasFetchMutationVariables = Exact<{ input: FetchSourceMangaInput; @@ -2601,7 +2614,7 @@ export type GetExtensionQueryVariables = Exact<{ }>; -export type GetExtensionQuery = { __typename?: 'Query', extension: { __typename?: 'ExtensionType', apkName: string, hasUpdate: boolean, iconUrl: string, isInstalled: boolean, isNsfw: boolean, isObsolete: boolean, lang: string, name: string, pkgName: string, versionCode: number, versionName: string } }; +export type GetExtensionQuery = { __typename?: 'Query', extension: { __typename?: 'ExtensionType', apkName: string, repo?: string | null, hasUpdate: boolean, iconUrl: string, isInstalled: boolean, isNsfw: boolean, isObsolete: boolean, lang: string, name: string, pkgName: string, versionCode: number, versionName: string } }; export type GetExtensionsQueryVariables = Exact<{ after?: InputMaybe; @@ -2616,7 +2629,7 @@ export type GetExtensionsQueryVariables = Exact<{ }>; -export type GetExtensionsQuery = { __typename?: 'Query', extensions: { __typename?: 'ExtensionNodeList', totalCount: number, nodes: Array<{ __typename?: 'ExtensionType', apkName: string, hasUpdate: boolean, iconUrl: string, isInstalled: boolean, isNsfw: boolean, isObsolete: boolean, lang: string, name: string, pkgName: string, versionCode: number, versionName: string }>, pageInfo: { __typename?: 'PageInfo', endCursor?: any | null, hasNextPage: boolean, hasPreviousPage: boolean, startCursor?: any | null } } }; +export type GetExtensionsQuery = { __typename?: 'Query', extensions: { __typename?: 'ExtensionNodeList', totalCount: number, nodes: Array<{ __typename?: 'ExtensionType', apkName: string, repo?: string | null, hasUpdate: boolean, iconUrl: string, isInstalled: boolean, isNsfw: boolean, isObsolete: boolean, lang: string, name: string, pkgName: string, versionCode: number, versionName: string }>, pageInfo: { __typename?: 'PageInfo', endCursor?: any | null, hasNextPage: boolean, hasPreviousPage: boolean, startCursor?: any | null } } }; export type GetGlobalMetadataQueryVariables = Exact<{ key: Scalars['String']['input']; @@ -2685,7 +2698,7 @@ export type GetWebuiUpdateStatusQuery = { __typename?: 'Query', getWebUIUpdateSt export type GetServerSettingsQueryVariables = Exact<{ [key: string]: never; }>; -export type GetServerSettingsQuery = { __typename?: 'Query', settings: { __typename?: 'SettingsType', ip: string, port: number, socksProxyEnabled: boolean, socksProxyHost: string, socksProxyPort: string, webUIFlavor: WebUiFlavor, initialOpenInBrowserEnabled: boolean, webUIInterface: WebUiInterface, electronPath: string, webUIChannel: WebUiChannel, webUIUpdateCheckInterval: number, downloadAsCbz: boolean, downloadsPath: string, autoDownloadNewChapters: boolean, excludeEntryWithUnreadChapters: boolean, autoDownloadAheadLimit: number, maxSourcesInParallel: number, excludeUnreadChapters: boolean, excludeNotStarted: boolean, excludeCompleted: boolean, globalUpdateInterval: number, updateMangas: boolean, basicAuthEnabled: boolean, basicAuthUsername: string, basicAuthPassword: string, debugLogsEnabled: boolean, gqlDebugLogsEnabled: boolean, systemTrayEnabled: boolean, backupPath: string, backupTime: string, backupInterval: number, backupTTL: number, localSourcePath: string } }; +export type GetServerSettingsQuery = { __typename?: 'Query', settings: { __typename?: 'SettingsType', ip: string, port: number, socksProxyEnabled: boolean, socksProxyHost: string, socksProxyPort: string, webUIFlavor: WebUiFlavor, initialOpenInBrowserEnabled: boolean, webUIInterface: WebUiInterface, electronPath: string, webUIChannel: WebUiChannel, webUIUpdateCheckInterval: number, downloadAsCbz: boolean, downloadsPath: string, autoDownloadNewChapters: boolean, excludeEntryWithUnreadChapters: boolean, autoDownloadAheadLimit: number, extensionRepos: Array, maxSourcesInParallel: number, excludeUnreadChapters: boolean, excludeNotStarted: boolean, excludeCompleted: boolean, globalUpdateInterval: number, updateMangas: boolean, basicAuthEnabled: boolean, basicAuthUsername: string, basicAuthPassword: string, debugLogsEnabled: boolean, gqlDebugLogsEnabled: boolean, systemTrayEnabled: boolean, backupPath: string, backupTime: string, backupInterval: number, backupTTL: number, localSourcePath: string } }; export type GetSourceQueryVariables = Exact<{ id: Scalars['LongString']['input']; diff --git a/src/lib/graphql/mutations/ExtensionMutation.ts b/src/lib/graphql/mutations/ExtensionMutation.ts index d0f0d805..231b6180 100644 --- a/src/lib/graphql/mutations/ExtensionMutation.ts +++ b/src/lib/graphql/mutations/ExtensionMutation.ts @@ -29,6 +29,7 @@ export const UPDATE_EXTENSION = gql` extension { pkgName apkName + repo versionName versionCode isInstalled @@ -46,6 +47,7 @@ export const UPDATE_EXTENSIONS = gql` extensions { pkgName apkName + repo versionName versionCode isInstalled diff --git a/src/lib/requests/RequestManager.ts b/src/lib/requests/RequestManager.ts index 2a59c97d..fc719491 100644 --- a/src/lib/requests/RequestManager.ts +++ b/src/lib/requests/RequestManager.ts @@ -1026,7 +1026,7 @@ export class RequestManager { ...cachedExtensions.data.fetchExtensions, extensions: cachedExtensions.data.fetchExtensions.extensions.map((extension) => { const isUpdatedExtension = - extension.apkName === response.data?.updateExtension.extension.apkName; + extension.apkName === response.data?.updateExtension.extension?.apkName; if (!isUpdatedExtension) { return extension; } diff --git a/src/screens/Extensions.tsx b/src/screens/Extensions.tsx index 9ebab782..e1b426ab 100644 --- a/src/screens/Extensions.tsx +++ b/src/screens/Extensions.tsx @@ -98,6 +98,9 @@ export function Extensions() { const theme = useTheme(); const isMobileWidth = useMediaQuery(theme.breakpoints.down('sm')); + const { data: serverSettingsData } = requestManager.useGetServerSettings(); + const usesCustomRepos = !!serverSettingsData?.settings.extensionRepos.length; + const inputRef = useRef(null); const { setTitle, setAction } = useContext(NavBarContext); const [shownLangs, setShownLangs] = useLocalStorage('shownExtensionLangs', extensionDefaultLangs()); @@ -255,7 +258,11 @@ export function Extensions() { return ( - + ); }} diff --git a/src/screens/settings/ServerSettings.tsx b/src/screens/settings/ServerSettings.tsx index 10ebed4d..ff8e4aa0 100644 --- a/src/screens/settings/ServerSettings.tsx +++ b/src/screens/settings/ServerSettings.tsx @@ -16,6 +16,7 @@ import { useLocalStorage } from '@/util/useLocalStorage.tsx'; import { TextSetting } from '@/components/settings/TextSetting.tsx'; import { ServerSettings as GqlServerSettings } from '@/typings.ts'; import { NumberSetting } from '@/components/settings/NumberSetting.tsx'; +import { MutableListSetting } from '@/components/settings/MutableListSetting.tsx'; type ServerSettingsType = Pick< GqlServerSettings, @@ -32,6 +33,7 @@ type ServerSettingsType = Pick< | 'basicAuthPassword' | 'maxSourcesInParallel' | 'localSourcePath' + | 'extensionRepos' >; const extractDownloadSettings = (settings: GqlServerSettings): ServerSettingsType => ({ @@ -48,6 +50,7 @@ const extractDownloadSettings = (settings: GqlServerSettings): ServerSettingsTyp basicAuthPassword: settings.basicAuthPassword, maxSourcesInParallel: settings.maxSourcesInParallel, localSourcePath: settings.localSourcePath, + extensionRepos: settings.extensionRepos, }); export const ServerSettings = () => { @@ -120,6 +123,21 @@ export const ServerSettings = () => { handleUpdate={(parallelSources) => updateSetting('maxSourcesInParallel', parallelSources)} /> + + {t('extension.title')} + + } + > + updateSetting('extensionRepos', repos)} + values={serverSettings?.extensionRepos} + addItemButtonTitle={t('extension.settings.repositories.custom.dialog.action.button.add')} + /> + diff --git a/versionToServerVersionMapping.json b/versionToServerVersionMapping.json index f7e43163..91fbdf0d 100644 --- a/versionToServerVersionMapping.json +++ b/versionToServerVersionMapping.json @@ -1,6 +1,6 @@ [ { "uiVersion": "PREVIEW", - "serverVersion": "r1438" + "serverVersion": "r1444" } ]