@@ -2,7 +2,7 @@ use tauri::State;
22use chrono:: Utc ;
33use uuid:: Uuid ;
44use 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]
88pub 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]
6986pub 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]
75128pub 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+ }
0 commit comments