1use std::io::Read;
2use std::path::{Path, PathBuf};
3
4use crate::AssetFileEntry;
5use crate::error::PlatformError;
6
7use super::PlatformFuture;
8use super::clipboard::ClipboardService;
9use super::device::{Device, DeviceHardware};
10use super::file::FileService;
11use super::location::Location;
12use super::media_interaction::{MediaInteraction, MediaKind};
13use super::media_runtime::MediaRuntime;
14use super::network::Network;
15use super::secure_store::SecureStore;
16use super::share::ShareService;
17use super::ui::{SurfacePresenter, UIUpdate, UserFeedback};
18use super::update::UpdateService;
19use super::wifi::Wifi;
20
21pub const ACTIVATION_ENVELOPE: &str = "lxnotify:v1:";
33
34pub fn wrap_activation(token: &str) -> String {
36 format!("{ACTIVATION_ENVELOPE}{token}")
37}
38
39pub fn unwrap_activation(payload: &str) -> Option<&str> {
41 payload
42 .trim()
43 .strip_prefix(ACTIVATION_ENVELOPE)
44 .filter(|token| !token.is_empty())
45}
46
47#[derive(Debug, Clone)]
49pub struct LocalNotificationShow {
50 pub id: String,
51 pub title: String,
52 pub body: String,
53 pub activation_token: String,
58 pub deliver_at_ms: Option<u64>,
59 pub silent: bool,
60}
61
62#[derive(Debug, Clone)]
64pub struct DesktopBannerAction {
65 pub id: String,
66 pub label: String,
67 pub style: DesktopBannerActionStyle,
68}
69
70#[derive(Debug, Clone, Copy, PartialEq, Eq)]
71pub enum DesktopBannerActionStyle {
72 Default,
73 Primary,
74 Destructive,
75}
76
77impl DesktopBannerActionStyle {
78 pub fn as_str(self) -> &'static str {
79 match self {
80 Self::Default => "default",
81 Self::Primary => "primary",
82 Self::Destructive => "destructive",
83 }
84 }
85
86 pub fn parse(value: &str) -> Option<Self> {
87 match value {
88 "default" => Some(Self::Default),
89 "primary" => Some(Self::Primary),
90 "destructive" => Some(Self::Destructive),
91 _ => None,
92 }
93 }
94}
95
96#[derive(Debug, Clone, PartialEq, Eq, Default)]
98pub enum DesktopBannerBackground {
99 #[default]
100 System,
101 Light,
102 Dark,
103 Color {
104 r: u8,
105 g: u8,
106 b: u8,
107 a: u8,
108 },
109}
110
111#[derive(Debug, Clone)]
113pub struct DesktopBannerShow {
114 pub id: String,
115 pub title: String,
116 pub body: String,
117 pub actions: Vec<DesktopBannerAction>,
118 pub timeout_ms: Option<u64>,
120 pub background: DesktopBannerBackground,
121}
122
123#[derive(Debug, Clone, PartialEq, Eq)]
125pub enum DesktopBannerOutcome {
126 Action { id: String, action: String },
127 Dismissed { id: String },
128 TimedOut { id: String },
129 Replaced { id: String },
130}
131
132impl DesktopBannerOutcome {
133 pub fn id(&self) -> &str {
134 match self {
135 Self::Action { id, .. }
136 | Self::Dismissed { id }
137 | Self::TimedOut { id }
138 | Self::Replaced { id } => id,
139 }
140 }
141
142 pub fn reason(&self) -> Option<&'static str> {
143 match self {
144 Self::Action { .. } => None,
145 Self::Dismissed { .. } => Some("dismissed"),
146 Self::TimedOut { .. } => Some("timeout"),
147 Self::Replaced { .. } => Some("replaced"),
148 }
149 }
150}
151
152#[derive(Debug, Clone, Copy, PartialEq, Eq)]
154pub enum LocalNotificationStatus {
155 Posted,
157 Scheduled,
159 Suppressed,
161}
162
163impl LocalNotificationStatus {
164 pub fn as_str(self) -> &'static str {
165 match self {
166 Self::Posted => "posted",
167 Self::Scheduled => "scheduled",
168 Self::Suppressed => "suppressed",
169 }
170 }
171
172 pub fn from_native(value: &str) -> Option<Self> {
174 match value {
175 "posted" => Some(Self::Posted),
176 "scheduled" => Some(Self::Scheduled),
177 "suppressed" => Some(Self::Suppressed),
178 _ => None,
179 }
180 }
181}
182
183#[derive(Debug, Clone, Copy, PartialEq, Eq)]
184pub enum AnimationType {
185 None = 0,
186 Forward = 1,
187 Backward = 2,
188}
189
190#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
191pub enum LxAppOpenMode {
192 #[default]
193 Normal = 0,
194 Panel = 1,
195}
196
197#[derive(Debug, Clone, Copy, PartialEq, Eq)]
198pub enum OpenUrlTarget {
199 External = 0,
200 SelfTarget = 1,
201 NewBrowserTab = 2,
203 AsideBrowser = 3,
206}
207
208impl OpenUrlTarget {
209 pub fn parse(raw: Option<&str>) -> Self {
210 match raw.map(|v| v.trim().to_ascii_lowercase()) {
211 Some(v) if v == "self" => Self::SelfTarget,
212 Some(v) if v == "new_browser_tab" => Self::NewBrowserTab,
213 Some(v) if v == "aside" => Self::AsideBrowser,
214 Some(v) if v == "external" => Self::External,
215 Some(v) => {
216 log::warn!("Invalid openURL target='{}', fallback to external", v);
217 Self::External
218 }
219 None => Self::External,
220 }
221 }
222}
223
224#[derive(Debug, Clone)]
225pub struct OpenUrlRequest {
226 pub owner_appid: String,
227 pub owner_session_id: u64,
228 pub url: String,
229 pub target: OpenUrlTarget,
230 pub want_tab_id: bool,
234}
235
236#[derive(Debug, Clone, Default, PartialEq, Eq)]
239pub struct OpenUrlResult {
240 pub tab_id: Option<String>,
241}
242
243#[derive(Debug, Clone, Copy, PartialEq, Eq)]
244pub enum BuiltinBrowserPage {
245 Downloads = 1,
246}
247
248impl From<i32> for AnimationType {
249 fn from(value: i32) -> Self {
250 match value {
251 1 => AnimationType::Forward,
252 2 => AnimationType::Backward,
253 _ => AnimationType::None,
254 }
255 }
256}
257
258pub trait AppRuntime:
259 Send
260 + Sync
261 + MediaInteraction
262 + MediaRuntime
263 + Network
264 + SurfacePresenter
265 + ClipboardService
266 + Device
267 + DeviceHardware
268 + SecureStore
269 + ShareService
270 + FileService
271 + Location
272 + UIUpdate
273 + UpdateService
274 + UserFeedback
275 + Wifi
276 + 'static
277{
278 fn read_asset<'a>(&'a self, path: &str) -> Result<Box<dyn Read + 'a>, PlatformError>;
280
281 fn asset_dir_iter<'a>(
283 &'a self,
284 asset_dir: &str,
285 ) -> Box<dyn Iterator<Item = Result<AssetFileEntry<'a>, PlatformError>> + 'a>;
286
287 fn app_data_dir(&self) -> PathBuf;
289
290 fn app_cache_dir(&self) -> PathBuf;
292
293 fn get_app_identifier(&self) -> Result<String, PlatformError>;
295
296 fn copy_album_media_to_file(
298 &self,
299 uri: &str,
300 dest_path: &Path,
301 kind: MediaKind,
302 ) -> Result<(), PlatformError> {
303 MediaRuntime::copy_album_media_to_file(self, uri, dest_path, kind)
304 }
305
306 fn get_system_locale(&self) -> &str;
308
309 fn show_lxapp(
315 &self,
316 appid: String,
317 title: String,
318 path: String,
319 webtag: String,
320 session_id: u64,
321 open_mode: LxAppOpenMode,
322 panel_id: String,
323 ) -> Result<(), PlatformError>;
324
325 fn request_lxapp_main_activation(&self, _appid: &str) {}
330
331 fn hide_lxapp(&self, appid: String, session_id: u64) -> Result<(), PlatformError>;
333
334 fn exit(&self) -> Result<(), PlatformError>;
336
337 fn set_tray_badge(&self, _text: &str) -> Result<bool, PlatformError> {
347 Ok(false)
348 }
349
350 fn set_tray_icon(&self, _icon: &str) -> Result<(), PlatformError> {
352 Ok(())
353 }
354
355 fn set_shell_sidebar_actions(
358 &self,
359 _items: &[lingxia_shell::ResolvedShellSidebarAction],
360 ) -> Result<(), PlatformError> {
361 Ok(())
362 }
363
364 fn set_shell_pins(&self, _items: &[lingxia_shell::ShellPin]) -> Result<(), PlatformError> {
367 Ok(())
368 }
369
370 fn set_control_session_indicator(&self, _active: bool) -> Result<(), PlatformError> {
374 Ok(())
375 }
376
377 fn set_tray_title(&self, _text: &str) -> Result<(), PlatformError> {
379 Ok(())
380 }
381
382 fn set_app_badge(&self, _text: &str) -> Result<bool, PlatformError> {
386 Ok(false)
387 }
388
389 fn autostart_is_enabled(&self) -> Result<bool, PlatformError> {
393 Err(PlatformError::NotSupported("autostart".to_string()))
394 }
395
396 fn autostart_set_enabled(&self, _enabled: bool) -> Result<(), PlatformError> {
398 Err(PlatformError::NotSupported("autostart".to_string()))
399 }
400
401 fn notification_permission(&self) -> Result<String, PlatformError> {
404 Err(PlatformError::NotSupported("notification".to_string()))
405 }
406
407 fn notification_request_permission(&self) -> Result<String, PlatformError> {
410 Err(PlatformError::NotSupported("notification".to_string()))
411 }
412
413 fn notification_show(
421 &self,
422 _request: &LocalNotificationShow,
423 ) -> Result<LocalNotificationStatus, PlatformError> {
424 Err(PlatformError::NotSupported("notification".to_string()))
425 }
426
427 fn notification_cancel(&self, _id: &str) -> Result<(), PlatformError> {
428 Err(PlatformError::NotSupported("notification".to_string()))
429 }
430
431 fn notification_cancel_all(&self) -> Result<(), PlatformError> {
432 Err(PlatformError::NotSupported("notification".to_string()))
433 }
434
435 fn banner_show(
437 &self,
438 _request: &DesktopBannerShow,
439 ) -> Result<DesktopBannerOutcome, PlatformError> {
440 Err(PlatformError::NotSupported("banner".to_string()))
441 }
442
443 fn banner_dismiss(&self, _id: &str) -> Result<(), PlatformError> {
445 Err(PlatformError::NotSupported("banner".to_string()))
446 }
447
448 fn set_tray_menu(&self, _items_json: &str) -> Result<(), PlatformError> {
452 Ok(())
453 }
454
455 fn set_tray_visible(&self, _visible: bool) -> Result<(), PlatformError> {
457 Ok(())
458 }
459
460 fn set_tray_click_intercept(&self, _intercept: bool) -> Result<(), PlatformError> {
464 Ok(())
465 }
466
467 fn navigate(
473 &self,
474 appid: String,
475 path: String,
476 webtag: String,
477 animation_type: AnimationType,
478 ) -> Result<(), PlatformError>;
479
480 fn open_url(&self, req: OpenUrlRequest) -> Result<OpenUrlResult, PlatformError>;
482
483 fn close_browser_tab(&self, _tab_id: &str) -> Result<(), PlatformError> {
487 Err(PlatformError::NotSupported("browser tab".to_string()))
488 }
489
490 fn activate_browser_tab(&self, _tab_id: String) -> PlatformFuture {
492 Box::pin(async { Err(PlatformError::NotSupported("browser tab".to_string())) })
493 }
494
495 fn open_builtin_browser_page(&self, _page: BuiltinBrowserPage) -> Result<(), PlatformError> {
496 Err(PlatformError::NotSupported(
497 "built-in browser pages".to_string(),
498 ))
499 }
500}
501
502#[cfg(test)]
503mod envelope_tests {
504 use super::{ACTIVATION_ENVELOPE, unwrap_activation, wrap_activation};
505
506 #[test]
507 fn the_envelope_round_trips_and_rejects_anything_else() {
508 assert_eq!(unwrap_activation(&wrap_activation("abc")), Some("abc"));
509 assert_eq!(unwrap_activation("https://example.com/x"), None);
510 assert_eq!(unwrap_activation(ACTIVATION_ENVELOPE), None);
511 assert_eq!(unwrap_activation(""), None);
512 }
513}
514
515#[cfg(test)]
516mod tests {
517 use super::OpenUrlTarget;
518
519 #[test]
520 fn parse_supports_new_browser_tab() {
521 assert_eq!(
522 OpenUrlTarget::parse(Some("new_browser_tab")),
523 OpenUrlTarget::NewBrowserTab
524 );
525 }
526
527 #[test]
528 fn parse_unknown_falls_back_to_external() {
529 assert_eq!(
530 OpenUrlTarget::parse(Some("foobar")),
531 OpenUrlTarget::External
532 );
533 }
534}