MULTI-BOARD !
This commit is contained in:
@@ -2,7 +2,10 @@ mod pty;
|
||||
mod storage;
|
||||
|
||||
use pty::{pty_kill, pty_resize, pty_spawn, pty_write, PtyState};
|
||||
use storage::{board_load, board_save};
|
||||
use storage::{
|
||||
board_create, board_delete, board_load, board_rename, board_save, boards_load_index,
|
||||
boards_set_current,
|
||||
};
|
||||
|
||||
#[cfg_attr(mobile, tauri::mobile_entry_point)]
|
||||
pub fn run() {
|
||||
@@ -35,8 +38,13 @@ pub fn run() {
|
||||
pty_write,
|
||||
pty_resize,
|
||||
pty_kill,
|
||||
boards_load_index,
|
||||
board_save,
|
||||
board_load,
|
||||
board_create,
|
||||
board_rename,
|
||||
board_delete,
|
||||
boards_set_current,
|
||||
])
|
||||
.run(tauri::generate_context!())
|
||||
.expect("error while running tauri application");
|
||||
|
||||
@@ -1,32 +1,119 @@
|
||||
use std::fs;
|
||||
use std::path::PathBuf;
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_json::Value;
|
||||
use tauri::{AppHandle, Manager};
|
||||
use uuid::Uuid;
|
||||
|
||||
fn board_path(app: &AppHandle) -> Result<PathBuf, String> {
|
||||
let dir = app
|
||||
#[derive(Serialize, Deserialize, Clone)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct BoardMeta {
|
||||
pub id: String,
|
||||
pub name: String,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct BoardIndex {
|
||||
pub version: u32,
|
||||
pub current_board_id: String,
|
||||
pub boards: Vec<BoardMeta>,
|
||||
}
|
||||
|
||||
fn data_dir(app: &AppHandle) -> Result<PathBuf, String> {
|
||||
let d = app
|
||||
.path()
|
||||
.app_data_dir()
|
||||
.map_err(|e| format!("app_data_dir: {e}"))?;
|
||||
fs::create_dir_all(&dir).map_err(|e| format!("create_dir_all: {e}"))?;
|
||||
Ok(dir.join("board.json"))
|
||||
fs::create_dir_all(&d).map_err(|e| format!("create_dir_all: {e}"))?;
|
||||
Ok(d)
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn board_save(app: AppHandle, state: Value) -> Result<(), String> {
|
||||
let path = board_path(&app)?;
|
||||
fn boards_dir(app: &AppHandle) -> Result<PathBuf, String> {
|
||||
let d = data_dir(app)?.join("boards");
|
||||
fs::create_dir_all(&d).map_err(|e| format!("create boards dir: {e}"))?;
|
||||
Ok(d)
|
||||
}
|
||||
|
||||
fn index_path(app: &AppHandle) -> Result<PathBuf, String> {
|
||||
Ok(data_dir(app)?.join("boards.json"))
|
||||
}
|
||||
|
||||
fn board_file(app: &AppHandle, id: &str) -> Result<PathBuf, String> {
|
||||
if id.is_empty() || id.contains('/') || id.contains('\\') || id.contains("..") {
|
||||
return Err(format!("invalid board id: {id:?}"));
|
||||
}
|
||||
Ok(boards_dir(app)?.join(format!("{id}.json")))
|
||||
}
|
||||
|
||||
fn atomic_write(path: &Path, body: &str) -> Result<(), String> {
|
||||
let tmp = path.with_extension("json.tmp");
|
||||
let body = serde_json::to_string_pretty(&state).map_err(|e| e.to_string())?;
|
||||
fs::write(&tmp, body).map_err(|e| format!("write tmp: {e}"))?;
|
||||
// Atomic replace so a crash mid-write can't corrupt the existing file.
|
||||
fs::rename(&tmp, &path).map_err(|e| format!("rename: {e}"))?;
|
||||
fs::rename(&tmp, path).map_err(|e| format!("rename: {e}"))?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn empty_board_json() -> String {
|
||||
serde_json::to_string_pretty(&serde_json::json!({
|
||||
"version": 1,
|
||||
"cards": [],
|
||||
"viewport": { "x": 0, "y": 0, "scale": 1 },
|
||||
"maxZ": 0,
|
||||
}))
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
fn write_index(app: &AppHandle, idx: &BoardIndex) -> Result<(), String> {
|
||||
let body = serde_json::to_string_pretty(idx).map_err(|e| format!("serialize index: {e}"))?;
|
||||
atomic_write(&index_path(app)?, &body)
|
||||
}
|
||||
|
||||
fn read_index(app: &AppHandle) -> Result<BoardIndex, String> {
|
||||
let path = index_path(app)?;
|
||||
if !path.exists() {
|
||||
// First run, or upgrading from the pre-multi-board single-file layout.
|
||||
// Migrate the old board.json (if any) into boards/default.json.
|
||||
let legacy = data_dir(app)?.join("board.json");
|
||||
let default_id = "default".to_string();
|
||||
let default_file = board_file(app, &default_id)?;
|
||||
if legacy.exists() && !default_file.exists() {
|
||||
let body = fs::read_to_string(&legacy).map_err(|e| format!("read legacy: {e}"))?;
|
||||
atomic_write(&default_file, &body)?;
|
||||
let _ = fs::remove_file(&legacy);
|
||||
} else if !default_file.exists() {
|
||||
atomic_write(&default_file, &empty_board_json())?;
|
||||
}
|
||||
let idx = BoardIndex {
|
||||
version: 1,
|
||||
current_board_id: default_id.clone(),
|
||||
boards: vec![BoardMeta {
|
||||
id: default_id,
|
||||
name: "Default".to_string(),
|
||||
}],
|
||||
};
|
||||
write_index(app, &idx)?;
|
||||
return Ok(idx);
|
||||
}
|
||||
let body = fs::read_to_string(&path).map_err(|e| format!("read index: {e}"))?;
|
||||
serde_json::from_str(&body).map_err(|e| format!("parse index: {e}"))
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn board_load(app: AppHandle) -> Result<Option<Value>, String> {
|
||||
let path = board_path(&app)?;
|
||||
pub fn boards_load_index(app: AppHandle) -> Result<BoardIndex, String> {
|
||||
read_index(&app)
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn board_save(app: AppHandle, id: String, state: Value) -> Result<(), String> {
|
||||
let path = board_file(&app, &id)?;
|
||||
let body = serde_json::to_string_pretty(&state).map_err(|e| format!("serialize: {e}"))?;
|
||||
atomic_write(&path, &body)
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn board_load(app: AppHandle, id: String) -> Result<Option<Value>, String> {
|
||||
let path = board_file(&app, &id)?;
|
||||
if !path.exists() {
|
||||
return Ok(None);
|
||||
}
|
||||
@@ -34,3 +121,58 @@ pub fn board_load(app: AppHandle) -> Result<Option<Value>, String> {
|
||||
let v = serde_json::from_str(&body).map_err(|e| format!("parse: {e}"))?;
|
||||
Ok(Some(v))
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn board_create(app: AppHandle, name: String) -> Result<BoardMeta, String> {
|
||||
let id = Uuid::new_v4().to_string();
|
||||
let mut idx = read_index(&app)?;
|
||||
let meta = BoardMeta {
|
||||
id: id.clone(),
|
||||
name,
|
||||
};
|
||||
idx.boards.push(meta.clone());
|
||||
write_index(&app, &idx)?;
|
||||
atomic_write(&board_file(&app, &id)?, &empty_board_json())?;
|
||||
Ok(meta)
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn board_rename(app: AppHandle, id: String, name: String) -> Result<(), String> {
|
||||
let mut idx = read_index(&app)?;
|
||||
let b = idx
|
||||
.boards
|
||||
.iter_mut()
|
||||
.find(|b| b.id == id)
|
||||
.ok_or_else(|| format!("board {id} not found"))?;
|
||||
b.name = name;
|
||||
write_index(&app, &idx)
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn board_delete(app: AppHandle, id: String) -> Result<BoardIndex, String> {
|
||||
let mut idx = read_index(&app)?;
|
||||
if idx.boards.len() <= 1 {
|
||||
return Err("can't delete the last board".to_string());
|
||||
}
|
||||
let before = idx.boards.len();
|
||||
idx.boards.retain(|b| b.id != id);
|
||||
if idx.boards.len() == before {
|
||||
return Err(format!("board {id} not found"));
|
||||
}
|
||||
if idx.current_board_id == id {
|
||||
idx.current_board_id = idx.boards[0].id.clone();
|
||||
}
|
||||
write_index(&app, &idx)?;
|
||||
let _ = fs::remove_file(board_file(&app, &id)?);
|
||||
Ok(idx)
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn boards_set_current(app: AppHandle, id: String) -> Result<(), String> {
|
||||
let mut idx = read_index(&app)?;
|
||||
if !idx.boards.iter().any(|b| b.id == id) {
|
||||
return Err(format!("board {id} not found"));
|
||||
}
|
||||
idx.current_board_id = id;
|
||||
write_index(&app, &idx)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user