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)}>
+
+
+
+
+ >
+ );
+};
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"
}
]