Skip to main content

max / makenotwork

11.1 KB · 414 lines History Blame Raw
1 //! Async data loading functions and the publish flow.
2
3 use tokio::sync::mpsc;
4
5 use crate::api::{CreatorStats, MnwApiClient};
6 use crate::staging;
7
8 use super::{AppEvent, DataPayload};
9
10 pub(super) async fn load_home_data(api: &MnwApiClient, user_id: &str, tx: &mpsc::Sender<AppEvent>) {
11 let projects = api.get_projects(user_id).await.unwrap_or_else(|e| {
12 tracing::warn!(error = %e, "failed to load projects");
13 Vec::new()
14 });
15 let stats = api.get_stats(user_id, "30d").await.unwrap_or_else(|e| {
16 tracing::warn!(error = %e, "failed to load stats");
17 CreatorStats {
18 current_revenue_cents: 0,
19 previous_revenue_cents: 0,
20 current_sales: 0,
21 previous_sales: 0,
22 current_followers: 0,
23 previous_followers: 0,
24 total_projects: 0,
25 total_items: 0,
26 }
27 });
28
29 let _ = tx
30 .send(AppEvent::DataLoaded(DataPayload::Home { projects, stats }))
31 .await;
32 }
33
34 pub(super) async fn load_project_items(
35 api: &MnwApiClient,
36 project_id: &str,
37 user_id: &str,
38 tx: &mpsc::Sender<AppEvent>,
39 ) {
40 let items = api
41 .get_project_items(project_id, user_id)
42 .await
43 .unwrap_or_else(|e| {
44 tracing::warn!(error = %e, %project_id, "failed to load project items");
45 Vec::new()
46 });
47
48 let _ = tx
49 .send(AppEvent::DataLoaded(DataPayload::ProjectItems { items }))
50 .await;
51 }
52
53 pub(super) async fn load_staged_files(
54 staging_dir: &std::path::Path,
55 api: &MnwApiClient,
56 user_id: &str,
57 tx: &mpsc::Sender<AppEvent>,
58 ) {
59 let files = staging::list_staged_files(staging_dir).await;
60 let storage = match api.get_storage_info(user_id).await {
61 Ok(s) => Some(s),
62 Err(e) => {
63 tracing::warn!(error = %e, "failed to load storage info");
64 None
65 }
66 };
67
68 let _ = tx
69 .send(AppEvent::DataLoaded(DataPayload::StagedFiles {
70 files,
71 storage,
72 }))
73 .await;
74 }
75
76 pub(super) async fn load_item_detail(
77 api: &MnwApiClient,
78 user_id: &str,
79 item_id: &str,
80 tx: &mpsc::Sender<AppEvent>,
81 ) {
82 let detail = api.get_item_detail(user_id, item_id).await;
83 let versions = api.get_item_versions(user_id, item_id).await;
84 let tags = api
85 .list_item_tags(user_id, item_id)
86 .await
87 .unwrap_or_default();
88
89 match detail {
90 Ok(detail) => {
91 let versions = versions.unwrap_or_default();
92 let _ = tx
93 .send(AppEvent::DataLoaded(DataPayload::ItemDetail {
94 detail,
95 versions,
96 }))
97 .await;
98 let _ = tx
99 .send(AppEvent::DataLoaded(DataPayload::ItemTags { tags }))
100 .await;
101 }
102 Err(e) => {
103 let _ = tx
104 .send(AppEvent::DataLoaded(DataPayload::ItemActionError {
105 error: e.to_string(),
106 }))
107 .await;
108 }
109 }
110 }
111
112 pub(super) async fn load_collections(
113 api: &MnwApiClient,
114 user_id: &str,
115 tx: &mpsc::Sender<AppEvent>,
116 ) {
117 match api.list_collections(user_id).await {
118 Ok(collections) => {
119 let _ = tx
120 .send(AppEvent::DataLoaded(DataPayload::CollectionsList {
121 collections,
122 }))
123 .await;
124 }
125 Err(e) => {
126 let _ = tx
127 .send(AppEvent::DataLoaded(DataPayload::GenericError {
128 error: e.to_string(),
129 }))
130 .await;
131 }
132 }
133 }
134
135 pub(super) async fn load_tiers(
136 api: &MnwApiClient,
137 user_id: &str,
138 project_id: &str,
139 tx: &mpsc::Sender<AppEvent>,
140 ) {
141 match api.list_tiers(user_id, project_id).await {
142 Ok(tiers) => {
143 let _ = tx
144 .send(AppEvent::DataLoaded(DataPayload::TiersList { tiers }))
145 .await;
146 }
147 Err(e) => {
148 let _ = tx
149 .send(AppEvent::DataLoaded(DataPayload::GenericError {
150 error: e.to_string(),
151 }))
152 .await;
153 }
154 }
155 }
156
157 pub(super) async fn search_tags(api: &MnwApiClient, query: &str, tx: &mpsc::Sender<AppEvent>) {
158 match api.search_tags(query).await {
159 Ok(results) => {
160 let _ = tx
161 .send(AppEvent::DataLoaded(DataPayload::TagSearchResults {
162 results,
163 }))
164 .await;
165 }
166 Err(_) => {
167 let _ = tx
168 .send(AppEvent::DataLoaded(DataPayload::TagSearchResults {
169 results: vec![],
170 }))
171 .await;
172 }
173 }
174 }
175
176 pub(super) async fn load_blog_posts(
177 api: &MnwApiClient,
178 user_id: &str,
179 project_id: &str,
180 tx: &mpsc::Sender<AppEvent>,
181 ) {
182 let posts = api
183 .list_blog_posts(user_id, project_id)
184 .await
185 .unwrap_or_else(|e| {
186 tracing::warn!(error = %e, %project_id, "failed to load blog posts");
187 Vec::new()
188 });
189 let _ = tx
190 .send(AppEvent::DataLoaded(DataPayload::BlogPosts { posts }))
191 .await;
192 }
193
194 pub(super) async fn load_promo_codes(
195 api: &MnwApiClient,
196 user_id: &str,
197 tx: &mpsc::Sender<AppEvent>,
198 ) {
199 let codes = api.list_promo_codes(user_id).await.unwrap_or_else(|e| {
200 tracing::warn!(error = %e, "failed to load promo codes");
201 Vec::new()
202 });
203 let _ = tx
204 .send(AppEvent::DataLoaded(DataPayload::PromoCodes { codes }))
205 .await;
206 }
207
208 pub(super) async fn load_license_keys(
209 api: &MnwApiClient,
210 user_id: &str,
211 item_id: &str,
212 tx: &mpsc::Sender<AppEvent>,
213 ) {
214 let keys = api
215 .list_license_keys(user_id, item_id)
216 .await
217 .unwrap_or_else(|e| {
218 tracing::warn!(error = %e, %item_id, "failed to load license keys");
219 Vec::new()
220 });
221 let _ = tx
222 .send(AppEvent::DataLoaded(DataPayload::LicenseKeys { keys }))
223 .await;
224 }
225
226 pub(super) async fn load_analytics(
227 api: &MnwApiClient,
228 user_id: &str,
229 range: &str,
230 tx: &mpsc::Sender<AppEvent>,
231 ) {
232 match api.get_analytics(user_id, range).await {
233 Ok(data) => {
234 let _ = tx
235 .send(AppEvent::DataLoaded(DataPayload::Analytics { data }))
236 .await;
237 }
238 Err(e) => {
239 let _ = tx
240 .send(AppEvent::DataLoaded(DataPayload::GenericError {
241 error: e.to_string(),
242 }))
243 .await;
244 }
245 }
246 }
247
248 pub(super) async fn load_transactions(
249 api: &MnwApiClient,
250 user_id: &str,
251 tx: &mpsc::Sender<AppEvent>,
252 ) {
253 let txs = api.get_transactions(user_id).await.unwrap_or_else(|e| {
254 tracing::warn!(error = %e, "failed to load transactions");
255 Vec::new()
256 });
257 let _ = tx
258 .send(AppEvent::DataLoaded(DataPayload::Transactions { txs }))
259 .await;
260 }
261
262 pub(super) async fn load_settings(api: &MnwApiClient, user_id: &str, tx: &mpsc::Sender<AppEvent>) {
263 let keys = api.list_ssh_keys(user_id).await.unwrap_or_else(|e| {
264 tracing::warn!(error = %e, "failed to load SSH keys");
265 Vec::new()
266 });
267 let storage = match api.get_storage_info(user_id).await {
268 Ok(s) => Some(s),
269 Err(e) => {
270 tracing::warn!(error = %e, "failed to load storage info for settings");
271 None
272 }
273 };
274 let _ = tx
275 .send(AppEvent::DataLoaded(DataPayload::Settings {
276 keys,
277 storage,
278 }))
279 .await;
280 }
281
282 /// Full publish flow: create item -> presign -> upload to S3 -> confirm -> delete staging file.
283 #[allow(clippy::too_many_arguments)]
284 pub(super) async fn publish_file(
285 api: &MnwApiClient,
286 user_id: &str,
287 project_id: &str,
288 title: &str,
289 item_type: &str,
290 file_type: &str,
291 filename: &str,
292 content_type: &str,
293 price_cents: i32,
294 file_path: &std::path::Path,
295 ) -> anyhow::Result<()> {
296 // Step 1: Create item
297 let item = api
298 .create_item(user_id, project_id, title, item_type, price_cents)
299 .await?;
300
301 // Steps 2+3: get an upload target and send the bytes. A large file goes
302 // through a multipart session (one part resident at a time, and the only
303 // path that clears S3's 5 GiB single-PUT ceiling); a small one keeps the
304 // simpler single presigned PUT.
305 let file_size = tokio::fs::metadata(file_path)
306 .await
307 .map_err(|e| anyhow::anyhow!("reading {}: {e}", file_path.display()))?
308 .len();
309
310 let s3_key = if file_size > crate::api::MULTIPART_THRESHOLD_BYTES {
311 api.upload_file_multipart(
312 &item.item_id,
313 file_type,
314 filename,
315 content_type,
316 file_path,
317 file_size,
318 |uploaded, total| {
319 tracing::debug!(uploaded, total, "multipart upload progress");
320 },
321 )
322 .await?
323 } else {
324 let presign = api
325 .presign_upload(user_id, &item.item_id, file_type, filename, content_type)
326 .await?;
327 api.upload_to_s3(
328 &presign.upload_url,
329 file_path,
330 content_type,
331 presign.cache_control.as_deref(),
332 )
333 .await?;
334 presign.s3_key
335 };
336
337 // Step 4: Confirm upload
338 api.confirm_upload(user_id, &item.item_id, file_type, &s3_key)
339 .await?;
340
341 // Step 5: Delete staging file
342 if let Err(e) = tokio::fs::remove_file(file_path).await {
343 tracing::warn!(error = %e, path = %file_path.display(), "failed to delete staging file after publish");
344 }
345
346 Ok(())
347 }
348
349 /// Bulk publish items.
350 pub(super) async fn bulk_publish(
351 api: &MnwApiClient,
352 user_id: &str,
353 item_ids: Vec<String>,
354 tx: &mpsc::Sender<AppEvent>,
355 ) {
356 let total = item_ids.len();
357 let mut ok = 0;
358 for id in &item_ids {
359 if api.publish_item(user_id, id).await.is_ok() {
360 ok += 1;
361 }
362 }
363 let msg = format!("Published {ok}/{total} items");
364 let _ = tx
365 .send(AppEvent::DataLoaded(DataPayload::BulkActionComplete {
366 message: msg,
367 }))
368 .await;
369 }
370
371 /// Bulk unpublish items.
372 pub(super) async fn bulk_unpublish(
373 api: &MnwApiClient,
374 user_id: &str,
375 item_ids: Vec<String>,
376 tx: &mpsc::Sender<AppEvent>,
377 ) {
378 let total = item_ids.len();
379 let mut ok = 0;
380 for id in &item_ids {
381 if api.unpublish_item(user_id, id).await.is_ok() {
382 ok += 1;
383 }
384 }
385 let msg = format!("Unpublished {ok}/{total} items");
386 let _ = tx
387 .send(AppEvent::DataLoaded(DataPayload::BulkActionComplete {
388 message: msg,
389 }))
390 .await;
391 }
392
393 /// Bulk delete items.
394 pub(super) async fn bulk_delete(
395 api: &MnwApiClient,
396 user_id: &str,
397 item_ids: Vec<String>,
398 tx: &mpsc::Sender<AppEvent>,
399 ) {
400 let total = item_ids.len();
401 let mut ok = 0;
402 for id in &item_ids {
403 if api.delete_item(user_id, id).await.is_ok() {
404 ok += 1;
405 }
406 }
407 let msg = format!("Deleted {ok}/{total} items");
408 let _ = tx
409 .send(AppEvent::DataLoaded(DataPayload::BulkActionComplete {
410 message: msg,
411 }))
412 .await;
413 }
414