修复快速设置学生丢失并新增重置入口

This commit is contained in:
Linkong
2026-03-21 00:03:14 +08:00
parent 1fe9dbe66a
commit 12d6be331c
7 changed files with 4916 additions and 4693 deletions
+161 -40
View File
@@ -1,62 +1,183 @@
use parking_lot::RwLock;
use std::sync::Arc;
use tauri::State;
use tauri::{AppHandle, Emitter, State};
use crate::db::connection::{create_sqlite_connection, DatabaseType};
use crate::db::migration::Migration;
use crate::services::settings::{SettingsKey, SettingsValue, SettingsService};
use crate::services::{AutoScoreService, ThemeService};
use crate::services::permission::PermissionLevel;
use crate::state::AppState;
use super::response::IpcResponse;
fn check_admin_permission(state: &Arc<RwLock<AppState>>) -> Result<(), String> {
let state_guard = state.read();
let mut permissions = state_guard.permissions.write();
let sender_id = 0;
if !permissions.require_permission(sender_id, PermissionLevel::Admin) {
return Err("Permission denied: Admin required".to_string());
}
let state_guard = state.read();
let mut permissions = state_guard.permissions.write();
let sender_id = 0;
if !permissions.require_permission(sender_id, PermissionLevel::Admin) {
return Err("Permission denied: Admin required".to_string());
}
Ok(())
}
#[tauri::command]
pub async fn data_export_json(
state: State<'_, Arc<RwLock<AppState>>>,
) -> Result<IpcResponse<String>, String> {
check_admin_permission(&state)?;
fn sqlite_db_path(app_handle: &AppHandle) -> Result<String, String> {
if cfg!(all(debug_assertions, desktop)) {
return Ok("data.sql".to_string());
}
let state_guard = state.read();
let settings = state_guard.settings.read();
let settings_value = settings.get_all();
let settings_json = serde_json::to_value(settings_value)
.map_err(|e| format!("Failed to serialize settings: {}", e))?;
let data_service = state_guard.data.read();
let students: Vec<crate::services::data::StudentExport> = Vec::new();
let events: Vec<crate::services::data::EventExport> = Vec::new();
let export_data = data_service.export_json(settings_json, students, events);
let json_string = serde_json::to_string_pretty(&export_data)
.map_err(|e| format!("Failed to serialize export data: {}", e))?;
Ok(IpcResponse::success(json_string))
let app_data_dir = app_handle
.path()
.app_data_dir()
.map_err(|e| format!("Failed to get app data directory: {}", e))?;
let data_dir = app_data_dir.join("data");
std::fs::create_dir_all(&data_dir)
.map_err(|e| format!("Failed to create data directory: {}", e))?;
let db_path = data_dir.join("data.sql");
db_path
.to_str()
.map(|s| s.to_string())
.ok_or_else(|| "Invalid sqlite database path".to_string())
}
#[tauri::command]
#[tauri::command]
pub async fn data_export_json(
state: State<'_, Arc<RwLock<AppState>>>,
) -> Result<IpcResponse<String>, String> {
check_admin_permission(&state)?;
let state_guard = state.read();
let settings = state_guard.settings.read();
let settings_value = settings.get_all();
let settings_json = serde_json::to_value(settings_value)
.map_err(|e| format!("Failed to serialize settings: {}", e))?;
let data_service = state_guard.data.read();
let students: Vec<crate::services::data::StudentExport> = Vec::new();
let events: Vec<crate::services::data::EventExport> = Vec::new();
let export_data = data_service.export_json(settings_json, students, events);
let json_string = serde_json::to_string_pretty(&export_data)
.map_err(|e| format!("Failed to serialize export data: {}", e))?;
Ok(IpcResponse::success(json_string))
}
#[tauri::command]
pub async fn data_import_json(
json_text: String,
state: State<'_, Arc<RwLock<AppState>>>,
) -> Result<IpcResponse<()>, String> {
check_admin_permission(&state)?;
let state_guard = state.read();
let mut data_service = state_guard.data.write();
let result = data_service.import_json(&json_text);
if result.success {
Ok(IpcResponse::success_empty())
} else {
check_admin_permission(&state)?;
let state_guard = state.read();
let mut data_service = state_guard.data.write();
let result = data_service.import_json(&json_text);
if result.success {
Ok(IpcResponse::success_empty())
} else {
Ok(IpcResponse::error(
result.message.as_deref().unwrap_or("Import failed"),
))
}
}
#[tauri::command]
pub async fn data_reset_all(
app_handle: AppHandle,
state: State<'_, Arc<RwLock<AppState>>>,
) -> Result<IpcResponse<()>, String> {
check_admin_permission(&state)?;
let sqlite_path = sqlite_db_path(&app_handle)?;
let sqlite_conn = create_sqlite_connection(&sqlite_path)
.await
.map_err(|e| format!("Failed to open sqlite database: {}", e))?;
let (active_conn, active_db_type) = {
let state_guard = state.read();
let active_conn = state_guard
.db
.read()
.clone()
.ok_or_else(|| "Database not connected".to_string())?;
let db_type = {
let settings = state_guard.settings.read();
match settings.get_value(SettingsKey::PgConnectionStatus) {
SettingsValue::Json(json) => match json.get("type").and_then(|value| value.as_str()) {
Some("postgresql") => DatabaseType::PostgreSQL,
_ => DatabaseType::SQLite,
},
_ => DatabaseType::SQLite,
}
};
(active_conn, db_type)
};
if active_db_type == DatabaseType::PostgreSQL {
Migration::reset_database(&active_conn, false)
.await
.map_err(|e| format!("Failed to reset PostgreSQL database: {}", e))?;
}
Migration::reset_database(&sqlite_conn, true)
.await
.map_err(|e| format!("Failed to reset SQLite database: {}", e))?;
{
let state_guard = state.read();
{
let mut db = state_guard.db.write();
*db = Some(sqlite_conn.clone());
}
{
let mut settings = state_guard.settings.write();
*settings = SettingsService::new();
settings.attach_db(Some(sqlite_conn.clone()));
settings
.initialize()
.await
.map_err(|e| format!("Failed to reinitialize settings: {}", e))?;
settings
.set_value(
SettingsKey::PgConnectionStatus,
SettingsValue::Json(serde_json::json!({
"connected": true,
"type": "sqlite"
})),
)
.await
.map_err(|e| format!("Failed to restore sqlite status: {}", e))?;
}
{
let mut permissions = state_guard.permissions.write();
permissions.clear_all_permissions();
permissions.update_password_status(false, false);
}
{
let mut theme = state_guard.theme.write();
*theme = ThemeService::new();
}
{
let mut auto_score = state_guard.auto_score.write();
*auto_score = AutoScoreService::new();
}
}
let _ = app_handle.emit(
"settings:changed",
serde_json::json!({
"key": "is_wizard_completed",
"value": false
}),
);
Ok(IpcResponse::success_empty())
}
+386 -385
View File
@@ -1,388 +1,389 @@
pub mod commands;
pub mod db;
pub mod models;
pub mod services;
pub mod state;
pub mod utils;
use crate::db::connection::DatabaseType;
use crate::db::connection::{create_postgres_connection, create_sqlite_connection};
use crate::db::migration::run_migration;
use crate::services::settings::{SettingsKey, SettingsValue};
use crate::{commands::*, state::AppState};
use parking_lot::RwLock;
use std::sync::Arc;
#[cfg(desktop)]
use tauri::{
image::Image,
menu::{Menu, MenuItem},
tray::{MouseButton, MouseButtonState, TrayIconBuilder, TrayIconEvent},
WindowEvent,
};
use tauri::{App, Manager};
use tokio::time::{timeout, Duration};
#[cfg_attr(mobile, tauri::mobile_entry_point)]
pub fn run() {
tauri::Builder::default()
.plugin(tauri_plugin_shell::init())
.setup(|app| {
let state = AppState::new(app.handle().clone());
app.manage(Arc::new(RwLock::new(state)));
setup_app(app)?;
Ok(())
})
.invoke_handler(tauri::generate_handler![
student_query,
student_create,
student_update,
student_delete,
student_import_from_xlsx,
tags_get_all,
tags_get_by_student,
tags_create,
tags_delete,
tags_update_student_tags,
reason_query,
reason_create,
reason_update,
reason_delete,
reward_setting_query,
reward_setting_create,
reward_setting_update,
reward_setting_delete,
reward_redeem,
reward_redemption_query,
event_query,
event_create,
event_delete,
event_query_by_student,
leaderboard_query,
db_settlement_query,
db_settlement_create,
db_settlement_leaderboard,
settings_get_all,
settings_get,
settings_set,
auth_get_status,
auth_login,
auth_logout,
auth_set_passwords,
auth_generate_recovery,
auth_reset_by_recovery,
auth_clear_all,
theme_list,
theme_current,
theme_set,
theme_save,
theme_delete,
auto_score_get_rules,
auto_score_add_rule,
auto_score_update_rule,
auto_score_delete_rule,
auto_score_toggle_rule,
auto_score_get_status,
auto_score_sort_rules,
board_query_sql,
log_query,
log_clear,
log_set_level,
log_write,
pub mod commands;
pub mod db;
pub mod models;
pub mod services;
pub mod state;
pub mod utils;
use crate::db::connection::DatabaseType;
use crate::db::connection::{create_postgres_connection, create_sqlite_connection};
use crate::db::migration::run_migration;
use crate::services::settings::{SettingsKey, SettingsValue};
use crate::{commands::*, state::AppState};
use parking_lot::RwLock;
use std::sync::Arc;
#[cfg(desktop)]
use tauri::{
image::Image,
menu::{Menu, MenuItem},
tray::{MouseButton, MouseButtonState, TrayIconBuilder, TrayIconEvent},
WindowEvent,
};
use tauri::{App, Manager};
use tokio::time::{timeout, Duration};
#[cfg_attr(mobile, tauri::mobile_entry_point)]
pub fn run() {
tauri::Builder::default()
.plugin(tauri_plugin_shell::init())
.setup(|app| {
let state = AppState::new(app.handle().clone());
app.manage(Arc::new(RwLock::new(state)));
setup_app(app)?;
Ok(())
})
.invoke_handler(tauri::generate_handler![
student_query,
student_create,
student_update,
student_delete,
student_import_from_xlsx,
tags_get_all,
tags_get_by_student,
tags_create,
tags_delete,
tags_update_student_tags,
reason_query,
reason_create,
reason_update,
reason_delete,
reward_setting_query,
reward_setting_create,
reward_setting_update,
reward_setting_delete,
reward_redeem,
reward_redemption_query,
event_query,
event_create,
event_delete,
event_query_by_student,
leaderboard_query,
db_settlement_query,
db_settlement_create,
db_settlement_leaderboard,
settings_get_all,
settings_get,
settings_set,
auth_get_status,
auth_login,
auth_logout,
auth_set_passwords,
auth_generate_recovery,
auth_reset_by_recovery,
auth_clear_all,
theme_list,
theme_current,
theme_set,
theme_save,
theme_delete,
auto_score_get_rules,
auto_score_add_rule,
auto_score_update_rule,
auto_score_delete_rule,
auto_score_toggle_rule,
auto_score_get_status,
auto_score_sort_rules,
board_query_sql,
log_query,
log_clear,
log_set_level,
log_write,
data_export_json,
data_import_json,
data_reset_all,
window_minimize,
window_maximize,
window_close,
window_is_maximized,
toggle_devtools,
window_resize,
window_set_resizable,
db_test_connection,
db_switch_connection,
db_get_status,
db_sync,
db_sync_preview,
db_sync_apply,
fs_get_config_structure,
fs_read_json,
fs_write_json,
fs_read_text,
fs_write_text,
fs_delete_file,
fs_list_files,
fs_file_exists,
http_server_start,
http_server_stop,
http_server_status,
mcp_server_start,
mcp_server_stop,
mcp_server_status,
register_url_protocol,
app_quit,
app_restart,
])
.run(tauri::generate_context!())
.expect("error while running tauri application");
}
pub fn setup_app(app: &mut App) -> Result<(), Box<dyn std::error::Error>> {
setup_database(app)?;
setup_tray(app)?;
setup_window_events(app)?;
Ok(())
}
fn setup_database(app: &mut App) -> Result<(), Box<dyn std::error::Error>> {
const DB_CONNECT_TIMEOUT_SECS: u64 = 15;
const DB_MIGRATION_TIMEOUT_SECS: u64 = 20;
let handle = app.handle().clone();
let db_path = if cfg!(all(debug_assertions, desktop)) {
std::path::PathBuf::from("data.sql")
} else {
let app_data_dir = handle
.path()
.app_data_dir()
.map_err(|e| format!("Failed to get app data directory: {}", e))?;
let data_dir = app_data_dir.join("data");
std::fs::create_dir_all(&data_dir)
.map_err(|e| format!("Failed to create data directory: {}", e))?;
data_dir.join("data.sql")
};
let db_path_str = db_path.to_str().ok_or("Invalid database path")?.to_string();
let db_result = tauri::async_runtime::block_on(async {
let sqlite_conn = create_sqlite_connection(&db_path_str).await?;
run_migration(&sqlite_conn, DatabaseType::SQLite).await?;
let state = handle.state::<crate::state::SafeAppState>();
let state_guard = state.write();
let mut active_conn = sqlite_conn.clone();
{
let mut settings = state_guard.settings.write();
settings.attach_db(Some(sqlite_conn.clone()));
settings
.initialize()
.await
.map_err(|e| format!("Failed to initialize settings from sqlite: {}", e))?;
let pg_connection_string = match settings.get_value(SettingsKey::PgConnectionString) {
SettingsValue::String(s) => s,
_ => String::new(),
};
if !pg_connection_string.trim().is_empty() {
match timeout(
Duration::from_secs(DB_CONNECT_TIMEOUT_SECS),
create_postgres_connection(&pg_connection_string),
)
.await
{
Err(_) => {
let timeout_message = "PostgreSQL auto-connect timeout".to_string();
eprintln!(
"PostgreSQL auto-connect timed out on startup, fallback to sqlite"
);
settings
.set_value(
SettingsKey::PgConnectionStatus,
SettingsValue::Json(serde_json::json!({
"connected": false,
"type": "sqlite",
"error": timeout_message
})),
)
.await
.map_err(|err| format!("Failed to save pg status: {}", err))?;
}
Ok(Err(e)) => {
eprintln!("PostgreSQL auto-connect failed, fallback to sqlite: {}", e);
settings
.set_value(
SettingsKey::PgConnectionStatus,
SettingsValue::Json(serde_json::json!({
"connected": false,
"type": "sqlite",
"error": e.to_string()
})),
)
.await
.map_err(|err| format!("Failed to save pg status: {}", err))?;
}
Ok(Ok(pg_conn)) => {
let migration_result = timeout(
Duration::from_secs(DB_MIGRATION_TIMEOUT_SECS),
run_migration(&pg_conn, DatabaseType::PostgreSQL),
)
.await;
if let Err(_) = migration_result {
let timeout_message = "PostgreSQL migration timeout".to_string();
eprintln!(
"PostgreSQL migration timed out on startup, fallback to sqlite"
);
settings
.set_value(
SettingsKey::PgConnectionStatus,
SettingsValue::Json(serde_json::json!({
"connected": false,
"type": "sqlite",
"error": timeout_message
})),
)
.await
.map_err(|err| format!("Failed to save pg status: {}", err))?;
} else if let Ok(Err(e)) = migration_result {
eprintln!(
"PostgreSQL migration failed on startup, fallback to sqlite: {}",
e
);
settings
.set_value(
SettingsKey::PgConnectionStatus,
SettingsValue::Json(serde_json::json!({
"connected": false,
"type": "sqlite",
"error": e.to_string()
})),
)
.await
.map_err(|err| format!("Failed to save pg status: {}", err))?;
} else {
active_conn = pg_conn;
settings
.set_value(
SettingsKey::PgConnectionStatus,
SettingsValue::Json(serde_json::json!({
"connected": true,
"type": "postgresql"
})),
)
.await
.map_err(|err| format!("Failed to save pg status: {}", err))?;
eprintln!("Auto connected to PostgreSQL from saved connection string");
}
}
}
} else {
settings
.set_value(
SettingsKey::PgConnectionStatus,
SettingsValue::Json(serde_json::json!({
"connected": true,
"type": "sqlite"
})),
)
.await
.map_err(|err| format!("Failed to save sqlite status: {}", err))?;
}
}
{
let mut db_guard = state_guard.db.write();
*db_guard = Some(active_conn);
}
Ok::<_, Box<dyn std::error::Error>>(())
});
if let Err(e) = db_result {
eprintln!("Failed to connect to database: {}", e);
} else {
eprintln!(
"Database bootstrap completed, sqlite settings db: {}",
db_path_str
);
}
Ok(())
}
#[cfg(desktop)]
fn setup_tray(app: &mut App) -> Result<(), Box<dyn std::error::Error>> {
let show_item = MenuItem::with_id(app, "show", "显示窗口", true, None::<&str>)?;
let hide_item = MenuItem::with_id(app, "hide", "隐藏窗口", true, None::<&str>)?;
let quit_item = MenuItem::with_id(app, "quit", "退出", true, None::<&str>)?;
let menu = Menu::with_items(app, &[&show_item, &hide_item, &quit_item])?;
let _tray = TrayIconBuilder::new()
.icon(Image::from_bytes(include_bytes!("../icons/icon.png"))?)
.menu(&menu)
.show_menu_on_left_click(false)
.on_menu_event(|app, event| match event.id.as_ref() {
"show" => {
if let Some(window) = app.get_webview_window("main") {
let _ = window.show();
let _ = window.set_focus();
}
}
"hide" => {
if let Some(window) = app.get_webview_window("main") {
let _ = window.hide();
}
}
"quit" => {
app.exit(0);
}
_ => {}
})
.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") {
let _ = window.show();
let _ = window.set_focus();
}
}
})
.build(app)?;
Ok(())
}
#[cfg(not(desktop))]
fn setup_tray(_app: &mut App) -> Result<(), Box<dyn std::error::Error>> {
Ok(())
}
#[cfg(desktop)]
fn setup_window_events(app: &mut App) -> Result<(), Box<dyn std::error::Error>> {
if let Some(window) = app.get_webview_window("main") {
let _ = window.set_size(tauri::Size::Logical(tauri::LogicalSize {
width: 1180.0,
height: 680.0,
}));
let _ = window.center();
#[cfg(target_os = "macos")]
{
let _ = window.set_shadow(true);
}
let window_clone = window.clone();
window.on_window_event(move |event| {
if let WindowEvent::CloseRequested { api, .. } = event {
api.prevent_close();
let _ = window_clone.hide();
}
});
}
Ok(())
}
#[cfg(not(desktop))]
fn setup_window_events(_app: &mut App) -> Result<(), Box<dyn std::error::Error>> {
Ok(())
}
window_maximize,
window_close,
window_is_maximized,
toggle_devtools,
window_resize,
window_set_resizable,
db_test_connection,
db_switch_connection,
db_get_status,
db_sync,
db_sync_preview,
db_sync_apply,
fs_get_config_structure,
fs_read_json,
fs_write_json,
fs_read_text,
fs_write_text,
fs_delete_file,
fs_list_files,
fs_file_exists,
http_server_start,
http_server_stop,
http_server_status,
mcp_server_start,
mcp_server_stop,
mcp_server_status,
register_url_protocol,
app_quit,
app_restart,
])
.run(tauri::generate_context!())
.expect("error while running tauri application");
}
pub fn setup_app(app: &mut App) -> Result<(), Box<dyn std::error::Error>> {
setup_database(app)?;
setup_tray(app)?;
setup_window_events(app)?;
Ok(())
}
fn setup_database(app: &mut App) -> Result<(), Box<dyn std::error::Error>> {
const DB_CONNECT_TIMEOUT_SECS: u64 = 15;
const DB_MIGRATION_TIMEOUT_SECS: u64 = 20;
let handle = app.handle().clone();
let db_path = if cfg!(all(debug_assertions, desktop)) {
std::path::PathBuf::from("data.sql")
} else {
let app_data_dir = handle
.path()
.app_data_dir()
.map_err(|e| format!("Failed to get app data directory: {}", e))?;
let data_dir = app_data_dir.join("data");
std::fs::create_dir_all(&data_dir)
.map_err(|e| format!("Failed to create data directory: {}", e))?;
data_dir.join("data.sql")
};
let db_path_str = db_path.to_str().ok_or("Invalid database path")?.to_string();
let db_result = tauri::async_runtime::block_on(async {
let sqlite_conn = create_sqlite_connection(&db_path_str).await?;
run_migration(&sqlite_conn, DatabaseType::SQLite).await?;
let state = handle.state::<crate::state::SafeAppState>();
let state_guard = state.write();
let mut active_conn = sqlite_conn.clone();
{
let mut settings = state_guard.settings.write();
settings.attach_db(Some(sqlite_conn.clone()));
settings
.initialize()
.await
.map_err(|e| format!("Failed to initialize settings from sqlite: {}", e))?;
let pg_connection_string = match settings.get_value(SettingsKey::PgConnectionString) {
SettingsValue::String(s) => s,
_ => String::new(),
};
if !pg_connection_string.trim().is_empty() {
match timeout(
Duration::from_secs(DB_CONNECT_TIMEOUT_SECS),
create_postgres_connection(&pg_connection_string),
)
.await
{
Err(_) => {
let timeout_message = "PostgreSQL auto-connect timeout".to_string();
eprintln!(
"PostgreSQL auto-connect timed out on startup, fallback to sqlite"
);
settings
.set_value(
SettingsKey::PgConnectionStatus,
SettingsValue::Json(serde_json::json!({
"connected": false,
"type": "sqlite",
"error": timeout_message
})),
)
.await
.map_err(|err| format!("Failed to save pg status: {}", err))?;
}
Ok(Err(e)) => {
eprintln!("PostgreSQL auto-connect failed, fallback to sqlite: {}", e);
settings
.set_value(
SettingsKey::PgConnectionStatus,
SettingsValue::Json(serde_json::json!({
"connected": false,
"type": "sqlite",
"error": e.to_string()
})),
)
.await
.map_err(|err| format!("Failed to save pg status: {}", err))?;
}
Ok(Ok(pg_conn)) => {
let migration_result = timeout(
Duration::from_secs(DB_MIGRATION_TIMEOUT_SECS),
run_migration(&pg_conn, DatabaseType::PostgreSQL),
)
.await;
if let Err(_) = migration_result {
let timeout_message = "PostgreSQL migration timeout".to_string();
eprintln!(
"PostgreSQL migration timed out on startup, fallback to sqlite"
);
settings
.set_value(
SettingsKey::PgConnectionStatus,
SettingsValue::Json(serde_json::json!({
"connected": false,
"type": "sqlite",
"error": timeout_message
})),
)
.await
.map_err(|err| format!("Failed to save pg status: {}", err))?;
} else if let Ok(Err(e)) = migration_result {
eprintln!(
"PostgreSQL migration failed on startup, fallback to sqlite: {}",
e
);
settings
.set_value(
SettingsKey::PgConnectionStatus,
SettingsValue::Json(serde_json::json!({
"connected": false,
"type": "sqlite",
"error": e.to_string()
})),
)
.await
.map_err(|err| format!("Failed to save pg status: {}", err))?;
} else {
active_conn = pg_conn;
settings
.set_value(
SettingsKey::PgConnectionStatus,
SettingsValue::Json(serde_json::json!({
"connected": true,
"type": "postgresql"
})),
)
.await
.map_err(|err| format!("Failed to save pg status: {}", err))?;
eprintln!("Auto connected to PostgreSQL from saved connection string");
}
}
}
} else {
settings
.set_value(
SettingsKey::PgConnectionStatus,
SettingsValue::Json(serde_json::json!({
"connected": true,
"type": "sqlite"
})),
)
.await
.map_err(|err| format!("Failed to save sqlite status: {}", err))?;
}
}
{
let mut db_guard = state_guard.db.write();
*db_guard = Some(active_conn);
}
Ok::<_, Box<dyn std::error::Error>>(())
});
if let Err(e) = db_result {
eprintln!("Failed to connect to database: {}", e);
} else {
eprintln!(
"Database bootstrap completed, sqlite settings db: {}",
db_path_str
);
}
Ok(())
}
#[cfg(desktop)]
fn setup_tray(app: &mut App) -> Result<(), Box<dyn std::error::Error>> {
let show_item = MenuItem::with_id(app, "show", "显示窗口", true, None::<&str>)?;
let hide_item = MenuItem::with_id(app, "hide", "隐藏窗口", true, None::<&str>)?;
let quit_item = MenuItem::with_id(app, "quit", "退出", true, None::<&str>)?;
let menu = Menu::with_items(app, &[&show_item, &hide_item, &quit_item])?;
let _tray = TrayIconBuilder::new()
.icon(Image::from_bytes(include_bytes!("../icons/icon.png"))?)
.menu(&menu)
.show_menu_on_left_click(false)
.on_menu_event(|app, event| match event.id.as_ref() {
"show" => {
if let Some(window) = app.get_webview_window("main") {
let _ = window.show();
let _ = window.set_focus();
}
}
"hide" => {
if let Some(window) = app.get_webview_window("main") {
let _ = window.hide();
}
}
"quit" => {
app.exit(0);
}
_ => {}
})
.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") {
let _ = window.show();
let _ = window.set_focus();
}
}
})
.build(app)?;
Ok(())
}
#[cfg(not(desktop))]
fn setup_tray(_app: &mut App) -> Result<(), Box<dyn std::error::Error>> {
Ok(())
}
#[cfg(desktop)]
fn setup_window_events(app: &mut App) -> Result<(), Box<dyn std::error::Error>> {
if let Some(window) = app.get_webview_window("main") {
let _ = window.set_size(tauri::Size::Logical(tauri::LogicalSize {
width: 1180.0,
height: 680.0,
}));
let _ = window.center();
#[cfg(target_os = "macos")]
{
let _ = window.set_shadow(true);
}
let window_clone = window.clone();
window.on_window_event(move |event| {
if let WindowEvent::CloseRequested { api, .. } = event {
api.prevent_close();
let _ = window_clone.hide();
}
});
}
Ok(())
}
#[cfg(not(desktop))]
fn setup_window_events(_app: &mut App) -> Result<(), Box<dyn std::error::Error>> {
Ok(())
}
File diff suppressed because it is too large Load Diff
+1383 -1321
View File
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+401 -400
View File
@@ -1,404 +1,405 @@
import { invoke } from "@tauri-apps/api/core"
import { listen, UnlistenFn } from "@tauri-apps/api/event"
export interface themeConfig {
name: string
id: string
mode: "light" | "dark"
config: {
tdesign: Record<string, string>
custom: Record<string, string>
}
}
export interface settingChange {
key: string
value: any
oldValue: any
}
export type settingsKey =
| "is_wizard_completed"
| "log_level"
| "window_zoom"
| "search_keyboard_layout"
| "disable_search_keyboard"
| "themes_custom"
| "auto_score_enabled"
| "auto_score_rules"
| "current_theme_id"
| "dashboards_config"
| "pg_connection_string"
| "pg_connection_status"
export interface settingsSpec {
is_wizard_completed: boolean
log_level: string
window_zoom: number
search_keyboard_layout: "t9" | "qwerty26"
disable_search_keyboard: boolean
themes_custom: themeConfig[]
auto_score_enabled: boolean
auto_score_rules: any[]
current_theme_id: string
dashboards_config: any[]
pg_connection_string: string
pg_connection_status: {
connected: boolean
type: "sqlite" | "postgresql"
error?: string
}
}
const api = {
// Theme
getThemes: (): Promise<{ success: boolean; data: themeConfig[] }> => invoke("theme_list"),
getCurrentTheme: (): Promise<{ success: boolean; data: themeConfig }> => invoke("theme_current"),
setTheme: (themeId: string): Promise<{ success: boolean }> => invoke("theme_set", { themeId }),
saveTheme: (theme: themeConfig): Promise<{ success: boolean }> => invoke("theme_save", { theme }),
deleteTheme: (themeId: string): Promise<{ success: boolean }> =>
invoke("theme_delete", { themeId }),
onThemeChanged: (callback: (theme: themeConfig) => void): Promise<UnlistenFn> => {
return listen<themeConfig | { theme?: themeConfig }>("theme:updated", (event) => {
const payload = event.payload as themeConfig | { theme?: themeConfig } | undefined
const theme =
payload && typeof payload === "object" && "theme" in payload
? payload.theme
: (payload as themeConfig | undefined)
if (theme) callback(theme)
})
},
// DB - Student
queryStudents: (params?: any): Promise<{ success: boolean; data: any[] }> =>
invoke("student_query", { params }),
createStudent: (data: {
name: string
}): Promise<{ success: boolean; data?: number; message?: string }> =>
invoke("student_create", { data }),
updateStudent: (id: number, data: any): Promise<{ success: boolean }> =>
invoke("student_update", { id, data }),
deleteStudent: (id: number): Promise<{ success: boolean }> => invoke("student_delete", { id }),
importStudentsFromXlsx: (params: {
names: string[]
}): Promise<{ success: boolean; data: { inserted: number; skipped: number; total: number } }> =>
invoke("student_import_from_xlsx", { params }),
// DB - Tags
tagsGetAll: (): Promise<{ success: boolean; data: any[] }> => invoke("tags_get_all"),
tagsGetByStudent: (studentId: number): Promise<{ success: boolean; data: any[] }> =>
invoke("tags_get_by_student", { studentId }),
tagsCreate: (name: string): Promise<{ success: boolean; data: any }> =>
invoke("tags_create", { name }),
tagsDelete: (id: number): Promise<{ success: boolean }> => invoke("tags_delete", { id }),
tagsUpdateStudentTags: (studentId: number, tagIds: number[]): Promise<{ success: boolean }> =>
invoke("tags_update_student_tags", { studentId, tagIds }),
// DB - Reason
queryReasons: (): Promise<{ success: boolean; data: any[] }> => invoke("reason_query"),
createReason: (data: any): Promise<{ success: boolean; data?: number; message?: string }> =>
invoke("reason_create", { data }),
updateReason: (id: number, data: any): Promise<{ success: boolean }> =>
invoke("reason_update", { id, data }),
deleteReason: (id: number): Promise<{ success: boolean }> => invoke("reason_delete", { id }),
// DB - Reward
rewardSettingQuery: (): Promise<{ success: boolean; data: any[] }> => invoke("reward_setting_query"),
rewardSettingCreate: (data: {
name: string
cost_points: number
}): Promise<{ success: boolean; data?: number; message?: string }> =>
invoke("reward_setting_create", { data }),
rewardSettingUpdate: (id: number, data: any): Promise<{ success: boolean; message?: string }> =>
invoke("reward_setting_update", { id, data }),
rewardSettingDelete: (id: number): Promise<{ success: boolean; message?: string }> =>
invoke("reward_setting_delete", { id }),
rewardRedeem: (data: {
student_name: string
reward_id: number
}): Promise<{
success: boolean
data?: { redemption_id: number; remaining_reward_points: number }
message?: string
}> => invoke("reward_redeem", { data }),
rewardRedemptionQuery: (params?: { limit?: number }): Promise<{ success: boolean; data: any[] }> =>
invoke("reward_redemption_query", { params }),
// DB - Event
queryEvents: (params?: { limit?: number }): Promise<{ success: boolean; data: any[] }> =>
invoke("event_query", { params }),
createEvent: (data: {
studentName: string
reasonContent: string
delta: number
}): Promise<{ success: boolean; data?: number; message?: string }> =>
invoke("event_create", { data }),
deleteEvent: (uuid: string): Promise<{ success: boolean }> => invoke("event_delete", { uuid }),
queryEventsByStudent: (params: {
studentName: string
limit?: number
startTime?: string
}): Promise<{ success: boolean; data: any[] }> => invoke("event_query_by_student", { params }),
queryLeaderboard: (params: {
range: "today" | "week" | "month"
}): Promise<{ success: boolean; data: { startTime: string; rows: any[] } }> =>
invoke("leaderboard_query", { params }),
boardQuerySql: (params: {
sql: string
limit?: number
}): Promise<{ success: boolean; data: any[]; message?: string }> =>
invoke("board_query_sql", { params }),
// Settlement
querySettlements: (): Promise<{ success: boolean; data: any[] }> => invoke("db_settlement_query"),
createSettlement: (): Promise<{ success: boolean; data: any }> => invoke("db_settlement_create"),
querySettlementLeaderboard: (params: {
settlementId: number
}): Promise<{ success: boolean; data: any }> => invoke("db_settlement_leaderboard", { params }),
// Settings & Sync
getAllSettings: (): Promise<{ success: boolean; data: settingsSpec }> =>
invoke("settings_get_all"),
getSetting: <K extends settingsKey>(
key: K
): Promise<{ success: boolean; data: settingsSpec[K] }> => invoke("settings_get", { key }),
setSetting: <K extends settingsKey>(
key: K,
value: settingsSpec[K]
): Promise<{ success: boolean }> => invoke("settings_set", { key, value }),
onSettingChanged: (callback: (change: settingChange) => void): Promise<UnlistenFn> => {
return listen<settingChange>("settings:changed", (event) => {
callback(event.payload)
})
},
// Auth & Security
authGetStatus: (): Promise<{
success: boolean
data: {
permission: string
hasAdminPassword: boolean
hasPointsPassword: boolean
hasRecoveryString: boolean
}
}> => invoke("auth_get_status"),
authLogin: (
password: string
): Promise<{ success: boolean; data?: { permission: string }; message?: string }> =>
invoke("auth_login", { password }),
authLogout: (): Promise<{ success: boolean; data: { permission: string } }> =>
invoke("auth_logout"),
authSetPasswords: (payload: {
adminPassword?: string | null
pointsPassword?: string | null
}): Promise<{ success: boolean; data?: { recoveryString: string }; message?: string }> =>
invoke("auth_set_passwords", payload),
authGenerateRecovery: (): Promise<{ success: boolean; data: { recoveryString: string } }> =>
invoke("auth_generate_recovery"),
authResetByRecovery: (
recoveryString: string
): Promise<{ success: boolean; data?: { recoveryString: string }; message?: string }> =>
invoke("auth_reset_by_recovery", { recoveryString }),
authClearAll: (): Promise<{ success: boolean }> => invoke("auth_clear_all"),
import { invoke } from "@tauri-apps/api/core"
import { listen, UnlistenFn } from "@tauri-apps/api/event"
export interface themeConfig {
name: string
id: string
mode: "light" | "dark"
config: {
tdesign: Record<string, string>
custom: Record<string, string>
}
}
export interface settingChange {
key: string
value: any
oldValue: any
}
export type settingsKey =
| "is_wizard_completed"
| "log_level"
| "window_zoom"
| "search_keyboard_layout"
| "disable_search_keyboard"
| "themes_custom"
| "auto_score_enabled"
| "auto_score_rules"
| "current_theme_id"
| "dashboards_config"
| "pg_connection_string"
| "pg_connection_status"
export interface settingsSpec {
is_wizard_completed: boolean
log_level: string
window_zoom: number
search_keyboard_layout: "t9" | "qwerty26"
disable_search_keyboard: boolean
themes_custom: themeConfig[]
auto_score_enabled: boolean
auto_score_rules: any[]
current_theme_id: string
dashboards_config: any[]
pg_connection_string: string
pg_connection_status: {
connected: boolean
type: "sqlite" | "postgresql"
error?: string
}
}
const api = {
// Theme
getThemes: (): Promise<{ success: boolean; data: themeConfig[] }> => invoke("theme_list"),
getCurrentTheme: (): Promise<{ success: boolean; data: themeConfig }> => invoke("theme_current"),
setTheme: (themeId: string): Promise<{ success: boolean }> => invoke("theme_set", { themeId }),
saveTheme: (theme: themeConfig): Promise<{ success: boolean }> => invoke("theme_save", { theme }),
deleteTheme: (themeId: string): Promise<{ success: boolean }> =>
invoke("theme_delete", { themeId }),
onThemeChanged: (callback: (theme: themeConfig) => void): Promise<UnlistenFn> => {
return listen<themeConfig | { theme?: themeConfig }>("theme:updated", (event) => {
const payload = event.payload as themeConfig | { theme?: themeConfig } | undefined
const theme =
payload && typeof payload === "object" && "theme" in payload
? payload.theme
: (payload as themeConfig | undefined)
if (theme) callback(theme)
})
},
// DB - Student
queryStudents: (params?: any): Promise<{ success: boolean; data: any[] }> =>
invoke("student_query", { params }),
createStudent: (data: {
name: string
}): Promise<{ success: boolean; data?: number; message?: string }> =>
invoke("student_create", { data }),
updateStudent: (id: number, data: any): Promise<{ success: boolean }> =>
invoke("student_update", { id, data }),
deleteStudent: (id: number): Promise<{ success: boolean }> => invoke("student_delete", { id }),
importStudentsFromXlsx: (params: {
names: string[]
}): Promise<{ success: boolean; data: { inserted: number; skipped: number; total: number } }> =>
invoke("student_import_from_xlsx", { params }),
// DB - Tags
tagsGetAll: (): Promise<{ success: boolean; data: any[] }> => invoke("tags_get_all"),
tagsGetByStudent: (studentId: number): Promise<{ success: boolean; data: any[] }> =>
invoke("tags_get_by_student", { studentId }),
tagsCreate: (name: string): Promise<{ success: boolean; data: any }> =>
invoke("tags_create", { name }),
tagsDelete: (id: number): Promise<{ success: boolean }> => invoke("tags_delete", { id }),
tagsUpdateStudentTags: (studentId: number, tagIds: number[]): Promise<{ success: boolean }> =>
invoke("tags_update_student_tags", { studentId, tagIds }),
// DB - Reason
queryReasons: (): Promise<{ success: boolean; data: any[] }> => invoke("reason_query"),
createReason: (data: any): Promise<{ success: boolean; data?: number; message?: string }> =>
invoke("reason_create", { data }),
updateReason: (id: number, data: any): Promise<{ success: boolean }> =>
invoke("reason_update", { id, data }),
deleteReason: (id: number): Promise<{ success: boolean }> => invoke("reason_delete", { id }),
// DB - Reward
rewardSettingQuery: (): Promise<{ success: boolean; data: any[] }> => invoke("reward_setting_query"),
rewardSettingCreate: (data: {
name: string
cost_points: number
}): Promise<{ success: boolean; data?: number; message?: string }> =>
invoke("reward_setting_create", { data }),
rewardSettingUpdate: (id: number, data: any): Promise<{ success: boolean; message?: string }> =>
invoke("reward_setting_update", { id, data }),
rewardSettingDelete: (id: number): Promise<{ success: boolean; message?: string }> =>
invoke("reward_setting_delete", { id }),
rewardRedeem: (data: {
student_name: string
reward_id: number
}): Promise<{
success: boolean
data?: { redemption_id: number; remaining_reward_points: number }
message?: string
}> => invoke("reward_redeem", { data }),
rewardRedemptionQuery: (params?: { limit?: number }): Promise<{ success: boolean; data: any[] }> =>
invoke("reward_redemption_query", { params }),
// DB - Event
queryEvents: (params?: { limit?: number }): Promise<{ success: boolean; data: any[] }> =>
invoke("event_query", { params }),
createEvent: (data: {
studentName: string
reasonContent: string
delta: number
}): Promise<{ success: boolean; data?: number; message?: string }> =>
invoke("event_create", { data }),
deleteEvent: (uuid: string): Promise<{ success: boolean }> => invoke("event_delete", { uuid }),
queryEventsByStudent: (params: {
studentName: string
limit?: number
startTime?: string
}): Promise<{ success: boolean; data: any[] }> => invoke("event_query_by_student", { params }),
queryLeaderboard: (params: {
range: "today" | "week" | "month"
}): Promise<{ success: boolean; data: { startTime: string; rows: any[] } }> =>
invoke("leaderboard_query", { params }),
boardQuerySql: (params: {
sql: string
limit?: number
}): Promise<{ success: boolean; data: any[]; message?: string }> =>
invoke("board_query_sql", { params }),
// Settlement
querySettlements: (): Promise<{ success: boolean; data: any[] }> => invoke("db_settlement_query"),
createSettlement: (): Promise<{ success: boolean; data: any }> => invoke("db_settlement_create"),
querySettlementLeaderboard: (params: {
settlementId: number
}): Promise<{ success: boolean; data: any }> => invoke("db_settlement_leaderboard", { params }),
// Settings & Sync
getAllSettings: (): Promise<{ success: boolean; data: settingsSpec }> =>
invoke("settings_get_all"),
getSetting: <K extends settingsKey>(
key: K
): Promise<{ success: boolean; data: settingsSpec[K] }> => invoke("settings_get", { key }),
setSetting: <K extends settingsKey>(
key: K,
value: settingsSpec[K]
): Promise<{ success: boolean }> => invoke("settings_set", { key, value }),
onSettingChanged: (callback: (change: settingChange) => void): Promise<UnlistenFn> => {
return listen<settingChange>("settings:changed", (event) => {
callback(event.payload)
})
},
// Auth & Security
authGetStatus: (): Promise<{
success: boolean
data: {
permission: string
hasAdminPassword: boolean
hasPointsPassword: boolean
hasRecoveryString: boolean
}
}> => invoke("auth_get_status"),
authLogin: (
password: string
): Promise<{ success: boolean; data?: { permission: string }; message?: string }> =>
invoke("auth_login", { password }),
authLogout: (): Promise<{ success: boolean; data: { permission: string } }> =>
invoke("auth_logout"),
authSetPasswords: (payload: {
adminPassword?: string | null
pointsPassword?: string | null
}): Promise<{ success: boolean; data?: { recoveryString: string }; message?: string }> =>
invoke("auth_set_passwords", payload),
authGenerateRecovery: (): Promise<{ success: boolean; data: { recoveryString: string } }> =>
invoke("auth_generate_recovery"),
authResetByRecovery: (
recoveryString: string
): Promise<{ success: boolean; data?: { recoveryString: string }; message?: string }> =>
invoke("auth_reset_by_recovery", { recoveryString }),
authClearAll: (): Promise<{ success: boolean }> => invoke("auth_clear_all"),
// Data import/export
exportDataJson: (): Promise<{ success: boolean; data: string }> => invoke("data_export_json"),
importDataJson: (jsonText: string): Promise<{ success: boolean }> =>
invoke("data_import_json", { jsonText }),
// Window
windowMinimize: (): Promise<void> => invoke("window_minimize"),
windowMaximize: (): Promise<boolean> => invoke("window_maximize"),
windowClose: (): Promise<void> => invoke("window_close"),
windowIsMaximized: (): Promise<boolean> => invoke("window_is_maximized"),
toggleDevTools: (): Promise<void> => invoke("toggle_devtools"),
windowResize: (width: number, height: number): Promise<void> =>
invoke("window_resize", { width, height }),
windowSetResizable: (resizable: boolean): Promise<void> =>
invoke("window_set_resizable", { resizable }),
onWindowMaximizedChanged: (callback: (maximized: boolean) => void): Promise<UnlistenFn> => {
return listen<boolean>("window:maximized-changed", (event) => {
callback(event.payload)
})
},
onNavigate: (callback: (route: string) => void): Promise<UnlistenFn> => {
return listen<string>("app:navigate", (event) => {
callback(event.payload)
})
},
// Logger
queryLogs: (
input?: number | { lines?: number }
): Promise<{ success: boolean; data: string[] }> => {
const lines = typeof input === "number" ? input : input?.lines
return invoke("log_query", { lines })
},
clearLogs: (): Promise<{ success: boolean }> => invoke("log_clear"),
setLogLevel: (level: string): Promise<{ success: boolean }> => invoke("log_set_level", { level }),
writeLog: (payload: {
level: string
message: string
meta?: any
}): Promise<{ success: boolean }> => invoke("log_write", { payload }),
// Database Connection
dbTestConnection: (
connectionString: string
): Promise<{ success: boolean; data: { success: boolean; error?: string } }> =>
invoke("db_test_connection", { connectionString }),
dbSwitchConnection: (
connectionString: string
): Promise<{ success: boolean; data: { type: "sqlite" | "postgresql" } }> =>
invoke("db_switch_connection", { connectionString }),
dbGetStatus: (): Promise<{
success: boolean
data: { type: string; connected: boolean; error?: string }
}> => invoke("db_get_status"),
dbSync: (): Promise<{ success: boolean; data: { success: boolean; message?: string } }> =>
invoke("db_sync"),
dbSyncPreview: (): Promise<{
success: boolean
data: {
can_sync: boolean
need_sync: boolean
local_only: number
remote_only: number
conflicts: Array<{
table: string
key: string
local_summary: string
remote_summary: string
}>
message?: string
}
}> => invoke("db_sync_preview"),
dbSyncApply: (strategy: "keep_local" | "keep_remote"): Promise<{
success: boolean
data: { success: boolean; synced_records: number; resolved_conflicts: number; message?: string }
}> => invoke("db_sync_apply", { strategy }),
// HTTP Server
httpServerStart: (config?: {
port?: number
host?: string
corsOrigin?: string
}): Promise<{ success: boolean; data: { url: string; config: any } }> =>
invoke("http_server_start", { config }),
httpServerStop: (): Promise<{ success: boolean }> => invoke("http_server_stop"),
httpServerStatus: (): Promise<{
success: boolean
data: { isRunning: boolean; config?: any; url?: string }
}> => invoke("http_server_status"),
// MCP Server
mcpServerStart: (config?: {
port?: number
host?: string
}): Promise<{ success: boolean; data?: { url: string; config: { port: number; host: string } } }> =>
invoke("mcp_server_start", { config }),
mcpServerStop: (): Promise<{ success: boolean }> => invoke("mcp_server_stop"),
mcpServerStatus: (): Promise<{
success: boolean
data?: {
is_running: boolean
config: { port: number; host: string }
url?: string | null
}
}> => invoke("mcp_server_status"),
// File System
fsGetConfigStructure: (): Promise<{
success: boolean
data: { configRoot: string; automatic: string; script: string }
}> => invoke("fs_get_config_structure"),
fsReadJson: (
relativePath: string,
folder?: "automatic" | "script"
): Promise<{ success: boolean; data: any }> => invoke("fs_read_json", { relativePath, folder }),
fsWriteJson: (
relativePath: string,
data: any,
folder?: "automatic" | "script"
): Promise<{ success: boolean }> => invoke("fs_write_json", { relativePath, data, folder }),
fsReadText: (
relativePath: string,
folder?: "automatic" | "script"
): Promise<{ success: boolean; data: string }> =>
invoke("fs_read_text", { relativePath, folder }),
fsWriteText: (
content: string,
relativePath: string,
folder?: "automatic" | "script"
): Promise<{ success: boolean }> => invoke("fs_write_text", { content, relativePath, folder }),
fsDeleteFile: (
relativePath: string,
folder?: "automatic" | "script"
): Promise<{ success: boolean }> => invoke("fs_delete_file", { relativePath, folder }),
fsListFiles: (folder?: "automatic" | "script"): Promise<{ success: boolean; data: any[] }> =>
invoke("fs_list_files", { folder }),
fsFileExists: (
relativePath: string,
folder?: "automatic" | "script"
): Promise<{ success: boolean; data: boolean }> =>
invoke("fs_file_exists", { relativePath, folder }),
// App
registerUrlProtocol: (): Promise<{
success: boolean
data?: { registered: boolean }
message?: string
}> => invoke("register_url_protocol"),
appQuit: (): Promise<void> => invoke("app_quit"),
appRestart: (): Promise<void> => invoke("app_restart"),
// Auto Score
autoScoreGetRules: (): Promise<{ success: boolean; data: any[] }> =>
invoke("auto_score_get_rules"),
autoScoreAddRule: (rule: any): Promise<{ success: boolean; data?: number; message?: string }> =>
invoke("auto_score_add_rule", { rule }),
autoScoreUpdateRule: (
rule: any
): Promise<{ success: boolean; data?: boolean; message?: string }> =>
invoke("auto_score_update_rule", { rule }),
autoScoreDeleteRule: (
ruleId: number
): Promise<{ success: boolean; data?: boolean; message?: string }> =>
invoke("auto_score_delete_rule", { ruleId }),
autoScoreToggleRule: (params: {
ruleId: number
enabled: boolean
}): Promise<{ success: boolean; data?: boolean; message?: string }> =>
invoke("auto_score_toggle_rule", params),
autoScoreGetStatus: (): Promise<{ success: boolean; data: { enabled: boolean } }> =>
invoke("auto_score_get_status"),
autoScoreSortRules: (
ruleIds: number[]
): Promise<{ success: boolean; data?: boolean; message?: string }> =>
invoke("auto_score_sort_rules", { ruleIds }),
// Generic invoke wrapper for backward compatibility with callers using `api.invoke`
invoke: async (channel: string, ...args: any[]): Promise<any> => {
switch (channel) {
case "auto-score:getRules":
return api.autoScoreGetRules()
case "auto-score:addRule":
return api.autoScoreAddRule(args[0])
case "auto-score:updateRule":
return api.autoScoreUpdateRule(args[0])
case "auto-score:deleteRule":
return api.autoScoreDeleteRule(args[0])
case "auto-score:toggleRule":
return api.autoScoreToggleRule(args[0])
case "auto-score:sortRules":
return api.autoScoreSortRules(args[0])
case "auto-score:getStatus":
return api.autoScoreGetStatus()
default:
throw new Error(`Unsupported legacy invoke channel: ${channel}`)
}
},
}
export default api
export { api }
dataResetAll: (): Promise<{ success: boolean }> => invoke("data_reset_all"),
// Window
windowMinimize: (): Promise<void> => invoke("window_minimize"),
windowMaximize: (): Promise<boolean> => invoke("window_maximize"),
windowClose: (): Promise<void> => invoke("window_close"),
windowIsMaximized: (): Promise<boolean> => invoke("window_is_maximized"),
toggleDevTools: (): Promise<void> => invoke("toggle_devtools"),
windowResize: (width: number, height: number): Promise<void> =>
invoke("window_resize", { width, height }),
windowSetResizable: (resizable: boolean): Promise<void> =>
invoke("window_set_resizable", { resizable }),
onWindowMaximizedChanged: (callback: (maximized: boolean) => void): Promise<UnlistenFn> => {
return listen<boolean>("window:maximized-changed", (event) => {
callback(event.payload)
})
},
onNavigate: (callback: (route: string) => void): Promise<UnlistenFn> => {
return listen<string>("app:navigate", (event) => {
callback(event.payload)
})
},
// Logger
queryLogs: (
input?: number | { lines?: number }
): Promise<{ success: boolean; data: string[] }> => {
const lines = typeof input === "number" ? input : input?.lines
return invoke("log_query", { lines })
},
clearLogs: (): Promise<{ success: boolean }> => invoke("log_clear"),
setLogLevel: (level: string): Promise<{ success: boolean }> => invoke("log_set_level", { level }),
writeLog: (payload: {
level: string
message: string
meta?: any
}): Promise<{ success: boolean }> => invoke("log_write", { payload }),
// Database Connection
dbTestConnection: (
connectionString: string
): Promise<{ success: boolean; data: { success: boolean; error?: string } }> =>
invoke("db_test_connection", { connectionString }),
dbSwitchConnection: (
connectionString: string
): Promise<{ success: boolean; data: { type: "sqlite" | "postgresql" } }> =>
invoke("db_switch_connection", { connectionString }),
dbGetStatus: (): Promise<{
success: boolean
data: { type: string; connected: boolean; error?: string }
}> => invoke("db_get_status"),
dbSync: (): Promise<{ success: boolean; data: { success: boolean; message?: string } }> =>
invoke("db_sync"),
dbSyncPreview: (): Promise<{
success: boolean
data: {
can_sync: boolean
need_sync: boolean
local_only: number
remote_only: number
conflicts: Array<{
table: string
key: string
local_summary: string
remote_summary: string
}>
message?: string
}
}> => invoke("db_sync_preview"),
dbSyncApply: (strategy: "keep_local" | "keep_remote"): Promise<{
success: boolean
data: { success: boolean; synced_records: number; resolved_conflicts: number; message?: string }
}> => invoke("db_sync_apply", { strategy }),
// HTTP Server
httpServerStart: (config?: {
port?: number
host?: string
corsOrigin?: string
}): Promise<{ success: boolean; data: { url: string; config: any } }> =>
invoke("http_server_start", { config }),
httpServerStop: (): Promise<{ success: boolean }> => invoke("http_server_stop"),
httpServerStatus: (): Promise<{
success: boolean
data: { isRunning: boolean; config?: any; url?: string }
}> => invoke("http_server_status"),
// MCP Server
mcpServerStart: (config?: {
port?: number
host?: string
}): Promise<{ success: boolean; data?: { url: string; config: { port: number; host: string } } }> =>
invoke("mcp_server_start", { config }),
mcpServerStop: (): Promise<{ success: boolean }> => invoke("mcp_server_stop"),
mcpServerStatus: (): Promise<{
success: boolean
data?: {
is_running: boolean
config: { port: number; host: string }
url?: string | null
}
}> => invoke("mcp_server_status"),
// File System
fsGetConfigStructure: (): Promise<{
success: boolean
data: { configRoot: string; automatic: string; script: string }
}> => invoke("fs_get_config_structure"),
fsReadJson: (
relativePath: string,
folder?: "automatic" | "script"
): Promise<{ success: boolean; data: any }> => invoke("fs_read_json", { relativePath, folder }),
fsWriteJson: (
relativePath: string,
data: any,
folder?: "automatic" | "script"
): Promise<{ success: boolean }> => invoke("fs_write_json", { relativePath, data, folder }),
fsReadText: (
relativePath: string,
folder?: "automatic" | "script"
): Promise<{ success: boolean; data: string }> =>
invoke("fs_read_text", { relativePath, folder }),
fsWriteText: (
content: string,
relativePath: string,
folder?: "automatic" | "script"
): Promise<{ success: boolean }> => invoke("fs_write_text", { content, relativePath, folder }),
fsDeleteFile: (
relativePath: string,
folder?: "automatic" | "script"
): Promise<{ success: boolean }> => invoke("fs_delete_file", { relativePath, folder }),
fsListFiles: (folder?: "automatic" | "script"): Promise<{ success: boolean; data: any[] }> =>
invoke("fs_list_files", { folder }),
fsFileExists: (
relativePath: string,
folder?: "automatic" | "script"
): Promise<{ success: boolean; data: boolean }> =>
invoke("fs_file_exists", { relativePath, folder }),
// App
registerUrlProtocol: (): Promise<{
success: boolean
data?: { registered: boolean }
message?: string
}> => invoke("register_url_protocol"),
appQuit: (): Promise<void> => invoke("app_quit"),
appRestart: (): Promise<void> => invoke("app_restart"),
// Auto Score
autoScoreGetRules: (): Promise<{ success: boolean; data: any[] }> =>
invoke("auto_score_get_rules"),
autoScoreAddRule: (rule: any): Promise<{ success: boolean; data?: number; message?: string }> =>
invoke("auto_score_add_rule", { rule }),
autoScoreUpdateRule: (
rule: any
): Promise<{ success: boolean; data?: boolean; message?: string }> =>
invoke("auto_score_update_rule", { rule }),
autoScoreDeleteRule: (
ruleId: number
): Promise<{ success: boolean; data?: boolean; message?: string }> =>
invoke("auto_score_delete_rule", { ruleId }),
autoScoreToggleRule: (params: {
ruleId: number
enabled: boolean
}): Promise<{ success: boolean; data?: boolean; message?: string }> =>
invoke("auto_score_toggle_rule", params),
autoScoreGetStatus: (): Promise<{ success: boolean; data: { enabled: boolean } }> =>
invoke("auto_score_get_status"),
autoScoreSortRules: (
ruleIds: number[]
): Promise<{ success: boolean; data?: boolean; message?: string }> =>
invoke("auto_score_sort_rules", { ruleIds }),
// Generic invoke wrapper for backward compatibility with callers using `api.invoke`
invoke: async (channel: string, ...args: any[]): Promise<any> => {
switch (channel) {
case "auto-score:getRules":
return api.autoScoreGetRules()
case "auto-score:addRule":
return api.autoScoreAddRule(args[0])
case "auto-score:updateRule":
return api.autoScoreUpdateRule(args[0])
case "auto-score:deleteRule":
return api.autoScoreDeleteRule(args[0])
case "auto-score:toggleRule":
return api.autoScoreToggleRule(args[0])
case "auto-score:sortRules":
return api.autoScoreSortRules(args[0])
case "auto-score:getStatus":
return api.autoScoreGetStatus()
default:
throw new Error(`Unsupported legacy invoke channel: ${channel}`)
}
},
}
export default api
export { api }