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 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 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"); 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 assert!(provider.statics.contains_key("OldDance"));
678 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 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}