1pub mod dynamic;
2use std::time::Duration;
3
4use crossterm::event::{KeyCode, KeyEvent};
5use mecomp_core::format_duration;
6use mecomp_prost::RecordId;
7use mecomp_prost::{
8 Album, AlbumBrief, Artist, ArtistBrief, Collection, DynamicPlaylist, Playlist, Song, SongBrief,
9};
10use ratatui::widgets::{Scrollbar, ScrollbarOrientation};
11use ratatui::{
12 layout::Alignment,
13 style::Style,
14 text::{Line, Span},
15 widgets::{Paragraph, Widget},
16};
17use tokio::sync::mpsc::UnboundedSender;
18use traits::ItemViewProps;
19
20use crate::state::action::{Action, LibraryAction, PopupAction};
21use crate::ui::components::content_view::views::traits::{SortMode, SortableViewProps};
22use crate::ui::widgets::popups::PopupType;
23use crate::ui::widgets::tree::item::CheckTreeItem;
24use crate::ui::widgets::tree::state::CheckTreeState;
25
26pub mod album;
27pub mod artist;
28pub mod collection;
29pub mod generic;
30pub mod none;
31pub mod playlist;
32pub mod radio;
33pub mod random;
34pub mod search;
35pub mod song;
36pub mod sort_mode;
37pub mod traits;
38
39#[allow(clippy::module_name_repetitions)]
41#[derive(Debug, Clone, Default, PartialEq, Eq)]
42pub struct ViewData {
43 pub album: Option<AlbumViewProps>,
44 pub artist: Option<ArtistViewProps>,
45 pub collection: Option<CollectionViewProps>,
46 pub dynamic_playlist: Option<DynamicPlaylistViewProps>,
47 pub playlist: Option<PlaylistViewProps>,
48 pub song: Option<SongViewProps>,
49 pub radio: Option<RadioViewProps>,
50 pub random: Option<RandomViewProps>,
51}
52
53#[derive(Debug, Clone, PartialEq, Eq)]
54pub struct AlbumViewProps {
55 pub id: RecordId,
56 pub album: Album,
57 pub artists: Vec<ArtistBrief>,
58 pub songs: Vec<SongBrief>,
59}
60
61impl ItemViewProps for AlbumViewProps {
62 fn id(&self) -> &RecordId {
63 &self.id
64 }
65
66 fn retrieve(view_data: &ViewData) -> Option<Self> {
67 view_data.album.clone()
68 }
69
70 fn title() -> &'static str {
71 "Album View"
72 }
73
74 fn name() -> &'static str
75 where
76 Self: Sized,
77 {
78 "album"
79 }
80
81 fn none_checked_string() -> &'static str
82 where
83 Self: Sized,
84 {
85 "entire album"
86 }
87
88 fn info_widget(&self) -> impl Widget {
89 let duration = self
90 .album
91 .runtime
92 .normalized()
93 .try_into()
94 .unwrap_or_default();
95
96 Paragraph::new(vec![
97 Line::from(vec![
98 Span::styled(&self.album.title, Style::default().bold()),
99 Span::raw(" "),
100 Span::styled(
101 self.album.artists.as_slice().join(", "),
102 Style::default().italic(),
103 ),
104 ]),
105 Line::from(vec![
106 Span::raw("Release Year: "),
107 Span::styled(
108 self.album
109 .release
110 .map_or_else(|| "unknown".to_string(), |y| y.to_string()),
111 Style::default().italic(),
112 ),
113 Span::raw(" Songs: "),
114 Span::styled(self.album.song_count.to_string(), Style::default().italic()),
115 Span::raw(" Duration: "),
116 Span::styled(format_duration(&duration), Style::default().italic()),
117 ]),
118 ])
119 .alignment(Alignment::Center)
120 }
121
122 fn tree_items(&self) -> Result<Vec<CheckTreeItem<'_, String>>, std::io::Error> {
123 let artist_tree = checktree_utils::create_artist_tree_item(self.artists.as_slice())?;
124 let song_tree = checktree_utils::create_song_tree_item(self.songs.as_ref())?;
125 Ok(vec![artist_tree, song_tree])
126 }
127}
128
129#[derive(Debug, Clone, PartialEq, Eq)]
130pub struct ArtistViewProps {
131 pub id: RecordId,
132 pub artist: Artist,
133 pub albums: Vec<AlbumBrief>,
134 pub songs: Vec<SongBrief>,
135}
136
137impl ItemViewProps for ArtistViewProps {
138 fn id(&self) -> &RecordId {
139 &self.id
140 }
141
142 fn retrieve(view_data: &ViewData) -> Option<Self> {
143 view_data.artist.clone()
144 }
145
146 fn title() -> &'static str {
147 "Artist View"
148 }
149
150 fn name() -> &'static str
151 where
152 Self: Sized,
153 {
154 "artist"
155 }
156
157 fn none_checked_string() -> &'static str
158 where
159 Self: Sized,
160 {
161 "entire artist"
162 }
163
164 fn info_widget(&self) -> impl Widget {
165 let duration = self
166 .artist
167 .runtime
168 .normalized()
169 .try_into()
170 .unwrap_or_default();
171
172 Paragraph::new(vec![
173 Line::from(Span::styled(&self.artist.name, Style::default().bold())),
174 Line::from(vec![
175 Span::raw("Albums: "),
176 Span::styled(
177 self.artist.album_count.to_string(),
178 Style::default().italic(),
179 ),
180 Span::raw(" Songs: "),
181 Span::styled(
182 self.artist.song_count.to_string(),
183 Style::default().italic(),
184 ),
185 Span::raw(" Duration: "),
186 Span::styled(format_duration(&duration), Style::default().italic()),
187 ]),
188 ])
189 .alignment(Alignment::Center)
190 }
191
192 fn tree_items(&self) -> Result<Vec<CheckTreeItem<'_, String>>, std::io::Error> {
193 let album_tree = checktree_utils::create_album_tree_item(self.albums.as_ref())?;
194 let song_tree = checktree_utils::create_song_tree_item(self.songs.as_ref())?;
195 Ok(vec![album_tree, song_tree])
196 }
197}
198
199#[derive(Debug, Clone, PartialEq, Eq)]
200pub struct CollectionViewProps {
201 pub id: RecordId,
202 pub collection: Collection,
203 pub songs: Vec<SongBrief>,
204}
205
206impl ItemViewProps for CollectionViewProps {
207 fn id(&self) -> &RecordId {
208 &self.id
209 }
210
211 fn retrieve(view_data: &ViewData) -> Option<Self> {
212 view_data.collection.clone()
213 }
214
215 fn title() -> &'static str {
216 "Collection View"
217 }
218
219 fn name() -> &'static str
220 where
221 Self: Sized,
222 {
223 "collection"
224 }
225
226 fn none_checked_string() -> &'static str
227 where
228 Self: Sized,
229 {
230 "entire collection"
231 }
232
233 fn info_widget(&self) -> impl Widget {
234 let duration = self
235 .collection
236 .runtime
237 .normalized()
238 .try_into()
239 .unwrap_or_default();
240
241 Paragraph::new(vec![
242 Line::from(Span::styled(&self.collection.name, Style::default().bold())),
243 Line::from(vec![
244 Span::raw("Songs: "),
245 Span::styled(
246 self.collection.song_count.to_string(),
247 Style::default().italic(),
248 ),
249 Span::raw(" Duration: "),
250 Span::styled(format_duration(&duration), Style::default().italic()),
251 ]),
252 ])
253 .alignment(Alignment::Center)
254 }
255
256 fn tree_items(&self) -> Result<Vec<CheckTreeItem<'_, String>>, std::io::Error> {
257 let items = self
258 .songs
259 .iter()
260 .map(checktree_utils::create_song_tree_leaf)
261 .collect::<Vec<_>>();
262 Ok(items)
263 }
264
265 fn extra_footer() -> Option<&'static str>
266 where
267 Self: Sized,
268 {
269 Some("s/S: change sort")
270 }
271
272 fn scrollbar() -> Option<ratatui::widgets::Scrollbar<'static>>
273 where
274 Self: Sized,
275 {
276 Some(Scrollbar::new(ScrollbarOrientation::VerticalRight))
277 }
278}
279
280impl SortableViewProps<SongBrief> for CollectionViewProps {
281 fn sort_items(&mut self, sort_mode: &impl SortMode<SongBrief>) {
282 sort_mode.sort_items(&mut self.songs);
283 }
284}
285
286#[derive(Debug, Clone, PartialEq, Eq)]
287pub struct DynamicPlaylistViewProps {
288 pub id: RecordId,
289 pub dynamic_playlist: DynamicPlaylist,
290 pub songs: Vec<SongBrief>,
291}
292
293impl ItemViewProps for DynamicPlaylistViewProps {
294 fn id(&self) -> &RecordId {
295 &self.id
296 }
297
298 fn retrieve(view_data: &ViewData) -> Option<Self> {
299 view_data.dynamic_playlist.clone()
300 }
301
302 fn title() -> &'static str {
303 "Dynamic Playlist View"
304 }
305
306 fn name() -> &'static str
307 where
308 Self: Sized,
309 {
310 "dynamic playlist"
311 }
312
313 fn none_checked_string() -> &'static str
314 where
315 Self: Sized,
316 {
317 "entire dynamic playlist"
318 }
319
320 fn info_widget(&self) -> impl Widget {
321 Paragraph::new(vec![
322 Line::from(Span::styled(
323 &self.dynamic_playlist.name,
324 Style::default().bold(),
325 )),
326 Line::from(vec![
327 Span::raw("Songs: "),
328 Span::styled(self.songs.len().to_string(), Style::default().italic()),
329 Span::raw(" Duration: "),
330 Span::styled(
331 format_duration(
332 &self
333 .songs
334 .iter()
335 .map(|s| {
336 TryInto::<Duration>::try_into(s.runtime.normalized())
337 .unwrap_or_default()
338 })
339 .sum(),
340 ),
341 Style::default().italic(),
342 ),
343 ]),
344 Line::from(Span::styled(
345 self.dynamic_playlist.query.clone(),
346 Style::default().italic(),
347 )),
348 ])
349 .alignment(Alignment::Center)
350 }
351
352 fn tree_items(&self) -> Result<Vec<CheckTreeItem<'_, String>>, std::io::Error> {
353 let items = self
354 .songs
355 .iter()
356 .map(checktree_utils::create_song_tree_leaf)
357 .collect::<Vec<_>>();
358 Ok(items)
359 }
360
361 fn extra_footer() -> Option<&'static str>
362 where
363 Self: Sized,
364 {
365 Some("s/S: sort | e: edit")
366 }
367
368 fn handle_extra_key_events(
369 &mut self,
370 key: KeyEvent,
371 action_tx: UnboundedSender<Action>,
372 _: &mut CheckTreeState<String>,
373 ) {
374 if matches!(key.code, KeyCode::Char('e')) {
376 action_tx
377 .send(Action::Popup(PopupAction::Open(
378 PopupType::DynamicPlaylistEditor(self.dynamic_playlist.clone()),
379 )))
380 .unwrap();
381 }
382 }
383
384 fn scrollbar() -> Option<Scrollbar<'static>>
385 where
386 Self: Sized,
387 {
388 Some(Scrollbar::new(ScrollbarOrientation::VerticalRight))
389 }
390}
391
392impl SortableViewProps<SongBrief> for DynamicPlaylistViewProps {
393 fn sort_items(&mut self, sort_mode: &impl SortMode<SongBrief>) {
394 sort_mode.sort_items(&mut self.songs);
395 }
396}
397
398#[derive(Debug, Clone, PartialEq, Eq)]
399pub struct PlaylistViewProps {
400 pub id: RecordId,
401 pub playlist: Playlist,
402 pub songs: Vec<SongBrief>,
403}
404
405impl ItemViewProps for PlaylistViewProps {
406 fn id(&self) -> &RecordId {
407 &self.id
408 }
409
410 fn retrieve(view_data: &ViewData) -> Option<Self> {
411 view_data.playlist.clone()
412 }
413
414 fn title() -> &'static str {
415 "Playlist View"
416 }
417
418 fn name() -> &'static str
419 where
420 Self: Sized,
421 {
422 "playlist"
423 }
424
425 fn none_checked_string() -> &'static str
426 where
427 Self: Sized,
428 {
429 "entire playlist"
430 }
431
432 fn info_widget(&self) -> impl Widget {
433 let duration = self
434 .playlist
435 .runtime
436 .normalized()
437 .try_into()
438 .unwrap_or_default();
439
440 Paragraph::new(vec![
441 Line::from(Span::styled(&self.playlist.name, Style::default().bold())),
442 Line::from(vec![
443 Span::raw("Songs: "),
444 Span::styled(
445 self.playlist.song_count.to_string(),
446 Style::default().italic(),
447 ),
448 Span::raw(" Duration: "),
449 Span::styled(format_duration(&duration), Style::default().italic()),
450 ]),
451 ])
452 .alignment(Alignment::Center)
453 }
454
455 fn tree_items(&self) -> Result<Vec<CheckTreeItem<'_, String>>, std::io::Error> {
456 let items = self
457 .songs
458 .iter()
459 .map(checktree_utils::create_song_tree_leaf)
460 .collect::<Vec<_>>();
461 Ok(items)
462 }
463
464 fn extra_footer() -> Option<&'static str>
465 where
466 Self: Sized,
467 {
468 Some("s/S: sort | d: remove selected | e: edit")
469 }
470
471 fn handle_extra_key_events(
472 &mut self,
473 key: KeyEvent,
474 action_tx: UnboundedSender<Action>,
475 tree_state: &mut CheckTreeState<String>,
476 ) {
477 match key.code {
478 KeyCode::Char('d') => {
480 let checked_things = tree_state.get_checked_things();
481 let id = self.id.ulid();
482
483 let to_remove = if checked_things.is_empty() {
484 tree_state.get_selected_thing().map(|thing| vec![thing])
485 } else {
486 Some(checked_things)
487 };
488
489 if let Some(targets) = to_remove {
490 let action = LibraryAction::RemoveSongsFromPlaylist(id, targets);
491 action_tx.send(Action::Library(action)).unwrap();
492 }
493 }
494 KeyCode::Char('e') => {
496 action_tx
497 .send(Action::Popup(PopupAction::Open(PopupType::PlaylistEditor(
498 self.playlist.clone().into(),
499 ))))
500 .unwrap();
501 }
502 _ => {}
503 }
504 }
505
506 fn scrollbar() -> Option<Scrollbar<'static>>
507 where
508 Self: Sized,
509 {
510 Some(Scrollbar::new(ScrollbarOrientation::VerticalRight))
511 }
512}
513
514impl SortableViewProps<SongBrief> for PlaylistViewProps {
515 fn sort_items(&mut self, sort_mode: &impl SortMode<SongBrief>) {
516 sort_mode.sort_items(&mut self.songs);
517 }
518}
519
520#[derive(Debug, Clone, PartialEq, Eq)]
521pub struct SongViewProps {
522 pub id: RecordId,
523 pub song: Song,
524 pub artists: Vec<ArtistBrief>,
525 pub album: AlbumBrief,
526 pub playlists: Vec<Playlist>,
527 pub collections: Vec<Collection>,
528}
529
530impl ItemViewProps for SongViewProps {
531 fn id(&self) -> &RecordId {
532 &self.id
533 }
534
535 fn retrieve(view_data: &ViewData) -> Option<Self> {
536 view_data.song.clone()
537 }
538
539 fn title() -> &'static str {
540 "Song View"
541 }
542
543 fn name() -> &'static str
544 where
545 Self: Sized,
546 {
547 "song"
548 }
549
550 fn none_checked_string() -> &'static str
551 where
552 Self: Sized,
553 {
554 "the song"
555 }
556
557 fn info_widget(&self) -> impl Widget {
558 let runtime: Duration = self
559 .song
560 .runtime
561 .normalized()
562 .try_into()
563 .unwrap_or_default();
564
565 Paragraph::new(vec![
566 Line::from(vec![
567 Span::styled(&self.song.title, Style::default().bold()),
568 Span::raw(" "),
569 Span::styled(
570 self.song.artists.as_slice().join(", "),
571 Style::default().italic(),
572 ),
573 ]),
574 Line::from(vec![
575 Span::raw("Track/Disc: "),
576 Span::styled(
577 format!(
578 "{}/{}",
579 self.song.track.unwrap_or_default(),
580 self.song.disc.unwrap_or_default()
581 ),
582 Style::default().italic(),
583 ),
584 Span::raw(" Duration: "),
585 Span::styled(
586 format!(
587 "{}:{:04.1}",
588 runtime.as_secs() / 60,
589 runtime.as_secs_f32() % 60.0,
590 ),
591 Style::default().italic(),
592 ),
593 Span::raw(" Genre(s): "),
594 Span::styled(
595 self.song.genres.as_slice().join(", "),
596 Style::default().italic(),
597 ),
598 ]),
599 ])
600 .alignment(Alignment::Center)
601 }
602
603 fn tree_items(&self) -> Result<Vec<CheckTreeItem<'_, String>>, std::io::Error> {
604 let artist_tree = checktree_utils::create_artist_tree_item(self.artists.as_slice())?;
605 let album_tree =
606 checktree_utils::create_album_tree_leaf(&self.album, Some(Span::raw("Album: ")));
607 let playlist_tree = checktree_utils::create_playlist_tree_item(&self.playlists)?;
608 let collection_tree = checktree_utils::create_collection_tree_item(&self.collections)?;
609 Ok(vec![
610 artist_tree,
611 album_tree,
612 playlist_tree,
613 collection_tree,
614 ])
615 }
616}
617
618#[derive(Debug, Clone, PartialEq, Eq)]
619pub struct RadioViewProps {
620 pub count: u32,
622 pub songs: Vec<SongBrief>,
624}
625
626#[derive(Debug, Clone, PartialEq, Eq)]
627pub struct RandomViewProps {
628 pub album: RecordId,
630 pub artist: RecordId,
632 pub song: RecordId,
634}
635
636pub mod checktree_utils {
637 use crossterm::event::{KeyModifiers, MouseButton, MouseEvent, MouseEventKind};
638 use mecomp_prost::{
639 AlbumBrief, ArtistBrief, CollectionBrief, DynamicPlaylist, PlaylistBrief, RecordId,
640 SongBrief,
641 };
642 use ratatui::{
643 layout::Position,
644 style::Style,
645 text::{Line, Span, Text},
646 };
647
648 use crate::{
649 state::action::{Action, AudioAction, PopupAction, QueueAction, ViewAction},
650 ui::{
651 components::content_view::ActiveView,
652 widgets::{
653 popups::PopupType,
654 tree::{item::CheckTreeItem, state::CheckTreeState},
655 },
656 },
657 };
658
659 impl CheckTreeState<String> {
660 #[must_use]
662 pub fn get_checked_things(&self) -> Vec<RecordId> {
663 self.checked()
664 .iter()
665 .filter_map(|id| id.iter().find_map(|id| id.parse::<RecordId>().ok()))
666 .collect()
667 }
668
669 #[must_use]
671 pub fn get_selected_thing(&self) -> Option<RecordId> {
672 self.selected()
673 .iter()
674 .find_map(|id| id.parse::<RecordId>().ok())
675 }
676
677 pub fn handle_mouse_event(
693 &mut self,
694 event: MouseEvent,
695 area: ratatui::layout::Rect,
696 swap_ctrl_click_behavior: bool,
697 ) -> Option<Action> {
698 let MouseEvent {
699 kind,
700 column,
701 row,
702 modifiers,
703 } = event;
704 let mouse_position = Position::new(column, row);
705
706 if !area.contains(mouse_position) {
707 return None;
708 }
709
710 match kind {
711 MouseEventKind::Down(MouseButton::Left) => {
712 let click_result = self.mouse_click(mouse_position);
714
715 let condition = modifiers.contains(KeyModifiers::CONTROL) && click_result;
717 let condition = if swap_ctrl_click_behavior {
719 !condition
720 } else {
721 condition
722 };
723
724 if condition {
726 self.get_selected_thing()
727 .map(|thing| Action::ActiveView(ViewAction::Set(thing.into())))
728 } else {
729 None
730 }
731 }
732 MouseEventKind::ScrollDown => {
733 self.key_down();
734 None
735 }
736 MouseEventKind::ScrollUp => {
737 self.key_up();
738 None
739 }
740 _ => None,
741 }
742 }
743 }
744
745 impl<'items> CheckTreeItem<'items, String> {
746 pub fn new_with_items<'a, 'text, Item, LeafFn>(
752 items: &'items [Item],
753 identifier: impl AsRef<str>,
754 text: impl Into<Text<'text>>,
755 leaf_fn: LeafFn,
756 ) -> Result<Self, std::io::Error>
757 where
758 'a: 'text,
759 'a: 'items,
760 'text: 'items,
761 LeafFn: FnMut(&'items Item) -> CheckTreeItem<'a, String>,
762 {
763 let identifier = identifier.as_ref().to_string();
764 let mut tree =
765 CheckTreeItem::new(identifier, text, items.iter().map(leaf_fn).collect())?;
766 if tree.children().is_empty() {
767 tree.add_child(create_dummy_leaf())?;
768 }
769 Ok(tree)
770 }
771 }
772
773 #[must_use]
781 pub fn construct_add_to_playlist_action(
782 checked_things: Vec<RecordId>,
783 current_thing: Option<&RecordId>,
784 ) -> Option<Action> {
785 if checked_things.is_empty() {
786 current_thing
787 .map(|id| Action::Popup(PopupAction::Open(PopupType::Playlist(vec![id.clone()]))))
788 } else {
789 Some(Action::Popup(PopupAction::Open(PopupType::Playlist(
790 checked_things,
791 ))))
792 }
793 }
794
795 #[must_use]
803 pub fn construct_add_to_queue_action(
804 checked_things: Vec<RecordId>,
805 current_thing: Option<&RecordId>,
806 ) -> Option<Action> {
807 if checked_things.is_empty() {
808 current_thing
809 .map(|id| Action::Audio(AudioAction::Queue(QueueAction::Add(vec![id.clone()]))))
810 } else {
811 Some(Action::Audio(AudioAction::Queue(QueueAction::Add(
812 checked_things,
813 ))))
814 }
815 }
816
817 #[must_use]
825 pub fn construct_start_radio_action(
826 checked_things: Vec<RecordId>,
827 current_thing: Option<&RecordId>,
828 ) -> Option<Action> {
829 if checked_things.is_empty() {
830 current_thing
831 .map(|id| Action::ActiveView(ViewAction::Set(ActiveView::Radio(vec![id.clone()]))))
832 } else {
833 Some(Action::ActiveView(ViewAction::Set(ActiveView::Radio(
834 checked_things,
835 ))))
836 }
837 }
838
839 fn create_dummy_leaf() -> CheckTreeItem<'static, String> {
840 CheckTreeItem::new_leaf("dummy".to_string(), "")
841 }
842
843 pub fn create_album_tree_item(
847 albums: &[AlbumBrief],
848 ) -> Result<CheckTreeItem<'_, String>, std::io::Error> {
849 CheckTreeItem::<String>::new_with_items(
850 albums,
851 "Albums",
852 format!("Albums ({}):", albums.len()),
853 |album| create_album_tree_leaf(album, None),
854 )
855 }
856
857 #[must_use]
858 pub fn create_album_tree_leaf<'a>(
859 album: &'a AlbumBrief,
860 prefix: Option<Span<'a>>,
861 ) -> CheckTreeItem<'a, String> {
862 CheckTreeItem::new_leaf(
863 album.id.to_string(),
864 Line::from(vec![
865 prefix.unwrap_or_default(),
866 Span::styled(&album.title, Style::default().bold()),
867 Span::raw(" "),
868 Span::styled(
869 album.artists.as_slice().join(", "),
870 Style::default().italic(),
871 ),
872 ]),
873 )
874 }
875
876 pub fn create_artist_tree_item(
880 artists: &[ArtistBrief],
881 ) -> Result<CheckTreeItem<'_, String>, std::io::Error> {
882 CheckTreeItem::<String>::new_with_items(
883 artists,
884 "Artists",
885 format!("Artists ({}):", artists.len()),
886 create_artist_tree_leaf,
887 )
888 }
889
890 #[must_use]
891 pub fn create_artist_tree_leaf(artist: &ArtistBrief) -> CheckTreeItem<'_, String> {
892 CheckTreeItem::new_leaf(
893 artist.id.to_string(),
894 Line::from(vec![Span::styled(&artist.name, Style::default().bold())]),
895 )
896 }
897
898 pub fn create_collection_tree_item<C: Into<CollectionBrief> + Clone>(
902 collections: &[C],
903 ) -> Result<CheckTreeItem<'_, String>, std::io::Error> {
904 CheckTreeItem::<String>::new_with_items(
905 collections,
906 "Collections",
907 format!("Collections ({}):", collections.len()),
908 create_collection_tree_leaf,
909 )
910 }
911
912 #[must_use]
913 pub fn create_collection_tree_leaf<C: Into<CollectionBrief> + Clone>(
914 collection: &C,
915 ) -> CheckTreeItem<'_, String> {
916 let collection: CollectionBrief = collection.clone().into();
917 CheckTreeItem::new_leaf(
918 collection.id.to_string(),
919 Line::from(vec![Span::styled(collection.name, Style::default().bold())]),
920 )
921 }
922
923 pub fn create_playlist_tree_item<P: Into<PlaylistBrief> + Clone>(
927 playlists: &[P],
928 ) -> Result<CheckTreeItem<'_, String>, std::io::Error> {
929 CheckTreeItem::<String>::new_with_items(
930 playlists,
931 "Playlists",
932 format!("Playlists ({}):", playlists.len()),
933 create_playlist_tree_leaf,
934 )
935 }
936
937 #[must_use]
938 pub fn create_playlist_tree_leaf<P: Into<PlaylistBrief> + Clone>(
939 playlist: &P,
940 ) -> CheckTreeItem<'_, String> {
941 let playlist: PlaylistBrief = playlist.clone().into();
942 CheckTreeItem::new_leaf(
943 playlist.id.to_string(),
944 Line::from(vec![Span::styled(playlist.name, Style::default().bold())]),
945 )
946 }
947
948 pub fn create_dynamic_playlist_tree_item(
952 dynamic_playlists: &[DynamicPlaylist],
953 ) -> Result<CheckTreeItem<'_, String>, std::io::Error> {
954 CheckTreeItem::<String>::new_with_items(
955 dynamic_playlists,
956 "Dynamic Playlists",
957 format!("Dynamic Playlists ({}):", dynamic_playlists.len()),
958 create_dynamic_playlist_tree_leaf,
959 )
960 }
961
962 #[must_use]
963 pub fn create_dynamic_playlist_tree_leaf(
964 dynamic_playlist: &DynamicPlaylist,
965 ) -> CheckTreeItem<'_, String> {
966 CheckTreeItem::new_leaf(
967 dynamic_playlist.id.to_string(),
968 Line::from(vec![Span::styled(
969 &dynamic_playlist.name,
970 Style::default().bold(),
971 )]),
972 )
973 }
974
975 pub fn create_song_tree_item(
979 songs: &[SongBrief],
980 ) -> Result<CheckTreeItem<'_, String>, std::io::Error> {
981 CheckTreeItem::<String>::new_with_items(
982 songs,
983 "Songs",
984 format!("Songs ({}):", songs.len()),
985 create_song_tree_leaf,
986 )
987 }
988
989 #[must_use]
990 pub fn create_song_tree_leaf(song: &SongBrief) -> CheckTreeItem<'_, String> {
991 CheckTreeItem::new_leaf(
992 song.id.to_string(),
993 Line::from(vec![
994 Span::styled(&song.title, Style::default().bold()),
995 Span::raw(" "),
996 Span::styled(
997 song.artists.as_slice().join(", "),
998 Style::default().italic(),
999 ),
1000 ]),
1001 )
1002 }
1003}