diff --git a/src/App.tsx b/src/App.tsx index 0c6cecd3..62b198d2 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -32,6 +32,7 @@ import { Updates } from '@/screens/Updates'; import '@/i18n'; import { LibrarySettings } from '@/screens/settings/LibrarySettings'; import { DefaultNavBar } from '@/components/navbar/DefaultNavBar'; +import { DownloadSettings } from '@/screens/settings/DownloadSettings.tsx'; if (__DEV__) { // Adds messages only in a dev environment @@ -63,6 +64,7 @@ export const App: React.FC = () => ( } /> } /> } /> + } /> } /> diff --git a/src/components/settings/NumberSetting.tsx b/src/components/settings/NumberSetting.tsx new file mode 100644 index 00000000..96248e30 --- /dev/null +++ b/src/components/settings/NumberSetting.tsx @@ -0,0 +1,162 @@ +/* + * 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 Dialog from '@mui/material/Dialog'; +import DialogContent from '@mui/material/DialogContent'; +import DialogTitle from '@mui/material/DialogTitle'; +import TextField from '@mui/material/TextField'; +import { InputAdornment, ListItemText } from '@mui/material'; +import DialogActions from '@mui/material/DialogActions'; +import Button from '@mui/material/Button'; +import { useCallback, useEffect, useState } from 'react'; +import { useTranslation } from 'react-i18next'; +import ListItemButton from '@mui/material/ListItemButton'; +import * as React from 'react'; +import ListItemIcon from '@mui/material/ListItemIcon'; +import Slider from '@mui/material/Slider'; + +type BaseProps = { + settingTitle: string; + settingValue: string; + settingIcon?: React.ReactNode; + value: number; + defaultValue?: number; + minValue?: number; + maxValue?: number; + stepSize?: number; + dialogTitle: string; + valueUnit: string; + handleUpdate: (value: number) => void; + showSlider?: never; +}; + +type PropsWithSlider = Omit & + Required> & { showSlider: true }; + +type Props = BaseProps | PropsWithSlider; + +export const NumberSetting = ({ + settingTitle, + settingValue, + settingIcon, + value, + defaultValue, + minValue, + maxValue, + stepSize, + dialogTitle, + valueUnit, + handleUpdate, + showSlider, +}: Props) => { + const { t } = useTranslation(); + + const [isDialogOpen, setIsDialogOpen] = useState(false); + const [dialogValue, setDialogValue] = useState(value); + + const closeDialog = useCallback( + (resetValue: boolean) => { + setIsDialogOpen(false); + + if (resetValue) { + setDialogValue(value); + } + }, + [value], + ); + + const closeDialogWithReset = useCallback(() => closeDialog(true), [closeDialog]); + + const updateSetting = useCallback( + (newValue: number, shouldCloseDialog: boolean = true) => { + if (shouldCloseDialog) { + closeDialog(false); + } + + const didValueChange = value !== newValue; + if (!didValueChange) { + return; + } + + handleUpdate(newValue); + }, + [value, handleUpdate, closeDialog], + ); + + useEffect(() => { + setDialogValue(value); + }, [value]); + + return ( + <> + setIsDialogOpen(true)}> + {settingIcon ? {settingIcon} : null} + + + + + + {dialogTitle} + {valueUnit}, + }} + autoFocus + value={dialogValue} + type="number" + onChange={(e) => setDialogValue(Number(e.target.value))} + /> + {showSlider ? ( + setDialogValue(newValue as number)} + /> + ) : null} + + + {defaultValue !== undefined ? ( + + ) : null} + + + + + + ); +}; diff --git a/src/components/settings/ServerDirSetting.tsx b/src/components/settings/ServerDirSetting.tsx new file mode 100644 index 00000000..00afa44b --- /dev/null +++ b/src/components/settings/ServerDirSetting.tsx @@ -0,0 +1,87 @@ +/* + * 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, ListItemText } from '@mui/material'; +import DialogContent from '@mui/material/DialogContent'; +import TextField from '@mui/material/TextField'; +import DialogActions from '@mui/material/DialogActions'; +import ListItemButton from '@mui/material/ListItemButton'; +import { useEffect, useState } from 'react'; +import { useTranslation } from 'react-i18next'; +import DialogContentText from '@mui/material/DialogContentText'; + +export const ServerDirSetting = ({ + settingName, + dialogDescription, + dirPath, + handlePathChange, +}: { + settingName: string; + dialogDescription: string; + dirPath?: string; + handlePathChange: (path: string) => void; +}) => { + const { t } = useTranslation(); + + const [isDialogOpen, setIsDialogOpen] = useState(false); + const [dialogDirPath, setDialogDirPath] = useState(dirPath ?? ''); + + useEffect(() => { + if (!dirPath) { + return; + } + + setDialogDirPath(dirPath); + }, [dirPath]); + + const closeDialog = () => { + setIsDialogOpen(false); + }; + + const updateSetting = () => { + closeDialog(); + handlePathChange(dialogDirPath); + }; + + return ( + <> + setIsDialogOpen(true)}> + + + + + + {settingName} + {dialogDescription} + setDialogDirPath(e.target.value)} + /> + + + + + + + + ); +}; diff --git a/src/components/settings/downloads/DownloadAheadSetting.tsx b/src/components/settings/downloads/DownloadAheadSetting.tsx new file mode 100644 index 00000000..0bb460aa --- /dev/null +++ b/src/components/settings/downloads/DownloadAheadSetting.tsx @@ -0,0 +1,68 @@ +/* + * 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'; + +const DEFAULT_LIMIT = 5; +const MIN_LIMIT = 2; +const MAX_LIMIT = 10; + +export const DownloadAheadSetting = () => { + const { t } = useTranslation(); + + const { data } = requestManager.useGetServerSettings(); + const downloadAheadLimit = data?.settings.autoDownloadAheadLimit ?? 0; + const shouldDownloadAhead = !!downloadAheadLimit; + const [mutateSettings] = requestManager.useUpdateServerSettings(); + + const updateSetting = useCallback((autoDownloadAheadLimit: number) => { + mutateSettings({ variables: { input: { settings: { autoDownloadAheadLimit } } } }); + }, []); + + const setDoAutoUpdates = (enable: boolean) => { + const globalUpdateInterval = enable ? DEFAULT_LIMIT : 0; + updateSetting(globalUpdateInterval); + }; + + return ( + + + + + setDoAutoUpdates(e.target.checked)} + /> + + + {shouldDownloadAhead ? ( + + ) : null} + + ); +}; diff --git a/src/components/settings/globalUpdate/GlobalUpdateSettingsInterval.tsx b/src/components/settings/globalUpdate/GlobalUpdateSettingsInterval.tsx index f2f50ad4..2d3e3dbd 100644 --- a/src/components/settings/globalUpdate/GlobalUpdateSettingsInterval.tsx +++ b/src/components/settings/globalUpdate/GlobalUpdateSettingsInterval.tsx @@ -7,20 +7,15 @@ */ import { useTranslation } from 'react-i18next'; -import { InputAdornment, List, ListItem, ListItemText, Switch } from '@mui/material'; +import { List, ListItem, ListItemText, Switch } from '@mui/material'; import ListItemSecondaryAction from '@mui/material/ListItemSecondaryAction'; -import ListItemButton from '@mui/material/ListItemButton'; -import { useEffect, useState } from 'react'; -import DialogContent from '@mui/material/DialogContent'; -import DialogTitle from '@mui/material/DialogTitle'; -import DialogActions from '@mui/material/DialogActions'; -import Button from '@mui/material/Button'; -import Dialog from '@mui/material/Dialog'; -import TextField from '@mui/material/TextField'; +import { useCallback } from 'react'; import { requestManager } from '@/lib/requests/RequestManager.ts'; +import { NumberSetting } from '@/components/settings/NumberSetting.tsx'; const DEFAULT_INTERVAL_HOURS = 12; const MIN_INTERVAL_HOURS = 6; +const MAX_INTERVAL_HOURS = 24 * 7 * 4; // 1 month export const GlobalUpdateSettingsInterval = () => { const { t } = useTranslation(); @@ -30,33 +25,15 @@ export const GlobalUpdateSettingsInterval = () => { const doAutoUpdates = !!autoUpdateIntervalHours; const [mutateSettings] = requestManager.useUpdateServerSettings(); - const [isDialogOpen, setIsDialogOpen] = useState(false); - const [dialogUpdateIntervalHours, setDialogUpdateIntervalHours] = useState(autoUpdateIntervalHours); - - const closeDialog = () => { - setIsDialogOpen(false); - }; - - const updateSetting = (globalUpdateInterval: number) => { - closeDialog(); - - const didIntervalChange = autoUpdateIntervalHours !== globalUpdateInterval; - if (!didIntervalChange) { - return; - } - + const updateSetting = useCallback((globalUpdateInterval: number) => { mutateSettings({ variables: { input: { settings: { globalUpdateInterval } } } }); - }; + }, []); const setDoAutoUpdates = (enable: boolean) => { const globalUpdateInterval = enable ? DEFAULT_INTERVAL_HOURS : 0; updateSetting(globalUpdateInterval); }; - useEffect(() => { - setDialogUpdateIntervalHours(autoUpdateIntervalHours); - }, [autoUpdateIntervalHours]); - return ( @@ -66,54 +43,20 @@ export const GlobalUpdateSettingsInterval = () => { {doAutoUpdates ? ( - <> - setIsDialogOpen(true)}> - - - - - - - {t('library.settings.global_update.auto_update.interval.label.title')} - - {t('global.time.hour_short')} - ), - }} - autoFocus - value={dialogUpdateIntervalHours} - type="number" - onChange={(e) => setDialogUpdateIntervalHours(Number(e.target.value))} - /> - - - - - - - + ) : null} ); diff --git a/src/i18n/locale/en.json b/src/i18n/locale/en.json index fc2cd10e..95c859a6 100644 --- a/src/i18n/locale/en.json +++ b/src/i18n/locale/en.json @@ -157,6 +157,35 @@ "queued": "Queued" } }, + "settings": { + "file_type": { + "label": { + "cbz": "Save as CBZ archive" + } + }, + "download_path": { + "label": { + "description": "The path to the directory on the server where downloaded files should get saved in", + "title": "Download location" + } + }, + "auto_download": { + "label": { + "new_chapters": "Download new chapters", + "ignore_with_unread_chapters": "Ignore automatic chapter downloads for entries with unread chapters" + }, + "title": "Auto-download" + }, + "download_ahead": { + "label": { + "while_reading": "Auto download while reading", + "unread_chapters_to_download": "Number of unread chapters to download", + "value": "{{chapters}} $t(chapter.title)" + }, + "title": "Download ahead" + }, + "title": "Download settings" + }, "title": "Downloads" }, "extension": { diff --git a/src/lib/graphql/Fragments.ts b/src/lib/graphql/Fragments.ts index e801d2da..03589f48 100644 --- a/src/lib/graphql/Fragments.ts +++ b/src/lib/graphql/Fragments.ts @@ -406,34 +406,54 @@ export const WEBUI_UPDATE_STATUS = gql` export const SERVER_SETTINGS = gql` fragment SERVER_SETTINGS on SettingsType { - autoDownloadNewChapters - backupInterval - backupPath - backupTTL - backupTime - basicAuthEnabled - basicAuthPassword - basicAuthUsername - debugLogsEnabled - downloadAsCbz - downloadsPath - electronPath - excludeCompleted - excludeNotStarted - excludeUnreadChapters - globalUpdateInterval - initialOpenInBrowserEnabled + # Server ip and port bindings ip - localSourcePath - maxSourcesInParallel port + + # Socks proxy socksProxyEnabled socksProxyHost socksProxyPort - systemTrayEnabled - webUIChannel + + # webUI webUIFlavor + initialOpenInBrowserEnabled webUIInterface + electronPath + webUIChannel webUIUpdateCheckInterval + + # downloader + downloadAsCbz + downloadsPath + autoDownloadNewChapters + excludeEntryWithUnreadChapters + autoDownloadAheadLimit + + # requests + maxSourcesInParallel + + # updater + excludeUnreadChapters + excludeNotStarted + excludeCompleted + globalUpdateInterval + updateMangas + + # Authentication + basicAuthEnabled + basicAuthUsername + basicAuthPassword + + # misc + debugLogsEnabled + gqlDebugLogsEnabled + systemTrayEnabled + + # backup + backupPath + backupTime + backupInterval + backupTTL } `; diff --git a/src/lib/graphql/generated/graphql.ts b/src/lib/graphql/generated/graphql.ts index 5f843c95..e1ac25dc 100644 --- a/src/lib/graphql/generated/graphql.ts +++ b/src/lib/graphql/generated/graphql.ts @@ -2108,7 +2108,7 @@ export type WebuiUpdateInfoFragment = { __typename?: 'WebUIUpdateInfo', channel: export type WebuiUpdateStatusFragment = { __typename?: 'WebUIUpdateStatus', progress: number, state: UpdateState, info: { __typename?: 'WebUIUpdateInfo', channel: string, tag: string, updateAvailable: boolean } }; -export type ServerSettingsFragment = { __typename?: 'SettingsType', autoDownloadNewChapters: boolean, backupInterval: number, backupPath: string, backupTTL: number, backupTime: string, basicAuthEnabled: boolean, basicAuthPassword: string, basicAuthUsername: string, debugLogsEnabled: boolean, downloadAsCbz: boolean, downloadsPath: string, electronPath: string, excludeCompleted: boolean, excludeNotStarted: boolean, excludeUnreadChapters: boolean, globalUpdateInterval: number, initialOpenInBrowserEnabled: boolean, ip: string, localSourcePath: string, maxSourcesInParallel: number, port: number, socksProxyEnabled: boolean, socksProxyHost: string, socksProxyPort: string, systemTrayEnabled: boolean, webUIChannel: WebUiChannel, webUIFlavor: WebUiFlavor, webUIInterface: WebUiInterface, webUIUpdateCheckInterval: number }; +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 }; export type CreateBackupMutationVariables = Exact<{ input: CreateBackupInput; @@ -2388,14 +2388,14 @@ export type ResetServerSettingsMutationVariables = Exact<{ }>; -export type ResetServerSettingsMutation = { __typename?: 'Mutation', resetSettings: { __typename?: 'ResetSettingsPayload', clientMutationId?: string | null, settings: { __typename?: 'SettingsType', autoDownloadNewChapters: boolean, backupInterval: number, backupPath: string, backupTTL: number, backupTime: string, basicAuthEnabled: boolean, basicAuthPassword: string, basicAuthUsername: string, debugLogsEnabled: boolean, downloadAsCbz: boolean, downloadsPath: string, electronPath: string, excludeCompleted: boolean, excludeNotStarted: boolean, excludeUnreadChapters: boolean, globalUpdateInterval: number, initialOpenInBrowserEnabled: boolean, ip: string, localSourcePath: string, maxSourcesInParallel: number, port: number, socksProxyEnabled: boolean, socksProxyHost: string, socksProxyPort: string, systemTrayEnabled: boolean, webUIChannel: WebUiChannel, webUIFlavor: WebUiFlavor, webUIInterface: WebUiInterface, webUIUpdateCheckInterval: number } } }; +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 } } }; export type UpdateServerSettingsMutationVariables = Exact<{ input: SetSettingsInput; }>; -export type UpdateServerSettingsMutation = { __typename?: 'Mutation', setSettings: { __typename?: 'SetSettingsPayload', clientMutationId?: string | null, settings: { __typename?: 'SettingsType', autoDownloadNewChapters: boolean, backupInterval: number, backupPath: string, backupTTL: number, backupTime: string, basicAuthEnabled: boolean, basicAuthPassword: string, basicAuthUsername: string, debugLogsEnabled: boolean, downloadAsCbz: boolean, downloadsPath: string, electronPath: string, excludeCompleted: boolean, excludeNotStarted: boolean, excludeUnreadChapters: boolean, globalUpdateInterval: number, initialOpenInBrowserEnabled: boolean, ip: string, localSourcePath: string, maxSourcesInParallel: number, port: number, socksProxyEnabled: boolean, socksProxyHost: string, socksProxyPort: string, systemTrayEnabled: boolean, webUIChannel: WebUiChannel, webUIFlavor: WebUiFlavor, webUIInterface: WebUiInterface, webUIUpdateCheckInterval: number } } }; +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 } } }; export type GetSourceMangasFetchMutationVariables = Exact<{ input: FetchSourceMangaInput; @@ -2591,7 +2591,7 @@ export type GetWebuiUpdateStatusQuery = { __typename?: 'Query', getWebUIUpdateSt export type GetServerSettingsQueryVariables = Exact<{ [key: string]: never; }>; -export type GetServerSettingsQuery = { __typename?: 'Query', settings: { __typename?: 'SettingsType', autoDownloadNewChapters: boolean, backupInterval: number, backupPath: string, backupTTL: number, backupTime: string, basicAuthEnabled: boolean, basicAuthPassword: string, basicAuthUsername: string, debugLogsEnabled: boolean, downloadAsCbz: boolean, downloadsPath: string, electronPath: string, excludeCompleted: boolean, excludeNotStarted: boolean, excludeUnreadChapters: boolean, globalUpdateInterval: number, initialOpenInBrowserEnabled: boolean, ip: string, localSourcePath: string, maxSourcesInParallel: number, port: number, socksProxyEnabled: boolean, socksProxyHost: string, socksProxyPort: string, systemTrayEnabled: boolean, webUIChannel: WebUiChannel, webUIFlavor: WebUiFlavor, webUIInterface: WebUiInterface, webUIUpdateCheckInterval: number } }; +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 } }; export type GetSourceQueryVariables = Exact<{ id: Scalars['LongString']['input']; diff --git a/src/screens/Settings.tsx b/src/screens/Settings.tsx index 256d74e8..aca083fb 100644 --- a/src/screens/Settings.tsx +++ b/src/screens/Settings.tsx @@ -6,7 +6,7 @@ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ -import { useContext, useEffect, useState } from 'react'; +import { useContext, useEffect, useMemo, useState } from 'react'; import AutoStoriesIcon from '@mui/icons-material/AutoStories'; import List from '@mui/material/List'; import ListAltIcon from '@mui/icons-material/ListAlt'; @@ -29,18 +29,19 @@ import DialogContent from '@mui/material/DialogContent'; import DialogContentText from '@mui/material/DialogContentText'; import TextField from '@mui/material/TextField'; import FavoriteIcon from '@mui/icons-material/Favorite'; -import Slider from '@mui/material/Slider'; -import { DialogTitle, Link, ListItemButton, MenuItem, Select } from '@mui/material'; -import ViewModuleIcon from '@mui/icons-material/ViewModule'; +import { Link, MenuItem, Select } from '@mui/material'; import { useTranslation } from 'react-i18next'; import LanguageIcon from '@mui/icons-material/Language'; import CollectionsOutlinedBookmarkIcon from '@mui/icons-material/CollectionsBookmarkOutlined'; +import GetAppOutlinedIcon from '@mui/icons-material/GetAppOutlined'; +import ViewModuleIcon from '@mui/icons-material/ViewModule'; import { requestManager } from '@/lib/requests/RequestManager.ts'; import { langCodeToName } from '@/util/language'; import { useLocalStorage } from '@/util/useLocalStorage'; import { ListItemLink } from '@/components/util/ListItemLink'; import { DarkTheme } from '@/components/context/DarkTheme'; import { NavBarContext } from '@/components/context/NavbarContext.tsx'; +import { NumberSetting } from '@/components/settings/NumberSetting.tsx'; export function Settings() { const { t, i18n } = useTranslation(); @@ -58,9 +59,9 @@ export function Settings() { const [dialogOpen, setDialogOpen] = useState(false); const [dialogValue, setDialogValue] = useState(serverAddress); - const [dialogOpenItemWidth, setDialogOpenItemWidth] = useState(false); - const [ItemWidth, setItemWidth] = useLocalStorage('ItemWidth', 300); - const [DialogItemWidth, setDialogItemWidth] = useState(ItemWidth); + const DEFAULT_ITEM_WIDTH = 300; + const itemWidthIcon = useMemo(() => , []); + const [itemWidth, setItemWidth] = useLocalStorage('ItemWidth', DEFAULT_ITEM_WIDTH); const handleDialogOpen = () => { setDialogValue(serverAddress); @@ -78,29 +79,6 @@ export function Settings() { requestManager.updateClient({ baseURL: serverBaseUrl }); }; - const handleDialogOpenItemWidth = () => { - setDialogItemWidth(ItemWidth); - setDialogOpenItemWidth(true); - }; - - const handleDialogCancelItemWidth = () => { - setDialogOpenItemWidth(false); - }; - - const handleDialogSubmitItemWidth = () => { - setDialogOpenItemWidth(false); - setItemWidth(DialogItemWidth); - }; - - const handleDialogResetItemWidth = () => { - setDialogOpenItemWidth(false); - setItemWidth(300); - }; - - const handleChange = (event: Event, newValue: number | number[]) => { - setDialogItemWidth(newValue as number); - }; - return ( <> @@ -122,6 +100,12 @@ export function Settings() { + + + + + + @@ -137,16 +121,20 @@ export function Settings() { setDarkTheme(!darkTheme)} /> - { - handleDialogOpenItemWidth(); - }} - > - - - - - + @@ -248,47 +236,6 @@ export function Settings() { - - - {t('settings.label.manga_item_width')} - - setDialogItemWidth(parseInt(e.target.value, 10))} - /> - - - - - - - - ); } diff --git a/src/screens/settings/DownloadSettings.tsx b/src/screens/settings/DownloadSettings.tsx new file mode 100644 index 00000000..f9857768 --- /dev/null +++ b/src/screens/settings/DownloadSettings.tsx @@ -0,0 +1,121 @@ +/* + * 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 ListSubheader from '@mui/material/ListSubheader'; +import { ServerDirSetting } from '@/components/settings/ServerDirSetting'; +import { NavBarContext, useSetDefaultBackTo } from '@/components/context/NavbarContext.tsx'; +import { ServerSettings } from '@/typings.ts'; +import { requestManager } from '@/lib/requests/RequestManager.ts'; +import { DownloadAheadSetting } from '@/components/settings/downloads/DownloadAheadSetting.tsx'; + +type DownloadSettingsType = Pick< + ServerSettings, + | 'downloadAsCbz' + | 'downloadsPath' + | 'autoDownloadNewChapters' + | 'autoDownloadAheadLimit' + | 'excludeEntryWithUnreadChapters' +>; + +const extractDownloadSettings = (settings: ServerSettings): DownloadSettingsType => ({ + downloadAsCbz: settings.downloadAsCbz, + downloadsPath: settings.downloadsPath, + autoDownloadNewChapters: settings.autoDownloadNewChapters, + autoDownloadAheadLimit: settings.autoDownloadAheadLimit, + excludeEntryWithUnreadChapters: settings.excludeEntryWithUnreadChapters, +}); + +export const DownloadSettings = () => { + const { t } = useTranslation(); + const { setTitle, setAction } = useContext(NavBarContext); + + useSetDefaultBackTo('settings'); + + useEffect(() => { + setTitle(t('download.settings.title')); + setAction(null); + }, [t]); + + const { data } = requestManager.useGetServerSettings(); + const downloadSettings = data ? extractDownloadSettings(data.settings) : undefined; + const [mutateSettings] = requestManager.useUpdateServerSettings(); + + const updateSetting = ( + setting: Setting, + value: DownloadSettingsType[Setting], + ) => { + mutateSettings({ variables: { input: { settings: { [setting]: value } } } }); + }; + + return ( + + updateSetting('downloadsPath', path)} + /> + + + + updateSetting('downloadAsCbz', e.target.checked)} + /> + + + + {t('download.settings.auto_download.title')} + + } + > + + + + updateSetting('autoDownloadNewChapters', e.target.checked)} + /> + + + {downloadSettings?.autoDownloadNewChapters ? ( + + + + updateSetting('excludeEntryWithUnreadChapters', e.target.checked)} + /> + + + ) : null} + + + {t('download.settings.download_ahead.title')} + + } + > + + + + ); +};