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,116 @@
use serde::{Deserialize, Serialize};
use crate::{RenameContext, RenameRule};
#[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq)]
pub enum SwapOccurrence {
#[default]
First,
Last,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SwapRule {
pub delimiter: String,
pub occurrence: SwapOccurrence,
#[serde(default)]
pub new_delimiter: Option<String>,
pub enabled: bool,
}
impl SwapRule {
pub fn new() -> Self {
Self {
delimiter: ", ".into(),
occurrence: SwapOccurrence::First,
new_delimiter: None,
enabled: true,
}
}
}
impl RenameRule for SwapRule {
fn apply(&self, filename: &str, _context: &RenameContext) -> String {
if self.delimiter.is_empty() {
return filename.to_string();
}
let pos = match self.occurrence {
SwapOccurrence::First => filename.find(&self.delimiter),
SwapOccurrence::Last => filename.rfind(&self.delimiter),
};
let pos = match pos {
Some(p) => p,
None => return filename.to_string(),
};
let left = &filename[..pos];
let right = &filename[pos + self.delimiter.len()..];
let joiner = self.new_delimiter.as_deref().unwrap_or(&self.delimiter);
format!("{}{}{}", right, joiner, left)
}
fn display_name(&self) -> &str {
"Swap"
}
fn rule_type(&self) -> &str {
"swap"
}
fn is_enabled(&self) -> bool {
self.enabled
}
}
#[cfg(test)]
mod tests {
use super::*;
fn ctx() -> RenameContext {
RenameContext::dummy(0)
}
#[test]
fn swap_comma_space() {
let rule = SwapRule::new();
assert_eq!(rule.apply("LastName, FirstName", &ctx()), "FirstName, LastName");
}
#[test]
fn swap_with_new_delimiter() {
let rule = SwapRule {
new_delimiter: Some(" ".into()),
..SwapRule::new()
};
assert_eq!(rule.apply("LastName, FirstName", &ctx()), "FirstName LastName");
}
#[test]
fn swap_first_occurrence() {
let rule = SwapRule {
delimiter: "-".into(),
occurrence: SwapOccurrence::First,
..SwapRule::new()
};
assert_eq!(rule.apply("a-b-c", &ctx()), "b-c-a");
}
#[test]
fn swap_last_occurrence() {
let rule = SwapRule {
delimiter: "-".into(),
occurrence: SwapOccurrence::Last,
..SwapRule::new()
};
assert_eq!(rule.apply("a-b-c", &ctx()), "c-a-b");
}
#[test]
fn no_delimiter_found() {
let rule = SwapRule::new();
assert_eq!(rule.apply("nodelimiter", &ctx()), "nodelimiter");
}
}