Skip to main content

mecomp_tui/ui/components/content_view/views/
mod.rs

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/// Data needed by the views (that isn't directly handled by a state store)
40#[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        // "e" key to edit the name/query of the dynamic playlist
375        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            // if there are checked items, remove them from the playlist, otherwise remove the whole playlist
479            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            // edit the playlist name
495            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    /// The number of similar songs to get
621    pub count: u32,
622    /// The songs that are similar to the things
623    pub songs: Vec<SongBrief>,
624}
625
626#[derive(Debug, Clone, PartialEq, Eq)]
627pub struct RandomViewProps {
628    /// id of a random album
629    pub album: RecordId,
630    /// id of a random artist
631    pub artist: RecordId,
632    /// id of a random song
633    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        /// Get the checked things from the tree state
661        #[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        /// Get the selected thing from the tree state
670        #[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        /// Handle mouse events interacting with the tree
678        ///
679        /// Assumes that the given area only includes the `CheckTree`
680        ///
681        /// # Arguments
682        ///
683        /// `event` - the mouse event to handle
684        /// `area` - the area of the tree in the terminal
685        /// `swap_ctrl_click_behavior` - whether to swap the behavior of ctrl+click and click:
686        ///  - if `true`, ctrl+click will toggle the check state of the item, and click will open the item
687        ///  - if `false` (default), ctrl+click will open the item, and click will toggle the check state of the item
688        ///
689        /// # Returns
690        ///
691        /// an action if the mouse event requires it
692        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                    // do a click (toggle check state or open item)
713                    let click_result = self.mouse_click(mouse_position);
714
715                    // if it was a control-click,
716                    let condition = modifiers.contains(KeyModifiers::CONTROL) && click_result;
717                    // and we aren't swapping the behavior,
718                    let condition = if swap_ctrl_click_behavior {
719                        !condition
720                    } else {
721                        condition
722                    };
723
724                    // then attempt to open the selected thing
725                    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        /// Create a `CheckTreeState` from a list of things
747        ///
748        /// # Errors
749        ///
750        /// returns an error if the tree state cannot be created (e.g. duplicate ids)
751        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    /// Construct an `Action` to add the checked things to a playlist, if there are any,
774    /// otherwise add the thing being displayed by the view
775    ///
776    /// # Returns
777    ///
778    /// None - if there are no checked things and the current thing is None
779    /// Some(Action) - if there are checked things or the current thing is Some
780    #[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    /// Construct an `Action` to add the checked things to the queue if there are any,
796    /// otherwise add the thing being displayed by the view
797    ///
798    /// # Returns
799    ///
800    /// None - if there are no checked things and the current thing is None
801    /// Some(Action) - if there are checked things or the current thing is Some
802    #[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    /// Construct an `Action` to start a radio from the checked things if there are any,
818    /// otherwise start a radio from the thing being displayed by the view
819    ///
820    /// # Returns
821    ///
822    /// None - if there are no checked things and the current thing is None
823    /// Some(Action) - if there are checked things or the current thing is Some
824    #[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    /// # Errors
844    ///
845    /// Returns an error if the tree item cannot be created (e.g. duplicate ids)
846    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    /// # Errors
877    ///
878    /// Returns an error if the tree item cannot be created (e.g. duplicate ids)
879    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    /// # Errors
899    ///
900    /// Returns an error if the tree item cannot be created (e.g. duplicate ids)
901    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    /// # Errors
924    ///
925    /// Returns an error if the tree item cannot be created (e.g. duplicate ids)
926    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    /// # Errors
949    ///
950    /// Returns an error if the tree item cannot be created (e.g. duplicate ids)
951    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    /// # Errors
976    ///
977    /// Returns an error if the tree item cannot be created (e.g. duplicate ids)
978    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}