Naming scheme editor: clickable field tokens + persist across restarts
Two usability fixes to the naming-scheme editor, both reported/requested
directly: the field-reference list ({show name}, {episode number}, etc.)
was plain unclickable text, so building a scheme meant typing every field
name out by hand; and the editor's text always reset to the built-in
sample expression on every launch, since nothing ever wrote it to disk -
only the explicit 1-9 preset slots (presets.rs) were persisted.
Field list -> clickable buttons: each field reference is now a real
<button> (.format-field-chip) wired to insertField(), which reads the
input's real DOM selectionStart/selectionEnd and inserts the token at
the caret, then restores focus and moves the caret to just after the
inserted text so clicking several fields in a row builds up
left-to-right. Purely a frontend change - no new Tauri commands needed.
Persistence: added last_naming_scheme to settings.rs's StoredSettings
(same settings.json already used for tv/movie source, credentials,
etc.), plus get_last_naming_scheme/save_last_naming_scheme commands.
RenamePanel.tsx loads it once on mount (restoring the saved scheme
before the user can interact with anything) and saves it back debounced
(600ms after typing settles) as the user edits - gated behind a
schemeLoaded flag so the save effect can never fire before the load
completes and clobber a saved scheme with the not-yet-replaced sample
default. An empty/blank saved value (fresh install) leaves the built-in
sample expression in place, same fallback behavior as every other
optional setting in this file.
Verified via cargo test --workspace (191 -> 193, two new settings.rs
tests: schema-evolution for the new field, and the blank-clears-to-None
behavior) + npm run build (clean) + a 7-check Playwright script against
npm run dev (mocking window.__TAURI_INTERNALS__.invoke): confirmed the
editor restores a mocked saved scheme on launch, clicking a field chip
inserts its token at a caret placed via setSelectionRange (not just
appended), a second click lands immediately after the first rather than
at the old position, and save_last_naming_scheme fires once (debounced)
with the exact edited text rather than on every keystroke.
Updated README's naming-scheme paragraph to describe both.
This commit is contained in:
@@ -119,6 +119,8 @@ pub fn run() {
|
||||
settings::get_opensubtitles_username,
|
||||
settings::has_opensubtitles_password,
|
||||
settings::save_opensubtitles_login,
|
||||
settings::get_last_naming_scheme,
|
||||
settings::save_last_naming_scheme,
|
||||
subtitles::search_subtitles,
|
||||
subtitles::download_subtitle,
|
||||
episodes::browse_episodes,
|
||||
|
||||
@@ -83,6 +83,13 @@ struct StoredSettings {
|
||||
/// [`has_opensubtitles_password`], which only reports whether one is set.
|
||||
#[serde(default)]
|
||||
opensubtitles_password: Option<String>,
|
||||
/// The naming-scheme expression the Rename panel's editor last held - saved on
|
||||
/// every edit (debounced on the frontend) and restored on startup, so a custom
|
||||
/// scheme survives closing and reopening the app instead of resetting to the
|
||||
/// built-in sample expression every time. Distinct from `presets.rs`'s 1-9 saved
|
||||
/// slots: this is the *current* scheme, not an explicitly named save.
|
||||
#[serde(default)]
|
||||
last_naming_scheme: Option<String>,
|
||||
}
|
||||
|
||||
fn settings_path(app: &AppHandle) -> Result<PathBuf, String> {
|
||||
@@ -294,6 +301,32 @@ pub fn save_opensubtitles_login(app: AppHandle, username: String, password: Stri
|
||||
save(&app, &settings)
|
||||
}
|
||||
|
||||
/// Returns the last-saved naming-scheme expression, or `None` if nothing (or only
|
||||
/// blank text) has been saved yet - the frontend falls back to its own built-in
|
||||
/// sample expression in that case.
|
||||
pub fn last_naming_scheme(app: &AppHandle) -> Option<String> {
|
||||
load(app).last_naming_scheme.filter(|t| !t.trim().is_empty())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn get_last_naming_scheme(app: AppHandle) -> Option<String> {
|
||||
last_naming_scheme(&app)
|
||||
}
|
||||
|
||||
/// Saves the naming-scheme editor's current text so it survives an app restart. The
|
||||
/// frontend debounces calls to this while the user is actively typing (see
|
||||
/// `RenamePanel.tsx`) rather than saving on every keystroke. Unlike
|
||||
/// `presets::save_preset`, an empty expression is accepted here (it just clears the
|
||||
/// saved value, same as every other credential field in this module) rather than
|
||||
/// rejected - the editor's text field can legitimately be emptied while editing.
|
||||
#[tauri::command]
|
||||
pub fn save_last_naming_scheme(app: AppHandle, expression: String) -> Result<(), String> {
|
||||
let mut settings = load(&app);
|
||||
let trimmed = expression.trim().to_string();
|
||||
settings.last_naming_scheme = if trimmed.is_empty() { None } else { Some(expression) };
|
||||
save(&app, &settings)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
@@ -372,6 +405,37 @@ mod tests {
|
||||
assert_eq!(parsed.opensubtitles_password, None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn stored_settings_without_a_last_naming_scheme_field_still_deserializes() {
|
||||
// Same schema-evolution guarantee as the OpenSubtitles test above, for the
|
||||
// field this naming-scheme-persistence fix added.
|
||||
let old_json = r#"{"tmdb_token":"abc123"}"#;
|
||||
let parsed: StoredSettings = serde_json::from_str(old_json).unwrap();
|
||||
assert_eq!(parsed.last_naming_scheme, None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn last_naming_scheme_round_trips_and_ignores_pure_whitespace() {
|
||||
// Pure merge logic (load/save both need a real AppHandle, exercised end to end
|
||||
// by the Playwright pass instead - same reasoning as every other credential
|
||||
// test in this file): a saved non-blank expression comes back unchanged,
|
||||
// preserving internal spacing exactly (naming schemes can have meaningful
|
||||
// literal spaces, e.g. " - " between fields), and a whitespace-only save reads
|
||||
// back as unset, matching the empty-string behavior of every other field here.
|
||||
let mut settings = StoredSettings::default();
|
||||
settings.last_naming_scheme = Some("{show name} - {episode name}".to_string());
|
||||
assert_eq!(
|
||||
settings.last_naming_scheme.as_deref().filter(|t| !t.trim().is_empty()),
|
||||
Some("{show name} - {episode name}")
|
||||
);
|
||||
|
||||
settings.last_naming_scheme = Some(" ".to_string());
|
||||
assert_eq!(
|
||||
settings.last_naming_scheme.as_deref().filter(|t| !t.trim().is_empty()),
|
||||
None
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn merge_login_update_keeps_the_other_field_when_one_is_blank() {
|
||||
// Updating just the password shouldn't silently clear a previously-saved
|
||||
|
||||
Reference in New Issue
Block a user