Skip to main content

max / mnw-cli

6.9 KB · 266 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(
11 api: &MnwApiClient,
12 user_id: &str,
13 tx: &mpsc::Sender<AppEvent>,
14 ) {
15 let projects = api.get_projects(user_id).await.unwrap_or_else(|e| {
16 tracing::warn!(error = %e, "failed to load projects");
17 Vec::new()
18 });
19 let stats = api
20 .get_stats(user_id, "30d")
21 .await
22 .unwrap_or_else(|e| {
23 tracing::warn!(error = %e, "failed to load stats");
24 CreatorStats {
25 current_revenue_cents: 0,
26 previous_revenue_cents: 0,
27 current_sales: 0,
28 previous_sales: 0,
29 current_followers: 0,
30 previous_followers: 0,
31 total_projects: 0,
32 total_items: 0,
33 }
34 });
35
36 let _ = tx
37 .send(AppEvent::DataLoaded(DataPayload::Home { projects, stats }))
38 .await;
39 }
40
41 pub(super) async fn load_project_items(
42 api: &MnwApiClient,
43 project_id: &str,
44 user_id: &str,
45 tx: &mpsc::Sender<AppEvent>,
46 ) {
47 let items = api
48 .get_project_items(project_id, user_id)
49 .await
50 .unwrap_or_else(|e| {
51 tracing::warn!(error = %e, %project_id, "failed to load project items");
52 Vec::new()
53 });
54
55 let _ = tx
56 .send(AppEvent::DataLoaded(DataPayload::ProjectItems { items }))
57 .await;
58 }
59
60 pub(super) async fn load_staged_files(
61 staging_dir: &std::path::Path,
62 api: &MnwApiClient,
63 user_id: &str,
64 tx: &mpsc::Sender<AppEvent>,
65 ) {
66 let files = staging::list_staged_files(staging_dir).await;
67 let storage = match api.get_storage_info(user_id).await {
68 Ok(s) => Some(s),
69 Err(e) => {
70 tracing::warn!(error = %e, "failed to load storage info");
71 None
72 }
73 };
74
75 let _ = tx
76 .send(AppEvent::DataLoaded(DataPayload::StagedFiles {
77 files,
78 storage,
79 }))
80 .await;
81 }
82
83 pub(super) async fn load_item_detail(
84 api: &MnwApiClient,
85 user_id: &str,
86 item_id: &str,
87 tx: &mpsc::Sender<AppEvent>,
88 ) {
89 let detail = api.get_item_detail(user_id, item_id).await;
90 let versions = api.get_item_versions(user_id, item_id).await;
91
92 match detail {
93 Ok(detail) => {
94 let versions = versions.unwrap_or_default();
95 let _ = tx
96 .send(AppEvent::DataLoaded(DataPayload::ItemDetail {
97 detail,
98 versions,
99 }))
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_blog_posts(
113 api: &MnwApiClient,
114 user_id: &str,
115 project_id: &str,
116 tx: &mpsc::Sender<AppEvent>,
117 ) {
118 let posts = api
119 .list_blog_posts(user_id, project_id)
120 .await
121 .unwrap_or_else(|e| {
122 tracing::warn!(error = %e, %project_id, "failed to load blog posts");
123 Vec::new()
124 });
125 let _ = tx
126 .send(AppEvent::DataLoaded(DataPayload::BlogPosts { posts }))
127 .await;
128 }
129
130 pub(super) async fn load_promo_codes(
131 api: &MnwApiClient,
132 user_id: &str,
133 tx: &mpsc::Sender<AppEvent>,
134 ) {
135 let codes = api
136 .list_promo_codes(user_id)
137 .await
138 .unwrap_or_else(|e| {
139 tracing::warn!(error = %e, "failed to load promo codes");
140 Vec::new()
141 });
142 let _ = tx
143 .send(AppEvent::DataLoaded(DataPayload::PromoCodes { codes }))
144 .await;
145 }
146
147 pub(super) async fn load_license_keys(
148 api: &MnwApiClient,
149 user_id: &str,
150 item_id: &str,
151 tx: &mpsc::Sender<AppEvent>,
152 ) {
153 let keys = api
154 .list_license_keys(user_id, item_id)
155 .await
156 .unwrap_or_else(|e| {
157 tracing::warn!(error = %e, %item_id, "failed to load license keys");
158 Vec::new()
159 });
160 let _ = tx
161 .send(AppEvent::DataLoaded(DataPayload::LicenseKeys { keys }))
162 .await;
163 }
164
165 pub(super) async fn load_analytics(
166 api: &MnwApiClient,
167 user_id: &str,
168 range: &str,
169 tx: &mpsc::Sender<AppEvent>,
170 ) {
171 match api.get_analytics(user_id, range).await {
172 Ok(data) => {
173 let _ = tx
174 .send(AppEvent::DataLoaded(DataPayload::Analytics { data }))
175 .await;
176 }
177 Err(e) => {
178 let _ = tx
179 .send(AppEvent::DataLoaded(DataPayload::GenericError {
180 error: e.to_string(),
181 }))
182 .await;
183 }
184 }
185 }
186
187 pub(super) async fn load_transactions(
188 api: &MnwApiClient,
189 user_id: &str,
190 tx: &mpsc::Sender<AppEvent>,
191 ) {
192 let txs = api.get_transactions(user_id).await.unwrap_or_else(|e| {
193 tracing::warn!(error = %e, "failed to load transactions");
194 Vec::new()
195 });
196 let _ = tx
197 .send(AppEvent::DataLoaded(DataPayload::Transactions { txs }))
198 .await;
199 }
200
201 pub(super) async fn load_settings(
202 api: &MnwApiClient,
203 user_id: &str,
204 tx: &mpsc::Sender<AppEvent>,
205 ) {
206 let keys = api.list_ssh_keys(user_id).await.unwrap_or_else(|e| {
207 tracing::warn!(error = %e, "failed to load SSH keys");
208 Vec::new()
209 });
210 let storage = match api.get_storage_info(user_id).await {
211 Ok(s) => Some(s),
212 Err(e) => {
213 tracing::warn!(error = %e, "failed to load storage info for settings");
214 None
215 }
216 };
217 let _ = tx
218 .send(AppEvent::DataLoaded(DataPayload::Settings { keys, storage }))
219 .await;
220 }
221
222 /// Full publish flow: create item -> presign -> upload to S3 -> confirm -> delete staging file.
223 #[allow(clippy::too_many_arguments)]
224 pub(super) async fn publish_file(
225 api: &MnwApiClient,
226 user_id: &str,
227 project_id: &str,
228 title: &str,
229 item_type: &str,
230 file_type: &str,
231 filename: &str,
232 content_type: &str,
233 price_cents: i32,
234 file_path: &std::path::Path,
235 ) -> anyhow::Result<()> {
236 // Step 1: Create item
237 let item = api
238 .create_item(user_id, project_id, title, item_type, price_cents)
239 .await?;
240
241 // Step 2: Get presigned URL
242 let presign = api
243 .presign_upload(user_id, &item.item_id, file_type, filename, content_type)
244 .await?;
245
246 // Step 3: Upload to S3
247 api.upload_to_s3(
248 &presign.upload_url,
249 file_path,
250 content_type,
251 presign.cache_control.as_deref(),
252 )
253 .await?;
254
255 // Step 4: Confirm upload
256 api.confirm_upload(user_id, &item.item_id, file_type, &presign.s3_key)
257 .await?;
258
259 // Step 5: Delete staging file
260 if let Err(e) = tokio::fs::remove_file(file_path).await {
261 tracing::warn!(error = %e, path = %file_path.display(), "failed to delete staging file after publish");
262 }
263
264 Ok(())
265 }
266