Skip to main content

lyrics_next/
ui.rs

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    // 保持UI和主循环不变
46    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    // 状态刷新
72    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    /// 关闭
124    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}