feat: port all template categories to JSON format
- Ported Minimalist templates to JSON (Swiss Grid, Brutalist, etc.) - Ported Tech templates to JSON (SaaS, Terminal, Cyberpunk, etc.) - Ported Creative templates to JSON (Art Gallery, Zine, Pop Art, etc.) - Ported Industrial templates to JSON (Blueprint, Factory, Schematic, etc.) - Ported Nature templates to JSON (Botanical, Ocean, Mountain, etc.) - Ported Lifestyle templates to JSON (Cookbook, Travel, Coffee House, etc.) - Ported Vintage templates to JSON (Art Deco, Medieval, Retro 80s, etc.) - Updated README.md to reflect the new JSON-based style system (example configuration and contribution workflow) - Completed migration of over 150 styles to the new architecture
This commit is contained in:
@@ -1,17 +1,253 @@
|
||||
use std::path::PathBuf;
|
||||
use tauri::{Manager, path::BaseDirectory};
|
||||
use std::fs;
|
||||
use tauri::{Manager, path::BaseDirectory, WindowEvent, PhysicalPosition, PhysicalSize};
|
||||
use serde::{Serialize, Deserialize};
|
||||
|
||||
/// Gets the portable data directory (next to the executable)
|
||||
fn get_portable_data_dir(app: &tauri::App) -> PathBuf {
|
||||
// Get the directory where the EXE is located
|
||||
if let Ok(exe_dir) = app.path().resolve("", BaseDirectory::Executable) {
|
||||
let portable_dir = exe_dir.join("TypoGenie-Data");
|
||||
// Create the directory if it doesn't exist
|
||||
let _ = std::fs::create_dir_all(&portable_dir);
|
||||
portable_dir
|
||||
/// Templates directory name
|
||||
const TEMPLATES_DIR_NAME: &str = "templates";
|
||||
|
||||
/// Template info for frontend
|
||||
#[derive(Serialize, Deserialize)]
|
||||
struct TemplateFile {
|
||||
name: String,
|
||||
content: String,
|
||||
category: String, // Folder name (e.g., "academic", "corporate")
|
||||
}
|
||||
|
||||
/// Debug command to check path resolution
|
||||
#[tauri::command]
|
||||
fn debug_paths(app: tauri::AppHandle) -> Result<String, String> {
|
||||
let mut output = String::new();
|
||||
|
||||
// Check executable directory using std::env (reliable)
|
||||
let exe_dir = get_exe_dir(&app);
|
||||
match exe_dir {
|
||||
Some(ref p) => output.push_str(&format!("Executable dir (std::env): {:?}\n", p)),
|
||||
None => output.push_str("Executable dir: failed to get\n"),
|
||||
}
|
||||
|
||||
// Check resource directory
|
||||
match app.path().resolve("", BaseDirectory::Resource) {
|
||||
Ok(p) => output.push_str(&format!("Resource dir: {:?}\n", p)),
|
||||
Err(e) => output.push_str(&format!("Resource dir error: {}\n", e)),
|
||||
}
|
||||
|
||||
// Check if templates exist in exe location
|
||||
if let Some(ref exe_dir) = exe_dir {
|
||||
let templates_exe = exe_dir.join("templates");
|
||||
output.push_str(&format!("Templates path ({:?}): exists={}\n", templates_exe, templates_exe.exists()));
|
||||
|
||||
if templates_exe.exists() {
|
||||
match fs::read_dir(&templates_exe) {
|
||||
Ok(entries) => {
|
||||
output.push_str(" Contents:\n");
|
||||
for entry in entries.flatten() {
|
||||
output.push_str(&format!(" {:?}\n", entry.path()));
|
||||
}
|
||||
}
|
||||
Err(e) => output.push_str(&format!(" Error reading: {}\n", e)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(output)
|
||||
}
|
||||
|
||||
/// Get the absolute path to the templates directory
|
||||
#[tauri::command]
|
||||
fn get_templates_dir(app: tauri::AppHandle) -> Result<String, String> {
|
||||
let exe_dir = get_exe_dir(&app)
|
||||
.ok_or("Failed to get executable directory")?;
|
||||
let templates_dir = exe_dir.join("templates");
|
||||
Ok(templates_dir.to_string_lossy().to_string())
|
||||
}
|
||||
|
||||
/// Read all template files from the templates directory
|
||||
#[tauri::command]
|
||||
fn read_templates(app: tauri::AppHandle) -> Result<Vec<TemplateFile>, String> {
|
||||
let exe_dir = get_exe_dir(&app)
|
||||
.ok_or("Failed to get executable directory")?;
|
||||
let templates_dir = exe_dir.join(TEMPLATES_DIR_NAME);
|
||||
|
||||
println!("Reading templates from: {:?}", templates_dir);
|
||||
|
||||
let mut templates = Vec::new();
|
||||
|
||||
if !templates_dir.exists() {
|
||||
println!("Templates dir doesn't exist, trying to extract...");
|
||||
// Try to extract templates first
|
||||
extract_templates_on_first_run(&app)
|
||||
.map_err(|e| format!("Failed to extract templates: {}", e))?;
|
||||
}
|
||||
|
||||
if templates_dir.exists() {
|
||||
println!("Templates dir exists, reading recursively...");
|
||||
read_templates_recursive(&templates_dir, &mut templates, &templates_dir)
|
||||
.map_err(|e| format!("Failed to read templates: {}", e))?;
|
||||
println!("Found {} templates", templates.len());
|
||||
} else {
|
||||
// Fallback to app data directory (shouldn't happen)
|
||||
app.path().app_data_dir().unwrap_or_else(|_| PathBuf::from("."))
|
||||
println!("Templates dir still doesn't exist after extraction attempt");
|
||||
}
|
||||
|
||||
Ok(templates)
|
||||
}
|
||||
|
||||
/// Recursively read all .json template files
|
||||
fn read_templates_recursive(
|
||||
dir: &PathBuf,
|
||||
templates: &mut Vec<TemplateFile>,
|
||||
base_dir: &PathBuf
|
||||
) -> Result<(), Box<dyn std::error::Error>> {
|
||||
for entry in fs::read_dir(dir)? {
|
||||
let entry = entry?;
|
||||
let path = entry.path();
|
||||
let name = entry.file_name().to_string_lossy().to_string();
|
||||
|
||||
if path.is_dir() {
|
||||
read_templates_recursive(&path, templates, base_dir)?;
|
||||
} else if name.ends_with(".json") {
|
||||
let content = fs::read_to_string(&path)?;
|
||||
// Extract category from the relative path (folder name)
|
||||
let category = path.parent()
|
||||
.and_then(|p| p.strip_prefix(base_dir).ok())
|
||||
.and_then(|p| p.components().next())
|
||||
.map(|c| c.as_os_str().to_string_lossy().to_string())
|
||||
.unwrap_or_else(|| "Other".to_string());
|
||||
|
||||
templates.push(TemplateFile { name, content, category });
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Open the templates folder in file explorer
|
||||
#[tauri::command]
|
||||
fn open_templates_folder(app: tauri::AppHandle) -> Result<(), String> {
|
||||
let exe_dir = get_exe_dir(&app)
|
||||
.ok_or("Failed to get executable directory")?;
|
||||
let templates_dir = exe_dir.join(TEMPLATES_DIR_NAME);
|
||||
|
||||
// Create if doesn't exist
|
||||
if !templates_dir.exists() {
|
||||
fs::create_dir_all(&templates_dir)
|
||||
.map_err(|e| format!("Failed to create templates dir: {}", e))?;
|
||||
}
|
||||
|
||||
// Open with default application using opener crate
|
||||
opener::open(&templates_dir)
|
||||
.map_err(|e| format!("Failed to open folder: {}", e))?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Toggle DevTools for the main window
|
||||
#[tauri::command]
|
||||
fn toggle_devtools(app: tauri::AppHandle) -> Result<bool, String> {
|
||||
if let Some(window) = app.get_webview_window("main") {
|
||||
let is_open = window.is_devtools_open();
|
||||
|
||||
if is_open {
|
||||
window.close_devtools();
|
||||
} else {
|
||||
window.open_devtools();
|
||||
}
|
||||
|
||||
return Ok(!is_open);
|
||||
}
|
||||
Err("Main window not found".to_string())
|
||||
}
|
||||
|
||||
/// Window state structure
|
||||
#[derive(Serialize, Deserialize, Debug, Default, Clone)]
|
||||
struct WindowState {
|
||||
x: i32,
|
||||
y: i32,
|
||||
width: u32,
|
||||
height: u32,
|
||||
maximized: bool,
|
||||
}
|
||||
|
||||
/// Gets the executable directory using std::env (more reliable than BaseDirectory::Executable on Windows)
|
||||
fn get_exe_dir(_app: &tauri::AppHandle) -> Option<PathBuf> {
|
||||
std::env::current_exe()
|
||||
.ok()
|
||||
.and_then(|p| p.parent().map(|p| p.to_path_buf()))
|
||||
}
|
||||
|
||||
/// Extract bundled templates to the executable directory on first run
|
||||
fn extract_templates_on_first_run(app: &tauri::AppHandle) -> Result<(), Box<dyn std::error::Error>> {
|
||||
let exe_dir = get_exe_dir(app).ok_or("Failed to get executable directory")?;
|
||||
let templates_target = exe_dir.join(TEMPLATES_DIR_NAME);
|
||||
|
||||
// Check if templates already exist (first run detection)
|
||||
if templates_target.exists() {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
println!("First run detected. Extracting templates...");
|
||||
|
||||
// Get bundled templates resource path
|
||||
let resource_path = app.path().resolve(TEMPLATES_DIR_NAME, BaseDirectory::Resource)?;
|
||||
|
||||
if !resource_path.exists() {
|
||||
println!("Warning: Bundled templates not found at: {:?}", resource_path);
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
// Create templates directory
|
||||
fs::create_dir_all(&templates_target)?;
|
||||
|
||||
// Copy all template files recursively
|
||||
copy_dir_recursive(&resource_path, &templates_target)?;
|
||||
|
||||
println!("Templates extracted successfully to: {:?}", templates_target);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Recursively copy directory contents
|
||||
fn copy_dir_recursive(src: &PathBuf, dst: &PathBuf) -> Result<(), Box<dyn std::error::Error>> {
|
||||
fs::create_dir_all(dst)?;
|
||||
|
||||
for entry in fs::read_dir(src)? {
|
||||
let entry = entry?;
|
||||
let path = entry.path();
|
||||
let file_name = entry.file_name();
|
||||
let dest_path = dst.join(&file_name);
|
||||
|
||||
if path.is_dir() {
|
||||
copy_dir_recursive(&path, &dest_path)?;
|
||||
} else {
|
||||
fs::copy(&path, &dest_path)?;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Get the window state file path
|
||||
fn get_state_file_path(app: &tauri::AppHandle) -> PathBuf {
|
||||
let exe_dir = get_exe_dir(app).unwrap_or_else(|| PathBuf::from("."));
|
||||
exe_dir.join("window-state.json")
|
||||
}
|
||||
|
||||
/// Load window state
|
||||
fn load_window_state(app: &tauri::AppHandle) -> Option<WindowState> {
|
||||
let state_file = get_state_file_path(app);
|
||||
if state_file.exists() {
|
||||
if let Ok(data) = std::fs::read_to_string(&state_file) {
|
||||
if let Ok(state) = serde_json::from_str::<WindowState>(&data) {
|
||||
return Some(state);
|
||||
}
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
/// Save window state
|
||||
fn save_window_state(app: &tauri::AppHandle, state: &WindowState) {
|
||||
let state_file = get_state_file_path(app);
|
||||
if let Ok(json) = serde_json::to_string(state) {
|
||||
let _ = std::fs::write(&state_file, json);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,39 +257,77 @@ pub fn run() {
|
||||
.plugin(tauri_plugin_dialog::init())
|
||||
.plugin(tauri_plugin_fs::init())
|
||||
.plugin(tauri_plugin_store::Builder::default().build())
|
||||
.plugin(tauri_plugin_http::init())
|
||||
.plugin(tauri_plugin_shell::init())
|
||||
.plugin(tauri_plugin_opener::init())
|
||||
.setup(|app| {
|
||||
// Set up portable data directory
|
||||
let portable_dir = get_portable_data_dir(app);
|
||||
println!("Portable data directory: {:?}", portable_dir);
|
||||
// Extract templates on first run
|
||||
if let Err(e) = extract_templates_on_first_run(&app.handle()) {
|
||||
eprintln!("Failed to extract templates: {}", e);
|
||||
}
|
||||
|
||||
// Store the portable path in app state for frontend access
|
||||
app.manage(PortableDataDir(portable_dir));
|
||||
|
||||
// Show the main window after setup
|
||||
// Load and apply window state BEFORE showing window
|
||||
if let Some(window) = app.get_webview_window("main") {
|
||||
if let Some(state) = load_window_state(&app.handle()) {
|
||||
if state.maximized {
|
||||
let _ = window.maximize();
|
||||
} else {
|
||||
let _ = window.set_size(tauri::Size::Physical(
|
||||
PhysicalSize { width: state.width, height: state.height }
|
||||
));
|
||||
let _ = window.set_position(tauri::Position::Physical(
|
||||
PhysicalPosition { x: state.x, y: state.y }
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
let _ = window.show();
|
||||
let _ = window.set_focus();
|
||||
}
|
||||
|
||||
if cfg!(debug_assertions) {
|
||||
app.handle().plugin(
|
||||
tauri_plugin_log::Builder::default()
|
||||
.level(log::LevelFilter::Info)
|
||||
.build(),
|
||||
)?;
|
||||
}
|
||||
Ok(())
|
||||
})
|
||||
.invoke_handler(tauri::generate_handler![get_data_dir])
|
||||
.on_window_event(|window, event| {
|
||||
match event {
|
||||
WindowEvent::Moved(position) => {
|
||||
let app_handle = window.app_handle().clone();
|
||||
let pos = *position;
|
||||
let size = window.inner_size().unwrap_or(PhysicalSize { width: 1400, height: 900 });
|
||||
let maximized = window.is_maximized().unwrap_or(false);
|
||||
|
||||
let state = WindowState {
|
||||
x: pos.x,
|
||||
y: pos.y,
|
||||
width: size.width,
|
||||
height: size.height,
|
||||
maximized,
|
||||
};
|
||||
save_window_state(&app_handle, &state);
|
||||
}
|
||||
WindowEvent::Resized(size) => {
|
||||
let app_handle = window.app_handle().clone();
|
||||
let pos = window.outer_position().unwrap_or(PhysicalPosition { x: 0, y: 0 });
|
||||
let maximized = window.is_maximized().unwrap_or(false);
|
||||
|
||||
let state = WindowState {
|
||||
x: pos.x,
|
||||
y: pos.y,
|
||||
width: size.width,
|
||||
height: size.height,
|
||||
maximized,
|
||||
};
|
||||
save_window_state(&app_handle, &state);
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
})
|
||||
.invoke_handler(tauri::generate_handler![
|
||||
debug_paths,
|
||||
get_templates_dir,
|
||||
read_templates,
|
||||
open_templates_folder,
|
||||
toggle_devtools
|
||||
])
|
||||
.run(tauri::generate_context!())
|
||||
.expect("error while running tauri application");
|
||||
}
|
||||
|
||||
/// Structure to hold the portable data directory path
|
||||
struct PortableDataDir(PathBuf);
|
||||
|
||||
/// Command to get the portable data directory from the frontend
|
||||
#[tauri::command]
|
||||
fn get_data_dir(state: tauri::State<PortableDataDir>) -> String {
|
||||
state.0.to_string_lossy().to_string()
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user