1use crate::constants;
7use crate::database;
8use crate::database::AppState;
9use crate::errors::AppError;
10use crate::models;
11use crate::utils::status_logic;
12use chrono::Utc;
13use rusqlite::{params, OptionalExtension};
14use serde::Deserialize;
15use tauri::State;
16use url::Url;
17
18#[derive(Debug, Deserialize)]
22pub struct GameInput {
23 pub id: String,
24 pub name: String,
25 pub platform: Option<String>,
26 #[serde(rename = "coverUrl")]
27 pub cover_url: Option<String>,
28 pub playtime: Option<i32>,
29 #[serde(rename = "userRating")]
30 pub user_rating: Option<i32>,
31 pub status: Option<String>,
32 #[serde(rename = "installPath")]
33 pub install_path: Option<String>,
34 #[serde(rename = "executablePath")]
35 pub executable_path: Option<String>,
36 #[serde(rename = "launchArgs")]
37 pub launch_args: Option<String>,
38}
39
40#[derive(serde::Deserialize)]
44pub struct UpdateGameDetailsInput {
45 pub id: String,
46 pub description: Option<String>, pub developer: Option<String>,
48 pub publisher: Option<String>,
49 pub released: Option<String>,
50}
51
52fn validate_input(game: &GameInput) -> Result<(), AppError> {
57 if game.name.trim().is_empty() {
58 return Err(AppError::ValidationError(
59 "Nome do jogo não pode ser vazio".to_string(),
60 ));
61 }
62
63 if game.name.len() > constants::MAX_NAME_LENGTH {
64 return Err(AppError::ValidationError(format!(
65 "Nome muito longo (max {})",
66 constants::MAX_NAME_LENGTH
67 )));
68 }
69
70 if let Some(ref url_str) = game.cover_url {
71 if url_str.len() > constants::MAX_URL_LENGTH {
72 return Err(AppError::ValidationError(format!(
73 "URL da capa muito longa (máximo {} caracteres)",
74 constants::MAX_URL_LENGTH
75 )));
76 }
77 if !url_str.starts_with("http") && !url_str.starts_with("asset://") {
79 let url = Url::parse(url_str)
80 .map_err(|_| AppError::ValidationError("URL inválida.".to_string()))?;
81 if url.scheme() != "http" && url.scheme() != "https" {
82 return Err(AppError::ValidationError(
83 "A URL deve ser HTTP, HTTPS ou Asset local.".to_string(),
84 ));
85 }
86 }
87 }
88
89 if let Some(ref p) = game.platform {
90 if p.len() > constants::MAX_PLATFORM_LENGTH {
91 return Err(AppError::ValidationError(format!(
92 "Plataforma muito longa (max {})",
93 constants::MAX_PLATFORM_LENGTH
94 )));
95 }
96 }
97
98 if let Some(time) = game.playtime {
99 if time < 0 {
100 return Err(AppError::ValidationError(
101 "Tempo jogado não pode ser negativo".to_string(),
102 ));
103 }
104 if time > constants::MAX_PLAYTIME {
105 return Err(AppError::ValidationError(
106 "Tempo jogado excessivo".to_string(),
107 ));
108 }
109 }
110
111 if let Some(r) = game.user_rating {
112 if !(constants::MIN_RATING..=constants::MAX_RATING).contains(&r) {
113 return Err(AppError::ValidationError(format!(
114 "Avaliação deve estar entre {} e {}",
115 constants::MIN_RATING,
116 constants::MAX_RATING
117 )));
118 }
119 }
120
121 Ok(())
122}
123
124#[tauri::command]
128pub fn add_game(state: State<AppState>, game: GameInput) -> Result<(), AppError> {
129 validate_input(&game)?;
130
131 let conn = state.library_db.lock()?;
132
133 let exists: bool = conn.query_row(
135 "SELECT EXISTS(SELECT 1 FROM games WHERE id = ?1)",
136 params![game.id],
137 |row| row.get(0),
138 )?;
139
140 if exists {
141 return Err(AppError::AlreadyExists(
142 "Já existe um jogo com este ID".to_string(),
143 ));
144 }
145
146 let final_status = game
148 .status
149 .unwrap_or_else(|| status_logic::calculate_status(game.playtime.unwrap_or(0)));
150
151 let added_at = Utc::now().to_rfc3339();
152 let platform = game.platform.unwrap_or("Manual".to_string());
153
154 conn.execute(
155 "INSERT INTO games (
156 id, name, cover_url, platform, platform_id, install_path, executable_path, launch_args,
157 user_rating, status, playtime, added_at
158 ) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11, ?12)",
159 params![
160 game.id,
161 game.name,
162 game.cover_url,
163 platform,
164 Option::<String>::None, game.install_path,
166 game.executable_path,
167 game.launch_args,
168 game.user_rating,
169 final_status,
170 game.playtime,
171 added_at
172 ],
173 )?;
174
175 Ok(())
176}
177
178#[tauri::command]
185pub fn update_game(state: State<AppState>, game: GameInput) -> Result<(), AppError> {
186 validate_input(&game)?;
187
188 let conn = state.library_db.lock()?;
189
190 conn.execute(
191 "UPDATE games SET
192 name = ?1,
193 cover_url = ?2,
194 platform = ?3,
195 playtime = ?4,
196 user_rating = ?5,
197 status = ?6,
198 install_path = ?7,
199 executable_path = ?8,
200 launch_args = ?9
201 WHERE id = ?10",
202 params![
203 game.name,
204 game.cover_url,
205 game.platform,
206 game.playtime,
207 game.user_rating,
208 game.status,
209 game.install_path,
210 game.executable_path,
211 game.launch_args,
212 game.id
213 ],
214 )?;
215
216 Ok(())
217}
218
219#[tauri::command]
224pub fn get_games(state: State<AppState>) -> Result<Vec<models::Game>, AppError> {
225 let conn = state.library_db.lock()?;
226
227 let mut stmt = conn
228 .prepare(
229 "SELECT
230 g.id, g.name, g.cover_url, g.platform, g.platform_id, g.install_path, g.executable_path,
231 g.launch_args, g.user_rating, g.favorite, g.status, g.playtime, g.last_played, g.added_at,
232 gd.genres, gd.developer, COALESCE(gd.is_adult, 0) as is_adult -- Campos da tabela game_details
233 FROM games g
234 LEFT JOIN game_details gd ON g.id = gd.game_id
235 ORDER BY g.name ASC"
236 )?;
237
238 let games = stmt
239 .query_map([], |row| {
240 Ok(models::Game {
241 id: row.get(0)?,
242 name: row.get(1)?,
243 cover_url: row.get(2)?,
244 platform: row.get(3)?,
245 platform_id: row.get(4)?,
246 install_path: row.get(5)?,
247 executable_path: row.get(6)?,
248 launch_args: row.get(7)?,
249 user_rating: row.get(8)?,
250 favorite: row.get(9)?,
251 status: row.get(10)?,
252 playtime: row.get(11)?,
253 last_played: row.get(12)?,
254 added_at: row.get(13)?,
255 genres: row.get(14)?,
256 developer: row.get(15)?,
257 is_adult: row.get(16)?,
258 })
259 })?
260 .collect::<Result<Vec<_>, _>>()?;
261
262 Ok(games)
263}
264
265#[tauri::command]
271pub fn get_library_game_details(
272 state: State<AppState>,
273 game_id: String,
274) -> Result<Option<models::GameDetails>, AppError> {
275 let conn = state.library_db.lock()?;
276
277 let mut stmt = conn.prepare(
278 "SELECT
279 game_id, steam_app_id, developer, publisher, release_date, genres, tags, series,
280 description_raw, description_ptbr, background_image, critic_score,
281 steam_review_label, steam_review_count, steam_review_score, steam_review_updated_at,
282 esrb_rating, is_adult, adult_tags, external_links, median_playtime,
283 estimated_playtime
284 FROM game_details
285 WHERE game_id = ?1",
286 )?;
287
288 let mut rows = stmt.query_map(params![game_id], |row| {
289 let links_json: Option<String> = row.get(19)?; let external_links = links_json.and_then(|json| serde_json::from_str(&json).ok());
291
292 let tags_json: Option<String> = row.get(6)?;
293 let tags = tags_json.map(|s| database::deserialize_tags(&s));
294
295 Ok(models::GameDetails {
296 game_id: row.get(0)?,
297 steam_app_id: row.get(1)?,
298 developer: row.get(2)?,
299 publisher: row.get(3)?,
300 release_date: row.get(4)?,
301 genres: row.get(5)?,
302 tags,
303 series: row.get(7)?,
304 description_raw: row.get(8)?,
305 description_ptbr: row.get(9)?,
306 background_image: row.get(10)?,
307 critic_score: row.get(11)?,
308 steam_review_label: row.get(12)?,
309 steam_review_count: row.get(13)?,
310 steam_review_score: row.get(14)?,
311 steam_review_updated_at: row.get(15)?,
312 esrb_rating: row.get(16)?,
313 is_adult: row.get(17).unwrap_or(false),
314 adult_tags: row.get(18)?,
315 external_links,
316 median_playtime: row.get(20)?,
317 estimated_playtime: row.get(21)?,
318 })
319 })?;
320
321 if let Some(row) = rows.next() {
322 Ok(Some(row?))
323 } else {
324 Ok(None)
325 }
326}
327
328#[tauri::command]
335pub fn toggle_favorite(state: State<AppState>, id: String) -> Result<(), AppError> {
336 let conn = state.library_db.lock()?;
337
338 conn.execute(
339 "UPDATE games SET favorite = NOT favorite WHERE id = ?1",
340 params![id],
341 )?;
342
343 Ok(())
344}
345
346#[tauri::command]
352pub fn set_game_status(state: State<AppState>, id: String, status: String) -> Result<(), AppError> {
353 let conn = state.library_db.lock()?;
354 conn.execute(
355 "UPDATE games SET status = ?1 WHERE id = ?2",
356 params![status, id],
357 )?;
358 Ok(())
359}
360
361#[tauri::command]
366pub fn set_game_rating(state: State<AppState>, id: String, rating: i32) -> Result<(), AppError> {
367 if !(0..=5).contains(&rating) {
369 return Err(AppError::ValidationError("Rating inválido".to_string()));
370 }
371
372 let conn = state.library_db.lock()?;
373
374 let val = if rating == 0 { None } else { Some(rating) };
376
377 conn.execute(
378 "UPDATE games SET user_rating = ?1 WHERE id = ?2",
379 params![val, id],
380 )?;
381 Ok(())
382}
383
384#[tauri::command]
388pub fn delete_game(state: State<AppState>, id: String) -> Result<(), AppError> {
389 let conn = state.library_db.lock()?;
390
391 conn.execute("DELETE FROM games WHERE id = ?1", params![id])?;
392
393 Ok(())
394}
395
396#[tauri::command]
402pub fn update_game_details(
403 state: State<AppState>,
404 payload: UpdateGameDetailsInput,
405) -> Result<(), AppError> {
406 let conn = state.library_db.lock().map_err(|_| AppError::MutexError)?;
407
408 let current_state: Option<Option<String>> = conn
410 .query_row(
411 "SELECT description_ptbr FROM game_details WHERE game_id = ?1",
412 params![payload.id],
413 |row| row.get(0),
414 )
415 .optional()?; match current_state {
418 Some(description_ptbr_atual) => {
420 if description_ptbr_atual.is_none() {
422 return Err(AppError::ValidationError(
423 "A descrição precisa ser traduzida (ou gerada) antes de ser editada manualmente.".to_string()
424 ));
425 }
426 conn.execute(
427 "UPDATE game_details SET
428 description_ptbr = ?1,
429 developer = ?2,
430 publisher = ?3,
431 release_date = ?4
432 WHERE game_id = ?5",
433 params![
434 payload.description,
435 payload.developer,
436 payload.publisher,
437 payload.released,
438 payload.id
439 ],
440 )?;
441 }
442
443 None => {
445 conn.execute(
446 "INSERT INTO game_details (game_id, description_ptbr, developer, publisher, release_date)
447 VALUES (?1, ?2, ?3, ?4, ?5)",
448 params![
449 payload.id,
450 payload.description, payload.developer,
452 payload.publisher,
453 payload.released
454 ],
455 )?;
456 }
457 }
458
459 Ok(())
460}