Skip to main content

max / mnw-cli

6.8 KB · 208 lines History Blame Raw
1 //! Upload screen — staged files, metadata editing, publish flow.
2
3 use ratatui::Frame;
4 use ratatui::layout::{Constraint, Layout};
5 use ratatui::style::{Color, Modifier, Style};
6 use ratatui::text::{Line, Span};
7 use ratatui::widgets::{Block, Borders, Paragraph, Row};
8
9 use crate::format;
10 use crate::staging;
11
12 use super::App;
13 use super::widgets;
14
15 pub fn render(frame: &mut Frame, app: &App) {
16 let area = frame.area();
17
18 let title = Line::from(vec![
19 Span::styled(" Makenot.work ", Style::default().add_modifier(Modifier::BOLD)),
20 Span::raw(" ── "),
21 Span::styled("Upload", Style::default().add_modifier(Modifier::BOLD)),
22 Span::raw(" "),
23 ]);
24
25 let block = Block::default()
26 .title(title)
27 .borders(Borders::ALL)
28 .border_style(Style::default().fg(Color::Gray));
29
30 let inner = block.inner(area);
31 frame.render_widget(block, area);
32
33 let chunks = Layout::vertical([
34 Constraint::Length(1), // spacer
35 Constraint::Length(1), // storage info
36 Constraint::Length(1), // spacer
37 Constraint::Length(1), // section header
38 Constraint::Min(3), // file list
39 Constraint::Length(1), // status line
40 Constraint::Length(1), // keybindings
41 ])
42 .split(inner);
43
44 // Storage info
45 render_storage_line(frame, app, chunks[1]);
46
47 // Section header
48 let file_count = app.staged_files.len();
49 let header = Paragraph::new(Line::from(vec![
50 Span::raw(" "),
51 Span::styled("Staged Files", Style::default().add_modifier(Modifier::BOLD)),
52 if file_count == 0 {
53 Span::raw("")
54 } else {
55 Span::raw(format!(" ({})", file_count))
56 },
57 ]));
58 frame.render_widget(header, chunks[3]);
59
60 // File list
61 if app.loading {
62 let loading = Paragraph::new(" Loading...");
63 frame.render_widget(loading, chunks[4]);
64 } else if app.staged_files.is_empty() {
65 let empty = Paragraph::new(Line::from(vec![
66 Span::raw(" No staged files. Upload with: "),
67 Span::styled(
68 "scp file.wav cli.makenot.work:upload/",
69 Style::default().add_modifier(Modifier::BOLD),
70 ),
71 ]));
72 frame.render_widget(empty, chunks[4]);
73 } else {
74 render_file_table(frame, app, chunks[4]);
75 }
76
77 // Status line (publish progress, errors, etc.)
78 if let Some(ref status) = app.upload_status {
79 let style = if status.starts_with("Error") {
80 Style::default().fg(Color::Red)
81 } else if status.starts_with("Published") {
82 Style::default().fg(Color::Green)
83 } else {
84 Style::default().fg(Color::Yellow)
85 };
86 let status_line = Paragraph::new(Line::from(vec![
87 Span::raw(" "),
88 Span::styled(status.as_str(), style),
89 ]));
90 frame.render_widget(status_line, chunks[5]);
91 }
92
93 // Keybindings
94 let mut key_spans = vec![
95 Span::raw(" "),
96 Span::styled("[j/k]", Style::default().add_modifier(Modifier::BOLD)),
97 Span::raw(" Nav "),
98 ];
99
100 if app.editing_field.is_some() {
101 key_spans.extend([
102 Span::styled("[Enter]", Style::default().add_modifier(Modifier::BOLD)),
103 Span::raw(" Confirm "),
104 Span::styled("[Esc]", Style::default().add_modifier(Modifier::BOLD)),
105 Span::raw(" Cancel"),
106 ]);
107 } else {
108 if !app.staged_files.is_empty() {
109 key_spans.extend([
110 Span::styled("[e]", Style::default().add_modifier(Modifier::BOLD)),
111 Span::raw(" Edit "),
112 Span::styled("[p]", Style::default().add_modifier(Modifier::BOLD)),
113 Span::raw(" Publish "),
114 Span::styled("[d]", Style::default().add_modifier(Modifier::BOLD)),
115 Span::raw(" Delete "),
116 ]);
117 }
118 key_spans.extend([
119 Span::styled("[r]", Style::default().add_modifier(Modifier::BOLD)),
120 Span::raw(" Refresh "),
121 Span::styled("[Esc]", Style::default().add_modifier(Modifier::BOLD)),
122 Span::raw(" Back"),
123 ]);
124 }
125
126 let keys = Paragraph::new(Line::from(key_spans)).style(Style::default().fg(Color::DarkGray));
127 frame.render_widget(keys, chunks[6]);
128 }
129
130 fn render_storage_line(frame: &mut Frame, app: &App, area: ratatui::layout::Rect) {
131 let text = if let Some(ref info) = app.storage_info {
132 let used = staging::format_bytes(info.storage_used_bytes as u64);
133 let max = staging::format_bytes(info.max_storage_bytes as u64);
134 let tier_label = app
135 .user
136 .creator_tier
137 .as_deref()
138 .map(format::format_tier)
139 .unwrap_or("No tier");
140
141 if info.allows_file_uploads {
142 format!(" Storage: {} / {} ({})", used, max, tier_label)
143 } else {
144 format!(" {} -- file uploads not available", tier_label)
145 }
146 } else {
147 " Storage: --".to_string()
148 };
149
150 frame.render_widget(Paragraph::new(text), area);
151 }
152
153 fn render_file_table(frame: &mut Frame, app: &App, area: ratatui::layout::Rect) {
154 let selected = app.selected_index;
155 let rows: Vec<Row> = app
156 .staged_files
157 .iter()
158 .enumerate()
159 .map(|(i, sf)| {
160 let file_type = sf
161 .classification
162 .map(|c| c.item_type)
163 .unwrap_or("?");
164
165 // Check if user has edited metadata for this file
166 let meta = app.file_metadata.get(i);
167 let title = meta
168 .and_then(|m| m.title.clone())
169 .unwrap_or_else(|| staging::derive_title(&sf.filename));
170 let project = meta
171 .and_then(|m| m.project_name.as_deref())
172 .unwrap_or("[none]");
173 let price = meta
174 .map(|m| format::format_price(m.price_cents))
175 .unwrap_or_else(|| "Free".to_string());
176
177 // Show edit indicator for editing field
178 let editing_marker = if app.editing_field.is_some() && i == selected {
179 "*"
180 } else {
181 " "
182 };
183
184 Row::new(vec![
185 format!("{}{}", editing_marker, sf.filename),
186 staging::format_bytes(sf.size),
187 file_type.to_string(),
188 title.to_string(),
189 project.to_string(),
190 price,
191 ])
192 .style(widgets::selected_style(i, Some(app.selected_index)))
193 })
194 .collect();
195
196 let widths = [
197 Constraint::Min(16),
198 Constraint::Length(10),
199 Constraint::Length(8),
200 Constraint::Length(16),
201 Constraint::Length(14),
202 Constraint::Length(8),
203 ];
204
205 widgets::render_table(frame, area, &[" File", "Size", "Type", "Title", "Project", "Price"], &widths, rows);
206 }
207
208