Skip to content

Commit ad504a6

Browse files
authored
Add files via upload
1 parent b527cb8 commit ad504a6

12 files changed

Lines changed: 527 additions & 33 deletions

File tree

src-tauri/Cargo.lock

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src-tauri/Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[package]
22
name = "zgamelib"
3-
version = "0.7.0"
3+
version = "0.8.0"
44
description = "ZGameLib - Personal Game Library"
55
authors = ["TheHolyOneZ"]
66
license = "MIT"
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
use tauri::State;
2+
use uuid::Uuid;
3+
use chrono::Utc;
4+
use crate::db::{DbState, queries};
5+
use crate::models::{Collection, Game};
6+
7+
#[tauri::command]
8+
pub fn get_collections(state: State<DbState>) -> Result<Vec<Collection>, String> {
9+
let conn = state.0.lock().map_err(|e| e.to_string())?;
10+
queries::get_all_collections(&conn).map_err(|e| e.to_string())
11+
}
12+
13+
#[tauri::command]
14+
pub fn create_collection(state: State<DbState>, name: String) -> Result<Collection, String> {
15+
if name.trim().is_empty() { return Err("Collection name cannot be empty".to_string()); }
16+
if name.len() > 100 { return Err("Collection name must be 100 characters or fewer".to_string()); }
17+
let conn = state.0.lock().map_err(|e| e.to_string())?;
18+
let id = Uuid::new_v4().to_string();
19+
let now = Utc::now().to_rfc3339();
20+
queries::insert_collection(&conn, &id, name.trim(), &now).map_err(|e| e.to_string())?;
21+
Ok(Collection { id, name: name.trim().to_string(), created_at: now, game_count: 0, description: None })
22+
}
23+
24+
#[tauri::command]
25+
pub fn rename_collection(state: State<DbState>, id: String, name: String) -> Result<(), String> {
26+
if name.trim().is_empty() { return Err("Collection name cannot be empty".to_string()); }
27+
if name.len() > 100 { return Err("Collection name must be 100 characters or fewer".to_string()); }
28+
let conn = state.0.lock().map_err(|e| e.to_string())?;
29+
queries::rename_collection(&conn, &id, name.trim()).map_err(|e| e.to_string())
30+
}
31+
32+
#[tauri::command]
33+
pub fn delete_collection(state: State<DbState>, id: String) -> Result<(), String> {
34+
let conn = state.0.lock().map_err(|e| e.to_string())?;
35+
queries::delete_collection(&conn, &id).map_err(|e| e.to_string())
36+
}
37+
38+
#[tauri::command]
39+
pub fn get_collection_games(state: State<DbState>, collection_id: String) -> Result<Vec<Game>, String> {
40+
let conn = state.0.lock().map_err(|e| e.to_string())?;
41+
queries::get_collection_games(&conn, &collection_id).map_err(|e| e.to_string())
42+
}
43+
44+
#[tauri::command]
45+
pub fn add_game_to_collection(state: State<DbState>, collection_id: String, game_id: String) -> Result<(), String> {
46+
let conn = state.0.lock().map_err(|e| e.to_string())?;
47+
queries::add_game_to_collection(&conn, &collection_id, &game_id).map_err(|e| e.to_string())
48+
}
49+
50+
#[tauri::command]
51+
pub fn remove_game_from_collection(state: State<DbState>, collection_id: String, game_id: String) -> Result<(), String> {
52+
let conn = state.0.lock().map_err(|e| e.to_string())?;
53+
queries::remove_game_from_collection(&conn, &collection_id, &game_id).map_err(|e| e.to_string())
54+
}
55+
56+
#[tauri::command]
57+
pub fn get_collections_for_game(state: State<DbState>, game_id: String) -> Result<Vec<Collection>, String> {
58+
let conn = state.0.lock().map_err(|e| e.to_string())?;
59+
queries::get_collections_for_game(&conn, &game_id).map_err(|e| e.to_string())
60+
}
61+
62+
#[tauri::command]
63+
pub fn update_collection_description(state: State<DbState>, id: String, description: Option<String>) -> Result<(), String> {
64+
let conn = state.0.lock().map_err(|e| e.to_string())?;
65+
queries::update_collection_description(&conn, &id, description.as_deref()).map_err(|e| e.to_string())
66+
}

src-tauri/src/commands/games.rs

Lines changed: 152 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ use tauri::State;
22
use chrono::Utc;
33
use uuid::Uuid;
44
use crate::db::{DbState, queries};
5-
use crate::models::{Game, Note, Session, CreateGamePayload, UpdateGamePayload, HltbData, WeeklyPlaytime};
5+
use crate::models::{Game, Note, Session, CreateGamePayload, UpdateGamePayload, HltbData, WeeklyPlaytime, IgdbMetadata, LibraryGrowthEntry};
66

77
#[tauri::command]
88
pub fn get_all_games(state: State<DbState>) -> Result<Vec<Game>, String> {
@@ -43,6 +43,11 @@ pub fn create_game(state: State<DbState>, payload: CreateGamePayload) -> Result<
4343
custom_fields: std::collections::HashMap::new(),
4444
hltb_main_mins: None,
4545
hltb_extra_mins: None,
46+
genre: None,
47+
developer: None,
48+
publisher: None,
49+
release_year: None,
50+
igdb_skipped: false,
4651
};
4752
queries::insert_game(&conn, &game).map_err(|e| e.to_string())?;
4853
Ok(game)
@@ -83,6 +88,10 @@ pub fn update_game(state: State<DbState>, payload: UpdateGamePayload) -> Result<
8388
if let Some(exe) = payload.exe_path { game.exe_path = Some(exe); }
8489
if let Some(dir) = payload.install_dir { game.install_dir = Some(dir); }
8590
if let Some(cf) = payload.custom_fields { game.custom_fields = cf; }
91+
if let Some(v) = payload.genre { game.genre = if v.is_empty() { None } else { Some(v) }; }
92+
if let Some(v) = payload.developer { game.developer = if v.is_empty() { None } else { Some(v) }; }
93+
if let Some(v) = payload.publisher { game.publisher = if v.is_empty() { None } else { Some(v) }; }
94+
if let Some(v) = payload.release_year { game.release_year = Some(v); }
8695

8796
queries::update_game(&conn, &game).map_err(|e| e.to_string())?;
8897
Ok(game)
@@ -373,3 +382,145 @@ pub fn batch_update_games(
373382
tx.commit().map_err(|e| e.to_string())?;
374383
Ok(())
375384
}
385+
386+
#[tauri::command]
387+
pub fn get_library_growth(state: State<DbState>) -> Result<Vec<LibraryGrowthEntry>, String> {
388+
let conn = state.0.lock().map_err(|e| e.to_string())?;
389+
let mut stmt = conn.prepare(
390+
"SELECT strftime('%Y-%m', date_added) as month, platform, COUNT(*) as cnt
391+
FROM games WHERE deleted_at IS NULL AND date_added IS NOT NULL
392+
GROUP BY month, platform ORDER BY month ASC"
393+
).map_err(|e| e.to_string())?;
394+
395+
let rows: Vec<(String, String, i64)> = stmt.query_map([], |row| {
396+
Ok((row.get(0)?, row.get(1)?, row.get(2)?))
397+
}).map_err(|e| e.to_string())?
398+
.filter_map(|r| r.ok())
399+
.collect();
400+
drop(stmt);
401+
402+
let mut month_map: std::collections::BTreeMap<String, LibraryGrowthEntry> = std::collections::BTreeMap::new();
403+
for (month, platform, cnt) in rows {
404+
let entry = month_map.entry(month.clone()).or_insert(LibraryGrowthEntry {
405+
month,
406+
steam: 0, epic: 0, gog: 0, custom: 0,
407+
});
408+
match platform.as_str() {
409+
"steam" => entry.steam += cnt,
410+
"epic" => entry.epic += cnt,
411+
"gog" => entry.gog += cnt,
412+
"custom" => entry.custom += cnt,
413+
_ => {}
414+
}
415+
}
416+
417+
Ok(month_map.into_values().collect())
418+
}
419+
420+
#[tauri::command]
421+
pub fn fetch_igdb_metadata(
422+
state: State<DbState>,
423+
game_id: String,
424+
game_name: String,
425+
client_id: String,
426+
client_secret: String,
427+
) -> Result<Option<IgdbMetadata>, String> {
428+
if client_id.trim().is_empty() || client_secret.trim().is_empty() {
429+
return Err("IGDB client_id and client_secret are required".to_string());
430+
}
431+
432+
let token_url = "https://id.twitch.tv/oauth2/token";
433+
let token_body = format!(
434+
"client_id={}&client_secret={}&grant_type=client_credentials",
435+
client_id.trim(), client_secret.trim()
436+
);
437+
let token_resp = ureq::post(token_url)
438+
.set("Content-Type", "application/x-www-form-urlencoded")
439+
.timeout(std::time::Duration::from_secs(15))
440+
.send_string(&token_body)
441+
.map_err(|e| format!("Failed to get IGDB token: {}", e))?;
442+
let token_json: serde_json::Value = serde_json::from_str(
443+
&token_resp.into_string().map_err(|e| e.to_string())?
444+
).map_err(|e| e.to_string())?;
445+
let access_token = token_json["access_token"]
446+
.as_str()
447+
.ok_or("Failed to parse access token")?
448+
.to_string();
449+
450+
let query = format!(
451+
"fields name,genres.name,involved_companies.company.name,involved_companies.developer,involved_companies.publisher,first_release_date,summary; search \"{}\"; limit 5;",
452+
game_name.replace('"', "")
453+
);
454+
let igdb_resp = ureq::post("https://api.igdb.com/v4/games")
455+
.set("Client-ID", client_id.trim())
456+
.set("Authorization", &format!("Bearer {}", access_token))
457+
.set("Content-Type", "text/plain")
458+
.timeout(std::time::Duration::from_secs(15))
459+
.send_string(&query)
460+
.map_err(|e| format!("IGDB request failed: {}", e))?;
461+
let igdb_games: serde_json::Value = serde_json::from_str(
462+
&igdb_resp.into_string().map_err(|e| e.to_string())?
463+
).map_err(|e| e.to_string())?;
464+
465+
let entry = match igdb_games.as_array().and_then(|a| a.first()) {
466+
Some(e) => e.clone(),
467+
None => return Ok(None),
468+
};
469+
470+
let genre = entry["genres"].as_array()
471+
.and_then(|a| a.first())
472+
.and_then(|g| g["name"].as_str())
473+
.map(|s| s.to_string());
474+
475+
let mut developer: Option<String> = None;
476+
let mut publisher: Option<String> = None;
477+
if let Some(companies) = entry["involved_companies"].as_array() {
478+
for ic in companies {
479+
let name = ic["company"]["name"].as_str().map(|s| s.to_string());
480+
if ic["developer"].as_bool().unwrap_or(false) && developer.is_none() {
481+
developer = name.clone();
482+
}
483+
if ic["publisher"].as_bool().unwrap_or(false) && publisher.is_none() {
484+
publisher = name;
485+
}
486+
}
487+
}
488+
489+
let release_year = entry["first_release_date"].as_i64().map(|ts| {
490+
let dt = chrono::DateTime::<chrono::Utc>::from_timestamp(ts, 0)
491+
.unwrap_or_else(|| chrono::Utc::now());
492+
dt.format("%Y").to_string().parse::<i64>().unwrap_or(0)
493+
});
494+
495+
let summary = entry["summary"].as_str().map(|s| s.to_string());
496+
497+
let meta = IgdbMetadata {
498+
genre: genre.clone(),
499+
developer: developer.clone(),
500+
publisher: publisher.clone(),
501+
release_year,
502+
summary: summary.clone(),
503+
};
504+
505+
{
506+
let conn = state.0.lock().map_err(|e| e.to_string())?;
507+
let _ = conn.execute(
508+
"UPDATE games SET genre=?1, developer=?2, publisher=?3, release_year=?4 WHERE id=?5",
509+
rusqlite::params![genre, developer, publisher, release_year, game_id],
510+
);
511+
if let Some(ref s) = summary {
512+
let _ = conn.execute(
513+
"UPDATE games SET description=?1 WHERE id=?2 AND (description IS NULL OR description = '')",
514+
rusqlite::params![s, game_id],
515+
);
516+
}
517+
}
518+
519+
Ok(Some(meta))
520+
}
521+
522+
#[tauri::command]
523+
pub fn clear_igdb_data(state: State<DbState>, id: String) -> Result<(), String> {
524+
let conn = state.0.lock().map_err(|e| e.to_string())?;
525+
queries::clear_igdb_data(&conn, &id).map_err(|e| e.to_string())
526+
}

src-tauri/src/commands/mod.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,3 +3,4 @@ pub mod scanner;
33
pub mod launcher;
44
pub mod settings;
55
pub mod modloader;
6+
pub mod collections;

src-tauri/src/commands/scanner.rs

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -508,6 +508,11 @@ fn scan_steam_games_inner(app: AppHandle, db: std::sync::Arc<std::sync::Mutex<ru
508508
custom_fields: std::collections::HashMap::new(),
509509
hltb_main_mins: None,
510510
hltb_extra_mins: None,
511+
genre: None,
512+
developer: None,
513+
publisher: None,
514+
release_year: None,
515+
igdb_skipped: false,
511516
};
512517
if queries::insert_game(&conn, &game).is_ok() {
513518
added += 1;
@@ -644,6 +649,11 @@ fn scan_epic_games_inner(app: AppHandle, db: std::sync::Arc<std::sync::Mutex<rus
644649
custom_fields: std::collections::HashMap::new(),
645650
hltb_main_mins: None,
646651
hltb_extra_mins: None,
652+
genre: None,
653+
developer: None,
654+
publisher: None,
655+
release_year: None,
656+
igdb_skipped: false,
647657
};
648658
if queries::insert_game(&conn, &game).is_ok() {
649659
added += 1;
@@ -788,6 +798,11 @@ fn scan_gog_games_inner(app: AppHandle, db: std::sync::Arc<std::sync::Mutex<rusq
788798
custom_fields: std::collections::HashMap::new(),
789799
hltb_main_mins: None,
790800
hltb_extra_mins: None,
801+
genre: None,
802+
developer: None,
803+
publisher: None,
804+
release_year: None,
805+
igdb_skipped: false,
791806
};
792807
if queries::insert_game(&conn, &game).is_ok() { added += 1; }
793808
}
@@ -1066,6 +1081,11 @@ fn scan_folder_for_games_inner(app: AppHandle, db: std::sync::Arc<std::sync::Mut
10661081
custom_fields: std::collections::HashMap::new(),
10671082
hltb_main_mins: None,
10681083
hltb_extra_mins: None,
1084+
genre: None,
1085+
developer: None,
1086+
publisher: None,
1087+
release_year: None,
1088+
igdb_skipped: false,
10691089
};
10701090
if queries::insert_game(&conn, &game).is_ok() {
10711091
added += 1;

0 commit comments

Comments
 (0)