/* * 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] : [])), ), ); }} /> ); })} ); };