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