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

1pub mod dynamic;
2use mecomp_core::format_duration;
3use mecomp_storage::db::schemas::{
4    RecordId, album::Album, artist::Artist, collection::Collection, dynamic::DynamicPlaylist,
5    playlist::Playlist, song::Song,
6};
7use one_or_many::OneOrMany;
8use ratatui::{
9    layout::Alignment,
10    style::{Style, Stylize},
11    text::{Line, Span},
12    widgets::{Paragraph, Widget},
13};
14use traits::ItemViewProps;
15
16use crate::ui::widgets::tree::item::CheckTreeItem;
17
18pub mod album;
19pub mod artist;
20pub mod collection;
21pub mod generic;
22pub mod none;
23pub mod playlist;
24pub mod radio;
25pub mod random;
26pub mod search;
27pub mod song;
28pub mod sort_mode;
29pub mod traits;
30
31/// Data needed by the views (that isn't directly handled by a state store)
32#[allow(clippy::module_name_repetitions)]
33#[derive(Debug, Clone, Default, PartialEq, Eq)]
34pub struct ViewData {
35    pub album: Option<AlbumViewProps>,
36    pub artist: Option<ArtistViewProps>,
37    pub collection: Option<CollectionViewProps>,
38    pub dynamic_playlist: Option<DynamicPlaylistViewProps>,
39    pub playlist: Option<PlaylistViewProps>,
40    pub song: Option<SongViewProps>,
41    pub radio: Option<RadioViewProps>,
42    pub random: Option<RandomViewProps>,
43}
44
45#[derive(Debug, Clone, PartialEq, Eq)]
46pub struct AlbumViewProps {
47    pub id: RecordId,
48    pub album: Album,
49    pub artists: OneOrMany<Artist>,
50    pub songs: Box<[Song]>,
51}
52
53impl ItemViewProps for AlbumViewProps {
54    fn id(&self) -> &RecordId {
55        &self.id
56    }
57
58    fn retrieve(view_data: &ViewData) -> Option<Self> {
59        view_data.album.clone()
60    }
61
62    fn title() -> &'static str {
63        "Album View"
64    }
65
66    fn name() -> &'static str
67    where
68        Self: Sized,
69    {
70        "album"
71    }
72
73    fn none_checked_string() -> &'static str
74    where
75        Self: Sized,
76    {
77        "entire album"
78    }
79
80    fn info_widget(&self) -> impl Widget {
81        Paragraph::new(vec![
82            Line::from(vec![
83                Span::styled(self.album.title.to_string(), Style::default().bold()),
84                Span::raw(" "),
85                Span::styled(
86                    self.album
87                        .artist
88                        .iter()
89                        .map(ToString::to_string)
90                        .collect::<Vec<String>>()
91                        .join(", "),
92                    Style::default().italic(),
93                ),
94            ]),
95            Line::from(vec![
96                Span::raw("Release Year: "),
97                Span::styled(
98                    self.album
99                        .release
100                        .map_or_else(|| "unknown".to_string(), |y| y.to_string()),
101                    Style::default().italic(),
102                ),
103                Span::raw("  Songs: "),
104                Span::styled(self.album.song_count.to_string(), Style::default().italic()),
105                Span::raw("  Duration: "),
106                Span::styled(
107                    format_duration(&self.album.runtime),
108                    Style::default().italic(),
109                ),
110            ]),
111        ])
112        .alignment(Alignment::Center)
113    }
114
115    fn tree_items(&self) -> Result<Vec<CheckTreeItem<String>>, std::io::Error> {
116        let artist_tree = checktree_utils::create_artist_tree_item(self.artists.as_slice())?;
117        let song_tree = checktree_utils::create_song_tree_item(self.songs.as_ref())?;
118        Ok(vec![artist_tree, song_tree])
119    }
120}
121
122#[derive(Debug, Clone, PartialEq, Eq)]
123pub struct ArtistViewProps {
124    pub id: RecordId,
125    pub artist: Artist,
126    pub albums: Box<[Album]>,
127    pub songs: Box<[Song]>,
128}
129
130impl ItemViewProps for ArtistViewProps {
131    fn id(&self) -> &RecordId {
132        &self.id
133    }
134
135    fn retrieve(view_data: &ViewData) -> Option<Self> {
136        view_data.artist.clone()
137    }
138
139    fn title() -> &'static str {
140        "Artist View"
141    }
142
143    fn name() -> &'static str
144    where
145        Self: Sized,
146    {
147        "artist"
148    }
149
150    fn none_checked_string() -> &'static str
151    where
152        Self: Sized,
153    {
154        "entire artist"
155    }
156
157    fn info_widget(&self) -> impl Widget {
158        Paragraph::new(vec![
159            Line::from(Span::styled(
160                self.artist.name.to_string(),
161                Style::default().bold(),
162            )),
163            Line::from(vec![
164                Span::raw("Albums: "),
165                Span::styled(
166                    self.artist.album_count.to_string(),
167                    Style::default().italic(),
168                ),
169                Span::raw("  Songs: "),
170                Span::styled(
171                    self.artist.song_count.to_string(),
172                    Style::default().italic(),
173                ),
174                Span::raw("  Duration: "),
175                Span::styled(
176                    format_duration(&self.artist.runtime),
177                    Style::default().italic(),
178                ),
179            ]),
180        ])
181        .alignment(Alignment::Center)
182    }
183
184    fn tree_items(&self) -> Result<Vec<CheckTreeItem<String>>, std::io::Error> {
185        let album_tree = checktree_utils::create_album_tree_item(self.albums.as_ref())?;
186        let song_tree = checktree_utils::create_song_tree_item(self.songs.as_ref())?;
187        Ok(vec![album_tree, song_tree])
188    }
189}
190
191#[derive(Debug, Clone, PartialEq, Eq)]
192pub struct CollectionViewProps {
193    pub id: RecordId,
194    pub collection: Collection,
195    pub songs: Box<[Song]>,
196}
197
198#[derive(Debug, Clone, PartialEq, Eq)]
199pub struct DynamicPlaylistViewProps {
200    pub id: RecordId,
201    pub dynamic_playlist: DynamicPlaylist,
202    pub songs: Box<[Song]>,
203}
204
205#[derive(Debug, Clone, PartialEq, Eq)]
206pub struct PlaylistViewProps {
207    pub id: RecordId,
208    pub playlist: Playlist,
209    pub songs: Box<[Song]>,
210}
211
212#[derive(Debug, Clone, PartialEq, Eq)]
213pub struct SongViewProps {
214    pub id: RecordId,
215    pub song: Song,
216    pub artists: OneOrMany<Artist>,
217    pub album: Album,
218    pub playlists: Box<[Playlist]>,
219    pub collections: Box<[Collection]>,
220}
221
222impl ItemViewProps for SongViewProps {
223    fn id(&self) -> &RecordId {
224        &self.id
225    }
226
227    fn retrieve(view_data: &ViewData) -> Option<Self> {
228        view_data.song.clone()
229    }
230
231    fn title() -> &'static str {
232        "Song View"
233    }
234
235    fn name() -> &'static str
236    where
237        Self: Sized,
238    {
239        "song"
240    }
241
242    fn none_checked_string() -> &'static str
243    where
244        Self: Sized,
245    {
246        "the song"
247    }
248
249    fn info_widget(&self) -> impl Widget {
250        Paragraph::new(vec![
251            Line::from(vec![
252                Span::styled(self.song.title.to_string(), Style::default().bold()),
253                Span::raw(" "),
254                Span::styled(
255                    self.song
256                        .artist
257                        .iter()
258                        .map(ToString::to_string)
259                        .collect::<Vec<String>>()
260                        .join(", "),
261                    Style::default().italic(),
262                ),
263            ]),
264            Line::from(vec![
265                Span::raw("Track/Disc: "),
266                Span::styled(
267                    format!(
268                        "{}/{}",
269                        self.song.track.unwrap_or_default(),
270                        self.song.disc.unwrap_or_default()
271                    ),
272                    Style::default().italic(),
273                ),
274                Span::raw("  Duration: "),
275                Span::styled(
276                    format!(
277                        "{}:{:04.1}",
278                        self.song.runtime.as_secs() / 60,
279                        self.song.runtime.as_secs_f32() % 60.0,
280                    ),
281                    Style::default().italic(),
282                ),
283                Span::raw("  Genre(s): "),
284                Span::styled(
285                    self.song
286                        .genre
287                        .iter()
288                        .map(ToString::to_string)
289                        .collect::<Vec<String>>()
290                        .join(", "),
291                    Style::default().italic(),
292                ),
293            ]),
294        ])
295        .alignment(Alignment::Center)
296    }
297
298    fn tree_items(&self) -> Result<Vec<CheckTreeItem<String>>, std::io::Error> {
299        let artist_tree = checktree_utils::create_artist_tree_item(self.artists.as_slice())?;
300        let album_tree =
301            checktree_utils::create_album_tree_leaf(&self.album, Some(Span::raw("Album: ")));
302        let playlist_tree = checktree_utils::create_playlist_tree_item(&self.playlists)?;
303        let collection_tree = checktree_utils::create_collection_tree_item(&self.collections)?;
304        Ok(vec![
305            artist_tree,
306            album_tree,
307            playlist_tree,
308            collection_tree,
309        ])
310    }
311}
312
313#[derive(Debug, Clone, PartialEq, Eq)]
314pub struct RadioViewProps {
315    /// The number of similar songs to get
316    pub count: u32,
317    /// The songs that are similar to the things
318    pub songs: Box<[Song]>,
319}
320
321#[derive(Debug, Clone, PartialEq, Eq)]
322pub struct RandomViewProps {
323    /// id of a random album
324    pub album: RecordId,
325    /// id of a random artist
326    pub artist: RecordId,
327    /// id of a random song
328    pub song: RecordId,
329}
330
331pub mod checktree_utils {
332    use crossterm::event::{MouseButton, MouseEvent, MouseEventKind};
333    use mecomp_storage::db::schemas::{
334        RecordId, album::Album, artist::Artist, collection::Collection, dynamic::DynamicPlaylist,
335        playlist::Playlist, song::Song,
336    };
337    use ratatui::{
338        layout::Position,
339        style::{Style, Stylize},
340        text::{Line, Span, Text},
341    };
342
343    use crate::{
344        state::action::{Action, AudioAction, PopupAction, QueueAction, ViewAction},
345        ui::{
346            components::content_view::ActiveView,
347            widgets::{
348                popups::PopupType,
349                tree::{item::CheckTreeItem, state::CheckTreeState},
350            },
351        },
352    };
353
354    impl CheckTreeState<String> {
355        /// Get the checked things from the tree state
356        #[must_use]
357        pub fn get_checked_things(&self) -> Vec<RecordId> {
358            self.checked()
359                .iter()
360                .filter_map(|id| id.iter().find_map(|id| id.parse::<RecordId>().ok()))
361                .collect()
362        }
363
364        /// Get the selected thing from the tree state
365        #[must_use]
366        pub fn get_selected_thing(&self) -> Option<RecordId> {
367            self.selected()
368                .iter()
369                .find_map(|id| id.parse::<RecordId>().ok())
370        }
371
372        /// Handle mouse events interacting with the tree
373        ///
374        /// Assumes that the given area only includes the `CheckTree`
375        ///
376        /// # Returns
377        ///
378        /// an action if the mouse event requires it
379        pub fn handle_mouse_event(
380            &mut self,
381            event: MouseEvent,
382            area: ratatui::layout::Rect,
383        ) -> Option<Action> {
384            let MouseEvent {
385                kind, column, row, ..
386            } = event;
387            let mouse_position = Position::new(column, row);
388
389            if !area.contains(mouse_position) {
390                return None;
391            }
392
393            match kind {
394                MouseEventKind::Down(MouseButton::Left) => {
395                    let selected_things = self.get_selected_thing();
396
397                    // if the selection didn't change, open the selected view
398                    (self.mouse_click(mouse_position)
399                        && selected_things == self.get_selected_thing())
400                    .then_some(selected_things)
401                    .flatten()
402                    .map(|thing| Action::ActiveView(ViewAction::Set(thing.into())))
403                }
404                MouseEventKind::ScrollDown => {
405                    self.key_down();
406                    None
407                }
408                MouseEventKind::ScrollUp => {
409                    self.key_up();
410                    None
411                }
412                _ => None,
413            }
414        }
415    }
416
417    impl<'items> CheckTreeItem<'items, String> {
418        /// Create a `CheckTreeState` from a list of things
419        ///
420        /// # Errors
421        ///
422        /// returns an error if the tree state cannot be created (e.g. duplicate ids)
423        pub fn new_with_items<'a, 'text, Item, LeafFn>(
424            items: &'items [Item],
425            identifier: impl AsRef<str>,
426            text: impl Into<Text<'text>>,
427            leaf_fn: LeafFn,
428        ) -> Result<Self, std::io::Error>
429        where
430            'a: 'text,
431            'a: 'items,
432            'text: 'items,
433            LeafFn: FnMut(&Item) -> CheckTreeItem<'a, String>,
434        {
435            let identifier = identifier.as_ref().to_string();
436            let mut tree =
437                CheckTreeItem::new(identifier, text, items.iter().map(leaf_fn).collect())?;
438            if tree.children().is_empty() {
439                tree.add_child(create_dummy_leaf())?;
440            }
441            Ok(tree)
442        }
443    }
444
445    /// Construct an `Action` to add the checked things to a playlist, if there are any,
446    /// otherwise add the thing being displayed by the view
447    ///
448    /// # Returns
449    ///
450    /// None - if there are no checked things and the current thing is None
451    /// Some(Action) - if there are checked things or the current thing is Some
452    #[must_use]
453    pub fn construct_add_to_playlist_action(
454        checked_things: Vec<RecordId>,
455        current_thing: Option<&RecordId>,
456    ) -> Option<Action> {
457        if checked_things.is_empty() {
458            current_thing
459                .map(|id| Action::Popup(PopupAction::Open(PopupType::Playlist(vec![id.clone()]))))
460        } else {
461            Some(Action::Popup(PopupAction::Open(PopupType::Playlist(
462                checked_things,
463            ))))
464        }
465    }
466
467    /// Construct an `Action` to add the checked things to the queue if there are any,
468    /// otherwise add the thing being displayed by the view
469    ///
470    /// # Returns
471    ///
472    /// None - if there are no checked things and the current thing is None
473    /// Some(Action) - if there are checked things or the current thing is Some
474    #[must_use]
475    pub fn construct_add_to_queue_action(
476        checked_things: Vec<RecordId>,
477        current_thing: Option<&RecordId>,
478    ) -> Option<Action> {
479        if checked_things.is_empty() {
480            current_thing
481                .map(|id| Action::Audio(AudioAction::Queue(QueueAction::Add(vec![id.clone()]))))
482        } else {
483            Some(Action::Audio(AudioAction::Queue(QueueAction::Add(
484                checked_things,
485            ))))
486        }
487    }
488
489    /// Construct an `Action` to start a radio from the checked things if there are any,
490    /// otherwise start a radio from the thing being displayed by the view
491    ///
492    /// # Returns
493    ///
494    /// None - if there are no checked things and the current thing is None
495    /// Some(Action) - if there are checked things or the current thing is Some
496    #[must_use]
497    pub fn construct_start_radio_action(
498        checked_things: Vec<RecordId>,
499        current_thing: Option<&RecordId>,
500    ) -> Option<Action> {
501        if checked_things.is_empty() {
502            current_thing
503                .map(|id| Action::ActiveView(ViewAction::Set(ActiveView::Radio(vec![id.clone()]))))
504        } else {
505            Some(Action::ActiveView(ViewAction::Set(ActiveView::Radio(
506                checked_things,
507            ))))
508        }
509    }
510
511    fn create_dummy_leaf() -> CheckTreeItem<'static, String> {
512        CheckTreeItem::new_leaf("dummy".to_string(), "")
513    }
514
515    /// # Errors
516    ///
517    /// Returns an error if the tree item cannot be created (e.g. duplicate ids)
518    pub fn create_album_tree_item(
519        albums: &[Album],
520    ) -> Result<CheckTreeItem<String>, std::io::Error> {
521        CheckTreeItem::<String>::new_with_items(
522            albums,
523            "Albums",
524            format!("Albums ({}):", albums.len()),
525            |album| create_album_tree_leaf(album, None),
526        )
527    }
528
529    pub fn create_album_tree_leaf<'a>(
530        album: &Album,
531        prefix: Option<Span<'a>>,
532    ) -> CheckTreeItem<'a, String> {
533        CheckTreeItem::new_leaf(
534            album.id.to_string(),
535            Line::from(vec![
536                prefix.unwrap_or_default(),
537                Span::styled(album.title.to_string(), Style::default().bold()),
538                Span::raw(" "),
539                Span::styled(
540                    album
541                        .artist
542                        .iter()
543                        .map(ToString::to_string)
544                        .collect::<Vec<String>>()
545                        .join(", "),
546                    Style::default().italic(),
547                ),
548            ]),
549        )
550    }
551
552    /// # Errors
553    ///
554    /// Returns an error if the tree item cannot be created (e.g. duplicate ids)
555    pub fn create_artist_tree_item(
556        artists: &[Artist],
557    ) -> Result<CheckTreeItem<String>, std::io::Error> {
558        CheckTreeItem::<String>::new_with_items(
559            artists,
560            "Artists",
561            format!("Artists ({}):", artists.len()),
562            create_artist_tree_leaf,
563        )
564    }
565
566    #[must_use]
567    pub fn create_artist_tree_leaf<'a>(artist: &Artist) -> CheckTreeItem<'a, String> {
568        CheckTreeItem::new_leaf(
569            artist.id.to_string(),
570            Line::from(vec![Span::styled(
571                artist.name.to_string(),
572                Style::default().bold(),
573            )]),
574        )
575    }
576
577    /// # Errors
578    ///
579    /// Returns an error if the tree item cannot be created (e.g. duplicate ids)
580    pub fn create_collection_tree_item(
581        collections: &[Collection],
582    ) -> Result<CheckTreeItem<String>, std::io::Error> {
583        CheckTreeItem::<String>::new_with_items(
584            collections,
585            "Collections",
586            format!("Collections ({}):", collections.len()),
587            create_collection_tree_leaf,
588        )
589    }
590
591    #[must_use]
592    pub fn create_collection_tree_leaf<'a>(collection: &Collection) -> CheckTreeItem<'a, String> {
593        CheckTreeItem::new_leaf(
594            collection.id.to_string(),
595            Line::from(vec![Span::styled(
596                collection.name.to_string(),
597                Style::default().bold(),
598            )]),
599        )
600    }
601
602    /// # Errors
603    ///
604    /// Returns an error if the tree item cannot be created (e.g. duplicate ids)
605    pub fn create_playlist_tree_item(
606        playlists: &[Playlist],
607    ) -> Result<CheckTreeItem<String>, std::io::Error> {
608        CheckTreeItem::<String>::new_with_items(
609            playlists,
610            "Playlists",
611            format!("Playlists ({}):", playlists.len()),
612            create_playlist_tree_leaf,
613        )
614    }
615
616    #[must_use]
617    pub fn create_playlist_tree_leaf<'a>(playlist: &Playlist) -> CheckTreeItem<'a, String> {
618        CheckTreeItem::new_leaf(
619            playlist.id.to_string(),
620            Line::from(vec![Span::styled(
621                playlist.name.to_string(),
622                Style::default().bold(),
623            )]),
624        )
625    }
626
627    /// # Errors
628    ///
629    /// Returns an error if the tree item cannot be created (e.g. duplicate ids)
630    pub fn create_dynamic_playlist_tree_item(
631        dynamic_playlists: &[DynamicPlaylist],
632    ) -> Result<CheckTreeItem<String>, std::io::Error> {
633        CheckTreeItem::<String>::new_with_items(
634            dynamic_playlists,
635            "Dynamic Playlists",
636            format!("Dynamic Playlists ({}):", dynamic_playlists.len()),
637            create_dynamic_playlist_tree_leaf,
638        )
639    }
640
641    #[must_use]
642    pub fn create_dynamic_playlist_tree_leaf<'a>(
643        dynamic_playlist: &DynamicPlaylist,
644    ) -> CheckTreeItem<'a, String> {
645        CheckTreeItem::new_leaf(
646            dynamic_playlist.id.to_string(),
647            Line::from(vec![Span::styled(
648                dynamic_playlist.name.to_string(),
649                Style::default().bold(),
650            )]),
651        )
652    }
653
654    /// # Errors
655    ///
656    /// Returns an error if the tree item cannot be created (e.g. duplicate ids)
657    pub fn create_song_tree_item(songs: &[Song]) -> Result<CheckTreeItem<String>, std::io::Error> {
658        CheckTreeItem::<String>::new_with_items(
659            songs,
660            "Songs",
661            format!("Songs ({}):", songs.len()),
662            create_song_tree_leaf,
663        )
664    }
665
666    pub fn create_song_tree_leaf<'a>(song: &Song) -> CheckTreeItem<'a, String> {
667        CheckTreeItem::new_leaf(
668            song.id.to_string(),
669            Line::from(vec![
670                Span::styled(song.title.to_string(), Style::default().bold()),
671                Span::raw(" "),
672                Span::styled(
673                    song.artist
674                        .iter()
675                        .map(ToString::to_string)
676                        .collect::<Vec<String>>()
677                        .join(", "),
678                    Style::default().italic(),
679                ),
680            ]),
681        )
682    }
683}