add even and odd buttons and include translations

This commit is contained in:
EthanHealy01 2025-09-16 17:15:09 +01:00
parent ff84fbf8b4
commit c27a23b20b
6 changed files with 128 additions and 32 deletions

View File

@ -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": {

View File

@ -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": {

View File

@ -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}
/>

View File

@ -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;
};

View File

@ -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>
);
};

View File

@ -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>