romm-tui 1.5.0

Terminal UI for the RomM API
Documentation
//! Metadata match picker key handler and screen transitions.

use std::time::{Duration, Instant};

use anyhow::Result;
use crossterm::event::{KeyCode, KeyEvent};

use romm_api::core::cache::RomCacheKey;
use romm_api::error::user_message_with_server_detail;

use super::super::{App, AppScreen};
use crate::tui::screens::game_detail::GameDetailPrevious;
use crate::tui::screens::metadata_match::{MetadataMatchPhase, MetadataMatchScreen};

impl App {
    pub(in crate::tui::app) fn metadata_apply_in_progress_message() -> String {
        "Metadata update already in progress.".into()
    }

    pub(in crate::tui::app) fn metadata_edit_supported(&self) -> bool {
        self.metadata_edit_compat.supported
    }

    pub(in crate::tui::app) fn metadata_edit_blocked_message(&self) -> String {
        self.metadata_edit_compat.unsupported_message().to_string()
    }

    pub(in crate::tui::app) fn open_metadata_match_screen(&mut self) {
        let rom_id = match &self.screen {
            AppScreen::GameDetail(detail) => Some(detail.rom.id),
            _ => None,
        };
        if rom_id.is_some_and(|id| self.metadata_apply_inflight_roms.contains(&id)) {
            if let AppScreen::GameDetail(detail) = &mut self.screen {
                detail.message = Some(Self::metadata_apply_in_progress_message());
                detail.message_clear_at = Some(Instant::now() + std::time::Duration::from_secs(5));
            }
            return;
        }
        if !self.metadata_edit_supported() {
            let msg = self.metadata_edit_blocked_message();
            if let AppScreen::GameDetail(detail) = &mut self.screen {
                detail.message = Some(msg);
                detail.message_clear_at = Some(Instant::now() + std::time::Duration::from_secs(5));
            }
            return;
        }
        let placeholder = self.transient_screen_placeholder();
        let prev = std::mem::replace(&mut self.screen, placeholder);
        let AppScreen::GameDetail(detail) = prev else {
            return;
        };
        self.metadata_search_gen = self.metadata_search_gen.wrapping_add(1);
        self.screen = AppScreen::MetadataMatch(Box::new(MetadataMatchScreen::new_for_rom(detail)));
    }

    pub(in crate::tui::app) fn handle_metadata_match(&mut self, key: &KeyEvent) -> Result<bool> {
        let picker = match &mut self.screen {
            AppScreen::MetadataMatch(p) => p,
            _ => return Ok(false),
        };

        if matches!(picker.phase, MetadataMatchPhase::QueryInput) {
            return self.handle_metadata_match_query_input(key);
        }

        match key.code {
            KeyCode::Esc => {
                let placeholder = self.transient_screen_placeholder();
                let prev = std::mem::replace(&mut self.screen, placeholder);
                if let AppScreen::MetadataMatch(p) = prev {
                    self.screen = AppScreen::GameDetail(p.previous);
                }
            }
            KeyCode::Char('r')
                if matches!(
                    picker.phase,
                    MetadataMatchPhase::Ready | MetadataMatchPhase::Failed(_)
                ) =>
            {
                picker.return_to_query_input();
            }
            KeyCode::Up | KeyCode::Char('k') => {
                if matches!(picker.phase, MetadataMatchPhase::Ready) {
                    picker.move_up();
                }
            }
            KeyCode::Down | KeyCode::Char('j') => {
                if matches!(picker.phase, MetadataMatchPhase::Ready) {
                    picker.move_down();
                }
            }
            KeyCode::Enter => {
                if !matches!(picker.phase, MetadataMatchPhase::Ready) {
                    return Ok(false);
                }
                let Some(fields) = picker.selected_update_fields() else {
                    return Ok(false);
                };
                if fields.match_fields.is_empty() {
                    let msg = "Selected result has no provider IDs to apply".to_string();
                    picker.phase = MetadataMatchPhase::Failed(msg.clone());
                    picker.message = Some(msg);
                    return Ok(false);
                }
                let rom_id = picker.previous.rom.id;
                let platform_id = picker.previous.rom.platform_id;
                picker.set_applying();
                self.spawn_metadata_apply_worker(rom_id, platform_id, fields, false);
            }
            KeyCode::Char('q') => return Ok(true),
            _ => {}
        }
        Ok(false)
    }

    fn handle_metadata_match_query_input(&mut self, key: &KeyEvent) -> Result<bool> {
        use crossterm::event::KeyModifiers;

        let picker = match &mut self.screen {
            AppScreen::MetadataMatch(p) => p,
            _ => return Ok(false),
        };

        match key.code {
            KeyCode::Esc => {
                let placeholder = self.transient_screen_placeholder();
                let prev = std::mem::replace(&mut self.screen, placeholder);
                if let AppScreen::MetadataMatch(p) = prev {
                    self.screen = AppScreen::GameDetail(p.previous);
                }
            }
            KeyCode::Enter => {
                let rom_id = picker.previous.rom.id;
                let term = picker.search_query.trim().to_string();
                if term.is_empty() {
                    picker.message = Some("Enter a search term".into());
                    return Ok(false);
                }
                picker.set_loading();
                self.spawn_metadata_search_worker(rom_id, term);
            }
            KeyCode::Backspace => picker.delete_char(),
            KeyCode::Delete => picker.delete_forward_char(),
            KeyCode::Left => picker.cursor_left(),
            KeyCode::Right => picker.cursor_right(),
            KeyCode::Home => picker.cursor_pos = 0,
            KeyCode::End => picker.cursor_pos = picker.search_query.len(),
            KeyCode::Char(c) if !key.modifiers.contains(KeyModifiers::CONTROL) => {
                picker.add_char(c)
            }
            KeyCode::Char('q') => return Ok(true),
            _ => {}
        }
        Ok(false)
    }

    pub(in crate::tui::app) fn spawn_metadata_unmatch(&mut self, rom_id: u64, platform_id: u64) {
        self.spawn_metadata_apply_worker(rom_id, platform_id, Default::default(), true);
    }

    fn finish_metadata_apply_success(&mut self, completed_rom_id: u64, rom: romm_api::types::Rom) {
        let platform_id = rom.platform_id;
        let applied = match &mut self.screen {
            AppScreen::MetadataMatch(picker)
                if picker.previous.rom.id == completed_rom_id && rom.id == completed_rom_id =>
            {
                picker.previous.apply_refreshed_rom(rom);
                let placeholder = self.transient_screen_placeholder();
                let prev = std::mem::replace(&mut self.screen, placeholder);
                if let AppScreen::MetadataMatch(p) = prev {
                    let mut detail = *p.previous;
                    detail.message = Some("Metadata updated.".into());
                    detail.message_clear_at = Some(Instant::now() + Duration::from_secs(3));
                    self.screen = AppScreen::GameDetail(Box::new(detail));
                }
                true
            }
            AppScreen::GameDetail(detail)
                if detail.rom.id == completed_rom_id && rom.id == completed_rom_id =>
            {
                detail.apply_refreshed_rom(rom);
                detail.metadata_unmatch_confirm = false;
                detail.message = Some("Metadata updated.".into());
                detail.message_clear_at = Some(Instant::now() + Duration::from_secs(3));
                true
            }
            _ => false,
        };
        if applied {
            self.invalidate_rom_lists_after_metadata_update(platform_id);
            self.mark_rom_list_screens_stale_after_metadata_update();
            if matches!(self.screen, AppScreen::LibraryBrowse(_)) {
                self.resume_library_rom_load_if_needed("metadata_apply_reload");
            }
            self.force_rom_reload_after_metadata = true;
            self.maybe_start_game_detail_cover_load();
            self.refresh_current_game_achievements();
        }
    }

    fn invalidate_rom_lists_after_metadata_update(&mut self, platform_id: u64) {
        self.rom_cache
            .remove_metadata_dependent_entries(platform_id);
        self.rom_partials
            .retain(|key, _| matches!(key, RomCacheKey::Platform(pid) if *pid != platform_id));
    }

    fn mark_rom_list_screens_stale_after_metadata_update(&mut self) {
        match &mut self.screen {
            AppScreen::GameDetail(detail) => match &mut detail.previous {
                GameDetailPrevious::Library(lib) => {
                    lib.clear_roms();
                    let should_reload = lib.expected_rom_count() > 0;
                    lib.set_rom_loading(should_reload);
                }
                GameDetailPrevious::Search(search) => {
                    search.clear_results();
                    search.loading = false;
                }
            },
            AppScreen::LibraryBrowse(lib) => {
                lib.clear_roms();
                let should_reload = lib.expected_rom_count() > 0;
                lib.set_rom_loading(should_reload);
            }
            AppScreen::Search(search) => {
                search.clear_results();
                search.loading = false;
            }
            _ => {}
        }
    }

    pub(in crate::tui::app) fn apply_metadata_search_complete(
        &mut self,
        done: super::super::background::types::MetadataSearchDone,
    ) {
        if done.gen != self.metadata_search_gen {
            return;
        }
        if let AppScreen::MetadataMatch(picker) = &mut self.screen {
            if !matches!(picker.phase, MetadataMatchPhase::Loading) {
                return;
            }
            if picker.previous.rom.id != done.rom_id {
                return;
            }
            match done.result {
                Ok(rows) => picker.apply_search_result(rows),
                Err(e) => picker.apply_search_error(user_message_with_server_detail(&e, 200)),
            }
        }
    }

    pub(in crate::tui::app) fn apply_metadata_apply_complete(
        &mut self,
        done: super::super::background::types::MetadataApplyDone,
    ) {
        self.metadata_apply_inflight_roms.remove(&done.rom_id);
        match done.result {
            Ok(rom) => self.finish_metadata_apply_success(done.rom_id, *rom),
            Err(e) => {
                let msg = format!(
                    "Metadata update failed: {}",
                    user_message_with_server_detail(&e, 200)
                );
                match &mut self.screen {
                    AppScreen::MetadataMatch(picker) if picker.previous.rom.id == done.rom_id => {
                        picker.phase = MetadataMatchPhase::Failed(msg.clone());
                        picker.message = Some(msg);
                    }
                    AppScreen::GameDetail(detail) if detail.rom.id == done.rom_id => {
                        detail.metadata_unmatch_confirm = false;
                        detail.message = Some(msg);
                        detail.message_clear_at = Some(Instant::now() + Duration::from_secs(5));
                    }
                    _ => {}
                }
            }
        }
    }
}