epik/games/
organization.rs

1// Game Organization Module for v1.2.0
2// Supports custom categories/tags, favorites, collections, sorting and filtering
3
4use serde::{Deserialize, Serialize};
5use std::collections::{HashMap, HashSet};
6
7// Custom tag for organization
8#[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// Game collection/category
16#[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// Sorting criteria
52#[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// Sort order
64#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
65pub enum SortOrder {
66    Ascending,
67    Descending,
68}
69
70// Multi-level sort specification
71#[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// Advanced filter criteria
91#[derive(Debug, Clone, Default, Serialize, Deserialize)]
92pub struct AdvancedFilter {
93    // Filter by tags (AND logic)
94    pub tags: Vec<String>,
95    // Filter by collections
96    pub collections: Vec<String>,
97    // Only installed games
98    pub only_installed: bool,
99    // Only games with updates available
100    pub only_updates_available: bool,
101    // Only favorite games
102    pub only_favorites: bool,
103    // Filter by genres
104    pub genres: Vec<String>,
105    // Minimum playtime in hours
106    pub min_playtime_hours: Option<f32>,
107    // Maximum playtime in hours
108    pub max_playtime_hours: Option<f32>,
109    // Minimum size in GB
110    pub min_size_gb: Option<f32>,
111    // Maximum size in GB
112    pub max_size_gb: Option<f32>,
113    // DLC ownership filter
114    pub has_dlc_owned: Option<bool>,
115    // Text search in title/description
116    pub text_search: Option<String>,
117}
118
119// View mode for displaying games
120#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
121pub enum ViewMode {
122    Grid,  // Cards in grid layout
123    List,  // Detailed list view
124    Compact, // Compact list with minimal info
125}
126
127impl Default for ViewMode {
128    fn default() -> Self {
129        Self::Grid
130    }
131}
132
133// Game organization preferences
134#[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// Game organization data structure
152#[derive(Debug, Clone, Serialize, Deserialize)]
153pub struct GameOrganization {
154    // Tags for games
155    pub tags: HashMap<String, GameTag>,
156    // Collections
157    pub collections: HashMap<String, GameCollection>,
158    // Game ID -> tag IDs mapping
159    pub game_tags: HashMap<String, Vec<String>>,
160    // Favorite games
161    pub favorites: HashSet<String>,
162    // User preferences
163    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        // Create default "All Games" collection
175        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    // Tag management
196    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        // Remove tag from all games
203        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    // Favorite management
231    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    // Collection management
244    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    // Preference management
280    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    // Filter games based on criteria
293    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                // Text search
300                if let Some(ref search) = filter.text_search {
301                    // This would need game title/description from external source
302                    // For now, just check against app_name
303                    if !game_id.to_lowercase().contains(&search.to_lowercase()) {
304                        return false;
305                    }
306                }
307
308                // Favorites filter
309                if filter.only_favorites && !self.is_favorite(game_id) {
310                    return false;
311                }
312
313                // Tags filter (AND logic)
314                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}