From 2b1c2a0b6c4e6302808fda0fbebc8a812540826e Mon Sep 17 00:00:00 2001 From: schroda <50052685+schroda@users.noreply.github.com> Date: Sun, 20 Jul 2025 17:37:39 +0200 Subject: [PATCH] Add "download conversion" setting support --- public/locales/en.json | 6 + .../graphql/fragments/SettingsFragments.ts | 5 + src/lib/graphql/generated/graphql.ts | 8 +- src/modules/downloads/Downloads.constants.ts | 6 + .../components/DownloadConversionSetting.tsx | 344 ++++++++++++++++++ .../downloads/screens/DownloadSettings.tsx | 7 + versionToServerVersionMapping.json | 2 +- 7 files changed, 373 insertions(+), 5 deletions(-) create mode 100644 src/modules/downloads/components/DownloadConversionSetting.tsx diff --git a/public/locales/en.json b/public/locales/en.json index fe308dce..cce101c8 100644 --- a/public/locales/en.json +++ b/public/locales/en.json @@ -205,6 +205,12 @@ }, "title": "Auto-download" }, + "conversion": { + "compression_level": "Compression level", + "description": "In case no MIME-Type is defined, the \"default\" one will be used for the conversion.", + "mime_type": "MIME-Type", + "title": "Download conversion" + }, "delete_chapters": { "label": { "allow_deletion_of_bookmarked": "Allow deleting bookmarked chapters", diff --git a/src/lib/graphql/fragments/SettingsFragments.ts b/src/lib/graphql/fragments/SettingsFragments.ts index f30f2195..35e16d41 100644 --- a/src/lib/graphql/fragments/SettingsFragments.ts +++ b/src/lib/graphql/fragments/SettingsFragments.ts @@ -37,6 +37,11 @@ export const SERVER_SETTINGS = gql` excludeEntryWithUnreadChapters autoDownloadNewChaptersLimit autoDownloadIgnoreReUploads + downloadConversions { + mimeType + target + compressionLevel + } # extensions extensionRepos diff --git a/src/lib/graphql/generated/graphql.ts b/src/lib/graphql/generated/graphql.ts index 2b1628e8..21d367a1 100644 --- a/src/lib/graphql/generated/graphql.ts +++ b/src/lib/graphql/generated/graphql.ts @@ -2994,7 +2994,7 @@ export type MangaScreenFieldsFragment = { __typename?: 'MangaType', artist?: str export type MangaLibraryDuplicateScreenFieldsFragment = { __typename?: 'MangaType', description?: string | null, id: number, title: string, thumbnailUrl?: string | null, thumbnailUrlLastFetched?: string | null, inLibrary: boolean, initialized: boolean, sourceId: string, unreadCount: number, downloadCount: number, bookmarkCount: number, hasDuplicateChapters: boolean, chapters: { __typename?: 'ChapterNodeList', totalCount: number } }; -export type ServerSettingsFragment = { __typename?: 'SettingsType', ip: string, port: number, socksProxyEnabled: boolean, socksProxyVersion: number, socksProxyHost: string, socksProxyPort: string, socksProxyUsername: string, socksProxyPassword: string, webUIFlavor: WebUiFlavor, initialOpenInBrowserEnabled: boolean, webUIInterface: WebUiInterface, electronPath: string, webUIChannel: WebUiChannel, webUIUpdateCheckInterval: number, downloadAsCbz: boolean, downloadsPath: string, autoDownloadNewChapters: boolean, excludeEntryWithUnreadChapters: boolean, autoDownloadNewChaptersLimit: number, autoDownloadIgnoreReUploads: boolean, extensionRepos: Array, maxSourcesInParallel: number, excludeUnreadChapters: boolean, excludeNotStarted: boolean, excludeCompleted: boolean, globalUpdateInterval: number, updateMangas: boolean, authMode: AuthMode, authPassword: string, authUsername: string, debugLogsEnabled: boolean, systemTrayEnabled: boolean, maxLogFileSize: string, maxLogFiles: number, maxLogFolderSize: string, backupPath: string, backupTime: string, backupInterval: number, backupTTL: number, localSourcePath: string, flareSolverrEnabled: boolean, flareSolverrUrl: string, flareSolverrTimeout: number, flareSolverrSessionName: string, flareSolverrSessionTtl: number, flareSolverrAsResponseFallback: boolean, opdsUseBinaryFileSizes: boolean, opdsItemsPerPage: number, opdsEnablePageReadProgress: boolean, opdsMarkAsReadOnDownload: boolean, opdsShowOnlyUnreadChapters: boolean, opdsShowOnlyDownloadedChapters: boolean, opdsChapterSortOrder: SortOrder }; +export type ServerSettingsFragment = { __typename?: 'SettingsType', ip: string, port: number, socksProxyEnabled: boolean, socksProxyVersion: number, socksProxyHost: string, socksProxyPort: string, socksProxyUsername: string, socksProxyPassword: string, webUIFlavor: WebUiFlavor, initialOpenInBrowserEnabled: boolean, webUIInterface: WebUiInterface, electronPath: string, webUIChannel: WebUiChannel, webUIUpdateCheckInterval: number, downloadAsCbz: boolean, downloadsPath: string, autoDownloadNewChapters: boolean, excludeEntryWithUnreadChapters: boolean, autoDownloadNewChaptersLimit: number, autoDownloadIgnoreReUploads: boolean, extensionRepos: Array, maxSourcesInParallel: number, excludeUnreadChapters: boolean, excludeNotStarted: boolean, excludeCompleted: boolean, globalUpdateInterval: number, updateMangas: boolean, authMode: AuthMode, authPassword: string, authUsername: string, debugLogsEnabled: boolean, systemTrayEnabled: boolean, maxLogFileSize: string, maxLogFiles: number, maxLogFolderSize: string, backupPath: string, backupTime: string, backupInterval: number, backupTTL: number, localSourcePath: string, flareSolverrEnabled: boolean, flareSolverrUrl: string, flareSolverrTimeout: number, flareSolverrSessionName: string, flareSolverrSessionTtl: number, flareSolverrAsResponseFallback: boolean, opdsUseBinaryFileSizes: boolean, opdsItemsPerPage: number, opdsEnablePageReadProgress: boolean, opdsMarkAsReadOnDownload: boolean, opdsShowOnlyUnreadChapters: boolean, opdsShowOnlyDownloadedChapters: boolean, opdsChapterSortOrder: SortOrder, downloadConversions: Array<{ __typename?: 'SettingsDownloadConversionType', mimeType: string, target: string, compressionLevel?: number | null }> }; export type SourceMetaFieldsFragment = { __typename?: 'SourceMetaType', sourceId: string, key: string, value: string }; @@ -3356,14 +3356,14 @@ export type ResetServerSettingsMutationVariables = Exact<{ }>; -export type ResetServerSettingsMutation = { __typename?: 'Mutation', resetSettings: { __typename?: 'ResetSettingsPayload', settings: { __typename?: 'SettingsType', ip: string, port: number, socksProxyEnabled: boolean, socksProxyVersion: number, socksProxyHost: string, socksProxyPort: string, socksProxyUsername: string, socksProxyPassword: string, webUIFlavor: WebUiFlavor, initialOpenInBrowserEnabled: boolean, webUIInterface: WebUiInterface, electronPath: string, webUIChannel: WebUiChannel, webUIUpdateCheckInterval: number, downloadAsCbz: boolean, downloadsPath: string, autoDownloadNewChapters: boolean, excludeEntryWithUnreadChapters: boolean, autoDownloadNewChaptersLimit: number, autoDownloadIgnoreReUploads: boolean, extensionRepos: Array, maxSourcesInParallel: number, excludeUnreadChapters: boolean, excludeNotStarted: boolean, excludeCompleted: boolean, globalUpdateInterval: number, updateMangas: boolean, authMode: AuthMode, authPassword: string, authUsername: string, debugLogsEnabled: boolean, systemTrayEnabled: boolean, maxLogFileSize: string, maxLogFiles: number, maxLogFolderSize: string, backupPath: string, backupTime: string, backupInterval: number, backupTTL: number, localSourcePath: string, flareSolverrEnabled: boolean, flareSolverrUrl: string, flareSolverrTimeout: number, flareSolverrSessionName: string, flareSolverrSessionTtl: number, flareSolverrAsResponseFallback: boolean, opdsUseBinaryFileSizes: boolean, opdsItemsPerPage: number, opdsEnablePageReadProgress: boolean, opdsMarkAsReadOnDownload: boolean, opdsShowOnlyUnreadChapters: boolean, opdsShowOnlyDownloadedChapters: boolean, opdsChapterSortOrder: SortOrder } } }; +export type ResetServerSettingsMutation = { __typename?: 'Mutation', resetSettings: { __typename?: 'ResetSettingsPayload', settings: { __typename?: 'SettingsType', ip: string, port: number, socksProxyEnabled: boolean, socksProxyVersion: number, socksProxyHost: string, socksProxyPort: string, socksProxyUsername: string, socksProxyPassword: string, webUIFlavor: WebUiFlavor, initialOpenInBrowserEnabled: boolean, webUIInterface: WebUiInterface, electronPath: string, webUIChannel: WebUiChannel, webUIUpdateCheckInterval: number, downloadAsCbz: boolean, downloadsPath: string, autoDownloadNewChapters: boolean, excludeEntryWithUnreadChapters: boolean, autoDownloadNewChaptersLimit: number, autoDownloadIgnoreReUploads: boolean, extensionRepos: Array, maxSourcesInParallel: number, excludeUnreadChapters: boolean, excludeNotStarted: boolean, excludeCompleted: boolean, globalUpdateInterval: number, updateMangas: boolean, authMode: AuthMode, authPassword: string, authUsername: string, debugLogsEnabled: boolean, systemTrayEnabled: boolean, maxLogFileSize: string, maxLogFiles: number, maxLogFolderSize: string, backupPath: string, backupTime: string, backupInterval: number, backupTTL: number, localSourcePath: string, flareSolverrEnabled: boolean, flareSolverrUrl: string, flareSolverrTimeout: number, flareSolverrSessionName: string, flareSolverrSessionTtl: number, flareSolverrAsResponseFallback: boolean, opdsUseBinaryFileSizes: boolean, opdsItemsPerPage: number, opdsEnablePageReadProgress: boolean, opdsMarkAsReadOnDownload: boolean, opdsShowOnlyUnreadChapters: boolean, opdsShowOnlyDownloadedChapters: boolean, opdsChapterSortOrder: SortOrder, downloadConversions: Array<{ __typename?: 'SettingsDownloadConversionType', mimeType: string, target: string, compressionLevel?: number | null }> } } }; export type UpdateServerSettingsMutationVariables = Exact<{ input: SetSettingsInput; }>; -export type UpdateServerSettingsMutation = { __typename?: 'Mutation', setSettings: { __typename?: 'SetSettingsPayload', settings: { __typename?: 'SettingsType', ip: string, port: number, socksProxyEnabled: boolean, socksProxyVersion: number, socksProxyHost: string, socksProxyPort: string, socksProxyUsername: string, socksProxyPassword: string, webUIFlavor: WebUiFlavor, initialOpenInBrowserEnabled: boolean, webUIInterface: WebUiInterface, electronPath: string, webUIChannel: WebUiChannel, webUIUpdateCheckInterval: number, downloadAsCbz: boolean, downloadsPath: string, autoDownloadNewChapters: boolean, excludeEntryWithUnreadChapters: boolean, autoDownloadNewChaptersLimit: number, autoDownloadIgnoreReUploads: boolean, extensionRepos: Array, maxSourcesInParallel: number, excludeUnreadChapters: boolean, excludeNotStarted: boolean, excludeCompleted: boolean, globalUpdateInterval: number, updateMangas: boolean, authMode: AuthMode, authPassword: string, authUsername: string, debugLogsEnabled: boolean, systemTrayEnabled: boolean, maxLogFileSize: string, maxLogFiles: number, maxLogFolderSize: string, backupPath: string, backupTime: string, backupInterval: number, backupTTL: number, localSourcePath: string, flareSolverrEnabled: boolean, flareSolverrUrl: string, flareSolverrTimeout: number, flareSolverrSessionName: string, flareSolverrSessionTtl: number, flareSolverrAsResponseFallback: boolean, opdsUseBinaryFileSizes: boolean, opdsItemsPerPage: number, opdsEnablePageReadProgress: boolean, opdsMarkAsReadOnDownload: boolean, opdsShowOnlyUnreadChapters: boolean, opdsShowOnlyDownloadedChapters: boolean, opdsChapterSortOrder: SortOrder } } }; +export type UpdateServerSettingsMutation = { __typename?: 'Mutation', setSettings: { __typename?: 'SetSettingsPayload', settings: { __typename?: 'SettingsType', ip: string, port: number, socksProxyEnabled: boolean, socksProxyVersion: number, socksProxyHost: string, socksProxyPort: string, socksProxyUsername: string, socksProxyPassword: string, webUIFlavor: WebUiFlavor, initialOpenInBrowserEnabled: boolean, webUIInterface: WebUiInterface, electronPath: string, webUIChannel: WebUiChannel, webUIUpdateCheckInterval: number, downloadAsCbz: boolean, downloadsPath: string, autoDownloadNewChapters: boolean, excludeEntryWithUnreadChapters: boolean, autoDownloadNewChaptersLimit: number, autoDownloadIgnoreReUploads: boolean, extensionRepos: Array, maxSourcesInParallel: number, excludeUnreadChapters: boolean, excludeNotStarted: boolean, excludeCompleted: boolean, globalUpdateInterval: number, updateMangas: boolean, authMode: AuthMode, authPassword: string, authUsername: string, debugLogsEnabled: boolean, systemTrayEnabled: boolean, maxLogFileSize: string, maxLogFiles: number, maxLogFolderSize: string, backupPath: string, backupTime: string, backupInterval: number, backupTTL: number, localSourcePath: string, flareSolverrEnabled: boolean, flareSolverrUrl: string, flareSolverrTimeout: number, flareSolverrSessionName: string, flareSolverrSessionTtl: number, flareSolverrAsResponseFallback: boolean, opdsUseBinaryFileSizes: boolean, opdsItemsPerPage: number, opdsEnablePageReadProgress: boolean, opdsMarkAsReadOnDownload: boolean, opdsShowOnlyUnreadChapters: boolean, opdsShowOnlyDownloadedChapters: boolean, opdsChapterSortOrder: SortOrder, downloadConversions: Array<{ __typename?: 'SettingsDownloadConversionType', mimeType: string, target: string, compressionLevel?: number | null }> } } }; export type GetSourceMangasFetchMutationVariables = Exact<{ input: FetchSourceMangaInput; @@ -3747,7 +3747,7 @@ export type GetWebuiUpdateStatusQuery = { __typename?: 'Query', getWebUIUpdateSt export type GetServerSettingsQueryVariables = Exact<{ [key: string]: never; }>; -export type GetServerSettingsQuery = { __typename?: 'Query', settings: { __typename?: 'SettingsType', ip: string, port: number, socksProxyEnabled: boolean, socksProxyVersion: number, socksProxyHost: string, socksProxyPort: string, socksProxyUsername: string, socksProxyPassword: string, webUIFlavor: WebUiFlavor, initialOpenInBrowserEnabled: boolean, webUIInterface: WebUiInterface, electronPath: string, webUIChannel: WebUiChannel, webUIUpdateCheckInterval: number, downloadAsCbz: boolean, downloadsPath: string, autoDownloadNewChapters: boolean, excludeEntryWithUnreadChapters: boolean, autoDownloadNewChaptersLimit: number, autoDownloadIgnoreReUploads: boolean, extensionRepos: Array, maxSourcesInParallel: number, excludeUnreadChapters: boolean, excludeNotStarted: boolean, excludeCompleted: boolean, globalUpdateInterval: number, updateMangas: boolean, authMode: AuthMode, authPassword: string, authUsername: string, debugLogsEnabled: boolean, systemTrayEnabled: boolean, maxLogFileSize: string, maxLogFiles: number, maxLogFolderSize: string, backupPath: string, backupTime: string, backupInterval: number, backupTTL: number, localSourcePath: string, flareSolverrEnabled: boolean, flareSolverrUrl: string, flareSolverrTimeout: number, flareSolverrSessionName: string, flareSolverrSessionTtl: number, flareSolverrAsResponseFallback: boolean, opdsUseBinaryFileSizes: boolean, opdsItemsPerPage: number, opdsEnablePageReadProgress: boolean, opdsMarkAsReadOnDownload: boolean, opdsShowOnlyUnreadChapters: boolean, opdsShowOnlyDownloadedChapters: boolean, opdsChapterSortOrder: SortOrder } }; +export type GetServerSettingsQuery = { __typename?: 'Query', settings: { __typename?: 'SettingsType', ip: string, port: number, socksProxyEnabled: boolean, socksProxyVersion: number, socksProxyHost: string, socksProxyPort: string, socksProxyUsername: string, socksProxyPassword: string, webUIFlavor: WebUiFlavor, initialOpenInBrowserEnabled: boolean, webUIInterface: WebUiInterface, electronPath: string, webUIChannel: WebUiChannel, webUIUpdateCheckInterval: number, downloadAsCbz: boolean, downloadsPath: string, autoDownloadNewChapters: boolean, excludeEntryWithUnreadChapters: boolean, autoDownloadNewChaptersLimit: number, autoDownloadIgnoreReUploads: boolean, extensionRepos: Array, maxSourcesInParallel: number, excludeUnreadChapters: boolean, excludeNotStarted: boolean, excludeCompleted: boolean, globalUpdateInterval: number, updateMangas: boolean, authMode: AuthMode, authPassword: string, authUsername: string, debugLogsEnabled: boolean, systemTrayEnabled: boolean, maxLogFileSize: string, maxLogFiles: number, maxLogFolderSize: string, backupPath: string, backupTime: string, backupInterval: number, backupTTL: number, localSourcePath: string, flareSolverrEnabled: boolean, flareSolverrUrl: string, flareSolverrTimeout: number, flareSolverrSessionName: string, flareSolverrSessionTtl: number, flareSolverrAsResponseFallback: boolean, opdsUseBinaryFileSizes: boolean, opdsItemsPerPage: number, opdsEnablePageReadProgress: boolean, opdsMarkAsReadOnDownload: boolean, opdsShowOnlyUnreadChapters: boolean, opdsShowOnlyDownloadedChapters: boolean, opdsChapterSortOrder: SortOrder, downloadConversions: Array<{ __typename?: 'SettingsDownloadConversionType', mimeType: string, target: string, compressionLevel?: number | null }> } }; export type GetSourceBrowseQueryVariables = Exact<{ id: Scalars['LongString']['input']; diff --git a/src/modules/downloads/Downloads.constants.ts b/src/modules/downloads/Downloads.constants.ts index d1f10059..629c1a6b 100644 --- a/src/modules/downloads/Downloads.constants.ts +++ b/src/modules/downloads/Downloads.constants.ts @@ -12,3 +12,9 @@ export const DOWNLOAD_AHEAD = { default: 2, step: 1, }; + +export const DOWNLOAD_CONVERSION_COMPRESSION = { + min: 0, + max: 1, + step: 0.01, +}; diff --git a/src/modules/downloads/components/DownloadConversionSetting.tsx b/src/modules/downloads/components/DownloadConversionSetting.tsx new file mode 100644 index 00000000..42332341 --- /dev/null +++ b/src/modules/downloads/components/DownloadConversionSetting.tsx @@ -0,0 +1,344 @@ +/* + * 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 ListItemButton from '@mui/material/ListItemButton'; +import ListItemText from '@mui/material/ListItemText'; +import Dialog from '@mui/material/Dialog'; +import DialogTitle from '@mui/material/DialogTitle'; +import DialogContent from '@mui/material/DialogContent'; +import { useState } from 'react'; +import TextField from '@mui/material/TextField'; +import Stack from '@mui/material/Stack'; +import DeleteIcon from '@mui/icons-material/Delete'; +import IconButton from '@mui/material/IconButton'; +import InputAdornment from '@mui/material/InputAdornment'; +import Button from '@mui/material/Button'; +import DialogActions from '@mui/material/DialogActions'; +import DialogContentText from '@mui/material/DialogContentText'; +import { SettingsDownloadConversion } from '@/lib/graphql/generated/graphql.ts'; +import { DOWNLOAD_CONVERSION_COMPRESSION } from '@/modules/downloads/Downloads.constants.ts'; +import { CustomTooltip } from '@/modules/core/components/CustomTooltip.tsx'; +import { TypographyMaxLines } from '@/modules/core/components/texts/TypographyMaxLines.tsx'; + +const DEFAULT_MIME_TYPE = 'default'; +const MIME_TYPE_PREFIX = 'image/'; +const DEFAULT_FOCUS_INDEX = -1; + +const normalizeMimeType = (mimeType: string): string => mimeType.replace(MIME_TYPE_PREFIX, ''); + +const isDefaultMimeType = (mimeType: string): boolean => + normalizeMimeType(mimeType.toLowerCase().trim()) === DEFAULT_MIME_TYPE; + +const isDuplicateConversion = (mimeType: string, index: number, conversions: SettingsDownloadConversion[]): boolean => + conversions.slice(0, index).some((conversion) => conversion.mimeType === mimeType); + +const isValidCompressionLevel = (compression: number | null | undefined): boolean => + compression == null || + (compression >= DOWNLOAD_CONVERSION_COMPRESSION.min && compression <= DOWNLOAD_CONVERSION_COMPRESSION.max); + +const containsInvalidConversion = (conversions: SettingsDownloadConversion[]): boolean => + conversions.some( + ({ mimeType, compressionLevel }, index) => + !isValidCompressionLevel(compressionLevel) || isDuplicateConversion(mimeType, index, conversions), + ); + +const normalizeConversions = (conversions: SettingsDownloadConversion[]): SettingsDownloadConversion[] => + conversions.map((conversion) => ({ + ...conversion, + mimeType: normalizeMimeType(conversion.mimeType), + target: normalizeMimeType(conversion.target), + })); + +const toValidServerConversions = (conversions: SettingsDownloadConversion[]): SettingsDownloadConversion[] => + conversions + .filter(({ mimeType, target }) => !!mimeType && !!target) + .map((conversion) => ({ + ...conversion, + mimeType: `${MIME_TYPE_PREFIX}${conversion.mimeType}`, + target: `${MIME_TYPE_PREFIX}${conversion.target}`, + })); + +const maybeAddDefault = (conversions: SettingsDownloadConversion[]) => { + const isDefaultDefined = conversions.some(({ mimeType }) => isDefaultMimeType(mimeType)); + + return [ + ...(isDefaultDefined + ? [] + : [ + { + mimeType: DEFAULT_MIME_TYPE, + target: '', + }, + ]), + ...conversions, + ]; +}; + +const didUpdateConversions = ( + conversions: SettingsDownloadConversion[], + tmpConversions: SettingsDownloadConversion[], +): boolean => { + if (conversions.length !== tmpConversions.length) { + return true; + } + + return conversions.some(({ mimeType, target, compressionLevel }, index) => { + const tmpConversion = tmpConversions[index]; + + return ( + normalizeMimeType(mimeType) !== tmpConversion.mimeType || + normalizeMimeType(target) !== tmpConversion.target || + compressionLevel !== tmpConversion.compressionLevel + ); + }); +}; + +const MimeTypeTextField = ({ + shouldAutoFocus, + isDefault, + isDuplicate, + label, + value, + onUpdate, +}: { + shouldAutoFocus: boolean; + isDefault: boolean; + isDuplicate: boolean; + label: string; + value: string; + onUpdate: (value: string) => void; +}) => { + const { t } = useTranslation(); + + return ( + {MIME_TYPE_PREFIX}, + }, + }} + onChange={(e) => onUpdate(e.target.value.trim())} + /> + ); +}; + +const Conversion = ({ + index, + shouldAutoFocusMimeTypeTextField, + conversion: { mimeType, target, compressionLevel }, + setFocusMimeTypeTextField, + onChange, + isDuplicate, +}: { + shouldAutoFocusMimeTypeTextField: boolean; + index: number; + conversion: SettingsDownloadConversion; + setFocusMimeTypeTextField: (focus: boolean) => void; + onChange: (newConversion: SettingsDownloadConversion | null) => void; + isDuplicate: boolean; +}) => { + const { t } = useTranslation(); + + const isCompressionLevelValid = isValidCompressionLevel(compressionLevel); + const isDefault = isDefaultMimeType(mimeType) && !isDuplicate; + const isDisabled = isDefault && !target && compressionLevel == null; + + return ( + + + { + setFocusMimeTypeTextField(true); + + onChange({ + mimeType: value, + target, + compressionLevel, + }); + }} + /> + + + onChange({ + mimeType, + target: value, + compressionLevel, + }) + } + /> + { + onChange({ + mimeType, + target, + compressionLevel: e.target.value ? Number(e.target.value) : undefined, + }); + }} + /> + + + { + setFocusMimeTypeTextField(false); + onChange(null); + }} + > + + + + + ); +}; + +export const DownloadConversionSetting = ({ + conversions, + updateSetting, +}: { + conversions: SettingsDownloadConversion[]; + updateSetting: (conversions: SettingsDownloadConversion[]) => void; +}) => { + const { t } = useTranslation(); + + const [isDialogOpen, setIsDialogOpen] = useState(false); + const [tmpConversions, setTmpConversions] = useState(normalizeConversions(maybeAddDefault(conversions))); + const [focusedMimeTypeTextFieldIndex, setFocusedMimeTypeTextFieldIndex] = useState(DEFAULT_FOCUS_INDEX); + + const hasInvalidConversion = containsInvalidConversion(tmpConversions); + + const hasChanged = didUpdateConversions(conversions, tmpConversions); + + const onClose = () => { + setTmpConversions(normalizeConversions(maybeAddDefault(conversions))); + setIsDialogOpen(false); + }; + + return ( + <> + setIsDialogOpen(true)}> + `${conversion.mimeType} → ${conversion.target}`) + .join('; ')} + secondaryTypographyProps={{ style: { display: 'flex', flexDirection: 'column' } }} + /> + + + {t('download.settings.conversion.title')} + + + {t('download.settings.conversion.description')} + + + {tmpConversions.map((conversion, index) => { + const { mimeType } = conversion; + + const isDuplicate = isDuplicateConversion(mimeType, index, tmpConversions); + const shouldAutoFocusMimeTypeTextField = index === focusedMimeTypeTextFieldIndex; + + return ( + + setFocusedMimeTypeTextFieldIndex(focus ? index : DEFAULT_FOCUS_INDEX) + } + shouldAutoFocusMimeTypeTextField={shouldAutoFocusMimeTypeTextField} + onChange={(newConversion) => { + setTmpConversions((prev) => + maybeAddDefault( + prev.toSpliced(index, 1, ...(newConversion ? [newConversion] : [])), + ), + ); + }} + /> + ); + })} + + + + + + + + + + + + + + ); +}; diff --git a/src/modules/downloads/screens/DownloadSettings.tsx b/src/modules/downloads/screens/DownloadSettings.tsx index 973cfa4c..b48c6323 100644 --- a/src/modules/downloads/screens/DownloadSettings.tsx +++ b/src/modules/downloads/screens/DownloadSettings.tsx @@ -32,6 +32,7 @@ import { MetadataDownloadSettings } from '@/modules/downloads/Downloads.types.ts import { ServerSettings } from '@/modules/settings/Settings.types.ts'; import { getErrorMessage } from '@/lib/HelperFunctions.ts'; import { useAppTitle } from '@/modules/navigation-bar/hooks/useAppTitle.ts'; +import { DownloadConversionSetting } from '@/modules/downloads/components/DownloadConversionSetting.tsx'; type DownloadSettingsType = Pick< ServerSettings, @@ -41,6 +42,7 @@ type DownloadSettingsType = Pick< | 'autoDownloadNewChaptersLimit' | 'excludeEntryWithUnreadChapters' | 'autoDownloadIgnoreReUploads' + | 'downloadConversions' >; const extractDownloadSettings = (settings: ServerSettings): DownloadSettingsType => ({ @@ -50,6 +52,7 @@ const extractDownloadSettings = (settings: ServerSettings): DownloadSettingsType autoDownloadNewChaptersLimit: settings.autoDownloadNewChaptersLimit, excludeEntryWithUnreadChapters: settings.excludeEntryWithUnreadChapters, autoDownloadIgnoreReUploads: settings.autoDownloadIgnoreReUploads, + downloadConversions: settings.downloadConversions, }); export const DownloadSettings = () => { @@ -134,6 +137,10 @@ export const DownloadSettings = () => { onChange={(e) => updateSetting('downloadAsCbz', e.target.checked)} /> + updateSetting('downloadConversions', conversions)} + /> diff --git a/versionToServerVersionMapping.json b/versionToServerVersionMapping.json index 18eaf6e8..2dd3a15c 100644 --- a/versionToServerVersionMapping.json +++ b/versionToServerVersionMapping.json @@ -1,7 +1,7 @@ [ { "uiVersion": "PREVIEW", - "serverVersion": "r1837" + "serverVersion": "r1856" }, { "tag": "20250703.01",