Skip to main content

danceinterpreter_rs/dataloading/dataprovider/
mod.rs

1use crate::dataloading::displayable_data::DisplayableData;
2use crate::dataloading::songinfo::SongInfo;
3use crate::dataloading::staticinfo::StaticInfo;
4use crate::traktor_api;
5use crate::traktor_api::TraktorDataProvider;
6use iced::Color;
7use std::cmp::PartialEq;
8use std::path::PathBuf;
9
10pub enum DeletedItem {
11    Playlist {
12        index: usize,
13        song: SongInfo,
14        played: bool,
15    },
16    Static {
17        name: String,
18        static_info: StaticInfo,
19    },
20}
21
22#[derive(Default, Debug, PartialEq, Clone)]
23pub enum ItemSource {
24    #[default]
25    Blank,
26    Traktor,
27    Other(SongInfo),
28    Static(String),
29    Playlist(usize),
30}
31
32#[derive(Debug, Clone)]
33pub enum ItemChange {
34    Blank,
35    Traktor,
36    StaticAbsolute(String),
37    PlaylistAbsolute(usize),
38    Previous,
39    Next,
40}
41#[derive(Debug, Clone)]
42pub enum SongDataEdit {
43    Title(String),
44    Artist(String),
45    Dance(String),
46}
47
48#[derive(Debug, Clone, PartialEq)]
49pub enum SubmitStaticResult {
50    Success,
51    Unchanged,
52    NotFound,
53    NeedsMerge { old_name: String, new_name: String },
54}
55
56use indexmap::IndexMap;
57
58#[derive(Debug, Clone, PartialEq)]
59pub struct PlaylistItem {
60    pub song: SongInfo,
61    pub played: bool,
62}
63
64#[derive(Default)]
65pub struct DataProvider {
66    pub traktor_provider: TraktorDataProvider,
67    pub statics_path: Option<PathBuf>,
68
69    playlist: Vec<PlaylistItem>,
70    statics: IndexMap<String, StaticInfo>,
71
72    deleted_items: Vec<DeletedItem>,
73
74    current: ItemSource,
75    next: Option<ItemSource>,
76
77    should_scroll: bool,
78}
79
80impl DataProvider {
81    pub fn set_vec(&mut self, vec: Vec<SongInfo>) {
82        self.playlist = vec
83            .into_iter()
84            .map(|song| PlaylistItem {
85                song,
86                played: false,
87            })
88            .collect();
89
90        let dances: Vec<String> = self
91            .playlist
92            .iter()
93            .map(|item| item.song.dance.clone())
94            .collect();
95        for dance in dances {
96            self.ensure_static(&dance);
97        }
98
99        if !self.playlist.is_empty() {
100            self.current = ItemSource::Playlist(0);
101        } else {
102            self.current = ItemSource::Blank;
103        }
104    }
105
106    pub fn set_statics(&mut self, vec: Vec<StaticInfo>) {
107        self.statics = vec.into_iter().map(|s| (s.name.clone(), s)).collect();
108    }
109
110    pub fn get_current_displayable_data(&self) -> Option<DisplayableData> {
111        match self.current {
112            ItemSource::Static(ref name) => self.statics.get(name).map(|s| s.into()),
113            ItemSource::Playlist(i) => self.playlist.get(i).map(|item| (&item.song).into()),
114            ItemSource::Other(ref song) => Some(song.into()),
115            ItemSource::Blank => None,
116            ItemSource::Traktor => self.traktor_provider.get_song_info().map(|s| s.into()),
117        }
118    }
119    pub fn get_next_displayable_data(&self) -> Option<DisplayableData> {
120        if let Some(next) = self.next.as_ref() {
121            return match next {
122                ItemSource::Static(name) => self.statics.get(name).map(|s| s.into()),
123                ItemSource::Playlist(i) => self.playlist.get(*i).map(|item| (&item.song).into()),
124                ItemSource::Other(song) => Some(song.into()),
125                ItemSource::Blank => None,
126                ItemSource::Traktor => self.traktor_provider.get_next_song_info().map(|s| s.into()),
127            };
128        }
129
130        match self.current {
131            ItemSource::Static(_) => None,
132            ItemSource::Playlist(i) => self.playlist.get(i + 1).map(|item| (&item.song).into()),
133            ItemSource::Other(ref song) => Some(song.into()),
134            ItemSource::Blank => None,
135            ItemSource::Traktor => self.traktor_provider.get_next_song_info().map(|s| s.into()),
136        }
137    }
138
139    pub fn prev(&mut self) {
140        self.should_scroll = true;
141
142        let ItemSource::Playlist(current_index) = self.current else {
143            return;
144        };
145
146        if current_index == 0 {
147            return;
148        }
149
150        self.set_current_as_played();
151        self.current = ItemSource::Playlist(current_index - 1);
152    }
153
154    pub fn next(&mut self) {
155        self.should_scroll = true;
156
157        if let Some(next) = self.next.take() {
158            self.set_current_as_played();
159            self.current = next;
160            return;
161        }
162
163        let ItemSource::Playlist(current_index) = self.current else {
164            return;
165        };
166
167        if current_index == self.playlist.len() - 1 {
168            return;
169        }
170
171        self.set_current_as_played();
172        self.current = ItemSource::Playlist(current_index + 1);
173    }
174
175    pub fn set_current(&mut self, n: ItemSource) {
176        self.set_current_as_played();
177
178        match n {
179            ItemSource::Static(ref name) => {
180                if self.statics.get(name).is_some() {
181                    self.current = n;
182                }
183            }
184            ItemSource::Playlist(i) => {
185                if self.playlist.get(i).is_some() {
186                    self.current = n;
187                }
188            }
189            _ => self.current = n,
190        }
191    }
192
193    pub fn set_next(&mut self, next: ItemSource) {
194        self.next = Some(next);
195    }
196
197    pub fn append_song(&mut self, song: SongInfo) {
198        self.ensure_static(&song.dance);
199        self.playlist.push(PlaylistItem {
200            song,
201            played: false,
202        });
203    }
204
205    pub fn ensure_static(&mut self, dance: &str) {
206        if !dance.is_empty() && !self.statics.contains_key(dance) {
207            self.statics
208                .insert(dance.to_string(), StaticInfo::new(dance.to_string()));
209        }
210    }
211
212    pub fn add_static(&mut self) {
213        let mut name = "New Static".to_string();
214        let mut counter = 1;
215        while self.statics.contains_key(&name) {
216            counter += 1;
217            name = format!("New Static {}", counter);
218        }
219        self.statics.insert(name.clone(), StaticInfo::new(name));
220        self.save_statics();
221    }
222
223    pub fn toggle_static_favorite(&mut self, name: &str) {
224        if let Some(song) = self.statics.get_mut(name) {
225            song.is_favorite = !song.is_favorite;
226            self.save_statics();
227        }
228    }
229
230    // handle_static_data_edit removed (handled in main.rs now)
231
232    pub fn delete_item(&mut self, song: ItemSource) {
233        if self.current == song {
234            self.current = ItemSource::Blank;
235        } else if self.next == Some(song.clone()) {
236            self.next = None;
237        }
238
239        if let ItemSource::Playlist(i) = song {
240            let item = self.playlist.remove(i);
241            self.deleted_items.push(DeletedItem::Playlist {
242                index: i,
243                song: item.song,
244                played: item.played,
245            });
246        } else if let ItemSource::Static(ref name) = song
247            && let Some(static_info) = self.statics.shift_remove(name)
248        {
249            self.deleted_items.push(DeletedItem::Static {
250                name: name.clone(),
251                static_info,
252            });
253
254            self.save_statics();
255        }
256    }
257
258    pub fn undo_delete(&mut self) {
259        if let Some(item) = self.deleted_items.pop() {
260            match item {
261                DeletedItem::Playlist {
262                    index,
263                    song,
264                    played,
265                } => {
266                    let insert_idx = index.min(self.playlist.len());
267                    self.playlist
268                        .insert(insert_idx, PlaylistItem { song, played });
269                }
270                DeletedItem::Static { name, static_info } => {
271                    self.statics.insert(name, static_info);
272                    self.save_statics();
273                }
274            }
275        }
276    }
277
278    pub fn handle_item_change(&mut self, change: ItemChange) {
279        match change {
280            ItemChange::Blank => {
281                self.set_current_as_played();
282                self.current = ItemSource::Blank;
283            }
284            ItemChange::Traktor => {
285                self.set_current_as_played();
286                self.current = ItemSource::Traktor;
287            }
288            ItemChange::StaticAbsolute(index) => {
289                self.traktor_provider.sync = false;
290                self.set_current_as_played();
291                self.current = ItemSource::Static(index);
292            }
293            ItemChange::PlaylistAbsolute(index) => {
294                self.set_current_as_played();
295                self.current = ItemSource::Playlist(index);
296            }
297            ItemChange::Previous => {
298                self.prev();
299            }
300            ItemChange::Next => {
301                self.next();
302            }
303        }
304    }
305
306    pub fn handle_song_data_edit(&mut self, idx: usize, edit: SongDataEdit) {
307        if let Some(song) = self.playlist.get_mut(idx).map(|item| &mut item.song) {
308            match edit {
309                SongDataEdit::Title(title) => {
310                    song.title = title;
311                }
312                SongDataEdit::Artist(artist) => {
313                    song.artist = artist;
314                }
315                SongDataEdit::Dance(v) => {
316                    song.dance = v;
317                }
318            }
319        }
320    }
321
322    pub fn handle_song_data_submit(&mut self, idx: usize) {
323        if let Some(item) = self.playlist.get(idx) {
324            let dance = item.song.dance.clone();
325            self.ensure_static(&dance);
326            self.save_statics();
327        }
328    }
329
330    pub fn process_traktor_message(&mut self, message: traktor_api::ServerMessage) {
331        self.set_current_as_played();
332        self.traktor_provider.process_message(
333            message,
334            &self
335                .playlist
336                .iter()
337                .map(|i| i.song.clone())
338                .collect::<Vec<_>>(),
339        );
340    }
341
342    pub fn get_current_traktor_index(&self) -> Option<usize> {
343        self.traktor_provider.get_current_index(
344            &self
345                .playlist
346                .iter()
347                .map(|i| i.song.clone())
348                .collect::<Vec<_>>(),
349        )
350    }
351
352    pub fn take_scroll_index(&mut self) -> Option<usize> {
353        let should_scroll = self.should_scroll | self.traktor_provider.take_should_scroll();
354        self.should_scroll = false;
355
356        if !should_scroll {
357            return None;
358        }
359
360        match self.current {
361            ItemSource::Traktor => self.get_current_traktor_index(),
362            ItemSource::Playlist(i) => Some(i),
363            _ => None,
364        }
365    }
366
367    pub fn get_play_state(&self, playlist_index: usize) -> (bool, bool, bool, bool) {
368        let mut is_current = false;
369        let mut is_next = false;
370        let mut is_traktor = false;
371        let is_played = self
372            .playlist
373            .get(playlist_index)
374            .map(|item| item.played)
375            .unwrap_or(false);
376
377        if let ItemSource::Playlist(i) = self.current {
378            is_current = playlist_index == i;
379            is_next = playlist_index == (i + 1);
380        }
381
382        if let Some(ItemSource::Playlist(i)) = self.next {
383            is_next = playlist_index == i;
384        }
385
386        if matches!(self.current, ItemSource::Traktor)
387            && let Some(index) = self.get_current_traktor_index()
388        {
389            is_traktor = playlist_index == index;
390        }
391
392        (is_current, is_next, is_traktor, is_played)
393    }
394
395    pub fn ensure_statics_for_playlist(&mut self) {
396        let dances: Vec<String> = self
397            .playlist
398            .iter()
399            .map(|item| item.song.dance.clone())
400            .collect();
401        for dance in dances {
402            self.ensure_static(&dance);
403        }
404        self.save_statics();
405    }
406
407    pub fn get_dance_color(&self, dance: &str) -> Option<Color> {
408        self.statics.get(dance).and_then(|s| s.color)
409    }
410
411    pub fn rename_static(&mut self, old_name: &str, new_name: &str) -> Result<(), &'static str> {
412        if let Some(static_info) = self.statics.get_mut(old_name) {
413            static_info.name = new_name.to_string();
414            Ok(())
415        } else {
416            Err("Static not found")
417        }
418    }
419
420    pub fn process_static_name_submit(&mut self, key: &str) -> SubmitStaticResult {
421        let static_info = match self.statics.get(key) {
422            Some(info) => info,
423            None => return SubmitStaticResult::NotFound,
424        };
425
426        let typed_name = static_info.name.clone();
427
428        if key == typed_name {
429            return SubmitStaticResult::Unchanged;
430        }
431
432        if self.statics.contains_key(&typed_name) {
433            return SubmitStaticResult::NeedsMerge {
434                old_name: key.to_string(),
435                new_name: typed_name,
436            };
437        }
438
439        // If it's a completely new name, do the rename/re-keying immediately
440        let _ = self.submit_static_name(key, &typed_name);
441        SubmitStaticResult::Success
442    }
443
444    pub fn submit_static_name(
445        &mut self,
446        old_name: &str,
447        new_name: &str,
448    ) -> Result<(), &'static str> {
449        if self.statics.contains_key(new_name) {
450            return Err("Static with new name already exists");
451        }
452
453        if let Some(mut static_info) = self.statics.shift_remove(old_name) {
454            static_info.name = new_name.to_string();
455            self.statics.insert(new_name.to_string(), static_info);
456            for item in &mut self.playlist {
457                if item.song.dance == old_name {
458                    item.song.dance = new_name.to_string();
459                }
460            }
461            self.save_statics();
462            Ok(())
463        } else {
464            Err("Static not found")
465        }
466    }
467
468    pub fn update_static_color(
469        &mut self,
470        name: &str,
471        color: Option<Color>,
472        save: bool,
473    ) -> Result<(), &'static str> {
474        if let Some(static_info) = self.statics.get_mut(name) {
475            static_info.color = color;
476            if save {
477                self.save_statics();
478            }
479            Ok(())
480        } else {
481            Err("Static not found")
482        }
483    }
484
485    pub fn merge_statics(&mut self, old_name: &str, new_name: &str) -> Result<(), &'static str> {
486        if !self.statics.contains_key(new_name) {
487            return Err("Target static does not exist");
488        }
489
490        if self.statics.shift_remove(old_name).is_some() {
491            for item in &mut self.playlist {
492                if item.song.dance == old_name {
493                    item.song.dance = new_name.to_string();
494                }
495            }
496            self.save_statics();
497            Ok(())
498        } else {
499            Err("Source static not found")
500        }
501    }
502
503    fn set_current_as_played(&mut self) {
504        let i = match self.current {
505            ItemSource::Playlist(i) => i,
506            ItemSource::Traktor => {
507                let Some(index) = self.get_current_traktor_index() else {
508                    return;
509                };
510                index
511            }
512            _ => return,
513        };
514
515        if let Some(item) = self.playlist.get_mut(i) {
516            item.played = true;
517        }
518    }
519
520    #[allow(dead_code)]
521    pub fn set_statics_path(&mut self, path: PathBuf) {
522        self.statics_path = Some(path);
523    }
524
525    fn save_statics(&self) {
526        let values: Vec<&StaticInfo> = self.statics.values().collect();
527        if let Ok(json) = serde_json::to_string_pretty(&values) {
528            let _ = std::fs::write(self.get_statics_path(), json);
529        }
530    }
531
532    pub fn get_statics_path(&self) -> PathBuf {
533        if let Some(path) = &self.statics_path {
534            return path.clone();
535        }
536
537        Self::default_statics_path()
538    }
539
540    #[cfg(test)]
541    fn default_statics_path() -> PathBuf {
542        let mut path = std::env::temp_dir();
543        path.push("danceinterpreter_test_statics.json");
544        path
545    }
546
547    #[cfg(not(test))]
548    fn default_statics_path() -> PathBuf {
549        if let Some(mut path) = dirs::config_dir() {
550            path.push("danceinterpreter");
551            let _ = std::fs::create_dir_all(&path);
552            path.push("statics.json");
553            path
554        } else {
555            PathBuf::from("./statics.json")
556        }
557    }
558
559    pub fn load_statics(&mut self) {
560        let statics: Vec<StaticInfo> = std::fs::read_to_string(self.get_statics_path())
561            .map(|file_content| serde_json::from_str(&file_content).ok())
562            .unwrap_or_default()
563            .unwrap_or_default();
564
565        self.set_statics(statics);
566        self.save_statics();
567    }
568
569    pub fn statics(&self) -> &IndexMap<String, StaticInfo> {
570        &self.statics
571    }
572
573    pub fn playlist(&self) -> &Vec<PlaylistItem> {
574        &self.playlist
575    }
576}
577
578#[cfg(test)]
579mod tests {
580    use super::*;
581    use crate::dataloading::songinfo::SongInfo;
582
583    fn test_provider() -> (DataProvider, tempfile::TempDir) {
584        let dir = tempfile::tempdir().expect("failed to create temp dir");
585        let mut provider = DataProvider::default();
586        provider.set_statics_path(dir.path().join("statics.json"));
587        (provider, dir)
588    }
589
590    #[test]
591    fn test_ensure_static_empty() {
592        let (mut provider, _dir) = test_provider();
593        provider.ensure_static("");
594        assert!(provider.statics.is_empty());
595    }
596
597    #[test]
598    fn test_ensure_static_new() {
599        let (mut provider, _dir) = test_provider();
600        provider.ensure_static("Waltz");
601        assert!(provider.statics.contains_key("Waltz"));
602    }
603
604    #[test]
605    fn test_ensure_static_existing() {
606        let (mut provider, _dir) = test_provider();
607        provider.ensure_static("Waltz");
608        provider.statics.get_mut("Waltz").unwrap().is_favorite = true;
609
610        provider.ensure_static("Waltz"); // Should not overwrite
611        assert!(provider.statics.get("Waltz").unwrap().is_favorite);
612    }
613
614    #[test]
615    fn test_playlist_dance_updates_triggering_static_creation() {
616        let (mut provider, _dir) = test_provider();
617        let song = SongInfo {
618            dance: "Waltz".to_string(),
619            ..Default::default()
620        };
621        provider.playlist.push(PlaylistItem {
622            song,
623            played: false,
624        });
625
626        provider.handle_song_data_edit(0, SongDataEdit::Dance("Tango".to_string()));
627        provider.handle_song_data_submit(0);
628
629        assert_eq!(provider.playlist[0].song.dance, "Tango");
630        assert!(provider.statics.contains_key("Tango"));
631    }
632
633    #[test]
634    fn test_ensure_statics_for_playlist() {
635        let (mut provider, _dir) = test_provider();
636        let song = SongInfo {
637            dance: "Waltz".to_string(),
638            ..Default::default()
639        };
640        provider.playlist.push(PlaylistItem {
641            song,
642            played: false,
643        });
644
645        provider.ensure_statics_for_playlist();
646        assert!(provider.statics.contains_key("Waltz"));
647    }
648
649    #[test]
650    fn test_submit_static_name() {
651        let (mut provider, _dir) = test_provider();
652        provider.ensure_static("OldDance");
653        let song = SongInfo {
654            dance: "OldDance".to_string(),
655            ..Default::default()
656        };
657        provider.playlist.push(PlaylistItem {
658            song,
659            played: false,
660        });
661
662        assert!(provider.submit_static_name("OldDance", "NewDance").is_ok());
663
664        assert!(!provider.statics.contains_key("OldDance"));
665        assert!(provider.statics.contains_key("NewDance"));
666        assert_eq!(provider.playlist[0].song.dance, "NewDance");
667    }
668
669    #[test]
670    fn test_rename_static_display_name() {
671        let (mut provider, _dir) = test_provider();
672        provider.ensure_static("OldDance");
673
674        assert!(provider.rename_static("OldDance", "NewDisplay").is_ok());
675
676        // The key should remain the same
677        assert!(provider.statics.contains_key("OldDance"));
678        // The display name should change
679        assert_eq!(provider.statics.get("OldDance").unwrap().name, "NewDisplay");
680    }
681
682    #[test]
683    fn test_update_static_color() {
684        let (mut provider, _dir) = test_provider();
685        provider.ensure_static("ColorDance");
686
687        let color = Color::from_rgb(1.0, 0.0, 0.0);
688        assert!(
689            provider
690                .update_static_color("ColorDance", Some(color), false)
691                .is_ok()
692        );
693
694        assert_eq!(
695            provider.statics.get("ColorDance").unwrap().color,
696            Some(color)
697        );
698
699        // Revert to None
700        assert!(
701            provider
702                .update_static_color("ColorDance", None, false)
703                .is_ok()
704        );
705        assert_eq!(provider.statics.get("ColorDance").unwrap().color, None);
706    }
707
708    #[test]
709    fn test_toggle_static_favorite() {
710        let (mut provider, _dir) = test_provider();
711        provider.ensure_static("FavDance");
712
713        assert!(!provider.statics.get("FavDance").unwrap().is_favorite);
714
715        provider.toggle_static_favorite("FavDance");
716        assert!(provider.statics.get("FavDance").unwrap().is_favorite);
717
718        provider.toggle_static_favorite("FavDance");
719        assert!(!provider.statics.get("FavDance").unwrap().is_favorite);
720    }
721
722    #[test]
723    fn test_merge_statics() {
724        let (mut provider, _dir) = test_provider();
725        provider.ensure_static("SourceDance");
726        provider.ensure_static("TargetDance");
727        let song = SongInfo {
728            dance: "SourceDance".to_string(),
729            ..Default::default()
730        };
731        provider.playlist.push(PlaylistItem {
732            song,
733            played: false,
734        });
735
736        assert!(provider.merge_statics("SourceDance", "TargetDance").is_ok());
737
738        assert!(!provider.statics.contains_key("SourceDance"));
739        assert!(provider.statics.contains_key("TargetDance"));
740        assert_eq!(provider.playlist[0].song.dance, "TargetDance");
741    }
742}