mirror of
https://github.com/Stirling-Tools/Stirling-PDF.git
synced 2025-09-18 09:29:24 +00:00
Initial commit of Rotate
This commit is contained in:
parent
a57373b968
commit
eee9d3b022
@ -759,7 +759,32 @@
|
||||
"title": "Rotate PDF",
|
||||
"header": "Rotate PDF",
|
||||
"selectAngle": "Select rotation angle (in multiples of 90 degrees):",
|
||||
"submit": "Rotate"
|
||||
"submit": "Rotate",
|
||||
"error": {
|
||||
"failed": "An error occurred while rotating the PDF.",
|
||||
"invalidAngle": "Rotation angle must be a multiple of 90 degrees"
|
||||
},
|
||||
"preview": {
|
||||
"title": "Rotation Preview"
|
||||
},
|
||||
"rotateLeft": "Rotate Anticlockwise",
|
||||
"rotateRight": "Rotate Clockwise",
|
||||
"noRotation": "No rotation",
|
||||
"currentAngle": "Current rotation: {{angle}}°",
|
||||
"controlsHelp": "Each click rotates by 90 degrees",
|
||||
"tooltip": {
|
||||
"header": {
|
||||
"title": "Rotate Settings Overview"
|
||||
},
|
||||
"description": {
|
||||
"title": "Description",
|
||||
"text": "Rotate your PDF pages clockwise or anticlockwise in 90-degree increments. All pages in the PDF will be rotated. The preview shows how your document will look after rotation."
|
||||
},
|
||||
"controls": {
|
||||
"title": "Controls",
|
||||
"text": "Use the rotation buttons to adjust orientation. Left button rotates anticlockwise, right button rotates clockwise. Each click rotates by 90 degrees."
|
||||
}
|
||||
}
|
||||
},
|
||||
"convert": {
|
||||
"title": "Convert",
|
||||
|
121
frontend/src/components/tools/rotate/RotateSettings.tsx
Normal file
121
frontend/src/components/tools/rotate/RotateSettings.tsx
Normal file
@ -0,0 +1,121 @@
|
||||
import React, { useMemo } from "react";
|
||||
import { Stack, Text, Box, ActionIcon, Group, Center } from "@mantine/core";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import RotateLeftIcon from "@mui/icons-material/RotateLeft";
|
||||
import RotateRightIcon from "@mui/icons-material/RotateRight";
|
||||
import { RotateParametersHook } from "../../../hooks/tools/rotate/useRotateParameters";
|
||||
import { useSelectedFiles } from "../../../contexts/file/fileHooks";
|
||||
import { useThumbnailGeneration } from "../../../hooks/useThumbnailGeneration";
|
||||
import DocumentThumbnail from "../../shared/filePreview/DocumentThumbnail";
|
||||
|
||||
interface RotateSettingsProps {
|
||||
parameters: RotateParametersHook;
|
||||
disabled?: boolean;
|
||||
}
|
||||
|
||||
const RotateSettings = ({ parameters, disabled = false }: RotateSettingsProps) => {
|
||||
const { t } = useTranslation();
|
||||
const { selectedFiles } = useSelectedFiles();
|
||||
const { getThumbnailFromCache, requestThumbnail } = useThumbnailGeneration();
|
||||
|
||||
// Get the first selected file for preview
|
||||
const selectedFile = useMemo(() => {
|
||||
return selectedFiles.length > 0 ? selectedFiles[0] : null;
|
||||
}, [selectedFiles]);
|
||||
|
||||
// Get thumbnail for the selected file
|
||||
const thumbnail = useMemo(() => {
|
||||
if (!selectedFile) return null;
|
||||
|
||||
const pageId = `${selectedFile.fileId}-1`;
|
||||
|
||||
// Try to get cached thumbnail first
|
||||
const cached = getThumbnailFromCache(pageId);
|
||||
if (cached) return cached;
|
||||
|
||||
// Request thumbnail if not cached
|
||||
requestThumbnail(pageId, selectedFile, 1).then(() => {
|
||||
// Component will re-render when thumbnail is available
|
||||
});
|
||||
|
||||
return null;
|
||||
}, [selectedFile, getThumbnailFromCache, requestThumbnail]);
|
||||
|
||||
// Calculate current angle display
|
||||
const currentAngle = parameters.parameters.angle;
|
||||
|
||||
return (
|
||||
<Stack gap="md">
|
||||
{/* Thumbnail Preview Section */}
|
||||
<Stack gap="xs">
|
||||
<Text size="sm" fw={500}>
|
||||
{t("rotate.preview.title", "Rotation Preview")}
|
||||
</Text>
|
||||
|
||||
<Center>
|
||||
<Box
|
||||
style={{
|
||||
width: '200px',
|
||||
height: '280px',
|
||||
border: '2px dashed var(--mantine-color-gray-4)',
|
||||
borderRadius: '8px',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
backgroundColor: 'var(--mantine-color-gray-0)',
|
||||
overflow: 'hidden'
|
||||
}}
|
||||
>
|
||||
<Box
|
||||
style={{
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
transform: `rotate(${currentAngle}deg)`,
|
||||
transition: 'transform 0.3s ease-in-out',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center'
|
||||
}}
|
||||
>
|
||||
<DocumentThumbnail
|
||||
file={selectedFile}
|
||||
thumbnail={thumbnail}
|
||||
style={{
|
||||
maxWidth: '180px',
|
||||
maxHeight: '260px'
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
</Box>
|
||||
</Center>
|
||||
</Stack>
|
||||
|
||||
{/* Rotation Controls */}
|
||||
<Group justify="center" gap="lg">
|
||||
<ActionIcon
|
||||
size="xl"
|
||||
variant="outline"
|
||||
onClick={parameters.rotateAnticlockwise}
|
||||
disabled={disabled}
|
||||
aria-label={t("rotate.rotateLeft", "Rotate Anticlockwise")}
|
||||
title={t("rotate.rotateLeft", "Rotate Anticlockwise")}
|
||||
>
|
||||
<RotateLeftIcon style={{ fontSize: '1.5rem' }} />
|
||||
</ActionIcon>
|
||||
|
||||
<ActionIcon
|
||||
size="xl"
|
||||
variant="outline"
|
||||
onClick={parameters.rotateClockwise}
|
||||
disabled={disabled}
|
||||
aria-label={t("rotate.rotateRight", "Rotate Clockwise")}
|
||||
title={t("rotate.rotateRight", "Rotate Clockwise")}
|
||||
>
|
||||
<RotateRightIcon style={{ fontSize: '1.5rem' }} />
|
||||
</ActionIcon>
|
||||
</Group>
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
|
||||
export default RotateSettings;
|
22
frontend/src/components/tooltips/useRotateTips.ts
Normal file
22
frontend/src/components/tooltips/useRotateTips.ts
Normal file
@ -0,0 +1,22 @@
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { TooltipContent } from '../../types/tips';
|
||||
|
||||
export const useRotateTips = (): TooltipContent => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return {
|
||||
header: {
|
||||
title: t("rotate.tooltip.header.title", "Rotate Settings Overview")
|
||||
},
|
||||
tips: [
|
||||
{
|
||||
title: t("rotate.tooltip.description.title", "Description"),
|
||||
description: t("rotate.tooltip.description.text", "Rotate your PDF pages clockwise or anticlockwise in 90-degree increments. All pages in the PDF will be rotated. The preview shows how your document will look after rotation.")
|
||||
},
|
||||
{
|
||||
title: t("rotate.tooltip.controls.title", "Controls"),
|
||||
description: t("rotate.tooltip.controls.text", "Use the rotation buttons to adjust orientation. Left button rotates anticlockwise, right button rotates clockwise. Each click rotates by 90 degrees.")
|
||||
}
|
||||
]
|
||||
};
|
||||
};
|
@ -18,6 +18,7 @@ import SingleLargePage from "../tools/SingleLargePage";
|
||||
import UnlockPdfForms from "../tools/UnlockPdfForms";
|
||||
import RemoveCertificateSign from "../tools/RemoveCertificateSign";
|
||||
import Flatten from "../tools/Flatten";
|
||||
import Rotate from "../tools/Rotate";
|
||||
import { compressOperationConfig } from "../hooks/tools/compress/useCompressOperation";
|
||||
import { splitOperationConfig } from "../hooks/tools/split/useSplitOperation";
|
||||
import { addPasswordOperationConfig } from "../hooks/tools/addPassword/useAddPasswordOperation";
|
||||
@ -35,6 +36,7 @@ import { mergeOperationConfig } from '../hooks/tools/merge/useMergeOperation';
|
||||
import { autoRenameOperationConfig } from "../hooks/tools/autoRename/useAutoRenameOperation";
|
||||
import { flattenOperationConfig } from "../hooks/tools/flatten/useFlattenOperation";
|
||||
import { redactOperationConfig } from "../hooks/tools/redact/useRedactOperation";
|
||||
import { rotateOperationConfig } from "../hooks/tools/rotate/useRotateOperation";
|
||||
import CompressSettings from "../components/tools/compress/CompressSettings";
|
||||
import SplitSettings from "../components/tools/split/SplitSettings";
|
||||
import AddPasswordSettings from "../components/tools/addPassword/AddPasswordSettings";
|
||||
@ -48,6 +50,7 @@ import ConvertSettings from "../components/tools/convert/ConvertSettings";
|
||||
import ChangePermissionsSettings from "../components/tools/changePermissions/ChangePermissionsSettings";
|
||||
import FlattenSettings from "../components/tools/flatten/FlattenSettings";
|
||||
import RedactSingleStepSettings from "../components/tools/redact/RedactSingleStepSettings";
|
||||
import RotateSettings from "../components/tools/rotate/RotateSettings";
|
||||
import Redact from "../tools/Redact";
|
||||
import AdjustPageScale from "../tools/AdjustPageScale";
|
||||
import { ToolId } from "../types/toolId";
|
||||
@ -310,10 +313,14 @@ export function useFlatToolRegistry(): ToolRegistry {
|
||||
rotate: {
|
||||
icon: <LocalIcon icon="rotate-right-rounded" width="1.5rem" height="1.5rem" />,
|
||||
name: t("home.rotate.title", "Rotate"),
|
||||
component: null,
|
||||
component: Rotate,
|
||||
description: t("home.rotate.desc", "Easily rotate your PDFs."),
|
||||
categoryId: ToolCategoryId.STANDARD_TOOLS,
|
||||
subcategoryId: SubcategoryId.PAGE_FORMATTING,
|
||||
maxFiles: -1,
|
||||
endpoints: ["rotate-pdf"],
|
||||
operationConfig: rotateOperationConfig,
|
||||
settingsComponent: RotateSettings,
|
||||
},
|
||||
split: {
|
||||
icon: <LocalIcon icon="content-cut-rounded" width="1.5rem" height="1.5rem" />,
|
||||
|
98
frontend/src/hooks/tools/rotate/useRotateOperation.test.ts
Normal file
98
frontend/src/hooks/tools/rotate/useRotateOperation.test.ts
Normal file
@ -0,0 +1,98 @@
|
||||
import { describe, expect, test, vi, beforeEach } from 'vitest';
|
||||
import { renderHook } from '@testing-library/react';
|
||||
import { useRotateOperation } from './useRotateOperation';
|
||||
import type { RotateParameters } from './useRotateParameters';
|
||||
|
||||
// Mock the useToolOperation hook
|
||||
vi.mock('../shared/useToolOperation', async () => {
|
||||
const actual = await vi.importActual('../shared/useToolOperation');
|
||||
return {
|
||||
...actual,
|
||||
useToolOperation: vi.fn()
|
||||
};
|
||||
});
|
||||
|
||||
// Mock the translation hook
|
||||
const mockT = vi.fn((key: string) => `translated-${key}`);
|
||||
vi.mock('react-i18next', () => ({
|
||||
useTranslation: () => ({ t: mockT })
|
||||
}));
|
||||
|
||||
// Mock the error handler
|
||||
vi.mock('../../../utils/toolErrorHandler', () => ({
|
||||
createStandardErrorHandler: vi.fn(() => 'error-handler-function')
|
||||
}));
|
||||
|
||||
// Import the mocked function
|
||||
import { SingleFileToolOperationConfig, ToolOperationHook, ToolType, useToolOperation } from '../shared/useToolOperation';
|
||||
|
||||
describe('useRotateOperation', () => {
|
||||
const mockUseToolOperation = vi.mocked(useToolOperation);
|
||||
|
||||
const getToolConfig = () => mockUseToolOperation.mock.calls[0][0] as SingleFileToolOperationConfig<RotateParameters>;
|
||||
|
||||
const mockToolOperationReturn: ToolOperationHook<unknown> = {
|
||||
files: [],
|
||||
thumbnails: [],
|
||||
downloadUrl: null,
|
||||
downloadFilename: '',
|
||||
isLoading: false,
|
||||
errorMessage: null,
|
||||
status: '',
|
||||
isGeneratingThumbnails: false,
|
||||
progress: null,
|
||||
executeOperation: vi.fn(),
|
||||
resetResults: vi.fn(),
|
||||
clearError: vi.fn(),
|
||||
cancelOperation: vi.fn(),
|
||||
undoOperation: vi.fn(),
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
mockUseToolOperation.mockReturnValue(mockToolOperationReturn);
|
||||
});
|
||||
|
||||
test.each([
|
||||
{ angle: 0 },
|
||||
{ angle: 90 },
|
||||
{ angle: 180 },
|
||||
{ angle: 270 },
|
||||
])('should create form data correctly with angle $angle', ({ angle }) => {
|
||||
renderHook(() => useRotateOperation());
|
||||
|
||||
const callArgs = getToolConfig();
|
||||
const buildFormData = callArgs.buildFormData;
|
||||
|
||||
const testParameters: RotateParameters = { angle };
|
||||
const testFile = new File(['test content'], 'test.pdf', { type: 'application/pdf' });
|
||||
const formData = buildFormData(testParameters, testFile);
|
||||
|
||||
// Verify the form data contains the file
|
||||
expect(formData.get('fileInput')).toBe(testFile);
|
||||
|
||||
// Verify angle parameter
|
||||
expect(formData.get('angle')).toBe(angle.toString());
|
||||
});
|
||||
|
||||
test('should use correct translation for error messages', () => {
|
||||
renderHook(() => useRotateOperation());
|
||||
|
||||
expect(mockT).toHaveBeenCalledWith(
|
||||
'rotate.error.failed',
|
||||
'An error occurred while rotating the PDF.'
|
||||
);
|
||||
});
|
||||
|
||||
test.each([
|
||||
{ property: 'toolType' as const, expectedValue: ToolType.singleFile },
|
||||
{ property: 'endpoint' as const, expectedValue: '/api/v1/general/rotate-pdf' },
|
||||
{ property: 'filePrefix' as const, expectedValue: 'rotated_' },
|
||||
{ property: 'operationType' as const, expectedValue: 'rotate' }
|
||||
])('should configure $property correctly', ({ property, expectedValue }) => {
|
||||
renderHook(() => useRotateOperation());
|
||||
|
||||
const callArgs = getToolConfig();
|
||||
expect(callArgs[property]).toBe(expectedValue);
|
||||
});
|
||||
});
|
31
frontend/src/hooks/tools/rotate/useRotateOperation.ts
Normal file
31
frontend/src/hooks/tools/rotate/useRotateOperation.ts
Normal file
@ -0,0 +1,31 @@
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useToolOperation, ToolType } from '../shared/useToolOperation';
|
||||
import { createStandardErrorHandler } from '../../../utils/toolErrorHandler';
|
||||
import { RotateParameters, defaultParameters } from './useRotateParameters';
|
||||
|
||||
// Static configuration that can be used by both the hook and automation executor
|
||||
export const buildRotateFormData = (parameters: RotateParameters, file: File): FormData => {
|
||||
const formData = new FormData();
|
||||
formData.append("fileInput", file);
|
||||
formData.append("angle", parameters.angle.toString());
|
||||
return formData;
|
||||
};
|
||||
|
||||
// Static configuration object
|
||||
export const rotateOperationConfig = {
|
||||
toolType: ToolType.singleFile,
|
||||
buildFormData: buildRotateFormData,
|
||||
operationType: 'rotate',
|
||||
endpoint: '/api/v1/general/rotate-pdf',
|
||||
filePrefix: 'rotated_',
|
||||
defaultParameters,
|
||||
} as const;
|
||||
|
||||
export const useRotateOperation = () => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return useToolOperation<RotateParameters>({
|
||||
...rotateOperationConfig,
|
||||
getErrorMessage: createStandardErrorHandler(t('rotate.error.failed', 'An error occurred while rotating the PDF.'))
|
||||
});
|
||||
};
|
156
frontend/src/hooks/tools/rotate/useRotateParameters.test.ts
Normal file
156
frontend/src/hooks/tools/rotate/useRotateParameters.test.ts
Normal file
@ -0,0 +1,156 @@
|
||||
import { describe, expect, test } from 'vitest';
|
||||
import { renderHook, act } from '@testing-library/react';
|
||||
import { useRotateParameters, defaultParameters } from './useRotateParameters';
|
||||
|
||||
describe('useRotateParameters', () => {
|
||||
test('should initialize with default parameters', () => {
|
||||
const { result } = renderHook(() => useRotateParameters());
|
||||
|
||||
expect(result.current.parameters).toEqual(defaultParameters);
|
||||
expect(result.current.parameters.angle).toBe(0);
|
||||
expect(result.current.hasRotation).toBe(false);
|
||||
});
|
||||
|
||||
test('should validate parameters correctly', () => {
|
||||
const { result } = renderHook(() => useRotateParameters());
|
||||
|
||||
// Default should be valid
|
||||
expect(result.current.validateParameters()).toBe(true);
|
||||
|
||||
// Set invalid angle
|
||||
act(() => {
|
||||
result.current.updateParameter('angle', 45);
|
||||
});
|
||||
expect(result.current.validateParameters()).toBe(false);
|
||||
|
||||
// Set valid angle
|
||||
act(() => {
|
||||
result.current.updateParameter('angle', 90);
|
||||
});
|
||||
expect(result.current.validateParameters()).toBe(true);
|
||||
});
|
||||
|
||||
test('should rotate clockwise correctly', () => {
|
||||
const { result } = renderHook(() => useRotateParameters());
|
||||
|
||||
act(() => {
|
||||
result.current.rotateClockwise();
|
||||
});
|
||||
expect(result.current.parameters.angle).toBe(90);
|
||||
expect(result.current.hasRotation).toBe(true);
|
||||
|
||||
act(() => {
|
||||
result.current.rotateClockwise();
|
||||
});
|
||||
expect(result.current.parameters.angle).toBe(180);
|
||||
|
||||
act(() => {
|
||||
result.current.rotateClockwise();
|
||||
});
|
||||
expect(result.current.parameters.angle).toBe(270);
|
||||
|
||||
act(() => {
|
||||
result.current.rotateClockwise();
|
||||
});
|
||||
expect(result.current.parameters.angle).toBe(0); // Should wrap around
|
||||
expect(result.current.hasRotation).toBe(false);
|
||||
});
|
||||
|
||||
test('should rotate anticlockwise correctly', () => {
|
||||
const { result } = renderHook(() => useRotateParameters());
|
||||
|
||||
act(() => {
|
||||
result.current.rotateAnticlockwise();
|
||||
});
|
||||
expect(result.current.parameters.angle).toBe(270);
|
||||
expect(result.current.hasRotation).toBe(true);
|
||||
|
||||
act(() => {
|
||||
result.current.rotateAnticlockwise();
|
||||
});
|
||||
expect(result.current.parameters.angle).toBe(180);
|
||||
|
||||
act(() => {
|
||||
result.current.rotateAnticlockwise();
|
||||
});
|
||||
expect(result.current.parameters.angle).toBe(90);
|
||||
|
||||
act(() => {
|
||||
result.current.rotateAnticlockwise();
|
||||
});
|
||||
expect(result.current.parameters.angle).toBe(0); // Should wrap around
|
||||
expect(result.current.hasRotation).toBe(false);
|
||||
});
|
||||
|
||||
test('should normalize angles correctly', () => {
|
||||
const { result } = renderHook(() => useRotateParameters());
|
||||
|
||||
expect(result.current.normalizeAngle(360)).toBe(0);
|
||||
expect(result.current.normalizeAngle(450)).toBe(90);
|
||||
expect(result.current.normalizeAngle(-90)).toBe(270);
|
||||
expect(result.current.normalizeAngle(-180)).toBe(180);
|
||||
});
|
||||
|
||||
test('should reset parameters correctly', () => {
|
||||
const { result } = renderHook(() => useRotateParameters());
|
||||
|
||||
// Set some rotation
|
||||
act(() => {
|
||||
result.current.rotateClockwise();
|
||||
});
|
||||
expect(result.current.parameters.angle).toBe(90);
|
||||
|
||||
act(() => {
|
||||
result.current.rotateClockwise();
|
||||
});
|
||||
expect(result.current.parameters.angle).toBe(180);
|
||||
|
||||
// Reset
|
||||
act(() => {
|
||||
result.current.resetParameters();
|
||||
});
|
||||
expect(result.current.parameters).toEqual(defaultParameters);
|
||||
expect(result.current.hasRotation).toBe(false);
|
||||
});
|
||||
|
||||
test('should update parameters and normalize angles', () => {
|
||||
const { result } = renderHook(() => useRotateParameters());
|
||||
|
||||
act(() => {
|
||||
result.current.updateParameter('angle', 450);
|
||||
});
|
||||
expect(result.current.parameters.angle).toBe(90); // Should be normalized
|
||||
|
||||
act(() => {
|
||||
result.current.updateParameter('angle', -90);
|
||||
});
|
||||
expect(result.current.parameters.angle).toBe(270); // Should be normalized
|
||||
});
|
||||
|
||||
test('should return correct endpoint name', () => {
|
||||
const { result } = renderHook(() => useRotateParameters());
|
||||
|
||||
expect(result.current.getEndpointName()).toBe('rotate-pdf');
|
||||
});
|
||||
|
||||
test('should detect rotation state correctly', () => {
|
||||
const { result } = renderHook(() => useRotateParameters());
|
||||
|
||||
// Initially no rotation
|
||||
expect(result.current.hasRotation).toBe(false);
|
||||
|
||||
// After rotation
|
||||
act(() => {
|
||||
result.current.rotateClockwise();
|
||||
});
|
||||
expect(result.current.hasRotation).toBe(true);
|
||||
|
||||
// After full rotation (360 degrees) - 3 more clicks to complete 360°
|
||||
for (let i = 0; i < 3; i++) {
|
||||
act(() => {
|
||||
result.current.rotateClockwise();
|
||||
});
|
||||
}
|
||||
expect(result.current.hasRotation).toBe(false);
|
||||
});
|
||||
});
|
71
frontend/src/hooks/tools/rotate/useRotateParameters.ts
Normal file
71
frontend/src/hooks/tools/rotate/useRotateParameters.ts
Normal file
@ -0,0 +1,71 @@
|
||||
import { BaseParameters } from '../../../types/parameters';
|
||||
import { useBaseParameters, BaseParametersHook } from '../shared/useBaseParameters';
|
||||
import { useMemo, useCallback } from 'react';
|
||||
|
||||
export interface RotateParameters extends BaseParameters {
|
||||
angle: number; // Current rotation angle (0, 90, 180, 270)
|
||||
}
|
||||
|
||||
export const defaultParameters: RotateParameters = {
|
||||
angle: 0,
|
||||
};
|
||||
|
||||
export type RotateParametersHook = BaseParametersHook<RotateParameters> & {
|
||||
rotateClockwise: () => void;
|
||||
rotateAnticlockwise: () => void;
|
||||
hasRotation: boolean;
|
||||
normalizeAngle: (angle: number) => number;
|
||||
};
|
||||
|
||||
export const useRotateParameters = (): RotateParametersHook => {
|
||||
const baseHook = useBaseParameters({
|
||||
defaultParameters,
|
||||
endpointName: 'rotate-pdf',
|
||||
validateFn: (params) => {
|
||||
// Angle must be a multiple of 90
|
||||
return params.angle % 90 === 0;
|
||||
},
|
||||
});
|
||||
|
||||
// Normalize angle to valid backend values (0, 90, 180, 270)
|
||||
const normalizeAngle = useCallback((angle: number): number => {
|
||||
const normalized = angle % 360;
|
||||
return normalized < 0 ? normalized + 360 : normalized;
|
||||
}, []);
|
||||
|
||||
// Rotate clockwise by 90 degrees
|
||||
const rotateClockwise = useCallback(() => {
|
||||
baseHook.updateParameter('angle', normalizeAngle(baseHook.parameters.angle + 90));
|
||||
}, [baseHook, normalizeAngle]);
|
||||
|
||||
// Rotate anticlockwise by 90 degrees
|
||||
const rotateAnticlockwise = useCallback(() => {
|
||||
baseHook.updateParameter('angle', normalizeAngle(baseHook.parameters.angle - 90));
|
||||
}, [baseHook, normalizeAngle]);
|
||||
|
||||
// Check if rotation will actually change the document
|
||||
const hasRotation = useMemo(() => {
|
||||
return baseHook.parameters.angle !== 0;
|
||||
}, [baseHook.parameters.angle]);
|
||||
|
||||
// Override updateParameter to normalize angles
|
||||
const updateParameter = useCallback(<K extends keyof RotateParameters>(
|
||||
parameter: K,
|
||||
value: RotateParameters[K]
|
||||
) => {
|
||||
if (parameter === 'angle') {
|
||||
baseHook.updateParameter(parameter, normalizeAngle(value as number) as RotateParameters[K]);
|
||||
} else {
|
||||
baseHook.updateParameter(parameter, value);
|
||||
}
|
||||
}, [baseHook, normalizeAngle]);
|
||||
|
||||
return {
|
||||
...baseHook,
|
||||
updateParameter,
|
||||
rotateClockwise,
|
||||
rotateAnticlockwise,
|
||||
hasRotation,
|
||||
normalizeAngle,
|
||||
};
|
||||
};
|
57
frontend/src/tools/Rotate.tsx
Normal file
57
frontend/src/tools/Rotate.tsx
Normal file
@ -0,0 +1,57 @@
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { createToolFlow } from "../components/tools/shared/createToolFlow";
|
||||
import RotateSettings from "../components/tools/rotate/RotateSettings";
|
||||
import { useRotateParameters } from "../hooks/tools/rotate/useRotateParameters";
|
||||
import { useRotateOperation } from "../hooks/tools/rotate/useRotateOperation";
|
||||
import { useBaseTool } from "../hooks/tools/shared/useBaseTool";
|
||||
import { BaseToolProps, ToolComponent } from "../types/tool";
|
||||
import { useRotateTips } from "../components/tooltips/useRotateTips";
|
||||
|
||||
const Rotate = (props: BaseToolProps) => {
|
||||
const { t } = useTranslation();
|
||||
const rotateTips = useRotateTips();
|
||||
|
||||
const base = useBaseTool(
|
||||
'rotate',
|
||||
useRotateParameters,
|
||||
useRotateOperation,
|
||||
props
|
||||
);
|
||||
|
||||
return createToolFlow({
|
||||
files: {
|
||||
selectedFiles: base.selectedFiles,
|
||||
isCollapsed: base.hasResults,
|
||||
},
|
||||
steps: [
|
||||
{
|
||||
title: "Settings",
|
||||
isCollapsed: base.settingsCollapsed,
|
||||
onCollapsedClick: base.settingsCollapsed ? base.handleSettingsReset : undefined,
|
||||
tooltip: rotateTips,
|
||||
content: (
|
||||
<RotateSettings
|
||||
parameters={base.params}
|
||||
disabled={base.endpointLoading}
|
||||
/>
|
||||
),
|
||||
},
|
||||
],
|
||||
executeButton: {
|
||||
text: t("rotate.submit", "Rotate"),
|
||||
isVisible: !base.hasResults,
|
||||
loadingText: t("loading"),
|
||||
onClick: base.handleExecute,
|
||||
disabled: !base.params.validateParameters() || !base.hasFiles || !base.endpointEnabled,
|
||||
},
|
||||
review: {
|
||||
isVisible: base.hasResults,
|
||||
operation: base.operation,
|
||||
title: t("rotate.title", "Rotation Results"),
|
||||
onFileClick: base.handleThumbnailClick,
|
||||
onUndo: base.handleUndo,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
export default Rotate as ToolComponent;
|
Loading…
x
Reference in New Issue
Block a user