use std::{
fs,
path::{Path, PathBuf},
};
use bevy::prelude::*;
use jackdaw_api_internal::paths::recent_file_path;
use jackdaw_jsn::format::{JsnHeader, JsnProject, JsnProjectConfig};
use serde::{Deserialize, Serialize};
#[derive(Resource)]
pub struct ProjectRoot {
pub root: PathBuf,
pub config: JsnProject,
}
impl ProjectRoot {
pub fn jsn_dir(&self) -> PathBuf {
self.root.join(".jsn")
}
pub fn assets_dir(&self) -> PathBuf {
self.root.join("assets")
}
pub fn to_relative(&self, path: impl AsRef<Path>) -> PathBuf {
let path = path.as_ref();
path.strip_prefix(&self.root).unwrap_or(path).into()
}
}
#[derive(Serialize, Deserialize, Default)]
pub struct RecentProjects {
pub projects: Vec<RecentEntry>,
}
#[derive(Serialize, Deserialize, Clone)]
pub struct RecentEntry {
pub path: PathBuf,
pub name: String,
pub last_opened: String,
}
pub fn read_recent_projects() -> RecentProjects {
let Some(path) = recent_file_path() else {
return RecentProjects::default();
};
let Ok(data) = std::fs::read_to_string(&path) else {
return RecentProjects::default();
};
let mut projects: RecentProjects = serde_json::from_str(&data).unwrap_or_default();
projects
.projects
.retain(|entry| fs::exists(entry.path.clone()).unwrap_or_default());
projects
}
pub fn save_recent_projects(projects: &RecentProjects) {
let Some(path) = recent_file_path() else {
return;
};
if let Some(parent) = path.parent() {
let _ = std::fs::create_dir_all(parent);
}
if let Ok(data) = serde_json::to_string_pretty(projects) {
let _ = std::fs::write(&path, data);
}
}
pub fn read_last_project() -> Option<PathBuf> {
let recent = read_recent_projects();
recent.projects.first().map(|e| e.path.clone())
}
pub fn save_project_config(root: &Path, project: &JsnProject) -> std::io::Result<()> {
let jsn_dir = root.join(".jsn");
std::fs::create_dir_all(&jsn_dir)?;
let path = jsn_dir.join("project.jsn");
let data = serde_json::to_string_pretty(project)
.map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidData, e))?;
std::fs::write(&path, data)
}
pub fn load_project_config(root: &Path) -> Option<JsnProject> {
let new_path = root.join(".jsn/project.jsn");
let legacy_path = root.join("project.jsn");
let path = if new_path.is_file() {
new_path
} else {
legacy_path
};
let data = std::fs::read_to_string(path).ok()?;
serde_json::from_str(&data).ok()
}
pub fn create_default_project(root: &Path) -> JsnProject {
let name = root
.file_name()
.map(|n| n.to_string_lossy().to_string())
.unwrap_or_else(|| "Untitled Project".to_string());
let project = JsnProject {
jsn: JsnHeader::default(),
project: JsnProjectConfig {
name,
description: String::new(),
default_scene: None,
layout: None,
},
};
let jsn_dir = root.join(".jsn");
let _ = std::fs::create_dir_all(&jsn_dir);
let path = jsn_dir.join("project.jsn");
if let Ok(data) = serde_json::to_string_pretty(&project) {
let _ = std::fs::write(&path, data);
}
project
}
pub fn remove_recent(path: &Path) {
let mut recent = read_recent_projects();
recent.projects.retain(|e| e.path != path);
save_recent_projects(&recent);
}
pub fn touch_recent(root: &Path, name: &str) {
let mut recent = read_recent_projects();
recent.projects.retain(|e| e.path != root);
recent.projects.insert(
0,
RecentEntry {
path: root.to_path_buf(),
name: name.to_string(),
last_opened: chrono_now(),
},
);
recent.projects.truncate(10);
save_recent_projects(&recent);
}
fn chrono_now() -> String {
let dur = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap_or_default();
let secs = dur.as_secs();
format!("{secs}")
}