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::{KeyModifiers, 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(
393 &mut self,
394 event: MouseEvent,
395 area: ratatui::layout::Rect,
396 swap_ctrl_click_behavior: bool,
397 ) -> Option<Action> {
398 let MouseEvent {
399 kind,
400 column,
401 row,
402 modifiers,
403 } = event;
404 let mouse_position = Position::new(column, row);
405
406 if !area.contains(mouse_position) {
407 return None;
408 }
409
410 match kind {
411 MouseEventKind::Down(MouseButton::Left) => {
412 let click_result = self.mouse_click(mouse_position);
414
415 let condition = modifiers.contains(KeyModifiers::CONTROL) && click_result;
417 let condition = if swap_ctrl_click_behavior {
419 !condition
420 } else {
421 condition
422 };
423
424 if condition {
426 self.get_selected_thing()
427 .map(|thing| Action::ActiveView(ViewAction::Set(thing.into())))
428 } else {
429 None
430 }
431 }
432 MouseEventKind::ScrollDown => {
433 self.key_down();
434 None
435 }
436 MouseEventKind::ScrollUp => {
437 self.key_up();
438 None
439 }
440 _ => None,
441 }
442 }
443 }
444
445 impl<'items> CheckTreeItem<'items, String> {
446 pub fn new_with_items<'a, 'text, Item, LeafFn>(
452 items: &'items [Item],
453 identifier: impl AsRef<str>,
454 text: impl Into<Text<'text>>,
455 leaf_fn: LeafFn,
456 ) -> Result<Self, std::io::Error>
457 where
458 'a: 'text,
459 'a: 'items,
460 'text: 'items,
461 LeafFn: FnMut(&Item) -> CheckTreeItem<'a, String>,
462 {
463 let identifier = identifier.as_ref().to_string();
464 let mut tree =
465 CheckTreeItem::new(identifier, text, items.iter().map(leaf_fn).collect())?;
466 if tree.children().is_empty() {
467 tree.add_child(create_dummy_leaf())?;
468 }
469 Ok(tree)
470 }
471 }
472
473 #[must_use]
481 pub fn construct_add_to_playlist_action(
482 checked_things: Vec<RecordId>,
483 current_thing: Option<&RecordId>,
484 ) -> Option<Action> {
485 if checked_things.is_empty() {
486 current_thing
487 .map(|id| Action::Popup(PopupAction::Open(PopupType::Playlist(vec![id.clone()]))))
488 } else {
489 Some(Action::Popup(PopupAction::Open(PopupType::Playlist(
490 checked_things,
491 ))))
492 }
493 }
494
495 #[must_use]
503 pub fn construct_add_to_queue_action(
504 checked_things: Vec<RecordId>,
505 current_thing: Option<&RecordId>,
506 ) -> Option<Action> {
507 if checked_things.is_empty() {
508 current_thing
509 .map(|id| Action::Audio(AudioAction::Queue(QueueAction::Add(vec![id.clone()]))))
510 } else {
511 Some(Action::Audio(AudioAction::Queue(QueueAction::Add(
512 checked_things,
513 ))))
514 }
515 }
516
517 #[must_use]
525 pub fn construct_start_radio_action(
526 checked_things: Vec<RecordId>,
527 current_thing: Option<&RecordId>,
528 ) -> Option<Action> {
529 if checked_things.is_empty() {
530 current_thing
531 .map(|id| Action::ActiveView(ViewAction::Set(ActiveView::Radio(vec![id.clone()]))))
532 } else {
533 Some(Action::ActiveView(ViewAction::Set(ActiveView::Radio(
534 checked_things,
535 ))))
536 }
537 }
538
539 fn create_dummy_leaf() -> CheckTreeItem<'static, String> {
540 CheckTreeItem::new_leaf("dummy".to_string(), "")
541 }
542
543 pub fn create_album_tree_item(
547 albums: &[AlbumBrief],
548 ) -> Result<CheckTreeItem<'_, String>, std::io::Error> {
549 CheckTreeItem::<String>::new_with_items(
550 albums,
551 "Albums",
552 format!("Albums ({}):", albums.len()),
553 |album| create_album_tree_leaf(album, None),
554 )
555 }
556
557 pub fn create_album_tree_leaf<'a>(
558 album: &AlbumBrief,
559 prefix: Option<Span<'a>>,
560 ) -> CheckTreeItem<'a, String> {
561 CheckTreeItem::new_leaf(
562 album.id.to_string(),
563 Line::from(vec![
564 prefix.unwrap_or_default(),
565 Span::styled(album.title.to_string(), Style::default().bold()),
566 Span::raw(" "),
567 Span::styled(
568 album
569 .artist
570 .iter()
571 .map(ToString::to_string)
572 .collect::<Vec<String>>()
573 .join(", "),
574 Style::default().italic(),
575 ),
576 ]),
577 )
578 }
579
580 pub fn create_artist_tree_item(
584 artists: &[ArtistBrief],
585 ) -> Result<CheckTreeItem<'_, String>, std::io::Error> {
586 CheckTreeItem::<String>::new_with_items(
587 artists,
588 "Artists",
589 format!("Artists ({}):", artists.len()),
590 create_artist_tree_leaf,
591 )
592 }
593
594 #[must_use]
595 pub fn create_artist_tree_leaf<'a>(artist: &ArtistBrief) -> CheckTreeItem<'a, String> {
596 CheckTreeItem::new_leaf(
597 artist.id.to_string(),
598 Line::from(vec![Span::styled(
599 artist.name.to_string(),
600 Style::default().bold(),
601 )]),
602 )
603 }
604
605 pub fn create_collection_tree_item(
609 collections: &[CollectionBrief],
610 ) -> Result<CheckTreeItem<'_, String>, std::io::Error> {
611 CheckTreeItem::<String>::new_with_items(
612 collections,
613 "Collections",
614 format!("Collections ({}):", collections.len()),
615 create_collection_tree_leaf,
616 )
617 }
618
619 #[must_use]
620 pub fn create_collection_tree_leaf<'a>(
621 collection: &CollectionBrief,
622 ) -> CheckTreeItem<'a, String> {
623 CheckTreeItem::new_leaf(
624 collection.id.to_string(),
625 Line::from(vec![Span::styled(
626 collection.name.to_string(),
627 Style::default().bold(),
628 )]),
629 )
630 }
631
632 pub fn create_playlist_tree_item(
636 playlists: &[PlaylistBrief],
637 ) -> Result<CheckTreeItem<'_, String>, std::io::Error> {
638 CheckTreeItem::<String>::new_with_items(
639 playlists,
640 "Playlists",
641 format!("Playlists ({}):", playlists.len()),
642 create_playlist_tree_leaf,
643 )
644 }
645
646 #[must_use]
647 pub fn create_playlist_tree_leaf<'a>(playlist: &PlaylistBrief) -> CheckTreeItem<'a, String> {
648 CheckTreeItem::new_leaf(
649 playlist.id.to_string(),
650 Line::from(vec![Span::styled(
651 playlist.name.to_string(),
652 Style::default().bold(),
653 )]),
654 )
655 }
656
657 pub fn create_dynamic_playlist_tree_item(
661 dynamic_playlists: &[DynamicPlaylist],
662 ) -> Result<CheckTreeItem<'_, String>, std::io::Error> {
663 CheckTreeItem::<String>::new_with_items(
664 dynamic_playlists,
665 "Dynamic Playlists",
666 format!("Dynamic Playlists ({}):", dynamic_playlists.len()),
667 create_dynamic_playlist_tree_leaf,
668 )
669 }
670
671 #[must_use]
672 pub fn create_dynamic_playlist_tree_leaf<'a>(
673 dynamic_playlist: &DynamicPlaylist,
674 ) -> CheckTreeItem<'a, String> {
675 CheckTreeItem::new_leaf(
676 dynamic_playlist.id.to_string(),
677 Line::from(vec![Span::styled(
678 dynamic_playlist.name.to_string(),
679 Style::default().bold(),
680 )]),
681 )
682 }
683
684 pub fn create_song_tree_item(
688 songs: &[SongBrief],
689 ) -> Result<CheckTreeItem<'_, String>, std::io::Error> {
690 CheckTreeItem::<String>::new_with_items(
691 songs,
692 "Songs",
693 format!("Songs ({}):", songs.len()),
694 create_song_tree_leaf,
695 )
696 }
697
698 pub fn create_song_tree_leaf<'a>(song: &SongBrief) -> CheckTreeItem<'a, String> {
699 CheckTreeItem::new_leaf(
700 song.id.to_string(),
701 Line::from(vec![
702 Span::styled(song.title.to_string(), Style::default().bold()),
703 Span::raw(" "),
704 Span::styled(
705 song.artist
706 .iter()
707 .map(ToString::to_string)
708 .collect::<Vec<String>>()
709 .join(", "),
710 Style::default().italic(),
711 ),
712 ]),
713 )
714 }
715}