Skip to main content

max / makenotwork

6.9 KB · 210 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(" Save "),
104 Span::styled("[Tab]", Style::default().add_modifier(Modifier::BOLD)),
105 Span::raw(" Next field "),
106 Span::styled("[Esc]", Style::default().add_modifier(Modifier::BOLD)),
107 Span::raw(" Cancel"),
108 ]);
109 } else {
110 if !app.staged_files.is_empty() {
111 key_spans.extend([
112 Span::styled("[e]", Style::default().add_modifier(Modifier::BOLD)),
113 Span::raw(" Edit "),
114 Span::styled("[p]", Style::default().add_modifier(Modifier::BOLD)),
115 Span::raw(" Publish "),
116 Span::styled("[d]", Style::default().add_modifier(Modifier::BOLD)),
117 Span::raw(" Delete "),
118 ]);
119 }
120 key_spans.extend([
121 Span::styled("[r]", Style::default().add_modifier(Modifier::BOLD)),
122 Span::raw(" Refresh "),
123 Span::styled("[Esc]", Style::default().add_modifier(Modifier::BOLD)),
124 Span::raw(" Back"),
125 ]);
126 }
127
128 let keys = Paragraph::new(Line::from(key_spans)).style(Style::default().fg(Color::DarkGray));
129 frame.render_widget(keys, chunks[6]);
130 }
131
132 fn render_storage_line(frame: &mut Frame, app: &App, area: ratatui::layout::Rect) {
133 let text = if let Some(ref info) = app.storage_info {
134 let used = staging::format_bytes(info.storage_used_bytes as u64);
135 let max = staging::format_bytes(info.max_storage_bytes as u64);
136 let tier_label = app
137 .user
138 .creator_tier
139 .as_deref()
140 .map(format::format_tier)
141 .unwrap_or("No tier");
142
143 if info.allows_file_uploads {
144 format!(" Storage: {} / {} ({})", used, max, tier_label)
145 } else {
146 format!(" {} -- file uploads not available", tier_label)
147 }
148 } else {
149 " Storage: --".to_string()
150 };
151
152 frame.render_widget(Paragraph::new(text), area);
153 }
154
155 fn render_file_table(frame: &mut Frame, app: &App, area: ratatui::layout::Rect) {
156 let selected = app.selected_index;
157 let rows: Vec<Row> = app
158 .staged_files
159 .iter()
160 .enumerate()
161 .map(|(i, sf)| {
162 let file_type = sf
163 .classification
164 .map(|c| c.item_type)
165 .unwrap_or("?");
166
167 // Check if user has edited metadata for this file
168 let meta = app.file_metadata.get(i);
169 let title = meta
170 .and_then(|m| m.title.clone())
171 .unwrap_or_else(|| staging::derive_title(&sf.filename));
172 let project = meta
173 .and_then(|m| m.project_name.as_deref())
174 .unwrap_or("[none]");
175 let price = meta
176 .map(|m| format::format_price(m.price_cents))
177 .unwrap_or_else(|| "Free".to_string());
178
179 // Show edit indicator for editing field
180 let editing_marker = if app.editing_field.is_some() && i == selected {
181 "*"
182 } else {
183 " "
184 };
185
186 Row::new(vec![
187 format!("{}{}", editing_marker, sf.filename),
188 staging::format_bytes(sf.size),
189 file_type.to_string(),
190 title.to_string(),
191 project.to_string(),
192 price,
193 ])
194 .style(widgets::selected_style(i, Some(app.selected_index)))
195 })
196 .collect();
197
198 let widths = [
199 Constraint::Min(16),
200 Constraint::Length(10),
201 Constraint::Length(8),
202 Constraint::Length(16),
203 Constraint::Length(14),
204 Constraint::Length(8),
205 ];
206
207 widgets::render_table(frame, area, &[" File", "Size", "Type", "Title", "Project", "Price"], &widths, rows);
208 }
209
210