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
99 lines
3.8 KiB
TypeScript
99 lines
3.8 KiB
TypeScript
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>
|
|
);
|
|
}
|