mirror of
https://github.com/Stirling-Tools/Stirling-PDF.git
synced 2025-09-18 09:29:24 +00:00
add even and odd buttons and include translations
This commit is contained in:
parent
ff84fbf8b4
commit
c27a23b20b
@ -1207,6 +1207,29 @@
|
|||||||
"every3rd": "Every 3rd",
|
"every3rd": "Every 3rd",
|
||||||
"oddWithinExcluding": "Odd within 1-20 excluding 5-7",
|
"oddWithinExcluding": "Odd within 1-20 excluding 5-7",
|
||||||
"combineSets": "Combine sets"
|
"combineSets": "Combine sets"
|
||||||
|
},
|
||||||
|
"firstNPages": {
|
||||||
|
"title": "First N Pages",
|
||||||
|
"placeholder": "Number of pages"
|
||||||
|
},
|
||||||
|
"lastNPages": {
|
||||||
|
"title": "Last N Pages",
|
||||||
|
"placeholder": "Number of pages"
|
||||||
|
},
|
||||||
|
"everyNthPage": {
|
||||||
|
"title": "Every Nth Page",
|
||||||
|
"placeholder": "Step size"
|
||||||
|
},
|
||||||
|
"range": {
|
||||||
|
"title": "Range",
|
||||||
|
"fromPlaceholder": "From",
|
||||||
|
"toPlaceholder": "To"
|
||||||
|
},
|
||||||
|
"keywords": {
|
||||||
|
"title": "Keywords"
|
||||||
|
},
|
||||||
|
"advanced": {
|
||||||
|
"title": "Advanced"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"compressPdfs": {
|
"compressPdfs": {
|
||||||
|
@ -852,6 +852,29 @@
|
|||||||
"every3rd": "Every 3rd",
|
"every3rd": "Every 3rd",
|
||||||
"oddWithinExcluding": "Odd within 1-20 excluding 5-7",
|
"oddWithinExcluding": "Odd within 1-20 excluding 5-7",
|
||||||
"combineSets": "Combine sets"
|
"combineSets": "Combine sets"
|
||||||
|
},
|
||||||
|
"firstNPages": {
|
||||||
|
"title": "First N Pages",
|
||||||
|
"placeholder": "Number of pages"
|
||||||
|
},
|
||||||
|
"lastNPages": {
|
||||||
|
"title": "Last N Pages",
|
||||||
|
"placeholder": "Number of pages"
|
||||||
|
},
|
||||||
|
"everyNthPage": {
|
||||||
|
"title": "Every Nth Page",
|
||||||
|
"placeholder": "Step size"
|
||||||
|
},
|
||||||
|
"range": {
|
||||||
|
"title": "Range",
|
||||||
|
"fromPlaceholder": "From",
|
||||||
|
"toPlaceholder": "To"
|
||||||
|
},
|
||||||
|
"keywords": {
|
||||||
|
"title": "Keywords"
|
||||||
|
},
|
||||||
|
"advanced": {
|
||||||
|
"title": "Advanced"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"compressPdfs": {
|
"compressPdfs": {
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
import { Flex } from '@mantine/core';
|
import { Flex } from '@mantine/core';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
import classes from './BulkSelectionPanel.module.css';
|
import classes from './BulkSelectionPanel.module.css';
|
||||||
import {
|
import {
|
||||||
appendExpression,
|
appendExpression,
|
||||||
@ -8,6 +9,7 @@ import {
|
|||||||
lastNExpression,
|
lastNExpression,
|
||||||
everyNthExpression,
|
everyNthExpression,
|
||||||
rangeExpression,
|
rangeExpression,
|
||||||
|
LogicalOperator,
|
||||||
} from './BulkSelection';
|
} from './BulkSelection';
|
||||||
import SelectPages from './SelectPages';
|
import SelectPages from './SelectPages';
|
||||||
import OperatorsSection from './OperatorsSection';
|
import OperatorsSection from './OperatorsSection';
|
||||||
@ -27,6 +29,7 @@ const AdvancedSelectionPanel = ({
|
|||||||
maxPages,
|
maxPages,
|
||||||
advancedOpened,
|
advancedOpened,
|
||||||
}: AdvancedSelectionPanelProps) => {
|
}: AdvancedSelectionPanelProps) => {
|
||||||
|
const { t } = useTranslation();
|
||||||
const [rangeEnd, setRangeEnd] = useState<number | ''>('');
|
const [rangeEnd, setRangeEnd] = useState<number | ''>('');
|
||||||
|
|
||||||
const handleRangeEndChange = (val: string | number) => {
|
const handleRangeEndChange = (val: string | number) => {
|
||||||
@ -47,11 +50,6 @@ const AdvancedSelectionPanel = ({
|
|||||||
return null;
|
return null;
|
||||||
};
|
};
|
||||||
|
|
||||||
const validateRangeEnd = (end: number): string | null => {
|
|
||||||
if (end <= 0) return 'Values must be positive';
|
|
||||||
return null;
|
|
||||||
};
|
|
||||||
|
|
||||||
// Named callback functions
|
// Named callback functions
|
||||||
const applyExpression = (expr: string) => {
|
const applyExpression = (expr: string) => {
|
||||||
const nextInput = appendExpression(csvInput, expr);
|
const nextInput = appendExpression(csvInput, expr);
|
||||||
@ -59,9 +57,13 @@ const AdvancedSelectionPanel = ({
|
|||||||
onUpdatePagesFromCSV(nextInput);
|
onUpdatePagesFromCSV(nextInput);
|
||||||
};
|
};
|
||||||
|
|
||||||
const insertOperator = (op: 'and' | 'or' | 'not') => {
|
const insertOperator = (op: LogicalOperator) => {
|
||||||
const next = insertOperatorSmart(csvInput, op);
|
const next = insertOperatorSmart(csvInput, op);
|
||||||
setCsvInput(next);
|
setCsvInput(next);
|
||||||
|
// Trigger visual selection update for 'even' and 'odd' operators
|
||||||
|
if (op === 'even' || op === 'odd') {
|
||||||
|
onUpdatePagesFromCSV(next);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleFirstNApply = (value: number) => {
|
const handleFirstNApply = (value: number) => {
|
||||||
@ -95,36 +97,36 @@ const AdvancedSelectionPanel = ({
|
|||||||
{/* Cards row */}
|
{/* Cards row */}
|
||||||
<Flex direction="row" mb="xs" wrap="wrap">
|
<Flex direction="row" mb="xs" wrap="wrap">
|
||||||
<SelectPages
|
<SelectPages
|
||||||
title="First N Pages"
|
title={t('bulkSelection.firstNPages.title', 'First N Pages')}
|
||||||
placeholder="Number of pages"
|
placeholder={t('bulkSelection.firstNPages.placeholder', 'Number of pages')}
|
||||||
onApply={handleFirstNApply}
|
onApply={handleFirstNApply}
|
||||||
maxPages={maxPages}
|
maxPages={maxPages}
|
||||||
validationFn={validatePositiveNumber}
|
validationFn={validatePositiveNumber}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<SelectPages
|
<SelectPages
|
||||||
title="Range"
|
title={t('bulkSelection.range.title', 'Range')}
|
||||||
placeholder="From"
|
placeholder={t('bulkSelection.range.fromPlaceholder', 'From')}
|
||||||
onApply={handleRangeApply}
|
onApply={handleRangeApply}
|
||||||
maxPages={maxPages}
|
maxPages={maxPages}
|
||||||
validationFn={validateRangeStart}
|
validationFn={validateRangeStart}
|
||||||
isRange={true}
|
isRange={true}
|
||||||
rangeEndValue={rangeEnd}
|
rangeEndValue={rangeEnd}
|
||||||
onRangeEndChange={handleRangeEndChange}
|
onRangeEndChange={handleRangeEndChange}
|
||||||
rangeEndPlaceholder="To"
|
rangeEndPlaceholder={t('bulkSelection.range.toPlaceholder', 'To')}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<SelectPages
|
<SelectPages
|
||||||
title="Last N Pages"
|
title={t('bulkSelection.lastNPages.title', 'Last N Pages')}
|
||||||
placeholder="Number of pages"
|
placeholder={t('bulkSelection.lastNPages.placeholder', 'Number of pages')}
|
||||||
onApply={handleLastNApply}
|
onApply={handleLastNApply}
|
||||||
maxPages={maxPages}
|
maxPages={maxPages}
|
||||||
validationFn={validatePositiveNumber}
|
validationFn={validatePositiveNumber}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<SelectPages
|
<SelectPages
|
||||||
title="Every Nth Page"
|
title={t('bulkSelection.everyNthPage.title', 'Every Nth Page')}
|
||||||
placeholder="Step size"
|
placeholder={t('bulkSelection.everyNthPage.placeholder', 'Step size')}
|
||||||
onApply={handleEveryNthApply}
|
onApply={handleEveryNthApply}
|
||||||
maxPages={maxPages}
|
maxPages={maxPages}
|
||||||
/>
|
/>
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
// Pure helper utilities for the BulkSelectionPanel UI
|
// Pure helper utilities for the BulkSelectionPanel UI
|
||||||
|
|
||||||
export type LogicalOperator = 'and' | 'or' | 'not';
|
export type LogicalOperator = 'and' | 'or' | 'not' | 'even' | 'odd';
|
||||||
|
|
||||||
// Returns a new CSV expression with expr appended.
|
// Returns a new CSV expression with expr appended.
|
||||||
// If current ends with an operator token, expr is appended directly.
|
// If current ends with an operator token, expr is appended directly.
|
||||||
@ -9,7 +9,12 @@ export function appendExpression(currentInput: string, expr: string): string {
|
|||||||
const current = (currentInput || '').trim();
|
const current = (currentInput || '').trim();
|
||||||
if (!current) return expr;
|
if (!current) return expr;
|
||||||
const endsWithOperator = /(\b(and|not|or)\s*|[&|,!]\s*)$/i.test(current);
|
const endsWithOperator = /(\b(and|not|or)\s*|[&|,!]\s*)$/i.test(current);
|
||||||
return endsWithOperator ? `${current}${expr}` : `${current} or ${expr}`;
|
// Add space if operator doesn't already have one
|
||||||
|
if (endsWithOperator) {
|
||||||
|
const needsSpace = !current.endsWith(' ');
|
||||||
|
return `${current}${needsSpace ? ' ' : ''}${expr}`;
|
||||||
|
}
|
||||||
|
return `${current} or ${expr}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Smartly inserts/normalizes a logical operator at the end of the current input.
|
// Smartly inserts/normalizes a logical operator at the end of the current input.
|
||||||
@ -18,6 +23,18 @@ export function insertOperatorSmart(currentInput: string, op: LogicalOperator):
|
|||||||
const text = (currentInput || '').trim();
|
const text = (currentInput || '').trim();
|
||||||
if (text.length === 0) return `${op} `;
|
if (text.length === 0) return `${op} `;
|
||||||
|
|
||||||
|
// Handle 'even' and 'odd' as page selection expressions, not logical operators
|
||||||
|
if (op === 'even' || op === 'odd') {
|
||||||
|
// If current input ends with a logical operator, append the page selection with proper spacing
|
||||||
|
const endsWithOperator = /(\b(and|not|or)\s*|[&|,!]\s*)$/i.test(text);
|
||||||
|
if (endsWithOperator) {
|
||||||
|
// Add space if the operator doesn't already have one
|
||||||
|
const needsSpace = !text.endsWith(' ');
|
||||||
|
return `${text}${needsSpace ? ' ' : ''}${op} `;
|
||||||
|
}
|
||||||
|
return `${text} or ${op} `;
|
||||||
|
}
|
||||||
|
|
||||||
// Extract up to the last two operator tokens (words or symbols) from the end
|
// Extract up to the last two operator tokens (words or symbols) from the end
|
||||||
const tokens: string[] = [];
|
const tokens: string[] = [];
|
||||||
let rest = text;
|
let rest = text;
|
||||||
@ -39,19 +56,19 @@ export function insertOperatorSmart(currentInput: string, op: LogicalOperator):
|
|||||||
|
|
||||||
// Normalize to allowed set
|
// Normalize to allowed set
|
||||||
const phrase = tokens.join(' ');
|
const phrase = tokens.join(' ');
|
||||||
const allowed = new Set(['and', 'or', 'not', 'and or', 'and not']);
|
const allowed = new Set(['and', 'or', 'not', 'and not', 'or not']);
|
||||||
|
|
||||||
// Helpers for transitions from a single trailing token
|
// Helpers for transitions from a single trailing token
|
||||||
const fromSingle = (t: string): string => {
|
const fromSingle = (t: string): string => {
|
||||||
if (t === 'and') {
|
if (t === 'and') {
|
||||||
if (click === 'and') return 'and';
|
if (click === 'and') return 'and';
|
||||||
if (click === 'or') return 'and or';
|
if (click === 'or') return 'or'; // 'and or' is invalid, so just use 'or'
|
||||||
return 'and not';
|
return 'and not';
|
||||||
}
|
}
|
||||||
if (t === 'or') {
|
if (t === 'or') {
|
||||||
if (click === 'and') return 'and';
|
if (click === 'and') return 'and';
|
||||||
if (click === 'or') return 'or';
|
if (click === 'or') return 'or';
|
||||||
return 'not';
|
return 'or not';
|
||||||
}
|
}
|
||||||
// t === 'not'
|
// t === 'not'
|
||||||
if (click === 'and') return 'and';
|
if (click === 'and') return 'and';
|
||||||
@ -61,17 +78,19 @@ export function insertOperatorSmart(currentInput: string, op: LogicalOperator):
|
|||||||
|
|
||||||
// From combined phrase
|
// From combined phrase
|
||||||
const fromCombo = (p: string): string => {
|
const fromCombo = (p: string): string => {
|
||||||
if (p === 'and or') {
|
|
||||||
if (click === 'or') return 'and or';
|
|
||||||
if (click === 'and') return 'and';
|
|
||||||
return 'and not';
|
|
||||||
}
|
|
||||||
if (p === 'and not') {
|
if (p === 'and not') {
|
||||||
if (click === 'not') return 'and not';
|
if (click === 'not') return 'and not';
|
||||||
if (click === 'and') return 'and';
|
if (click === 'and') return 'and';
|
||||||
return 'and or';
|
if (click === 'or') return 'or'; // 'and not or' is invalid, so just use 'or'
|
||||||
|
return 'and not';
|
||||||
}
|
}
|
||||||
// Invalid combos (e.g., 'not and', 'not or', 'or and') → collapse to clicked op
|
if (p === 'or not') {
|
||||||
|
if (click === 'not') return 'or not';
|
||||||
|
if (click === 'or') return 'or';
|
||||||
|
if (click === 'and') return 'and'; // 'or not and' is invalid, so just use 'and'
|
||||||
|
return 'or not';
|
||||||
|
}
|
||||||
|
// Invalid combos (e.g., 'not and', 'not or', 'or and', 'and or') → collapse to clicked op
|
||||||
return click;
|
return click;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -1,15 +1,19 @@
|
|||||||
import { Button, Text, Group } from '@mantine/core';
|
import { Button, Text, Group, Divider } from '@mantine/core';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
import classes from './BulkSelectionPanel.module.css';
|
import classes from './BulkSelectionPanel.module.css';
|
||||||
|
import { LogicalOperator } from './BulkSelection';
|
||||||
|
|
||||||
interface OperatorsSectionProps {
|
interface OperatorsSectionProps {
|
||||||
csvInput: string;
|
csvInput: string;
|
||||||
onInsertOperator: (op: 'and' | 'or' | 'not') => void;
|
onInsertOperator: (op: LogicalOperator) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
const OperatorsSection = ({ csvInput, onInsertOperator }: OperatorsSectionProps) => {
|
const OperatorsSection = ({ csvInput, onInsertOperator }: OperatorsSectionProps) => {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<Text size="xs" c="var(--text-muted)" fw={500} mb="xs">Add Operators:</Text>
|
<Text size="xs" c="var(--text-muted)" fw={500} mb="xs">{t('bulkSelection.keywords.title', 'Keywords')}:</Text>
|
||||||
<Group gap="sm" wrap="nowrap">
|
<Group gap="sm" wrap="nowrap">
|
||||||
<Button
|
<Button
|
||||||
size="sm"
|
size="sm"
|
||||||
@ -42,6 +46,29 @@ const OperatorsSection = ({ csvInput, onInsertOperator }: OperatorsSectionProps)
|
|||||||
<Text size="xs" fw={500}>not</Text>
|
<Text size="xs" fw={500}>not</Text>
|
||||||
</Button>
|
</Button>
|
||||||
</Group>
|
</Group>
|
||||||
|
<Divider my="sm" />
|
||||||
|
<Group gap="sm" wrap="nowrap">
|
||||||
|
<Button
|
||||||
|
size="sm"
|
||||||
|
variant="outline"
|
||||||
|
className={classes.operatorChip}
|
||||||
|
onClick={() => onInsertOperator('even')}
|
||||||
|
disabled={!csvInput.trim()}
|
||||||
|
title="Combine selections (both conditions must be true)"
|
||||||
|
>
|
||||||
|
<Text size="xs" fw={500}>even</Text>
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
size="sm"
|
||||||
|
variant="outline"
|
||||||
|
className={classes.operatorChip}
|
||||||
|
onClick={() => onInsertOperator('odd')}
|
||||||
|
disabled={!csvInput.trim()}
|
||||||
|
title="Add to selection (either condition can be true)"
|
||||||
|
>
|
||||||
|
<Text size="xs" fw={500}>odd</Text>
|
||||||
|
</Button>
|
||||||
|
</Group>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
import { TextInput, Button, Text, Flex, Switch } from '@mantine/core';
|
import { TextInput, Button, Text, Flex, Switch } from '@mantine/core';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
import LocalIcon from '../../shared/LocalIcon';
|
import LocalIcon from '../../shared/LocalIcon';
|
||||||
import { Tooltip } from '../../shared/Tooltip';
|
import { Tooltip } from '../../shared/Tooltip';
|
||||||
import { usePageSelectionTips } from '../../tooltips/usePageSelectionTips';
|
import { usePageSelectionTips } from '../../tooltips/usePageSelectionTips';
|
||||||
@ -21,6 +22,7 @@ const PageSelectionInput = ({
|
|||||||
advancedOpened,
|
advancedOpened,
|
||||||
onToggleAdvanced,
|
onToggleAdvanced,
|
||||||
}: PageSelectionInputProps) => {
|
}: PageSelectionInputProps) => {
|
||||||
|
const { t } = useTranslation();
|
||||||
const pageSelectionTips = usePageSelectionTips();
|
const pageSelectionTips = usePageSelectionTips();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -43,12 +45,12 @@ const PageSelectionInput = ({
|
|||||||
</Tooltip>
|
</Tooltip>
|
||||||
{typeof advancedOpened === 'boolean' && (
|
{typeof advancedOpened === 'boolean' && (
|
||||||
<Flex align="center" gap="xs">
|
<Flex align="center" gap="xs">
|
||||||
<Text size="sm" c="var(--text-secondary)">Advanced</Text>
|
<Text size="sm" c="var(--text-secondary)">{t('bulkSelection.advanced.title', 'Advanced')}</Text>
|
||||||
<Switch
|
<Switch
|
||||||
size="sm"
|
size="sm"
|
||||||
checked={!!advancedOpened}
|
checked={!!advancedOpened}
|
||||||
onChange={(e) => onToggleAdvanced?.(e.currentTarget.checked)}
|
onChange={(e) => onToggleAdvanced?.(e.currentTarget.checked)}
|
||||||
title="Advanced"
|
title={t('bulkSelection.advanced.title', 'Advanced')}
|
||||||
className={classes.advancedSwitch}
|
className={classes.advancedSwitch}
|
||||||
/>
|
/>
|
||||||
</Flex>
|
</Flex>
|
||||||
|
Loading…
x
Reference in New Issue
Block a user