Feature/settings support custom extension repos (#539)

* [Codegen] Support custom extension repos

* [VersionMapping] Require server version "r1444" for preview
This commit is contained in:
schroda
2024-01-06 18:38:16 +01:00
committed by GitHub
parent b42c3103f1
commit 39b79c6ebe
12 changed files with 245 additions and 33 deletions

View File

@@ -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<InstalledStates>(
getInstalledState(isInstalled, isObsolete, hasUpdate),
@@ -184,6 +186,11 @@ export function ExtensionCard(props: IProps) {
</Typography>
)}
</Typography>
{usesCustomRepos && (
<Typography variant="caption" display="block">
{repo}
</Typography>
)}
</Box>
</Box>

View File

@@ -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<TextSettingProps, 'isPassword' | 'disabled'> & { handleDelete: () => void }) => {
const { t } = useTranslation();
return (
<Stack direction="row">
<TextSetting {...textSettingProps} dialogTitle="" />
<Tooltip title={t('chapter.action.download.delete.label.action')}>
<IconButton size="large" onClick={handleDelete}>
<DeleteIcon />
</IconButton>
</Tooltip>
</Stack>
);
};
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 (
<>
<ListItemButton onClick={() => setIsDialogOpen(true)}>
<ListItemText
primary={settingName}
secondary={values?.length ? values?.join(', ') : description}
secondaryTypographyProps={{ style: { display: 'flex', flexDirection: 'column' } }}
/>
</ListItemButton>
<Dialog open={isDialogOpen} onClose={() => closeDialog()} fullWidth>
<DialogTitle>{settingName}</DialogTitle>
{!!description && (
<DialogContent>
<DialogContentText sx={{ paddingBottom: '10px' }}>{description}</DialogContentText>
</DialogContent>
)}
<DialogContent dividers sx={{ maxHeight: '300px' }}>
<List>
{dialogValues.map((dialogValue, index) => (
<MutableListItem
settingName={dialogValue === '' ? t('global.label.placeholder') : ''}
placeholder="https://github.com/MY_ACCOUNT/MY_REPO/tree/repo"
handleChange={(newValue: string) => updateSetting(index, newValue)}
handleDelete={() => updateSetting(index, undefined)}
value={dialogValue}
/>
))}
</List>
</DialogContent>
<DialogActions>
<Stack sx={{ width: '100%' }} direction="row" justifyContent="space-between">
<Button onClick={() => updateSetting(dialogValues.length, '')}>
{addItemButtonTitle ?? t('global.button.add')}
</Button>
<Stack direction="row">
<Button onClick={() => closeDialog()}>{t('global.button.cancel')}</Button>
<Button onClick={() => saveChanges()}>{t('global.button.ok')}</Button>
</Stack>
</Stack>
</DialogActions>
</Dialog>
</>
);
};

View File

@@ -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 = ({
<ListItemText
primary={settingName}
secondary={isPassword ? value?.replace(/./g, '*') : value ?? t('global.label.loading')}
secondaryTypographyProps={{ style: { display: 'flex', flexDirection: 'column' } }}
secondaryTypographyProps={{
sx: { display: 'flex', flexDirection: 'column', wordWrap: 'break-word' },
}}
/>
</ListItemButton>
<Dialog open={isDialogOpen} onClose={() => closeDialog()} fullWidth>
<DialogContent>
<DialogTitle sx={{ paddingLeft: 0 }}>{settingName}</DialogTitle>
<DialogTitle sx={{ paddingLeft: 0 }}>{dialogTitle}</DialogTitle>
{!!dialogDescription && (
<DialogContentText sx={{ paddingBottom: '10px' }}>{dialogDescription}</DialogContentText>
)}

View File

@@ -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"

View File

@@ -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

View File

@@ -254,7 +254,7 @@ export type ExtensionNodeListFieldPolicy = {
pageInfo?: FieldPolicy<any> | FieldReadFunction<any>,
totalCount?: FieldPolicy<any> | FieldReadFunction<any>
};
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<any> | FieldReadFunction<any>,
hasUpdate?: FieldPolicy<any> | FieldReadFunction<any>,
@@ -265,6 +265,7 @@ export type ExtensionTypeFieldPolicy = {
lang?: FieldPolicy<any> | FieldReadFunction<any>,
name?: FieldPolicy<any> | FieldReadFunction<any>,
pkgName?: FieldPolicy<any> | FieldReadFunction<any>,
repo?: FieldPolicy<any> | FieldReadFunction<any>,
source?: FieldPolicy<any> | FieldReadFunction<any>,
versionCode?: FieldPolicy<any> | FieldReadFunction<any>,
versionName?: FieldPolicy<any> | FieldReadFunction<any>
@@ -356,7 +357,7 @@ export type MangaNodeListFieldPolicy = {
pageInfo?: FieldPolicy<any> | FieldReadFunction<any>,
totalCount?: FieldPolicy<any> | FieldReadFunction<any>
};
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<any> | FieldReadFunction<any>,
artist?: FieldPolicy<any> | FieldReadFunction<any>,
@@ -382,6 +383,7 @@ export type MangaTypeFieldPolicy = {
thumbnailUrl?: FieldPolicy<any> | FieldReadFunction<any>,
title?: FieldPolicy<any> | FieldReadFunction<any>,
unreadCount?: FieldPolicy<any> | FieldReadFunction<any>,
updateStrategy?: FieldPolicy<any> | FieldReadFunction<any>,
url?: FieldPolicy<any> | FieldReadFunction<any>
};
export type MetaEdgeKeySpecifier = ('cursor' | 'node' | MetaEdgeKeySpecifier)[];
@@ -473,7 +475,7 @@ export type PageInfoFieldPolicy = {
hasPreviousPage?: FieldPolicy<any> | FieldReadFunction<any>,
startCursor?: FieldPolicy<any> | FieldReadFunction<any>
};
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<any> | FieldReadFunction<any>,
autoDownloadNewChapters?: FieldPolicy<any> | FieldReadFunction<any>,
@@ -492,6 +494,7 @@ export type PartialSettingsTypeFieldPolicy = {
excludeEntryWithUnreadChapters?: FieldPolicy<any> | FieldReadFunction<any>,
excludeNotStarted?: FieldPolicy<any> | FieldReadFunction<any>,
excludeUnreadChapters?: FieldPolicy<any> | FieldReadFunction<any>,
extensionRepos?: FieldPolicy<any> | FieldReadFunction<any>,
globalUpdateInterval?: FieldPolicy<any> | FieldReadFunction<any>,
gqlDebugLogsEnabled?: FieldPolicy<any> | FieldReadFunction<any>,
initialOpenInBrowserEnabled?: FieldPolicy<any> | FieldReadFunction<any>,
@@ -585,7 +588,7 @@ export type SetSettingsPayloadFieldPolicy = {
clientMutationId?: FieldPolicy<any> | FieldReadFunction<any>,
settings?: FieldPolicy<any> | FieldReadFunction<any>
};
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<any> | FieldReadFunction<any>,
autoDownloadNewChapters?: FieldPolicy<any> | FieldReadFunction<any>,
@@ -604,6 +607,7 @@ export type SettingsFieldPolicy = {
excludeEntryWithUnreadChapters?: FieldPolicy<any> | FieldReadFunction<any>,
excludeNotStarted?: FieldPolicy<any> | FieldReadFunction<any>,
excludeUnreadChapters?: FieldPolicy<any> | FieldReadFunction<any>,
extensionRepos?: FieldPolicy<any> | FieldReadFunction<any>,
globalUpdateInterval?: FieldPolicy<any> | FieldReadFunction<any>,
gqlDebugLogsEnabled?: FieldPolicy<any> | FieldReadFunction<any>,
initialOpenInBrowserEnabled?: FieldPolicy<any> | FieldReadFunction<any>,
@@ -621,7 +625,7 @@ export type SettingsFieldPolicy = {
webUIInterface?: FieldPolicy<any> | FieldReadFunction<any>,
webUIUpdateCheckInterval?: FieldPolicy<any> | FieldReadFunction<any>
};
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<any> | FieldReadFunction<any>,
autoDownloadNewChapters?: FieldPolicy<any> | FieldReadFunction<any>,
@@ -640,6 +644,7 @@ export type SettingsTypeFieldPolicy = {
excludeEntryWithUnreadChapters?: FieldPolicy<any> | FieldReadFunction<any>,
excludeNotStarted?: FieldPolicy<any> | FieldReadFunction<any>,
excludeUnreadChapters?: FieldPolicy<any> | FieldReadFunction<any>,
extensionRepos?: FieldPolicy<any> | FieldReadFunction<any>,
globalUpdateInterval?: FieldPolicy<any> | FieldReadFunction<any>,
gqlDebugLogsEnabled?: FieldPolicy<any> | FieldReadFunction<any>,
initialOpenInBrowserEnabled?: FieldPolicy<any> | FieldReadFunction<any>,

View File

@@ -511,6 +511,7 @@ export type ExtensionConditionInput = {
lang?: InputMaybe<Scalars['String']['input']>;
name?: InputMaybe<Scalars['String']['input']>;
pkgName?: InputMaybe<Scalars['String']['input']>;
repo?: InputMaybe<Scalars['String']['input']>;
versionCode?: InputMaybe<Scalars['Int']['input']>;
versionName?: InputMaybe<Scalars['String']['input']>;
};
@@ -534,6 +535,7 @@ export type ExtensionFilterInput = {
not?: InputMaybe<ExtensionFilterInput>;
or?: InputMaybe<Array<ExtensionFilterInput>>;
pkgName?: InputMaybe<StringFilterInput>;
repo?: InputMaybe<StringFilterInput>;
versionCode?: InputMaybe<IntFilterInput>;
versionName?: InputMaybe<StringFilterInput>;
};
@@ -563,6 +565,7 @@ export type ExtensionType = {
lang: Scalars['String']['output'];
name: Scalars['String']['output'];
pkgName: Scalars['String']['output'];
repo?: Maybe<Scalars['String']['output']>;
source: SourceNodeList;
versionCode: Scalars['Int']['output'];
versionName: Scalars['String']['output'];
@@ -880,6 +883,7 @@ export type MangaType = {
thumbnailUrl?: Maybe<Scalars['String']['output']>;
title: Scalars['String']['output'];
unreadCount: Scalars['Int']['output'];
updateStrategy: UpdateStrategy;
url: Scalars['String']['output'];
};
@@ -1263,6 +1267,7 @@ export type PartialSettingsType = Settings & {
excludeEntryWithUnreadChapters?: Maybe<Scalars['Boolean']['output']>;
excludeNotStarted?: Maybe<Scalars['Boolean']['output']>;
excludeUnreadChapters?: Maybe<Scalars['Boolean']['output']>;
extensionRepos?: Maybe<Array<Scalars['String']['output']>>;
globalUpdateInterval?: Maybe<Scalars['Float']['output']>;
gqlDebugLogsEnabled?: Maybe<Scalars['Boolean']['output']>;
initialOpenInBrowserEnabled?: Maybe<Scalars['Boolean']['output']>;
@@ -1299,6 +1304,7 @@ export type PartialSettingsTypeInput = {
excludeEntryWithUnreadChapters?: InputMaybe<Scalars['Boolean']['input']>;
excludeNotStarted?: InputMaybe<Scalars['Boolean']['input']>;
excludeUnreadChapters?: InputMaybe<Scalars['Boolean']['input']>;
extensionRepos?: InputMaybe<Array<Scalars['String']['input']>>;
globalUpdateInterval?: InputMaybe<Scalars['Float']['input']>;
gqlDebugLogsEnabled?: InputMaybe<Scalars['Boolean']['input']>;
initialOpenInBrowserEnabled?: InputMaybe<Scalars['Boolean']['input']>;
@@ -1583,6 +1589,7 @@ export type Settings = {
excludeEntryWithUnreadChapters?: Maybe<Scalars['Boolean']['output']>;
excludeNotStarted?: Maybe<Scalars['Boolean']['output']>;
excludeUnreadChapters?: Maybe<Scalars['Boolean']['output']>;
extensionRepos?: Maybe<Array<Scalars['String']['output']>>;
globalUpdateInterval?: Maybe<Scalars['Float']['output']>;
gqlDebugLogsEnabled?: Maybe<Scalars['Boolean']['output']>;
initialOpenInBrowserEnabled?: Maybe<Scalars['Boolean']['output']>;
@@ -1620,6 +1627,7 @@ export type SettingsType = Settings & {
excludeEntryWithUnreadChapters: Scalars['Boolean']['output'];
excludeNotStarted: Scalars['Boolean']['output'];
excludeUnreadChapters: Scalars['Boolean']['output'];
extensionRepos: Array<Scalars['String']['output']>;
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<Scalars['String']['output']>;
extension: ExtensionType;
extension?: Maybe<ExtensionType>;
};
export type UpdateExtensionsInput = {
@@ -2052,6 +2060,11 @@ export type UpdateStopPayload = {
clientMutationId?: Maybe<Scalars['String']['output']>;
};
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<string>, 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<string>, 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<string>, 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<Scalars['Cursor']['input']>;
@@ -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<string>, 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'];

View File

@@ -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

View File

@@ -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;
}

View File

@@ -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<HTMLInputElement>(null);
const { setTitle, setAction } = useContext(NavBarContext);
const [shownLangs, setShownLangs] = useLocalStorage<string[]>('shownExtensionLangs', extensionDefaultLangs());
@@ -255,7 +258,11 @@ export function Extensions() {
return (
<StyledGroupItemWrapper key={item.apkName} isLastItem={index === visibleExtensions.length - 1}>
<ExtensionCard extension={item} handleUpdate={handleExtensionUpdate} />
<ExtensionCard
extension={item}
handleUpdate={handleExtensionUpdate}
usesCustomRepos={usesCustomRepos}
/>
</StyledGroupItemWrapper>
);
}}

View File

@@ -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)}
/>
</List>
<List
subheader={
<ListSubheader component="div" id="server-settings-extension-repos">
{t('extension.title')}
</ListSubheader>
}
>
<MutableListSetting
settingName={t('extension.settings.repositories.custom.label.title')}
description={t('extension.settings.repositories.custom.label.description')}
handleChange={(repos) => updateSetting('extensionRepos', repos)}
values={serverSettings?.extensionRepos}
addItemButtonTitle={t('extension.settings.repositories.custom.dialog.action.button.add')}
/>
</List>
<List
subheader={
<ListSubheader component="div" id="server-settings-requests">

View File

@@ -1,6 +1,6 @@
[
{
"uiVersion": "PREVIEW",
"serverVersion": "r1438"
"serverVersion": "r1444"
}
]