feat: add first version of TougliGui with same features as on google sheet

This commit is contained in:
2026-04-21 22:02:20 +02:00
parent 79d5c4baaa
commit f571d8bb3f
53 changed files with 12416 additions and 0 deletions

211
src-tauri/src/commands.rs Normal file
View File

@ -0,0 +1,211 @@
use tauri::{AppHandle, Manager, State};
use serde::{Deserialize, Serialize};
use std::sync::Mutex;
use rusqlite::Connection;
use crate::{db, parser};
pub struct DbState(pub Mutex<Connection>);
#[derive(Debug, Serialize, Deserialize)]
pub struct GuideListItem {
pub gid: String,
pub name: String,
pub last_synced_at: Option<String>,
pub total_quests: usize,
pub completed_quests: usize,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct SyncResult {
pub synced: usize,
pub errors: Vec<String>,
}
#[tauri::command]
pub fn get_profiles(state: State<DbState>) -> Result<Vec<db::Profile>, String> {
let conn = state.0.lock().map_err(|e| e.to_string())?;
db::get_profiles(&conn).map_err(|e| e.to_string())
}
#[tauri::command]
pub fn create_profile(state: State<DbState>, name: String) -> Result<db::Profile, String> {
let conn = state.0.lock().map_err(|e| e.to_string())?;
db::create_profile(&conn, &name).map_err(|e| e.to_string())
}
#[tauri::command]
pub fn delete_profile(state: State<DbState>, profile_id: String) -> Result<(), String> {
let conn = state.0.lock().map_err(|e| e.to_string())?;
db::delete_profile(&conn, &profile_id).map_err(|e| e.to_string())
}
#[tauri::command]
pub fn get_completed_quests(state: State<DbState>, profile_id: String) -> Result<Vec<String>, String> {
let conn = state.0.lock().map_err(|e| e.to_string())?;
db::get_completed_quests(&conn, &profile_id).map_err(|e| e.to_string())
}
#[tauri::command]
pub fn toggle_quest(state: State<DbState>, profile_id: String, quest_name: String) -> Result<bool, String> {
let conn = state.0.lock().map_err(|e| e.to_string())?;
db::toggle_quest(&conn, &profile_id, &quest_name).map_err(|e| e.to_string())
}
#[tauri::command]
pub fn get_guides_list(state: State<DbState>, profile_id: String) -> Result<Vec<GuideListItem>, String> {
let conn = state.0.lock().map_err(|e| e.to_string())?;
let guides = db::get_guides(&conn).map_err(|e| e.to_string())?;
let completed = db::get_completed_quests(&conn, &profile_id).map_err(|e| e.to_string())?;
let completed_set: std::collections::HashSet<String> = completed.into_iter().collect();
let items = guides.into_iter().map(|g| {
let data: parser::GuideData = serde_json::from_str(&g.data).unwrap_or_else(|_| parser::GuideData {
name: g.name.clone(),
gid: g.gid.clone(),
effect: String::new(),
recommended_level: None,
combat_legend: vec![],
resources: vec![],
sections: vec![],
});
let all_quests = collect_quest_names(&data);
let total = all_quests.len();
let completed_count = all_quests.iter().filter(|q| completed_set.contains(*q)).count();
GuideListItem {
gid: g.gid,
name: g.name,
last_synced_at: g.last_synced_at,
total_quests: total,
completed_quests: completed_count,
}
}).collect();
Ok(items)
}
#[tauri::command]
pub fn get_guide(state: State<DbState>, gid: String) -> Result<parser::GuideData, String> {
let conn = state.0.lock().map_err(|e| e.to_string())?;
let guides = db::get_guides(&conn).map_err(|e| e.to_string())?;
if let Some(g) = guides.into_iter().find(|g| g.gid == gid) {
serde_json::from_str(&g.data).map_err(|e| e.to_string())
} else {
Err(format!("Guide {} not found", gid))
}
}
#[tauri::command]
pub async fn sync_guides(state: State<'_, DbState>) -> Result<SyncResult, String> {
let mut synced = 0;
let mut errors = Vec::new();
for (gid, name) in parser::TABS {
let gid = gid.to_string();
let name = name.to_string();
// Run blocking HTTP+parse in a dedicated thread to avoid runtime conflict
let result = tokio::task::spawn_blocking({
let gid = gid.clone();
let name = name.clone();
move || -> Result<String, String> {
let csv = parser::fetch_csv(&gid)?;
let links = parser::fetch_quest_links(&gid);
let data = parser::parse_guide_with_links(&gid, &name, &csv, &links);
serde_json::to_string(&data).map_err(|e| e.to_string())
}
}).await.map_err(|e| e.to_string())?;
match result {
Ok(json) => {
let conn = state.0.lock().map_err(|e| e.to_string())?;
db::upsert_guide(&conn, &gid, &name, &json).map_err(|e| e.to_string())?;
synced += 1;
}
Err(e) => {
errors.push(format!("{}: {}", name, e));
}
}
}
Ok(SyncResult { synced, errors })
}
#[derive(Debug, Serialize, Deserialize)]
pub struct TabInfo {
pub gid: String,
pub name: String,
}
#[tauri::command]
pub fn get_tabs_list() -> Vec<TabInfo> {
parser::TABS.iter().map(|(gid, name)| TabInfo {
gid: gid.to_string(),
name: name.to_string(),
}).collect()
}
#[tauri::command]
pub async fn sync_single_guide(state: State<'_, DbState>, gid: String, name: String) -> Result<(), String> {
let json = tokio::task::spawn_blocking({
let gid = gid.clone();
let name = name.clone();
move || -> Result<String, String> {
let csv = parser::fetch_csv(&gid)?;
let links = parser::fetch_quest_links(&gid);
let data = parser::parse_guide_with_links(&gid, &name, &csv, &links);
serde_json::to_string(&data).map_err(|e| e.to_string())
}
}).await.map_err(|e| e.to_string())??;
let conn = state.0.lock().map_err(|e| e.to_string())?;
db::upsert_guide(&conn, &gid, &name, &json).map_err(|e| e.to_string())?;
Ok(())
}
// This remains for backwards compat but delegates to per-guide approach
#[tauri::command]
pub fn has_guides(state: State<DbState>) -> Result<bool, String> {
let conn = state.0.lock().map_err(|e| e.to_string())?;
let guides = db::get_guides(&conn).map_err(|e| e.to_string())?;
Ok(!guides.is_empty())
}
#[tauri::command]
pub fn get_setting(state: State<DbState>, key: String) -> Result<Option<String>, String> {
let conn = state.0.lock().map_err(|e| e.to_string())?;
Ok(db::get_setting(&conn, &key))
}
#[tauri::command]
pub fn set_setting(state: State<DbState>, key: String, value: String) -> Result<(), String> {
let conn = state.0.lock().map_err(|e| e.to_string())?;
db::set_setting(&conn, &key, &value).map_err(|e| e.to_string())
}
#[tauri::command]
pub fn set_always_on_top(app: AppHandle, value: bool) -> Result<(), String> {
if let Some(win) = app.get_webview_window("main") {
win.set_always_on_top(value).map_err(|e| e.to_string())
} else {
Err("Window not found".to_string())
}
}
fn collect_quest_names(data: &parser::GuideData) -> Vec<String> {
let mut names = Vec::new();
for section in &data.sections {
for item in &section.items {
match item {
parser::SectionItem::Quest(q) => names.push(q.name.clone()),
parser::SectionItem::Group(g) => {
for q in &g.quests { names.push(q.name.clone()); }
}
_ => {}
}
}
}
names
}