pipeline cards, context menus, presets, settings overhaul

rewrote pipeline as draggable card strip with per-rule config popovers,
added right-click menus to pipeline cards, sidebar tree, and file list,
preset import/export with BRU format support, new rules (hash, swap,
truncate, sanitize, padding, randomize, text editor, folder name,
transliterate), settings dialog with all sections, overlay collision
containment, tooltips on icon buttons, empty pipeline default
This commit is contained in:
2026-03-14 19:04:35 +02:00
parent 9dca2bedfa
commit 6f5b862234
105 changed files with 17257 additions and 1369 deletions

View File

@@ -0,0 +1,98 @@
import { useRuleStore } from "@/stores/ruleStore";
import { Input } from "@/components/ui/input";
import { SegmentedControl } from "@/components/ui/segmented-control";
import { Checkbox } from "@/components/ui/checkbox";
import { IconPlus, IconTrash } from "@tabler/icons-react";
import type { ExtensionConfig as ExtensionConfigType } from "@/types/rules";
const extModes = [
{ value: "Same", label: "Same" },
{ value: "Lower", label: "lower" },
{ value: "Upper", label: "UPPER" },
{ value: "Title", label: "Title" },
{ value: "Extra", label: "Extra" },
{ value: "Remove", label: "Remove" },
{ value: "Fixed", label: "Fixed" },
] as const;
export function ExtensionConfig({ ruleId }: { ruleId: string }) {
const rule = useRuleStore((s) => s.pipeline.find((r) => r.id === ruleId))?.config as ExtensionConfigType | undefined;
const updateRule = useRuleStore((s) => s.updateRule);
if (!rule) return null;
const update = (changes: Partial<ExtensionConfigType>) => updateRule(ruleId, changes);
const mappings = rule.mapping || [];
const addMapping = () => {
update({ mapping: [...mappings, ["", ""]] });
};
const removeMapping = (idx: number) => {
const next = mappings.filter((_, i) => i !== idx);
update({ mapping: next.length > 0 ? next : null });
};
const updateMapping = (idx: number, pos: 0 | 1, val: string) => {
const next = mappings.map((m, i) => {
if (i !== idx) return m;
const copy: [string, string] = [...m];
copy[pos] = val;
return copy;
});
update({ mapping: next });
};
return (
<div className="flex flex-col gap-3 text-sm">
<div className="flex flex-col gap-1">
<span className="text-xs text-muted-foreground">Mode</span>
<SegmentedControl value={rule.mode} onChange={(m) => update({ mode: m })} options={extModes} size="sm" />
</div>
{rule.mode === "Fixed" && (
<label className="flex flex-col gap-1">
<span className="text-xs text-muted-foreground">New extension</span>
<Input
value={rule.fixed_value}
onChange={(e) => update({ fixed_value: e.target.value })}
placeholder="e.g. txt, jpg, png"
className="h-8 text-xs font-mono"
/>
</label>
)}
{mappings.length > 0 && (
<div className="flex flex-col gap-1.5">
<span className="text-xs text-muted-foreground">Extension mappings</span>
{mappings.map((m, idx) => (
<div key={idx} className="flex gap-2 items-center">
<Input
value={m[0]}
onChange={(e) => updateMapping(idx, 0, e.target.value)}
placeholder="From..."
className="h-7 text-xs font-mono flex-1"
/>
<span className="text-xs text-muted-foreground">{"->"}</span>
<Input
value={m[1]}
onChange={(e) => updateMapping(idx, 1, e.target.value)}
placeholder="To..."
className="h-7 text-xs font-mono flex-1"
/>
<button onClick={() => removeMapping(idx)} className="text-muted-foreground hover:text-destructive p-0.5">
<IconTrash size={14} />
</button>
</div>
))}
</div>
)}
<div className="flex items-center gap-3">
<label className="flex items-center gap-1.5 cursor-pointer text-xs">
<Checkbox checked={rule.multi_extension} onCheckedChange={(c) => update({ multi_extension: !!c })} />
Multi-extension (e.g. .tar.gz)
</label>
<button onClick={addMapping} className="flex items-center gap-1 text-xs text-muted-foreground hover:text-foreground ml-auto">
<IconPlus size={14} /> Mapping
</button>
</div>
</div>
);
}