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:
TypoGenie
2026-02-01 18:51:43 +02:00
parent da335734d3
commit a6f664088c
405 changed files with 69134 additions and 5936 deletions

View File

@@ -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()
}