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#[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 pub count: u32,
319 pub songs: Box<[Song]>,
321}
322
323#[derive(Debug, Clone, PartialEq, Eq)]
324pub struct RandomViewProps {
325 pub album: Thing,
327 pub artist: Thing,
329 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 #[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 #[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 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 (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 #[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 #[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 #[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 #[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 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 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 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 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 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 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}