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 anyhow::Result;
311 use crossterm::event::{KeyModifiers, MouseButton, MouseEventKind};
312 use pretty_assertions::assert_eq;
313 use ratatui::buffer::Buffer;
314
315 #[test]
316 fn test_new() {
317 let (tx, _) = tokio::sync::mpsc::unbounded_channel();
318 let state = state_with_everything();
319 let view = ArtistView::new(&state, tx);
320
321 assert_eq!(view.name(), "Artist View");
322 assert_eq!(view.props, Some(state.additional_view_data.artist.unwrap()));
323 }
324
325 #[test]
326 fn test_move_with_state() {
327 let (tx, _) = tokio::sync::mpsc::unbounded_channel();
328 let state = AppState::default();
329 let new_state = state_with_everything();
330 let view = ArtistView::new(&state, tx).move_with_state(&new_state);
331
332 assert_eq!(
333 view.props,
334 Some(new_state.additional_view_data.artist.unwrap())
335 );
336 }
337
338 #[test]
339 fn test_render_no_artist() -> Result<()> {
340 let (tx, _) = tokio::sync::mpsc::unbounded_channel();
341 let view = ArtistView::new(&AppState::default(), tx);
342
343 let (mut terminal, area) = setup_test_terminal(18, 3);
344 let props = RenderProps {
345 area,
346 is_focused: true,
347 };
348 let buffer = terminal
349 .draw(|frame| view.render(frame, props))
350 .unwrap()
351 .buffer
352 .clone();
353 #[rustfmt::skip]
354 let expected = Buffer::with_lines([
355 "┌Artist View─────┐",
356 "│No active artist│",
357 "└────────────────┘",
358 ]);
359
360 assert_buffer_eq(&buffer, &expected);
361
362 Ok(())
363 }
364
365 #[test]
366 fn test_render() -> Result<()> {
367 let (tx, _) = tokio::sync::mpsc::unbounded_channel();
368 let view = ArtistView::new(&state_with_everything(), tx);
369
370 let (mut terminal, area) = setup_test_terminal(60, 9);
371 let props = RenderProps {
372 area,
373 is_focused: true,
374 };
375 let buffer = terminal
376 .draw(|frame| view.render(frame, props))
377 .unwrap()
378 .buffer
379 .clone();
380 let expected = Buffer::with_lines([
381 "┌Artist View───────────────────────────────────────────────┐",
382 "│ Test Artist │",
383 "│ Albums: 1 Songs: 1 Duration: 00:03:00.00 │",
384 "│ │",
385 "│q: add to queue | r: start radio | p: add to playlist─────│",
386 "│Performing operations on entire artist────────────────────│",
387 "│▶ Albums (1): │",
388 "│▶ Songs (1): │",
389 "└ ⏎ : Open | ←/↑/↓/→: Navigate | ␣ Check───────────────────┘",
390 ]);
391
392 assert_buffer_eq(&buffer, &expected);
393
394 Ok(())
395 }
396
397 #[test]
398 fn test_render_with_checked() -> Result<()> {
399 let (tx, _) = tokio::sync::mpsc::unbounded_channel();
400 let mut view = ArtistView::new(&state_with_everything(), tx);
401 let (mut terminal, area) = setup_test_terminal(60, 9);
402 let props = RenderProps {
403 area,
404 is_focused: true,
405 };
406 let buffer = terminal
407 .draw(|frame| view.render(frame, props))
408 .unwrap()
409 .buffer
410 .clone();
411 let expected = Buffer::with_lines([
412 "┌Artist View───────────────────────────────────────────────┐",
413 "│ Test Artist │",
414 "│ Albums: 1 Songs: 1 Duration: 00:03:00.00 │",
415 "│ │",
416 "│q: add to queue | r: start radio | p: add to playlist─────│",
417 "│Performing operations on entire artist────────────────────│",
418 "│▶ Albums (1): │",
419 "│▶ Songs (1): │",
420 "└ ⏎ : Open | ←/↑/↓/→: Navigate | ␣ Check───────────────────┘",
421 ]);
422 assert_buffer_eq(&buffer, &expected);
423
424 view.handle_key_event(KeyEvent::from(KeyCode::Down));
426 view.handle_key_event(KeyEvent::from(KeyCode::Down));
427 view.handle_key_event(KeyEvent::from(KeyCode::Right));
428 let _frame = terminal.draw(|frame| view.render(frame, props)).unwrap();
429 view.handle_key_event(KeyEvent::from(KeyCode::Down));
430 view.handle_key_event(KeyEvent::from(KeyCode::Char(' ')));
431
432 let buffer = terminal
433 .draw(|frame| view.render(frame, props))
434 .unwrap()
435 .buffer
436 .clone();
437 let expected = Buffer::with_lines([
438 "┌Artist View───────────────────────────────────────────────┐",
439 "│ Test Artist │",
440 "│ Albums: 1 Songs: 1 Duration: 00:03:00.00 │",
441 "│ │",
442 "│q: add to queue | r: start radio | p: add to playlist─────│",
443 "│Performing operations on checked items────────────────────│",
444 "│▼ Songs (1): │",
445 "│ ☑ Test Song Test Artist │",
446 "└ ⏎ : Open | ←/↑/↓/→: Navigate | ␣ Check───────────────────┘",
447 ]);
448
449 assert_buffer_eq(&buffer, &expected);
450
451 Ok(())
452 }
453
454 #[test]
455 fn smoke_navigation() {
456 let (tx, _) = tokio::sync::mpsc::unbounded_channel();
457 let mut view = ArtistView::new(&state_with_everything(), tx);
458
459 view.handle_key_event(KeyEvent::from(KeyCode::Up));
460 view.handle_key_event(KeyEvent::from(KeyCode::PageUp));
461 view.handle_key_event(KeyEvent::from(KeyCode::Down));
462 view.handle_key_event(KeyEvent::from(KeyCode::PageDown));
463 view.handle_key_event(KeyEvent::from(KeyCode::Left));
464 view.handle_key_event(KeyEvent::from(KeyCode::Right));
465 }
466
467 #[test]
468 fn test_actions() {
469 let (tx, mut rx) = tokio::sync::mpsc::unbounded_channel();
470 let mut view = ArtistView::new(&state_with_everything(), tx);
471
472 let (mut terminal, area) = setup_test_terminal(60, 9);
474 let props = RenderProps {
475 area,
476 is_focused: true,
477 };
478 let _frame = terminal.draw(|frame| view.render(frame, props)).unwrap();
479
480 view.handle_key_event(KeyEvent::from(KeyCode::Char('q')));
483 assert_eq!(
484 rx.blocking_recv().unwrap(),
485 Action::Audio(AudioAction::Queue(QueueAction::Add(vec![(
486 "artist",
487 item_id()
488 )
489 .into()])))
490 );
491 view.handle_key_event(KeyEvent::from(KeyCode::Char('r')));
492 assert_eq!(
493 rx.blocking_recv().unwrap(),
494 Action::ActiveView(ViewAction::Set(ActiveView::Radio(vec![(
495 "artist",
496 item_id()
497 )
498 .into()],)))
499 );
500 view.handle_key_event(KeyEvent::from(KeyCode::Char('p')));
501 assert_eq!(
502 rx.blocking_recv().unwrap(),
503 Action::Popup(PopupAction::Open(PopupType::Playlist(vec![(
504 "artist",
505 item_id()
506 )
507 .into()])))
508 );
509
510 view.handle_key_event(KeyEvent::from(KeyCode::Down));
513 view.handle_key_event(KeyEvent::from(KeyCode::Down));
514 view.handle_key_event(KeyEvent::from(KeyCode::Right));
515 let _frame = terminal.draw(|frame| view.render(frame, props)).unwrap();
516 view.handle_key_event(KeyEvent::from(KeyCode::Down));
517
518 view.handle_key_event(KeyEvent::from(KeyCode::Enter));
520 assert_eq!(
521 rx.blocking_recv().unwrap(),
522 Action::ActiveView(ViewAction::Set(ActiveView::Song(item_id())))
523 );
524
525 view.handle_key_event(KeyEvent::from(KeyCode::Char(' ')));
527
528 view.handle_key_event(KeyEvent::from(KeyCode::Char('q')));
530 assert_eq!(
531 rx.blocking_recv().unwrap(),
532 Action::Audio(AudioAction::Queue(QueueAction::Add(vec![(
533 "song",
534 item_id()
535 )
536 .into()])))
537 );
538
539 view.handle_key_event(KeyEvent::from(KeyCode::Char('r')));
541 assert_eq!(
542 rx.blocking_recv().unwrap(),
543 Action::ActiveView(ViewAction::Set(ActiveView::Radio(vec![(
544 "song",
545 item_id()
546 )
547 .into()],)))
548 );
549
550 view.handle_key_event(KeyEvent::from(KeyCode::Char('p')));
552 assert_eq!(
553 rx.blocking_recv().unwrap(),
554 Action::Popup(PopupAction::Open(PopupType::Playlist(vec![(
555 "song",
556 item_id()
557 )
558 .into()])))
559 );
560 }
561
562 #[test]
563 fn test_mouse_event() {
564 let (tx, mut rx) = tokio::sync::mpsc::unbounded_channel();
565 let mut view = ArtistView::new(&state_with_everything(), tx);
566
567 let (mut terminal, area) = setup_test_terminal(60, 9);
569 let props = RenderProps {
570 area,
571 is_focused: true,
572 };
573 let buffer = terminal
574 .draw(|frame| view.render(frame, props))
575 .unwrap()
576 .buffer
577 .clone();
578 let expected = Buffer::with_lines([
579 "┌Artist View───────────────────────────────────────────────┐",
580 "│ Test Artist │",
581 "│ Albums: 1 Songs: 1 Duration: 00:03:00.00 │",
582 "│ │",
583 "│q: add to queue | r: start radio | p: add to playlist─────│",
584 "│Performing operations on entire artist────────────────────│",
585 "│▶ Albums (1): │",
586 "│▶ Songs (1): │",
587 "└ ⏎ : Open | ←/↑/↓/→: Navigate | ␣ Check───────────────────┘",
588 ]);
589 assert_buffer_eq(&buffer, &expected);
590
591 view.handle_mouse_event(
593 MouseEvent {
594 kind: MouseEventKind::Down(MouseButton::Left),
595 column: 2,
596 row: 6,
597 modifiers: KeyModifiers::empty(),
598 },
599 area,
600 );
601 let buffer = terminal
602 .draw(|frame| view.render(frame, props))
603 .unwrap()
604 .buffer
605 .clone();
606 let expected = Buffer::with_lines([
607 "┌Artist View───────────────────────────────────────────────┐",
608 "│ Test Artist │",
609 "│ Albums: 1 Songs: 1 Duration: 00:03:00.00 │",
610 "│ │",
611 "│q: add to queue | r: start radio | p: add to playlist─────│",
612 "│Performing operations on entire artist────────────────────│",
613 "│▼ Albums (1): │",
614 "│ ☐ Test Album Test Artist │",
615 "└ ⏎ : Open | ←/↑/↓/→: Navigate | ␣ Check───────────────────┘",
616 ]);
617 assert_buffer_eq(&buffer, &expected);
618
619 view.handle_mouse_event(
621 MouseEvent {
622 kind: MouseEventKind::ScrollDown,
623 column: 2,
624 row: 6,
625 modifiers: KeyModifiers::empty(),
626 },
627 area,
628 );
629 let buffer = terminal
630 .draw(|frame| view.render(frame, props))
631 .unwrap()
632 .buffer
633 .clone();
634 assert_buffer_eq(&buffer, &expected);
635
636 view.handle_mouse_event(
638 MouseEvent {
639 kind: MouseEventKind::Down(MouseButton::Left),
640 column: 2,
641 row: 7,
642 modifiers: KeyModifiers::empty(),
643 },
644 area,
645 );
646 assert_eq!(
647 rx.blocking_recv().unwrap(),
648 Action::ActiveView(ViewAction::Set(ActiveView::Album(item_id())))
649 );
650 let buffer = terminal
651 .draw(|frame| view.render(frame, props))
652 .unwrap()
653 .buffer
654 .clone();
655 let expected = Buffer::with_lines([
656 "┌Artist View───────────────────────────────────────────────┐",
657 "│ Test Artist │",
658 "│ Albums: 1 Songs: 1 Duration: 00:03:00.00 │",
659 "│ │",
660 "│q: add to queue | r: start radio | p: add to playlist─────│",
661 "│Performing operations on checked items────────────────────│",
662 "│▼ Albums (1): │",
663 "│ ☑ Test Album Test Artist │",
664 "└ ⏎ : Open | ←/↑/↓/→: Navigate | ␣ Check───────────────────┘",
665 ]);
666 assert_buffer_eq(&buffer, &expected);
667
668 view.handle_mouse_event(
670 MouseEvent {
671 kind: MouseEventKind::ScrollUp,
672 column: 2,
673 row: 7,
674 modifiers: KeyModifiers::empty(),
675 },
676 area,
677 );
678 let buffer = terminal
679 .draw(|frame| view.render(frame, props))
680 .unwrap()
681 .buffer
682 .clone();
683 assert_buffer_eq(&buffer, &expected);
684 }
685}
686
687#[cfg(test)]
688mod library_view_tests {
689 use super::*;
690 use crate::test_utils::{
691 assert_buffer_eq, item_id, setup_test_terminal, state_with_everything,
692 };
693 use anyhow::Result;
694 use crossterm::event::{KeyModifiers, MouseButton, MouseEventKind};
695 use pretty_assertions::assert_eq;
696 use ratatui::buffer::Buffer;
697
698 #[test]
699 fn test_new() {
700 let (tx, _) = tokio::sync::mpsc::unbounded_channel();
701 let state = state_with_everything();
702 let view = LibraryArtistsView::new(&state, tx);
703
704 assert_eq!(view.name(), "Library Artists View");
705 assert_eq!(view.props.artists, state.library.artists);
706 }
707
708 #[test]
709 fn test_move_with_state() {
710 let (tx, _) = tokio::sync::mpsc::unbounded_channel();
711 let state = AppState::default();
712 let new_state = state_with_everything();
713 let view = LibraryArtistsView::new(&state, tx).move_with_state(&new_state);
714
715 assert_eq!(view.props.artists, new_state.library.artists);
716 }
717
718 #[test]
719 fn test_render() -> Result<()> {
720 let (tx, _) = tokio::sync::mpsc::unbounded_channel();
721 let view = LibraryArtistsView::new(&state_with_everything(), tx);
722
723 let (mut terminal, area) = setup_test_terminal(60, 6);
724 let props = RenderProps {
725 area,
726 is_focused: true,
727 };
728 let buffer = terminal
729 .draw(|frame| view.render(frame, props))
730 .unwrap()
731 .buffer
732 .clone();
733 let expected = Buffer::with_lines([
734 "┌Library Artists sorted by: Name───────────────────────────┐",
735 "│──────────────────────────────────────────────────────────│",
736 "│☐ Test Artist │",
737 "│ │",
738 "│s/S: change sort──────────────────────────────────────────│",
739 "└ ⏎ : Open | ←/↑/↓/→: Navigate | ␣ Check───────────────────┘",
740 ]);
741
742 assert_buffer_eq(&buffer, &expected);
743
744 Ok(())
745 }
746
747 #[test]
748 fn test_render_with_checked() -> Result<()> {
749 let (tx, _) = tokio::sync::mpsc::unbounded_channel();
750 let mut view = LibraryArtistsView::new(&state_with_everything(), tx);
751 let (mut terminal, area) = setup_test_terminal(60, 6);
752 let props = RenderProps {
753 area,
754 is_focused: true,
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 "│──────────────────────────────────────────────────────────│",
764 "│☐ Test Artist │",
765 "│ │",
766 "│s/S: change sort──────────────────────────────────────────│",
767 "└ ⏎ : Open | ←/↑/↓/→: Navigate | ␣ Check───────────────────┘",
768 ]);
769 assert_buffer_eq(&buffer, &expected);
770
771 view.handle_key_event(KeyEvent::from(KeyCode::Down));
773 view.handle_key_event(KeyEvent::from(KeyCode::Char(' ')));
774
775 let buffer = terminal
776 .draw(|frame| view.render(frame, props))
777 .unwrap()
778 .buffer
779 .clone();
780 let expected = Buffer::with_lines([
781 "┌Library Artists sorted by: Name───────────────────────────┐",
782 "│q: add to queue | r: start radio | p: add to playlist ────│",
783 "│☑ Test Artist │",
784 "│ │",
785 "│s/S: change sort──────────────────────────────────────────│",
786 "└ ⏎ : Open | ←/↑/↓/→: Navigate | ␣ Check───────────────────┘",
787 ]);
788
789 assert_buffer_eq(&buffer, &expected);
790
791 Ok(())
792 }
793
794 #[test]
795 fn test_sort_keys() {
796 let (tx, _) = tokio::sync::mpsc::unbounded_channel();
797 let mut view = LibraryArtistsView::new(&state_with_everything(), tx);
798
799 assert_eq!(view.props.sort_mode, NameSort::default());
800 view.handle_key_event(KeyEvent::from(KeyCode::Char('s')));
801 assert_eq!(view.props.sort_mode, NameSort::default());
802 view.handle_key_event(KeyEvent::from(KeyCode::Char('S')));
803 assert_eq!(view.props.sort_mode, NameSort::default());
804 }
805
806 #[test]
807 fn smoke_navigation() {
808 let (tx, _) = tokio::sync::mpsc::unbounded_channel();
809 let mut view = LibraryArtistsView::new(&state_with_everything(), tx);
810
811 view.handle_key_event(KeyEvent::from(KeyCode::Up));
812 view.handle_key_event(KeyEvent::from(KeyCode::PageUp));
813 view.handle_key_event(KeyEvent::from(KeyCode::Down));
814 view.handle_key_event(KeyEvent::from(KeyCode::PageDown));
815 view.handle_key_event(KeyEvent::from(KeyCode::Left));
816 view.handle_key_event(KeyEvent::from(KeyCode::Right));
817 }
818
819 #[test]
820 fn test_actions() {
821 let (tx, mut rx) = tokio::sync::mpsc::unbounded_channel();
822 let mut view = LibraryArtistsView::new(&state_with_everything(), tx);
823
824 let (mut terminal, area) = setup_test_terminal(60, 9);
826 let props = RenderProps {
827 area,
828 is_focused: true,
829 };
830 terminal.draw(|frame| view.render(frame, props)).unwrap();
831
832 view.handle_key_event(KeyEvent::from(KeyCode::Down));
834
835 view.handle_key_event(KeyEvent::from(KeyCode::Char('p')));
838 view.handle_key_event(KeyEvent::from(KeyCode::Char('r')));
839 view.handle_key_event(KeyEvent::from(KeyCode::Char('q')));
840 view.handle_key_event(KeyEvent::from(KeyCode::Enter));
842 let action = rx.blocking_recv().unwrap();
843 assert_eq!(
844 action,
845 Action::ActiveView(ViewAction::Set(ActiveView::Artist(item_id())))
846 );
847
848 view.handle_key_event(KeyEvent::from(KeyCode::Char(' ')));
850
851 view.handle_key_event(KeyEvent::from(KeyCode::Char('q')));
853 let action = rx.blocking_recv().unwrap();
854 assert_eq!(
855 action,
856 Action::Audio(AudioAction::Queue(QueueAction::Add(vec![(
857 "artist",
858 item_id()
859 )
860 .into()])))
861 );
862
863 view.handle_key_event(KeyEvent::from(KeyCode::Char('r')));
865 let action = rx.blocking_recv().unwrap();
866 assert_eq!(
867 action,
868 Action::ActiveView(ViewAction::Set(ActiveView::Radio(vec![(
869 "artist",
870 item_id()
871 )
872 .into()],)))
873 );
874
875 view.handle_key_event(KeyEvent::from(KeyCode::Char('p')));
877 let action = rx.blocking_recv().unwrap();
878 assert_eq!(
879 action,
880 Action::Popup(PopupAction::Open(PopupType::Playlist(vec![(
881 "artist",
882 item_id()
883 )
884 .into()])))
885 );
886 }
887
888 #[test]
889 fn test_mouse() {
890 let (tx, mut rx) = tokio::sync::mpsc::unbounded_channel();
891 let mut view = LibraryArtistsView::new(&state_with_everything(), tx);
892
893 let (mut terminal, area) = setup_test_terminal(60, 6);
895 let props = RenderProps {
896 area,
897 is_focused: true,
898 };
899 let buffer = terminal
900 .draw(|frame| view.render(frame, props))
901 .unwrap()
902 .buffer
903 .clone();
904 let expected = Buffer::with_lines([
905 "┌Library Artists sorted by: Name───────────────────────────┐",
906 "│──────────────────────────────────────────────────────────│",
907 "│☐ Test Artist │",
908 "│ │",
909 "│s/S: change sort──────────────────────────────────────────│",
910 "└ ⏎ : Open | ←/↑/↓/→: Navigate | ␣ Check───────────────────┘",
911 ]);
912 assert_buffer_eq(&buffer, &expected);
913
914 view.handle_mouse_event(
916 MouseEvent {
917 kind: MouseEventKind::Down(MouseButton::Left),
918 column: 2,
919 row: 2,
920 modifiers: KeyModifiers::empty(),
921 },
922 area,
923 );
924 let buffer = terminal
925 .draw(|frame| view.render(frame, props))
926 .unwrap()
927 .buffer
928 .clone();
929 let expected = Buffer::with_lines([
930 "┌Library Artists sorted by: Name───────────────────────────┐",
931 "│q: add to queue | r: start radio | p: add to playlist ────│",
932 "│☑ Test Artist │",
933 "│ │",
934 "│s/S: change sort──────────────────────────────────────────│",
935 "└ ⏎ : Open | ←/↑/↓/→: Navigate | ␣ Check───────────────────┘",
936 ]);
937 assert_buffer_eq(&buffer, &expected);
938
939 view.handle_mouse_event(
941 MouseEvent {
942 kind: MouseEventKind::ScrollDown,
943 column: 2,
944 row: 2,
945 modifiers: KeyModifiers::empty(),
946 },
947 area,
948 );
949 let buffer = terminal
950 .draw(|frame| view.render(frame, props))
951 .unwrap()
952 .buffer
953 .clone();
954 assert_buffer_eq(&buffer, &expected);
955
956 view.handle_mouse_event(
958 MouseEvent {
959 kind: MouseEventKind::ScrollUp,
960 column: 2,
961 row: 2,
962 modifiers: KeyModifiers::empty(),
963 },
964 area,
965 );
966 let buffer = terminal
967 .draw(|frame| view.render(frame, props))
968 .unwrap()
969 .buffer
970 .clone();
971 assert_buffer_eq(&buffer, &expected);
972
973 view.handle_mouse_event(
975 MouseEvent {
976 kind: MouseEventKind::Down(MouseButton::Left),
977 column: 2,
978 row: 2,
979 modifiers: KeyModifiers::empty(),
980 },
981 area,
982 );
983 assert_eq!(
984 rx.blocking_recv().unwrap(),
985 Action::ActiveView(ViewAction::Set(ActiveView::Artist(item_id())))
986 );
987
988 let mouse = MouseEvent {
990 kind: MouseEventKind::Down(MouseButton::Left),
991 column: 2,
992 row: 3,
993 modifiers: KeyModifiers::empty(),
994 };
995 view.handle_mouse_event(mouse, area);
996 assert_eq!(view.tree_state.lock().unwrap().get_selected_thing(), None);
997 view.handle_mouse_event(mouse, area);
998 assert_eq!(
999 rx.try_recv(),
1000 Err(tokio::sync::mpsc::error::TryRecvError::Empty)
1001 );
1002 }
1003}