use std::fs::{self, File};
use std::future::Future;
use std::io::Read;
use std::path::{Component, Path, PathBuf};
use std::sync::{Arc, Mutex};
use lingxia_shell::{ResolvedShellSidebarAction, ShellPin};
use lingxia_webview::WebTag;
use super::{file, not_supported, surface, ui_update};
use crate::AssetFileEntry;
use crate::error::PlatformError;
use crate::traits::app_runtime::{
AnimationType, AppRuntime, BuiltinBrowserPage, DesktopBannerOutcome, DesktopBannerShow,
LocalNotificationShow, LxAppOpenMode, OpenUrlRequest, OpenUrlResult,
};
use crate::traits::share::{ShareRequest, ShareResult, ShareService};
use crate::traits::stream_decoder::{VideoStreamDecoderHandle, VideoStreamDecoderManager};
const DEFAULT_APP_IDENTIFIER: &str = "app.lingxia.windows";
type WindowsAppExitHandler = Arc<dyn Fn() + Send + Sync>;
static WINDOWS_APP_EXIT_HANDLER: Mutex<Option<WindowsAppExitHandler>> = Mutex::new(None);
pub type WindowsSidebarActionsHandler =
Arc<dyn Fn(&[ResolvedShellSidebarAction]) -> bool + Send + Sync>;
static WINDOWS_SIDEBAR_ACTIONS_HANDLER: Mutex<Option<WindowsSidebarActionsHandler>> =
Mutex::new(None);
pub type WindowsBuiltinBrowserDownloadsHandler = Arc<dyn Fn() -> bool + Send + Sync>;
static WINDOWS_BUILTIN_BROWSER_DOWNLOADS_HANDLER: Mutex<
Option<WindowsBuiltinBrowserDownloadsHandler>,
> = Mutex::new(None);
pub type WindowsShellPinsHandler = Arc<dyn Fn(&[ShellPin]) -> bool + Send + Sync>;
static WINDOWS_SHELL_PINS_HANDLER: Mutex<Option<WindowsShellPinsHandler>> = Mutex::new(None);
pub type WindowsLxAppMainActivationHandler = Arc<dyn Fn(&str) + Send + Sync>;
static WINDOWS_LXAPP_MAIN_ACTIVATION_HANDLER: Mutex<Option<WindowsLxAppMainActivationHandler>> =
Mutex::new(None);
pub type WindowsLxAppHiddenHandler = Arc<dyn Fn(&str, u64) + Send + Sync>;
static WINDOWS_LXAPP_HIDDEN_HANDLER: Mutex<Option<WindowsLxAppHiddenHandler>> = Mutex::new(None);
pub fn set_windows_app_exit_handler(handler: WindowsAppExitHandler) {
if let Ok(mut slot) = WINDOWS_APP_EXIT_HANDLER.lock() {
*slot = Some(handler);
}
}
pub fn set_windows_sidebar_actions_handler(handler: WindowsSidebarActionsHandler) {
if let Ok(mut slot) = WINDOWS_SIDEBAR_ACTIONS_HANDLER.lock() {
*slot = Some(handler);
}
}
pub fn set_windows_builtin_browser_downloads_handler(
handler: WindowsBuiltinBrowserDownloadsHandler,
) {
if let Ok(mut slot) = WINDOWS_BUILTIN_BROWSER_DOWNLOADS_HANDLER.lock() {
*slot = Some(handler);
}
}
pub fn set_windows_shell_pins_handler(handler: WindowsShellPinsHandler) {
if let Ok(mut slot) = WINDOWS_SHELL_PINS_HANDLER.lock() {
*slot = Some(handler);
}
}
pub fn set_windows_lxapp_main_activation_handler(handler: WindowsLxAppMainActivationHandler) {
if let Ok(mut slot) = WINDOWS_LXAPP_MAIN_ACTIVATION_HANDLER.lock() {
*slot = Some(handler);
}
}
pub fn set_windows_lxapp_hidden_handler(handler: WindowsLxAppHiddenHandler) {
if let Ok(mut slot) = WINDOWS_LXAPP_HIDDEN_HANDLER.lock() {
*slot = Some(handler);
}
}
fn invoke_windows_sidebar_actions_handler(items: &[ResolvedShellSidebarAction]) -> bool {
WINDOWS_SIDEBAR_ACTIONS_HANDLER
.lock()
.ok()
.and_then(|slot| slot.clone())
.is_none_or(|handler| handler(items))
}
fn invoke_windows_builtin_browser_downloads_handler() -> bool {
WINDOWS_BUILTIN_BROWSER_DOWNLOADS_HANDLER
.lock()
.ok()
.and_then(|slot| slot.clone())
.is_some_and(|handler| handler())
}
fn is_windows_builtin_browser_downloads(page: BuiltinBrowserPage) -> bool {
matches!(page, BuiltinBrowserPage::Downloads)
}
fn invoke_windows_shell_pins_handler(items: &[ShellPin]) -> bool {
WINDOWS_SHELL_PINS_HANDLER
.lock()
.ok()
.and_then(|slot| slot.clone())
.is_none_or(|handler| handler(items))
}
fn invoke_windows_lxapp_main_activation_handler(appid: &str) {
if let Some(handler) = WINDOWS_LXAPP_MAIN_ACTIVATION_HANDLER
.lock()
.ok()
.and_then(|slot| slot.clone())
{
handler(appid);
}
}
fn invoke_windows_lxapp_hidden_handler(appid: &str, session_id: u64) {
if let Some(handler) = WINDOWS_LXAPP_HIDDEN_HANDLER
.lock()
.ok()
.and_then(|slot| slot.clone())
{
handler(appid, session_id);
}
}
pub(crate) fn request_windows_app_exit() {
let handler = WINDOWS_APP_EXIT_HANDLER
.lock()
.ok()
.and_then(|slot| slot.clone());
if let Some(handler) = handler {
handler();
} else {
std::process::exit(0);
}
}
const AUTOSTART_RUN_KEY: &str = r"Software\Microsoft\Windows\CurrentVersion\Run";
fn autostart_command(exe: &Path) -> String {
format!("\"{}\"", exe.display())
}
fn read_autostart_run_entry(name: &str) -> Option<String> {
use windows::Win32::System::Registry::HKEY_CURRENT_USER;
super::registry::read_string(HKEY_CURRENT_USER, AUTOSTART_RUN_KEY, name)
}
fn write_autostart_run_entry(name: &str, exe: &Path) -> Result<(), PlatformError> {
use windows::Win32::System::Registry::{
HKEY, HKEY_CURRENT_USER, KEY_SET_VALUE, REG_OPTION_NON_VOLATILE, REG_SZ, RegCloseKey,
RegCreateKeyExW, RegSetValueExW,
};
use windows::core::{HSTRING, PCWSTR};
let subkey = HSTRING::from(AUTOSTART_RUN_KEY);
let value = HSTRING::from(name);
let data: Vec<u8> = autostart_command(exe)
.encode_utf16()
.chain(std::iter::once(0))
.flat_map(u16::to_le_bytes)
.collect();
unsafe {
let mut key = HKEY::default();
RegCreateKeyExW(
HKEY_CURRENT_USER,
&subkey,
None,
PCWSTR::null(),
REG_OPTION_NON_VOLATILE,
KEY_SET_VALUE,
None,
&mut key,
None,
)
.ok()
.map_err(|err| PlatformError::Platform(format!("cannot open Run key: {err}")))?;
let status = RegSetValueExW(key, &value, None, REG_SZ, Some(&data));
let _ = RegCloseKey(key);
status
.ok()
.map_err(|err| PlatformError::Platform(format!("cannot write Run entry: {err}")))
}
}
fn remove_autostart_run_entry(name: &str) -> Result<(), PlatformError> {
use windows::Win32::Foundation::ERROR_FILE_NOT_FOUND;
use windows::Win32::System::Registry::{
HKEY, HKEY_CURRENT_USER, KEY_SET_VALUE, RegCloseKey, RegDeleteValueW, RegOpenKeyExW,
};
use windows::core::HSTRING;
let subkey = HSTRING::from(AUTOSTART_RUN_KEY);
let value = HSTRING::from(name);
unsafe {
let mut key = HKEY::default();
let open = RegOpenKeyExW(HKEY_CURRENT_USER, &subkey, None, KEY_SET_VALUE, &mut key);
if open == ERROR_FILE_NOT_FOUND {
return Ok(());
}
open.ok()
.map_err(|err| PlatformError::Platform(format!("cannot open Run key: {err}")))?;
let status = RegDeleteValueW(key, &value);
let _ = RegCloseKey(key);
if status == ERROR_FILE_NOT_FOUND {
return Ok(());
}
status
.ok()
.map_err(|err| PlatformError::Platform(format!("cannot delete Run entry: {err}")))
}
}
type WindowsOpenUrlHandler =
Arc<dyn Fn(&OpenUrlRequest) -> Result<Option<OpenUrlResult>, PlatformError> + Send + Sync>;
static WINDOWS_OPEN_URL_HANDLER: Mutex<Option<WindowsOpenUrlHandler>> = Mutex::new(None);
type WindowsBrowserTabHandler = Arc<dyn Fn(&str) -> bool + Send + Sync>;
static WINDOWS_CLOSE_BROWSER_TAB_HANDLER: Mutex<Option<WindowsBrowserTabHandler>> =
Mutex::new(None);
type WindowsActivateBrowserTabHandler =
Arc<dyn Fn(String) -> crate::traits::PlatformFuture + Send + Sync>;
static WINDOWS_ACTIVATE_BROWSER_TAB_HANDLER: Mutex<Option<WindowsActivateBrowserTabHandler>> =
Mutex::new(None);
pub fn set_windows_open_url_handler(handler: WindowsOpenUrlHandler) {
if let Ok(mut slot) = WINDOWS_OPEN_URL_HANDLER.lock() {
*slot = Some(handler);
}
}
fn invoke_windows_open_url_handler(
req: &OpenUrlRequest,
) -> Result<Option<OpenUrlResult>, PlatformError> {
let Some(handler) = WINDOWS_OPEN_URL_HANDLER
.lock()
.ok()
.and_then(|slot| slot.clone())
else {
return Ok(None);
};
handler(req)
}
pub fn set_windows_close_browser_tab_handler(handler: WindowsBrowserTabHandler) {
if let Ok(mut slot) = WINDOWS_CLOSE_BROWSER_TAB_HANDLER.lock() {
*slot = Some(handler);
}
}
pub fn set_windows_activate_browser_tab_handler(handler: WindowsActivateBrowserTabHandler) {
if let Ok(mut slot) = WINDOWS_ACTIVATE_BROWSER_TAB_HANDLER.lock() {
*slot = Some(handler);
}
}
fn invoke_windows_activate_browser_tab_handler(tab_id: String) -> crate::traits::PlatformFuture {
let handler = WINDOWS_ACTIVATE_BROWSER_TAB_HANDLER
.lock()
.ok()
.and_then(|slot| slot.clone());
match handler {
Some(handler) => handler(tab_id),
None => Box::pin(async { Err(PlatformError::NotSupported("browser tab".to_string())) }),
}
}
fn invoke_windows_close_browser_tab_handler(tab_id: &str) -> bool {
let handler = WINDOWS_CLOSE_BROWSER_TAB_HANDLER
.lock()
.ok()
.and_then(|slot| slot.clone());
handler.map(|handler| handler(tab_id)).unwrap_or(false)
}
type WindowsTrayMenuHandler = Arc<dyn Fn(&str) + Send + Sync>;
static WINDOWS_TRAY_MENU_HANDLER: Mutex<Option<WindowsTrayMenuHandler>> = Mutex::new(None);
pub fn set_windows_tray_menu_handler(handler: WindowsTrayMenuHandler) {
if let Ok(mut slot) = WINDOWS_TRAY_MENU_HANDLER.lock() {
*slot = Some(handler);
}
}
fn invoke_windows_tray_menu_handler(items_json: &str) {
let handler = WINDOWS_TRAY_MENU_HANDLER
.lock()
.ok()
.and_then(|slot| slot.clone());
if let Some(handler) = handler {
handler(items_json);
}
}
type WindowsTrayClickInterceptHandler = Arc<dyn Fn(bool) + Send + Sync>;
static WINDOWS_TRAY_CLICK_INTERCEPT_HANDLER: Mutex<Option<WindowsTrayClickInterceptHandler>> =
Mutex::new(None);
pub fn set_windows_tray_click_intercept_handler(handler: WindowsTrayClickInterceptHandler) {
if let Ok(mut slot) = WINDOWS_TRAY_CLICK_INTERCEPT_HANDLER.lock() {
*slot = Some(handler);
}
}
fn invoke_windows_tray_click_intercept_handler(intercept: bool) {
let handler = WINDOWS_TRAY_CLICK_INTERCEPT_HANDLER
.lock()
.ok()
.and_then(|slot| slot.clone());
if let Some(handler) = handler {
handler(intercept);
}
}
type WindowsExclusiveUpdateReadyHandler = Arc<dyn Fn() -> bool + Send + Sync>;
static WINDOWS_EXCLUSIVE_UPDATE_READY_HANDLER: Mutex<Option<WindowsExclusiveUpdateReadyHandler>> =
Mutex::new(None);
pub fn set_windows_exclusive_update_ready_handler(handler: WindowsExclusiveUpdateReadyHandler) {
if let Ok(mut slot) = WINDOWS_EXCLUSIVE_UPDATE_READY_HANDLER.lock() {
*slot = Some(handler);
}
}
pub(crate) fn invoke_windows_exclusive_update_ready() -> bool {
let handler = WINDOWS_EXCLUSIVE_UPDATE_READY_HANDLER
.lock()
.ok()
.and_then(|slot| slot.clone());
handler.map(|handler| handler()).unwrap_or(false)
}
#[derive(Debug, Clone)]
pub struct Platform {
data_dir: PathBuf,
cache_dir: PathBuf,
asset_dir: PathBuf,
locale: String,
app_identifier: String,
product_name: String,
}
impl Default for Platform {
fn default() -> Self {
Self::from_env().unwrap_or_else(|_| {
let base = default_state_root();
Self {
data_dir: base.join("data"),
cache_dir: base.join("cache"),
asset_dir: default_asset_dir(),
locale: current_locale(),
app_identifier: DEFAULT_APP_IDENTIFIER.to_string(),
product_name: "LingXia".to_string(),
}
})
}
}
impl Platform {
pub fn new(data_dir: String, cache_dir: String, locale: String) -> Result<Self, PlatformError> {
Ok(Self {
data_dir: PathBuf::from(data_dir),
cache_dir: PathBuf::from(cache_dir),
asset_dir: default_asset_dir(),
locale,
app_identifier: DEFAULT_APP_IDENTIFIER.to_string(),
product_name: "LingXia".to_string(),
})
}
pub fn from_env() -> Result<Self, PlatformError> {
Self::from_asset_dir(default_asset_dir())
}
pub fn from_asset_dir(asset_dir: impl Into<PathBuf>) -> Result<Self, PlatformError> {
let asset_dir = asset_dir.into();
let config = GeneratedAppConfig::read_from_assets(&asset_dir);
let product_name = config.product_name.unwrap_or_else(|| "LingXia".to_string());
let app_identifier = config
.windows_app_id
.unwrap_or_else(|| DEFAULT_APP_IDENTIFIER.to_string());
let root = state_root_for_product(&product_name);
Ok(Self {
data_dir: root.join("data"),
cache_dir: root.join("cache"),
asset_dir,
locale: current_locale(),
app_identifier,
product_name,
})
}
pub fn asset_dir(&self) -> &Path {
&self.asset_dir
}
pub(super) fn app_identifier(&self) -> &str {
&self.app_identifier
}
pub(super) fn autostart_value_name(&self) -> String {
if self.app_identifier != DEFAULT_APP_IDENTIFIER {
self.app_identifier.clone()
} else {
format!("{DEFAULT_APP_IDENTIFIER}.{}", self.product_name)
}
}
pub(super) fn product_name(&self) -> &str {
&self.product_name
}
pub fn install_taskbar_identity(&self) {
use windows::Win32::UI::Shell::SetCurrentProcessExplicitAppUserModelID;
use windows::core::PCWSTR;
let id = self.autostart_value_name();
let id: String = id
.chars()
.map(|c| if c.is_whitespace() { '.' } else { c })
.take(127)
.collect();
let wide: Vec<u16> = id.encode_utf16().chain(std::iter::once(0)).collect();
if let Err(err) = unsafe { SetCurrentProcessExplicitAppUserModelID(PCWSTR(wide.as_ptr())) }
{
log::warn!("failed to set process AppUserModelID {id:?}: {err}");
}
}
pub(super) fn data_dir(&self) -> &Path {
&self.data_dir
}
fn resolve_asset_path(&self, path: &str) -> Result<PathBuf, PlatformError> {
let normalized = normalize_relative_path(path)?;
Ok(self.asset_dir.join(normalized))
}
fn collect_files_recursively<'a>(
&'a self,
asset_dir: &str,
) -> Vec<Result<AssetFileEntry<'a>, PlatformError>> {
let root = match self.resolve_asset_path(asset_dir) {
Ok(path) => path,
Err(err) => return vec![Err(err)],
};
let base = self.asset_dir.clone();
let mut out = Vec::new();
collect_asset_files(&base, &root, &mut out);
out
}
}
impl AppRuntime for Platform {
fn read_asset<'a>(&'a self, path: &str) -> Result<Box<dyn Read + 'a>, PlatformError> {
let path = self.resolve_asset_path(path)?;
let file = File::open(&path).map_err(|err| {
if err.kind() == std::io::ErrorKind::NotFound {
PlatformError::AssetNotFound(path.display().to_string())
} else {
PlatformError::Platform(format!("failed to open asset {}: {err}", path.display()))
}
})?;
Ok(Box::new(file))
}
fn asset_dir_iter<'a>(
&'a self,
asset_dir: &str,
) -> Box<dyn Iterator<Item = Result<AssetFileEntry<'a>, PlatformError>> + 'a> {
Box::new(self.collect_files_recursively(asset_dir).into_iter())
}
fn app_data_dir(&self) -> PathBuf {
self.data_dir.clone()
}
fn app_cache_dir(&self) -> PathBuf {
self.cache_dir.clone()
}
fn get_app_identifier(&self) -> Result<String, PlatformError> {
Ok(self.app_identifier.clone())
}
fn get_system_locale(&self) -> &str {
&self.locale
}
fn show_lxapp(
&self,
appid: String,
title: String,
_path: String,
webtag: String,
_session_id: u64,
open_mode: LxAppOpenMode,
panel_id: String,
) -> Result<(), PlatformError> {
let webtag = WebTag::from(webtag.as_str());
if !matches!(open_mode, LxAppOpenMode::Panel) {
ui_update::sync_windows_ui(&appid);
}
surface::show_webtag_window(
webtag,
self.product_name.clone(),
title,
true,
open_mode,
panel_id,
);
Ok(())
}
fn request_lxapp_main_activation(&self, appid: &str) {
invoke_windows_lxapp_main_activation_handler(appid);
}
fn hide_lxapp(&self, appid: String, session_id: u64) -> Result<(), PlatformError> {
surface::hide_lxapp_window(&appid, session_id);
invoke_windows_lxapp_hidden_handler(&appid, session_id);
Ok(())
}
fn exit(&self) -> Result<(), PlatformError> {
request_windows_app_exit();
Ok(())
}
fn autostart_is_enabled(&self) -> Result<bool, PlatformError> {
let exe = std::env::current_exe().map_err(|err| {
PlatformError::Platform(format!("cannot resolve app executable: {err}"))
})?;
Ok(read_autostart_run_entry(&self.autostart_value_name())
.is_some_and(|cmd| cmd.eq_ignore_ascii_case(&autostart_command(&exe))))
}
fn notification_permission(&self) -> Result<String, PlatformError> {
super::notification::permission(self)
}
fn notification_request_permission(&self) -> Result<String, PlatformError> {
super::notification::permission(self)
}
fn notification_show(
&self,
request: &LocalNotificationShow,
) -> Result<crate::traits::app_runtime::LocalNotificationStatus, PlatformError> {
super::notification::show(self, request)
}
fn notification_cancel(&self, id: &str) -> Result<(), PlatformError> {
super::notification::cancel(self, id)
}
fn notification_cancel_all(&self) -> Result<(), PlatformError> {
super::notification::cancel_all(self)
}
fn banner_show(
&self,
request: &DesktopBannerShow,
) -> Result<DesktopBannerOutcome, PlatformError> {
crate::desktop::banner::show(request.clone())
}
fn banner_dismiss(&self, id: &str) -> Result<(), PlatformError> {
crate::desktop::banner::dismiss(id);
Ok(())
}
fn autostart_set_enabled(&self, enabled: bool) -> Result<(), PlatformError> {
let name = self.autostart_value_name();
if enabled {
let exe = std::env::current_exe().map_err(|err| {
PlatformError::Platform(format!("cannot resolve app executable: {err}"))
})?;
write_autostart_run_entry(&name, &exe)
} else {
remove_autostart_run_entry(&name)
}
}
fn set_app_badge(&self, text: &str) -> Result<bool, PlatformError> {
super::badge::set_app_badge(text)
}
fn set_tray_menu(&self, items_json: &str) -> Result<(), PlatformError> {
invoke_windows_tray_menu_handler(items_json);
Ok(())
}
fn set_shell_sidebar_actions(
&self,
items: &[ResolvedShellSidebarAction],
) -> Result<(), PlatformError> {
if invoke_windows_sidebar_actions_handler(items) {
Ok(())
} else {
Err(PlatformError::Platform(
"Windows shell rejected resolved sidebar actions".to_string(),
))
}
}
fn open_builtin_browser_page(&self, page: BuiltinBrowserPage) -> Result<(), PlatformError> {
if is_windows_builtin_browser_downloads(page)
&& invoke_windows_builtin_browser_downloads_handler()
{
Ok(())
} else {
Err(PlatformError::NotSupported(
"built-in browser page".to_string(),
))
}
}
fn set_shell_pins(&self, items: &[ShellPin]) -> Result<(), PlatformError> {
if invoke_windows_shell_pins_handler(items) {
Ok(())
} else {
Err(PlatformError::Platform(
"Windows shell rejected Pins".to_string(),
))
}
}
fn set_tray_click_intercept(&self, intercept: bool) -> Result<(), PlatformError> {
invoke_windows_tray_click_intercept_handler(intercept);
Ok(())
}
fn set_control_session_indicator(&self, active: bool) -> Result<(), PlatformError> {
if active {
super::control_session_indicator::show(self.get_system_locale());
} else {
super::control_session_indicator::hide();
}
Ok(())
}
fn navigate(
&self,
appid: String,
_path: String,
webtag: String,
animation_type: AnimationType,
) -> Result<(), PlatformError> {
let webtag = WebTag::from(webtag.as_str());
ui_update::sync_windows_ui(&appid);
surface::navigate_webtag_window(webtag, self.product_name.clone(), animation_type);
Ok(())
}
fn open_url(&self, req: OpenUrlRequest) -> Result<OpenUrlResult, PlatformError> {
if let Some(result) = invoke_windows_open_url_handler(&req)? {
return Ok(result);
}
file::open_with_shell_detached(&req.url)?;
Ok(OpenUrlResult::default())
}
fn close_browser_tab(&self, tab_id: &str) -> Result<(), PlatformError> {
if invoke_windows_close_browser_tab_handler(tab_id) {
Ok(())
} else {
Err(PlatformError::NotSupported("browser tab".to_string()))
}
}
fn activate_browser_tab(&self, tab_id: String) -> crate::traits::PlatformFuture {
invoke_windows_activate_browser_tab_handler(tab_id)
}
}
impl crate::traits::ui::SurfacePresenter for Platform {
fn present_layout(
&self,
window_id: &str,
plan: &lingxia_surface::LayoutPresentationPlan,
) -> Result<(), PlatformError> {
surface::present_layout(window_id, plan, &self.product_name)
}
fn present_surface(
&self,
request: crate::traits::ui::SurfaceRequest,
) -> Result<(), PlatformError> {
surface::present_surface(request, &self.product_name)
}
fn close_surface(&self, app_id: &str, id: &str, reason: &str) -> Result<(), PlatformError> {
surface::close_surface(app_id, id, reason)
}
fn show_surface(&self, app_id: &str, id: &str) -> Result<(), PlatformError> {
surface::show_surface(app_id, id)
}
fn hide_surface(&self, app_id: &str, id: &str) -> Result<(), PlatformError> {
surface::hide_surface(app_id, id)
}
fn ensure_managed_surface_provider(
&self,
request: crate::traits::ui::ManagedSurfaceProviderRequest,
) -> crate::traits::PlatformFuture {
surface::ensure_managed_surface_provider(request)
}
fn destroy_managed_surface_provider(
&self,
request: crate::traits::ui::ManagedSurfaceProviderDestroyRequest,
) -> crate::traits::PlatformFuture {
surface::destroy_managed_surface_provider(request)
}
}
impl ShareService for Platform {
fn share(
&self,
_request: ShareRequest,
) -> impl Future<Output = Result<ShareResult, PlatformError>> + Send {
async { not_supported("share") }
}
}
impl VideoStreamDecoderManager for Platform {
fn create_stream_decoder(
&self,
_component_id: &str,
) -> Result<Box<dyn VideoStreamDecoderHandle>, PlatformError> {
not_supported("create_stream_decoder")
}
}
fn normalize_relative_path(path: &str) -> Result<PathBuf, PlatformError> {
let mut out = PathBuf::new();
for component in Path::new(path.trim_start_matches(['/', '\\'])).components() {
match component {
Component::Normal(part) => out.push(part),
Component::CurDir => {}
Component::RootDir | Component::Prefix(_) | Component::ParentDir => {
return Err(PlatformError::InvalidParameter(format!(
"asset path must be relative and stay inside assets: {path}"
)));
}
}
}
Ok(out)
}
fn collect_asset_files<'a>(
base: &Path,
dir: &Path,
out: &mut Vec<Result<AssetFileEntry<'a>, PlatformError>>,
) {
let entries = match fs::read_dir(dir) {
Ok(entries) => entries,
Err(err) => {
out.push(Err(PlatformError::Platform(format!(
"failed to read asset directory {}: {err}",
dir.display()
))));
return;
}
};
for entry in entries {
let entry = match entry {
Ok(entry) => entry,
Err(err) => {
out.push(Err(PlatformError::Platform(format!(
"failed to read asset directory entry: {err}"
))));
continue;
}
};
let path = entry.path();
if path.is_dir() {
collect_asset_files(base, &path, out);
continue;
}
if !path.is_file() {
continue;
}
let relative = path
.strip_prefix(base)
.unwrap_or(&path)
.to_string_lossy()
.replace('\\', "/");
match File::open(&path) {
Ok(file) => out.push(Ok(AssetFileEntry {
path: relative,
reader: Box::new(file),
})),
Err(err) => out.push(Err(PlatformError::Platform(format!(
"failed to open asset {}: {err}",
path.display()
)))),
}
}
}
fn state_root_override() -> Option<PathBuf> {
std::env::var_os("LINGXIA_STATE_ROOT")
.map(PathBuf::from)
.filter(|path| !path.as_os_str().is_empty())
}
fn default_state_root() -> PathBuf {
state_root_override().unwrap_or_else(|| {
std::env::var_os("LOCALAPPDATA")
.map(PathBuf::from)
.unwrap_or_else(std::env::temp_dir)
.join("LingXia")
})
}
fn state_root_for_product(product_name: &str) -> PathBuf {
state_root_override().unwrap_or_else(|| {
std::env::var_os("LOCALAPPDATA")
.map(PathBuf::from)
.unwrap_or_else(std::env::temp_dir)
.join(product_name)
})
}
fn default_asset_dir() -> PathBuf {
std::env::current_exe()
.ok()
.and_then(|exe| exe.parent().map(Path::to_path_buf))
.map(|dir| dir.join("assets"))
.unwrap_or_else(|| PathBuf::from("assets"))
}
pub fn current_locale() -> String {
use windows::Win32::Globalization::GetUserDefaultLocaleName;
let mut buffer = [0u16; 85];
let len = unsafe { GetUserDefaultLocaleName(&mut buffer) };
if len > 1 {
String::from_utf16_lossy(&buffer[..len as usize - 1])
} else {
"en-US".to_string()
}
}
#[derive(Debug, Default)]
struct GeneratedAppConfig {
product_name: Option<String>,
windows_app_id: Option<String>,
}
impl GeneratedAppConfig {
fn read_from_assets(asset_dir: &Path) -> Self {
let Ok(content) = std::fs::read_to_string(asset_dir.join("app.json")) else {
return Self::default();
};
let Ok(json) = serde_json::from_str::<serde_json::Value>(&content) else {
return Self::default();
};
Self {
product_name: json
.get("productName")
.and_then(serde_json::Value::as_str)
.map(str::trim)
.filter(|value| !value.is_empty())
.map(str::to_string),
windows_app_id: json
.get("windowsAppId")
.and_then(serde_json::Value::as_str)
.map(str::trim)
.filter(|value| !value.is_empty())
.map(str::to_string),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn windows_builtin_browser_handler_is_downloads_only() {
assert!(is_windows_builtin_browser_downloads(
BuiltinBrowserPage::Downloads
));
}
}