//! Upload screen — staged files, metadata editing, publish flow. use makeover_tui::makeover_layout::{Column, Priority, Width}; use makeover_tui::table::{self, Cell, Sizing, TableStyle}; use ratatui::Frame; use ratatui::layout::{Constraint, Layout, Rect}; use ratatui::style::{Modifier, Style}; use ratatui::text::{Line, Span}; use ratatui::widgets::{Block, Borders, Paragraph, TableState}; use crate::format; use crate::staging; use super::App; /// The staged file list's columns. /// /// The filename is what the creator recognises a staged file by, and it carries /// the editing marker, so it is the essential one. The title and the project /// are what publishing needs filled in, which keeps them above the size, the /// type and the price. const FILE_COLUMNS: [Column<'static>; 6] = [ Column { name: "File", width: Width::Fill, priority: Priority::Essential, sortable: false, sorted: None, }, Column { name: "Size", width: Width::Fixed, priority: Priority::Optional, sortable: false, sorted: None, }, Column { name: "Type", width: Width::Fixed, priority: Priority::Optional, sortable: false, sorted: None, }, Column { name: "Title", width: Width::Fixed, priority: Priority::Secondary, sortable: false, sorted: None, }, Column { name: "Project", width: Width::Fixed, priority: Priority::Secondary, sortable: false, sorted: None, }, Column { name: "Price", width: Width::Fixed, priority: Priority::Optional, sortable: false, sorted: None, }, ]; /// The tracks the hand-written `Constraint`s carried. const FILE_SIZING: Sizing<'static> = Sizing { lengths: &[ ("File", 16), ("Size", 10), ("Type", 8), ("Title", 16), ("Project", 14), ("Price", 8), ], fallback: 8, }; pub(crate) fn render(frame: &mut Frame, app: &App) { let area = frame.area(); let title = Line::from(vec![ Span::styled( " Makenot.work ", Style::default().add_modifier(Modifier::BOLD), ), Span::raw(" ── "), Span::styled("Upload", Style::default().add_modifier(Modifier::BOLD)), Span::raw(" "), ]); let block = Block::default() .title(title) .borders(Borders::ALL) .border_style(Style::default().fg(app.theme.line_border)); let inner = block.inner(area); frame.render_widget(block, area); let chunks = Layout::vertical([ Constraint::Length(1), // spacer Constraint::Length(1), // storage info Constraint::Length(1), // spacer Constraint::Length(1), // section header Constraint::Min(3), // file list Constraint::Length(1), // status line Constraint::Length(1), // keybindings ]) .split(inner); // Storage info render_storage_line(frame, app, chunks[1]); // Section header let file_count = app.staged_files.len(); let header = Paragraph::new(Line::from(vec![ Span::raw(" "), Span::styled( "Staged Files", Style::default().add_modifier(Modifier::BOLD), ), if file_count == 0 { Span::raw("") } else { Span::raw(format!(" ({file_count})")) }, ])); frame.render_widget(header, chunks[3]); // File list if app.loading { let loading = Paragraph::new(" Loading..."); frame.render_widget(loading, chunks[4]); } else if app.staged_files.is_empty() { let empty = Paragraph::new(Line::from(vec![ Span::raw(" No staged files. Upload with: "), Span::styled( "scp file.wav cli.makenot.work:upload/", Style::default().add_modifier(Modifier::BOLD), ), ])); frame.render_widget(empty, chunks[4]); } else { render_file_table(frame, app, chunks[4]); } // Status line (publish progress, errors, etc.) if let Some(ref status) = app.upload_status { let style = if status.starts_with("Error") { Style::default().fg(app.theme.status_danger) } else if status.starts_with("Published") { Style::default().fg(app.theme.status_success) } else { Style::default().fg(app.theme.status_warning) }; let status_line = Paragraph::new(Line::from(vec![ Span::raw(" "), Span::styled(status.as_str(), style), ])); frame.render_widget(status_line, chunks[5]); } // Keybindings let mut key_spans = vec![ Span::raw(" "), Span::styled("[j/k]", Style::default().add_modifier(Modifier::BOLD)), Span::raw(" Nav "), ]; if app.editing_field.is_some() { key_spans.extend([ Span::styled("[Enter]", Style::default().add_modifier(Modifier::BOLD)), Span::raw(" Save "), Span::styled("[Tab]", Style::default().add_modifier(Modifier::BOLD)), Span::raw(" Next field "), Span::styled("[Esc]", Style::default().add_modifier(Modifier::BOLD)), Span::raw(" Cancel"), ]); } else { if !app.staged_files.is_empty() { key_spans.extend([ Span::styled("[e]", Style::default().add_modifier(Modifier::BOLD)), Span::raw(" Edit "), Span::styled("[p]", Style::default().add_modifier(Modifier::BOLD)), Span::raw(" Publish "), Span::styled("[d]", Style::default().add_modifier(Modifier::BOLD)), Span::raw(" Delete "), ]); } key_spans.extend([ Span::styled("[r]", Style::default().add_modifier(Modifier::BOLD)), Span::raw(" Refresh "), Span::styled("[Esc]", Style::default().add_modifier(Modifier::BOLD)), Span::raw(" Back"), ]); } let keys = Paragraph::new(Line::from(key_spans)).style(Style::default().fg(app.theme.content_muted)); frame.render_widget(keys, chunks[6]); } fn render_storage_line(frame: &mut Frame, app: &App, area: ratatui::layout::Rect) { let text = if let Some(ref info) = app.storage_info { let used = staging::format_bytes(info.storage_used_bytes as u64); let max = staging::format_bytes(info.max_storage_bytes as u64); let tier_label = app .user .creator_tier .as_deref() .map_or("No tier", format::format_tier); if info.allows_file_uploads { format!(" Storage: {used} / {max} ({tier_label})") } else { format!(" {tier_label} -- file uploads not available") } } else { " Storage: --".to_string() }; frame.render_widget(Paragraph::new(text), area); } fn render_file_table(frame: &mut Frame, app: &App, area: Rect) { let area = super::indent(area, 2); let selected = app.selected_index; let rows: Vec> = app .staged_files .iter() .enumerate() .map(|(i, sf)| { // Check if user has edited metadata for this file let meta = app.file_metadata.get(i); let title = meta .and_then(|m| m.title.clone()) .unwrap_or_else(|| staging::derive_title(&sf.filename)); let project = meta .and_then(|m| m.project_name.as_deref()) .unwrap_or("[none]"); let price = meta.map_or_else( || "Free".to_string(), |m| format::format_price(m.price_cents, app.currency()), ); // Show edit indicator for editing field let editing_marker = if app.editing_field.is_some() && i == selected { "*" } else { " " }; vec![ Cell::new("File", format!("{editing_marker}{}", sf.filename)), Cell::new("Size", staging::format_bytes(sf.size)), Cell::new("Type", sf.classification.map_or("?", |c| c.item_type)), Cell::new("Title", title), Cell::new("Project", project), Cell::new("Price", price), ] }) .collect(); let style = TableStyle::from_theme(&app.theme); let table = table::table(&FILE_COLUMNS, &rows, &FILE_SIZING, &style, area.width); let mut state = TableState::default().with_selected(Some(app.selected_index)); frame.render_stateful_widget(table, area, &mut state); }