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

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