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:
128
crates/nomina-app/src/commands/context_menu.rs
Normal file
128
crates/nomina-app/src/commands/context_menu.rs
Normal file
@@ -0,0 +1,128 @@
|
||||
use std::path::PathBuf;
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
use winreg::enums::*;
|
||||
#[cfg(target_os = "windows")]
|
||||
use winreg::RegKey;
|
||||
|
||||
#[tauri::command]
|
||||
pub fn get_launch_args() -> Vec<String> {
|
||||
std::env::args().skip(1).collect()
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn register_context_menu() -> Result<(), String> {
|
||||
#[cfg(target_os = "windows")]
|
||||
{
|
||||
let exe = std::env::current_exe()
|
||||
.map_err(|e| format!("Failed to get exe path: {}", e))?;
|
||||
let exe_str = exe.to_string_lossy().to_string();
|
||||
let icon = format!("{},0", exe_str);
|
||||
|
||||
let hkcu = RegKey::predef(HKEY_CURRENT_USER);
|
||||
let base = r"Software\Classes";
|
||||
|
||||
// Context menu for files: *\shell\Nomina
|
||||
let (key, _) = hkcu
|
||||
.create_subkey(format!(r"{}\*\shell\Nomina", base))
|
||||
.map_err(|e| format!("Registry error: {}", e))?;
|
||||
key.set_value("", &"Edit in Nomina")
|
||||
.map_err(|e| format!("Registry error: {}", e))?;
|
||||
key.set_value("Icon", &icon)
|
||||
.map_err(|e| format!("Registry error: {}", e))?;
|
||||
|
||||
let (cmd, _) = hkcu
|
||||
.create_subkey(format!(r"{}\*\shell\Nomina\command", base))
|
||||
.map_err(|e| format!("Registry error: {}", e))?;
|
||||
cmd.set_value("", &format!("\"{}\" \"%1\"", exe_str))
|
||||
.map_err(|e| format!("Registry error: {}", e))?;
|
||||
|
||||
// Context menu for folders: Directory\shell\Nomina
|
||||
let (key, _) = hkcu
|
||||
.create_subkey(format!(r"{}\Directory\shell\Nomina", base))
|
||||
.map_err(|e| format!("Registry error: {}", e))?;
|
||||
key.set_value("", &"Edit in Nomina")
|
||||
.map_err(|e| format!("Registry error: {}", e))?;
|
||||
key.set_value("Icon", &icon)
|
||||
.map_err(|e| format!("Registry error: {}", e))?;
|
||||
|
||||
let (cmd, _) = hkcu
|
||||
.create_subkey(format!(r"{}\Directory\shell\Nomina\command", base))
|
||||
.map_err(|e| format!("Registry error: {}", e))?;
|
||||
cmd.set_value("", &format!("\"{}\" \"%1\"", exe_str))
|
||||
.map_err(|e| format!("Registry error: {}", e))?;
|
||||
|
||||
// Context menu for folder background: Directory\Background\shell\Nomina
|
||||
let (key, _) = hkcu
|
||||
.create_subkey(format!(r"{}\Directory\Background\shell\Nomina", base))
|
||||
.map_err(|e| format!("Registry error: {}", e))?;
|
||||
key.set_value("", &"Edit in Nomina")
|
||||
.map_err(|e| format!("Registry error: {}", e))?;
|
||||
key.set_value("Icon", &icon)
|
||||
.map_err(|e| format!("Registry error: {}", e))?;
|
||||
|
||||
let (cmd, _) = hkcu
|
||||
.create_subkey(format!(r"{}\Directory\Background\shell\Nomina\command", base))
|
||||
.map_err(|e| format!("Registry error: {}", e))?;
|
||||
// %V gives the current folder path when right-clicking background
|
||||
cmd.set_value("", &format!("\"{}\" \"%V\"", exe_str))
|
||||
.map_err(|e| format!("Registry error: {}", e))?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
Err("Context menu registration is only supported on Windows".to_string())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn unregister_context_menu() -> Result<(), String> {
|
||||
#[cfg(target_os = "windows")]
|
||||
{
|
||||
let hkcu = RegKey::predef(HKEY_CURRENT_USER);
|
||||
let base = r"Software\Classes";
|
||||
|
||||
let _ = hkcu.delete_subkey_all(format!(r"{}\*\shell\Nomina", base));
|
||||
let _ = hkcu.delete_subkey_all(format!(r"{}\Directory\shell\Nomina", base));
|
||||
let _ = hkcu.delete_subkey_all(format!(r"{}\Directory\Background\shell\Nomina", base));
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
Err("Context menu registration is only supported on Windows".to_string())
|
||||
}
|
||||
|
||||
/// Given CLI args (paths), figure out what folder to open and which files to select.
|
||||
/// Returns (folder_path, selected_file_paths).
|
||||
#[tauri::command]
|
||||
pub fn resolve_launch_paths(args: Vec<String>) -> Option<(String, Vec<String>)> {
|
||||
if args.is_empty() {
|
||||
return None;
|
||||
}
|
||||
|
||||
let paths: Vec<PathBuf> = args.iter().map(PathBuf::from).filter(|p| p.exists()).collect();
|
||||
if paths.is_empty() {
|
||||
return None;
|
||||
}
|
||||
|
||||
// If single directory passed (from background click or folder right-click), open it
|
||||
if paths.len() == 1 && paths[0].is_dir() {
|
||||
let folder = paths[0].to_string_lossy().to_string();
|
||||
return Some((folder, vec![]));
|
||||
}
|
||||
|
||||
// For files (or mix), use the parent of the first path as the folder
|
||||
// and collect all paths as the selection
|
||||
let folder = paths[0]
|
||||
.parent()
|
||||
.map(|p| p.to_string_lossy().to_string())
|
||||
.unwrap_or_default();
|
||||
|
||||
if folder.is_empty() {
|
||||
return None;
|
||||
}
|
||||
|
||||
let selected: Vec<String> = paths.iter().map(|p| p.to_string_lossy().to_string()).collect();
|
||||
Some((folder, selected))
|
||||
}
|
||||
@@ -48,3 +48,55 @@ pub async fn get_file_metadata(path: String) -> Result<FileEntry, String> {
|
||||
accessed: meta.accessed().ok().map(|t| chrono::DateTime::<chrono::Utc>::from(t)),
|
||||
})
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn reveal_in_explorer(path: String) -> Result<(), String> {
|
||||
let p = PathBuf::from(&path);
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
{
|
||||
if p.is_dir() {
|
||||
std::process::Command::new("explorer")
|
||||
.arg(&path)
|
||||
.spawn()
|
||||
.map_err(|e| e.to_string())?;
|
||||
} else {
|
||||
std::process::Command::new("explorer")
|
||||
.args(["/select,", &path])
|
||||
.spawn()
|
||||
.map_err(|e| e.to_string())?;
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
{
|
||||
if p.is_dir() {
|
||||
std::process::Command::new("open")
|
||||
.arg(&path)
|
||||
.spawn()
|
||||
.map_err(|e| e.to_string())?;
|
||||
} else {
|
||||
std::process::Command::new("open")
|
||||
.args(["-R", &path])
|
||||
.spawn()
|
||||
.map_err(|e| e.to_string())?;
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
{
|
||||
let target = if p.is_dir() {
|
||||
path.clone()
|
||||
} else {
|
||||
p.parent()
|
||||
.map(|p| p.to_string_lossy().to_string())
|
||||
.unwrap_or(path.clone())
|
||||
};
|
||||
std::process::Command::new("xdg-open")
|
||||
.arg(&target)
|
||||
.spawn()
|
||||
.map_err(|e| e.to_string())?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -2,3 +2,5 @@ pub mod files;
|
||||
pub mod rename;
|
||||
pub mod presets;
|
||||
pub mod undo;
|
||||
pub mod context_menu;
|
||||
pub mod updates;
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
use std::path::PathBuf;
|
||||
|
||||
use nomina_core::bru;
|
||||
use nomina_core::preset::NominaPreset;
|
||||
|
||||
#[tauri::command]
|
||||
@@ -19,6 +20,11 @@ pub async fn load_preset(path: String) -> Result<NominaPreset, String> {
|
||||
NominaPreset::load(&PathBuf::from(path)).map_err(|e| e.to_string())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn delete_preset(path: String) -> Result<(), String> {
|
||||
std::fs::remove_file(&path).map_err(|e| e.to_string())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn list_presets() -> Result<Vec<PresetInfo>, String> {
|
||||
let dir = get_presets_dir();
|
||||
@@ -70,3 +76,21 @@ fn sanitize_filename(name: &str) -> String {
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn export_preset(source_path: String, dest_path: String) -> Result<(), String> {
|
||||
std::fs::copy(&source_path, &dest_path).map_err(|e| e.to_string())?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn import_preset(path: String) -> Result<NominaPreset, String> {
|
||||
let p = PathBuf::from(&path);
|
||||
let ext = p.extension().and_then(|e| e.to_str()).unwrap_or("").to_lowercase();
|
||||
|
||||
match ext.as_str() {
|
||||
"nomina" => NominaPreset::load(&p).map_err(|e| e.to_string()),
|
||||
"bru" => bru::parse_bru_file(&p).map_err(|e| e.to_string()),
|
||||
_ => Err(format!("Unsupported preset format: .{}", ext)),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -29,30 +29,62 @@ fn build_rule(cfg: &RuleConfig) -> Option<Box<dyn RenameRule>> {
|
||||
if !cfg.enabled {
|
||||
return None;
|
||||
}
|
||||
let val = &cfg.config;
|
||||
// Re-inject `enabled` - #[serde(flatten)] on RuleConfig consumes it
|
||||
// before individual rule structs can see it
|
||||
let mut val = cfg.config.clone();
|
||||
if let serde_json::Value::Object(ref mut map) = val {
|
||||
map.insert("enabled".to_string(), serde_json::Value::Bool(cfg.enabled));
|
||||
}
|
||||
match cfg.rule_type.as_str() {
|
||||
"replace" => serde_json::from_value::<rules::ReplaceRule>(val.clone())
|
||||
"replace" => serde_json::from_value::<rules::ReplaceRule>(val)
|
||||
.ok()
|
||||
.map(|r| Box::new(r) as Box<dyn RenameRule>),
|
||||
"regex" => serde_json::from_value::<rules::RegexRule>(val.clone())
|
||||
"regex" => serde_json::from_value::<rules::RegexRule>(val)
|
||||
.ok()
|
||||
.map(|r| Box::new(r) as Box<dyn RenameRule>),
|
||||
"remove" => serde_json::from_value::<rules::RemoveRule>(val.clone())
|
||||
"remove" => serde_json::from_value::<rules::RemoveRule>(val)
|
||||
.ok()
|
||||
.map(|r| Box::new(r) as Box<dyn RenameRule>),
|
||||
"add" => serde_json::from_value::<rules::AddRule>(val.clone())
|
||||
"add" => serde_json::from_value::<rules::AddRule>(val)
|
||||
.ok()
|
||||
.map(|r| Box::new(r) as Box<dyn RenameRule>),
|
||||
"case" => serde_json::from_value::<rules::CaseRule>(val.clone())
|
||||
"case" => serde_json::from_value::<rules::CaseRule>(val)
|
||||
.ok()
|
||||
.map(|r| Box::new(r) as Box<dyn RenameRule>),
|
||||
"numbering" => serde_json::from_value::<rules::NumberingRule>(val.clone())
|
||||
"numbering" => serde_json::from_value::<rules::NumberingRule>(val)
|
||||
.ok()
|
||||
.map(|r| Box::new(r) as Box<dyn RenameRule>),
|
||||
"date" => serde_json::from_value::<rules::DateRule>(val.clone())
|
||||
"date" => serde_json::from_value::<rules::DateRule>(val)
|
||||
.ok()
|
||||
.map(|r| Box::new(r) as Box<dyn RenameRule>),
|
||||
"move_parts" => serde_json::from_value::<rules::MovePartsRule>(val.clone())
|
||||
"move_parts" => serde_json::from_value::<rules::MovePartsRule>(val)
|
||||
.ok()
|
||||
.map(|r| Box::new(r) as Box<dyn RenameRule>),
|
||||
"text_editor" => serde_json::from_value::<rules::TextEditorRule>(val)
|
||||
.ok()
|
||||
.map(|r| Box::new(r) as Box<dyn RenameRule>),
|
||||
"hash" => serde_json::from_value::<rules::HashRule>(val)
|
||||
.ok()
|
||||
.map(|r| Box::new(r) as Box<dyn RenameRule>),
|
||||
"folder_name" => serde_json::from_value::<rules::FolderNameRule>(val)
|
||||
.ok()
|
||||
.map(|r| Box::new(r) as Box<dyn RenameRule>),
|
||||
"transliterate" => serde_json::from_value::<rules::TransliterateRule>(val)
|
||||
.ok()
|
||||
.map(|r| Box::new(r) as Box<dyn RenameRule>),
|
||||
"padding" => serde_json::from_value::<rules::PaddingRule>(val)
|
||||
.ok()
|
||||
.map(|r| Box::new(r) as Box<dyn RenameRule>),
|
||||
"truncate" => serde_json::from_value::<rules::TruncateRule>(val)
|
||||
.ok()
|
||||
.map(|r| Box::new(r) as Box<dyn RenameRule>),
|
||||
"randomize" => serde_json::from_value::<rules::RandomizeRule>(val)
|
||||
.ok()
|
||||
.map(|r| Box::new(r) as Box<dyn RenameRule>),
|
||||
"swap" => serde_json::from_value::<rules::SwapRule>(val)
|
||||
.ok()
|
||||
.map(|r| Box::new(r) as Box<dyn RenameRule>),
|
||||
"sanitize" => serde_json::from_value::<rules::SanitizeRule>(val)
|
||||
.ok()
|
||||
.map(|r| Box::new(r) as Box<dyn RenameRule>),
|
||||
_ => None,
|
||||
@@ -64,12 +96,20 @@ pub async fn preview_rename(
|
||||
rules: Vec<RuleConfig>,
|
||||
directory: String,
|
||||
filters: Option<FilterConfig>,
|
||||
selected_paths: Option<Vec<String>>,
|
||||
) -> Result<Vec<PreviewResult>, String> {
|
||||
let scanner = FileScanner::new(
|
||||
PathBuf::from(&directory),
|
||||
filters.unwrap_or_default(),
|
||||
);
|
||||
let files = scanner.scan();
|
||||
let all_files = scanner.scan();
|
||||
|
||||
let files = if let Some(ref paths) = selected_paths {
|
||||
let file_map: std::collections::HashMap<PathBuf, _> = all_files.into_iter().map(|f| (f.path.clone(), f)).collect();
|
||||
paths.iter().filter_map(|p| file_map.get(&PathBuf::from(p)).cloned()).collect()
|
||||
} else {
|
||||
all_files
|
||||
};
|
||||
|
||||
let mut pipeline = Pipeline::new();
|
||||
for cfg in &rules {
|
||||
@@ -77,7 +117,11 @@ pub async fn preview_rename(
|
||||
pipeline.add_step(rule, cfg.step_mode.clone());
|
||||
}
|
||||
if cfg.rule_type == "extension" {
|
||||
if let Ok(ext_rule) = serde_json::from_value(cfg.config.clone()) {
|
||||
let mut ext_val = cfg.config.clone();
|
||||
if let serde_json::Value::Object(ref mut map) = ext_val {
|
||||
map.insert("enabled".to_string(), serde_json::Value::Bool(cfg.enabled));
|
||||
}
|
||||
if let Ok(ext_rule) = serde_json::from_value(ext_val) {
|
||||
pipeline.extension_rule = Some(ext_rule);
|
||||
}
|
||||
}
|
||||
@@ -87,12 +131,40 @@ pub async fn preview_rename(
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn execute_rename(operations: Vec<PreviewResult>) -> Result<RenameReport, String> {
|
||||
let valid: Vec<&PreviewResult> = operations
|
||||
pub async fn execute_rename(
|
||||
operations: Vec<PreviewResult>,
|
||||
create_backup: Option<bool>,
|
||||
undo_limit: Option<usize>,
|
||||
skip_read_only: Option<bool>,
|
||||
conflict_strategy: Option<String>,
|
||||
backup_path: Option<String>,
|
||||
) -> Result<RenameReport, String> {
|
||||
let skip_ro = skip_read_only.unwrap_or(true);
|
||||
let strategy = conflict_strategy.unwrap_or_else(|| "suffix".to_string());
|
||||
|
||||
let mut valid: Vec<&PreviewResult> = operations
|
||||
.iter()
|
||||
.filter(|op| !op.has_conflict && !op.has_error && op.original_name != op.new_name)
|
||||
.collect();
|
||||
|
||||
// skip read-only files if enabled
|
||||
if skip_ro {
|
||||
valid.retain(|op| {
|
||||
if let Ok(meta) = std::fs::metadata(&op.original_path) {
|
||||
!meta.permissions().readonly()
|
||||
} else {
|
||||
true
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// sort deepest paths first so children are renamed before parents
|
||||
valid.sort_by(|a, b| {
|
||||
let depth_a = a.original_path.components().count();
|
||||
let depth_b = b.original_path.components().count();
|
||||
depth_b.cmp(&depth_a)
|
||||
});
|
||||
|
||||
// validation pass
|
||||
for op in &valid {
|
||||
if !op.original_path.exists() {
|
||||
@@ -100,6 +172,32 @@ pub async fn execute_rename(operations: Vec<PreviewResult>) -> Result<RenameRepo
|
||||
}
|
||||
}
|
||||
|
||||
// create backups if requested
|
||||
if create_backup.unwrap_or(false) && !valid.is_empty() {
|
||||
let backup_dir = if let Some(ref bp) = backup_path {
|
||||
if !bp.is_empty() {
|
||||
PathBuf::from(bp)
|
||||
} else {
|
||||
let first_parent = valid[0].original_path.parent().unwrap();
|
||||
first_parent.join("_nomina_backup")
|
||||
}
|
||||
} else {
|
||||
let first_parent = valid[0].original_path.parent().unwrap();
|
||||
first_parent.join("_nomina_backup")
|
||||
};
|
||||
std::fs::create_dir_all(&backup_dir)
|
||||
.map_err(|e| format!("Failed to create backup directory: {}", e))?;
|
||||
|
||||
for op in &valid {
|
||||
if op.original_path.is_file() {
|
||||
let dest = backup_dir.join(&op.original_name);
|
||||
if let Err(e) = std::fs::copy(&op.original_path, &dest) {
|
||||
return Err(format!("Failed to backup {}: {}", op.original_name, e));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let mut succeeded = 0;
|
||||
let mut failed = Vec::new();
|
||||
let mut undo_entries = Vec::new();
|
||||
@@ -108,11 +206,17 @@ pub async fn execute_rename(operations: Vec<PreviewResult>) -> Result<RenameRepo
|
||||
let parent = op.original_path.parent().unwrap();
|
||||
let new_path = parent.join(&op.new_name);
|
||||
|
||||
// if target exists and isn't another file we're renaming, use temp
|
||||
let needs_temp = new_path.exists()
|
||||
// if target exists and isn't another file we're renaming
|
||||
let target_conflict = new_path.exists()
|
||||
&& !valid.iter().any(|other| other.original_path == new_path);
|
||||
|
||||
let result = if needs_temp {
|
||||
if target_conflict && strategy == "skip" {
|
||||
failed.push(format!("{}: target already exists (skipped)", op.original_name));
|
||||
continue;
|
||||
}
|
||||
|
||||
let result = if target_conflict {
|
||||
// suffix strategy - use temp rename
|
||||
let tmp_name = format!("__nomina_tmp_{}", uuid::Uuid::new_v4());
|
||||
let tmp_path = parent.join(&tmp_name);
|
||||
std::fs::rename(&op.original_path, &tmp_path)
|
||||
@@ -145,7 +249,8 @@ pub async fn execute_rename(operations: Vec<PreviewResult>) -> Result<RenameRepo
|
||||
|
||||
let undo_path = get_undo_log_path();
|
||||
let mut log = UndoLog::load(&undo_path).unwrap_or_else(|_| UndoLog::new());
|
||||
log.add_batch(batch.clone());
|
||||
let max = undo_limit.unwrap_or(nomina_core::undo::DEFAULT_MAX_UNDO_BATCHES);
|
||||
log.add_batch_with_limit(batch.clone(), max);
|
||||
let _ = log.save(&undo_path);
|
||||
|
||||
Ok(RenameReport {
|
||||
|
||||
75
crates/nomina-app/src/commands/updates.rs
Normal file
75
crates/nomina-app/src/commands/updates.rs
Normal file
@@ -0,0 +1,75 @@
|
||||
use serde::Serialize;
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
pub struct UpdateInfo {
|
||||
pub available: bool,
|
||||
pub current_version: String,
|
||||
pub latest_version: String,
|
||||
pub url: String,
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn check_for_updates() -> Result<UpdateInfo, String> {
|
||||
let current = env!("CARGO_PKG_VERSION").to_string();
|
||||
let api_url = "https://git.lashman.live/api/v1/repos/lashman/nomina/releases?limit=1";
|
||||
|
||||
let client = reqwest::Client::builder()
|
||||
.timeout(std::time::Duration::from_secs(10))
|
||||
.build()
|
||||
.map_err(|e| e.to_string())?;
|
||||
|
||||
let resp = client
|
||||
.get(api_url)
|
||||
.header("Accept", "application/json")
|
||||
.send()
|
||||
.await
|
||||
.map_err(|e| format!("Network error: {}", e))?;
|
||||
|
||||
if !resp.status().is_success() {
|
||||
return Err(format!("Server returned {}", resp.status()));
|
||||
}
|
||||
|
||||
let releases: Vec<serde_json::Value> = resp
|
||||
.json()
|
||||
.await
|
||||
.map_err(|e| format!("Parse error: {}", e))?;
|
||||
|
||||
let latest = releases.first().ok_or("No releases found")?;
|
||||
let tag = latest["tag_name"]
|
||||
.as_str()
|
||||
.unwrap_or("0.0.0")
|
||||
.trim_start_matches('v')
|
||||
.to_string();
|
||||
let url = latest["html_url"]
|
||||
.as_str()
|
||||
.unwrap_or("")
|
||||
.to_string();
|
||||
|
||||
let available = version_newer(&tag, ¤t);
|
||||
|
||||
Ok(UpdateInfo {
|
||||
available,
|
||||
current_version: current,
|
||||
latest_version: tag,
|
||||
url,
|
||||
})
|
||||
}
|
||||
|
||||
fn version_newer(latest: &str, current: &str) -> bool {
|
||||
let parse = |s: &str| -> Vec<u32> {
|
||||
s.split('.').filter_map(|p| p.parse().ok()).collect()
|
||||
};
|
||||
let l = parse(latest);
|
||||
let c = parse(current);
|
||||
for i in 0..l.len().max(c.len()) {
|
||||
let lv = l.get(i).copied().unwrap_or(0);
|
||||
let cv = c.get(i).copied().unwrap_or(0);
|
||||
if lv > cv {
|
||||
return true;
|
||||
}
|
||||
if lv < cv {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
false
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
|
||||
#![windows_subsystem = "windows"]
|
||||
|
||||
mod commands;
|
||||
|
||||
@@ -9,15 +9,24 @@ fn main() {
|
||||
.invoke_handler(tauri::generate_handler![
|
||||
commands::files::scan_directory,
|
||||
commands::files::get_file_metadata,
|
||||
commands::files::reveal_in_explorer,
|
||||
commands::rename::preview_rename,
|
||||
commands::rename::execute_rename,
|
||||
commands::presets::save_preset,
|
||||
commands::presets::load_preset,
|
||||
commands::presets::list_presets,
|
||||
commands::presets::delete_preset,
|
||||
commands::presets::export_preset,
|
||||
commands::presets::import_preset,
|
||||
commands::undo::undo_last,
|
||||
commands::undo::undo_batch,
|
||||
commands::undo::get_undo_history,
|
||||
commands::undo::clear_undo_history,
|
||||
commands::context_menu::get_launch_args,
|
||||
commands::context_menu::register_context_menu,
|
||||
commands::context_menu::unregister_context_menu,
|
||||
commands::context_menu::resolve_launch_paths,
|
||||
commands::updates::check_for_updates,
|
||||
])
|
||||
.run(tauri::generate_context!())
|
||||
.expect("error running nomina");
|
||||
|
||||
Reference in New Issue
Block a user