From d66a89059d7e4d2726d049504b0163c3f3c2d0f1 Mon Sep 17 00:00:00 2001 From: Gabriel Kaszewski Date: Thu, 7 May 2026 00:43:08 +0200 Subject: [PATCH] refactor: simplify token handling and enhance input rendering in UI --- crates/tui/src/main.rs | 51 +++++-------- crates/tui/src/ui.rs | 165 ++++++++--------------------------------- 2 files changed, 47 insertions(+), 169 deletions(-) diff --git a/crates/tui/src/main.rs b/crates/tui/src/main.rs index 025ec12..cc62364 100644 --- a/crates/tui/src/main.rs +++ b/crates/tui/src/main.rs @@ -122,10 +122,7 @@ fn handle_command(cmd: Command, app: &App, client: &Arc, tx: &mpsc::S } Command::LoadDiary { offset } => { - let token = match &app.token { - Some(t) => t.clone(), - None => return, - }; + let Some(token) = app.token.clone() else { return }; let c = client.clone(); let tx = tx.clone(); tokio::spawn(async move { @@ -138,10 +135,7 @@ fn handle_command(cmd: Command, app: &App, client: &Arc, tx: &mpsc::S } Command::LoadHistory { movie_id } => { - let token = match &app.token { - Some(t) => t.clone(), - None => return, - }; + let Some(token) = app.token.clone() else { return }; let c = client.clone(); let tx = tx.clone(); tokio::spawn(async move { @@ -154,10 +148,7 @@ fn handle_command(cmd: Command, app: &App, client: &Arc, tx: &mpsc::S } Command::CreateReview(req) => { - let token = match &app.token { - Some(t) => t.clone(), - None => return, - }; + let Some(token) = app.token.clone() else { return }; let c = client.clone(); let tx = tx.clone(); tokio::spawn(async move { @@ -170,10 +161,7 @@ fn handle_command(cmd: Command, app: &App, client: &Arc, tx: &mpsc::S } Command::DeleteReview(id) => { - let token = match &app.token { - Some(t) => t.clone(), - None => return, - }; + let Some(token) = app.token.clone() else { return }; let c = client.clone(); let tx = tx.clone(); tokio::spawn(async move { @@ -186,10 +174,7 @@ fn handle_command(cmd: Command, app: &App, client: &Arc, tx: &mpsc::S } Command::ImportNext(index) => { - let token = match &app.token { - Some(t) => t.clone(), - None => return, - }; + let Some(token) = app.token.clone() else { return }; let req = match &app.screen { Screen::Main(m) => match m.bulk_import.valid_requests.get(index) { Some(r) => r.clone(), @@ -209,6 +194,16 @@ fn handle_command(cmd: Command, app: &App, client: &Arc, tx: &mpsc::S // ── Key → Action ────────────────────────────────────────────────────────────── +fn tab_shortcut(code: KeyCode) -> Option { + match code { + KeyCode::Char('1') => Some(Action::TabSelect(Tab::Diary)), + KeyCode::Char('2') => Some(Action::TabSelect(Tab::AddReview)), + KeyCode::Char('3') => Some(Action::TabSelect(Tab::BulkImport)), + KeyCode::Char('4') => Some(Action::TabSelect(Tab::Settings)), + _ => None, + } +} + fn key_to_action(app: &App, key: ratatui::crossterm::event::KeyEvent) -> Option { // Ctrl+C always quits if key.modifiers.contains(KeyModifiers::CONTROL) && key.code == KeyCode::Char('c') { @@ -247,11 +242,7 @@ fn key_to_action(app: &App, key: ratatui::crossterm::event::KeyEvent) -> Option< KeyCode::BackTab => Some(Action::TabPrev), KeyCode::Char('>') | KeyCode::Char('m') => Some(Action::LoadMore), KeyCode::Char('<') | KeyCode::Char('b') => Some(Action::LoadPrev), - KeyCode::Char('1') => Some(Action::TabSelect(Tab::Diary)), - KeyCode::Char('2') => Some(Action::TabSelect(Tab::AddReview)), - KeyCode::Char('3') => Some(Action::TabSelect(Tab::BulkImport)), - KeyCode::Char('4') => Some(Action::TabSelect(Tab::Settings)), - _ => None, + _ => tab_shortcut(key.code), }, Tab::AddReview => match key.code { @@ -280,10 +271,7 @@ fn key_to_action(app: &App, key: ratatui::crossterm::event::KeyEvent) -> Option< KeyCode::Tab if !in_path => Some(Action::TabNext), KeyCode::BackTab if !in_path => Some(Action::TabPrev), KeyCode::Char('q') if !in_path => Some(Action::Quit), - KeyCode::Char('1') if !in_path => Some(Action::TabSelect(Tab::Diary)), - KeyCode::Char('2') if !in_path => Some(Action::TabSelect(Tab::AddReview)), - KeyCode::Char('3') if !in_path => Some(Action::TabSelect(Tab::BulkImport)), - KeyCode::Char('4') if !in_path => Some(Action::TabSelect(Tab::Settings)), + _ if !in_path => tab_shortcut(key.code), _ => None, } } @@ -301,10 +289,7 @@ fn key_to_action(app: &App, key: ratatui::crossterm::event::KeyEvent) -> Option< }, KeyCode::Esc => Some(Action::Escape), KeyCode::Char('q') => Some(Action::Quit), - KeyCode::Char('1') if !on_url => Some(Action::TabSelect(Tab::Diary)), - KeyCode::Char('2') if !on_url => Some(Action::TabSelect(Tab::AddReview)), - KeyCode::Char('3') if !on_url => Some(Action::TabSelect(Tab::BulkImport)), - KeyCode::Char('4') if !on_url => Some(Action::TabSelect(Tab::Settings)), + _ if !on_url => tab_shortcut(key.code), _ => None, } } diff --git a/crates/tui/src/ui.rs b/crates/tui/src/ui.rs index ac58bde..a62c154 100644 --- a/crates/tui/src/ui.rs +++ b/crates/tui/src/ui.rs @@ -63,14 +63,7 @@ fn draw_setup(frame: &mut Frame, area: Rect, state: &SetupState) { inner[1], ); - let url_display = format!("{}_", state.api_url); - let url_widget = Paragraph::new(url_display).block( - Block::default() - .title("API URL") - .borders(Borders::ALL) - .border_style(Style::default().fg(Color::Yellow)), - ); - frame.render_widget(url_widget, inner[2]); + render_input(frame, inner[2], "API URL", &state.api_url, true); if let Some(err) = &state.error { frame.render_widget( @@ -105,45 +98,9 @@ fn draw_login(frame: &mut Frame, area: Rect, state: &LoginState) { .margin(1) .split(popup); - let email_style = if state.focused == LoginField::Email { - Style::default().fg(Color::Yellow) - } else { - Style::default() - }; - let pass_style = if state.focused == LoginField::Password { - Style::default().fg(Color::Yellow) - } else { - Style::default() - }; - - let email_display = if state.focused == LoginField::Email { - format!("{}_", state.email) - } else { - state.email.clone() - }; - let pass_display = if state.focused == LoginField::Password { - format!("{}_", "*".repeat(state.password.len())) - } else { - "*".repeat(state.password.len()) - }; - frame.render_widget( - Paragraph::new(email_display).block( - Block::default() - .title("Email") - .borders(Borders::ALL) - .border_style(email_style), - ), - rows[1], - ); - frame.render_widget( - Paragraph::new(pass_display).block( - Block::default() - .title("Password") - .borders(Borders::ALL) - .border_style(pass_style), - ), - rows[3], - ); + let pass_masked = "*".repeat(state.password.len()); + render_input(frame, rows[1], "Email", &state.email, state.focused == LoginField::Email); + render_input(frame, rows[3], "Password", &pass_masked, state.focused == LoginField::Password); frame.render_widget( Paragraph::new("Tab: next field Enter: login").alignment(Alignment::Center), rows[4], @@ -218,13 +175,12 @@ fn draw_diary(frame: &mut Frame, area: Rect, state: &DiaryState) { let can_load_prev = state.offset > 0; let page = state.offset / 20 + 1; let total_pages = state.total.div_ceil(20).max(1); - let mut hints = format!(" Diary ({} entries, page {}/{}) ", state.total, page, total_pages); - if can_load_prev { hints.push_str("[b: prev] "); } - if can_load_more { hints.push_str("[m: next] "); } - let list_title = hints; + let mut title = format!(" Diary ({} entries, page {}/{}) ", state.total, page, total_pages); + if can_load_prev { title.push_str("[b: prev] "); } + if can_load_more { title.push_str("[m: next] "); } let mut list_state = ListState::default(); list_state.select(Some(state.selected)); - let list = List::new(items).block(Block::default().title(list_title).borders(Borders::ALL)); + let list = List::new(items).block(Block::default().title(title).borders(Borders::ALL)); frame.render_stateful_widget(list, cols[0], &mut list_state); // Delete confirmation overlay @@ -317,79 +273,23 @@ fn draw_add_review(frame: &mut Frame, area: Rect, state: &AddReviewState) { ]) .split(inner); - let fs = |f: AddReviewField| { - if state.focused == f { - Style::default().fg(Color::Yellow) - } else { - Style::default() - } - }; - let ft = |s: &str, f: AddReviewField| { - if state.focused == f { - format!("{s}_") - } else { - s.to_string() - } - }; + render_input(frame, rows[0], "External ID (TMDB/OMDB)", &state.external_id, state.focused == AddReviewField::ExternalId); + render_input(frame, rows[1], "Title", &state.title, state.focused == AddReviewField::Title); + render_input(frame, rows[2], "Year", &state.year, state.focused == AddReviewField::Year); + let rating_active = state.focused == AddReviewField::Rating; frame.render_widget( - Paragraph::new(ft(&state.external_id, AddReviewField::ExternalId)).block( - Block::default() - .title("External ID (TMDB/OMDB)") - .borders(Borders::ALL) - .border_style(fs(AddReviewField::ExternalId)), - ), - rows[0], - ); - frame.render_widget( - Paragraph::new(ft(&state.title, AddReviewField::Title)).block( - Block::default() - .title("Title") - .borders(Borders::ALL) - .border_style(fs(AddReviewField::Title)), - ), - rows[1], - ); - frame.render_widget( - Paragraph::new(ft(&state.year, AddReviewField::Year)).block( - Block::default() - .title("Year") - .borders(Borders::ALL) - .border_style(fs(AddReviewField::Year)), - ), - rows[2], - ); - frame.render_widget( - Paragraph::new(format!( - "{} \u{2190} \u{2192} to adjust", - stars(state.rating) - )) - .block( + Paragraph::new(format!("{} \u{2190} \u{2192} to adjust", stars(state.rating))).block( Block::default() .title("Rating (0-5)") .borders(Borders::ALL) - .border_style(fs(AddReviewField::Rating)), + .border_style(if rating_active { Style::default().fg(Color::Yellow) } else { Style::default() }), ), rows[3], ); - frame.render_widget( - Paragraph::new(ft(&state.watched_at, AddReviewField::WatchedAt)).block( - Block::default() - .title("Watched at (YYYY-MM-DDTHH:MM:SS)") - .borders(Borders::ALL) - .border_style(fs(AddReviewField::WatchedAt)), - ), - rows[4], - ); - frame.render_widget( - Paragraph::new(ft(&state.comment, AddReviewField::Comment)).block( - Block::default() - .title("Comment (optional)") - .borders(Borders::ALL) - .border_style(fs(AddReviewField::Comment)), - ), - rows[5], - ); + + render_input(frame, rows[4], "Watched at (YYYY-MM-DDTHH:MM:SS)", &state.watched_at, state.focused == AddReviewField::WatchedAt); + render_input(frame, rows[5], "Comment (optional)", &state.comment, state.focused == AddReviewField::Comment); let submit_style = if state.focused == AddReviewField::Submit { Style::default() @@ -607,25 +507,7 @@ fn draw_settings(frame: &mut Frame, area: Rect, state: &SettingsState) { ]) .split(inner); - let url_style = if state.focused == SettingsField::ApiUrl { - Style::default().fg(Color::Yellow) - } else { - Style::default() - }; - let url_display = if state.focused == SettingsField::ApiUrl { - format!("{}_", state.api_url) - } else { - state.api_url.clone() - }; - frame.render_widget( - Paragraph::new(url_display).block( - Block::default() - .title("API URL") - .borders(Borders::ALL) - .border_style(url_style), - ), - rows[0], - ); + render_input(frame, rows[0], "API URL", &state.api_url, state.focused == SettingsField::ApiUrl); let save_style = if state.focused == SettingsField::Save { Style::default() @@ -672,6 +554,17 @@ fn draw_status_bar(frame: &mut Frame, area: Rect, status: Option<&StatusMsg>, lo // ── Helpers ─────────────────────────────────────────────────────────────────── +fn render_input(frame: &mut Frame, area: Rect, title: &str, value: &str, active: bool) { + let text = if active { format!("{value}_") } else { value.to_string() }; + let border_style = if active { Style::default().fg(Color::Yellow) } else { Style::default() }; + frame.render_widget( + Paragraph::new(text).block( + Block::default().title(title).borders(Borders::ALL).border_style(border_style), + ), + area, + ); +} + fn stars(rating: u8) -> String { format!( "{}{}",