From 3766540ce59d19918cfa2f7b8e1454782087d71f Mon Sep 17 00:00:00 2001 From: schroda <50052685+schroda@users.noreply.github.com> Date: Mon, 20 Nov 2023 22:14:24 +0100 Subject: [PATCH] Add WebUI settings (#460) * Add WebUI settings * [VersionMapping] Require server version "r1427" for preview --- src/App.tsx | 2 + src/components/settings/SelectSetting.tsx | 158 ++++++++++++++++ .../webUI/WebUIUpdateIntervalSetting.tsx | 84 +++++++++ src/i18n/locale/en.json | 79 +++++++- src/screens/Settings.tsx | 7 + src/screens/settings/WebUISettings.tsx | 169 ++++++++++++++++++ versionToServerVersionMapping.json | 2 +- 7 files changed, 499 insertions(+), 2 deletions(-) create mode 100644 src/components/settings/SelectSetting.tsx create mode 100644 src/components/settings/webUI/WebUIUpdateIntervalSetting.tsx create mode 100644 src/screens/settings/WebUISettings.tsx diff --git a/src/App.tsx b/src/App.tsx index 6f011f1c..d4d695ea 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -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 = () => ( } /> } /> } /> + } /> {/* Manga Routes */} diff --git a/src/components/settings/SelectSetting.tsx b/src/components/settings/SelectSetting.tsx new file mode 100644 index 00000000..52b81cef --- /dev/null +++ b/src/components/settings/SelectSetting.tsx @@ -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, DisplayInfo: SelectSettingValueDisplayInfo]; + +export const SelectSetting = ({ + settingName, + dialogDescription, + value, + defaultValue, + values, + handleChange, + disabled = false, +}: { + settingName: string; + dialogDescription?: string; + value?: SettingValue; + defaultValue: SettingValue; + values: SelectSettingValue[]; + 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 ( + <> + setIsDialogOpen(true)}> + + + + closeDialog()} fullWidth> + + {settingName} + {!!dialogDescription && ( + {dialogDescription} + )} + {(!!dialogValueDisplayInfo.description || !!dialogValueDisplayInfo.disclaimer) && ( + + {dialogValueDisplayInfo.description && ( + + {t(dialogValueDisplayInfo.description)} + + )} + {dialogValueDisplayInfo.disclaimer && ( + + + + {t(dialogValueDisplayInfo.disclaimer)} + + + )} + + )} + {/* {!!dialogValueDisplayInfo.disclaimer && ( */} + {/* */} + {/* {t(dialogValueDisplayInfo.disclaimer)} */} + {/* */} + {/* )} */} + + + + + + + + + + + ); +}; diff --git a/src/components/settings/webUI/WebUIUpdateIntervalSetting.tsx b/src/components/settings/webUI/WebUIUpdateIntervalSetting.tsx new file mode 100644 index 00000000..e1d804d7 --- /dev/null +++ b/src/components/settings/webUI/WebUIUpdateIntervalSetting.tsx @@ -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 ( + + + + + setDoAutoUpdates(e.target.checked)} + /> + + + + + ); +}; diff --git a/src/i18n/locale/en.json b/src/i18n/locale/en.json index 731fc5c8..9c9baa4a 100644 --- a/src/i18n/locale/en.json +++ b/src/i18n/locale/en.json @@ -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": { diff --git a/src/screens/Settings.tsx b/src/screens/Settings.tsx index d341e819..78f9a26c 100644 --- a/src/screens/Settings.tsx +++ b/src/screens/Settings.tsx @@ -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() { + + + + + + diff --git a/src/screens/settings/WebUISettings.tsx b/src/screens/settings/WebUISettings.tsx new file mode 100644 index 00000000..9537edfa --- /dev/null +++ b/src/screens/settings/WebUISettings.tsx @@ -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[] = 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[] = 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[] = 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: Setting, + value: WebUISettingsType[Setting], + ) => { + mutateSettings({ variables: { input: { settings: { [setting]: value } } } }); + }; + + return ( + + + settingName={t('settings.webui.flavor.label.title')} + value={webUISettings?.webUIFlavor} + defaultValue={WebUiFlavor.Webui} + values={FLAVOR_SELECT_VALUES} + handleChange={(flavor) => updateSetting('webUIFlavor', flavor)} + /> + + + + updateSetting('initialOpenInBrowserEnabled', e.target.checked)} + /> + + + + settingName={t('settings.webui.interface.label.title')} + value={webUISettings?.webUIInterface} + defaultValue={WebUiInterface.Browser} + values={INTERFACE_SELECT_VALUES} + handleChange={(webUIInterface) => updateSetting('webUIInterface', webUIInterface)} + /> + updateSetting('electronPath', path)} + /> + + settingName={t('settings.webui.channel.label.title')} + value={webUISettings?.webUIChannel} + defaultValue={WebUiChannel.Stable} + values={CHANNEL_SELECT_VALUES} + handleChange={(channel) => updateSetting('webUIChannel', channel)} + disabled={!isDefaultWebUI} + /> + + + ); +}; diff --git a/versionToServerVersionMapping.json b/versionToServerVersionMapping.json index 80ef0844..acad1a08 100644 --- a/versionToServerVersionMapping.json +++ b/versionToServerVersionMapping.json @@ -1,6 +1,6 @@ [ { "uiVersion": "PREVIEW", - "serverVersion": "r1410" + "serverVersion": "r1427" } ]