Feat: bank / pattern import / export feature + documentation
Some checks failed
Deploy Website / deploy (push) Failing after 31s
Some checks failed
Deploy Website / deploy (push) Failing after 31s
This commit is contained in:
@@ -168,6 +168,111 @@ impl App {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn share_pattern(&mut self, bank: usize, pattern: usize) {
|
||||
let pattern_data = &self.project_state.project.banks[bank].patterns[pattern];
|
||||
match model::share::export(pattern_data) {
|
||||
Ok(encoded) => {
|
||||
let len = encoded.len();
|
||||
if let Some(clip) = &mut self.clipboard {
|
||||
let _ = clip.set_text(encoded);
|
||||
}
|
||||
if len > 2000 {
|
||||
self.ui.flash(
|
||||
&format!("Shared ({len} chars — too long for Discord)"),
|
||||
200,
|
||||
FlashKind::Error,
|
||||
);
|
||||
} else {
|
||||
self.ui
|
||||
.flash(&format!("Shared ({len} chars)"), 150, FlashKind::Success);
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
self.ui
|
||||
.flash(&format!("Share failed: {e}"), 200, FlashKind::Error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn share_bank(&mut self, bank: usize) {
|
||||
let bank_data = &self.project_state.project.banks[bank];
|
||||
match model::share::export_bank(bank_data) {
|
||||
Ok(encoded) => {
|
||||
let len = encoded.len();
|
||||
if let Some(clip) = &mut self.clipboard {
|
||||
let _ = clip.set_text(encoded);
|
||||
}
|
||||
if len > 2000 {
|
||||
self.ui.flash(
|
||||
&format!("Bank shared ({len} chars — too long for Discord)"),
|
||||
200,
|
||||
FlashKind::Error,
|
||||
);
|
||||
} else {
|
||||
self.ui
|
||||
.flash(&format!("Bank shared ({len} chars)"), 150, FlashKind::Success);
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
self.ui
|
||||
.flash(&format!("Share failed: {e}"), 200, FlashKind::Error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn import_bank(&mut self, bank: usize) {
|
||||
let text = match self.clipboard.as_mut().and_then(|c| c.get_text().ok()) {
|
||||
Some(t) => t,
|
||||
None => {
|
||||
self.ui.flash("Clipboard empty", 150, FlashKind::Error);
|
||||
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 self.clipboard.as_mut().and_then(|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) => {
|
||||
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 {
|
||||
self.load_step_to_editor();
|
||||
}
|
||||
self.ui
|
||||
.flash("Pattern imported", 150, FlashKind::Success);
|
||||
}
|
||||
Err(e) => {
|
||||
self.ui
|
||||
.flash(&format!("Import failed: {e}"), 200, FlashKind::Error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn harden_steps(&mut self) {
|
||||
let (bank, pattern) = self.current_bank_pattern();
|
||||
let indices = self.selected_steps();
|
||||
|
||||
@@ -105,6 +105,10 @@ impl App {
|
||||
AppCommand::ResetBank { bank } => self.reset_bank(bank),
|
||||
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::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),
|
||||
|
||||
@@ -28,6 +28,7 @@ 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 })
|
||||
@@ -41,7 +42,9 @@ impl App {
|
||||
let data = self.project_state.project.pattern_at(*bank, *pattern).clone();
|
||||
Some(UndoScope::Pattern { bank: *bank, pattern: *pattern, data })
|
||||
}
|
||||
AppCommand::ResetBank { bank } | AppCommand::PasteBank { bank } => {
|
||||
AppCommand::ResetBank { bank }
|
||||
| AppCommand::PasteBank { bank }
|
||||
| AppCommand::ImportBank { bank } => {
|
||||
let data = self.project_state.project.banks[*bank].clone();
|
||||
Some(UndoScope::Bank { bank: *bank, data })
|
||||
}
|
||||
|
||||
@@ -67,6 +67,20 @@ pub enum AppCommand {
|
||||
bank: usize,
|
||||
pattern: usize,
|
||||
},
|
||||
SharePattern {
|
||||
bank: usize,
|
||||
pattern: usize,
|
||||
},
|
||||
ImportPattern {
|
||||
bank: usize,
|
||||
pattern: usize,
|
||||
},
|
||||
ShareBank {
|
||||
bank: usize,
|
||||
},
|
||||
ImportBank {
|
||||
bank: usize,
|
||||
},
|
||||
CopyPatterns {
|
||||
bank: usize,
|
||||
patterns: Vec<usize>,
|
||||
|
||||
@@ -236,6 +236,14 @@ pub(super) fn handle_main_page(ctx: &mut InputContext, key: KeyEvent, ctrl: bool
|
||||
KeyCode::Char('D') => {
|
||||
ctx.dispatch(AppCommand::EvaluatePrelude);
|
||||
}
|
||||
KeyCode::Char('g') => {
|
||||
let (bank, pattern) = (ctx.app.editor_ctx.bank, ctx.app.editor_ctx.pattern);
|
||||
ctx.dispatch(AppCommand::SharePattern { bank, pattern });
|
||||
}
|
||||
KeyCode::Char('G') => {
|
||||
let (bank, pattern) = (ctx.app.editor_ctx.bank, ctx.app.editor_ctx.pattern);
|
||||
ctx.dispatch(AppCommand::ImportPattern { bank, pattern });
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
InputResult::Continue
|
||||
|
||||
@@ -638,6 +638,9 @@ 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 });
|
||||
}
|
||||
}
|
||||
ctx.dispatch(AppCommand::CloseModal);
|
||||
InputResult::Continue
|
||||
|
||||
@@ -265,6 +265,33 @@ pub(super) fn handle_patterns_page(ctx: &mut InputContext, key: KeyEvent) -> Inp
|
||||
ctx.dispatch(AppCommand::ClearSolos);
|
||||
ctx.app.send_mute_state(ctx.seq_cmd_tx);
|
||||
}
|
||||
KeyCode::Char('g') => {
|
||||
let bank = ctx.app.patterns_nav.bank_cursor;
|
||||
match ctx.app.patterns_nav.column {
|
||||
PatternsColumn::Patterns => {
|
||||
let pattern = ctx.app.patterns_nav.pattern_cursor;
|
||||
ctx.dispatch(AppCommand::SharePattern { bank, pattern });
|
||||
}
|
||||
PatternsColumn::Banks => {
|
||||
ctx.dispatch(AppCommand::ShareBank { bank });
|
||||
}
|
||||
}
|
||||
}
|
||||
KeyCode::Char('G') => {
|
||||
let bank = ctx.app.patterns_nav.bank_cursor;
|
||||
match ctx.app.patterns_nav.column {
|
||||
PatternsColumn::Patterns => {
|
||||
let pattern = ctx.app.patterns_nav.pattern_cursor;
|
||||
ctx.dispatch(AppCommand::ImportPattern { bank, pattern });
|
||||
}
|
||||
PatternsColumn::Banks => {
|
||||
ctx.dispatch(AppCommand::OpenModal(Modal::Confirm {
|
||||
action: ConfirmAction::ImportBank { bank },
|
||||
selected: false,
|
||||
}));
|
||||
}
|
||||
}
|
||||
}
|
||||
KeyCode::Char('s') => super::open_save(ctx),
|
||||
KeyCode::Char('l') => super::open_load(ctx),
|
||||
KeyCode::Char('?') => {
|
||||
|
||||
@@ -127,6 +127,10 @@ pub const DOCS: &[DocEntry] = &[
|
||||
"Soundfonts",
|
||||
include_str!("../../docs/tutorials/soundfont.md"),
|
||||
),
|
||||
Topic(
|
||||
"Sharing",
|
||||
include_str!("../../docs/tutorials/sharing.md"),
|
||||
),
|
||||
];
|
||||
|
||||
pub fn topic_count() -> usize {
|
||||
|
||||
@@ -9,8 +9,8 @@ pub use cagire_forth::{
|
||||
Variables, Word, WordCompile, WORDS,
|
||||
};
|
||||
pub use cagire_project::{
|
||||
load, load_str, save, Bank, FollowUp, LaunchQuantization, Pattern, PatternSpeed, Project,
|
||||
SyncMode, MAX_BANKS, MAX_PATTERNS,
|
||||
load, load_str, save, share, Bank, FollowUp, LaunchQuantization, Pattern, PatternSpeed,
|
||||
Project, SyncMode, MAX_BANKS, MAX_PATTERNS,
|
||||
};
|
||||
pub use script::ScriptEngine;
|
||||
|
||||
|
||||
@@ -11,6 +11,7 @@ pub enum ConfirmAction {
|
||||
ResetBank { bank: usize },
|
||||
ResetPatterns { bank: usize, patterns: Vec<usize> },
|
||||
ResetBanks { banks: Vec<usize> },
|
||||
ImportBank { bank: usize },
|
||||
}
|
||||
|
||||
impl ConfirmAction {
|
||||
@@ -26,6 +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),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -52,6 +52,8 @@ pub fn bindings_for(page: Page, plugin_mode: bool) -> Vec<(&'static str, &'stati
|
||||
bindings.push(("X", "Clear solos", "Clear all solos"));
|
||||
bindings.push(("d", "Prelude", "Edit prelude script"));
|
||||
bindings.push(("D", "Eval prelude", "Re-evaluate prelude without editing"));
|
||||
bindings.push(("g", "Share", "Export pattern to clipboard"));
|
||||
bindings.push(("G", "Import", "Import pattern from clipboard"));
|
||||
}
|
||||
Page::Patterns => {
|
||||
bindings.push(("←→↑↓", "Navigate", "Move between banks/patterns"));
|
||||
@@ -71,6 +73,8 @@ pub fn bindings_for(page: Page, plugin_mode: bool) -> Vec<(&'static str, &'stati
|
||||
bindings.push(("x", "Solo", "Stage solo for pattern"));
|
||||
bindings.push(("M", "Clear mutes", "Clear all mutes"));
|
||||
bindings.push(("X", "Clear solos", "Clear all solos"));
|
||||
bindings.push(("g", "Share", "Export bank or pattern to clipboard"));
|
||||
bindings.push(("G", "Import", "Import bank or pattern from clipboard"));
|
||||
bindings.push(("Ctrl+C", "Copy", "Copy bank/pattern"));
|
||||
bindings.push(("Ctrl+V", "Paste", "Paste bank/pattern"));
|
||||
bindings.push(("Del", "Reset", "Reset bank/pattern"));
|
||||
|
||||
Reference in New Issue
Block a user