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 colors::{border_color, TEXT_HIGHLIGHT},
19 components::{content_view::ActiveView, Component, ComponentRender, RenderProps},
20 widgets::{
21 popups::PopupType,
22 tree::{state::CheckTreeState, CheckTree},
23 },
24 AppState,
25 },
26};
27
28use super::{
29 checktree_utils::create_artist_tree_leaf, generic::ItemView, sort_mode::NameSort,
30 traits::SortMode, ArtistViewProps,
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 }
167 KeyCode::Char('S') => {
168 self.props.sort_mode = self.props.sort_mode.prev();
169 self.props.sort_mode.sort_items(&mut self.props.artists);
170 }
171 _ => {}
172 }
173 }
174
175 fn handle_mouse_event(&mut self, mouse: MouseEvent, area: Rect) {
176 let area = area.inner(Margin::new(1, 2));
178
179 let result = self
180 .tree_state
181 .lock()
182 .unwrap()
183 .handle_mouse_event(mouse, area);
184 if let Some(action) = result {
185 self.action_tx.send(action).unwrap();
186 }
187 }
188}
189
190impl ComponentRender<RenderProps> for LibraryArtistsView {
191 fn render_border(&self, frame: &mut ratatui::Frame, props: RenderProps) -> RenderProps {
192 let border_style = Style::default().fg(border_color(props.is_focused).into());
193
194 let border = Block::bordered()
196 .title_top(Line::from(vec![
197 Span::styled("Library Artists".to_string(), Style::default().bold()),
198 Span::raw(" sorted by: "),
199 Span::styled(self.props.sort_mode.to_string(), Style::default().italic()),
200 ]))
201 .title_bottom(" \u{23CE} : Open | ←/↑/↓/→: Navigate | \u{2423} Check")
202 .border_style(border_style);
203 let content_area = border.inner(props.area);
204 frame.render_widget(border, props.area);
205
206 let border = Block::default()
208 .borders(Borders::TOP | Borders::BOTTOM)
209 .title_top(
210 self.tree_state
211 .lock()
212 .unwrap()
213 .get_checked_things()
214 .is_empty()
215 .not()
216 .then_some("q: add to queue | r: start radio | p: add to playlist ")
217 .unwrap_or_default(),
218 )
219 .title_bottom("s/S: change sort")
220 .border_style(border_style);
221 let area = border.inner(content_area);
222 frame.render_widget(border, content_area);
223
224 RenderProps { area, ..props }
225 }
226
227 fn render_content(&self, frame: &mut ratatui::Frame, props: RenderProps) {
228 let items = self
230 .props
231 .artists
232 .iter()
233 .map(create_artist_tree_leaf)
234 .collect::<Vec<_>>();
235
236 frame.render_stateful_widget(
238 CheckTree::new(&items)
239 .unwrap()
240 .highlight_style(Style::default().fg(TEXT_HIGHLIGHT.into()).bold())
241 .experimental_scrollbar(Some(Scrollbar::new(ScrollbarOrientation::VerticalRight))),
242 props.area,
243 &mut self.tree_state.lock().unwrap(),
244 );
245 }
246}
247
248#[cfg(test)]
249mod sort_mode_tests {
250 use super::*;
251 use pretty_assertions::assert_eq;
252 use rstest::rstest;
253 use std::time::Duration;
254
255 #[rstest]
256 #[case(NameSort::default(), NameSort::default())]
257 fn test_sort_mode_next_prev(
258 #[case] mode: NameSort<Artist>,
259 #[case] expected: NameSort<Artist>,
260 ) {
261 assert_eq!(mode.next(), expected);
262 assert_eq!(mode.next().prev(), mode);
263 }
264
265 #[rstest]
266 #[case(NameSort::default(), "Name")]
267 fn test_sort_mode_display(#[case] mode: NameSort<Artist>, #[case] expected: &str) {
268 assert_eq!(mode.to_string(), expected);
269 }
270
271 #[rstest]
272 fn test_sort_items() {
273 let mut artists = vec![
274 Artist {
275 id: Artist::generate_id(),
276 name: "C".into(),
277 song_count: 1,
278 album_count: 1,
279 runtime: Duration::from_secs(180),
280 },
281 Artist {
282 id: Artist::generate_id(),
283 name: "B".into(),
284 song_count: 1,
285 album_count: 1,
286 runtime: Duration::from_secs(180),
287 },
288 Artist {
289 id: Artist::generate_id(),
290 name: "A".into(),
291 song_count: 1,
292 album_count: 1,
293 runtime: Duration::from_secs(180),
294 },
295 ];
296
297 NameSort::default().sort_items(&mut artists);
298 assert_eq!(artists[0].name, "A");
299 assert_eq!(artists[1].name, "B");
300 assert_eq!(artists[2].name, "C");
301 }
302}
303
304#[cfg(test)]
305mod item_view_tests {
306 use super::*;
307 use crate::test_utils::{
308 assert_buffer_eq, item_id, setup_test_terminal, state_with_everything,
309 };
310 use crossterm::event::{KeyModifiers, MouseButton, MouseEventKind};
311 use pretty_assertions::assert_eq;
312 use ratatui::buffer::Buffer;
313
314 #[test]
315 fn test_new() {
316 let (tx, _) = tokio::sync::mpsc::unbounded_channel();
317 let state = state_with_everything();
318 let view = ArtistView::new(&state, tx);
319
320 assert_eq!(view.name(), "Artist View");
321 assert_eq!(view.props, Some(state.additional_view_data.artist.unwrap()));
322 }
323
324 #[test]
325 fn test_move_with_state() {
326 let (tx, _) = tokio::sync::mpsc::unbounded_channel();
327 let state = AppState::default();
328 let new_state = state_with_everything();
329 let view = ArtistView::new(&state, tx).move_with_state(&new_state);
330
331 assert_eq!(
332 view.props,
333 Some(new_state.additional_view_data.artist.unwrap())
334 );
335 }
336
337 #[test]
338 fn test_render_no_artist() {
339 let (tx, _) = tokio::sync::mpsc::unbounded_channel();
340 let view = ArtistView::new(&AppState::default(), tx);
341
342 let (mut terminal, area) = setup_test_terminal(18, 3);
343 let props = RenderProps {
344 area,
345 is_focused: true,
346 };
347 let buffer = terminal
348 .draw(|frame| view.render(frame, props))
349 .unwrap()
350 .buffer
351 .clone();
352 #[rustfmt::skip]
353 let expected = Buffer::with_lines([
354 "┌Artist View─────┐",
355 "│No active artist│",
356 "└────────────────┘",
357 ]);
358
359 assert_buffer_eq(&buffer, &expected);
360 }
361
362 #[test]
363 fn test_render() {
364 let (tx, _) = tokio::sync::mpsc::unbounded_channel();
365 let view = ArtistView::new(&state_with_everything(), tx);
366
367 let (mut terminal, area) = setup_test_terminal(60, 9);
368 let props = RenderProps {
369 area,
370 is_focused: true,
371 };
372 let buffer = terminal
373 .draw(|frame| view.render(frame, props))
374 .unwrap()
375 .buffer
376 .clone();
377 let expected = Buffer::with_lines([
378 "┌Artist View───────────────────────────────────────────────┐",
379 "│ Test Artist │",
380 "│ Albums: 1 Songs: 1 Duration: 00:03:00.00 │",
381 "│ │",
382 "│q: add to queue | r: start radio | p: add to playlist─────│",
383 "│Performing operations on entire artist────────────────────│",
384 "│▶ Albums (1): │",
385 "│▶ Songs (1): │",
386 "└ ⏎ : Open | ←/↑/↓/→: Navigate | ␣ Check───────────────────┘",
387 ]);
388
389 assert_buffer_eq(&buffer, &expected);
390 }
391
392 #[test]
393 fn test_render_with_checked() {
394 let (tx, _) = tokio::sync::mpsc::unbounded_channel();
395 let mut view = ArtistView::new(&state_with_everything(), tx);
396 let (mut terminal, area) = setup_test_terminal(60, 9);
397 let props = RenderProps {
398 area,
399 is_focused: true,
400 };
401 let buffer = terminal
402 .draw(|frame| view.render(frame, props))
403 .unwrap()
404 .buffer
405 .clone();
406 let expected = Buffer::with_lines([
407 "┌Artist View───────────────────────────────────────────────┐",
408 "│ Test Artist │",
409 "│ Albums: 1 Songs: 1 Duration: 00:03:00.00 │",
410 "│ │",
411 "│q: add to queue | r: start radio | p: add to playlist─────│",
412 "│Performing operations on entire artist────────────────────│",
413 "│▶ Albums (1): │",
414 "│▶ Songs (1): │",
415 "└ ⏎ : Open | ←/↑/↓/→: Navigate | ␣ Check───────────────────┘",
416 ]);
417 assert_buffer_eq(&buffer, &expected);
418
419 view.handle_key_event(KeyEvent::from(KeyCode::Down));
421 view.handle_key_event(KeyEvent::from(KeyCode::Down));
422 view.handle_key_event(KeyEvent::from(KeyCode::Right));
423 let _frame = terminal.draw(|frame| view.render(frame, props)).unwrap();
424 view.handle_key_event(KeyEvent::from(KeyCode::Down));
425 view.handle_key_event(KeyEvent::from(KeyCode::Char(' ')));
426
427 let buffer = terminal
428 .draw(|frame| view.render(frame, props))
429 .unwrap()
430 .buffer
431 .clone();
432 let expected = Buffer::with_lines([
433 "┌Artist View───────────────────────────────────────────────┐",
434 "│ Test Artist │",
435 "│ Albums: 1 Songs: 1 Duration: 00:03:00.00 │",
436 "│ │",
437 "│q: add to queue | r: start radio | p: add to playlist─────│",
438 "│Performing operations on checked items────────────────────│",
439 "│▼ Songs (1): │",
440 "│ ☑ Test Song Test Artist │",
441 "└ ⏎ : Open | ←/↑/↓/→: Navigate | ␣ Check───────────────────┘",
442 ]);
443
444 assert_buffer_eq(&buffer, &expected);
445 }
446
447 #[test]
448 fn smoke_navigation() {
449 let (tx, _) = tokio::sync::mpsc::unbounded_channel();
450 let mut view = ArtistView::new(&state_with_everything(), tx);
451
452 view.handle_key_event(KeyEvent::from(KeyCode::Up));
453 view.handle_key_event(KeyEvent::from(KeyCode::PageUp));
454 view.handle_key_event(KeyEvent::from(KeyCode::Down));
455 view.handle_key_event(KeyEvent::from(KeyCode::PageDown));
456 view.handle_key_event(KeyEvent::from(KeyCode::Left));
457 view.handle_key_event(KeyEvent::from(KeyCode::Right));
458 }
459
460 #[test]
461 fn test_actions() {
462 let (tx, mut rx) = tokio::sync::mpsc::unbounded_channel();
463 let mut view = ArtistView::new(&state_with_everything(), tx);
464
465 let (mut terminal, area) = setup_test_terminal(60, 9);
467 let props = RenderProps {
468 area,
469 is_focused: true,
470 };
471 let _frame = terminal.draw(|frame| view.render(frame, props)).unwrap();
472
473 view.handle_key_event(KeyEvent::from(KeyCode::Char('q')));
476 assert_eq!(
477 rx.blocking_recv().unwrap(),
478 Action::Audio(AudioAction::Queue(QueueAction::Add(vec![(
479 "artist",
480 item_id()
481 )
482 .into()])))
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",
489 item_id()
490 )
491 .into()],)))
492 );
493 view.handle_key_event(KeyEvent::from(KeyCode::Char('p')));
494 assert_eq!(
495 rx.blocking_recv().unwrap(),
496 Action::Popup(PopupAction::Open(PopupType::Playlist(vec![(
497 "artist",
498 item_id()
499 )
500 .into()])))
501 );
502
503 view.handle_key_event(KeyEvent::from(KeyCode::Down));
506 view.handle_key_event(KeyEvent::from(KeyCode::Down));
507 view.handle_key_event(KeyEvent::from(KeyCode::Right));
508 let _frame = terminal.draw(|frame| view.render(frame, props)).unwrap();
509 view.handle_key_event(KeyEvent::from(KeyCode::Down));
510
511 view.handle_key_event(KeyEvent::from(KeyCode::Enter));
513 assert_eq!(
514 rx.blocking_recv().unwrap(),
515 Action::ActiveView(ViewAction::Set(ActiveView::Song(item_id())))
516 );
517
518 view.handle_key_event(KeyEvent::from(KeyCode::Char(' ')));
520
521 view.handle_key_event(KeyEvent::from(KeyCode::Char('q')));
523 assert_eq!(
524 rx.blocking_recv().unwrap(),
525 Action::Audio(AudioAction::Queue(QueueAction::Add(vec![(
526 "song",
527 item_id()
528 )
529 .into()])))
530 );
531
532 view.handle_key_event(KeyEvent::from(KeyCode::Char('r')));
534 assert_eq!(
535 rx.blocking_recv().unwrap(),
536 Action::ActiveView(ViewAction::Set(ActiveView::Radio(vec![(
537 "song",
538 item_id()
539 )
540 .into()],)))
541 );
542
543 view.handle_key_event(KeyEvent::from(KeyCode::Char('p')));
545 assert_eq!(
546 rx.blocking_recv().unwrap(),
547 Action::Popup(PopupAction::Open(PopupType::Playlist(vec![(
548 "song",
549 item_id()
550 )
551 .into()])))
552 );
553 }
554
555 #[test]
556 #[allow(clippy::too_many_lines)]
557 fn test_mouse_event() {
558 let (tx, mut rx) = tokio::sync::mpsc::unbounded_channel();
559 let mut view = ArtistView::new(&state_with_everything(), tx);
560
561 let (mut terminal, area) = setup_test_terminal(60, 9);
563 let props = RenderProps {
564 area,
565 is_focused: true,
566 };
567 let buffer = terminal
568 .draw(|frame| view.render(frame, props))
569 .unwrap()
570 .buffer
571 .clone();
572 let expected = Buffer::with_lines([
573 "┌Artist View───────────────────────────────────────────────┐",
574 "│ Test Artist │",
575 "│ Albums: 1 Songs: 1 Duration: 00:03:00.00 │",
576 "│ │",
577 "│q: add to queue | r: start radio | p: add to playlist─────│",
578 "│Performing operations on entire artist────────────────────│",
579 "│▶ Albums (1): │",
580 "│▶ Songs (1): │",
581 "└ ⏎ : Open | ←/↑/↓/→: Navigate | ␣ Check───────────────────┘",
582 ]);
583 assert_buffer_eq(&buffer, &expected);
584
585 view.handle_mouse_event(
587 MouseEvent {
588 kind: MouseEventKind::Down(MouseButton::Left),
589 column: 2,
590 row: 6,
591 modifiers: KeyModifiers::empty(),
592 },
593 area,
594 );
595 let buffer = terminal
596 .draw(|frame| view.render(frame, props))
597 .unwrap()
598 .buffer
599 .clone();
600 let expected = Buffer::with_lines([
601 "┌Artist View───────────────────────────────────────────────┐",
602 "│ Test Artist │",
603 "│ Albums: 1 Songs: 1 Duration: 00:03:00.00 │",
604 "│ │",
605 "│q: add to queue | r: start radio | p: add to playlist─────│",
606 "│Performing operations on entire artist────────────────────│",
607 "│▼ Albums (1): │",
608 "│ ☐ Test Album Test Artist │",
609 "└ ⏎ : Open | ←/↑/↓/→: Navigate | ␣ Check───────────────────┘",
610 ]);
611 assert_buffer_eq(&buffer, &expected);
612
613 view.handle_mouse_event(
615 MouseEvent {
616 kind: MouseEventKind::ScrollDown,
617 column: 2,
618 row: 6,
619 modifiers: KeyModifiers::empty(),
620 },
621 area,
622 );
623 let buffer = terminal
624 .draw(|frame| view.render(frame, props))
625 .unwrap()
626 .buffer
627 .clone();
628 assert_buffer_eq(&buffer, &expected);
629
630 view.handle_mouse_event(
632 MouseEvent {
633 kind: MouseEventKind::Down(MouseButton::Left),
634 column: 2,
635 row: 7,
636 modifiers: KeyModifiers::empty(),
637 },
638 area,
639 );
640 assert_eq!(
641 rx.blocking_recv().unwrap(),
642 Action::ActiveView(ViewAction::Set(ActiveView::Album(item_id())))
643 );
644 let buffer = terminal
645 .draw(|frame| view.render(frame, props))
646 .unwrap()
647 .buffer
648 .clone();
649 let expected = Buffer::with_lines([
650 "┌Artist View───────────────────────────────────────────────┐",
651 "│ Test Artist │",
652 "│ Albums: 1 Songs: 1 Duration: 00:03:00.00 │",
653 "│ │",
654 "│q: add to queue | r: start radio | p: add to playlist─────│",
655 "│Performing operations on checked items────────────────────│",
656 "│▼ Albums (1): │",
657 "│ ☑ Test Album Test Artist │",
658 "└ ⏎ : Open | ←/↑/↓/→: Navigate | ␣ Check───────────────────┘",
659 ]);
660 assert_buffer_eq(&buffer, &expected);
661
662 view.handle_mouse_event(
664 MouseEvent {
665 kind: MouseEventKind::ScrollUp,
666 column: 2,
667 row: 7,
668 modifiers: KeyModifiers::empty(),
669 },
670 area,
671 );
672 let buffer = terminal
673 .draw(|frame| view.render(frame, props))
674 .unwrap()
675 .buffer
676 .clone();
677 assert_buffer_eq(&buffer, &expected);
678 }
679}
680
681#[cfg(test)]
682mod library_view_tests {
683 use super::*;
684 use crate::test_utils::{
685 assert_buffer_eq, item_id, setup_test_terminal, state_with_everything,
686 };
687 use crossterm::event::{KeyModifiers, MouseButton, MouseEventKind};
688 use pretty_assertions::assert_eq;
689 use ratatui::buffer::Buffer;
690
691 #[test]
692 fn test_new() {
693 let (tx, _) = tokio::sync::mpsc::unbounded_channel();
694 let state = state_with_everything();
695 let view = LibraryArtistsView::new(&state, tx);
696
697 assert_eq!(view.name(), "Library Artists View");
698 assert_eq!(view.props.artists, state.library.artists);
699 }
700
701 #[test]
702 fn test_move_with_state() {
703 let (tx, _) = tokio::sync::mpsc::unbounded_channel();
704 let state = AppState::default();
705 let new_state = state_with_everything();
706 let view = LibraryArtistsView::new(&state, tx).move_with_state(&new_state);
707
708 assert_eq!(view.props.artists, new_state.library.artists);
709 }
710
711 #[test]
712 fn test_render() {
713 let (tx, _) = tokio::sync::mpsc::unbounded_channel();
714 let view = LibraryArtistsView::new(&state_with_everything(), tx);
715
716 let (mut terminal, area) = setup_test_terminal(60, 6);
717 let props = RenderProps {
718 area,
719 is_focused: true,
720 };
721 let buffer = terminal
722 .draw(|frame| view.render(frame, props))
723 .unwrap()
724 .buffer
725 .clone();
726 let expected = Buffer::with_lines([
727 "┌Library Artists sorted by: Name───────────────────────────┐",
728 "│──────────────────────────────────────────────────────────│",
729 "│☐ Test Artist │",
730 "│ │",
731 "│s/S: change sort──────────────────────────────────────────│",
732 "└ ⏎ : Open | ←/↑/↓/→: Navigate | ␣ Check───────────────────┘",
733 ]);
734
735 assert_buffer_eq(&buffer, &expected);
736 }
737
738 #[test]
739 fn test_render_with_checked() {
740 let (tx, _) = tokio::sync::mpsc::unbounded_channel();
741 let mut view = LibraryArtistsView::new(&state_with_everything(), tx);
742 let (mut terminal, area) = setup_test_terminal(60, 6);
743 let props = RenderProps {
744 area,
745 is_focused: true,
746 };
747 let buffer = terminal
748 .draw(|frame| view.render(frame, props))
749 .unwrap()
750 .buffer
751 .clone();
752 let expected = Buffer::with_lines([
753 "┌Library Artists sorted by: Name───────────────────────────┐",
754 "│──────────────────────────────────────────────────────────│",
755 "│☐ Test Artist │",
756 "│ │",
757 "│s/S: change sort──────────────────────────────────────────│",
758 "└ ⏎ : Open | ←/↑/↓/→: Navigate | ␣ Check───────────────────┘",
759 ]);
760 assert_buffer_eq(&buffer, &expected);
761
762 view.handle_key_event(KeyEvent::from(KeyCode::Down));
764 view.handle_key_event(KeyEvent::from(KeyCode::Char(' ')));
765
766 let buffer = terminal
767 .draw(|frame| view.render(frame, props))
768 .unwrap()
769 .buffer
770 .clone();
771 let expected = Buffer::with_lines([
772 "┌Library Artists sorted by: Name───────────────────────────┐",
773 "│q: add to queue | r: start radio | p: add to playlist ────│",
774 "│☑ Test Artist │",
775 "│ │",
776 "│s/S: change sort──────────────────────────────────────────│",
777 "└ ⏎ : Open | ←/↑/↓/→: Navigate | ␣ Check───────────────────┘",
778 ]);
779
780 assert_buffer_eq(&buffer, &expected);
781 }
782
783 #[test]
784 fn test_sort_keys() {
785 let (tx, _) = tokio::sync::mpsc::unbounded_channel();
786 let mut view = LibraryArtistsView::new(&state_with_everything(), tx);
787
788 assert_eq!(view.props.sort_mode, NameSort::default());
789 view.handle_key_event(KeyEvent::from(KeyCode::Char('s')));
790 assert_eq!(view.props.sort_mode, NameSort::default());
791 view.handle_key_event(KeyEvent::from(KeyCode::Char('S')));
792 assert_eq!(view.props.sort_mode, NameSort::default());
793 }
794
795 #[test]
796 fn smoke_navigation() {
797 let (tx, _) = tokio::sync::mpsc::unbounded_channel();
798 let mut view = LibraryArtistsView::new(&state_with_everything(), tx);
799
800 view.handle_key_event(KeyEvent::from(KeyCode::Up));
801 view.handle_key_event(KeyEvent::from(KeyCode::PageUp));
802 view.handle_key_event(KeyEvent::from(KeyCode::Down));
803 view.handle_key_event(KeyEvent::from(KeyCode::PageDown));
804 view.handle_key_event(KeyEvent::from(KeyCode::Left));
805 view.handle_key_event(KeyEvent::from(KeyCode::Right));
806 }
807
808 #[test]
809 fn test_actions() {
810 let (tx, mut rx) = tokio::sync::mpsc::unbounded_channel();
811 let mut view = LibraryArtistsView::new(&state_with_everything(), tx);
812
813 let (mut terminal, area) = setup_test_terminal(60, 9);
815 let props = RenderProps {
816 area,
817 is_focused: true,
818 };
819 terminal.draw(|frame| view.render(frame, props)).unwrap();
820
821 view.handle_key_event(KeyEvent::from(KeyCode::Down));
823
824 view.handle_key_event(KeyEvent::from(KeyCode::Char('p')));
827 view.handle_key_event(KeyEvent::from(KeyCode::Char('r')));
828 view.handle_key_event(KeyEvent::from(KeyCode::Char('q')));
829 view.handle_key_event(KeyEvent::from(KeyCode::Enter));
831 let action = rx.blocking_recv().unwrap();
832 assert_eq!(
833 action,
834 Action::ActiveView(ViewAction::Set(ActiveView::Artist(item_id())))
835 );
836
837 view.handle_key_event(KeyEvent::from(KeyCode::Char(' ')));
839
840 view.handle_key_event(KeyEvent::from(KeyCode::Char('q')));
842 let action = rx.blocking_recv().unwrap();
843 assert_eq!(
844 action,
845 Action::Audio(AudioAction::Queue(QueueAction::Add(vec![(
846 "artist",
847 item_id()
848 )
849 .into()])))
850 );
851
852 view.handle_key_event(KeyEvent::from(KeyCode::Char('r')));
854 let action = rx.blocking_recv().unwrap();
855 assert_eq!(
856 action,
857 Action::ActiveView(ViewAction::Set(ActiveView::Radio(vec![(
858 "artist",
859 item_id()
860 )
861 .into()],)))
862 );
863
864 view.handle_key_event(KeyEvent::from(KeyCode::Char('p')));
866 let action = rx.blocking_recv().unwrap();
867 assert_eq!(
868 action,
869 Action::Popup(PopupAction::Open(PopupType::Playlist(vec![(
870 "artist",
871 item_id()
872 )
873 .into()])))
874 );
875 }
876
877 #[test]
878 fn test_mouse() {
879 let (tx, mut rx) = tokio::sync::mpsc::unbounded_channel();
880 let mut view = LibraryArtistsView::new(&state_with_everything(), tx);
881
882 let (mut terminal, area) = setup_test_terminal(60, 6);
884 let props = RenderProps {
885 area,
886 is_focused: true,
887 };
888 let buffer = terminal
889 .draw(|frame| view.render(frame, props))
890 .unwrap()
891 .buffer
892 .clone();
893 let expected = Buffer::with_lines([
894 "┌Library Artists sorted by: Name───────────────────────────┐",
895 "│──────────────────────────────────────────────────────────│",
896 "│☐ Test Artist │",
897 "│ │",
898 "│s/S: change sort──────────────────────────────────────────│",
899 "└ ⏎ : Open | ←/↑/↓/→: Navigate | ␣ Check───────────────────┘",
900 ]);
901 assert_buffer_eq(&buffer, &expected);
902
903 view.handle_mouse_event(
905 MouseEvent {
906 kind: MouseEventKind::Down(MouseButton::Left),
907 column: 2,
908 row: 2,
909 modifiers: KeyModifiers::empty(),
910 },
911 area,
912 );
913 let buffer = terminal
914 .draw(|frame| view.render(frame, props))
915 .unwrap()
916 .buffer
917 .clone();
918 let expected = Buffer::with_lines([
919 "┌Library Artists sorted by: Name───────────────────────────┐",
920 "│q: add to queue | r: start radio | p: add to playlist ────│",
921 "│☑ Test Artist │",
922 "│ │",
923 "│s/S: change sort──────────────────────────────────────────│",
924 "└ ⏎ : Open | ←/↑/↓/→: Navigate | ␣ Check───────────────────┘",
925 ]);
926 assert_buffer_eq(&buffer, &expected);
927
928 view.handle_mouse_event(
930 MouseEvent {
931 kind: MouseEventKind::ScrollDown,
932 column: 2,
933 row: 2,
934 modifiers: KeyModifiers::empty(),
935 },
936 area,
937 );
938 let buffer = terminal
939 .draw(|frame| view.render(frame, props))
940 .unwrap()
941 .buffer
942 .clone();
943 assert_buffer_eq(&buffer, &expected);
944
945 view.handle_mouse_event(
947 MouseEvent {
948 kind: MouseEventKind::ScrollUp,
949 column: 2,
950 row: 2,
951 modifiers: KeyModifiers::empty(),
952 },
953 area,
954 );
955 let buffer = terminal
956 .draw(|frame| view.render(frame, props))
957 .unwrap()
958 .buffer
959 .clone();
960 assert_buffer_eq(&buffer, &expected);
961
962 view.handle_mouse_event(
964 MouseEvent {
965 kind: MouseEventKind::Down(MouseButton::Left),
966 column: 2,
967 row: 2,
968 modifiers: KeyModifiers::empty(),
969 },
970 area,
971 );
972 assert_eq!(
973 rx.blocking_recv().unwrap(),
974 Action::ActiveView(ViewAction::Set(ActiveView::Artist(item_id())))
975 );
976
977 let mouse = MouseEvent {
979 kind: MouseEventKind::Down(MouseButton::Left),
980 column: 2,
981 row: 3,
982 modifiers: KeyModifiers::empty(),
983 };
984 view.handle_mouse_event(mouse, area);
985 assert_eq!(view.tree_state.lock().unwrap().get_selected_thing(), None);
986 view.handle_mouse_event(mouse, area);
987 assert_eq!(
988 rx.try_recv(),
989 Err(tokio::sync::mpsc::error::TryRecvError::Empty)
990 );
991 }
992}