P1e: persist undo and redo history across reloads
This commit is contained in:
@@ -141,6 +141,8 @@ pub struct ProjectData {
|
||||
pub cores: Vec<Core>,
|
||||
pub journal: Vec<JournalEntry>,
|
||||
pub seq: u64,
|
||||
pub history: Vec<ProjectEdit>,
|
||||
pub redo: Vec<ProjectEdit>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Serialize, Deserialize)]
|
||||
@@ -271,8 +273,8 @@ impl Session {
|
||||
cores: data.cores,
|
||||
journal: data.journal,
|
||||
project_name: data.name,
|
||||
history: Vec::new(),
|
||||
redo: Vec::new(),
|
||||
history: data.history,
|
||||
redo: data.redo,
|
||||
engine,
|
||||
};
|
||||
session.sanitize_faces();
|
||||
@@ -296,6 +298,14 @@ impl Session {
|
||||
!self.redo.is_empty()
|
||||
}
|
||||
|
||||
pub fn history(&self) -> &[ProjectEdit] {
|
||||
&self.history
|
||||
}
|
||||
|
||||
pub fn redo(&self) -> &[ProjectEdit] {
|
||||
&self.redo
|
||||
}
|
||||
|
||||
pub fn bpm(&self) -> f32 {
|
||||
self.bpm
|
||||
}
|
||||
@@ -1227,6 +1237,8 @@ mod tests {
|
||||
cores: vec![test_core()],
|
||||
journal: Vec::new(),
|
||||
seq: 0,
|
||||
history: Vec::new(),
|
||||
redo: Vec::new(),
|
||||
},
|
||||
);
|
||||
engine.clear();
|
||||
|
||||
@@ -224,6 +224,8 @@ async fn save_now(shared: &Shared) {
|
||||
pattern: session.pattern(),
|
||||
cores: &state.cores,
|
||||
journal: session.journal(),
|
||||
history: session.history(),
|
||||
redo: session.redo(),
|
||||
};
|
||||
let result = {
|
||||
let mut store = shared.store.lock().expect("project store lock poisoned");
|
||||
|
||||
@@ -10,7 +10,7 @@ use std::path::{Path, PathBuf};
|
||||
use rusqlite::{Connection, OptionalExtension, params};
|
||||
use serde_json::Value;
|
||||
use thiserror::Error;
|
||||
use zhila_core::{JournalEntry, ProjectData};
|
||||
use zhila_core::{JournalEntry, ProjectData, ProjectEdit};
|
||||
use zhila_protocol::{CoreSummary, Pattern, Seal, VEIN_COUNT};
|
||||
|
||||
const SCHEMA_VERSION: &str = "1";
|
||||
@@ -45,6 +45,8 @@ pub struct ProjectSnapshot<'a> {
|
||||
pub pattern: &'a Pattern,
|
||||
pub cores: &'a [CoreSummary],
|
||||
pub journal: &'a [JournalEntry],
|
||||
pub history: &'a [ProjectEdit],
|
||||
pub redo: &'a [ProjectEdit],
|
||||
}
|
||||
|
||||
impl ProjectStore {
|
||||
@@ -68,6 +70,8 @@ impl ProjectStore {
|
||||
bpm REAL NOT NULL,
|
||||
gains TEXT NOT NULL,
|
||||
pattern TEXT NOT NULL,
|
||||
history TEXT NOT NULL DEFAULT '[]',
|
||||
redo TEXT NOT NULL DEFAULT '[]',
|
||||
saved_at INTEGER NOT NULL
|
||||
);
|
||||
CREATE TABLE IF NOT EXISTS cores (
|
||||
@@ -88,6 +92,14 @@ impl ProjectStore {
|
||||
);",
|
||||
)?;
|
||||
let _ = conn.execute("ALTER TABLE cores ADD COLUMN faces TEXT", []);
|
||||
let _ = conn.execute(
|
||||
"ALTER TABLE project ADD COLUMN history TEXT NOT NULL DEFAULT '[]'",
|
||||
[],
|
||||
);
|
||||
let _ = conn.execute(
|
||||
"ALTER TABLE project ADD COLUMN redo TEXT NOT NULL DEFAULT '[]'",
|
||||
[],
|
||||
);
|
||||
conn.execute(
|
||||
"INSERT INTO meta (key, value) VALUES ('schema', ?1)
|
||||
ON CONFLICT(key) DO UPDATE SET value = excluded.value",
|
||||
@@ -108,7 +120,7 @@ impl ProjectStore {
|
||||
let row = self
|
||||
.conn
|
||||
.query_row(
|
||||
"SELECT name, revision, bpm, gains, pattern FROM project WHERE id = 1",
|
||||
"SELECT name, revision, bpm, gains, pattern, history, redo FROM project WHERE id = 1",
|
||||
[],
|
||||
|row| {
|
||||
Ok((
|
||||
@@ -117,11 +129,14 @@ impl ProjectStore {
|
||||
row.get::<_, f64>(2)?,
|
||||
row.get::<_, String>(3)?,
|
||||
row.get::<_, String>(4)?,
|
||||
row.get::<_, String>(5)?,
|
||||
row.get::<_, String>(6)?,
|
||||
))
|
||||
},
|
||||
)
|
||||
.optional()?;
|
||||
let Some((name, revision, bpm, gains_json, pattern_json)) = row else {
|
||||
let Some((name, revision, bpm, gains_json, pattern_json, history_json, redo_json)) = row
|
||||
else {
|
||||
return Ok(None);
|
||||
};
|
||||
|
||||
@@ -132,6 +147,15 @@ impl ProjectStore {
|
||||
}
|
||||
let mut pattern: Pattern = serde_json::from_str(&pattern_json)?;
|
||||
let mut warnings = Vec::new();
|
||||
let history: Vec<ProjectEdit> =
|
||||
serde_json::from_str(&history_json).unwrap_or_else(|error| {
|
||||
warnings.push(format!("history: {error}"));
|
||||
Vec::new()
|
||||
});
|
||||
let redo: Vec<ProjectEdit> = serde_json::from_str(&redo_json).unwrap_or_else(|error| {
|
||||
warnings.push(format!("redo: {error}"));
|
||||
Vec::new()
|
||||
});
|
||||
|
||||
let mut cores = Vec::new();
|
||||
{
|
||||
@@ -254,6 +278,8 @@ impl ProjectStore {
|
||||
cores,
|
||||
journal,
|
||||
seq,
|
||||
history,
|
||||
redo,
|
||||
},
|
||||
warnings,
|
||||
}))
|
||||
@@ -262,17 +288,21 @@ impl ProjectStore {
|
||||
pub fn save(&mut self, snapshot: &ProjectSnapshot<'_>) -> Result<(), ProjectError> {
|
||||
let gains = serde_json::to_string(snapshot.gains)?;
|
||||
let pattern = serde_json::to_string(snapshot.pattern)?;
|
||||
let history = serde_json::to_string(snapshot.history)?;
|
||||
let redo = serde_json::to_string(snapshot.redo)?;
|
||||
let saved_at = now_ms();
|
||||
let tx = self.conn.transaction()?;
|
||||
tx.execute(
|
||||
"INSERT INTO project (id, name, revision, bpm, gains, pattern, saved_at)
|
||||
VALUES (1, ?1, ?2, ?3, ?4, ?5, ?6)
|
||||
"INSERT INTO project (id, name, revision, bpm, gains, pattern, history, redo, saved_at)
|
||||
VALUES (1, ?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8)
|
||||
ON CONFLICT(id) DO UPDATE SET
|
||||
name = excluded.name,
|
||||
revision = excluded.revision,
|
||||
bpm = excluded.bpm,
|
||||
gains = excluded.gains,
|
||||
pattern = excluded.pattern,
|
||||
history = excluded.history,
|
||||
redo = excluded.redo,
|
||||
saved_at = excluded.saved_at",
|
||||
params![
|
||||
snapshot.name,
|
||||
@@ -280,6 +310,8 @@ impl ProjectStore {
|
||||
snapshot.bpm as f64,
|
||||
gains,
|
||||
pattern,
|
||||
history,
|
||||
redo,
|
||||
saved_at as i64
|
||||
],
|
||||
)?;
|
||||
@@ -423,7 +455,7 @@ fn now_ms() -> u64 {
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use zhila_core::Core;
|
||||
use zhila_core::{Core, CoreEdit, ProjectEdit};
|
||||
use zhila_protocol::{Actor, Command, FaceRef, PatternRow, Seal};
|
||||
|
||||
fn fixture() -> Vec<u8> {
|
||||
@@ -484,6 +516,15 @@ mod tests {
|
||||
at_ms: 12,
|
||||
command: Command::SetTempo { bpm: 132.0 },
|
||||
}];
|
||||
let history = vec![ProjectEdit {
|
||||
bpm: 120.0,
|
||||
gains,
|
||||
pattern: pattern.clone(),
|
||||
cores: vec![CoreEdit {
|
||||
core: core.summary.id.clone(),
|
||||
boundaries: core.face_boundaries(),
|
||||
}],
|
||||
}];
|
||||
store
|
||||
.write_asset(&core.summary, "clicks-01.wav", &bytes)
|
||||
.unwrap();
|
||||
@@ -496,6 +537,8 @@ mod tests {
|
||||
pattern: &pattern,
|
||||
cores: std::slice::from_ref(&core.summary),
|
||||
journal: &journal,
|
||||
history: &history,
|
||||
redo: &[],
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
@@ -517,6 +560,9 @@ mod tests {
|
||||
core.summary.id
|
||||
);
|
||||
assert_eq!(loaded.data.journal.len(), 1);
|
||||
assert_eq!(loaded.data.history.len(), 1);
|
||||
assert_eq!(loaded.data.history[0].bpm, 120.0);
|
||||
assert_eq!(loaded.data.history[0].cores[0].core, core.summary.id);
|
||||
assert_eq!(loaded.data.seq, 1);
|
||||
assert!(loaded.warnings.is_empty());
|
||||
assert!(store.asset_path("assets/../project.db").is_none());
|
||||
|
||||
Reference in New Issue
Block a user