From 403b677d206a7e217b9fb2160badead534adcfca Mon Sep 17 00:00:00 2001 From: schroda <50052685+schroda@users.noreply.github.com> Date: Sun, 7 Jan 2024 00:04:43 +0100 Subject: [PATCH 1/8] Move "TextSetting" to new folder --- src/components/settings/MutableListSetting.tsx | 2 +- src/components/settings/{ => text}/TextSetting.tsx | 0 src/screens/settings/Backup.tsx | 2 +- src/screens/settings/DownloadSettings.tsx | 2 +- src/screens/settings/ServerSettings.tsx | 2 +- src/screens/settings/WebUISettings.tsx | 2 +- 6 files changed, 5 insertions(+), 5 deletions(-) rename src/components/settings/{ => text}/TextSetting.tsx (100%) diff --git a/src/components/settings/MutableListSetting.tsx b/src/components/settings/MutableListSetting.tsx index 8555727d94..deaa6a0066 100644 --- a/src/components/settings/MutableListSetting.tsx +++ b/src/components/settings/MutableListSetting.tsx @@ -15,7 +15,7 @@ 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'; +import { TextSetting, TextSettingProps } from '@/components/settings/text/TextSetting.tsx'; const MutableListItem = ({ handleDelete, diff --git a/src/components/settings/TextSetting.tsx b/src/components/settings/text/TextSetting.tsx similarity index 100% rename from src/components/settings/TextSetting.tsx rename to src/components/settings/text/TextSetting.tsx diff --git a/src/screens/settings/Backup.tsx b/src/screens/settings/Backup.tsx index d94c4a6167..2ff8929d78 100644 --- a/src/screens/settings/Backup.tsx +++ b/src/screens/settings/Backup.tsx @@ -28,7 +28,7 @@ import { ListItemLink } from '@/components/util/ListItemLink'; import { NavBarContext, useSetDefaultBackTo } from '@/components/context/NavbarContext'; import { BackupRestoreState, ValidateBackupQuery } from '@/lib/graphql/generated/graphql.ts'; import { Progress } from '@/components/util/Progress.tsx'; -import { TextSetting } from '@/components/settings/TextSetting.tsx'; +import { TextSetting } from '@/components/settings/text/TextSetting.tsx'; import { NumberSetting } from '@/components/settings/NumberSetting.tsx'; import { TimeSetting } from '@/components/settings/TimeSetting.tsx'; import { ServerSettings } from '@/typings.ts'; diff --git a/src/screens/settings/DownloadSettings.tsx b/src/screens/settings/DownloadSettings.tsx index 24b6dbc2ad..f6974ace6d 100644 --- a/src/screens/settings/DownloadSettings.tsx +++ b/src/screens/settings/DownloadSettings.tsx @@ -11,7 +11,7 @@ import { useContext, useEffect } from 'react'; import List from '@mui/material/List'; import { ListItem, ListItemText, Switch } from '@mui/material'; import ListSubheader from '@mui/material/ListSubheader'; -import { TextSetting } from '@/components/settings/TextSetting.tsx'; +import { TextSetting } from '@/components/settings/text/TextSetting.tsx'; import { NavBarContext, useSetDefaultBackTo } from '@/components/context/NavbarContext.tsx'; import { MetadataServerSettingKeys, MetadataServerSettings, ServerSettings } from '@/typings.ts'; import { requestManager } from '@/lib/requests/RequestManager.ts'; diff --git a/src/screens/settings/ServerSettings.tsx b/src/screens/settings/ServerSettings.tsx index ff8e4aa032..5f3d127aff 100644 --- a/src/screens/settings/ServerSettings.tsx +++ b/src/screens/settings/ServerSettings.tsx @@ -13,7 +13,7 @@ import ListSubheader from '@mui/material/ListSubheader'; import { NavBarContext, useSetDefaultBackTo } from '@/components/context/NavbarContext.tsx'; import { requestManager } from '@/lib/requests/RequestManager.ts'; import { useLocalStorage } from '@/util/useLocalStorage.tsx'; -import { TextSetting } from '@/components/settings/TextSetting.tsx'; +import { TextSetting } from '@/components/settings/text/TextSetting.tsx'; import { ServerSettings as GqlServerSettings } from '@/typings.ts'; import { NumberSetting } from '@/components/settings/NumberSetting.tsx'; import { MutableListSetting } from '@/components/settings/MutableListSetting.tsx'; diff --git a/src/screens/settings/WebUISettings.tsx b/src/screens/settings/WebUISettings.tsx index 5a031f3163..fd05d83617 100644 --- a/src/screens/settings/WebUISettings.tsx +++ b/src/screens/settings/WebUISettings.tsx @@ -14,7 +14,7 @@ import { NavBarContext, useSetDefaultBackTo } from '@/components/context/NavbarC 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 { TextSetting } from '@/components/settings/text/TextSetting.tsx'; import { SelectSetting, SelectSettingValue, From 6c16cd75aebfefc96dd2a8b13f2d7c232e043c44 Mon Sep 17 00:00:00 2001 From: schroda <50052685+schroda@users.noreply.github.com> Date: Sun, 7 Jan 2024 00:05:34 +0100 Subject: [PATCH 2/8] Extract dialog of "TextSetting" --- src/components/settings/text/TextSetting.tsx | 98 ++------------- .../settings/text/TextSettingDialog.tsx | 113 ++++++++++++++++++ 2 files changed, 120 insertions(+), 91 deletions(-) create mode 100644 src/components/settings/text/TextSettingDialog.tsx diff --git a/src/components/settings/text/TextSetting.tsx b/src/components/settings/text/TextSetting.tsx index bfd461a2f0..9ed90484ad 100644 --- a/src/components/settings/text/TextSetting.tsx +++ b/src/components/settings/text/TextSetting.tsx @@ -6,67 +6,22 @@ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ -import { Button, Dialog, DialogTitle, InputAdornment, ListItemText } from '@mui/material'; -import DialogContent from '@mui/material/DialogContent'; -import TextField from '@mui/material/TextField'; -import DialogActions from '@mui/material/DialogActions'; +import { ListItemText } from '@mui/material'; import ListItemButton from '@mui/material/ListItemButton'; -import { useEffect, useState } from 'react'; +import { useState } from 'react'; import { useTranslation } from 'react-i18next'; -import DialogContentText from '@mui/material/DialogContentText'; -import IconButton from '@mui/material/IconButton'; -import { Visibility, VisibilityOff } from '@mui/icons-material'; +import { TextSettingDialog, TextSettingDialogProps } from '@/components/settings/text/TextSettingDialog.tsx'; -export type TextSettingProps = { - settingName: string; - dialogTitle?: string; - dialogDescription?: string; - value?: string; - handleChange: (value: string) => void; - isPassword?: boolean; - placeholder?: string; +export type TextSettingProps = Omit & { disabled?: boolean; }; -export const TextSetting = ({ - settingName, - dialogTitle = settingName, - dialogDescription, - value, - handleChange, - isPassword = false, - placeholder = '', - disabled = false, -}: TextSettingProps) => { +export const TextSetting = (props: TextSettingProps) => { const { t } = useTranslation(); const [isDialogOpen, setIsDialogOpen] = useState(false); - const [dialogValue, setDialogValue] = useState(value ?? ''); - const [showPassword, setShowPassword] = useState(false); - const handleClickShowPassword = () => setShowPassword((show) => !show); - - useEffect(() => { - if (!value) { - return; - } - - setDialogValue(value); - }, [value]); - - const closeDialog = (resetValue: boolean = true) => { - if (resetValue) { - setDialogValue(value ?? ''); - } - - setShowPassword(false); - setIsDialogOpen(false); - }; - - const updateSetting = () => { - closeDialog(false); - handleChange(dialogValue); - }; + const { settingName, value, isPassword = false, disabled = false } = props; return ( <> @@ -80,46 +35,7 @@ export const TextSetting = ({ /> - closeDialog()} fullWidth> - - {dialogTitle} - {!!dialogDescription && ( - {dialogDescription} - )} - setDialogValue(e.target.value)} - InputProps={{ - endAdornment: isPassword ? ( - - - {showPassword ? : } - - - ) : null, - }} - /> - - - - - - + ); }; diff --git a/src/components/settings/text/TextSettingDialog.tsx b/src/components/settings/text/TextSettingDialog.tsx new file mode 100644 index 0000000000..ce1f59ba9c --- /dev/null +++ b/src/components/settings/text/TextSettingDialog.tsx @@ -0,0 +1,113 @@ +/* + * 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, InputAdornment } from '@mui/material'; +import DialogContent from '@mui/material/DialogContent'; +import DialogContentText from '@mui/material/DialogContentText'; +import TextField from '@mui/material/TextField'; +import IconButton from '@mui/material/IconButton'; +import { Visibility, VisibilityOff } from '@mui/icons-material'; +import DialogActions from '@mui/material/DialogActions'; +import { useEffect, useState } from 'react'; +import { useTranslation } from 'react-i18next'; + +export type TextSettingDialogProps = { + settingName: string; + dialogTitle?: string; + dialogDescription?: string; + value?: string; + handleChange: (value: string) => void; + isPassword?: boolean; + placeholder?: string; + isDialogOpen: boolean; + setIsDialogOpen: (open: boolean) => void; +}; + +export const TextSettingDialog = ({ + settingName, + dialogTitle = settingName, + dialogDescription, + value, + handleChange, + isPassword = false, + placeholder = '', + isDialogOpen, + setIsDialogOpen, +}: TextSettingDialogProps) => { + const { t } = useTranslation(); + + const [dialogValue, setDialogValue] = useState(value ?? ''); + const [showPassword, setShowPassword] = useState(false); + + const handleClickShowPassword = () => setShowPassword((show) => !show); + + useEffect(() => { + if (!value) { + return; + } + + setDialogValue(value); + }, [value]); + + const closeDialog = (resetValue: boolean = true) => { + if (resetValue) { + setDialogValue(value ?? ''); + } + + setShowPassword(false); + setIsDialogOpen(false); + }; + + const updateSetting = () => { + closeDialog(false); + handleChange(dialogValue); + }; + + return ( + closeDialog()} fullWidth> + + {dialogTitle} + {!!dialogDescription && ( + {dialogDescription} + )} + setDialogValue(e.target.value)} + InputProps={{ + endAdornment: isPassword ? ( + + + {showPassword ? : } + + + ) : null, + }} + /> + + + + + + + ); +}; From 3e6dcbdea4bd37340889b813f60781cadf76ab61 Mon Sep 17 00:00:00 2001 From: schroda <50052685+schroda@users.noreply.github.com> Date: Sun, 7 Jan 2024 00:08:51 +0100 Subject: [PATCH 3/8] Immediately show dialog when adding a list item --- src/components/settings/MutableListSetting.tsx | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/src/components/settings/MutableListSetting.tsx b/src/components/settings/MutableListSetting.tsx index deaa6a0066..8f539b4491 100644 --- a/src/components/settings/MutableListSetting.tsx +++ b/src/components/settings/MutableListSetting.tsx @@ -16,6 +16,7 @@ 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/text/TextSetting.tsx'; +import { TextSettingDialog } from '@/components/settings/text/TextSettingDialog.tsx'; const MutableListItem = ({ handleDelete, @@ -53,6 +54,8 @@ export const MutableListSetting = ({ const [isDialogOpen, setIsDialogOpen] = useState(false); const [dialogValues, setDialogValues] = useState(values ?? []); + const [isAddItemDialogOpen, setIsAddItemDialogOpen] = useState(false); + useEffect(() => { if (!values) { return; @@ -105,7 +108,7 @@ export const MutableListSetting = ({ {dialogValues.map((dialogValue, index) => ( updateSetting(index, newValue)} handleDelete={() => updateSetting(index, undefined)} @@ -116,7 +119,7 @@ export const MutableListSetting = ({ - @@ -126,6 +129,16 @@ export const MutableListSetting = ({ + + {isAddItemDialogOpen && ( + updateSetting(dialogValues.length, newValue)} + isDialogOpen={isAddItemDialogOpen} + setIsDialogOpen={setIsAddItemDialogOpen} + /> + )} ); }; From 785cff92268354f1fb13f15bd722e72e55aadd5e Mon Sep 17 00:00:00 2001 From: schroda <50052685+schroda@users.noreply.github.com> Date: Sun, 7 Jan 2024 00:10:54 +0100 Subject: [PATCH 4/8] Reuse "TextSettingProps" for "MutableListSetting" --- src/components/settings/MutableListSetting.tsx | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/src/components/settings/MutableListSetting.tsx b/src/components/settings/MutableListSetting.tsx index 8f539b4491..a74d5ef2b4 100644 --- a/src/components/settings/MutableListSetting.tsx +++ b/src/components/settings/MutableListSetting.tsx @@ -36,19 +36,20 @@ const MutableListItem = ({ ); }; +type MutableListSettingProps = Pick & { + values?: string[]; + description?: string[]; + addItemButtonTitle?: string; + handleChange: (values: string[]) => void; +}; + export const MutableListSetting = ({ settingName, description, values, handleChange, addItemButtonTitle, -}: { - settingName: string; - description?: string; - values?: string[]; - handleChange: (values: string[]) => void; - addItemButtonTitle?: string; -}) => { +}: MutableListSettingProps) => { const { t } = useTranslation(); const [isDialogOpen, setIsDialogOpen] = useState(false); From 5715413534a4a89daba933cfcb4804a8b7444b46 Mon Sep 17 00:00:00 2001 From: schroda <50052685+schroda@users.noreply.github.com> Date: Sun, 7 Jan 2024 00:13:14 +0100 Subject: [PATCH 5/8] Add "placeholder" prop to "MutableListSetting" --- src/components/settings/MutableListSetting.tsx | 5 +++-- src/screens/settings/ServerSettings.tsx | 1 + 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/src/components/settings/MutableListSetting.tsx b/src/components/settings/MutableListSetting.tsx index a74d5ef2b4..cfbe73d729 100644 --- a/src/components/settings/MutableListSetting.tsx +++ b/src/components/settings/MutableListSetting.tsx @@ -49,6 +49,7 @@ export const MutableListSetting = ({ values, handleChange, addItemButtonTitle, + placeholder, }: MutableListSettingProps) => { const { t } = useTranslation(); @@ -110,7 +111,7 @@ export const MutableListSetting = ({ {dialogValues.map((dialogValue, index) => ( updateSetting(index, newValue)} handleDelete={() => updateSetting(index, undefined)} value={dialogValue} @@ -134,7 +135,7 @@ export const MutableListSetting = ({ {isAddItemDialogOpen && ( updateSetting(dialogValues.length, newValue)} isDialogOpen={isAddItemDialogOpen} setIsDialogOpen={setIsAddItemDialogOpen} diff --git a/src/screens/settings/ServerSettings.tsx b/src/screens/settings/ServerSettings.tsx index 5f3d127aff..2aa81901ad 100644 --- a/src/screens/settings/ServerSettings.tsx +++ b/src/screens/settings/ServerSettings.tsx @@ -136,6 +136,7 @@ export const ServerSettings = () => { handleChange={(repos) => updateSetting('extensionRepos', repos)} values={serverSettings?.extensionRepos} addItemButtonTitle={t('extension.settings.repositories.custom.dialog.action.button.add')} + placeholder="https://github.com/MY_ACCOUNT/MY_REPO/tree/repo" /> Date: Sun, 7 Jan 2024 00:19:22 +0100 Subject: [PATCH 6/8] Prevent duplicates by default --- src/components/settings/MutableListSetting.tsx | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/components/settings/MutableListSetting.tsx b/src/components/settings/MutableListSetting.tsx index cfbe73d729..5454c93205 100644 --- a/src/components/settings/MutableListSetting.tsx +++ b/src/components/settings/MutableListSetting.tsx @@ -41,6 +41,7 @@ type MutableListSettingProps = Pick void; + allowDuplicates?: boolean; }; export const MutableListSetting = ({ @@ -50,6 +51,7 @@ export const MutableListSetting = ({ handleChange, addItemButtonTitle, placeholder, + allowDuplicates = false, }: MutableListSettingProps) => { const { t } = useTranslation(); @@ -81,6 +83,11 @@ export const MutableListSetting = ({ return; } + const isDuplicate = !allowDuplicates && dialogValues.includes(newValue); + if (isDuplicate) { + return; + } + setDialogValues(dialogValues.toSpliced(index, 1, newValue.trim())); }; From aa7732d3e35908d1bbabf052a302cc36b1ae1fa3 Mon Sep 17 00:00:00 2001 From: schroda <50052685+schroda@users.noreply.github.com> Date: Sun, 7 Jan 2024 00:40:58 +0100 Subject: [PATCH 7/8] Clear dialog values on save --- src/components/settings/MutableListSetting.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/settings/MutableListSetting.tsx b/src/components/settings/MutableListSetting.tsx index 5454c93205..7c2711e661 100644 --- a/src/components/settings/MutableListSetting.tsx +++ b/src/components/settings/MutableListSetting.tsx @@ -92,7 +92,7 @@ export const MutableListSetting = ({ }; const saveChanges = () => { - closeDialog(); + closeDialog(true); handleChange(dialogValues.filter((dialogValue) => dialogValue !== '')); }; From cae61f68bb7bb7f0237e98ca28c27b1c3817ff32 Mon Sep 17 00:00:00 2001 From: schroda <50052685+schroda@users.noreply.github.com> Date: Sun, 7 Jan 2024 16:23:36 +0100 Subject: [PATCH 8/8] Validate repo url --- src/components/settings/MutableListSetting.tsx | 16 +++++++++++++++- src/i18n/locale/en.json | 6 ++++++ src/screens/settings/ServerSettings.tsx | 6 ++++++ 3 files changed, 27 insertions(+), 1 deletion(-) diff --git a/src/components/settings/MutableListSetting.tsx b/src/components/settings/MutableListSetting.tsx index 7c2711e661..b87b0374f3 100644 --- a/src/components/settings/MutableListSetting.tsx +++ b/src/components/settings/MutableListSetting.tsx @@ -17,6 +17,7 @@ import IconButton from '@mui/material/IconButton'; import DialogContentText from '@mui/material/DialogContentText'; import { TextSetting, TextSettingProps } from '@/components/settings/text/TextSetting.tsx'; import { TextSettingDialog } from '@/components/settings/text/TextSettingDialog.tsx'; +import { makeToast } from '@/components/util/Toast.tsx'; const MutableListItem = ({ handleDelete, @@ -38,10 +39,12 @@ const MutableListItem = ({ type MutableListSettingProps = Pick & { values?: string[]; - description?: string[]; + description?: string; addItemButtonTitle?: string; handleChange: (values: string[]) => void; allowDuplicates?: boolean; + validateItem?: (value: string) => boolean; + invalidItemError?: string; }; export const MutableListSetting = ({ @@ -52,6 +55,8 @@ export const MutableListSetting = ({ addItemButtonTitle, placeholder, allowDuplicates = false, + validateItem = () => true, + invalidItemError, }: MutableListSettingProps) => { const { t } = useTranslation(); @@ -88,6 +93,15 @@ export const MutableListSetting = ({ return; } + if (newValue === '') { + return; + } + + if (!validateItem(newValue)) { + makeToast(invalidItemError ?? t('global.error.label.invalid_input'), 'error'); + return; + } + setDialogValues(dialogValues.toSpliced(index, 1, newValue.trim())); }; diff --git a/src/i18n/locale/en.json b/src/i18n/locale/en.json index 734c703ac4..b20e61f850 100644 --- a/src/i18n/locale/en.json +++ b/src/i18n/locale/en.json @@ -243,6 +243,11 @@ } } }, + "error": { + "label": { + "invalid_url": "Invalid repository url" + } + }, "label": { "description": "Add custom repositories from which extensions can be installed", "title": "Custom repositories" @@ -309,6 +314,7 @@ "failed_to_save_changes": "Failed to save changes", "invalid_action": "This is not a valid action", "invalid_file_type": "Invalid filetype", + "invalid_input": "Invalid input", "update_failed": "Could not check for updates" } }, diff --git a/src/screens/settings/ServerSettings.tsx b/src/screens/settings/ServerSettings.tsx index 2aa81901ad..0fed83a909 100644 --- a/src/screens/settings/ServerSettings.tsx +++ b/src/screens/settings/ServerSettings.tsx @@ -137,6 +137,12 @@ export const ServerSettings = () => { values={serverSettings?.extensionRepos} addItemButtonTitle={t('extension.settings.repositories.custom.dialog.action.button.add')} placeholder="https://github.com/MY_ACCOUNT/MY_REPO/tree/repo" + validateItem={(repo) => + !!repo.match( + /https:\/\/(?:www|raw)?(?:github|githubusercontent)\.com\/([^/]+)\/([^/]+)(?:\/(?:tree|blob)\/(.*))?\/?/g, + ) + } + invalidItemError={t('extension.settings.repositories.custom.error.label.invalid_url')} />