diff --git a/frontend/public/locales/en-GB/translation.toml b/frontend/public/locales/en-GB/translation.toml index e9d008535..f9e13f3c1 100644 --- a/frontend/public/locales/en-GB/translation.toml +++ b/frontend/public/locales/en-GB/translation.toml @@ -1278,6 +1278,9 @@ cbzOptions = "CBZ to PDF Options" optimizeForEbook = "Optimize PDF for ebook readers (uses Ghostscript)" cbzOutputOptions = "PDF to CBZ Options" cbzDpi = "DPI for image rendering" +cbrOptions = "CBR Options" +cbrOutputOptions = "PDF to CBR Options" +cbrDpi = "DPI for image rendering" [convert.ebookOptions] ebookOptions = "eBook to PDF Options" diff --git a/frontend/src/core/components/fileEditor/FileEditorThumbnail.tsx b/frontend/src/core/components/fileEditor/FileEditorThumbnail.tsx index 32ad10840..953bd018f 100644 --- a/frontend/src/core/components/fileEditor/FileEditorThumbnail.tsx +++ b/frontend/src/core/components/fileEditor/FileEditorThumbnail.tsx @@ -112,6 +112,7 @@ const FileEditorThumbnail = ({ }, [file.name]); const isCBZ = extLower === 'cbz'; + const isCBR = extLower === 'cbr'; const pageLabel = useMemo( () => @@ -226,7 +227,7 @@ const FileEditorThumbnail = ({ alert({ alertType: 'success', title: `Unzipping ${file.name}`, expandable: false, durationMs: 2500 }); } }, - hidden: !isZipFile || !onUnzipFile || isCBZ, + hidden: !isZipFile || !onUnzipFile || isCBZ || isCBR, }, { id: 'close', @@ -238,7 +239,7 @@ const FileEditorThumbnail = ({ }, color: 'red', } - ], [t, file.id, file.name, isZipFile, onViewFile, onDownloadFile, onUnzipFile, handleCloseWithConfirmation]); + ], [t, file.id, file.name, isZipFile, isCBR, onViewFile, onDownloadFile, onUnzipFile, handleCloseWithConfirmation]); // ---- Card interactions ---- const handleCardClick = () => { diff --git a/frontend/src/core/components/fileManager/FileListItem.tsx b/frontend/src/core/components/fileManager/FileListItem.tsx index 94da35af5..87b458e10 100644 --- a/frontend/src/core/components/fileManager/FileListItem.tsx +++ b/frontend/src/core/components/fileManager/FileListItem.tsx @@ -52,6 +52,11 @@ const FileListItem: React.FC = ({ // Check if this is a ZIP file const isZipFile = zipFileService.isZipFileStub(file); + // Check file extension + const extLower = (file.name?.match(/\.([a-z0-9]+)$/i)?.[1] || '').toLowerCase(); + const isCBZ = extLower === 'cbz'; + const isCBR = extLower === 'cbr'; + // Keep item in hovered state if menu is open const shouldShowHovered = isHovered || isMenuOpen; @@ -240,7 +245,7 @@ const FileListItem: React.FC = ({ )} {/* Unzip option for ZIP files */} - {isZipFile && !isHistoryFile && ( + {isZipFile && !isHistoryFile && !isCBZ && !isCBR && ( <> } diff --git a/frontend/src/core/components/tools/convert/ConvertFromCbrSettings.tsx b/frontend/src/core/components/tools/convert/ConvertFromCbrSettings.tsx new file mode 100644 index 000000000..8f1a80946 --- /dev/null +++ b/frontend/src/core/components/tools/convert/ConvertFromCbrSettings.tsx @@ -0,0 +1,36 @@ +import { Stack, Text, Checkbox } from '@mantine/core'; +import { useTranslation } from 'react-i18next'; +import { ConvertParameters } from "@app/hooks/tools/convert/useConvertParameters"; + +interface ConvertFromCbrSettingsProps { + parameters: ConvertParameters; + onParameterChange: (key: K, value: ConvertParameters[K]) => void; + disabled?: boolean; +} + +const ConvertFromCbrSettings = ({ + parameters, + onParameterChange, + disabled = false +}: ConvertFromCbrSettingsProps) => { + const { t } = useTranslation(); + + return ( + + {t("convert.cbrOptions", "CBR Options")}: + + onParameterChange('cbrOptions', { + ...parameters.cbrOptions, + optimizeForEbook: event.currentTarget.checked + })} + disabled={disabled} + data-testid="optimize-ebook-checkbox" + /> + + ); +}; + +export default ConvertFromCbrSettings; diff --git a/frontend/src/core/components/tools/convert/ConvertSettings.tsx b/frontend/src/core/components/tools/convert/ConvertSettings.tsx index c31928112..127fbb13c 100644 --- a/frontend/src/core/components/tools/convert/ConvertSettings.tsx +++ b/frontend/src/core/components/tools/convert/ConvertSettings.tsx @@ -17,6 +17,8 @@ import ConvertFromEmailSettings from "@app/components/tools/convert/ConvertFromE import ConvertFromCbzSettings from "@app/components/tools/convert/ConvertFromCbzSettings"; import ConvertToCbzSettings from "@app/components/tools/convert/ConvertToCbzSettings"; import ConvertToPdfaSettings from "@app/components/tools/convert/ConvertToPdfaSettings"; +import ConvertFromCbrSettings from "@app/components/tools/convert/ConvertFromCbrSettings"; +import ConvertToCbrSettings from "@app/components/tools/convert/ConvertToCbrSettings"; import ConvertFromEbookSettings from "@app/components/tools/convert/ConvertFromEbookSettings"; import { ConvertParameters } from "@app/hooks/tools/convert/useConvertParameters"; import { @@ -143,6 +145,12 @@ const ConvertSettings = ({ onParameterChange('pdfaOptions', { outputFormat: 'pdfa-1', }); + onParameterChange('cbrOptions', { + optimizeForEbook: false, + }); + onParameterChange('pdfToCbrOptions', { + dpi: 150, + }); onParameterChange('cbzOptions', { optimizeForEbook: false, }); @@ -217,6 +225,12 @@ const ConvertSettings = ({ onParameterChange('pdfaOptions', { outputFormat: 'pdfa-1', }); + onParameterChange('cbrOptions', { + optimizeForEbook: false, + }); + onParameterChange('pdfToCbrOptions', { + dpi: 150, + }); onParameterChange('cbzOptions', { optimizeForEbook: false, }); @@ -385,6 +399,30 @@ const ConvertSettings = ({ )} + {/* CBR to PDF options */} + {parameters.fromExtension === 'cbr' && parameters.toExtension === 'pdf' && ( + <> + + + + )} + + {/* PDF to CBR options */} + {parameters.fromExtension === 'pdf' && parameters.toExtension === 'cbr' && ( + <> + + + + )} + ); }; diff --git a/frontend/src/core/components/tools/convert/ConvertToCbrSettings.tsx b/frontend/src/core/components/tools/convert/ConvertToCbrSettings.tsx new file mode 100644 index 000000000..db85eda77 --- /dev/null +++ b/frontend/src/core/components/tools/convert/ConvertToCbrSettings.tsx @@ -0,0 +1,39 @@ +import { Stack, Text, NumberInput } from "@mantine/core"; +import { useTranslation } from "react-i18next"; +import { ConvertParameters } from "@app/hooks/tools/convert/useConvertParameters"; + +interface ConvertToCbrSettingsProps { + parameters: ConvertParameters; + onParameterChange: (key: K, value: ConvertParameters[K]) => void; + disabled?: boolean; +} + +const ConvertToCbrSettings = ({ + parameters, + onParameterChange, + disabled = false +}: ConvertToCbrSettingsProps) => { + const { t } = useTranslation(); + + return ( + + {t("convert.cbrOutputOptions", "PDF to CBR Options")}: + + + typeof val === 'number' && + onParameterChange('pdfToCbrOptions', { ...parameters.pdfToCbrOptions, dpi: val }) + } + min={72} + max={600} + step={50} + disabled={disabled} + /> + + ); +}; + +export default ConvertToCbrSettings; diff --git a/frontend/src/core/constants/convertConstants.ts b/frontend/src/core/constants/convertConstants.ts index 80e205967..0ffe5783f 100644 --- a/frontend/src/core/constants/convertConstants.ts +++ b/frontend/src/core/constants/convertConstants.ts @@ -34,6 +34,8 @@ export const CONVERSION_ENDPOINTS = { 'html-pdf': '/api/v1/convert/html/pdf', 'markdown-pdf': '/api/v1/convert/markdown/pdf', 'eml-pdf': '/api/v1/convert/eml/pdf', + 'cbr-pdf': '/api/v1/convert/cbr/pdf', + 'pdf-cbr': '/api/v1/convert/pdf/cbr', 'ebook-pdf': '/api/v1/convert/ebook/pdf', 'pdf-text-editor': '/api/v1/convert/pdf/text-editor', 'text-editor-pdf': '/api/v1/convert/text-editor/pdf' @@ -57,6 +59,8 @@ export const ENDPOINT_NAMES = { 'markdown-pdf': 'markdown-to-pdf', 'eml-pdf': 'eml-to-pdf', 'ebook-pdf': 'ebook-to-pdf', + 'cbr-pdf': 'cbr-to-pdf', + 'pdf-cbr': 'pdf-to-cbr', 'pdf-text-editor': 'pdf-to-text-editor', 'text-editor-pdf': 'text-editor-to-pdf' } as const; @@ -68,6 +72,7 @@ export const FROM_FORMAT_OPTIONS = [ { value: 'image', label: 'Images', group: 'Multiple Files' }, { value: 'pdf', label: 'PDF', group: 'Document' }, { value: 'cbz', label: 'CBZ', group: 'Archive' }, + { value: 'cbr', label: 'CBR', group: 'Archive' }, { value: 'docx', label: 'DOCX', group: 'Document' }, { value: 'doc', label: 'DOC', group: 'Document' }, { value: 'odt', label: 'ODT', group: 'Document' }, @@ -103,6 +108,7 @@ export const TO_FORMAT_OPTIONS = [ { value: 'docx', label: 'DOCX', group: 'Document' }, { value: 'odt', label: 'ODT', group: 'Document' }, { value: 'cbz', label: 'CBZ', group: 'Archive' }, + { value: 'cbr', label: 'CBR', group: 'Archive' }, { value: 'csv', label: 'CSV', group: 'Spreadsheet' }, { value: 'pptx', label: 'PPTX', group: 'Presentation' }, { value: 'odp', label: 'ODP', group: 'Presentation' }, @@ -123,7 +129,7 @@ export const TO_FORMAT_OPTIONS = [ export const CONVERSION_MATRIX: Record = { 'any': ['pdf'], // Mixed files always convert to PDF 'image': ['pdf'], // Multiple images always convert to PDF - 'pdf': ['png', 'jpg', 'gif', 'tiff', 'bmp', 'webp', 'docx', 'odt', 'pptx', 'odp', 'csv', 'txt', 'rtf', 'md', 'html', 'xml', 'pdfa', 'cbz'], + 'pdf': ['png', 'jpg', 'gif', 'tiff', 'bmp', 'webp', 'docx', 'odt', 'pptx', 'odp', 'csv', 'txt', 'rtf', 'md', 'html', 'xml', 'pdfa', 'cbz', 'cbr'], 'cbz': ['pdf'], 'docx': ['pdf'], 'doc': ['pdf'], 'odt': ['pdf'], 'xlsx': ['pdf'], 'xls': ['pdf'], 'ods': ['pdf'], @@ -134,6 +140,7 @@ export const CONVERSION_MATRIX: Record = { 'md': ['pdf'], 'txt': ['pdf'], 'rtf': ['pdf'], 'eml': ['pdf'], + 'cbr': ['pdf'], 'epub': ['pdf'], 'mobi': ['pdf'], 'azw3': ['pdf'], 'fb2': ['pdf'] }; @@ -149,6 +156,7 @@ export const EXTENSION_TO_ENDPOINT: Record> = { 'txt': 'pdf-to-text', 'rtf': 'pdf-to-text', 'md': 'pdf-to-markdown', 'html': 'pdf-to-html', 'xml': 'pdf-to-xml', 'pdfa': 'pdf-to-pdfa', + 'cbr': 'pdf-to-cbr', 'cbz': 'pdf-to-cbz' }, 'cbz': { 'pdf': 'cbz-to-pdf' }, @@ -161,6 +169,7 @@ export const EXTENSION_TO_ENDPOINT: Record> = { 'zip': { 'pdf': 'html-to-pdf' }, 'md': { 'pdf': 'markdown-to-pdf' }, 'txt': { 'pdf': 'file-to-pdf' }, 'rtf': { 'pdf': 'file-to-pdf' }, + 'cbr': { 'pdf': 'cbr-to-pdf' }, 'eml': { 'pdf': 'eml-to-pdf' }, 'epub': { 'pdf': 'ebook-to-pdf' }, 'mobi': { 'pdf': 'ebook-to-pdf' }, 'azw3': { 'pdf': 'ebook-to-pdf' }, 'fb2': { 'pdf': 'ebook-to-pdf' } }; diff --git a/frontend/src/core/constants/convertSupportedFornats.ts b/frontend/src/core/constants/convertSupportedFornats.ts index afd1f4384..49adf35a6 100644 --- a/frontend/src/core/constants/convertSupportedFornats.ts +++ b/frontend/src/core/constants/convertSupportedFornats.ts @@ -15,7 +15,7 @@ export const CONVERT_SUPPORTED_FORMATS = [ // Ebook formats 'epub', 'mobi', 'azw3', 'fb2', // Archive formats - 'zip', 'cbz', + 'zip', 'cbr', 'cbz', // Other 'dbf', 'fods', 'vsd', 'vor', 'vor3', 'vor4', 'uop', 'pct', 'ps', 'pdf', ]; diff --git a/frontend/src/core/hooks/tools/convert/useConvertOperation.ts b/frontend/src/core/hooks/tools/convert/useConvertOperation.ts index d5867422f..8fae6cbcf 100644 --- a/frontend/src/core/hooks/tools/convert/useConvertOperation.ts +++ b/frontend/src/core/hooks/tools/convert/useConvertOperation.ts @@ -21,6 +21,8 @@ export const shouldProcessFilesSeparately = ( (parameters.fromExtension === 'pdf' && parameters.toExtension === 'pdfa') || // PDF to text-like formats should be one output per input (parameters.fromExtension === 'pdf' && ['txt', 'rtf', 'csv'].includes(parameters.toExtension)) || + // PDF to CBR conversions (each PDF should generate its own archive) + (parameters.fromExtension === 'pdf' && parameters.toExtension === 'cbr') || // PDF to office format conversions (each PDF should generate its own office file) (parameters.fromExtension === 'pdf' && isOfficeFormat(parameters.toExtension)) || // Office files to PDF conversions (each file should be processed separately via LibreOffice) @@ -40,12 +42,12 @@ export const shouldProcessFilesSeparately = ( // Static function that can be used by both the hook and automation executor export const buildConvertFormData = (parameters: ConvertParameters, selectedFiles: File[]): FormData => { const formData = new FormData(); + const { fromExtension, toExtension, imageOptions, htmlOptions, emailOptions, pdfaOptions, cbrOptions, pdfToCbrOptions, cbzOptions, cbzOutputOptions, ebookOptions } = parameters; selectedFiles.forEach(file => { formData.append("fileInput", file); }); - const { fromExtension, toExtension, imageOptions, htmlOptions, emailOptions, pdfaOptions, cbzOptions, cbzOutputOptions, ebookOptions } = parameters; if (isImageFormat(toExtension)) { formData.append("imageFormat", toExtension); @@ -73,6 +75,10 @@ export const buildConvertFormData = (parameters: ConvertParameters, selectedFile formData.append("outputFormat", pdfaOptions.outputFormat); } else if (fromExtension === 'pdf' && toExtension === 'csv') { formData.append("pageNumbers", "all"); + } else if (fromExtension === 'cbr' && toExtension === 'pdf') { + formData.append("optimizeForEbook", cbrOptions.optimizeForEbook.toString()); + } else if (fromExtension === 'pdf' && toExtension === 'cbr') { + formData.append("dpi", pdfToCbrOptions.dpi.toString()); } else if (fromExtension === 'cbz' && toExtension === 'pdf') { formData.append("optimizeForEbook", (cbzOptions?.optimizeForEbook ?? false).toString()); } else if (fromExtension === 'pdf' && toExtension === 'cbz') { diff --git a/frontend/src/core/hooks/tools/convert/useConvertParameters.ts b/frontend/src/core/hooks/tools/convert/useConvertParameters.ts index ce2ae0fd7..e5cd6d6c6 100644 --- a/frontend/src/core/hooks/tools/convert/useConvertParameters.ts +++ b/frontend/src/core/hooks/tools/convert/useConvertParameters.ts @@ -36,6 +36,12 @@ export interface ConvertParameters extends BaseParameters { pdfaOptions: { outputFormat: string; }; + cbrOptions: { + optimizeForEbook: boolean; + }; + pdfToCbrOptions: { + dpi: number; + }; cbzOptions: { optimizeForEbook: boolean; }; @@ -81,6 +87,12 @@ export const defaultParameters: ConvertParameters = { pdfaOptions: { outputFormat: 'pdfa-1', }, + cbrOptions: { + optimizeForEbook: false, + }, + pdfToCbrOptions: { + dpi: 150, + }, cbzOptions: { optimizeForEbook: false, }, diff --git a/frontend/src/core/tests/convert/ConvertIntegration.test.tsx b/frontend/src/core/tests/convert/ConvertIntegration.test.tsx index bb2f0bcf1..73e776c32 100644 --- a/frontend/src/core/tests/convert/ConvertIntegration.test.tsx +++ b/frontend/src/core/tests/convert/ConvertIntegration.test.tsx @@ -152,6 +152,12 @@ describe('Convert Tool Integration Tests', () => { pdfaOptions: { outputFormat: '' }, + cbrOptions: { + optimizeForEbook: false + }, + pdfToCbrOptions: { + dpi: 150 + }, cbzOptions: { optimizeForEbook: false }, @@ -225,6 +231,12 @@ describe('Convert Tool Integration Tests', () => { pdfaOptions: { outputFormat: '' }, + cbrOptions: { + optimizeForEbook: false + }, + pdfToCbrOptions: { + dpi: 150 + }, cbzOptions: { optimizeForEbook: false }, @@ -276,6 +288,12 @@ describe('Convert Tool Integration Tests', () => { pdfaOptions: { outputFormat: '' }, + cbrOptions: { + optimizeForEbook: false + }, + pdfToCbrOptions: { + dpi: 150 + }, cbzOptions: { optimizeForEbook: false }, @@ -336,6 +354,12 @@ describe('Convert Tool Integration Tests', () => { pdfaOptions: { outputFormat: '' }, + cbrOptions: { + optimizeForEbook: false + }, + pdfToCbrOptions: { + dpi: 150 + }, cbzOptions: { optimizeForEbook: false }, @@ -400,6 +424,12 @@ describe('Convert Tool Integration Tests', () => { pdfaOptions: { outputFormat: '' }, + cbrOptions: { + optimizeForEbook: false + }, + pdfToCbrOptions: { + dpi: 150 + }, cbzOptions: { optimizeForEbook: false }, @@ -462,6 +492,12 @@ describe('Convert Tool Integration Tests', () => { pdfaOptions: { outputFormat: '' }, + cbrOptions: { + optimizeForEbook: false + }, + pdfToCbrOptions: { + dpi: 150 + }, cbzOptions: { optimizeForEbook: false }, @@ -520,6 +556,12 @@ describe('Convert Tool Integration Tests', () => { pdfaOptions: { outputFormat: '' }, + cbrOptions: { + optimizeForEbook: false + }, + pdfToCbrOptions: { + dpi: 150 + }, cbzOptions: { optimizeForEbook: false }, @@ -575,6 +617,12 @@ describe('Convert Tool Integration Tests', () => { pdfaOptions: { outputFormat: '' }, + cbrOptions: { + optimizeForEbook: false + }, + pdfToCbrOptions: { + dpi: 150 + }, cbzOptions: { optimizeForEbook: false }, @@ -632,6 +680,12 @@ describe('Convert Tool Integration Tests', () => { pdfaOptions: { outputFormat: '' }, + cbrOptions: { + optimizeForEbook: false + }, + pdfToCbrOptions: { + dpi: 150 + }, cbzOptions: { optimizeForEbook: false }, @@ -686,6 +740,12 @@ describe('Convert Tool Integration Tests', () => { pdfaOptions: { outputFormat: '' }, + cbrOptions: { + optimizeForEbook: false + }, + pdfToCbrOptions: { + dpi: 150 + }, cbzOptions: { optimizeForEbook: false }, @@ -746,6 +806,12 @@ describe('Convert Tool Integration Tests', () => { pdfaOptions: { outputFormat: '' }, + cbrOptions: { + optimizeForEbook: false + }, + pdfToCbrOptions: { + dpi: 150 + }, cbzOptions: { optimizeForEbook: false }, @@ -805,6 +871,12 @@ describe('Convert Tool Integration Tests', () => { pdfaOptions: { outputFormat: '' }, + cbrOptions: { + optimizeForEbook: false + }, + pdfToCbrOptions: { + dpi: 150 + }, cbzOptions: { optimizeForEbook: false }, diff --git a/frontend/src/core/utils/urlMapping.ts b/frontend/src/core/utils/urlMapping.ts index 9de3d0f5c..f7bb0d666 100644 --- a/frontend/src/core/utils/urlMapping.ts +++ b/frontend/src/core/utils/urlMapping.ts @@ -28,6 +28,8 @@ export const URL_TO_TOOL_MAP: Record = { '/pdf-to-pdfa': 'convert', '/pdf-to-word': 'convert', '/pdf-to-xml': 'convert', + '/cbr-to-pdf': 'convert', + '/pdf-to-cbr': 'convert', '/cbz-to-pdf': 'convert', '/pdf-to-cbz': 'convert',