Skip to main content

max / makenotwork

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