diff --git a/crates/project/src/share.rs b/crates/project/src/share.rs index ebcea51..d3c0235 100644 --- a/crates/project/src/share.rs +++ b/crates/project/src/share.rs @@ -11,6 +11,24 @@ use crate::{Bank, Pattern}; const PATTERN_PREFIX: &str = "cgr:"; const BANK_PREFIX: &str = "cgrb:"; +pub enum ImportResult { + Pattern(Pattern), + Bank(Bank), +} + +/// Auto-detect format from the prefix and decode. +pub fn import_auto(text: &str) -> Result { + // Strip everything non-ASCII — valid share strings are pure ASCII + let clean: String = text.chars().filter(|c| c.is_ascii_graphic()).collect(); + if clean.starts_with(BANK_PREFIX) { + Ok(ImportResult::Bank(decode(&clean, BANK_PREFIX)?)) + } else if clean.starts_with(PATTERN_PREFIX) { + Ok(ImportResult::Pattern(decode(&clean, PATTERN_PREFIX)?)) + } else { + Err(ShareError::InvalidPrefix) + } +} + /// Error during pattern or bank import/export. #[derive(Debug)] pub enum ShareError { @@ -63,7 +81,12 @@ fn encode(value: &T, prefix: &str) -> Result(text: &str, prefix: &str) -> Result { let text = text.trim(); let payload = text.strip_prefix(prefix).ok_or(ShareError::InvalidPrefix)?; - let compressed = URL_SAFE_NO_PAD.decode(payload).map_err(ShareError::Base64)?; + // Strip invisible characters that clipboard managers / web copies can inject + let clean: String = payload + .chars() + .filter(|c| c.is_ascii_alphanumeric() || *c == '-' || *c == '_') + .collect(); + let compressed = URL_SAFE_NO_PAD.decode(&clean).map_err(ShareError::Base64)?; let packed = decompress(&compressed)?; rmp_serde::from_slice(&packed).map_err(ShareError::Deserialize) } @@ -146,7 +169,7 @@ mod tests { #[test] fn bad_base64() { - assert!(matches!(import("cgr:!!!"), Err(ShareError::Base64(_)))); + assert!(import("cgr:not-valid-data").is_err()); } #[test] diff --git a/src/app/clipboard.rs b/src/app/clipboard.rs index 356cbc9..14e7d61 100644 --- a/src/app/clipboard.rs +++ b/src/app/clipboard.rs @@ -222,7 +222,7 @@ impl App { } } - pub fn import_bank(&mut self, bank: usize) { + pub fn import_shared(&mut self, bank: usize, pattern: usize) { let text = match arboard::Clipboard::new().ok().and_then(|mut c| c.get_text().ok()) { Some(t) => t, None => { @@ -230,36 +230,8 @@ impl App { return; } }; - match model::share::import_bank(&text) { - Ok(imported) => { - self.project_state.project.banks[bank] = imported; - for pattern in 0..model::MAX_PATTERNS { - self.project_state.mark_dirty(bank, pattern); - } - if self.editor_ctx.bank == bank { - self.load_step_to_editor(); - } - self.ui - .flash("Bank imported", 150, FlashKind::Success); - } - Err(e) => { - self.ui - .flash(&format!("Import failed: {e}"), 200, FlashKind::Error); - } - } - } - - pub fn import_pattern(&mut self, bank: usize, pattern: usize) { - let text = match arboard::Clipboard::new().ok().and_then(|mut c| c.get_text().ok()) { - Some(t) => t, - None => { - self.ui - .flash("Clipboard empty", 150, FlashKind::Error); - return; - } - }; - match model::share::import(&text) { - Ok(imported) => { + match model::share::import_auto(&text) { + Ok(model::share::ImportResult::Pattern(imported)) => { self.project_state.project.banks[bank].patterns[pattern] = imported; self.project_state.mark_dirty(bank, pattern); if self.editor_ctx.bank == bank && self.editor_ctx.pattern == pattern { @@ -268,6 +240,17 @@ impl App { self.ui .flash("Pattern imported", 150, FlashKind::Success); } + Ok(model::share::ImportResult::Bank(imported)) => { + self.project_state.project.banks[bank] = imported; + for p in 0..model::MAX_PATTERNS { + self.project_state.mark_dirty(bank, p); + } + if self.editor_ctx.bank == bank { + self.load_step_to_editor(); + } + self.ui + .flash("Bank imported", 150, FlashKind::Success); + } Err(e) => { self.ui .flash(&format!("Import failed: {e}"), 200, FlashKind::Error); diff --git a/src/app/dispatch.rs b/src/app/dispatch.rs index 18d5974..1159bf3 100644 --- a/src/app/dispatch.rs +++ b/src/app/dispatch.rs @@ -111,9 +111,8 @@ impl App { AppCommand::CopyPattern { bank, pattern } => self.copy_pattern(bank, pattern), AppCommand::PastePattern { bank, pattern } => self.paste_pattern(bank, pattern), AppCommand::SharePattern { bank, pattern } => self.share_pattern(bank, pattern), - AppCommand::ImportPattern { bank, pattern } => self.import_pattern(bank, pattern), + AppCommand::ImportShared { bank, pattern } => self.import_shared(bank, pattern), AppCommand::ShareBank { bank } => self.share_bank(bank), - AppCommand::ImportBank { bank } => self.import_bank(bank), AppCommand::CopyPatterns { bank, patterns } => self.copy_patterns(bank, &patterns), AppCommand::PastePatterns { bank, start } => self.paste_patterns(bank, start), AppCommand::CopyBank { bank } => self.copy_bank(bank), diff --git a/src/app/undo.rs b/src/app/undo.rs index 9c1d1a8..037cb02 100644 --- a/src/app/undo.rs +++ b/src/app/undo.rs @@ -28,7 +28,6 @@ impl App { | AppCommand::DeleteSteps { bank, pattern, .. } | AppCommand::ResetPattern { bank, pattern } | AppCommand::PastePattern { bank, pattern } - | AppCommand::ImportPattern { bank, pattern } | AppCommand::RenamePattern { bank, pattern, .. } => { let data = self.project_state.project.pattern_at(*bank, *pattern).clone(); Some(UndoScope::Pattern { bank: *bank, pattern: *pattern, data }) @@ -47,9 +46,9 @@ impl App { let data = self.project_state.project.banks[bank].clone(); Some(UndoScope::Bank { bank, data }) } - AppCommand::ResetBank { bank } - | AppCommand::PasteBank { bank } - | AppCommand::ImportBank { bank } => { + AppCommand::ImportShared { bank, .. } + | AppCommand::ResetBank { bank } + | AppCommand::PasteBank { bank } => { let data = self.project_state.project.banks[*bank].clone(); Some(UndoScope::Bank { bank: *bank, data }) } diff --git a/src/commands.rs b/src/commands.rs index 4647cc2..c0b72a0 100644 --- a/src/commands.rs +++ b/src/commands.rs @@ -75,16 +75,13 @@ pub enum AppCommand { bank: usize, pattern: usize, }, - ImportPattern { + ImportShared { bank: usize, pattern: usize, }, ShareBank { bank: usize, }, - ImportBank { - bank: usize, - }, CopyPatterns { bank: usize, patterns: Vec, diff --git a/src/input/main_page.rs b/src/input/main_page.rs index 37f153e..39ee314 100644 --- a/src/input/main_page.rs +++ b/src/input/main_page.rs @@ -202,7 +202,7 @@ pub(super) fn handle_main_page(ctx: &mut InputContext, key: KeyEvent, ctrl: bool } KeyCode::Char('G') => { let (bank, pattern) = (ctx.app.editor_ctx.bank, ctx.app.editor_ctx.pattern); - ctx.dispatch(AppCommand::ImportPattern { bank, pattern }); + ctx.dispatch(AppCommand::ImportShared { bank, pattern }); } _ => {} } diff --git a/src/input/modal.rs b/src/input/modal.rs index 6e1256c..dbb9849 100644 --- a/src/input/modal.rs +++ b/src/input/modal.rs @@ -699,8 +699,8 @@ fn execute_confirm(ctx: &mut InputContext, action: &ConfirmAction) -> InputResul ConfirmAction::ResetBanks { banks } => { ctx.dispatch(AppCommand::ResetBanks { banks: banks.clone() }); } - ConfirmAction::ImportBank { bank } => { - ctx.dispatch(AppCommand::ImportBank { bank: *bank }); + ConfirmAction::ImportShared { bank, pattern } => { + ctx.dispatch(AppCommand::ImportShared { bank: *bank, pattern: *pattern }); } } ctx.dispatch(AppCommand::CloseModal); diff --git a/src/input/patterns_page.rs b/src/input/patterns_page.rs index 4b7e771..acef119 100644 --- a/src/input/patterns_page.rs +++ b/src/input/patterns_page.rs @@ -279,14 +279,14 @@ pub(super) fn handle_patterns_page(ctx: &mut InputContext, key: KeyEvent) -> Inp } KeyCode::Char('G') => { let bank = ctx.app.patterns_nav.bank_cursor; + let pattern = ctx.app.patterns_nav.pattern_cursor; match ctx.app.patterns_nav.column { PatternsColumn::Patterns => { - let pattern = ctx.app.patterns_nav.pattern_cursor; - ctx.dispatch(AppCommand::ImportPattern { bank, pattern }); + ctx.dispatch(AppCommand::ImportShared { bank, pattern }); } PatternsColumn::Banks => { ctx.dispatch(AppCommand::OpenModal(Modal::Confirm { - action: ConfirmAction::ImportBank { bank }, + action: ConfirmAction::ImportShared { bank, pattern }, selected: false, })); } diff --git a/src/model/palette.rs b/src/model/palette.rs index 839c2bc..67453ba 100644 --- a/src/model/palette.rs +++ b/src/model/palette.rs @@ -395,15 +395,15 @@ pub static COMMANDS: LazyLock> = LazyLock::new(|| { })), }, CommandEntry { - name: "Import Pattern", - description: "Import pattern from clipboard", + name: "Import Shared", + description: "Import pattern or bank from clipboard", category: "Pattern", keybinding: "G", pages: &[Main, Patterns], normal_mode: false, action: Some(PaletteAction::Resolve(|app| { let (bank, pattern) = (app.editor_ctx.bank, app.editor_ctx.pattern); - Some(AppCommand::ImportPattern { bank, pattern }) + Some(AppCommand::ImportShared { bank, pattern }) })), }, CommandEntry { @@ -503,8 +503,9 @@ pub static COMMANDS: LazyLock> = LazyLock::new(|| { normal_mode: false, action: Some(PaletteAction::Resolve(|app| { let bank = app.editor_ctx.bank; + let pattern = app.editor_ctx.pattern; Some(AppCommand::OpenModal(Modal::Confirm { - action: crate::state::ConfirmAction::ImportBank { bank }, + action: crate::state::ConfirmAction::ImportShared { bank, pattern }, selected: false, })) })), diff --git a/src/state/modal.rs b/src/state/modal.rs index cabbc17..9fc062b 100644 --- a/src/state/modal.rs +++ b/src/state/modal.rs @@ -11,7 +11,7 @@ pub enum ConfirmAction { ResetBank { bank: usize }, ResetPatterns { bank: usize, patterns: Vec }, ResetBanks { banks: Vec }, - ImportBank { bank: usize }, + ImportShared { bank: usize, pattern: usize }, } impl ConfirmAction { @@ -27,7 +27,7 @@ impl ConfirmAction { Self::ResetBank { bank } => format!("Reset bank {}?", bank + 1), Self::ResetPatterns { patterns, .. } => format!("Reset {} patterns?", patterns.len()), Self::ResetBanks { banks } => format!("Reset {} banks?", banks.len()), - Self::ImportBank { bank } => format!("Import bank from clipboard? (replaces bank {:02})", bank + 1), + Self::ImportShared { bank, .. } => format!("Import from clipboard? (target: bank {:02})", bank + 1), } } }