1use std::path::PathBuf;
113
114use CommonLibrary::{
115 Error::CommonError::CommonError,
116 UserInterface::{
117 DTO::{
118 InputBoxOptionsDTO::InputBoxOptionsDTO,
119 MessageSeverity::MessageSeverity,
120 OpenDialogOptionsDTO::OpenDialogOptionsDTO,
121 QuickPickItemDTO::QuickPickItemDTO,
122 QuickPickOptionsDTO::QuickPickOptionsDTO,
123 SaveDialogOptionsDTO::SaveDialogOptionsDTO,
124 },
125 UserInterfaceProvider::UserInterfaceProvider,
126 },
127};
128use async_trait::async_trait;
129use serde::Serialize;
130use serde_json::{Value, json};
131use tauri::Emitter;
132use tauri_plugin_dialog::{DialogExt, FilePath};
133use tokio::time::{Duration, timeout};
134use uuid::Uuid;
135
136use super::{MountainEnvironment::MountainEnvironment, Utility};
137use crate::dev_log;
138
139#[derive(Serialize, Clone)]
140struct UserInterfaceRequest<TPayload:Serialize + Clone> {
141 pub RequestIdentifier:String,
142
143 pub Payload:TPayload,
144}
145
146#[async_trait]
147impl UserInterfaceProvider for MountainEnvironment {
148 async fn ShowMessage(
151 &self,
152
153 Severity:MessageSeverity,
154
155 Message:String,
156
157 Options:Option<Value>,
158 ) -> Result<Option<String>, CommonError> {
159 dev_log!("window", "[UserInterfaceProvider] Showing interactive message: {}", Message);
160
161 let Payload = json!({ "Severity": Severity, "Message": Message, "Options": Options });
162
163 let ResponseValue = SendUserInterfaceRequest(self, "sky://ui/show-message-request", Payload).await?;
164
165 Ok(ResponseValue.as_str().map(String::from))
166 }
167
168 async fn ShowOpenDialog(&self, Options:Option<OpenDialogOptionsDTO>) -> Result<Option<Vec<PathBuf>>, CommonError> {
171 dev_log!("window", "[UserInterfaceProvider] Showing open dialog.");
172
173 let mut Builder = self.ApplicationHandle.dialog().file();
174
175 let (CanSelectMany, CanSelectFolders, CanSelectFiles) = if let Some(ref opts) = Options {
176 if let Some(title) = &opts.Base.Title {
177 Builder = Builder.set_title(title);
178 }
179
180 if let Some(path_string) = &opts.Base.DefaultPath {
181 Builder = Builder.set_directory(PathBuf::from(path_string));
182 }
183
184 if let Some(filters) = &opts.Base.FilterList {
185 for filter in filters {
186 let extensions:Vec<&str> = filter.ExtensionList.iter().map(AsRef::as_ref).collect();
187
188 Builder = Builder.add_filter(&filter.Name, &extensions);
189 }
190 }
191
192 (
193 opts.CanSelectMany.unwrap_or(false),
194 opts.CanSelectFolders.unwrap_or(false),
195 opts.CanSelectFiles.unwrap_or(true),
196 )
197 } else {
198 (false, false, true)
199 };
200
201 let PickedPaths:Option<Vec<FilePath>> = tokio::task::spawn_blocking(move || {
202 if CanSelectFolders {
203 if CanSelectMany {
204 Builder.blocking_pick_folders()
205 } else {
206 Builder.blocking_pick_folder().map(|p| vec![p])
207 }
208 } else if CanSelectFiles {
209 if CanSelectMany {
210 Builder.blocking_pick_files()
211 } else {
212 Builder.blocking_pick_file().map(|p| vec![p])
213 }
214 } else {
215 None
216 }
217 })
218 .await
219 .map_err(|Error| CommonError::UserInterfaceInteraction { Reason:format!("Dialog task failed: {}", Error) })?;
220
221 Ok(PickedPaths.map(|paths| paths.into_iter().filter_map(|p| p.into_path().ok()).collect()))
222 }
223
224 async fn ShowSaveDialog(&self, Options:Option<SaveDialogOptionsDTO>) -> Result<Option<PathBuf>, CommonError> {
226 dev_log!("window", "[UserInterfaceProvider] Showing save dialog.");
227
228 let mut Builder = self.ApplicationHandle.dialog().file();
229
230 if let Some(options) = Options {
231 if let Some(title) = options.Base.Title {
232 Builder = Builder.set_title(title);
233 }
234
235 if let Some(path_string) = options.Base.DefaultPath {
236 let path = PathBuf::from(path_string);
237
238 if let Some(parent) = path.parent() {
239 Builder = Builder.set_directory(parent);
240 }
241
242 if let Some(file_name) = path.file_name().and_then(|n| n.to_str()) {
243 Builder = Builder.set_file_name(file_name);
244 }
245 }
246
247 if let Some(filters) = options.Base.FilterList {
248 for filter in filters {
249 let extensions:Vec<&str> = filter.ExtensionList.iter().map(AsRef::as_ref).collect();
250
251 Builder = Builder.add_filter(filter.Name, &extensions);
252 }
253 }
254 }
255
256 let PickedFile = tokio::task::spawn_blocking(move || Builder.blocking_save_file())
257 .await
258 .map_err(|Error| {
259 CommonError::UserInterfaceInteraction { Reason:format!("Dialog task failed: {}", Error) }
260 })?;
261
262 Ok(PickedFile.and_then(|p| p.into_path().ok()))
263 }
264
265 async fn ShowQuickPick(
267 &self,
268
269 Items:Vec<QuickPickItemDTO>,
270
271 Options:Option<QuickPickOptionsDTO>,
272 ) -> Result<Option<Vec<String>>, CommonError> {
273 dev_log!(
274 "window",
275 "[UserInterfaceProvider] Showing quick pick with {} items.",
276 Items.len()
277 );
278
279 let Payload = json!({ "Items": Items, "Options": Options });
280
281 let ResponseValue = SendUserInterfaceRequest(self, "sky://ui/show-quick-pick-request", Payload).await?;
282
283 serde_json::from_value(ResponseValue).map_err(|Error| {
284 CommonError::SerializationError {
285 Description:format!("Failed to deserialize quick pick response: {}", Error),
286 }
287 })
288 }
289
290 async fn ShowInputBox(&self, Options:Option<InputBoxOptionsDTO>) -> Result<Option<String>, CommonError> {
292 dev_log!("window", "[UserInterfaceProvider] Showing input box.");
293
294 let ResponseValue = SendUserInterfaceRequest(self, "sky://ui/show-input-box-request", Options).await?;
295
296 serde_json::from_value(ResponseValue).map_err(|Error| {
297 CommonError::SerializationError {
298 Description:format!("Failed to deserialize input box response: {}", Error),
299 }
300 })
301 }
302}
303
304async fn SendUserInterfaceRequest<TPayload:Serialize + Clone>(
309 Environment:&MountainEnvironment,
310
311 EventName:&str,
312
313 Payload:TPayload,
314) -> Result<Value, CommonError> {
315 let RequestIdentifier = Uuid::new_v4().to_string();
316
317 let (Sender, Receiver) = tokio::sync::oneshot::channel();
318
319 {
320 let mut PendingRequestsGuard = Environment
321 .ApplicationState
322 .UI
323 .PendingUserInterfaceRequest
324 .lock()
325 .map_err(Utility::MapApplicationStateLockErrorToCommonError)?;
326
327 PendingRequestsGuard.insert(RequestIdentifier.clone(), Sender);
328 }
329
330 let EventPayload = UserInterfaceRequest { RequestIdentifier:RequestIdentifier.clone(), Payload };
331
332 Environment.ApplicationHandle.emit(EventName, EventPayload).map_err(|Error| {
333 CommonError::UserInterfaceInteraction {
334 Reason:format!("Failed to emit UI request '{}': {}", EventName, Error.to_string()),
335 }
336 })?;
337
338 match timeout(Duration::from_secs(300), Receiver).await {
339 Ok(Ok(Ok(Value))) => Ok(Value),
340
341 Ok(Ok(Err(Error))) => Err(Error),
342
343 Ok(Err(_)) => {
344 Err(CommonError::UserInterfaceInteraction {
345 Reason:format!("UI response channel closed for request ID: {}", RequestIdentifier),
346 })
347 },
348
349 Err(_) => {
350 dev_log!(
351 "window",
352 "warn: [UserInterfaceProvider] UI request '{}' with ID {} timed out.",
353 EventName,
354 RequestIdentifier
355 );
356
357 let mut Guard = Environment
358 .ApplicationState
359 .UI
360 .PendingUserInterfaceRequest
361 .lock()
362 .map_err(Utility::MapApplicationStateLockErrorToCommonError)?;
363
364 Guard.remove(&RequestIdentifier);
365
366 Err(CommonError::UserInterfaceInteraction {
367 Reason:format!("UI request timed out for request ID: {}", RequestIdentifier),
368 })
369 },
370 }
371}