Skip to content

Commit b757052

Browse files
authored
Add files via upload
1 parent ce644db commit b757052

12 files changed

Lines changed: 520 additions & 32 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.5.0"
3+
version = "0.6.0"
44
description = "ZGameLib - Personal Game Library"
55
authors = ["TheHolyOneZ"]
66
license = "MIT"

src-tauri/src/commands/games.rs

Lines changed: 180 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, CreateGamePayload, UpdateGamePayload};
5+
use crate::models::{Game, Note, Session, CreateGamePayload, UpdateGamePayload, HltbData};
66

77
#[tauri::command]
88
pub fn get_all_games(state: State<DbState>) -> Result<Vec<Game>, String> {
@@ -38,6 +38,11 @@ pub fn create_game(state: State<DbState>, payload: CreateGamePayload) -> Result<
3838
epic_app_name: payload.epic_app_name,
3939
tags: vec![],
4040
sort_order: 0,
41+
deleted_at: None,
42+
is_pinned: false,
43+
custom_fields: std::collections::HashMap::new(),
44+
hltb_main_mins: None,
45+
hltb_extra_mins: None,
4146
};
4247
queries::insert_game(&conn, &game).map_err(|e| e.to_string())?;
4348
Ok(game)
@@ -50,6 +55,17 @@ pub fn update_game(state: State<DbState>, payload: UpdateGamePayload) -> Result<
5055
.map_err(|e| e.to_string())?
5156
.ok_or("Game not found")?;
5257

58+
if let Some(ref name) = payload.name {
59+
if name.len() > 255 { return Err("Name must be 255 characters or fewer".to_string()); }
60+
}
61+
if let Some(ref desc) = payload.description {
62+
if desc.len() > 10_000 { return Err("Description must be 10,000 characters or fewer".to_string()); }
63+
}
64+
if let Some(ref tags) = payload.tags {
65+
if tags.len() > 100 { return Err("Maximum 100 tags allowed".to_string()); }
66+
if tags.iter().any(|t| t.len() > 50) { return Err("Each tag must be 50 characters or fewer".to_string()); }
67+
}
68+
5369
if let Some(name) = payload.name { game.name = name; }
5470
if let Some(cover) = payload.cover_path { game.cover_path = Some(cover); }
5571
if let Some(desc) = payload.description { game.description = Some(desc); }
@@ -60,17 +76,54 @@ pub fn update_game(state: State<DbState>, payload: UpdateGamePayload) -> Result<
6076
if let Some(tags) = payload.tags { game.tags = tags; }
6177
if let Some(exe) = payload.exe_path { game.exe_path = Some(exe); }
6278
if let Some(dir) = payload.install_dir { game.install_dir = Some(dir); }
79+
if let Some(cf) = payload.custom_fields { game.custom_fields = cf; }
6380

6481
queries::update_game(&conn, &game).map_err(|e| e.to_string())?;
6582
Ok(game)
6683
}
6784

6885
#[tauri::command]
6986
pub fn delete_game(state: State<DbState>, id: String) -> Result<(), String> {
87+
let conn = state.0.lock().map_err(|e| e.to_string())?;
88+
let now = Utc::now().to_rfc3339();
89+
queries::soft_delete_game(&conn, &id, &now).map_err(|e| e.to_string())
90+
}
91+
92+
#[tauri::command]
93+
pub fn permanent_delete_game(state: State<DbState>, id: String) -> Result<(), String> {
7094
let conn = state.0.lock().map_err(|e| e.to_string())?;
7195
queries::delete_game(&conn, &id).map_err(|e| e.to_string())
7296
}
7397

98+
#[tauri::command]
99+
pub fn restore_game(state: State<DbState>, id: String) -> Result<(), String> {
100+
let conn = state.0.lock().map_err(|e| e.to_string())?;
101+
queries::restore_game(&conn, &id).map_err(|e| e.to_string())
102+
}
103+
104+
#[tauri::command]
105+
pub fn purge_trash(state: State<DbState>) -> Result<usize, String> {
106+
let conn = state.0.lock().map_err(|e| e.to_string())?;
107+
queries::purge_trash(&conn).map_err(|e| e.to_string())
108+
}
109+
110+
#[tauri::command]
111+
pub fn get_trashed_games(state: State<DbState>) -> Result<Vec<Game>, String> {
112+
let conn = state.0.lock().map_err(|e| e.to_string())?;
113+
queries::get_trashed_games(&conn).map_err(|e| e.to_string())
114+
}
115+
116+
#[tauri::command]
117+
pub fn toggle_pinned(state: State<DbState>, id: String) -> Result<bool, String> {
118+
let conn = state.0.lock().map_err(|e| e.to_string())?;
119+
let mut game = queries::get_game_by_id(&conn, &id)
120+
.map_err(|e| e.to_string())?
121+
.ok_or("Game not found")?;
122+
game.is_pinned = !game.is_pinned;
123+
queries::update_game(&conn, &game).map_err(|e| e.to_string())?;
124+
Ok(game.is_pinned)
125+
}
126+
74127
#[tauri::command]
75128
pub fn toggle_favorite(state: State<DbState>, id: String) -> Result<bool, String> {
76129
let conn = state.0.lock().map_err(|e| e.to_string())?;
@@ -142,3 +195,129 @@ pub fn delete_note(state: State<DbState>, id: String) -> Result<(), String> {
142195
let conn = state.0.lock().map_err(|e| e.to_string())?;
143196
queries::delete_note(&conn, &id).map_err(|e| e.to_string())
144197
}
198+
199+
#[tauri::command]
200+
pub fn reorder_games(state: State<DbState>, ids: Vec<String>) -> Result<(), String> {
201+
let conn = state.0.lock().map_err(|e| e.to_string())?;
202+
let tx = conn.unchecked_transaction().map_err(|e| e.to_string())?;
203+
for (i, id) in ids.iter().enumerate() {
204+
conn.execute(
205+
"UPDATE games SET sort_order = ?1 WHERE id = ?2",
206+
rusqlite::params![i as i64, id],
207+
).map_err(|e| e.to_string())?;
208+
}
209+
tx.commit().map_err(|e| e.to_string())?;
210+
Ok(())
211+
}
212+
213+
#[tauri::command]
214+
pub fn fetch_hltb_data(state: State<DbState>, game_id: String, game_name: String) -> Result<Option<HltbData>, String> {
215+
let raw = match ureq::get("https://howlongtobeat.com/")
216+
.set("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36")
217+
.call()
218+
{
219+
Ok(r) => r.into_string().unwrap_or_default(),
220+
Err(_) => return Ok(None),
221+
};
222+
223+
let hash = {
224+
let needle = "/api/search/";
225+
raw.find(needle)
226+
.and_then(|i| {
227+
let after = &raw[i + needle.len()..];
228+
let end = after.find('"').unwrap_or(after.len().min(20));
229+
let h = &after[..end];
230+
if h.len() >= 8 && h.len() <= 40 { Some(h.to_string()) } else { None }
231+
})
232+
};
233+
234+
let hash = match hash {
235+
Some(h) => h,
236+
None => return Ok(None),
237+
};
238+
239+
let url = format!("https://howlongtobeat.com/api/search/{}", hash);
240+
let body = serde_json::json!({
241+
"searchType": "games",
242+
"searchTerms": [game_name],
243+
"searchPage": 1,
244+
"size": 1,
245+
"searchOptions": {
246+
"games": {
247+
"userId": 0,
248+
"platform": "",
249+
"sortCategory": "popular",
250+
"rangeCategory": "main",
251+
"rangeTime": {"min": null, "max": null},
252+
"gameplay": {"perspective": "", "flow": "", "genre": "", "subGenre": ""},
253+
"rangeYear": {"min": "", "max": ""},
254+
"modifier": ""
255+
},
256+
"users": {"sortCategory": "postcount"},
257+
"lists": {"sortCategory": "follows"},
258+
"filter": "",
259+
"sort": 0,
260+
"randomizer": 0
261+
}
262+
});
263+
264+
let resp = match ureq::post(&url)
265+
.set("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36")
266+
.set("Referer", "https://howlongtobeat.com/")
267+
.set("Content-Type", "application/json")
268+
.send_string(&body.to_string())
269+
{
270+
Ok(r) => r.into_string().unwrap_or_default(),
271+
Err(_) => return Ok(None),
272+
};
273+
274+
let json: serde_json::Value = match serde_json::from_str(&resp) {
275+
Ok(v) => v,
276+
Err(_) => return Ok(None),
277+
};
278+
279+
let entry = match json["data"].as_array().and_then(|a| a.first()) {
280+
Some(e) => e.clone(),
281+
None => return Ok(None),
282+
};
283+
284+
let secs_to_mins = |v: &serde_json::Value| -> Option<i64> {
285+
v.as_i64().filter(|&s| s > 0).map(|s| s / 60)
286+
};
287+
288+
let data = HltbData {
289+
main_mins: secs_to_mins(&entry["comp_main"]),
290+
extra_mins: secs_to_mins(&entry["comp_plus"]),
291+
complete_mins: secs_to_mins(&entry["comp_100"]),
292+
};
293+
294+
{
295+
let conn = state.0.lock().map_err(|e| e.to_string())?;
296+
let _ = conn.execute(
297+
"UPDATE games SET hltb_main_mins = ?1, hltb_extra_mins = ?2 WHERE id = ?3",
298+
rusqlite::params![data.main_mins, data.extra_mins, game_id],
299+
);
300+
}
301+
302+
Ok(Some(data))
303+
}
304+
305+
#[tauri::command]
306+
pub fn get_sessions(state: State<DbState>, game_id: String) -> Result<Vec<Session>, String> {
307+
let conn = state.0.lock().map_err(|e| e.to_string())?;
308+
let mut stmt = conn.prepare(
309+
"SELECT id, game_id, started_at, ended_at, duration_mins FROM sessions WHERE game_id = ?1 ORDER BY started_at DESC LIMIT 50"
310+
).map_err(|e| e.to_string())?;
311+
let sessions = stmt.query_map(rusqlite::params![game_id], |row| {
312+
Ok(Session {
313+
id: row.get(0)?,
314+
game_id: row.get(1)?,
315+
started_at: row.get(2)?,
316+
ended_at: row.get(3)?,
317+
duration_mins: row.get(4)?,
318+
})
319+
}).map_err(|e| e.to_string())?
320+
.filter_map(|r| r.ok())
321+
.collect();
322+
Ok(sessions)
323+
}

src-tauri/src/commands/launcher.rs

Lines changed: 18 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@ fn is_pid_running(pid: u32) -> bool {
5151
out.contains('"')
5252
}
5353

54-
fn finish_session(app: &AppHandle, game_id: &str, elapsed_mins: i64) {
54+
fn finish_session(app: &AppHandle, game_id: &str, started_at: &str, elapsed_mins: i64) {
5555
let now = Utc::now().to_rfc3339();
5656
{
5757
let db = app.state::<DbState>();
@@ -61,6 +61,13 @@ fn finish_session(app: &AppHandle, game_id: &str, elapsed_mins: i64) {
6161
"UPDATE games SET playtime_mins = playtime_mins + ?1, last_played = ?2 WHERE id = ?3",
6262
rusqlite::params![elapsed_mins, now, game_id],
6363
);
64+
if elapsed_mins > 0 {
65+
let session_id = uuid::Uuid::new_v4().to_string();
66+
let _ = conn.execute(
67+
"INSERT INTO sessions (id, game_id, started_at, ended_at, duration_mins) VALUES (?1, ?2, ?3, ?4, ?5)",
68+
rusqlite::params![session_id, game_id, started_at, now, elapsed_mins],
69+
);
70+
}
6471
}
6572
}
6673
let _ = app.emit("game-session-ended", game_id);
@@ -109,12 +116,13 @@ pub fn launch_game(app: AppHandle, state: State<DbState>, id: String) -> Result<
109116
let app_clone = app.clone();
110117
let game_id = id.clone();
111118
let start = std::time::Instant::now();
119+
let started_at = now.clone();
112120
let mut child = child;
113121

114122
std::thread::spawn(move || {
115123
let _ = child.wait();
116124
let elapsed_mins = start.elapsed().as_secs() as i64 / 60;
117-
finish_session(&app_clone, &game_id, elapsed_mins);
125+
finish_session(&app_clone, &game_id, &started_at, elapsed_mins);
118126
});
119127

120128
Ok(())
@@ -164,6 +172,7 @@ pub fn launch_steam_game(
164172
let app_clone = app.clone();
165173
let gid = game_id.clone();
166174
let pids = active_pids.0.clone();
175+
let started_at = now.clone();
167176

168177
std::thread::spawn(move || {
169178
#[cfg(windows)]
@@ -199,7 +208,9 @@ pub fn launch_steam_game(
199208
}
200209
pids.lock().unwrap_or_else(|e| e.into_inner()).remove(&pid);
201210
let elapsed_mins = start.elapsed().as_secs() as i64 / 60;
202-
finish_session(&app_clone, &gid, elapsed_mins);
211+
finish_session(&app_clone, &gid, &started_at, elapsed_mins);
212+
} else {
213+
finish_session(&app_clone, &gid, &started_at, 0);
203214
}
204215
}
205216
});
@@ -256,6 +267,7 @@ pub fn launch_epic_game(
256267
let app_clone = app.clone();
257268
let gid = game_id.clone();
258269
let pids = active_pids.0.clone();
270+
let started_at = now.clone();
259271

260272
std::thread::spawn(move || {
261273
#[cfg(windows)]
@@ -291,7 +303,9 @@ pub fn launch_epic_game(
291303
}
292304
pids.lock().unwrap_or_else(|e| e.into_inner()).remove(&pid);
293305
let elapsed_mins = start.elapsed().as_secs() as i64 / 60;
294-
finish_session(&app_clone, &gid, elapsed_mins);
306+
finish_session(&app_clone, &gid, &started_at, elapsed_mins);
307+
} else {
308+
finish_session(&app_clone, &gid, &started_at, 0);
295309
}
296310
}
297311
});

0 commit comments

Comments
 (0)