1pub mod dynamic;
2use std::time::Duration;
3
4use mecomp_core::format_duration;
5use mecomp_prost::RecordId;
6use mecomp_prost::{
7 Album, AlbumBrief, Artist, ArtistBrief, Collection, DynamicPlaylist, Playlist, Song, SongBrief,
8};
9use ratatui::{
10 layout::Alignment,
11 style::{Style, Stylize},
12 text::{Line, Span},
13 widgets::{Paragraph, Widget},
14};
15use traits::ItemViewProps;
16
17use crate::ui::widgets::tree::item::CheckTreeItem;
18
19pub mod album;
20pub mod artist;
21pub mod collection;
22pub mod generic;
23pub mod none;
24pub mod playlist;
25pub mod radio;
26pub mod random;
27pub mod search;
28pub mod song;
29pub mod sort_mode;
30pub mod traits;
31
32#[allow(clippy::module_name_repetitions)]
34#[derive(Debug, Clone, Default, PartialEq, Eq)]
35pub struct ViewData {
36 pub album: Option<AlbumViewProps>,
37 pub artist: Option<ArtistViewProps>,
38 pub collection: Option<CollectionViewProps>,
39 pub dynamic_playlist: Option<DynamicPlaylistViewProps>,
40 pub playlist: Option<PlaylistViewProps>,
41 pub song: Option<SongViewProps>,
42 pub radio: Option<RadioViewProps>,
43 pub random: Option<RandomViewProps>,
44}
45
46#[derive(Debug, Clone, PartialEq, Eq)]
47pub struct AlbumViewProps {
48 pub id: RecordId,
49 pub album: Album,
50 pub artists: Vec<ArtistBrief>,
51 pub songs: Vec<SongBrief>,
52}
53
54impl ItemViewProps for AlbumViewProps {
55 fn id(&self) -> &RecordId {
56 &self.id
57 }
58
59 fn retrieve(view_data: &ViewData) -> Option<Self> {
60 view_data.album.clone()
61 }
62
63 fn title() -> &'static str {
64 "Album View"
65 }
66
67 fn name() -> &'static str
68 where
69 Self: Sized,
70 {
71 "album"
72 }
73
74 fn none_checked_string() -> &'static str
75 where
76 Self: Sized,
77 {
78 "entire album"
79 }
80
81 fn info_widget(&self) -> impl Widget {
82 let duration = self
83 .album
84 .runtime
85 .normalized()
86 .try_into()
87 .unwrap_or_default();
88
89 Paragraph::new(vec![
90 Line::from(vec![
91 Span::styled(&self.album.title, Style::default().bold()),
92 Span::raw(" "),
93 Span::styled(
94 self.album.artists.as_slice().join(", "),
95 Style::default().italic(),
96 ),
97 ]),
98 Line::from(vec![
99 Span::raw("Release Year: "),
100 Span::styled(
101 self.album
102 .release
103 .map_or_else(|| "unknown".to_string(), |y| y.to_string()),
104 Style::default().italic(),
105 ),
106 Span::raw(" Songs: "),
107 Span::styled(self.album.song_count.to_string(), Style::default().italic()),
108 Span::raw(" Duration: "),
109 Span::styled(format_duration(&duration), Style::default().italic()),
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: Vec<AlbumBrief>,
127 pub songs: Vec<SongBrief>,
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 let duration = self
159 .artist
160 .runtime
161 .normalized()
162 .try_into()
163 .unwrap_or_default();
164
165 Paragraph::new(vec![
166 Line::from(Span::styled(&self.artist.name, Style::default().bold())),
167 Line::from(vec![
168 Span::raw("Albums: "),
169 Span::styled(
170 self.artist.album_count.to_string(),
171 Style::default().italic(),
172 ),
173 Span::raw(" Songs: "),
174 Span::styled(
175 self.artist.song_count.to_string(),
176 Style::default().italic(),
177 ),
178 Span::raw(" Duration: "),
179 Span::styled(format_duration(&duration), Style::default().italic()),
180 ]),
181 ])
182 .alignment(Alignment::Center)
183 }
184
185 fn tree_items(&self) -> Result<Vec<CheckTreeItem<'_, String>>, std::io::Error> {
186 let album_tree = checktree_utils::create_album_tree_item(self.albums.as_ref())?;
187 let song_tree = checktree_utils::create_song_tree_item(self.songs.as_ref())?;
188 Ok(vec![album_tree, song_tree])
189 }
190}
191
192#[derive(Debug, Clone, PartialEq, Eq)]
193pub struct CollectionViewProps {
194 pub id: RecordId,
195 pub collection: Collection,
196 pub songs: Vec<SongBrief>,
197}
198
199#[derive(Debug, Clone, PartialEq, Eq)]
200pub struct DynamicPlaylistViewProps {
201 pub id: RecordId,
202 pub dynamic_playlist: DynamicPlaylist,
203 pub songs: Vec<SongBrief>,
204}
205
206#[derive(Debug, Clone, PartialEq, Eq)]
207pub struct PlaylistViewProps {
208 pub id: RecordId,
209 pub playlist: Playlist,
210 pub songs: Vec<SongBrief>,
211}
212
213#[derive(Debug, Clone, PartialEq, Eq)]
214pub struct SongViewProps {
215 pub id: RecordId,
216 pub song: Song,
217 pub artists: Vec<ArtistBrief>,
218 pub album: AlbumBrief,
219 pub playlists: Vec<Playlist>,
220 pub collections: Vec<Collection>,
221}
222
223impl ItemViewProps for SongViewProps {
224 fn id(&self) -> &RecordId {
225 &self.id
226 }
227
228 fn retrieve(view_data: &ViewData) -> Option<Self> {
229 view_data.song.clone()
230 }
231
232 fn title() -> &'static str {
233 "Song View"
234 }
235
236 fn name() -> &'static str
237 where
238 Self: Sized,
239 {
240 "song"
241 }
242
243 fn none_checked_string() -> &'static str
244 where
245 Self: Sized,
246 {
247 "the song"
248 }
249
250 fn info_widget(&self) -> impl Widget {
251 let runtime: Duration = self
252 .song
253 .runtime
254 .normalized()
255 .try_into()
256 .unwrap_or_default();
257
258 Paragraph::new(vec![
259 Line::from(vec![
260 Span::styled(&self.song.title, Style::default().bold()),
261 Span::raw(" "),
262 Span::styled(
263 self.song.artists.as_slice().join(", "),
264 Style::default().italic(),
265 ),
266 ]),
267 Line::from(vec![
268 Span::raw("Track/Disc: "),
269 Span::styled(
270 format!(
271 "{}/{}",
272 self.song.track.unwrap_or_default(),
273 self.song.disc.unwrap_or_default()
274 ),
275 Style::default().italic(),
276 ),
277 Span::raw(" Duration: "),
278 Span::styled(
279 format!(
280 "{}:{:04.1}",
281 runtime.as_secs() / 60,
282 runtime.as_secs_f32() % 60.0,
283 ),
284 Style::default().italic(),
285 ),
286 Span::raw(" Genre(s): "),
287 Span::styled(
288 self.song.genres.as_slice().join(", "),
289 Style::default().italic(),
290 ),
291 ]),
292 ])
293 .alignment(Alignment::Center)
294 }
295
296 fn tree_items(&self) -> Result<Vec<CheckTreeItem<'_, String>>, std::io::Error> {
297 let artist_tree = checktree_utils::create_artist_tree_item(self.artists.as_slice())?;
298 let album_tree =
299 checktree_utils::create_album_tree_leaf(&self.album, Some(Span::raw("Album: ")));
300 let playlist_tree = checktree_utils::create_playlist_tree_item(&self.playlists)?;
301 let collection_tree = checktree_utils::create_collection_tree_item(&self.collections)?;
302 Ok(vec![
303 artist_tree,
304 album_tree,
305 playlist_tree,
306 collection_tree,
307 ])
308 }
309}
310
311#[derive(Debug, Clone, PartialEq, Eq)]
312pub struct RadioViewProps {
313 pub count: u32,
315 pub songs: Vec<SongBrief>,
317}
318
319#[derive(Debug, Clone, PartialEq, Eq)]
320pub struct RandomViewProps {
321 pub album: RecordId,
323 pub artist: RecordId,
325 pub song: RecordId,
327}
328
329pub mod checktree_utils {
330 use crossterm::event::{KeyModifiers, MouseButton, MouseEvent, MouseEventKind};
331 use mecomp_prost::{
332 AlbumBrief, ArtistBrief, CollectionBrief, DynamicPlaylist, PlaylistBrief, RecordId,
333 SongBrief,
334 };
335 use ratatui::{
336 layout::Position,
337 style::{Style, Stylize},
338 text::{Line, Span, Text},
339 };
340
341 use crate::{
342 state::action::{Action, AudioAction, PopupAction, QueueAction, ViewAction},
343 ui::{
344 components::content_view::ActiveView,
345 widgets::{
346 popups::PopupType,
347 tree::{item::CheckTreeItem, state::CheckTreeState},
348 },
349 },
350 };
351
352 impl CheckTreeState<String> {
353 #[must_use]
355 pub fn get_checked_things(&self) -> Vec<RecordId> {
356 self.checked()
357 .iter()
358 .filter_map(|id| id.iter().find_map(|id| id.parse::<RecordId>().ok()))
359 .collect()
360 }
361
362 #[must_use]
364 pub fn get_selected_thing(&self) -> Option<RecordId> {
365 self.selected()
366 .iter()
367 .find_map(|id| id.parse::<RecordId>().ok())
368 }
369
370 pub fn handle_mouse_event(
386 &mut self,
387 event: MouseEvent,
388 area: ratatui::layout::Rect,
389 swap_ctrl_click_behavior: bool,
390 ) -> Option<Action> {
391 let MouseEvent {
392 kind,
393 column,
394 row,
395 modifiers,
396 } = event;
397 let mouse_position = Position::new(column, row);
398
399 if !area.contains(mouse_position) {
400 return None;
401 }
402
403 match kind {
404 MouseEventKind::Down(MouseButton::Left) => {
405 let click_result = self.mouse_click(mouse_position);
407
408 let condition = modifiers.contains(KeyModifiers::CONTROL) && click_result;
410 let condition = if swap_ctrl_click_behavior {
412 !condition
413 } else {
414 condition
415 };
416
417 if condition {
419 self.get_selected_thing()
420 .map(|thing| Action::ActiveView(ViewAction::Set(thing.into())))
421 } else {
422 None
423 }
424 }
425 MouseEventKind::ScrollDown => {
426 self.key_down();
427 None
428 }
429 MouseEventKind::ScrollUp => {
430 self.key_up();
431 None
432 }
433 _ => None,
434 }
435 }
436 }
437
438 impl<'items> CheckTreeItem<'items, String> {
439 pub fn new_with_items<'a, 'text, Item, LeafFn>(
445 items: &'items [Item],
446 identifier: impl AsRef<str>,
447 text: impl Into<Text<'text>>,
448 leaf_fn: LeafFn,
449 ) -> Result<Self, std::io::Error>
450 where
451 'a: 'text,
452 'a: 'items,
453 'text: 'items,
454 LeafFn: FnMut(&'items Item) -> CheckTreeItem<'a, String>,
455 {
456 let identifier = identifier.as_ref().to_string();
457 let mut tree =
458 CheckTreeItem::new(identifier, text, items.iter().map(leaf_fn).collect())?;
459 if tree.children().is_empty() {
460 tree.add_child(create_dummy_leaf())?;
461 }
462 Ok(tree)
463 }
464 }
465
466 #[must_use]
474 pub fn construct_add_to_playlist_action(
475 checked_things: Vec<RecordId>,
476 current_thing: Option<&RecordId>,
477 ) -> Option<Action> {
478 if checked_things.is_empty() {
479 current_thing
480 .map(|id| Action::Popup(PopupAction::Open(PopupType::Playlist(vec![id.clone()]))))
481 } else {
482 Some(Action::Popup(PopupAction::Open(PopupType::Playlist(
483 checked_things,
484 ))))
485 }
486 }
487
488 #[must_use]
496 pub fn construct_add_to_queue_action(
497 checked_things: Vec<RecordId>,
498 current_thing: Option<&RecordId>,
499 ) -> Option<Action> {
500 if checked_things.is_empty() {
501 current_thing
502 .map(|id| Action::Audio(AudioAction::Queue(QueueAction::Add(vec![id.clone()]))))
503 } else {
504 Some(Action::Audio(AudioAction::Queue(QueueAction::Add(
505 checked_things,
506 ))))
507 }
508 }
509
510 #[must_use]
518 pub fn construct_start_radio_action(
519 checked_things: Vec<RecordId>,
520 current_thing: Option<&RecordId>,
521 ) -> Option<Action> {
522 if checked_things.is_empty() {
523 current_thing
524 .map(|id| Action::ActiveView(ViewAction::Set(ActiveView::Radio(vec![id.clone()]))))
525 } else {
526 Some(Action::ActiveView(ViewAction::Set(ActiveView::Radio(
527 checked_things,
528 ))))
529 }
530 }
531
532 fn create_dummy_leaf() -> CheckTreeItem<'static, String> {
533 CheckTreeItem::new_leaf("dummy".to_string(), "")
534 }
535
536 pub fn create_album_tree_item(
540 albums: &[AlbumBrief],
541 ) -> Result<CheckTreeItem<'_, String>, std::io::Error> {
542 CheckTreeItem::<String>::new_with_items(
543 albums,
544 "Albums",
545 format!("Albums ({}):", albums.len()),
546 |album| create_album_tree_leaf(album, None),
547 )
548 }
549
550 #[must_use]
551 pub fn create_album_tree_leaf<'a>(
552 album: &'a AlbumBrief,
553 prefix: Option<Span<'a>>,
554 ) -> CheckTreeItem<'a, String> {
555 CheckTreeItem::new_leaf(
556 album.id.to_string(),
557 Line::from(vec![
558 prefix.unwrap_or_default(),
559 Span::styled(&album.title, Style::default().bold()),
560 Span::raw(" "),
561 Span::styled(
562 album.artists.as_slice().join(", "),
563 Style::default().italic(),
564 ),
565 ]),
566 )
567 }
568
569 pub fn create_artist_tree_item(
573 artists: &[ArtistBrief],
574 ) -> Result<CheckTreeItem<'_, String>, std::io::Error> {
575 CheckTreeItem::<String>::new_with_items(
576 artists,
577 "Artists",
578 format!("Artists ({}):", artists.len()),
579 create_artist_tree_leaf,
580 )
581 }
582
583 #[must_use]
584 pub fn create_artist_tree_leaf(artist: &ArtistBrief) -> CheckTreeItem<'_, String> {
585 CheckTreeItem::new_leaf(
586 artist.id.to_string(),
587 Line::from(vec![Span::styled(&artist.name, Style::default().bold())]),
588 )
589 }
590
591 pub fn create_collection_tree_item<C: Into<CollectionBrief> + Clone>(
595 collections: &[C],
596 ) -> Result<CheckTreeItem<'_, String>, std::io::Error> {
597 CheckTreeItem::<String>::new_with_items(
598 collections,
599 "Collections",
600 format!("Collections ({}):", collections.len()),
601 create_collection_tree_leaf,
602 )
603 }
604
605 #[must_use]
606 pub fn create_collection_tree_leaf<C: Into<CollectionBrief> + Clone>(
607 collection: &C,
608 ) -> CheckTreeItem<'_, String> {
609 let collection: CollectionBrief = collection.clone().into();
610 CheckTreeItem::new_leaf(
611 collection.id.to_string(),
612 Line::from(vec![Span::styled(collection.name, Style::default().bold())]),
613 )
614 }
615
616 pub fn create_playlist_tree_item<P: Into<PlaylistBrief> + Clone>(
620 playlists: &[P],
621 ) -> Result<CheckTreeItem<'_, String>, std::io::Error> {
622 CheckTreeItem::<String>::new_with_items(
623 playlists,
624 "Playlists",
625 format!("Playlists ({}):", playlists.len()),
626 create_playlist_tree_leaf,
627 )
628 }
629
630 #[must_use]
631 pub fn create_playlist_tree_leaf<P: Into<PlaylistBrief> + Clone>(
632 playlist: &P,
633 ) -> CheckTreeItem<'_, String> {
634 let playlist: PlaylistBrief = playlist.clone().into();
635 CheckTreeItem::new_leaf(
636 playlist.id.to_string(),
637 Line::from(vec![Span::styled(playlist.name, Style::default().bold())]),
638 )
639 }
640
641 pub fn create_dynamic_playlist_tree_item(
645 dynamic_playlists: &[DynamicPlaylist],
646 ) -> Result<CheckTreeItem<'_, String>, std::io::Error> {
647 CheckTreeItem::<String>::new_with_items(
648 dynamic_playlists,
649 "Dynamic Playlists",
650 format!("Dynamic Playlists ({}):", dynamic_playlists.len()),
651 create_dynamic_playlist_tree_leaf,
652 )
653 }
654
655 #[must_use]
656 pub fn create_dynamic_playlist_tree_leaf(
657 dynamic_playlist: &DynamicPlaylist,
658 ) -> CheckTreeItem<'_, String> {
659 CheckTreeItem::new_leaf(
660 dynamic_playlist.id.to_string(),
661 Line::from(vec![Span::styled(
662 &dynamic_playlist.name,
663 Style::default().bold(),
664 )]),
665 )
666 }
667
668 pub fn create_song_tree_item(
672 songs: &[SongBrief],
673 ) -> Result<CheckTreeItem<'_, String>, std::io::Error> {
674 CheckTreeItem::<String>::new_with_items(
675 songs,
676 "Songs",
677 format!("Songs ({}):", songs.len()),
678 create_song_tree_leaf,
679 )
680 }
681
682 #[must_use]
683 pub fn create_song_tree_leaf(song: &SongBrief) -> CheckTreeItem<'_, String> {
684 CheckTreeItem::new_leaf(
685 song.id.to_string(),
686 Line::from(vec![
687 Span::styled(&song.title, Style::default().bold()),
688 Span::raw(" "),
689 Span::styled(
690 song.artists.as_slice().join(", "),
691 Style::default().italic(),
692 ),
693 ]),
694 )
695 }
696}