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#[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 pub count: u32,
322 pub songs: Box<[SongBrief]>,
324}
325
326#[derive(Debug, Clone, PartialEq, Eq)]
327pub struct RandomViewProps {
328 pub album: RecordId,
330 pub artist: RecordId,
332 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 #[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 #[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 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 (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 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 #[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 #[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 #[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 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 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 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 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 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 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}