Add WebUI settings (#460)

* Add WebUI settings

* [VersionMapping] Require server version "r1427" for preview
This commit is contained in:
schroda
2023-11-20 22:14:24 +01:00
committed by GitHub
parent cdf9229457
commit 3766540ce5
7 changed files with 499 additions and 2 deletions

View File

@@ -34,6 +34,7 @@ import { LibrarySettings } from '@/screens/settings/LibrarySettings';
import { DefaultNavBar } from '@/components/navbar/DefaultNavBar';
import { DownloadSettings } from '@/screens/settings/DownloadSettings.tsx';
import { ServerSettings } from '@/screens/settings/ServerSettings.tsx';
import { WebUISettings } from '@/screens/settings/WebUISettings.tsx';
if (__DEV__) {
// Adds messages only in a dev environment
@@ -68,6 +69,7 @@ export const App: React.FC = () => (
<Route path="downloadSettings" element={<DownloadSettings />} />
<Route path="backup" element={<Backup />} />
<Route path="server" element={<ServerSettings />} />
<Route path="webUI" element={<WebUISettings />} />
</Route>
{/* Manga Routes */}

View File

@@ -0,0 +1,158 @@
/*
* 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,
FormControl,
ListItemText,
MenuItem,
Select,
Stack,
Typography,
} from '@mui/material';
import DialogContent from '@mui/material/DialogContent';
import DialogActions from '@mui/material/DialogActions';
import ListItemButton from '@mui/material/ListItemButton';
import { useEffect, useMemo, useState } from 'react';
import { useTranslation } from 'react-i18next';
import DialogContentText from '@mui/material/DialogContentText';
import InfoIcon from '@mui/icons-material/Info';
import { TranslationKey } from '@/typings.ts';
export type SelectSettingValueDisplayInfo = {
text: TranslationKey;
description?: TranslationKey;
disclaimer?: TranslationKey;
};
export type SelectSettingValue<Value> = [Value: Value, DisplayInfo: SelectSettingValueDisplayInfo];
export const SelectSetting = <SettingValue extends string | number>({
settingName,
dialogDescription,
value,
defaultValue,
values,
handleChange,
disabled = false,
}: {
settingName: string;
dialogDescription?: string;
value?: SettingValue;
defaultValue: SettingValue;
values: SelectSettingValue<SettingValue>[];
handleChange: (value: SettingValue) => void;
disabled?: boolean;
}) => {
const { t } = useTranslation();
const [isDialogOpen, setIsDialogOpen] = useState(false);
const [dialogValue, setDialogValue] = useState(value ?? defaultValue);
const valueDisplayText = useMemo(() => values.find(([key]) => key === value)?.[1]?.text, [value]);
const dialogValueDisplayInfo = useMemo(() => values.find(([key]) => key === dialogValue)![1], [dialogValue]);
useEffect(() => {
if (!value) {
return;
}
setDialogValue(value);
}, [value]);
const closeDialog = (resetValue: boolean = true) => {
if (resetValue) {
setDialogValue(value ?? defaultValue);
}
setIsDialogOpen(false);
};
const updateSetting = () => {
closeDialog(false);
handleChange(dialogValue);
};
return (
<>
<ListItemButton disabled={disabled} onClick={() => setIsDialogOpen(true)}>
<ListItemText
primary={settingName}
secondary={valueDisplayText ? t(valueDisplayText) : t('global.label.loading')}
secondaryTypographyProps={{ style: { display: 'flex', flexDirection: 'column' } }}
/>
</ListItemButton>
<Dialog open={isDialogOpen} onClose={() => closeDialog()} fullWidth>
<DialogContent>
<DialogTitle sx={{ paddingLeft: 0 }}>{settingName}</DialogTitle>
{!!dialogDescription && (
<DialogContentText sx={{ paddingBottom: '10px' }}>{dialogDescription}</DialogContentText>
)}
{(!!dialogValueDisplayInfo.description || !!dialogValueDisplayInfo.disclaimer) && (
<DialogContentText sx={{ paddingBottom: '10px' }} component="div">
{dialogValueDisplayInfo.description && (
<Typography
variant="body1"
sx={{
whiteSpace: 'pre-line',
}}
>
{t(dialogValueDisplayInfo.description)}
</Typography>
)}
{dialogValueDisplayInfo.disclaimer && (
<Stack direction="row" alignItems="center">
<InfoIcon color="warning" />
<Typography
variant="body1"
sx={{
marginLeft: '10px',
marginTop: '5px',
whiteSpace: 'pre-line',
}}
>
{t(dialogValueDisplayInfo.disclaimer)}
</Typography>
</Stack>
)}
</DialogContentText>
)}
{/* {!!dialogValueDisplayInfo.disclaimer && ( */}
{/* <DialogContentText sx={{ paddingBottom: '10px', color: 'orange' }}> */}
{/* {t(dialogValueDisplayInfo.disclaimer)} */}
{/* </DialogContentText> */}
{/* )} */}
<FormControl fullWidth>
<Select
id="dialog-select"
value={dialogValue}
onChange={(e) => setDialogValue(e.target.value as SettingValue)}
>
{values.map(([selectValue, { text: selectText }]) => (
<MenuItem key={selectValue} value={selectValue}>
{t(selectText)}
</MenuItem>
))}
</Select>
</FormControl>
</DialogContent>
<DialogActions>
<Button onClick={() => closeDialog()} color="primary">
{t('global.button.cancel')}
</Button>
<Button onClick={() => updateSetting()} color="primary">
{t('global.button.ok')}
</Button>
</DialogActions>
</Dialog>
</>
);
};

View File

@@ -0,0 +1,84 @@
/*
* 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 { useTranslation } from 'react-i18next';
import { List, ListItem, ListItemText, Switch } from '@mui/material';
import ListItemSecondaryAction from '@mui/material/ListItemSecondaryAction';
import { useCallback } from 'react';
import { requestManager } from '@/lib/requests/RequestManager.ts';
import { NumberSetting } from '@/components/settings/NumberSetting.tsx';
import { getPersistedServerSetting, usePersistedValue } from '@/util/usePersistedValue.tsx';
const DEFAULT_VALUE = 23;
const MIN_VALUE = 1;
const MAX_VALUE = 23; // 1 month
export const WebUIUpdateIntervalSetting = ({ disabled = false }: { disabled?: boolean }) => {
const { t } = useTranslation();
const { data } = requestManager.useGetServerSettings();
const updateCheckInterval = data?.settings.webUIUpdateCheckInterval;
const shouldAutoUpdate = !!updateCheckInterval;
const [mutateSettings] = requestManager.useUpdateServerSettings();
const [currentUpdateCheckInterval, persistUpdateCheckInterval] = usePersistedValue(
'lastUpdateCheckInterval',
DEFAULT_VALUE,
updateCheckInterval,
getPersistedServerSetting,
);
const updateSetting = useCallback(
(webUIUpdateCheckInterval: number) => {
persistUpdateCheckInterval(
webUIUpdateCheckInterval === 0 ? currentUpdateCheckInterval : webUIUpdateCheckInterval,
);
mutateSettings({ variables: { input: { settings: { webUIUpdateCheckInterval } } } });
},
[currentUpdateCheckInterval],
);
const setDoAutoUpdates = (enable: boolean) => {
const globalUpdateInterval = enable ? currentUpdateCheckInterval : 0;
updateSetting(globalUpdateInterval);
};
return (
<List>
<ListItem disabled={disabled}>
<ListItemText primary={t('settings.webui.auto_update.label.title')} />
<ListItemSecondaryAction>
<Switch
disabled={disabled}
edge="end"
checked={shouldAutoUpdate}
onChange={(e) => setDoAutoUpdates(e.target.checked)}
/>
</ListItemSecondaryAction>
</ListItem>
<NumberSetting
settingTitle={t('settings.webui.auto_update.label.interval')}
settingValue={
updateCheckInterval !== undefined
? t('library.settings.global_update.auto_update.interval.label.value', {
hours: currentUpdateCheckInterval,
})
: undefined
}
value={currentUpdateCheckInterval}
minValue={MIN_VALUE}
maxValue={MAX_VALUE}
defaultValue={DEFAULT_VALUE}
showSlider
dialogTitle={t('settings.webui.auto_update.label.interval')}
valueUnit={t('global.time.hour_short')}
handleUpdate={updateSetting}
disabled={disabled || !shouldAutoUpdate}
/>
</List>
);
};

View File

@@ -620,7 +620,84 @@
"settings": "Server Settings"
}
},
"title": "Settings"
"title": "Settings",
"webui": {
"auto_update": {
"label": {
"interval": "Update interval",
"title": "Automatically download the latest version"
}
},
"channel": {
"label": {
"title": "Channel"
},
"option": {
"bundled": {
"label": {
"description": "Use the version that was delivered with the server release",
"title": "Bundled"
}
},
"preview": {
"label": {
"description": "Use the latest features and help us get them ready for a stable release.",
"disclaimer": "Features and changes in this version might not be completely ready yet and can cause bugs.\nMake sure that you have automatic backups enabled to prevent loss of your library!",
"title": "Preview"
}
},
"stable": {
"label": {
"description": "Use the latest released version.",
"title": "Stable"
}
}
}
},
"electron_path": {
"label": {
"description": "The path to the electron installation on the server",
"title": "Electron path"
}
},
"flavor": {
"label": {
"title": "Flavor"
},
"option": {
"custom": {
"label": {
"description": "Use a custom $t(settings.webui.title.webui).\nTo use a custom $t(settings.webui.title.webui) replace the content of the \"webUI\" directory in the root directory on the server with the files of the custom $t(settings.webui.title.webui)",
"title": "Custom"
}
},
"webui": {
"label": {
"description": "Use the default $t(settings.webui.title.webui)"
}
}
}
},
"interface": {
"label": {
"description": "Where to start the $t(settings.webui.title.webui) when starting the server",
"title": "Interface"
},
"option": {
"label": {
"browser": "Browser",
"electron": "Electron"
}
}
},
"label": {
"initial_open_browser": "Open the $t(settings.webui.title.webui) when starting the server"
},
"title": {
"settings": "$t(settings.webui.title.webui) Settings",
"webui": "WebUI"
}
}
},
"source": {
"configuration": {

View File

@@ -26,6 +26,7 @@ import CollectionsOutlinedBookmarkIcon from '@mui/icons-material/CollectionsBook
import GetAppOutlinedIcon from '@mui/icons-material/GetAppOutlined';
import ViewModuleIcon from '@mui/icons-material/ViewModule';
import DnsIcon from '@mui/icons-material/Dns';
import WebIcon from '@mui/icons-material/Web';
import { langCodeToName } from '@/util/language';
import { useLocalStorage } from '@/util/useLocalStorage';
import { ListItemLink } from '@/components/util/ListItemLink';
@@ -145,6 +146,12 @@ export function Settings() {
</Select>
</ListItemSecondaryAction>
</ListItem>
<ListItemLink to="/settings/webUI">
<ListItemIcon>
<WebIcon />
</ListItemIcon>
<ListItemText primary={t('settings.webui.title.webui')} />
</ListItemLink>
<ListItemLink to="/settings/server">
<ListItemIcon>
<DnsIcon />

View File

@@ -0,0 +1,169 @@
/*
* 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 { useTranslation } from 'react-i18next';
import { useContext, useEffect } from 'react';
import List from '@mui/material/List';
import { ListItem, ListItemText, Switch } from '@mui/material';
import ListItemSecondaryAction from '@mui/material/ListItemSecondaryAction';
import { NavBarContext, useSetDefaultBackTo } from '@/components/context/NavbarContext.tsx';
import { ServerSettings } from '@/typings.ts';
import { requestManager } from '@/lib/requests/RequestManager.ts';
import { WebUIUpdateIntervalSetting } from '@/components/settings/webUI/WebUIUpdateIntervalSetting.tsx';
import { TextSetting } from '@/components/settings/TextSetting.tsx';
import {
SelectSetting,
SelectSettingValue,
SelectSettingValueDisplayInfo,
} from '@/components/settings/SelectSetting.tsx';
import { WebUiChannel, WebUiFlavor, WebUiInterface } from '@/lib/graphql/generated/graphql.ts';
type WebUISettingsType = Pick<
ServerSettings,
| 'webUIFlavor'
| 'initialOpenInBrowserEnabled'
| 'webUIInterface'
| 'electronPath'
| 'webUIChannel'
| 'webUIUpdateCheckInterval'
>;
const FLAVORS = Object.values(WebUiFlavor);
const FLAVOR_TO_TRANSLATION_KEY: { [flavor in WebUiFlavor]: SelectSettingValueDisplayInfo } = {
[WebUiFlavor.Webui]: {
text: 'settings.webui.title.webui',
description: 'settings.webui.flavor.option.webui.label.description',
},
[WebUiFlavor.Custom]: {
text: 'settings.webui.flavor.option.custom.label.title',
description: 'settings.webui.flavor.option.custom.label.description',
},
};
const FLAVOR_SELECT_VALUES: SelectSettingValue<WebUiFlavor>[] = FLAVORS.map((flavor) => [
flavor,
FLAVOR_TO_TRANSLATION_KEY[flavor],
]);
const CHANNELS = Object.values(WebUiChannel);
const CHANNEL_TO_TRANSLATION_KEYS: {
[channel in WebUiChannel]: SelectSettingValueDisplayInfo;
} = {
[WebUiChannel.Bundled]: {
text: 'settings.webui.channel.option.bundled.label.title',
description: 'settings.webui.channel.option.bundled.label.description',
},
[WebUiChannel.Stable]: {
text: 'settings.webui.channel.option.stable.label.title',
description: 'settings.webui.channel.option.stable.label.description',
},
[WebUiChannel.Preview]: {
text: 'settings.webui.channel.option.preview.label.title',
description: 'settings.webui.channel.option.preview.label.description',
disclaimer: 'settings.webui.channel.option.preview.label.disclaimer',
},
};
const CHANNEL_SELECT_VALUES: SelectSettingValue<WebUiChannel>[] = CHANNELS.map((channel) => [
channel,
CHANNEL_TO_TRANSLATION_KEYS[channel],
]);
const INTERFACES = Object.values(WebUiInterface);
const INTERFACE_TO_TRANSLATION_KEYS: {
[webUIInterface in WebUiInterface]: SelectSettingValueDisplayInfo;
} = {
[WebUiInterface.Browser]: {
text: 'settings.webui.interface.option.label.browser',
description: 'settings.webui.interface.label.description',
},
[WebUiInterface.Electron]: {
text: 'settings.webui.interface.option.label.electron',
description: 'settings.webui.interface.label.description',
},
};
const INTERFACE_SELECT_VALUES: SelectSettingValue<WebUiInterface>[] = INTERFACES.map((webUIInterface) => [
webUIInterface,
INTERFACE_TO_TRANSLATION_KEYS[webUIInterface],
]);
const extractWebUISettings = (settings: ServerSettings): WebUISettingsType => ({
webUIFlavor: settings.webUIFlavor,
initialOpenInBrowserEnabled: settings.initialOpenInBrowserEnabled,
webUIInterface: settings.webUIInterface,
electronPath: settings.electronPath,
webUIChannel: settings.webUIChannel,
webUIUpdateCheckInterval: settings.webUIUpdateCheckInterval,
});
export const WebUISettings = () => {
const { t } = useTranslation();
const { setTitle, setAction } = useContext(NavBarContext);
useSetDefaultBackTo('settings');
useEffect(() => {
setTitle(t('settings.webui.title.settings'));
setAction(null);
}, [t]);
const { data } = requestManager.useGetServerSettings();
const webUISettings = data ? extractWebUISettings(data.settings) : undefined;
const [mutateSettings] = requestManager.useUpdateServerSettings();
const isDefaultWebUI = webUISettings?.webUIFlavor === WebUiFlavor.Webui;
const updateSetting = <Setting extends keyof WebUISettingsType>(
setting: Setting,
value: WebUISettingsType[Setting],
) => {
mutateSettings({ variables: { input: { settings: { [setting]: value } } } });
};
return (
<List>
<SelectSetting<WebUiFlavor>
settingName={t('settings.webui.flavor.label.title')}
value={webUISettings?.webUIFlavor}
defaultValue={WebUiFlavor.Webui}
values={FLAVOR_SELECT_VALUES}
handleChange={(flavor) => updateSetting('webUIFlavor', flavor)}
/>
<ListItem>
<ListItemText primary={t('settings.webui.label.initial_open_browser')} />
<ListItemSecondaryAction>
<Switch
edge="end"
checked={!!webUISettings?.initialOpenInBrowserEnabled}
onChange={(e) => updateSetting('initialOpenInBrowserEnabled', e.target.checked)}
/>
</ListItemSecondaryAction>
</ListItem>
<SelectSetting<WebUiInterface>
settingName={t('settings.webui.interface.label.title')}
value={webUISettings?.webUIInterface}
defaultValue={WebUiInterface.Browser}
values={INTERFACE_SELECT_VALUES}
handleChange={(webUIInterface) => updateSetting('webUIInterface', webUIInterface)}
/>
<TextSetting
settingName={t('settings.webui.electron_path.label.title')}
dialogDescription={t('settings.webui.electron_path.label.description')}
value={webUISettings?.electronPath}
handleChange={(path) => updateSetting('electronPath', path)}
/>
<SelectSetting<WebUiChannel>
settingName={t('settings.webui.channel.label.title')}
value={webUISettings?.webUIChannel}
defaultValue={WebUiChannel.Stable}
values={CHANNEL_SELECT_VALUES}
handleChange={(channel) => updateSetting('webUIChannel', channel)}
disabled={!isDefaultWebUI}
/>
<WebUIUpdateIntervalSetting disabled={!isDefaultWebUI} />
</List>
);
};

View File

@@ -1,6 +1,6 @@
[
{
"uiVersion": "PREVIEW",
"serverVersion": "r1410"
"serverVersion": "r1427"
}
]