mirror of
https://github.com/arsvendg/Stirling-PDF.git
synced 2026-07-16 03:20:46 +02:00
Save signatures to server (#5080)
# Description of Changes <!-- Please provide a summary of the changes, including: - What was changed - Why the change was made - Any challenges encountered Closes #(issue_number) --> --- ## Checklist ### General - [ ] I have read the [Contribution Guidelines](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/CONTRIBUTING.md) - [ ] I have read the [Stirling-PDF Developer Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md) (if applicable) - [ ] I have read the [How to add new languages to Stirling-PDF](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md) (if applicable) - [ ] I have performed a self-review of my own code - [ ] My changes generate no new warnings ### Documentation - [ ] I have updated relevant docs on [Stirling-PDF's doc repo](https://github.com/Stirling-Tools/Stirling-Tools.github.io/blob/main/docs/) (if functionality has heavily changed) - [ ] I have read the section [Add New Translation Tags](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md#add-new-translation-tags) (for new translation tags only) ### Translations (if applicable) - [ ] I ran [`scripts/counter_translation.py`](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/docs/counter_translation.md) ### UI Changes (if applicable) - [ ] Screenshots or videos demonstrating the UI changes are attached (e.g., as comments or direct attachments in the PR) ### Testing (if applicable) - [ ] I have tested my changes locally. Refer to the [Testing Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md#6-testing) for more details. --------- Co-authored-by: Anthony Stirling <[email protected]>
This commit is contained in:
co-authored by
Anthony Stirling
parent
fde449e738
commit
651f17f1c6
@@ -1,13 +1,15 @@
|
||||
import { useCallback, useEffect, useRef, useState } from 'react';
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { ActionIcon, Alert, Badge, Box, Card, Group, Stack, Text, TextInput, Tooltip } from '@mantine/core';
|
||||
import { LocalIcon } from '@app/components/shared/LocalIcon';
|
||||
import { MAX_SAVED_SIGNATURES, SavedSignature, SavedSignatureType } from '@app/hooks/tools/sign/useSavedSignatures';
|
||||
import type { StorageType } from '@app/services/signatureStorageService';
|
||||
|
||||
interface SavedSignaturesSectionProps {
|
||||
signatures: SavedSignature[];
|
||||
disabled?: boolean;
|
||||
isAtCapacity: boolean;
|
||||
storageType?: StorageType | null;
|
||||
onUseSignature: (signature: SavedSignature) => void;
|
||||
onDeleteSignature: (signature: SavedSignature) => void;
|
||||
onRenameSignature: (id: string, label: string) => void;
|
||||
@@ -24,6 +26,7 @@ export const SavedSignaturesSection = ({
|
||||
signatures,
|
||||
disabled = false,
|
||||
isAtCapacity,
|
||||
storageType: _storageType,
|
||||
onUseSignature,
|
||||
onDeleteSignature,
|
||||
onRenameSignature,
|
||||
@@ -36,20 +39,30 @@ export const SavedSignaturesSection = ({
|
||||
[t, translationScope]
|
||||
);
|
||||
const [labelDrafts, setLabelDrafts] = useState<Record<string, string>>({});
|
||||
const [activeIndex, setActiveIndex] = useState(0);
|
||||
const activeSignature = signatures[activeIndex];
|
||||
const activeSignatureRef = useRef<SavedSignature | null>(activeSignature ?? null);
|
||||
const appliedSignatureIdRef = useRef<string | null>(null);
|
||||
|
||||
// Group signatures by scope
|
||||
const groupedSignatures = useMemo(() => {
|
||||
const personal = signatures.filter(sig => sig.scope === 'personal');
|
||||
const shared = signatures.filter(sig => sig.scope === 'shared');
|
||||
const localStorage = signatures.filter(sig => sig.scope === 'localStorage');
|
||||
return { personal, shared, localStorage };
|
||||
}, [signatures]);
|
||||
|
||||
// Separate carousel state for each category
|
||||
const [activePersonalIndex, setActivePersonalIndex] = useState(0);
|
||||
const [activeSharedIndex, setActiveSharedIndex] = useState(0);
|
||||
const [activeLocalStorageIndex, setActiveLocalStorageIndex] = useState(0);
|
||||
|
||||
const activePersonalSignature = groupedSignatures.personal[activePersonalIndex];
|
||||
const activeSharedSignature = groupedSignatures.shared[activeSharedIndex];
|
||||
const activeLocalStorageSignature = groupedSignatures.localStorage[activeLocalStorageIndex];
|
||||
|
||||
const onUseSignatureRef = useRef(onUseSignature);
|
||||
|
||||
useEffect(() => {
|
||||
onUseSignatureRef.current = onUseSignature;
|
||||
}, [onUseSignature]);
|
||||
|
||||
useEffect(() => {
|
||||
activeSignatureRef.current = activeSignature ?? null;
|
||||
}, [activeSignature]);
|
||||
|
||||
useEffect(() => {
|
||||
setLabelDrafts(prev => {
|
||||
const nextDrafts: Record<string, string> = {};
|
||||
@@ -60,25 +73,18 @@ export const SavedSignaturesSection = ({
|
||||
});
|
||||
}, [signatures]);
|
||||
|
||||
// Reset carousel indices when categories change
|
||||
useEffect(() => {
|
||||
if (signatures.length === 0) {
|
||||
setActiveIndex(0);
|
||||
return;
|
||||
}
|
||||
setActiveIndex(prev => Math.min(prev, Math.max(signatures.length - 1, 0)));
|
||||
}, [signatures.length]);
|
||||
setActivePersonalIndex(prev => Math.min(prev, Math.max(groupedSignatures.personal.length - 1, 0)));
|
||||
}, [groupedSignatures.personal.length]);
|
||||
|
||||
const handleNavigate = useCallback(
|
||||
(direction: 'prev' | 'next') => {
|
||||
setActiveIndex(prev => {
|
||||
if (direction === 'prev') {
|
||||
return Math.max(0, prev - 1);
|
||||
}
|
||||
return Math.min(signatures.length - 1, prev + 1);
|
||||
});
|
||||
},
|
||||
[signatures.length]
|
||||
);
|
||||
useEffect(() => {
|
||||
setActiveSharedIndex(prev => Math.min(prev, Math.max(groupedSignatures.shared.length - 1, 0)));
|
||||
}, [groupedSignatures.shared.length]);
|
||||
|
||||
useEffect(() => {
|
||||
setActiveLocalStorageIndex(prev => Math.min(prev, Math.max(groupedSignatures.localStorage.length - 1, 0)));
|
||||
}, [groupedSignatures.localStorage.length]);
|
||||
|
||||
const renderPreview = (signature: SavedSignature) => {
|
||||
if (signature.type === 'text') {
|
||||
@@ -193,21 +199,6 @@ export const SavedSignaturesSection = ({
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
const signature = activeSignatureRef.current;
|
||||
if (!signature || disabled) {
|
||||
appliedSignatureIdRef.current = null;
|
||||
return;
|
||||
}
|
||||
|
||||
if (appliedSignatureIdRef.current === signature.id) {
|
||||
return;
|
||||
}
|
||||
|
||||
appliedSignatureIdRef.current = signature.id;
|
||||
onUseSignatureRef.current(signature);
|
||||
}, [activeSignature?.id, disabled]);
|
||||
|
||||
return (
|
||||
<Stack gap="sm">
|
||||
<Group justify="space-between" align="flex-start">
|
||||
@@ -234,67 +225,253 @@ export const SavedSignaturesSection = ({
|
||||
{signatures.length === 0 ? (
|
||||
emptyState
|
||||
) : (
|
||||
<Stack gap="xs">
|
||||
<Group justify="space-between" align="center">
|
||||
<Text size="sm" c="dimmed">
|
||||
{translate('saved.carouselPosition', '{{current}} of {{total}}', {
|
||||
current: activeIndex + 1,
|
||||
total: signatures.length,
|
||||
})}
|
||||
</Text>
|
||||
<Group gap={4}>
|
||||
<ActionIcon
|
||||
variant="light"
|
||||
aria-label={translate('saved.prev', 'Previous')}
|
||||
onClick={() => handleNavigate('prev')}
|
||||
disabled={disabled || activeIndex === 0}
|
||||
>
|
||||
<LocalIcon icon="material-symbols:chevron-left-rounded" width={18} height={18} />
|
||||
</ActionIcon>
|
||||
<ActionIcon
|
||||
variant="light"
|
||||
aria-label={translate('saved.next', 'Next')}
|
||||
onClick={() => handleNavigate('next')}
|
||||
disabled={disabled || activeIndex >= signatures.length - 1}
|
||||
>
|
||||
<LocalIcon icon="material-symbols:chevron-right-rounded" width={18} height={18} />
|
||||
</ActionIcon>
|
||||
</Group>
|
||||
</Group>
|
||||
<Stack gap="md">
|
||||
{/* Personal Signatures */}
|
||||
{groupedSignatures.personal.length > 0 && activePersonalSignature && (
|
||||
<Stack gap="xs">
|
||||
<Group gap="xs">
|
||||
<LocalIcon icon="material-symbols:person-rounded" width={18} height={18} />
|
||||
<Text fw={600} size="sm">
|
||||
{translate('saved.personalHeading', 'Personal Signatures')}
|
||||
</Text>
|
||||
</Group>
|
||||
<Text size="xs" c="dimmed">
|
||||
{translate('saved.personalDescription', 'Only you can see these signatures.')}
|
||||
</Text>
|
||||
|
||||
{activeSignature && (
|
||||
<Card withBorder padding="sm" key={activeSignature.id}>
|
||||
<Stack gap="sm">
|
||||
<Group justify="space-between" align="center">
|
||||
<Badge color={typeBadgeColor[activeSignature.type]} variant="light">
|
||||
{typeLabel(activeSignature.type)}
|
||||
</Badge>
|
||||
<Tooltip label={translate('saved.delete', 'Remove')}>
|
||||
<ActionIcon
|
||||
variant="subtle"
|
||||
color="red"
|
||||
aria-label={translate('saved.delete', 'Remove')}
|
||||
onClick={() => onDeleteSignature(activeSignature)}
|
||||
disabled={disabled}
|
||||
>
|
||||
<LocalIcon icon="material-symbols:delete-outline-rounded" width={18} height={18} />
|
||||
</ActionIcon>
|
||||
</Tooltip>
|
||||
<Group justify="space-between" align="center">
|
||||
<Text size="sm" c="dimmed">
|
||||
{translate('saved.carouselPosition', '{{current}} of {{total}}', {
|
||||
current: activePersonalIndex + 1,
|
||||
total: groupedSignatures.personal.length,
|
||||
})}
|
||||
</Text>
|
||||
<Group gap={4}>
|
||||
<ActionIcon
|
||||
variant="light"
|
||||
aria-label={translate('saved.prev', 'Previous')}
|
||||
onClick={() => setActivePersonalIndex(prev => Math.max(0, prev - 1))}
|
||||
disabled={disabled || activePersonalIndex === 0}
|
||||
>
|
||||
<LocalIcon icon="material-symbols:chevron-left-rounded" width={18} height={18} />
|
||||
</ActionIcon>
|
||||
<ActionIcon
|
||||
variant="light"
|
||||
aria-label={translate('saved.next', 'Next')}
|
||||
onClick={() => setActivePersonalIndex(prev => Math.min(groupedSignatures.personal.length - 1, prev + 1))}
|
||||
disabled={disabled || activePersonalIndex >= groupedSignatures.personal.length - 1}
|
||||
>
|
||||
<LocalIcon icon="material-symbols:chevron-right-rounded" width={18} height={18} />
|
||||
</ActionIcon>
|
||||
</Group>
|
||||
</Group>
|
||||
|
||||
{renderPreview(activeSignature)}
|
||||
<Card withBorder padding="sm">
|
||||
<Stack gap="sm">
|
||||
<Group justify="space-between" align="center">
|
||||
<Badge color={typeBadgeColor[activePersonalSignature.type]} variant="light">
|
||||
{typeLabel(activePersonalSignature.type)}
|
||||
</Badge>
|
||||
<Group gap="xs">
|
||||
<ActionIcon
|
||||
variant="subtle"
|
||||
color="blue"
|
||||
aria-label="Use signature"
|
||||
onClick={() => onUseSignature(activePersonalSignature)}
|
||||
disabled={disabled}
|
||||
>
|
||||
<LocalIcon icon="material-symbols:check-circle-outline-rounded" width={18} height={18} />
|
||||
</ActionIcon>
|
||||
<Tooltip label={translate('saved.delete', 'Remove')}>
|
||||
<ActionIcon
|
||||
variant="subtle"
|
||||
color="red"
|
||||
aria-label={translate('saved.delete', 'Remove')}
|
||||
onClick={() => onDeleteSignature(activePersonalSignature)}
|
||||
disabled={disabled}
|
||||
>
|
||||
<LocalIcon icon="material-symbols:delete-outline-rounded" width={18} height={18} />
|
||||
</ActionIcon>
|
||||
</Tooltip>
|
||||
</Group>
|
||||
</Group>
|
||||
{renderPreview(activePersonalSignature)}
|
||||
<TextInput
|
||||
label={translate('saved.label', 'Label')}
|
||||
value={labelDrafts[activePersonalSignature.id] ?? activePersonalSignature.label}
|
||||
onChange={event => handleLabelChange(event, activePersonalSignature)}
|
||||
onBlur={() => handleLabelBlur(activePersonalSignature)}
|
||||
onKeyDown={event => handleLabelKeyDown(event, activePersonalSignature)}
|
||||
disabled={disabled}
|
||||
/>
|
||||
</Stack>
|
||||
</Card>
|
||||
</Stack>
|
||||
)}
|
||||
|
||||
<TextInput
|
||||
label={translate('saved.label', 'Label')}
|
||||
value={labelDrafts[activeSignature.id] ?? activeSignature.label}
|
||||
onChange={event => handleLabelChange(event, activeSignature)}
|
||||
onBlur={() => handleLabelBlur(activeSignature)}
|
||||
onKeyDown={event => handleLabelKeyDown(event, activeSignature)}
|
||||
disabled={disabled}
|
||||
/>
|
||||
{/* Shared Signatures */}
|
||||
{groupedSignatures.shared.length > 0 && activeSharedSignature && (
|
||||
<Stack gap="xs">
|
||||
<Group gap="xs">
|
||||
<LocalIcon icon="material-symbols:groups-rounded" width={18} height={18} />
|
||||
<Text fw={600} size="sm">
|
||||
{translate('saved.sharedHeading', 'Shared Signatures')}
|
||||
</Text>
|
||||
</Group>
|
||||
<Text size="xs" c="dimmed">
|
||||
{translate('saved.sharedDescription', 'All users can see and use these signatures.')}
|
||||
</Text>
|
||||
|
||||
</Stack>
|
||||
</Card>
|
||||
<Group justify="space-between" align="center">
|
||||
<Text size="sm" c="dimmed">
|
||||
{translate('saved.carouselPosition', '{{current}} of {{total}}', {
|
||||
current: activeSharedIndex + 1,
|
||||
total: groupedSignatures.shared.length,
|
||||
})}
|
||||
</Text>
|
||||
<Group gap={4}>
|
||||
<ActionIcon
|
||||
variant="light"
|
||||
aria-label={translate('saved.prev', 'Previous')}
|
||||
onClick={() => setActiveSharedIndex(prev => Math.max(0, prev - 1))}
|
||||
disabled={disabled || activeSharedIndex === 0}
|
||||
>
|
||||
<LocalIcon icon="material-symbols:chevron-left-rounded" width={18} height={18} />
|
||||
</ActionIcon>
|
||||
<ActionIcon
|
||||
variant="light"
|
||||
aria-label={translate('saved.next', 'Next')}
|
||||
onClick={() => setActiveSharedIndex(prev => Math.min(groupedSignatures.shared.length - 1, prev + 1))}
|
||||
disabled={disabled || activeSharedIndex >= groupedSignatures.shared.length - 1}
|
||||
>
|
||||
<LocalIcon icon="material-symbols:chevron-right-rounded" width={18} height={18} />
|
||||
</ActionIcon>
|
||||
</Group>
|
||||
</Group>
|
||||
|
||||
<Card withBorder padding="sm">
|
||||
<Stack gap="sm">
|
||||
<Group justify="space-between" align="center">
|
||||
<Badge color={typeBadgeColor[activeSharedSignature.type]} variant="light">
|
||||
{typeLabel(activeSharedSignature.type)}
|
||||
</Badge>
|
||||
<Group gap="xs">
|
||||
<ActionIcon
|
||||
variant="subtle"
|
||||
color="blue"
|
||||
aria-label="Use signature"
|
||||
onClick={() => onUseSignature(activeSharedSignature)}
|
||||
disabled={disabled}
|
||||
>
|
||||
<LocalIcon icon="material-symbols:check-circle-outline-rounded" width={18} height={18} />
|
||||
</ActionIcon>
|
||||
<Tooltip label={translate('saved.delete', 'Remove')}>
|
||||
<ActionIcon
|
||||
variant="subtle"
|
||||
color="red"
|
||||
aria-label={translate('saved.delete', 'Remove')}
|
||||
onClick={() => onDeleteSignature(activeSharedSignature)}
|
||||
disabled={disabled}
|
||||
>
|
||||
<LocalIcon icon="material-symbols:delete-outline-rounded" width={18} height={18} />
|
||||
</ActionIcon>
|
||||
</Tooltip>
|
||||
</Group>
|
||||
</Group>
|
||||
{renderPreview(activeSharedSignature)}
|
||||
<TextInput
|
||||
label={translate('saved.label', 'Label')}
|
||||
value={labelDrafts[activeSharedSignature.id] ?? activeSharedSignature.label}
|
||||
onChange={event => handleLabelChange(event, activeSharedSignature)}
|
||||
onBlur={() => handleLabelBlur(activeSharedSignature)}
|
||||
onKeyDown={event => handleLabelKeyDown(event, activeSharedSignature)}
|
||||
disabled={disabled}
|
||||
/>
|
||||
</Stack>
|
||||
</Card>
|
||||
</Stack>
|
||||
)}
|
||||
|
||||
{/* Browser Storage (localStorage) - Temporary */}
|
||||
{groupedSignatures.localStorage.length > 0 && activeLocalStorageSignature && (
|
||||
<Stack gap="xs">
|
||||
<Alert color="blue" title={translate('saved.tempStorageTitle', 'Temporary browser storage')}>
|
||||
<Text size="xs">
|
||||
{translate(
|
||||
'saved.tempStorageDescription',
|
||||
'Signatures are stored in your browser only. They will be lost if you clear browser data or switch browsers.'
|
||||
)}
|
||||
</Text>
|
||||
</Alert>
|
||||
|
||||
<Group justify="space-between" align="center">
|
||||
<Text size="sm" c="dimmed">
|
||||
{translate('saved.carouselPosition', '{{current}} of {{total}}', {
|
||||
current: activeLocalStorageIndex + 1,
|
||||
total: groupedSignatures.localStorage.length,
|
||||
})}
|
||||
</Text>
|
||||
<Group gap={4}>
|
||||
<ActionIcon
|
||||
variant="light"
|
||||
aria-label={translate('saved.prev', 'Previous')}
|
||||
onClick={() => setActiveLocalStorageIndex(prev => Math.max(0, prev - 1))}
|
||||
disabled={disabled || activeLocalStorageIndex === 0}
|
||||
>
|
||||
<LocalIcon icon="material-symbols:chevron-left-rounded" width={18} height={18} />
|
||||
</ActionIcon>
|
||||
<ActionIcon
|
||||
variant="light"
|
||||
aria-label={translate('saved.next', 'Next')}
|
||||
onClick={() => setActiveLocalStorageIndex(prev => Math.min(groupedSignatures.localStorage.length - 1, prev + 1))}
|
||||
disabled={disabled || activeLocalStorageIndex >= groupedSignatures.localStorage.length - 1}
|
||||
>
|
||||
<LocalIcon icon="material-symbols:chevron-right-rounded" width={18} height={18} />
|
||||
</ActionIcon>
|
||||
</Group>
|
||||
</Group>
|
||||
|
||||
<Card withBorder padding="sm">
|
||||
<Stack gap="sm">
|
||||
<Group justify="space-between" align="center">
|
||||
<Badge color={typeBadgeColor[activeLocalStorageSignature.type]} variant="light">
|
||||
{typeLabel(activeLocalStorageSignature.type)}
|
||||
</Badge>
|
||||
<Group gap="xs">
|
||||
<ActionIcon
|
||||
variant="subtle"
|
||||
color="blue"
|
||||
aria-label="Use signature"
|
||||
onClick={() => onUseSignature(activeLocalStorageSignature)}
|
||||
disabled={disabled}
|
||||
>
|
||||
<LocalIcon icon="material-symbols:check-circle-outline-rounded" width={18} height={18} />
|
||||
</ActionIcon>
|
||||
<Tooltip label={translate('saved.delete', 'Remove')}>
|
||||
<ActionIcon
|
||||
variant="subtle"
|
||||
color="red"
|
||||
aria-label={translate('saved.delete', 'Remove')}
|
||||
onClick={() => onDeleteSignature(activeLocalStorageSignature)}
|
||||
disabled={disabled}
|
||||
>
|
||||
<LocalIcon icon="material-symbols:delete-outline-rounded" width={18} height={18} />
|
||||
</ActionIcon>
|
||||
</Tooltip>
|
||||
</Group>
|
||||
</Group>
|
||||
{renderPreview(activeLocalStorageSignature)}
|
||||
<TextInput
|
||||
label={translate('saved.label', 'Label')}
|
||||
value={labelDrafts[activeLocalStorageSignature.id] ?? activeLocalStorageSignature.label}
|
||||
onChange={event => handleLabelChange(event, activeLocalStorageSignature)}
|
||||
onBlur={() => handleLabelBlur(activeLocalStorageSignature)}
|
||||
onKeyDown={event => handleLabelKeyDown(event, activeLocalStorageSignature)}
|
||||
disabled={disabled}
|
||||
/>
|
||||
</Stack>
|
||||
</Card>
|
||||
</Stack>
|
||||
)}
|
||||
</Stack>
|
||||
)}
|
||||
|
||||
@@ -16,6 +16,7 @@ import { ColorPicker } from "@app/components/annotation/shared/ColorPicker";
|
||||
import { LocalIcon } from "@app/components/shared/LocalIcon";
|
||||
import { useSavedSignatures, SavedSignature, SavedSignaturePayload, SavedSignatureType, MAX_SAVED_SIGNATURES, AddSignatureResult } from '@app/hooks/tools/sign/useSavedSignatures';
|
||||
import { SavedSignaturesSection } from '@app/components/tools/sign/SavedSignaturesSection';
|
||||
import { buildSignaturePreview } from '@app/utils/signaturePreview';
|
||||
|
||||
type SignatureDrafts = {
|
||||
canvas?: string;
|
||||
@@ -99,6 +100,7 @@ const SignSettings = ({
|
||||
removeSignature,
|
||||
updateSignatureLabel,
|
||||
byTypeCounts,
|
||||
storageType,
|
||||
} = useSavedSignatures();
|
||||
const [signatureSource, setSignatureSource] = useState<SignatureSource>(() => {
|
||||
const paramSource = parameters.signatureType as SignatureSource;
|
||||
@@ -157,11 +159,11 @@ const SignSettings = ({
|
||||
}, [canvasSignatureData, imageSignatureData, buildTextSignatureKey, parameters.signerName, parameters.fontSize, parameters.fontFamily, parameters.textColor]);
|
||||
|
||||
const saveSignatureToLibrary = useCallback(
|
||||
(payload: SavedSignaturePayload, type: SavedSignatureType): AddSignatureResult => {
|
||||
async (payload: SavedSignaturePayload, type: SavedSignatureType, scope: 'personal' | 'shared'): Promise<AddSignatureResult> => {
|
||||
if (isSavedSignatureLimitReached) {
|
||||
return { success: false, reason: 'limit' };
|
||||
}
|
||||
return addSignature(payload, getDefaultSavedLabel(type));
|
||||
return await addSignature(payload, getDefaultSavedLabel(type), scope);
|
||||
},
|
||||
[addSignature, getDefaultSavedLabel, isSavedSignatureLimitReached]
|
||||
);
|
||||
@@ -176,40 +178,57 @@ const SignSettings = ({
|
||||
[signatureKeysByType]
|
||||
);
|
||||
|
||||
const handleSaveCanvasSignature = useCallback(() => {
|
||||
const handleSaveCanvasSignature = useCallback(async (scope: 'personal' | 'shared') => {
|
||||
if (!canvasSignatureData) {
|
||||
return;
|
||||
}
|
||||
const result = saveSignatureToLibrary({ type: 'canvas', dataUrl: canvasSignatureData }, 'canvas');
|
||||
const result = await saveSignatureToLibrary({ type: 'canvas', dataUrl: canvasSignatureData }, 'canvas', scope);
|
||||
if (result.success) {
|
||||
setLastSavedKeyForType('canvas');
|
||||
}
|
||||
}, [canvasSignatureData, saveSignatureToLibrary, setLastSavedKeyForType]);
|
||||
|
||||
const handleSaveImageSignature = useCallback(() => {
|
||||
const handleSaveImageSignature = useCallback(async (scope: 'personal' | 'shared') => {
|
||||
if (!imageSignatureData) {
|
||||
return;
|
||||
}
|
||||
const result = saveSignatureToLibrary({ type: 'image', dataUrl: imageSignatureData }, 'image');
|
||||
const result = await saveSignatureToLibrary({ type: 'image', dataUrl: imageSignatureData }, 'image', scope);
|
||||
if (result.success) {
|
||||
setLastSavedKeyForType('image');
|
||||
}
|
||||
}, [imageSignatureData, saveSignatureToLibrary, setLastSavedKeyForType]);
|
||||
|
||||
const handleSaveTextSignature = useCallback(() => {
|
||||
const handleSaveTextSignature = useCallback(async (scope: 'personal' | 'shared') => {
|
||||
const signerName = (parameters.signerName ?? '').trim();
|
||||
if (!signerName) {
|
||||
return;
|
||||
}
|
||||
const result = saveSignatureToLibrary(
|
||||
|
||||
// Generate image from text signature
|
||||
const preview = await buildSignaturePreview({
|
||||
signatureType: 'text',
|
||||
signerName,
|
||||
fontFamily: parameters.fontFamily ?? 'Helvetica',
|
||||
fontSize: parameters.fontSize ?? 16,
|
||||
textColor: parameters.textColor ?? '#000000',
|
||||
});
|
||||
|
||||
if (!preview?.dataUrl) {
|
||||
console.error('Failed to generate text signature preview');
|
||||
return;
|
||||
}
|
||||
|
||||
const result = await saveSignatureToLibrary(
|
||||
{
|
||||
type: 'text',
|
||||
dataUrl: preview.dataUrl,
|
||||
signerName,
|
||||
fontFamily: parameters.fontFamily ?? 'Helvetica',
|
||||
fontSize: parameters.fontSize ?? 16,
|
||||
textColor: parameters.textColor ?? '#000000',
|
||||
},
|
||||
'text'
|
||||
'text',
|
||||
scope
|
||||
);
|
||||
if (result.success) {
|
||||
setLastSavedKeyForType('text');
|
||||
@@ -286,16 +305,21 @@ const SignSettings = ({
|
||||
[updateSignatureLabel]
|
||||
);
|
||||
|
||||
const renderSaveButton = (type: SavedSignatureType, isReady: boolean, onClick: () => void) => {
|
||||
const renderSaveButton = (type: SavedSignatureType, isReady: boolean, onClick: (scope: 'personal' | 'shared') => void, scope: 'personal' | 'shared', icon: string, label: string) => {
|
||||
if (!canUseSavedLibrary) {
|
||||
return null;
|
||||
}
|
||||
const label = translate('saved.saveButton', 'Save signature');
|
||||
const currentKey = signatureKeysByType[type];
|
||||
const lastSavedKey = lastSavedSignatureKeys[type];
|
||||
const hasChanges = Boolean(currentKey && currentKey !== lastSavedKey);
|
||||
const isSaved = isReady && !hasChanges;
|
||||
|
||||
// Only show backend storage buttons when backend is available
|
||||
const showButton = storageType === 'backend' || storageType === null;
|
||||
if (!showButton) {
|
||||
return null;
|
||||
}
|
||||
|
||||
let tooltipMessage: string | undefined;
|
||||
if (!isReady) {
|
||||
tooltipMessage = translate('saved.saveUnavailable', 'Create a signature first to save it.');
|
||||
@@ -312,11 +336,11 @@ const SignSettings = ({
|
||||
size="xs"
|
||||
variant="outline"
|
||||
color={isSaved ? 'green' : undefined}
|
||||
onClick={onClick}
|
||||
onClick={() => onClick(scope)}
|
||||
disabled={!isReady || disabled || isSavedSignatureLimitReached || !hasChanges}
|
||||
leftSection={<LocalIcon icon="material-symbols:save-rounded" width={16} height={16} />}
|
||||
leftSection={<LocalIcon icon={icon} width={16} height={16} />}
|
||||
>
|
||||
{isSaved ? translate('saved.status.saved', 'Saved') : label}
|
||||
{label}
|
||||
</Button>
|
||||
);
|
||||
|
||||
@@ -331,15 +355,38 @@ const SignSettings = ({
|
||||
return button;
|
||||
};
|
||||
|
||||
const renderSaveButtonRow = (type: SavedSignatureType, isReady: boolean, onClick: () => void) => {
|
||||
const button = renderSaveButton(type, isReady, onClick);
|
||||
if (!button) {
|
||||
const renderSaveButtonRow = (type: SavedSignatureType, isReady: boolean, onClick: (scope: 'personal' | 'shared') => void) => {
|
||||
if (!canUseSavedLibrary) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const personalButton = renderSaveButton(
|
||||
type,
|
||||
isReady,
|
||||
onClick,
|
||||
'personal',
|
||||
'material-symbols:person-rounded',
|
||||
translate('saved.savePersonal', 'Save Personal')
|
||||
);
|
||||
|
||||
const sharedButton = renderSaveButton(
|
||||
type,
|
||||
isReady,
|
||||
onClick,
|
||||
'shared',
|
||||
'material-symbols:groups-rounded',
|
||||
translate('saved.saveShared', 'Save Shared')
|
||||
);
|
||||
|
||||
if (!personalButton && !sharedButton) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<Box style={{ alignSelf: 'flex-start', marginTop: '0.4rem' }}>
|
||||
{button}
|
||||
</Box>
|
||||
<Group gap="xs" style={{ alignSelf: 'flex-start', marginTop: '0.4rem' }}>
|
||||
{personalButton}
|
||||
{sharedButton}
|
||||
</Group>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -744,6 +791,7 @@ const SignSettings = ({
|
||||
signatures={savedSignatures}
|
||||
disabled={disabled}
|
||||
isAtCapacity={isSavedSignatureLimitReached}
|
||||
storageType={storageType}
|
||||
onUseSignature={handleUseSavedSignature}
|
||||
onDeleteSignature={handleDeleteSavedSignature}
|
||||
onRenameSignature={handleRenameSavedSignature}
|
||||
|
||||
Reference in New Issue
Block a user