diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml new file mode 100644 index 0000000..909ab41 --- /dev/null +++ b/src-tauri/Cargo.toml @@ -0,0 +1,34 @@ +[package] +name = "local-time-tracker" +version = "1.0.0" +description = "A local time tracking app with invoicing" +authors = ["you"] +edition = "2021" + +[lib] +name = "local_time_tracker_lib" +crate-type = ["lib", "cdylib", "staticlib"] + +[build-dependencies] +tauri-build = { version = "2", features = [] } + +[dependencies] +tauri = { version = "2", features = ["tray-icon"] } +tauri-plugin-shell = "2" +tauri-plugin-dialog = "2" +tauri-plugin-fs = "2" +tauri-plugin-notification = "2" +serde = { version = "1", features = ["derive"] } +serde_json = "1" +rusqlite = { version = "0.32", features = ["bundled"] } +chrono = { version = "0.4", features = ["serde"] } +directories = "5" +log = "0.4" +env_logger = "0.11" + +[profile.release] +panic = "abort" +codegen-units = 1 +lto = true +opt-level = "s" +strip = true diff --git a/src-tauri/build.rs b/src-tauri/build.rs new file mode 100644 index 0000000..d860e1e --- /dev/null +++ b/src-tauri/build.rs @@ -0,0 +1,3 @@ +fn main() { + tauri_build::build() +} diff --git a/src-tauri/capabilities/default.json b/src-tauri/capabilities/default.json new file mode 100644 index 0000000..4241219 --- /dev/null +++ b/src-tauri/capabilities/default.json @@ -0,0 +1,28 @@ +{ + "$schema": "https://schema.tauri.app/config/2", + "identifier": "default", + "description": "Default capabilities for the app", + "windows": ["main"], + "permissions": [ + "core:default", + "core:window:allow-close", + "core:window:allow-minimize", + "core:window:allow-maximize", + "core:window:allow-unmaximize", + "core:window:allow-show", + "core:window:allow-hide", + "core:window:allow-set-focus", + "core:window:allow-is-maximized", + "shell:allow-open", + "dialog:allow-open", + "dialog:allow-save", + "dialog:allow-message", + "dialog:allow-ask", + "dialog:allow-confirm", + "fs:allow-read-text-file", + "fs:allow-write-text-file", + "notification:allow-is-permission-granted", + "notification:allow-request-permission", + "notification:allow-notify" + ] +} diff --git a/src-tauri/icons/128x128.png b/src-tauri/icons/128x128.png new file mode 100644 index 0000000..7a9bb19 Binary files /dev/null and b/src-tauri/icons/128x128.png differ diff --git a/src-tauri/icons/128x128@2x.png b/src-tauri/icons/128x128@2x.png new file mode 100644 index 0000000..c016d71 Binary files /dev/null and b/src-tauri/icons/128x128@2x.png differ diff --git a/src-tauri/icons/32x32.png b/src-tauri/icons/32x32.png new file mode 100644 index 0000000..5d1833c Binary files /dev/null and b/src-tauri/icons/32x32.png differ diff --git a/src-tauri/icons/icon.icns b/src-tauri/icons/icon.icns new file mode 100644 index 0000000..9409991 Binary files /dev/null and b/src-tauri/icons/icon.icns differ diff --git a/src-tauri/icons/icon.ico b/src-tauri/icons/icon.ico new file mode 100644 index 0000000..f6290c1 Binary files /dev/null and b/src-tauri/icons/icon.ico differ diff --git a/src-tauri/icons/icon.png b/src-tauri/icons/icon.png new file mode 100644 index 0000000..c016d71 Binary files /dev/null and b/src-tauri/icons/icon.png differ diff --git a/src-tauri/src/commands.rs b/src-tauri/src/commands.rs new file mode 100644 index 0000000..f04cdff --- /dev/null +++ b/src-tauri/src/commands.rs @@ -0,0 +1,355 @@ +use crate::AppState; +use rusqlite::params; +use serde::{Deserialize, Serialize}; +use tauri::State; + +#[derive(Debug, Serialize, Deserialize)] +pub struct Client { + pub id: Option, + pub name: String, + pub email: Option, + pub address: Option, +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct Project { + pub id: Option, + pub client_id: Option, + pub name: String, + pub hourly_rate: f64, + pub color: String, + pub archived: bool, +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct Task { + pub id: Option, + pub project_id: i64, + pub name: String, +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct TimeEntry { + pub id: Option, + pub project_id: i64, + pub task_id: Option, + pub description: Option, + pub start_time: String, + pub end_time: Option, + pub duration: i64, +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct Invoice { + pub id: Option, + pub client_id: i64, + pub invoice_number: String, + pub date: String, + pub due_date: Option, + pub subtotal: f64, + pub tax_rate: f64, + pub tax_amount: f64, + pub discount: f64, + pub total: f64, + pub notes: Option, + pub status: String, +} + +// Client commands +#[tauri::command] +pub fn get_clients(state: State) -> Result, String> { + let conn = state.db.lock().map_err(|e| e.to_string())?; + let mut stmt = conn.prepare("SELECT id, name, email, address FROM clients ORDER BY name").map_err(|e| e.to_string())?; + let clients = stmt.query_map([], |row| { + Ok(Client { + id: Some(row.get(0)?), + name: row.get(1)?, + email: row.get(2)?, + address: row.get(3)?, + }) + }).map_err(|e| e.to_string())?; + clients.collect::, _>>().map_err(|e| e.to_string()) +} + +#[tauri::command] +pub fn create_client(state: State, client: Client) -> Result { + let conn = state.db.lock().map_err(|e| e.to_string())?; + conn.execute( + "INSERT INTO clients (name, email, address) VALUES (?1, ?2, ?3)", + params![client.name, client.email, client.address], + ).map_err(|e| e.to_string())?; + Ok(conn.last_insert_rowid()) +} + +#[tauri::command] +pub fn update_client(state: State, client: Client) -> Result<(), String> { + let conn = state.db.lock().map_err(|e| e.to_string())?; + conn.execute( + "UPDATE clients SET name = ?1, email = ?2, address = ?3 WHERE id = ?4", + params![client.name, client.email, client.address, client.id], + ).map_err(|e| e.to_string())?; + Ok(()) +} + +#[tauri::command] +pub fn delete_client(state: State, id: i64) -> Result<(), String> { + let conn = state.db.lock().map_err(|e| e.to_string())?; + conn.execute("DELETE FROM clients WHERE id = ?1", params![id]).map_err(|e| e.to_string())?; + Ok(()) +} + +// Project commands +#[tauri::command] +pub fn get_projects(state: State) -> Result, String> { + let conn = state.db.lock().map_err(|e| e.to_string())?; + let mut stmt = conn.prepare("SELECT id, client_id, name, hourly_rate, color, archived FROM projects ORDER BY name").map_err(|e| e.to_string())?; + let projects = stmt.query_map([], |row| { + Ok(Project { + id: Some(row.get(0)?), + client_id: row.get(1)?, + name: row.get(2)?, + hourly_rate: row.get(3)?, + color: row.get(4)?, + archived: row.get::<_, i32>(5)? != 0, + }) + }).map_err(|e| e.to_string())?; + projects.collect::, _>>().map_err(|e| e.to_string()) +} + +#[tauri::command] +pub fn create_project(state: State, project: Project) -> Result { + let conn = state.db.lock().map_err(|e| e.to_string())?; + conn.execute( + "INSERT INTO projects (client_id, name, hourly_rate, color, archived) VALUES (?1, ?2, ?3, ?4, ?5)", + params![project.client_id, project.name, project.hourly_rate, project.color, project.archived as i32], + ).map_err(|e| e.to_string())?; + Ok(conn.last_insert_rowid()) +} + +#[tauri::command] +pub fn update_project(state: State, project: Project) -> Result<(), String> { + let conn = state.db.lock().map_err(|e| e.to_string())?; + conn.execute( + "UPDATE projects SET client_id = ?1, name = ?2, hourly_rate = ?3, color = ?4, archived = ?5 WHERE id = ?6", + params![project.client_id, project.name, project.hourly_rate, project.color, project.archived as i32, project.id], + ).map_err(|e| e.to_string())?; + Ok(()) +} + +#[tauri::command] +pub fn delete_project(state: State, id: i64) -> Result<(), String> { + let conn = state.db.lock().map_err(|e| e.to_string())?; + conn.execute("DELETE FROM projects WHERE id = ?1", params![id]).map_err(|e| e.to_string())?; + Ok(()) +} + +// Task commands +#[tauri::command] +pub fn get_tasks(state: State, project_id: i64) -> Result, String> { + let conn = state.db.lock().map_err(|e| e.to_string())?; + let mut stmt = conn.prepare("SELECT id, project_id, name FROM tasks WHERE project_id = ?1 ORDER BY name").map_err(|e| e.to_string())?; + let tasks = stmt.query_map(params![project_id], |row| { + Ok(Task { + id: Some(row.get(0)?), + project_id: row.get(1)?, + name: row.get(2)?, + }) + }).map_err(|e| e.to_string())?; + tasks.collect::, _>>().map_err(|e| e.to_string()) +} + +#[tauri::command] +pub fn create_task(state: State, task: Task) -> Result { + let conn = state.db.lock().map_err(|e| e.to_string())?; + conn.execute( + "INSERT INTO tasks (project_id, name) VALUES (?1, ?2)", + params![task.project_id, task.name], + ).map_err(|e| e.to_string())?; + Ok(conn.last_insert_rowid()) +} + +#[tauri::command] +pub fn delete_task(state: State, id: i64) -> Result<(), String> { + let conn = state.db.lock().map_err(|e| e.to_string())?; + conn.execute("DELETE FROM tasks WHERE id = ?1", params![id]).map_err(|e| e.to_string())?; + Ok(()) +} + +// Time entry commands +#[tauri::command] +pub fn get_time_entries(state: State, start_date: Option, end_date: Option) -> Result, String> { + let conn = state.db.lock().map_err(|e| e.to_string())?; + let query = match (start_date, end_date) { + (Some(start), Some(end)) => { + let mut stmt = conn.prepare( + "SELECT id, project_id, task_id, description, start_time, end_time, duration + FROM time_entries + WHERE date(start_time) >= date(?1) AND date(start_time) <= date(?2) + ORDER BY start_time DESC" + ).map_err(|e| e.to_string())?; + stmt.query_map(params![start, end], |row| { + Ok(TimeEntry { + id: Some(row.get(0)?), + project_id: row.get(1)?, + task_id: row.get(2)?, + description: row.get(3)?, + start_time: row.get(4)?, + end_time: row.get(5)?, + duration: row.get(6)?, + }) + }).map_err(|e| e.to_string())?.collect::, _>>().map_err(|e| e.to_string())? + } + _ => { + let mut stmt = conn.prepare( + "SELECT id, project_id, task_id, description, start_time, end_time, duration + FROM time_entries ORDER BY start_time DESC LIMIT 100" + ).map_err(|e| e.to_string())?; + stmt.query_map([], |row| { + Ok(TimeEntry { + id: Some(row.get(0)?), + project_id: row.get(1)?, + task_id: row.get(2)?, + description: row.get(3)?, + start_time: row.get(4)?, + end_time: row.get(5)?, + duration: row.get(6)?, + }) + }).map_err(|e| e.to_string())?.collect::, _>>().map_err(|e| e.to_string())? + } + }; + Ok(query) +} + +#[tauri::command] +pub fn create_time_entry(state: State, entry: TimeEntry) -> Result { + let conn = state.db.lock().map_err(|e| e.to_string())?; + conn.execute( + "INSERT INTO time_entries (project_id, task_id, description, start_time, end_time, duration) + VALUES (?1, ?2, ?3, ?4, ?5, ?6)", + params![entry.project_id, entry.task_id, entry.description, entry.start_time, entry.end_time, entry.duration], + ).map_err(|e| e.to_string())?; + Ok(conn.last_insert_rowid()) +} + +#[tauri::command] +pub fn update_time_entry(state: State, entry: TimeEntry) -> Result<(), String> { + let conn = state.db.lock().map_err(|e| e.to_string())?; + conn.execute( + "UPDATE time_entries SET project_id = ?1, task_id = ?2, description = ?3, + start_time = ?4, end_time = ?5, duration = ?6 WHERE id = ?7", + params![entry.project_id, entry.task_id, entry.description, entry.start_time, entry.end_time, entry.duration, entry.id], + ).map_err(|e| e.to_string())?; + Ok(()) +} + +#[tauri::command] +pub fn delete_time_entry(state: State, id: i64) -> Result<(), String> { + let conn = state.db.lock().map_err(|e| e.to_string())?; + conn.execute("DELETE FROM time_entries WHERE id = ?1", params![id]).map_err(|e| e.to_string())?; + Ok(()) +} + +// Reports +#[tauri::command] +pub fn get_reports(state: State, start_date: String, end_date: String) -> Result { + let conn = state.db.lock().map_err(|e| e.to_string())?; + + // Total hours + let total: i64 = conn.query_row( + "SELECT COALESCE(SUM(duration), 0) FROM time_entries + WHERE date(start_time) >= date(?1) AND date(start_time) <= date(?2)", + params![start_date, end_date], + |row| row.get(0), + ).map_err(|e| e.to_string())?; + + // By project + let mut stmt = conn.prepare( + "SELECT p.name, p.color, SUM(t.duration) as total_duration + FROM time_entries t + JOIN projects p ON t.project_id = p.id + WHERE date(t.start_time) >= date(?1) AND date(t.start_time) <= date(?2) + GROUP BY p.id + ORDER BY total_duration DESC" + ).map_err(|e| e.to_string())?; + + let by_project: Vec = stmt.query_map(params![start_date, end_date], |row| { + Ok(serde_json::json!({ + "name": row.get::<_, String>(0)?, + "color": row.get::<_, String>(1)?, + "duration": row.get::<_, i64>(2)? + })) + }).map_err(|e| e.to_string())? + .collect::, _>>().map_err(|e| e.to_string())?; + + Ok(serde_json::json!({ + "totalSeconds": total, + "byProject": by_project + })) +} + +// Invoice commands +#[tauri::command] +pub fn create_invoice(state: State, invoice: Invoice) -> Result { + let conn = state.db.lock().map_err(|e| e.to_string())?; + conn.execute( + "INSERT INTO invoices (client_id, invoice_number, date, due_date, subtotal, tax_rate, tax_amount, discount, total, notes, status) + VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11)", + params![invoice.client_id, invoice.invoice_number, invoice.date, invoice.due_date, + invoice.subtotal, invoice.tax_rate, invoice.tax_amount, invoice.discount, + invoice.total, invoice.notes, invoice.status], + ).map_err(|e| e.to_string())?; + Ok(conn.last_insert_rowid()) +} + +#[tauri::command] +pub fn get_invoices(state: State) -> Result, String> { + let conn = state.db.lock().map_err(|e| e.to_string())?; + let mut stmt = conn.prepare( + "SELECT id, client_id, invoice_number, date, due_date, subtotal, tax_rate, tax_amount, discount, total, notes, status + FROM invoices ORDER BY date DESC" + ).map_err(|e| e.to_string())?; + let invoices = stmt.query_map([], |row| { + Ok(Invoice { + id: Some(row.get(0)?), + client_id: row.get(1)?, + invoice_number: row.get(2)?, + date: row.get(3)?, + due_date: row.get(4)?, + subtotal: row.get(5)?, + tax_rate: row.get(6)?, + tax_amount: row.get(7)?, + discount: row.get(8)?, + total: row.get(9)?, + notes: row.get(10)?, + status: row.get(11)?, + }) + }).map_err(|e| e.to_string())?; + invoices.collect::, _>>().map_err(|e| e.to_string()) +} + +// Settings commands +#[tauri::command] +pub fn get_settings(state: State) -> Result, String> { + let conn = state.db.lock().map_err(|e| e.to_string())?; + let mut stmt = conn.prepare("SELECT key, value FROM settings").map_err(|e| e.to_string())?; + let settings = stmt.query_map([], |row| { + Ok((row.get::<_, String>(0)?, row.get::<_, String>(1)?)) + }).map_err(|e| e.to_string())?; + + let mut result = std::collections::HashMap::new(); + for setting in settings { + let (key, value) = setting.map_err(|e| e.to_string())?; + result.insert(key, value); + } + Ok(result) +} + +#[tauri::command] +pub fn update_settings(state: State, key: String, value: String) -> Result<(), String> { + let conn = state.db.lock().map_err(|e| e.to_string())?; + conn.execute( + "INSERT OR REPLACE INTO settings (key, value) VALUES (?1, ?2)", + params![key, value], + ).map_err(|e| e.to_string())?; + Ok(()) +} diff --git a/src-tauri/src/database.rs b/src-tauri/src/database.rs new file mode 100644 index 0000000..4675e22 --- /dev/null +++ b/src-tauri/src/database.rs @@ -0,0 +1,118 @@ +use rusqlite::Connection; + +pub fn init_db(conn: &Connection) -> Result<(), rusqlite::Error> { + conn.execute( + "CREATE TABLE IF NOT EXISTS clients ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + name TEXT NOT NULL, + email TEXT, + address TEXT, + created_at TEXT DEFAULT CURRENT_TIMESTAMP + )", + [], + )?; + + conn.execute( + "CREATE TABLE IF NOT EXISTS projects ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + client_id INTEGER, + name TEXT NOT NULL, + hourly_rate REAL DEFAULT 0, + color TEXT DEFAULT '#F59E0B', + archived INTEGER DEFAULT 0, + created_at TEXT DEFAULT CURRENT_TIMESTAMP, + FOREIGN KEY (client_id) REFERENCES clients(id) + )", + [], + )?; + + conn.execute( + "CREATE TABLE IF NOT EXISTS tasks ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + project_id INTEGER NOT NULL, + name TEXT NOT NULL, + created_at TEXT DEFAULT CURRENT_TIMESTAMP, + FOREIGN KEY (project_id) REFERENCES projects(id) + )", + [], + )?; + + conn.execute( + "CREATE TABLE IF NOT EXISTS time_entries ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + project_id INTEGER NOT NULL, + task_id INTEGER, + description TEXT, + start_time TEXT NOT NULL, + end_time TEXT, + duration INTEGER DEFAULT 0, + created_at TEXT DEFAULT CURRENT_TIMESTAMP, + FOREIGN KEY (project_id) REFERENCES projects(id), + FOREIGN KEY (task_id) REFERENCES tasks(id) + )", + [], + )?; + + conn.execute( + "CREATE TABLE IF NOT EXISTS invoices ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + client_id INTEGER NOT NULL, + invoice_number TEXT NOT NULL, + date TEXT NOT NULL, + due_date TEXT, + subtotal REAL DEFAULT 0, + tax_rate REAL DEFAULT 0, + tax_amount REAL DEFAULT 0, + discount REAL DEFAULT 0, + total REAL DEFAULT 0, + notes TEXT, + status TEXT DEFAULT 'draft', + created_at TEXT DEFAULT CURRENT_TIMESTAMP, + FOREIGN KEY (client_id) REFERENCES clients(id) + )", + [], + )?; + + conn.execute( + "CREATE TABLE IF NOT EXISTS invoice_items ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + invoice_id INTEGER NOT NULL, + description TEXT NOT NULL, + quantity REAL DEFAULT 1, + rate REAL DEFAULT 0, + amount REAL DEFAULT 0, + time_entry_id INTEGER, + FOREIGN KEY (invoice_id) REFERENCES invoices(id), + FOREIGN KEY (time_entry_id) REFERENCES time_entries(id) + )", + [], + )?; + + conn.execute( + "CREATE TABLE IF NOT EXISTS settings ( + key TEXT PRIMARY KEY, + value TEXT + )", + [], + )?; + + // Insert default settings + conn.execute( + "INSERT OR IGNORE INTO settings (key, value) VALUES ('default_hourly_rate', '50')", + [], + )?; + conn.execute( + "INSERT OR IGNORE INTO settings (key, value) VALUES ('idle_detection', 'true')", + [], + )?; + conn.execute( + "INSERT OR IGNORE INTO settings (key, value) VALUES ('idle_timeout', '5')", + [], + )?; + conn.execute( + "INSERT OR IGNORE INTO settings (key, value) VALUES ('reminder_interval', '30')", + [], + )?; + + Ok(()) +} diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs new file mode 100644 index 0000000..1f6390d --- /dev/null +++ b/src-tauri/src/lib.rs @@ -0,0 +1,106 @@ +use rusqlite::Connection; +use serde::{Deserialize, Serialize}; +use std::sync::Mutex; +use tauri::{Manager, State}; +use std::path::PathBuf; + +mod database; +mod commands; + +pub struct AppState { + pub db: Mutex, +} + +fn get_data_dir() -> PathBuf { + let exe_path = std::env::current_exe().unwrap(); + let exe_dir = exe_path.parent().unwrap(); + let data_dir = exe_dir.join("data"); + std::fs::create_dir_all(&data_dir).ok(); + data_dir +} + +#[cfg_attr(mobile, tauri::mobile_entry_point)] +pub fn run() { + env_logger::init(); + + let data_dir = get_data_dir(); + let db_path = data_dir.join("timetracker.db"); + + let conn = Connection::open(&db_path).expect("Failed to open database"); + database::init_db(&conn).expect("Failed to initialize database"); + + tauri::Builder::default() + .plugin(tauri_plugin_shell::init()) + .plugin(tauri_plugin_dialog::init()) + .plugin(tauri_plugin_fs::init()) + .plugin(tauri_plugin_notification::init()) + .manage(AppState { db: Mutex::new(conn) }) + .invoke_handler(tauri::generate_handler![ + commands::get_clients, + commands::create_client, + commands::update_client, + commands::delete_client, + commands::get_projects, + commands::create_project, + commands::update_project, + commands::delete_project, + commands::get_tasks, + commands::create_task, + commands::delete_task, + commands::get_time_entries, + commands::create_time_entry, + commands::update_time_entry, + commands::delete_time_entry, + commands::get_reports, + commands::create_invoice, + commands::get_invoices, + commands::get_settings, + commands::update_settings, + ]) + .setup(|app| { + #[cfg(desktop)] + { + use tauri::tray::{MouseButton, MouseButtonState, TrayIconBuilder, TrayIconEvent}; + use tauri::menu::{Menu, MenuItem}; + + let quit = MenuItem::with_id(app, "quit", "Quit", true, None::<&str>)?; + let show = MenuItem::with_id(app, "show", "Show Window", true, None::<&str>)?; + let menu = Menu::with_items(app, &[&show, &quit])?; + + let _tray = TrayIconBuilder::new() + .menu(&menu) + .menu_on_left_click(false) + .on_menu_event(|app, event| { + match event.id.as_ref() { + "quit" => { + app.exit(0); + } + "show" => { + if let Some(window) = app.get_webview_window("main") { + window.show().ok(); + window.set_focus().ok(); + } + } + _ => {} + } + }) + .on_tray_icon_event(|tray, event| { + if let TrayIconEvent::Click { + button: MouseButton::Left, + button_state: MouseButtonState::Up, + .. + } = event { + let app = tray.app_handle(); + if let Some(window) = app.get_webview_window("main") { + window.show().ok(); + window.set_focus().ok(); + } + } + }) + .build(app)?; + } + Ok(()) + }) + .run(tauri::generate_context!()) + .expect("error while running tauri application"); +} diff --git a/src-tauri/src/main.rs b/src-tauri/src/main.rs new file mode 100644 index 0000000..a530cec --- /dev/null +++ b/src-tauri/src/main.rs @@ -0,0 +1,8 @@ +#![cfg_attr( + all(not(debug_assertions), target_os = "windows"), + windows_subsystem = "windows" +)] + +fn main() { + local_time_tracker_lib::run(); +} diff --git a/src-tauri/tauri.conf.json b/src-tauri/tauri.conf.json new file mode 100644 index 0000000..ca6e1c9 --- /dev/null +++ b/src-tauri/tauri.conf.json @@ -0,0 +1,56 @@ +{ + "$schema": "https://schema.tauri.app/config/2", + "productName": "LocalTimeTracker", + "version": "1.0.0", + "identifier": "com.localtimetracker.app", + "build": { + "beforeDevCommand": "npm run dev", + "devUrl": "http://localhost:1420", + "beforeBuildCommand": "npm run build", + "frontendDist": "../dist", + "devtools": true + }, + "app": { + "windows": [ + { + "title": "LocalTimeTracker", + "width": 1200, + "height": 800, + "minWidth": 800, + "minHeight": 600, + "decorations": false, + "transparent": false, + "resizable": true, + "center": true + } + ], + "trayIcon": { + "iconPath": "icons/icon.png", + "iconAsTemplate": true + }, + "security": { + "csp": null + } + }, + "bundle": { + "active": true, + "targets": "all", + "icon": [ + "icons/32x32.png", + "icons/128x128.png", + "icons/128x128@2x.png", + "icons/icon.icns", + "icons/icon.ico" + ], + "windows": { + "webviewInstallMode": { + "type": "embedBootstrapper" + } + } + }, + "plugins": { + "shell": { + "open": true + } + } +}