1use std::time::Duration;
2
3use anyhow::Result;
4use crossterm::event::{Event, EventStream, KeyCode, KeyEventKind};
5use help::HelpScreen;
6use lyrics::LyricsScreen;
7use ratatui::{
8 Frame,
9 buffer::Buffer,
10 layout::{Alignment, Rect},
11 style::{
12 Color, Modifier, Style,
13 palette::material::{BLUE, BROWN, GRAY, LIGHT_BLUE, WHITE, YELLOW},
14 },
15 widgets::{Block, Borders, Paragraph, Widget},
16};
17use search::SearchScreen;
18use tokio_stream::StreamExt;
19
20mod help;
21mod lyrics;
22mod search;
23
24#[derive(Default, Clone, Debug)]
25enum Screen {
26 #[default]
27 Lyrics,
28 Search,
29 Help,
30}
31
32#[derive(Clone, Default)]
33pub struct App {
34 exit: bool,
35 screen: Screen,
36
37 lyrics: LyricsScreen,
38 search: SearchScreen,
39 help: HelpScreen,
40}
41
42impl App {
43 const FRAMES_PER_SECOND: f32 = 30.0;
44
45 pub async fn run(&mut self) -> Result<()> {
47 let mut terminal = ratatui::init();
48
49 rust_i18n::set_locale("zh");
50 let period = Duration::from_secs_f32(1.0 / Self::FRAMES_PER_SECOND);
51 let mut interval = tokio::time::interval(period);
52 let mut events = EventStream::new();
53
54 while !self.exit {
55 tokio::select! {
56 _ = interval.tick() => {
57 terminal.draw(|frame| self.draw(frame))?;
58 self.update().await;
59 },
60 Some(Ok(event)) = events.next() => self.handle_event(&event).await,
61 }
62 }
63 Ok(())
64 }
65
66 pub fn restore_term(&self) -> Result<()> {
67 ratatui::restore();
68 Ok(())
69 }
70
71 async fn update(&mut self) {
73 match self.screen {
74 Screen::Lyrics => {
75 self.lyrics.update().await;
76 }
77 Screen::Search => {
78 if self.search.lyrics_reset() {
79 self.lyrics.reset();
80 self.screen = Screen::Lyrics;
81 }
82 self.search.update().await;
83 }
84 _ => {}
85 }
86 }
87
88 fn draw<'a>(&mut self, frame: &mut Frame<'a>) {
89 let area = frame.area();
90 let buf = frame.buffer_mut();
91 match self.screen {
92 Screen::Lyrics => self.lyrics.render(area, buf),
93 Screen::Search => self.search.render(area, buf),
94 Screen::Help => self.help.render(area, buf),
95 }
96 }
97
98 async fn handle_event(&mut self, event: &Event) {
99 if let Event::Key(key) = event
100 && key.kind == KeyEventKind::Press
101 {
102 match self.screen {
103 Screen::Lyrics => match key.code {
104 KeyCode::Char('h') | KeyCode::Char('?') => self.screen = Screen::Help,
105 KeyCode::Char('s') => self.screen = Screen::Search,
106 KeyCode::Char('q') | KeyCode::Esc => self.exit(),
107 _ => self.lyrics.handle_key_event(key).await,
108 },
109 Screen::Search => match key.code {
110 KeyCode::Char('q') | KeyCode::Esc => self.screen = Screen::Lyrics,
111 KeyCode::Char('h') | KeyCode::Char('?') => self.screen = Screen::Help,
112 _ => self.search.handle_key_event(key).await,
113 },
114 Screen::Help => match key.code {
115 KeyCode::Char('q') | KeyCode::Esc => self.screen = Screen::Lyrics,
116 KeyCode::Char('t') => self.toggle_locale(),
117 _ => {}
118 },
119 }
120 }
121 }
122
123 fn exit(&mut self) {
125 self.exit = true;
126 }
127
128 fn toggle_locale(&self) {
129 let locale = rust_i18n::locale();
130 if &*locale == "en" {
131 rust_i18n::set_locale("zh");
132 } else {
133 rust_i18n::set_locale("en");
134 }
135 }
136}
137
138const LYRICS_HEADER_STYLE: Style = Style::new().fg(BLUE.c400);
139const LYRICS_GAUGE_STYLE: Style = Style::new()
140 .add_modifier(Modifier::ITALIC)
141 .add_modifier(Modifier::BOLD)
142 .fg(WHITE);
143
144const HELP_KEY_STYLE: Style = Style::new()
145 .fg(LIGHT_BLUE.c400)
146 .add_modifier(Modifier::BOLD);
147
148const NORMAL_ROW_BG: Color = GRAY.c900;
149const ALT_ROW_BG_COLOR: Color = GRAY.c800;
150const SELECTED_STYLE: Style = Style::new().bg(BROWN.c900).add_modifier(Modifier::BOLD);
151
152const fn alternate_colors(i: usize) -> Color {
153 if i.is_multiple_of(2) {
154 NORMAL_ROW_BG
155 } else {
156 ALT_ROW_BG_COLOR
157 }
158}
159
160fn render_error(area: Rect, buf: &mut Buffer, err_msg: &str) {
161 Paragraph::new(err_msg)
162 .style(Style::default().fg(Color::Red))
163 .block(
164 Block::default()
165 .title("ERROR")
166 .title_alignment(Alignment::Center)
167 .borders(Borders::ALL),
168 )
169 .render(area, buf);
170}