use ratatui::layout::{Constraint, Layout, Rect};
use ratatui::style::{Color, Style};
use ratatui::text::{Line, Span};
use ratatui::widgets::{Block, Borders, Gauge, Paragraph};
use ratatui::Frame;
use std::sync::{Arc, Mutex};
use crate::core::download::{DownloadJob, DownloadStatus};
use crate::core::utils::format_size;
use crate::tui::utils::{open_in_browser, truncate};
use crate::types::Rom;
use super::{LibraryBrowseScreen, SearchScreen};
pub enum GameDetailPrevious {
Library(LibraryBrowseScreen),
Search(SearchScreen),
}
pub struct GameDetailScreen {
pub rom: Rom,
pub other_files: Vec<Rom>,
pub previous: GameDetailPrevious,
pub show_technical: bool,
pub message: Option<String>,
pub downloads: Arc<Mutex<Vec<DownloadJob>>>,
pub has_started_download: bool,
pub download_completion_acknowledged: bool,
}
impl GameDetailScreen {
pub fn new(
rom: Rom,
other_files: Vec<Rom>,
previous: GameDetailPrevious,
downloads: Arc<Mutex<Vec<DownloadJob>>>,
) -> Self {
Self {
rom,
other_files,
previous,
show_technical: false,
message: None,
downloads,
has_started_download: false,
download_completion_acknowledged: false,
}
}
pub fn toggle_technical(&mut self) {
self.show_technical = !self.show_technical;
}
pub fn open_cover(&mut self) {
self.message = None;
let url = self.rom.url_cover.as_deref().filter(|s| !s.is_empty());
match url {
Some(u) => match open_in_browser(u) {
Ok(_) => self.message = Some("Opened in browser".to_string()),
Err(e) => self.message = Some(format!("Failed: {}", e)),
},
None => self.message = Some("No cover URL".to_string()),
}
}
pub fn clear_message(&mut self) {
self.message = None;
}
fn active_download(&self) -> Option<DownloadJob> {
self.downloads.lock().ok().and_then(|list| {
list.iter()
.rev()
.find(|j| {
j.rom_id == self.rom.id
&& (matches!(j.status, DownloadStatus::Downloading)
|| (!self.download_completion_acknowledged
&& matches!(
j.status,
DownloadStatus::Done | DownloadStatus::Error(_)
)))
})
.cloned()
})
}
pub fn render(&self, f: &mut Frame, area: Rect) {
let chunks = Layout::default()
.constraints([Constraint::Min(10), Constraint::Length(3)])
.direction(ratatui::layout::Direction::Vertical)
.split(area);
let title = self.rom.name.as_str();
let platform = self
.rom
.platform_display_name
.as_deref()
.or(self.rom.platform_custom_name.as_deref())
.unwrap_or("—");
let summary = self.rom.summary.as_deref().unwrap_or("").trim();
let path = self.rom.fs_path.as_str();
let size = format_size(self.rom.fs_size_bytes);
let cover_text = if self.rom.url_cover.is_some() {
"[Cover] (o: open in browser)"
} else {
"No cover"
};
let mut lines: Vec<Line> = vec![
Line::from(vec![
Span::styled("Title: ", Style::default().fg(Color::Cyan)),
Span::raw(title),
]),
Line::from(vec![
Span::styled("Platform: ", Style::default().fg(Color::Cyan)),
Span::raw(platform),
]),
Line::from(""),
Line::from(vec![
Span::styled("Cover: ", Style::default().fg(Color::Cyan)),
Span::raw(cover_text),
]),
Line::from(""),
Line::from(vec![Span::styled(
"Summary: ",
Style::default().fg(Color::Cyan),
)]),
Line::from(if summary.is_empty() { "—" } else { summary }),
Line::from(""),
Line::from(vec![
Span::styled("File: ", Style::default().fg(Color::Cyan)),
Span::raw(path),
]),
Line::from(vec![
Span::styled("Size: ", Style::default().fg(Color::Cyan)),
Span::raw(size),
]),
];
if !self.other_files.is_empty() {
lines.push(Line::from(""));
lines.push(Line::from(vec![
Span::styled(
"Other files (updates/DLC): ",
Style::default().fg(Color::Cyan),
),
Span::raw(format!("{} file(s)", self.other_files.len())),
]));
for other in self.other_files.iter().take(10) {
let label = other.fs_name.as_str();
lines.push(Line::from(format!(" • {}", label)));
}
if self.other_files.len() > 10 {
lines.push(Line::from(format!(
" … and {} more",
self.other_files.len() - 10
)));
}
}
if self.show_technical {
lines.push(Line::from(""));
lines.push(Line::from(Span::styled(
"Technical:",
Style::default().fg(Color::Yellow),
)));
lines.push(Line::from(format!(" ID: {}", self.rom.id)));
lines.push(Line::from(format!(
" Platform ID: {}",
self.rom.platform_id
)));
if let Some(s) = &self.rom.slug {
lines.push(Line::from(format!(" Slug: {}", s)));
}
lines.push(Line::from(format!(
" Identified: {}",
self.rom.is_identified
)));
}
let block = Block::default().title("Game detail").borders(Borders::ALL);
let p = Paragraph::new(lines)
.block(block)
.wrap(ratatui::widgets::Wrap { trim: true });
f.render_widget(p, chunks[0]);
let footer_area = chunks[1];
if let Some(job) = self.active_download() {
let (label, style) = match &job.status {
DownloadStatus::Downloading => (
format!("Downloading… {}%", job.percent()),
Style::default().fg(Color::Cyan),
),
DownloadStatus::Done => (
"Download complete".to_string(),
Style::default().fg(Color::Green),
),
DownloadStatus::Error(msg) => (
format!("Error: {}", truncate(msg, 50)),
Style::default().fg(Color::Red),
),
};
let gauge = Gauge::default()
.block(Block::default().borders(Borders::ALL))
.gauge_style(style)
.percent(job.percent())
.label(label);
f.render_widget(gauge, footer_area);
} else {
let help = if self.show_technical {
"Enter: Download | o: Open cover | m: Hide technical | Esc: Back"
} else {
"Enter: Download | o: Open cover | m: More technical details | Esc: Back"
};
let msg = self.message.as_deref().unwrap_or(help);
let footer = Paragraph::new(msg).block(Block::default().borders(Borders::ALL));
f.render_widget(footer, footer_area);
}
}
}