onyx-tasks/apps/flutter/rust/src/api.rs

258 lines
9.1 KiB
Rust

use std::path::PathBuf;
use std::sync::Mutex;
use once_cell::sync::Lazy;
use uuid::Uuid;
use bevy_tasks_core::{
config::{AppConfig, WorkspaceConfig},
models::{Task, TaskList, TaskStatus},
repository::TaskRepository,
};
// ── State ───────────────────────────────────────────────────────────
struct AppState {
config: AppConfig,
repo: Option<TaskRepository>,
}
static STATE: Lazy<Mutex<AppState>> = Lazy::new(|| {
let config_path = AppConfig::get_config_path();
let config = AppConfig::load_from_file(&config_path).unwrap_or_default();
Mutex::new(AppState { config, repo: None })
});
fn ensure_repo(state: &mut AppState) -> Result<(), String> {
if state.repo.is_some() {
return Ok(());
}
let (_name, ws) = state.config.get_current_workspace().map_err(|e| e.to_string())?;
let repo = TaskRepository::new(ws.path.clone()).map_err(|e| e.to_string())?;
state.repo = Some(repo);
Ok(())
}
// ── DTOs ────────────────────────────────────────────────────────────
pub struct TaskDto {
pub id: String,
pub title: String,
pub description: String,
pub status: String,
pub due_date: Option<String>,
pub created_at: String,
pub updated_at: String,
pub parent_id: Option<String>,
}
pub struct TaskListDto {
pub id: String,
pub title: String,
pub created_at: String,
pub updated_at: String,
pub group_by_due_date: bool,
}
pub struct WorkspaceEntry {
pub name: String,
pub path: String,
pub webdav_url: Option<String>,
pub last_sync: Option<String>,
}
pub struct AppConfigDto {
pub workspaces: Vec<WorkspaceEntry>,
pub current_workspace: Option<String>,
}
fn task_to_dto(t: &Task) -> TaskDto {
TaskDto {
id: t.id.to_string(),
title: t.title.clone(),
description: t.description.clone(),
status: match t.status {
TaskStatus::Backlog => "backlog".into(),
TaskStatus::Completed => "completed".into(),
},
due_date: t.due_date.map(|d| d.to_rfc3339()),
created_at: t.created_at.to_rfc3339(),
updated_at: t.updated_at.to_rfc3339(),
parent_id: t.parent_id.map(|id| id.to_string()),
}
}
fn config_to_dto(c: &AppConfig) -> AppConfigDto {
AppConfigDto {
workspaces: c
.workspaces
.iter()
.map(|(name, ws)| WorkspaceEntry {
name: name.clone(),
path: ws.path.to_string_lossy().into_owned(),
webdav_url: ws.webdav_url.clone(),
last_sync: ws.last_sync.map(|d| d.to_rfc3339()),
})
.collect(),
current_workspace: c.current_workspace.clone(),
}
}
// ── Config commands ─────────────────────────────────────────────────
pub fn get_config() -> Result<AppConfigDto, String> {
let s = STATE.lock().unwrap();
Ok(config_to_dto(&s.config))
}
pub fn init_workspace(path: String) -> Result<(), String> {
TaskRepository::init(PathBuf::from(path))
.map(|_| ())
.map_err(|e| e.to_string())
}
pub fn add_workspace(name: String, path: String) -> Result<(), String> {
let mut s = STATE.lock().unwrap();
let ws = WorkspaceConfig::new(PathBuf::from(&path));
s.config.add_workspace(name.clone(), ws);
s.config.set_current_workspace(name).map_err(|e| e.to_string())?;
s.repo = None;
let config_path = AppConfig::get_config_path();
s.config.save_to_file(&config_path).map_err(|e| e.to_string())
}
pub fn set_current_workspace(name: String) -> Result<(), String> {
let mut s = STATE.lock().unwrap();
s.config.set_current_workspace(name).map_err(|e| e.to_string())?;
s.repo = None;
let config_path = AppConfig::get_config_path();
s.config.save_to_file(&config_path).map_err(|e| e.to_string())
}
pub fn remove_workspace(name: String) -> Result<(), String> {
let mut s = STATE.lock().unwrap();
s.config.remove_workspace(&name);
s.repo = None;
let config_path = AppConfig::get_config_path();
s.config.save_to_file(&config_path).map_err(|e| e.to_string())
}
// ── List commands ───────────────────────────────────────────────────
pub fn get_lists() -> Result<Vec<TaskListDto>, String> {
let mut s = STATE.lock().unwrap();
ensure_repo(&mut s)?;
let lists = s.repo.as_ref().unwrap().get_lists().map_err(|e| e.to_string())?;
Ok(lists
.iter()
.map(|l| TaskListDto {
id: l.id.to_string(),
title: l.title.clone(),
created_at: l.created_at.to_rfc3339(),
updated_at: l.updated_at.to_rfc3339(),
group_by_due_date: l.group_by_due_date,
})
.collect())
}
pub fn create_list(name: String) -> Result<TaskListDto, String> {
let mut s = STATE.lock().unwrap();
ensure_repo(&mut s)?;
let list = s.repo.as_mut().unwrap().create_list(name).map_err(|e| e.to_string())?;
Ok(TaskListDto {
id: list.id.to_string(),
title: list.title.clone(),
created_at: list.created_at.to_rfc3339(),
updated_at: list.updated_at.to_rfc3339(),
group_by_due_date: list.group_by_due_date,
})
}
pub fn delete_list(list_id: String) -> Result<(), String> {
let mut s = STATE.lock().unwrap();
ensure_repo(&mut s)?;
let id = Uuid::parse_str(&list_id).map_err(|e| e.to_string())?;
s.repo.as_mut().unwrap().delete_list(id).map_err(|e| e.to_string())
}
// ── Task commands ───────────────────────────────────────────────────
pub fn list_tasks(list_id: String) -> Result<Vec<TaskDto>, String> {
let mut s = STATE.lock().unwrap();
ensure_repo(&mut s)?;
let id = Uuid::parse_str(&list_id).map_err(|e| e.to_string())?;
let tasks = s.repo.as_ref().unwrap().list_tasks(id).map_err(|e| e.to_string())?;
Ok(tasks.iter().map(|t| task_to_dto(t)).collect())
}
pub fn create_task(list_id: String, title: String, description: String) -> Result<TaskDto, String> {
let mut s = STATE.lock().unwrap();
ensure_repo(&mut s)?;
let id = Uuid::parse_str(&list_id).map_err(|e| e.to_string())?;
let mut task = Task::new(title);
if !description.is_empty() {
task.description = description;
}
let created = s.repo.as_mut().unwrap().create_task(id, task).map_err(|e| e.to_string())?;
Ok(task_to_dto(&created))
}
pub fn update_task(list_id: String, task: TaskDto) -> Result<(), String> {
let mut s = STATE.lock().unwrap();
ensure_repo(&mut s)?;
let lid = Uuid::parse_str(&list_id).map_err(|e| e.to_string())?;
let tid = Uuid::parse_str(&task.id).map_err(|e| e.to_string())?;
let mut existing = s.repo.as_ref().unwrap().get_task(lid, tid).map_err(|e| e.to_string())?;
existing.title = task.title;
existing.description = task.description;
existing.due_date = task
.due_date
.as_deref()
.and_then(|d| chrono::DateTime::parse_from_rfc3339(d).ok())
.map(|d| d.with_timezone(&chrono::Utc));
s.repo.as_mut().unwrap().update_task(lid, existing).map_err(|e| e.to_string())
}
pub fn delete_task(list_id: String, task_id: String) -> Result<(), String> {
let mut s = STATE.lock().unwrap();
ensure_repo(&mut s)?;
let lid = Uuid::parse_str(&list_id).map_err(|e| e.to_string())?;
let tid = Uuid::parse_str(&task_id).map_err(|e| e.to_string())?;
s.repo.as_mut().unwrap().delete_task(lid, tid).map_err(|e| e.to_string())
}
pub fn toggle_task(list_id: String, task_id: String) -> Result<TaskDto, String> {
let mut s = STATE.lock().unwrap();
ensure_repo(&mut s)?;
let lid = Uuid::parse_str(&list_id).map_err(|e| e.to_string())?;
let tid = Uuid::parse_str(&task_id).map_err(|e| e.to_string())?;
let repo = s.repo.as_mut().unwrap();
let mut task = repo.get_task(lid, tid).map_err(|e| e.to_string())?;
match task.status {
TaskStatus::Backlog => task.complete(),
TaskStatus::Completed => task.uncomplete(),
}
repo.update_task(lid, task.clone()).map_err(|e| e.to_string())?;
Ok(task_to_dto(&task))
}
pub fn reorder_task(list_id: String, task_id: String, new_position: u32) -> Result<(), String> {
let mut s = STATE.lock().unwrap();
ensure_repo(&mut s)?;
let lid = Uuid::parse_str(&list_id).map_err(|e| e.to_string())?;
let tid = Uuid::parse_str(&task_id).map_err(|e| e.to_string())?;
s.repo
.as_mut()
.unwrap()
.reorder_task(lid, tid, new_position as usize)
.map_err(|e| e.to_string())
}
// ── Test function ───────────────────────────────────────────────────
pub fn greet(name: String) -> String {
format!("Hello, {name}! From Rust via flutter_rust_bridge.")
}