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

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