mirror of
https://github.com/Stirling-Tools/Stirling-PDF.git
synced 2025-09-18 01:19: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",
|
||||
"oddWithinExcluding": "Odd within 1-20 excluding 5-7",
|
||||
"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": {
|
||||
|
@ -852,6 +852,29 @@
|
||||
"every3rd": "Every 3rd",
|
||||
"oddWithinExcluding": "Odd within 1-20 excluding 5-7",
|
||||
"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": {
|
||||
|
@ -1,5 +1,6 @@
|
||||
import { useState } from 'react';
|
||||
import { Flex } from '@mantine/core';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import classes from './BulkSelectionPanel.module.css';
|
||||
import {
|
||||
appendExpression,
|
||||
@ -8,6 +9,7 @@ import {
|
||||
lastNExpression,
|
||||
everyNthExpression,
|
||||
rangeExpression,
|
||||
LogicalOperator,
|
||||
} from './BulkSelection';
|
||||
import SelectPages from './SelectPages';
|
||||
import OperatorsSection from './OperatorsSection';
|
||||
@ -27,6 +29,7 @@ const AdvancedSelectionPanel = ({
|
||||
maxPages,
|
||||
advancedOpened,
|
||||
}: AdvancedSelectionPanelProps) => {
|
||||
const { t } = useTranslation();
|
||||
const [rangeEnd, setRangeEnd] = useState<number | ''>('');
|
||||
|
||||
const handleRangeEndChange = (val: string | number) => {
|
||||
@ -47,11 +50,6 @@ const AdvancedSelectionPanel = ({
|
||||
return null;
|
||||
};
|
||||
|
||||
const validateRangeEnd = (end: number): string | null => {
|
||||
if (end <= 0) return 'Values must be positive';
|
||||
return null;
|
||||
};
|
||||
|
||||
// Named callback functions
|
||||
const applyExpression = (expr: string) => {
|
||||
const nextInput = appendExpression(csvInput, expr);
|
||||
@ -59,9 +57,13 @@ const AdvancedSelectionPanel = ({
|
||||
onUpdatePagesFromCSV(nextInput);
|
||||
};
|
||||
|
||||
const insertOperator = (op: 'and' | 'or' | 'not') => {
|
||||
const insertOperator = (op: LogicalOperator) => {
|
||||
const next = insertOperatorSmart(csvInput, op);
|
||||
setCsvInput(next);
|
||||
// Trigger visual selection update for 'even' and 'odd' operators
|
||||
if (op === 'even' || op === 'odd') {
|
||||
onUpdatePagesFromCSV(next);
|
||||
}
|
||||
};
|
||||
|
||||
const handleFirstNApply = (value: number) => {
|
||||
@ -95,36 +97,36 @@ const AdvancedSelectionPanel = ({
|
||||
{/* Cards row */}
|
||||
<Flex direction="row" mb="xs" wrap="wrap">
|
||||
<SelectPages
|
||||
title="First N Pages"
|
||||
placeholder="Number of pages"
|
||||
title={t('bulkSelection.firstNPages.title', 'First N Pages')}
|
||||
placeholder={t('bulkSelection.firstNPages.placeholder', 'Number of pages')}
|
||||
onApply={handleFirstNApply}
|
||||
maxPages={maxPages}
|
||||
validationFn={validatePositiveNumber}
|
||||
/>
|
||||
|
||||
<SelectPages
|
||||
title="Range"
|
||||
placeholder="From"
|
||||
title={t('bulkSelection.range.title', 'Range')}
|
||||
placeholder={t('bulkSelection.range.fromPlaceholder', 'From')}
|
||||
onApply={handleRangeApply}
|
||||
maxPages={maxPages}
|
||||
validationFn={validateRangeStart}
|
||||
isRange={true}
|
||||
rangeEndValue={rangeEnd}
|
||||
onRangeEndChange={handleRangeEndChange}
|
||||
rangeEndPlaceholder="To"
|
||||
rangeEndPlaceholder={t('bulkSelection.range.toPlaceholder', 'To')}
|
||||
/>
|
||||
|
||||
<SelectPages
|
||||
title="Last N Pages"
|
||||
placeholder="Number of pages"
|
||||
title={t('bulkSelection.lastNPages.title', 'Last N Pages')}
|
||||
placeholder={t('bulkSelection.lastNPages.placeholder', 'Number of pages')}
|
||||
onApply={handleLastNApply}
|
||||
maxPages={maxPages}
|
||||
validationFn={validatePositiveNumber}
|
||||
/>
|
||||
|
||||
<SelectPages
|
||||
title="Every Nth Page"
|
||||
placeholder="Step size"
|
||||
title={t('bulkSelection.everyNthPage.title', 'Every Nth Page')}
|
||||
placeholder={t('bulkSelection.everyNthPage.placeholder', 'Step size')}
|
||||
onApply={handleEveryNthApply}
|
||||
maxPages={maxPages}
|
||||
/>
|
||||
|
@ -1,6 +1,6 @@
|
||||
// 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.
|
||||
// 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();
|
||||
if (!current) return expr;
|
||||
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.
|
||||
@ -18,6 +23,18 @@ export function insertOperatorSmart(currentInput: string, op: LogicalOperator):
|
||||
const text = (currentInput || '').trim();
|
||||
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
|
||||
const tokens: string[] = [];
|
||||
let rest = text;
|
||||
@ -39,19 +56,19 @@ export function insertOperatorSmart(currentInput: string, op: LogicalOperator):
|
||||
|
||||
// Normalize to allowed set
|
||||
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
|
||||
const fromSingle = (t: string): string => {
|
||||
if (t === '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';
|
||||
}
|
||||
if (t === 'or') {
|
||||
if (click === 'and') return 'and';
|
||||
if (click === 'or') return 'or';
|
||||
return 'not';
|
||||
return 'or not';
|
||||
}
|
||||
// t === 'not'
|
||||
if (click === 'and') return 'and';
|
||||
@ -61,17 +78,19 @@ export function insertOperatorSmart(currentInput: string, op: LogicalOperator):
|
||||
|
||||
// From combined phrase
|
||||
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 (click === 'not') return 'and not';
|
||||
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;
|
||||
};
|
||||
|
||||
|
@ -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 { LogicalOperator } from './BulkSelection';
|
||||
|
||||
interface OperatorsSectionProps {
|
||||
csvInput: string;
|
||||
onInsertOperator: (op: 'and' | 'or' | 'not') => void;
|
||||
onInsertOperator: (op: LogicalOperator) => void;
|
||||
}
|
||||
|
||||
const OperatorsSection = ({ csvInput, onInsertOperator }: OperatorsSectionProps) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<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">
|
||||
<Button
|
||||
size="sm"
|
||||
@ -42,6 +46,29 @@ const OperatorsSection = ({ csvInput, onInsertOperator }: OperatorsSectionProps)
|
||||
<Text size="xs" fw={500}>not</Text>
|
||||
</Button>
|
||||
</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>
|
||||
);
|
||||
};
|
||||
|
@ -1,4 +1,5 @@
|
||||
import { TextInput, Button, Text, Flex, Switch } from '@mantine/core';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import LocalIcon from '../../shared/LocalIcon';
|
||||
import { Tooltip } from '../../shared/Tooltip';
|
||||
import { usePageSelectionTips } from '../../tooltips/usePageSelectionTips';
|
||||
@ -21,6 +22,7 @@ const PageSelectionInput = ({
|
||||
advancedOpened,
|
||||
onToggleAdvanced,
|
||||
}: PageSelectionInputProps) => {
|
||||
const { t } = useTranslation();
|
||||
const pageSelectionTips = usePageSelectionTips();
|
||||
|
||||
return (
|
||||
@ -43,12 +45,12 @@ const PageSelectionInput = ({
|
||||
</Tooltip>
|
||||
{typeof advancedOpened === 'boolean' && (
|
||||
<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
|
||||
size="sm"
|
||||
checked={!!advancedOpened}
|
||||
onChange={(e) => onToggleAdvanced?.(e.currentTarget.checked)}
|
||||
title="Advanced"
|
||||
title={t('bulkSelection.advanced.title', 'Advanced')}
|
||||
className={classes.advancedSwitch}
|
||||
/>
|
||||
</Flex>
|
||||
|
Loading…
x
Reference in New Issue
Block a user