1use std::{ops::Not as _, sync::Mutex};
4
5use crossterm::event::{KeyCode, KeyEvent, MouseEvent};
6use mecomp_storage::db::schemas::artist::Artist;
7use ratatui::{
8 layout::{Margin, Rect},
9 style::{Style, Stylize},
10 text::{Line, Span},
11 widgets::{Block, Borders, Scrollbar, ScrollbarOrientation},
12};
13use tokio::sync::mpsc::UnboundedSender;
14
15use crate::{
16 state::action::{Action, AudioAction, PopupAction, QueueAction, ViewAction},
17 ui::{
18 AppState,
19 colors::{TEXT_HIGHLIGHT, border_color},
20 components::{Component, ComponentRender, RenderProps, content_view::ActiveView},
21 widgets::{
22 popups::PopupType,
23 tree::{CheckTree, state::CheckTreeState},
24 },
25 },
26};
27
28use super::{
29 ArtistViewProps, checktree_utils::create_artist_tree_leaf, generic::ItemView,
30 sort_mode::NameSort, traits::SortMode,
31};
32
33#[allow(clippy::module_name_repetitions)]
34pub type ArtistView = ItemView<ArtistViewProps>;
35
36pub struct LibraryArtistsView {
37 pub action_tx: UnboundedSender<Action>,
39 props: Props,
41 tree_state: Mutex<CheckTreeState<String>>,
43}
44
45struct Props {
46 artists: Box<[Artist]>,
47 sort_mode: NameSort<Artist>,
48}
49impl Component for LibraryArtistsView {
50 fn new(state: &AppState, action_tx: UnboundedSender<Action>) -> Self
51 where
52 Self: Sized,
53 {
54 let sort_mode = NameSort::default();
55 let mut artists = state.library.artists.clone();
56 sort_mode.sort_items(&mut artists);
57 Self {
58 action_tx,
59 props: Props { artists, sort_mode },
60 tree_state: Mutex::new(CheckTreeState::default()),
61 }
62 }
63
64 fn move_with_state(self, state: &AppState) -> Self
65 where
66 Self: Sized,
67 {
68 let mut artists = state.library.artists.clone();
69 self.props.sort_mode.sort_items(&mut artists);
70 let tree_state = (state.active_view == ActiveView::Artists)
71 .then_some(self.tree_state)
72 .unwrap_or_default();
73
74 Self {
75 props: Props {
76 artists,
77 ..self.props
78 },
79 tree_state,
80 ..self
81 }
82 }
83
84 fn name(&self) -> &'static str {
85 "Library Artists View"
86 }
87
88 fn handle_key_event(&mut self, key: KeyEvent) {
89 match key.code {
90 KeyCode::PageUp => {
92 self.tree_state.lock().unwrap().select_relative(|current| {
93 current.map_or(self.props.artists.len().saturating_sub(1), |c| {
94 c.saturating_sub(10)
95 })
96 });
97 }
98 KeyCode::Up => {
99 self.tree_state.lock().unwrap().key_up();
100 }
101 KeyCode::PageDown => {
102 self.tree_state
103 .lock()
104 .unwrap()
105 .select_relative(|current| current.map_or(0, |c| c.saturating_add(10)));
106 }
107 KeyCode::Down => {
108 self.tree_state.lock().unwrap().key_down();
109 }
110 KeyCode::Left => {
111 self.tree_state.lock().unwrap().key_left();
112 }
113 KeyCode::Right => {
114 self.tree_state.lock().unwrap().key_right();
115 }
116 KeyCode::Char(' ') => {
117 self.tree_state.lock().unwrap().key_space();
118 }
119 KeyCode::Enter => {
121 if self.tree_state.lock().unwrap().toggle_selected() {
122 let things = self.tree_state.lock().unwrap().get_selected_thing();
123
124 if let Some(thing) = things {
125 self.action_tx
126 .send(Action::ActiveView(ViewAction::Set(thing.into())))
127 .unwrap();
128 }
129 }
130 }
131 KeyCode::Char('q') => {
133 let things = self.tree_state.lock().unwrap().get_checked_things();
134 if !things.is_empty() {
135 self.action_tx
136 .send(Action::Audio(AudioAction::Queue(QueueAction::Add(things))))
137 .unwrap();
138 }
139 }
140 KeyCode::Char('r') => {
142 let things = self.tree_state.lock().unwrap().get_checked_things();
143 if !things.is_empty() {
144 self.action_tx
145 .send(Action::ActiveView(ViewAction::Set(ActiveView::Radio(
146 things,
147 ))))
148 .unwrap();
149 }
150 }
151 KeyCode::Char('p') => {
153 let things = self.tree_state.lock().unwrap().get_checked_things();
154 if !things.is_empty() {
155 self.action_tx
156 .send(Action::Popup(PopupAction::Open(PopupType::Playlist(
157 things,
158 ))))
159 .unwrap();
160 }
161 }
162 KeyCode::Char('s') => {
164 self.props.sort_mode = self.props.sort_mode.next();
165 self.props.sort_mode.sort_items(&mut self.props.artists);
166 self.tree_state.lock().unwrap().scroll_selected_into_view();
167 }
168 KeyCode::Char('S') => {
169 self.props.sort_mode = self.props.sort_mode.prev();
170 self.props.sort_mode.sort_items(&mut self.props.artists);
171 self.tree_state.lock().unwrap().scroll_selected_into_view();
172 }
173 _ => {}
174 }
175 }
176
177 fn handle_mouse_event(&mut self, mouse: MouseEvent, area: Rect) {
178 let area = area.inner(Margin::new(1, 2));
180
181 let result = self
182 .tree_state
183 .lock()
184 .unwrap()
185 .handle_mouse_event(mouse, area);
186 if let Some(action) = result {
187 self.action_tx.send(action).unwrap();
188 }
189 }
190}
191
192impl ComponentRender<RenderProps> for LibraryArtistsView {
193 fn render_border(&self, frame: &mut ratatui::Frame, props: RenderProps) -> RenderProps {
194 let border_style = Style::default().fg(border_color(props.is_focused).into());
195
196 let border = Block::bordered()
198 .title_top(Line::from(vec![
199 Span::styled("Library Artists".to_string(), Style::default().bold()),
200 Span::raw(" sorted by: "),
201 Span::styled(self.props.sort_mode.to_string(), Style::default().italic()),
202 ]))
203 .title_bottom(" \u{23CE} : Open | ←/↑/↓/→: Navigate | \u{2423} Check")
204 .border_style(border_style);
205 let content_area = border.inner(props.area);
206 frame.render_widget(border, props.area);
207
208 let border = Block::default()
210 .borders(Borders::TOP | Borders::BOTTOM)
211 .title_top(
212 self.tree_state
213 .lock()
214 .unwrap()
215 .get_checked_things()
216 .is_empty()
217 .not()
218 .then_some("q: add to queue | r: start radio | p: add to playlist ")
219 .unwrap_or_default(),
220 )
221 .title_bottom("s/S: change sort")
222 .border_style(border_style);
223 let area = border.inner(content_area);
224 frame.render_widget(border, content_area);
225
226 RenderProps { area, ..props }
227 }
228
229 fn render_content(&self, frame: &mut ratatui::Frame, props: RenderProps) {
230 let items = self
232 .props
233 .artists
234 .iter()
235 .map(create_artist_tree_leaf)
236 .collect::<Vec<_>>();
237
238 frame.render_stateful_widget(
240 CheckTree::new(&items)
241 .unwrap()
242 .highlight_style(Style::default().fg(TEXT_HIGHLIGHT.into()).bold())
243 .experimental_scrollbar(Some(Scrollbar::new(ScrollbarOrientation::VerticalRight))),
244 props.area,
245 &mut self.tree_state.lock().unwrap(),
246 );
247 }
248}
249
250#[cfg(test)]
251mod sort_mode_tests {
252 use super::*;
253 use pretty_assertions::assert_eq;
254 use rstest::rstest;
255 use std::time::Duration;
256
257 #[rstest]
258 #[case(NameSort::default(), NameSort::default())]
259 fn test_sort_mode_next_prev(
260 #[case] mode: NameSort<Artist>,
261 #[case] expected: NameSort<Artist>,
262 ) {
263 assert_eq!(mode.next(), expected);
264 assert_eq!(mode.next().prev(), mode);
265 }
266
267 #[rstest]
268 #[case(NameSort::default(), "Name")]
269 fn test_sort_mode_display(#[case] mode: NameSort<Artist>, #[case] expected: &str) {
270 assert_eq!(mode.to_string(), expected);
271 }
272
273 #[rstest]
274 fn test_sort_items() {
275 let mut artists = vec![
276 Artist {
277 id: Artist::generate_id(),
278 name: "C".into(),
279 song_count: 1,
280 album_count: 1,
281 runtime: Duration::from_secs(180),
282 },
283 Artist {
284 id: Artist::generate_id(),
285 name: "B".into(),
286 song_count: 1,
287 album_count: 1,
288 runtime: Duration::from_secs(180),
289 },
290 Artist {
291 id: Artist::generate_id(),
292 name: "A".into(),
293 song_count: 1,
294 album_count: 1,
295 runtime: Duration::from_secs(180),
296 },
297 ];
298
299 NameSort::default().sort_items(&mut artists);
300 assert_eq!(artists[0].name, "A");
301 assert_eq!(artists[1].name, "B");
302 assert_eq!(artists[2].name, "C");
303 }
304}
305
306#[cfg(test)]
307mod item_view_tests {
308 use super::*;
309 use crate::test_utils::{
310 assert_buffer_eq, item_id, setup_test_terminal, state_with_everything,
311 };
312 use crossterm::event::{KeyModifiers, MouseButton, MouseEventKind};
313 use pretty_assertions::assert_eq;
314 use ratatui::buffer::Buffer;
315
316 #[test]
317 fn test_new() {
318 let (tx, _) = tokio::sync::mpsc::unbounded_channel();
319 let state = state_with_everything();
320 let view = ArtistView::new(&state, tx);
321
322 assert_eq!(view.name(), "Artist View");
323 assert_eq!(view.props, Some(state.additional_view_data.artist.unwrap()));
324 }
325
326 #[test]
327 fn test_move_with_state() {
328 let (tx, _) = tokio::sync::mpsc::unbounded_channel();
329 let state = AppState::default();
330 let new_state = state_with_everything();
331 let view = ArtistView::new(&state, tx).move_with_state(&new_state);
332
333 assert_eq!(
334 view.props,
335 Some(new_state.additional_view_data.artist.unwrap())
336 );
337 }
338
339 #[test]
340 fn test_render_no_artist() {
341 let (tx, _) = tokio::sync::mpsc::unbounded_channel();
342 let view = ArtistView::new(&AppState::default(), tx);
343
344 let (mut terminal, area) = setup_test_terminal(18, 3);
345 let props = RenderProps {
346 area,
347 is_focused: true,
348 };
349 let buffer = terminal
350 .draw(|frame| view.render(frame, props))
351 .unwrap()
352 .buffer
353 .clone();
354 #[rustfmt::skip]
355 let expected = Buffer::with_lines([
356 "┌Artist View─────┐",
357 "│No active artist│",
358 "└────────────────┘",
359 ]);
360
361 assert_buffer_eq(&buffer, &expected);
362 }
363
364 #[test]
365 fn test_render() {
366 let (tx, _) = tokio::sync::mpsc::unbounded_channel();
367 let view = ArtistView::new(&state_with_everything(), tx);
368
369 let (mut terminal, area) = setup_test_terminal(60, 9);
370 let props = RenderProps {
371 area,
372 is_focused: true,
373 };
374 let buffer = terminal
375 .draw(|frame| view.render(frame, props))
376 .unwrap()
377 .buffer
378 .clone();
379 let expected = Buffer::with_lines([
380 "┌Artist View───────────────────────────────────────────────┐",
381 "│ Test Artist │",
382 "│ Albums: 1 Songs: 1 Duration: 00:03:00.00 │",
383 "│ │",
384 "│q: add to queue | r: start radio | p: add to playlist─────│",
385 "│Performing operations on entire artist────────────────────│",
386 "│▶ Albums (1): │",
387 "│▶ Songs (1): │",
388 "└ ⏎ : Open | ←/↑/↓/→: Navigate | ␣ Check───────────────────┘",
389 ]);
390
391 assert_buffer_eq(&buffer, &expected);
392 }
393
394 #[test]
395 fn test_render_with_checked() {
396 let (tx, _) = tokio::sync::mpsc::unbounded_channel();
397 let mut view = ArtistView::new(&state_with_everything(), tx);
398 let (mut terminal, area) = setup_test_terminal(60, 9);
399 let props = RenderProps {
400 area,
401 is_focused: true,
402 };
403 let buffer = terminal
404 .draw(|frame| view.render(frame, props))
405 .unwrap()
406 .buffer
407 .clone();
408 let expected = Buffer::with_lines([
409 "┌Artist View───────────────────────────────────────────────┐",
410 "│ Test Artist │",
411 "│ Albums: 1 Songs: 1 Duration: 00:03:00.00 │",
412 "│ │",
413 "│q: add to queue | r: start radio | p: add to playlist─────│",
414 "│Performing operations on entire artist────────────────────│",
415 "│▶ Albums (1): │",
416 "│▶ Songs (1): │",
417 "└ ⏎ : Open | ←/↑/↓/→: Navigate | ␣ Check───────────────────┘",
418 ]);
419 assert_buffer_eq(&buffer, &expected);
420
421 view.handle_key_event(KeyEvent::from(KeyCode::Down));
423 view.handle_key_event(KeyEvent::from(KeyCode::Down));
424 view.handle_key_event(KeyEvent::from(KeyCode::Right));
425 let _frame = terminal.draw(|frame| view.render(frame, props)).unwrap();
426 view.handle_key_event(KeyEvent::from(KeyCode::Down));
427 view.handle_key_event(KeyEvent::from(KeyCode::Char(' ')));
428
429 let buffer = terminal
430 .draw(|frame| view.render(frame, props))
431 .unwrap()
432 .buffer
433 .clone();
434 let expected = Buffer::with_lines([
435 "┌Artist View───────────────────────────────────────────────┐",
436 "│ Test Artist │",
437 "│ Albums: 1 Songs: 1 Duration: 00:03:00.00 │",
438 "│ │",
439 "│q: add to queue | r: start radio | p: add to playlist─────│",
440 "│Performing operations on checked items────────────────────│",
441 "│▼ Songs (1): │",
442 "│ ☑ Test Song Test Artist │",
443 "└ ⏎ : Open | ←/↑/↓/→: Navigate | ␣ Check───────────────────┘",
444 ]);
445
446 assert_buffer_eq(&buffer, &expected);
447 }
448
449 #[test]
450 fn smoke_navigation() {
451 let (tx, _) = tokio::sync::mpsc::unbounded_channel();
452 let mut view = ArtistView::new(&state_with_everything(), tx);
453
454 view.handle_key_event(KeyEvent::from(KeyCode::Up));
455 view.handle_key_event(KeyEvent::from(KeyCode::PageUp));
456 view.handle_key_event(KeyEvent::from(KeyCode::Down));
457 view.handle_key_event(KeyEvent::from(KeyCode::PageDown));
458 view.handle_key_event(KeyEvent::from(KeyCode::Left));
459 view.handle_key_event(KeyEvent::from(KeyCode::Right));
460 }
461
462 #[test]
463 fn test_actions() {
464 let (tx, mut rx) = tokio::sync::mpsc::unbounded_channel();
465 let mut view = ArtistView::new(&state_with_everything(), tx);
466
467 let (mut terminal, area) = setup_test_terminal(60, 9);
469 let props = RenderProps {
470 area,
471 is_focused: true,
472 };
473 let _frame = terminal.draw(|frame| view.render(frame, props)).unwrap();
474
475 view.handle_key_event(KeyEvent::from(KeyCode::Char('q')));
478 assert_eq!(
479 rx.blocking_recv().unwrap(),
480 Action::Audio(AudioAction::Queue(QueueAction::Add(vec![
481 ("artist", item_id()).into()
482 ])))
483 );
484 view.handle_key_event(KeyEvent::from(KeyCode::Char('r')));
485 assert_eq!(
486 rx.blocking_recv().unwrap(),
487 Action::ActiveView(ViewAction::Set(ActiveView::Radio(vec![
488 ("artist", item_id()).into()
489 ],)))
490 );
491 view.handle_key_event(KeyEvent::from(KeyCode::Char('p')));
492 assert_eq!(
493 rx.blocking_recv().unwrap(),
494 Action::Popup(PopupAction::Open(PopupType::Playlist(vec![
495 ("artist", item_id()).into()
496 ])))
497 );
498
499 view.handle_key_event(KeyEvent::from(KeyCode::Down));
502 view.handle_key_event(KeyEvent::from(KeyCode::Down));
503 view.handle_key_event(KeyEvent::from(KeyCode::Right));
504 let _frame = terminal.draw(|frame| view.render(frame, props)).unwrap();
505 view.handle_key_event(KeyEvent::from(KeyCode::Down));
506
507 view.handle_key_event(KeyEvent::from(KeyCode::Enter));
509 assert_eq!(
510 rx.blocking_recv().unwrap(),
511 Action::ActiveView(ViewAction::Set(ActiveView::Song(item_id())))
512 );
513
514 view.handle_key_event(KeyEvent::from(KeyCode::Char(' ')));
516
517 view.handle_key_event(KeyEvent::from(KeyCode::Char('q')));
519 assert_eq!(
520 rx.blocking_recv().unwrap(),
521 Action::Audio(AudioAction::Queue(QueueAction::Add(vec![
522 ("song", item_id()).into()
523 ])))
524 );
525
526 view.handle_key_event(KeyEvent::from(KeyCode::Char('r')));
528 assert_eq!(
529 rx.blocking_recv().unwrap(),
530 Action::ActiveView(ViewAction::Set(ActiveView::Radio(vec![
531 ("song", item_id()).into()
532 ],)))
533 );
534
535 view.handle_key_event(KeyEvent::from(KeyCode::Char('p')));
537 assert_eq!(
538 rx.blocking_recv().unwrap(),
539 Action::Popup(PopupAction::Open(PopupType::Playlist(vec![
540 ("song", item_id()).into()
541 ])))
542 );
543 }
544
545 #[test]
546 #[allow(clippy::too_many_lines)]
547 fn test_mouse_event() {
548 let (tx, mut rx) = tokio::sync::mpsc::unbounded_channel();
549 let mut view = ArtistView::new(&state_with_everything(), tx);
550
551 let (mut terminal, area) = setup_test_terminal(60, 9);
553 let props = RenderProps {
554 area,
555 is_focused: true,
556 };
557 let buffer = terminal
558 .draw(|frame| view.render(frame, props))
559 .unwrap()
560 .buffer
561 .clone();
562 let expected = Buffer::with_lines([
563 "┌Artist View───────────────────────────────────────────────┐",
564 "│ Test Artist │",
565 "│ Albums: 1 Songs: 1 Duration: 00:03:00.00 │",
566 "│ │",
567 "│q: add to queue | r: start radio | p: add to playlist─────│",
568 "│Performing operations on entire artist────────────────────│",
569 "│▶ Albums (1): │",
570 "│▶ Songs (1): │",
571 "└ ⏎ : Open | ←/↑/↓/→: Navigate | ␣ Check───────────────────┘",
572 ]);
573 assert_buffer_eq(&buffer, &expected);
574
575 view.handle_mouse_event(
577 MouseEvent {
578 kind: MouseEventKind::Down(MouseButton::Left),
579 column: 2,
580 row: 6,
581 modifiers: KeyModifiers::empty(),
582 },
583 area,
584 );
585 let buffer = terminal
586 .draw(|frame| view.render(frame, props))
587 .unwrap()
588 .buffer
589 .clone();
590 let expected = Buffer::with_lines([
591 "┌Artist View───────────────────────────────────────────────┐",
592 "│ Test Artist │",
593 "│ Albums: 1 Songs: 1 Duration: 00:03:00.00 │",
594 "│ │",
595 "│q: add to queue | r: start radio | p: add to playlist─────│",
596 "│Performing operations on entire artist────────────────────│",
597 "│▼ Albums (1): │",
598 "│ ☐ Test Album Test Artist │",
599 "└ ⏎ : Open | ←/↑/↓/→: Navigate | ␣ Check───────────────────┘",
600 ]);
601 assert_buffer_eq(&buffer, &expected);
602
603 view.handle_mouse_event(
605 MouseEvent {
606 kind: MouseEventKind::ScrollDown,
607 column: 2,
608 row: 6,
609 modifiers: KeyModifiers::empty(),
610 },
611 area,
612 );
613 let buffer = terminal
614 .draw(|frame| view.render(frame, props))
615 .unwrap()
616 .buffer
617 .clone();
618 assert_buffer_eq(&buffer, &expected);
619
620 view.handle_mouse_event(
622 MouseEvent {
623 kind: MouseEventKind::Down(MouseButton::Left),
624 column: 2,
625 row: 7,
626 modifiers: KeyModifiers::empty(),
627 },
628 area,
629 );
630 assert_eq!(
631 rx.blocking_recv().unwrap(),
632 Action::ActiveView(ViewAction::Set(ActiveView::Album(item_id())))
633 );
634 let buffer = terminal
635 .draw(|frame| view.render(frame, props))
636 .unwrap()
637 .buffer
638 .clone();
639 let expected = Buffer::with_lines([
640 "┌Artist View───────────────────────────────────────────────┐",
641 "│ Test Artist │",
642 "│ Albums: 1 Songs: 1 Duration: 00:03:00.00 │",
643 "│ │",
644 "│q: add to queue | r: start radio | p: add to playlist─────│",
645 "│Performing operations on checked items────────────────────│",
646 "│▼ Albums (1): │",
647 "│ ☑ Test Album Test Artist │",
648 "└ ⏎ : Open | ←/↑/↓/→: Navigate | ␣ Check───────────────────┘",
649 ]);
650 assert_buffer_eq(&buffer, &expected);
651
652 view.handle_mouse_event(
654 MouseEvent {
655 kind: MouseEventKind::ScrollUp,
656 column: 2,
657 row: 7,
658 modifiers: KeyModifiers::empty(),
659 },
660 area,
661 );
662 let buffer = terminal
663 .draw(|frame| view.render(frame, props))
664 .unwrap()
665 .buffer
666 .clone();
667 assert_buffer_eq(&buffer, &expected);
668 }
669}
670
671#[cfg(test)]
672mod library_view_tests {
673 use super::*;
674 use crate::test_utils::{
675 assert_buffer_eq, item_id, setup_test_terminal, state_with_everything,
676 };
677 use crossterm::event::{KeyModifiers, MouseButton, MouseEventKind};
678 use pretty_assertions::assert_eq;
679 use ratatui::buffer::Buffer;
680
681 #[test]
682 fn test_new() {
683 let (tx, _) = tokio::sync::mpsc::unbounded_channel();
684 let state = state_with_everything();
685 let view = LibraryArtistsView::new(&state, tx);
686
687 assert_eq!(view.name(), "Library Artists View");
688 assert_eq!(view.props.artists, state.library.artists);
689 }
690
691 #[test]
692 fn test_move_with_state() {
693 let (tx, _) = tokio::sync::mpsc::unbounded_channel();
694 let state = AppState::default();
695 let new_state = state_with_everything();
696 let view = LibraryArtistsView::new(&state, tx).move_with_state(&new_state);
697
698 assert_eq!(view.props.artists, new_state.library.artists);
699 }
700
701 #[test]
702 fn test_render() {
703 let (tx, _) = tokio::sync::mpsc::unbounded_channel();
704 let view = LibraryArtistsView::new(&state_with_everything(), tx);
705
706 let (mut terminal, area) = setup_test_terminal(60, 6);
707 let props = RenderProps {
708 area,
709 is_focused: true,
710 };
711 let buffer = terminal
712 .draw(|frame| view.render(frame, props))
713 .unwrap()
714 .buffer
715 .clone();
716 let expected = Buffer::with_lines([
717 "┌Library Artists sorted by: Name───────────────────────────┐",
718 "│──────────────────────────────────────────────────────────│",
719 "│☐ Test Artist │",
720 "│ │",
721 "│s/S: change sort──────────────────────────────────────────│",
722 "└ ⏎ : Open | ←/↑/↓/→: Navigate | ␣ Check───────────────────┘",
723 ]);
724
725 assert_buffer_eq(&buffer, &expected);
726 }
727
728 #[test]
729 fn test_render_with_checked() {
730 let (tx, _) = tokio::sync::mpsc::unbounded_channel();
731 let mut view = LibraryArtistsView::new(&state_with_everything(), tx);
732 let (mut terminal, area) = setup_test_terminal(60, 6);
733 let props = RenderProps {
734 area,
735 is_focused: true,
736 };
737 let buffer = terminal
738 .draw(|frame| view.render(frame, props))
739 .unwrap()
740 .buffer
741 .clone();
742 let expected = Buffer::with_lines([
743 "┌Library Artists sorted by: Name───────────────────────────┐",
744 "│──────────────────────────────────────────────────────────│",
745 "│☐ Test Artist │",
746 "│ │",
747 "│s/S: change sort──────────────────────────────────────────│",
748 "└ ⏎ : Open | ←/↑/↓/→: Navigate | ␣ Check───────────────────┘",
749 ]);
750 assert_buffer_eq(&buffer, &expected);
751
752 view.handle_key_event(KeyEvent::from(KeyCode::Down));
754 view.handle_key_event(KeyEvent::from(KeyCode::Char(' ')));
755
756 let buffer = terminal
757 .draw(|frame| view.render(frame, props))
758 .unwrap()
759 .buffer
760 .clone();
761 let expected = Buffer::with_lines([
762 "┌Library Artists sorted by: Name───────────────────────────┐",
763 "│q: add to queue | r: start radio | p: add to playlist ────│",
764 "│☑ Test Artist │",
765 "│ │",
766 "│s/S: change sort──────────────────────────────────────────│",
767 "└ ⏎ : Open | ←/↑/↓/→: Navigate | ␣ Check───────────────────┘",
768 ]);
769
770 assert_buffer_eq(&buffer, &expected);
771 }
772
773 #[test]
774 fn test_sort_keys() {
775 let (tx, _) = tokio::sync::mpsc::unbounded_channel();
776 let mut view = LibraryArtistsView::new(&state_with_everything(), tx);
777
778 assert_eq!(view.props.sort_mode, NameSort::default());
779 view.handle_key_event(KeyEvent::from(KeyCode::Char('s')));
780 assert_eq!(view.props.sort_mode, NameSort::default());
781 view.handle_key_event(KeyEvent::from(KeyCode::Char('S')));
782 assert_eq!(view.props.sort_mode, NameSort::default());
783 }
784
785 #[test]
786 fn smoke_navigation() {
787 let (tx, _) = tokio::sync::mpsc::unbounded_channel();
788 let mut view = LibraryArtistsView::new(&state_with_everything(), tx);
789
790 view.handle_key_event(KeyEvent::from(KeyCode::Up));
791 view.handle_key_event(KeyEvent::from(KeyCode::PageUp));
792 view.handle_key_event(KeyEvent::from(KeyCode::Down));
793 view.handle_key_event(KeyEvent::from(KeyCode::PageDown));
794 view.handle_key_event(KeyEvent::from(KeyCode::Left));
795 view.handle_key_event(KeyEvent::from(KeyCode::Right));
796 }
797
798 #[test]
799 fn test_actions() {
800 let (tx, mut rx) = tokio::sync::mpsc::unbounded_channel();
801 let mut view = LibraryArtistsView::new(&state_with_everything(), tx);
802
803 let (mut terminal, area) = setup_test_terminal(60, 9);
805 let props = RenderProps {
806 area,
807 is_focused: true,
808 };
809 terminal.draw(|frame| view.render(frame, props)).unwrap();
810
811 view.handle_key_event(KeyEvent::from(KeyCode::Down));
813
814 view.handle_key_event(KeyEvent::from(KeyCode::Char('p')));
817 view.handle_key_event(KeyEvent::from(KeyCode::Char('r')));
818 view.handle_key_event(KeyEvent::from(KeyCode::Char('q')));
819 view.handle_key_event(KeyEvent::from(KeyCode::Enter));
821 let action = rx.blocking_recv().unwrap();
822 assert_eq!(
823 action,
824 Action::ActiveView(ViewAction::Set(ActiveView::Artist(item_id())))
825 );
826
827 view.handle_key_event(KeyEvent::from(KeyCode::Char(' ')));
829
830 view.handle_key_event(KeyEvent::from(KeyCode::Char('q')));
832 let action = rx.blocking_recv().unwrap();
833 assert_eq!(
834 action,
835 Action::Audio(AudioAction::Queue(QueueAction::Add(vec![
836 ("artist", item_id()).into()
837 ])))
838 );
839
840 view.handle_key_event(KeyEvent::from(KeyCode::Char('r')));
842 let action = rx.blocking_recv().unwrap();
843 assert_eq!(
844 action,
845 Action::ActiveView(ViewAction::Set(ActiveView::Radio(vec![
846 ("artist", item_id()).into()
847 ],)))
848 );
849
850 view.handle_key_event(KeyEvent::from(KeyCode::Char('p')));
852 let action = rx.blocking_recv().unwrap();
853 assert_eq!(
854 action,
855 Action::Popup(PopupAction::Open(PopupType::Playlist(vec![
856 ("artist", item_id()).into()
857 ])))
858 );
859 }
860
861 #[test]
862 fn test_mouse() {
863 let (tx, mut rx) = tokio::sync::mpsc::unbounded_channel();
864 let mut view = LibraryArtistsView::new(&state_with_everything(), tx);
865
866 let (mut terminal, area) = setup_test_terminal(60, 6);
868 let props = RenderProps {
869 area,
870 is_focused: true,
871 };
872 let buffer = terminal
873 .draw(|frame| view.render(frame, props))
874 .unwrap()
875 .buffer
876 .clone();
877 let expected = Buffer::with_lines([
878 "┌Library Artists sorted by: Name───────────────────────────┐",
879 "│──────────────────────────────────────────────────────────│",
880 "│☐ Test Artist │",
881 "│ │",
882 "│s/S: change sort──────────────────────────────────────────│",
883 "└ ⏎ : Open | ←/↑/↓/→: Navigate | ␣ Check───────────────────┘",
884 ]);
885 assert_buffer_eq(&buffer, &expected);
886
887 view.handle_mouse_event(
889 MouseEvent {
890 kind: MouseEventKind::Down(MouseButton::Left),
891 column: 2,
892 row: 2,
893 modifiers: KeyModifiers::empty(),
894 },
895 area,
896 );
897 let buffer = terminal
898 .draw(|frame| view.render(frame, props))
899 .unwrap()
900 .buffer
901 .clone();
902 let expected = Buffer::with_lines([
903 "┌Library Artists sorted by: Name───────────────────────────┐",
904 "│q: add to queue | r: start radio | p: add to playlist ────│",
905 "│☑ Test Artist │",
906 "│ │",
907 "│s/S: change sort──────────────────────────────────────────│",
908 "└ ⏎ : Open | ←/↑/↓/→: Navigate | ␣ Check───────────────────┘",
909 ]);
910 assert_buffer_eq(&buffer, &expected);
911
912 view.handle_mouse_event(
914 MouseEvent {
915 kind: MouseEventKind::ScrollDown,
916 column: 2,
917 row: 2,
918 modifiers: KeyModifiers::empty(),
919 },
920 area,
921 );
922 let buffer = terminal
923 .draw(|frame| view.render(frame, props))
924 .unwrap()
925 .buffer
926 .clone();
927 assert_buffer_eq(&buffer, &expected);
928
929 view.handle_mouse_event(
931 MouseEvent {
932 kind: MouseEventKind::ScrollUp,
933 column: 2,
934 row: 2,
935 modifiers: KeyModifiers::empty(),
936 },
937 area,
938 );
939 let buffer = terminal
940 .draw(|frame| view.render(frame, props))
941 .unwrap()
942 .buffer
943 .clone();
944 assert_buffer_eq(&buffer, &expected);
945
946 view.handle_mouse_event(
948 MouseEvent {
949 kind: MouseEventKind::Down(MouseButton::Left),
950 column: 2,
951 row: 2,
952 modifiers: KeyModifiers::empty(),
953 },
954 area,
955 );
956 assert_eq!(
957 rx.blocking_recv().unwrap(),
958 Action::ActiveView(ViewAction::Set(ActiveView::Artist(item_id())))
959 );
960
961 let mouse = MouseEvent {
963 kind: MouseEventKind::Down(MouseButton::Left),
964 column: 2,
965 row: 3,
966 modifiers: KeyModifiers::empty(),
967 };
968 view.handle_mouse_event(mouse, area);
969 assert_eq!(view.tree_state.lock().unwrap().get_selected_thing(), None);
970 view.handle_mouse_event(mouse, area);
971 assert_eq!(
972 rx.try_recv(),
973 Err(tokio::sync::mpsc::error::TryRecvError::Empty)
974 );
975 }
976}