1use serde::{Deserialize, Serialize};
5use std::collections::{HashMap, HashSet};
6
7#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
9pub struct GameTag {
10 pub id: String,
11 pub name: String,
12 pub color: Option<String>,
13}
14
15#[derive(Debug, Clone, Serialize, Deserialize)]
17pub struct GameCollection {
18 pub id: String,
19 pub name: String,
20 pub description: Option<String>,
21 pub is_favorite: bool,
22 pub game_ids: HashSet<String>,
23 pub created_at: chrono::DateTime<chrono::Utc>,
24}
25
26impl GameCollection {
27 pub fn new(name: String) -> Self {
28 Self {
29 id: uuid::Uuid::new_v4().to_string(),
30 name,
31 description: None,
32 is_favorite: false,
33 game_ids: HashSet::new(),
34 created_at: chrono::Utc::now(),
35 }
36 }
37
38 pub fn add_game(&mut self, game_id: String) {
39 self.game_ids.insert(game_id);
40 }
41
42 pub fn remove_game(&mut self, game_id: &str) {
43 self.game_ids.remove(game_id);
44 }
45
46 pub fn contains_game(&self, game_id: &str) -> bool {
47 self.game_ids.contains(game_id)
48 }
49}
50
51#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
53pub enum SortCriteria {
54 Name,
55 PlayTime,
56 InstallDate,
57 Size,
58 LastPlayed,
59 DateAdded,
60 UpdateDate,
61}
62
63#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
65pub enum SortOrder {
66 Ascending,
67 Descending,
68}
69
70#[derive(Debug, Clone, Serialize, Deserialize)]
72pub struct MultiLevelSort {
73 pub primary: SortCriteria,
74 pub primary_order: SortOrder,
75 pub secondary: Option<(SortCriteria, SortOrder)>,
76 pub tertiary: Option<(SortCriteria, SortOrder)>,
77}
78
79impl Default for MultiLevelSort {
80 fn default() -> Self {
81 Self {
82 primary: SortCriteria::Name,
83 primary_order: SortOrder::Ascending,
84 secondary: None,
85 tertiary: None,
86 }
87 }
88}
89
90#[derive(Debug, Clone, Default, Serialize, Deserialize)]
92pub struct AdvancedFilter {
93 pub tags: Vec<String>,
95 pub collections: Vec<String>,
97 pub only_installed: bool,
99 pub only_updates_available: bool,
101 pub only_favorites: bool,
103 pub genres: Vec<String>,
105 pub min_playtime_hours: Option<f32>,
107 pub max_playtime_hours: Option<f32>,
109 pub min_size_gb: Option<f32>,
111 pub max_size_gb: Option<f32>,
113 pub has_dlc_owned: Option<bool>,
115 pub text_search: Option<String>,
117}
118
119#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
121pub enum ViewMode {
122 Grid, List, Compact, }
126
127impl Default for ViewMode {
128 fn default() -> Self {
129 Self::Grid
130 }
131}
132
133#[derive(Debug, Clone, Serialize, Deserialize)]
135pub struct OrganizationPreferences {
136 pub view_mode: ViewMode,
137 pub sort: MultiLevelSort,
138 pub filter: AdvancedFilter,
139}
140
141impl Default for OrganizationPreferences {
142 fn default() -> Self {
143 Self {
144 view_mode: ViewMode::Grid,
145 sort: MultiLevelSort::default(),
146 filter: AdvancedFilter::default(),
147 }
148 }
149}
150
151#[derive(Debug, Clone, Serialize, Deserialize)]
153pub struct GameOrganization {
154 pub tags: HashMap<String, GameTag>,
156 pub collections: HashMap<String, GameCollection>,
158 pub game_tags: HashMap<String, Vec<String>>,
160 pub favorites: HashSet<String>,
162 pub preferences: OrganizationPreferences,
164}
165
166impl Default for GameOrganization {
167 fn default() -> Self {
168 Self::new()
169 }
170}
171
172impl GameOrganization {
173 pub fn new() -> Self {
174 let mut collections = HashMap::new();
176 let all_games = GameCollection {
177 id: "all_games".to_string(),
178 name: "All Games".to_string(),
179 description: Some("All games in library".to_string()),
180 is_favorite: false,
181 game_ids: HashSet::new(),
182 created_at: chrono::Utc::now(),
183 };
184 collections.insert("all_games".to_string(), all_games);
185
186 Self {
187 tags: HashMap::new(),
188 collections,
189 game_tags: HashMap::new(),
190 favorites: HashSet::new(),
191 preferences: OrganizationPreferences::default(),
192 }
193 }
194
195 pub fn add_tag(&mut self, tag: GameTag) {
197 self.tags.insert(tag.id.clone(), tag);
198 }
199
200 pub fn remove_tag(&mut self, tag_id: &str) {
201 self.tags.remove(tag_id);
202 for tags in self.game_tags.values_mut() {
204 tags.retain(|t| t != tag_id);
205 }
206 }
207
208 pub fn tag_game(&mut self, game_id: String, tag_id: String) {
209 self.game_tags.entry(game_id).or_insert_with(Vec::new).push(tag_id);
210 }
211
212 pub fn untag_game(&mut self, game_id: &str, tag_id: &str) {
213 if let Some(tags) = self.game_tags.get_mut(game_id) {
214 tags.retain(|t| t != tag_id);
215 }
216 }
217
218 pub fn get_game_tags(&self, game_id: &str) -> Vec<&GameTag> {
219 self.game_tags
220 .get(game_id)
221 .map(|tag_ids| {
222 tag_ids
223 .iter()
224 .filter_map(|tag_id| self.tags.get(tag_id))
225 .collect()
226 })
227 .unwrap_or_default()
228 }
229
230 pub fn add_favorite(&mut self, game_id: String) {
232 self.favorites.insert(game_id);
233 }
234
235 pub fn remove_favorite(&mut self, game_id: &str) {
236 self.favorites.remove(game_id);
237 }
238
239 pub fn is_favorite(&self, game_id: &str) -> bool {
240 self.favorites.contains(game_id)
241 }
242
243 pub fn create_collection(&mut self, name: String) -> String {
245 let collection = GameCollection::new(name);
246 let id = collection.id.clone();
247 self.collections.insert(id.clone(), collection);
248 id
249 }
250
251 pub fn delete_collection(&mut self, collection_id: &str) {
252 self.collections.remove(collection_id);
253 }
254
255 pub fn add_to_collection(&mut self, collection_id: &str, game_id: String) {
256 if let Some(collection) = self.collections.get_mut(collection_id) {
257 collection.add_game(game_id);
258 }
259 }
260
261 pub fn remove_from_collection(&mut self, collection_id: &str, game_id: &str) {
262 if let Some(collection) = self.collections.get_mut(collection_id) {
263 collection.remove_game(game_id);
264 }
265 }
266
267 pub fn get_collection(&self, collection_id: &str) -> Option<&GameCollection> {
268 self.collections.get(collection_id)
269 }
270
271 pub fn get_collection_mut(&mut self, collection_id: &str) -> Option<&mut GameCollection> {
272 self.collections.get_mut(collection_id)
273 }
274
275 pub fn list_collections(&self) -> Vec<&GameCollection> {
276 self.collections.values().collect()
277 }
278
279 pub fn set_view_mode(&mut self, mode: ViewMode) {
281 self.preferences.view_mode = mode;
282 }
283
284 pub fn set_sort(&mut self, sort: MultiLevelSort) {
285 self.preferences.sort = sort;
286 }
287
288 pub fn set_filter(&mut self, filter: AdvancedFilter) {
289 self.preferences.filter = filter;
290 }
291
292 pub fn filter_games(&self, game_ids: &[String]) -> Vec<String> {
294 let filter = &self.preferences.filter;
295
296 game_ids
297 .iter()
298 .filter(|game_id| {
299 if let Some(ref search) = filter.text_search {
301 if !game_id.to_lowercase().contains(&search.to_lowercase()) {
304 return false;
305 }
306 }
307
308 if filter.only_favorites && !self.is_favorite(game_id) {
310 return false;
311 }
312
313 if !filter.tags.is_empty() {
315 let game_tags: Vec<String> = self
316 .game_tags
317 .get(game_id.as_str())
318 .cloned()
319 .unwrap_or_default();
320 if !filter.tags.iter().all(|tag| game_tags.contains(tag)) {
321 return false;
322 }
323 }
324
325 true
326 })
327 .cloned()
328 .collect()
329 }
330}
331
332#[cfg(test)]
333mod tests {
334 use super::*;
335
336 #[test]
337 fn test_organization_new() {
338 let org = GameOrganization::new();
339 assert!(org.collections.contains_key("all_games"));
340 assert!(org.tags.is_empty());
341 assert!(org.favorites.is_empty());
342 }
343
344 #[test]
345 fn test_tag_management() {
346 let mut org = GameOrganization::new();
347 let tag = GameTag {
348 id: "tag1".to_string(),
349 name: "Favorites".to_string(),
350 color: Some("#FF0000".to_string()),
351 };
352
353 org.add_tag(tag);
354 assert!(org.tags.contains_key("tag1"));
355
356 org.tag_game("game1".to_string(), "tag1".to_string());
357 assert_eq!(org.get_game_tags("game1").len(), 1);
358 }
359
360 #[test]
361 fn test_favorite_management() {
362 let mut org = GameOrganization::new();
363 org.add_favorite("game1".to_string());
364 assert!(org.is_favorite("game1"));
365
366 org.remove_favorite("game1");
367 assert!(!org.is_favorite("game1"));
368 }
369
370 #[test]
371 fn test_collection_management() {
372 let mut org = GameOrganization::new();
373 let collection_id = org.create_collection("My Games".to_string());
374
375 org.add_to_collection(&collection_id, "game1".to_string());
376 {
377 let collection = org.get_collection(&collection_id).unwrap();
378 assert!(collection.contains_game("game1"));
379 }
380
381 org.remove_from_collection(&collection_id, "game1");
382 {
383 let collection = org.get_collection(&collection_id).unwrap();
384 assert!(!collection.contains_game("game1"));
385 }
386 }
387
388 #[test]
389 fn test_filtering() {
390 let mut org = GameOrganization::new();
391 org.add_favorite("game1".to_string());
392 org.add_favorite("game2".to_string());
393
394 org.preferences.filter.only_favorites = true;
395
396 let filtered = org.filter_games(&["game1".to_string(), "game2".to_string(), "game3".to_string()]);
397 assert_eq!(filtered.len(), 2);
398 }
399
400 #[test]
401 fn test_view_modes() {
402 let mut org = GameOrganization::new();
403 org.set_view_mode(ViewMode::List);
404 assert_eq!(org.preferences.view_mode, ViewMode::List);
405 }
406}