Skip to content

Commit

Permalink
Feature/settings support custom extension repos (#539)
Browse files Browse the repository at this point in the history
* [Codegen] Support custom extension repos

* [VersionMapping] Require server version "r1444" for preview
  • Loading branch information
schroda authored Jan 6, 2024
1 parent b42c310 commit 39b79c6
Show file tree
Hide file tree
Showing 12 changed files with 244 additions and 32 deletions.
9 changes: 8 additions & 1 deletion src/components/ExtensionCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import { makeToast } from '@/components/util/Toast.tsx';
interface IProps {
extension: PartialExtension;
handleUpdate: () => void;
usesCustomRepos: boolean;
}

enum ExtensionAction {
Expand Down Expand Up @@ -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),
Expand Down Expand Up @@ -184,6 +186,11 @@ export function ExtensionCard(props: IProps) {
</Typography>
)}
</Typography>
{usesCustomRepos && (
<Typography variant="caption" display="block">
{repo}
</Typography>
)}
</Box>
</Box>

Expand Down
131 changes: 131 additions & 0 deletions src/components/settings/MutableListSetting.tsx
Original file line number Diff line number Diff line change
@@ -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>
</>
);
};
28 changes: 17 additions & 11 deletions src/components/settings/TextSetting.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,23 +17,27 @@ import DialogContentText from '@mui/material/DialogContentText';
import IconButton from '@mui/material/IconButton';
import { Visibility, VisibilityOff } from '@mui/icons-material';

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,
}: {
settingName: string;
dialogDescription?: string;
value?: string;
handleChange: (value: string) => void;
isPassword?: boolean;
placeholder?: string;
disabled?: boolean;
}) => {
}: TextSettingProps) => {
const { t } = useTranslation();

const [isDialogOpen, setIsDialogOpen] = useState(false);
Expand Down Expand Up @@ -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>
)}
Expand Down
19 changes: 19 additions & 0 deletions src/i18n/locale/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand All @@ -247,6 +264,7 @@
},
"global": {
"button": {
"add": "Add",
"browse": "Browse",
"cancel": "Cancel",
"clear": "Clear",
Expand Down Expand Up @@ -326,6 +344,7 @@
"none": "None",
"other": "Other",
"password": "Password",
"placeholder": "Placeholder",
"sort": "Sort",
"unknown": "Unknown",
"username": "Username"
Expand Down
4 changes: 4 additions & 0 deletions src/lib/graphql/Fragments.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -473,6 +474,9 @@ export const SERVER_SETTINGS = gql`
excludeEntryWithUnreadChapters
autoDownloadAheadLimit
# extensions
extensionRepos
# requests
maxSourcesInParallel
Expand Down
15 changes: 10 additions & 5 deletions src/lib/graphql/generated/apollo-helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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>,
Expand All @@ -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>
Expand Down Expand Up @@ -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>,
Expand All @@ -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)[];
Expand Down Expand Up @@ -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>,
Expand All @@ -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>,
Expand Down Expand Up @@ -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>,
Expand All @@ -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>,
Expand All @@ -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>,
Expand All @@ -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>,
Expand Down
Loading

0 comments on commit 39b79c6

Please sign in to comment.