integration of active_app module
This commit is contained in:
1
src-tauri/Cargo.lock
generated
1
src-tauri/Cargo.lock
generated
@@ -1184,6 +1184,7 @@ dependencies = [
|
|||||||
"gif",
|
"gif",
|
||||||
"image",
|
"image",
|
||||||
"keyring",
|
"keyring",
|
||||||
|
"lazy_static",
|
||||||
"objc2 0.6.3",
|
"objc2 0.6.3",
|
||||||
"objc2-app-kit",
|
"objc2-app-kit",
|
||||||
"objc2-foundation 0.3.2",
|
"objc2-foundation 0.3.2",
|
||||||
|
|||||||
@@ -48,6 +48,7 @@ image = {version = "0.25.9", default-features = false, features = ["gif"] }
|
|||||||
gif = "0.14.1"
|
gif = "0.14.1"
|
||||||
raw-window-handle = "0.6"
|
raw-window-handle = "0.6"
|
||||||
enigo = { version = "0.6.1", features = ["wayland"] }
|
enigo = { version = "0.6.1", features = ["wayland"] }
|
||||||
|
lazy_static = "1.5.0"
|
||||||
|
|
||||||
[target.'cfg(not(any(target_os = "android", target_os = "ios")))'.dependencies]
|
[target.'cfg(not(any(target_os = "android", target_os = "ios")))'.dependencies]
|
||||||
tauri-plugin-global-shortcut = "2"
|
tauri-plugin-global-shortcut = "2"
|
||||||
@@ -55,11 +56,24 @@ tauri-plugin-positioner = "2"
|
|||||||
|
|
||||||
[target.'cfg(target_os = "macos")'.dependencies]
|
[target.'cfg(target_os = "macos")'.dependencies]
|
||||||
objc2 = "0.6.3"
|
objc2 = "0.6.3"
|
||||||
objc2-app-kit = { version = "0.3.2", features = ["NSWindow", "NSWindowScripting"] }
|
objc2-app-kit = { version = "0.3.2", features = [
|
||||||
objc2-foundation = "0.3.2"
|
"NSWindow",
|
||||||
|
"NSWindowScripting",
|
||||||
|
"NSWorkspace",
|
||||||
|
"NSRunningApplication"
|
||||||
|
] }
|
||||||
|
objc2-foundation = { version = "0.3.2", features = ["NSNotification", "NSString", "NSURL"] }
|
||||||
|
|
||||||
[target.'cfg(target_os = "windows")'.dependencies]
|
[target.'cfg(target_os = "windows")'.dependencies]
|
||||||
windows = { version = "0.58", features = [
|
windows = { version = "0.58", features = [
|
||||||
"Win32_Foundation",
|
"Win32_Foundation",
|
||||||
|
"Win32_System_ProcessStatus",
|
||||||
|
"Win32_UI_WindowsAndMessaging",
|
||||||
|
"Win32_System_Threading",
|
||||||
|
"Win32_UI_Shell_PropertiesSystem",
|
||||||
|
"Win32_System_Com",
|
||||||
|
"Win32_UI_Accessibility",
|
||||||
|
"Win32_Storage_FileSystem",
|
||||||
|
"Win32_Globalization",
|
||||||
"Win32_UI_Input_KeyboardAndMouse",
|
"Win32_UI_Input_KeyboardAndMouse",
|
||||||
] }
|
] }
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ use crate::{
|
|||||||
lock_w,
|
lock_w,
|
||||||
remotes::health::{HealthError, HealthRemote},
|
remotes::health::{HealthError, HealthRemote},
|
||||||
services::{
|
services::{
|
||||||
|
active_app::init_active_app_changes_listener,
|
||||||
auth::{get_access_token, get_tokens},
|
auth::{get_access_token, get_tokens},
|
||||||
health_manager::show_health_manager_with_error,
|
health_manager::show_health_manager_with_error,
|
||||||
scene::{close_splash_window, open_scene_window, open_splash_window},
|
scene::{close_splash_window, open_scene_window, open_splash_window},
|
||||||
@@ -23,6 +24,10 @@ pub async fn start_fdoll() {
|
|||||||
let mut guard = lock_w!(FDOLL);
|
let mut guard = lock_w!(FDOLL);
|
||||||
guard.tray = Some(tray);
|
guard.tray = Some(tray);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Begin listening for foreground app changes
|
||||||
|
init_active_app_changes_listener();
|
||||||
|
|
||||||
if let Err(err) = init_startup_sequence().await {
|
if let Err(err) = init_startup_sequence().await {
|
||||||
tracing::error!("startup sequence failed: {err}");
|
tracing::error!("startup sequence failed: {err}");
|
||||||
show_health_manager_with_error(Some(err.to_string()));
|
show_health_manager_with_error(Some(err.to_string()));
|
||||||
|
|||||||
425
src-tauri/src/services/active_app.rs
Normal file
425
src-tauri/src/services/active_app.rs
Normal file
@@ -0,0 +1,425 @@
|
|||||||
|
use std::fmt::Debug;
|
||||||
|
use ts_rs::TS;
|
||||||
|
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use tauri::Emitter;
|
||||||
|
use tracing::error;
|
||||||
|
|
||||||
|
use crate::get_app_handle;
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize, TS)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
#[ts(export)]
|
||||||
|
pub struct AppMetadata {
|
||||||
|
pub localized: Option<String>,
|
||||||
|
pub unlocalized: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn listen_for_active_app_changes<F>(callback: F)
|
||||||
|
where
|
||||||
|
F: Fn(AppMetadata) + Send + 'static,
|
||||||
|
{
|
||||||
|
listen_impl(callback)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(target_os = "macos")]
|
||||||
|
fn listen_impl<F>(callback: F)
|
||||||
|
where
|
||||||
|
F: Fn(AppMetadata) + Send + 'static,
|
||||||
|
{
|
||||||
|
macos::listen_for_active_app_changes(callback);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(target_os = "windows")]
|
||||||
|
fn listen_impl<F>(callback: F)
|
||||||
|
where
|
||||||
|
F: Fn(AppMetadata) + Send + 'static,
|
||||||
|
{
|
||||||
|
windows_impl::listen_for_active_app_changes(callback);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(not(any(target_os = "macos", target_os = "windows")))]
|
||||||
|
fn listen_impl<F>(_callback: F)
|
||||||
|
where
|
||||||
|
F: Fn(AppMetadata) + Send + 'static,
|
||||||
|
{
|
||||||
|
// no-op on unsupported platforms
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(target_os = "macos")]
|
||||||
|
mod macos {
|
||||||
|
use super::AppMetadata;
|
||||||
|
use lazy_static::lazy_static;
|
||||||
|
use objc2::runtime::{AnyClass, AnyObject, Sel};
|
||||||
|
use objc2::{class, msg_send, sel};
|
||||||
|
use objc2_foundation::NSString;
|
||||||
|
use std::ffi::CStr;
|
||||||
|
use std::sync::{Mutex, Once};
|
||||||
|
|
||||||
|
#[allow(unused_imports)] // for framework linking, not referenced in code
|
||||||
|
use objc2_app_kit::NSWorkspace;
|
||||||
|
|
||||||
|
lazy_static! {
|
||||||
|
static ref CALLBACK: Mutex<Option<Box<dyn Fn(AppMetadata) + Send + 'static>>> =
|
||||||
|
Mutex::new(None);
|
||||||
|
}
|
||||||
|
static INIT_OBSERVER: Once = Once::new();
|
||||||
|
|
||||||
|
pub fn listen_for_active_app_changes<F>(callback: F)
|
||||||
|
where
|
||||||
|
F: Fn(AppMetadata) + Send + 'static,
|
||||||
|
{
|
||||||
|
INIT_OBSERVER.call_once(|| {
|
||||||
|
register_objc_observer_class();
|
||||||
|
});
|
||||||
|
*CALLBACK.lock().unwrap() = Some(Box::new(callback));
|
||||||
|
|
||||||
|
unsafe {
|
||||||
|
let cls = AnyClass::get(CStr::from_bytes_with_nul(b"RustActiveAppObserver\0").unwrap())
|
||||||
|
.unwrap();
|
||||||
|
let observer: *mut AnyObject = msg_send![cls, new];
|
||||||
|
|
||||||
|
let ws: *mut AnyObject = msg_send![class!(NSWorkspace), sharedWorkspace];
|
||||||
|
let nc: *mut AnyObject = msg_send![ws, notificationCenter];
|
||||||
|
let notif_name = NSString::from_str("NSWorkspaceDidActivateApplicationNotification");
|
||||||
|
let _: () = msg_send![
|
||||||
|
nc,
|
||||||
|
addObserver: observer,
|
||||||
|
selector: sel!(appActivated:),
|
||||||
|
name: &*notif_name,
|
||||||
|
object: ws
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn register_objc_observer_class() {
|
||||||
|
use objc2::runtime::ClassBuilder;
|
||||||
|
|
||||||
|
let cname = CStr::from_bytes_with_nul(b"RustActiveAppObserver\0").unwrap();
|
||||||
|
let super_cls = class!(NSObject);
|
||||||
|
let mut builder = ClassBuilder::new(&cname, super_cls).unwrap();
|
||||||
|
unsafe {
|
||||||
|
builder.add_method(
|
||||||
|
sel!(appActivated:),
|
||||||
|
app_activated as extern "C" fn(*mut AnyObject, Sel, *mut AnyObject),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
builder.register();
|
||||||
|
}
|
||||||
|
|
||||||
|
extern "C" fn app_activated(_self: *mut AnyObject, _cmd: Sel, _notif: *mut AnyObject) {
|
||||||
|
let info = unsafe { get_active_app_names_macos_internal() };
|
||||||
|
if let Some(cb) = CALLBACK.lock().unwrap().as_ref() {
|
||||||
|
cb(info);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub unsafe fn get_active_app_names_macos_internal() -> AppMetadata {
|
||||||
|
let ws: *mut AnyObject = msg_send![class!(NSWorkspace), sharedWorkspace];
|
||||||
|
let front_app: *mut AnyObject = msg_send![ws, frontmostApplication];
|
||||||
|
if front_app.is_null() {
|
||||||
|
return AppMetadata {
|
||||||
|
localized: None,
|
||||||
|
unlocalized: None,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
let name: *mut NSString = msg_send![front_app, localizedName];
|
||||||
|
let localized = if name.is_null() {
|
||||||
|
None
|
||||||
|
} else {
|
||||||
|
Some(unsafe {
|
||||||
|
CStr::from_ptr((*name).UTF8String())
|
||||||
|
.to_string_lossy()
|
||||||
|
.into_owned()
|
||||||
|
})
|
||||||
|
};
|
||||||
|
|
||||||
|
let exe_url: *mut AnyObject = msg_send![front_app, executableURL];
|
||||||
|
let unlocalized = if exe_url.is_null() {
|
||||||
|
None
|
||||||
|
} else {
|
||||||
|
let exe_name: *mut NSString = msg_send![exe_url, lastPathComponent];
|
||||||
|
if exe_name.is_null() {
|
||||||
|
None
|
||||||
|
} else {
|
||||||
|
Some(unsafe {
|
||||||
|
CStr::from_ptr((*exe_name).UTF8String())
|
||||||
|
.to_string_lossy()
|
||||||
|
.into_owned()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
};
|
||||||
|
AppMetadata {
|
||||||
|
localized,
|
||||||
|
unlocalized,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(target_os = "windows")]
|
||||||
|
mod windows_impl {
|
||||||
|
use super::AppMetadata;
|
||||||
|
use std::ffi::OsString;
|
||||||
|
use std::iter;
|
||||||
|
use std::os::windows::ffi::OsStringExt;
|
||||||
|
use std::path::Path;
|
||||||
|
use std::ptr;
|
||||||
|
use windows::core::PCWSTR;
|
||||||
|
use windows::Win32::Foundation::HWND;
|
||||||
|
use windows::Win32::Globalization::GetUserDefaultLangID;
|
||||||
|
use windows::Win32::System::ProcessStatus::GetModuleFileNameExW;
|
||||||
|
use windows::Win32::System::Threading::{OpenProcess, PROCESS_QUERY_LIMITED_INFORMATION};
|
||||||
|
use windows::Win32::UI::Accessibility::{SetWinEventHook, UnhookWinEvent, HWINEVENTHOOK};
|
||||||
|
use windows::Win32::UI::WindowsAndMessaging::{
|
||||||
|
DispatchMessageW, GetForegroundWindow, GetMessageW, GetWindowTextW,
|
||||||
|
GetWindowThreadProcessId, EVENT_SYSTEM_FOREGROUND, MSG, WINEVENT_OUTOFCONTEXT,
|
||||||
|
};
|
||||||
|
|
||||||
|
pub fn listen_for_active_app_changes<F>(callback: F)
|
||||||
|
where
|
||||||
|
F: Fn(AppMetadata) + Send + 'static,
|
||||||
|
{
|
||||||
|
// Run the hook on a background thread so we don’t block the caller.
|
||||||
|
std::thread::spawn(move || unsafe {
|
||||||
|
let hook = SetWinEventHook(
|
||||||
|
EVENT_SYSTEM_FOREGROUND,
|
||||||
|
EVENT_SYSTEM_FOREGROUND,
|
||||||
|
None,
|
||||||
|
Some(win_event_proc::<F>),
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
WINEVENT_OUTOFCONTEXT,
|
||||||
|
);
|
||||||
|
|
||||||
|
if hook.is_invalid() {
|
||||||
|
eprintln!("Failed to set event hook");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// store callback in TLS for this thread
|
||||||
|
CALLBACK.with(|cb| cb.replace(Some(Box::new(callback))));
|
||||||
|
|
||||||
|
let mut msg = MSG::default();
|
||||||
|
while GetMessageW(&mut msg, None, 0, 0).as_bool() {
|
||||||
|
DispatchMessageW(&msg);
|
||||||
|
}
|
||||||
|
|
||||||
|
let _ = UnhookWinEvent(hook);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
thread_local! {
|
||||||
|
static CALLBACK: std::cell::RefCell<Option<Box<dyn Fn(AppMetadata) + Send + 'static>>> =
|
||||||
|
std::cell::RefCell::new(None);
|
||||||
|
}
|
||||||
|
|
||||||
|
unsafe extern "system" fn win_event_proc<F>(
|
||||||
|
_h_win_event_hook: HWINEVENTHOOK,
|
||||||
|
event: u32,
|
||||||
|
hwnd: HWND,
|
||||||
|
_id_object: i32,
|
||||||
|
_id_child: i32,
|
||||||
|
_id_event_thread: u32,
|
||||||
|
_dw_ms_event_time: u32,
|
||||||
|
) {
|
||||||
|
if event == EVENT_SYSTEM_FOREGROUND {
|
||||||
|
let names = get_active_app_names_windows(Some(hwnd));
|
||||||
|
CALLBACK.with(|cb| {
|
||||||
|
if let Some(ref cb) = *cb.borrow() {
|
||||||
|
cb(names);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_active_app_names_windows(hwnd_override: Option<HWND>) -> AppMetadata {
|
||||||
|
unsafe {
|
||||||
|
let hwnd = hwnd_override.unwrap_or_else(|| GetForegroundWindow());
|
||||||
|
if hwnd.0 == std::ptr::null_mut() {
|
||||||
|
return AppMetadata {
|
||||||
|
localized: None,
|
||||||
|
unlocalized: None,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
let mut pid: u32 = 0;
|
||||||
|
GetWindowThreadProcessId(hwnd, Some(&mut pid));
|
||||||
|
if pid == 0 {
|
||||||
|
return AppMetadata {
|
||||||
|
localized: None,
|
||||||
|
unlocalized: None,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
let process_handle = match OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION, false, pid) {
|
||||||
|
Ok(h) if !h.is_invalid() => h,
|
||||||
|
_ => {
|
||||||
|
return AppMetadata {
|
||||||
|
localized: None,
|
||||||
|
unlocalized: None,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
};
|
||||||
|
let mut buffer: [u16; 1024] = [0; 1024];
|
||||||
|
let size = GetModuleFileNameExW(process_handle, None, &mut buffer);
|
||||||
|
let exe_path = if size > 0 {
|
||||||
|
OsString::from_wide(&buffer[..size as usize])
|
||||||
|
} else {
|
||||||
|
return AppMetadata {
|
||||||
|
localized: None,
|
||||||
|
unlocalized: None,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
let exe_path_str = exe_path.to_string_lossy();
|
||||||
|
let exe_name = Path::new(&*exe_path_str)
|
||||||
|
.file_name()
|
||||||
|
.and_then(|s| s.to_str())
|
||||||
|
.unwrap_or("");
|
||||||
|
let is_uwp = exe_name.eq_ignore_ascii_case("ApplicationFrameHost.exe");
|
||||||
|
let (localized, unlocalized) = if is_uwp {
|
||||||
|
(
|
||||||
|
get_window_title(hwnd),
|
||||||
|
Some("ApplicationFrameHost".to_string()),
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
let unlocalized = Path::new(&*exe_path_str)
|
||||||
|
.file_stem()
|
||||||
|
.and_then(|s| s.to_str())
|
||||||
|
.map(|s| s.to_string());
|
||||||
|
let localized = get_file_description(&exe_path_str).or_else(|| unlocalized.clone());
|
||||||
|
(localized, unlocalized)
|
||||||
|
};
|
||||||
|
AppMetadata {
|
||||||
|
localized,
|
||||||
|
unlocalized,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_window_title(hwnd: HWND) -> Option<String> {
|
||||||
|
unsafe {
|
||||||
|
let mut buffer: [u16; 512] = [0; 512];
|
||||||
|
let len = GetWindowTextW(hwnd, &mut buffer);
|
||||||
|
if len == 0 {
|
||||||
|
None
|
||||||
|
} else {
|
||||||
|
Some(
|
||||||
|
OsString::from_wide(&buffer[..len as usize])
|
||||||
|
.to_string_lossy()
|
||||||
|
.into_owned(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_file_description(exe_path: &str) -> Option<String> {
|
||||||
|
unsafe {
|
||||||
|
let wide_path: Vec<u16> = exe_path.encode_utf16().chain(iter::once(0)).collect();
|
||||||
|
let size = windows::Win32::Storage::FileSystem::GetFileVersionInfoSizeW(
|
||||||
|
PCWSTR(wide_path.as_ptr()),
|
||||||
|
None,
|
||||||
|
);
|
||||||
|
if size == 0 {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
let mut buffer: Vec<u8> = vec![0; size as usize];
|
||||||
|
let result = windows::Win32::Storage::FileSystem::GetFileVersionInfoW(
|
||||||
|
PCWSTR(wide_path.as_ptr()),
|
||||||
|
0,
|
||||||
|
size,
|
||||||
|
buffer.as_mut_ptr() as *mut _,
|
||||||
|
);
|
||||||
|
if result.is_err() {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
let mut translations_ptr: *mut u8 = ptr::null_mut();
|
||||||
|
let mut translations_len: u32 = 0;
|
||||||
|
let translation_query: Vec<u16> = "\\VarFileInfo\\Translation"
|
||||||
|
.encode_utf16()
|
||||||
|
.chain(iter::once(0))
|
||||||
|
.collect();
|
||||||
|
let trans_result = windows::Win32::Storage::FileSystem::VerQueryValueW(
|
||||||
|
buffer.as_ptr() as *const _,
|
||||||
|
PCWSTR(translation_query.as_ptr()),
|
||||||
|
&mut translations_ptr as *mut _ as *mut *mut _,
|
||||||
|
&mut translations_len,
|
||||||
|
);
|
||||||
|
let system_lang_id = GetUserDefaultLangID();
|
||||||
|
if trans_result.as_bool() && translations_len >= 4 {
|
||||||
|
let translations = std::slice::from_raw_parts(
|
||||||
|
translations_ptr as *const u16,
|
||||||
|
(translations_len / 2) as usize,
|
||||||
|
);
|
||||||
|
let mut lang_attempts = Vec::new();
|
||||||
|
for i in (0..translations.len()).step_by(2) {
|
||||||
|
if i + 1 < translations.len() {
|
||||||
|
let lang = translations[i];
|
||||||
|
let codepage = translations[i + 1];
|
||||||
|
if lang == system_lang_id {
|
||||||
|
lang_attempts.insert(0, (lang, codepage));
|
||||||
|
} else {
|
||||||
|
lang_attempts.push((lang, codepage));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (lang, codepage) in lang_attempts {
|
||||||
|
let query = format!(
|
||||||
|
"\\StringFileInfo\\{:04x}{:04x}\\FileDescription",
|
||||||
|
lang, codepage
|
||||||
|
);
|
||||||
|
let wide_query: Vec<u16> = query.encode_utf16().chain(iter::once(0)).collect();
|
||||||
|
let mut value_ptr: *mut u8 = ptr::null_mut();
|
||||||
|
let mut value_len: u32 = 0;
|
||||||
|
let query_result = windows::Win32::Storage::FileSystem::VerQueryValueW(
|
||||||
|
buffer.as_ptr() as *const _,
|
||||||
|
PCWSTR(wide_query.as_ptr()),
|
||||||
|
&mut value_ptr as *mut _ as *mut *mut _,
|
||||||
|
&mut value_len,
|
||||||
|
);
|
||||||
|
if query_result.as_bool() && !value_ptr.is_null() && value_len > 0 {
|
||||||
|
let wide_str = std::slice::from_raw_parts(
|
||||||
|
value_ptr as *const u16,
|
||||||
|
(value_len as usize).saturating_sub(1),
|
||||||
|
);
|
||||||
|
let description =
|
||||||
|
OsString::from_wide(wide_str).to_string_lossy().into_owned();
|
||||||
|
if !description.is_empty() {
|
||||||
|
return Some(description);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let fallback_query: Vec<u16> = "\\StringFileInfo\\040904b0\\FileDescription"
|
||||||
|
.encode_utf16()
|
||||||
|
.chain(iter::once(0))
|
||||||
|
.collect();
|
||||||
|
let mut value_ptr: *mut u8 = ptr::null_mut();
|
||||||
|
let mut value_len: u32 = 0;
|
||||||
|
let query_result = windows::Win32::Storage::FileSystem::VerQueryValueW(
|
||||||
|
buffer.as_ptr() as *const _,
|
||||||
|
PCWSTR(fallback_query.as_ptr()),
|
||||||
|
&mut value_ptr as *mut _ as *mut *mut _,
|
||||||
|
&mut value_len,
|
||||||
|
);
|
||||||
|
if query_result.as_bool() && !value_ptr.is_null() && value_len > 0 {
|
||||||
|
let wide_str = std::slice::from_raw_parts(
|
||||||
|
value_ptr as *const u16,
|
||||||
|
(value_len as usize).saturating_sub(1),
|
||||||
|
);
|
||||||
|
Some(OsString::from_wide(wide_str).to_string_lossy().into_owned())
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub static ACTIVE_APP_CHANGED: &str = "active-app-changed";
|
||||||
|
|
||||||
|
pub fn init_active_app_changes_listener() {
|
||||||
|
let app_handle = get_app_handle();
|
||||||
|
listen_for_active_app_changes(|app_names: AppMetadata| {
|
||||||
|
if let Err(e) = app_handle.emit(ACTIVE_APP_CHANGED, app_names) {
|
||||||
|
error!("Failed to emit active app changed event: {}", e);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -1,3 +1,4 @@
|
|||||||
|
pub mod active_app;
|
||||||
pub mod app_menu;
|
pub mod app_menu;
|
||||||
pub mod auth;
|
pub mod auth;
|
||||||
pub mod client_config_manager;
|
pub mod client_config_manager;
|
||||||
|
|||||||
@@ -10,11 +10,14 @@
|
|||||||
import { invoke } from "@tauri-apps/api/core";
|
import { invoke } from "@tauri-apps/api/core";
|
||||||
|
|
||||||
import DesktopPet from "./components/DesktopPet.svelte";
|
import DesktopPet from "./components/DesktopPet.svelte";
|
||||||
|
import { listen } from "@tauri-apps/api/event";
|
||||||
|
import { onMount } from "svelte";
|
||||||
|
import type { AppMetadata } from "../../types/bindings/AppMetadata";
|
||||||
|
|
||||||
let innerWidth = 0;
|
let innerWidth = $state(0);
|
||||||
let innerHeight = 0;
|
let innerHeight = $state(0);
|
||||||
|
|
||||||
$: isInteractive = $sceneInteractive;
|
let isInteractive = $derived($sceneInteractive);
|
||||||
|
|
||||||
function getFriendById(userId: string) {
|
function getFriendById(userId: string) {
|
||||||
const friend = $appData?.friends?.find((f) => f.friend.id === userId);
|
const friend = $appData?.friends?.find((f) => f.friend.id === userId);
|
||||||
@@ -29,6 +32,18 @@
|
|||||||
const friend = $appData?.friends?.find((f) => f.friend.id === userId);
|
const friend = $appData?.friends?.find((f) => f.friend.id === userId);
|
||||||
return friend?.friend.activeDoll;
|
return friend?.friend.activeDoll;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let appMetadata: AppMetadata | null = $state(null);
|
||||||
|
|
||||||
|
onMount(() => {
|
||||||
|
const unlisten = listen<AppMetadata>("active-app-changed", (event) => {
|
||||||
|
appMetadata = event.payload;
|
||||||
|
});
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
unlisten.then((u) => u());
|
||||||
|
};
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<svelte:window bind:innerWidth bind:innerHeight />
|
<svelte:window bind:innerWidth bind:innerHeight />
|
||||||
@@ -63,11 +78,14 @@
|
|||||||
)})
|
)})
|
||||||
</span>
|
</span>
|
||||||
|
|
||||||
|
<span class="font-mono text-xs badge py-3">
|
||||||
|
{appMetadata?.localized}
|
||||||
|
</span>
|
||||||
|
|
||||||
{#if Object.keys($friendsCursorPositions).length > 0}
|
{#if Object.keys($friendsCursorPositions).length > 0}
|
||||||
<div class="flex flex-col gap-2">
|
<div class="flex flex-col gap-2">
|
||||||
<div>
|
<div>
|
||||||
{#each Object.entries($friendsCursorPositions) as [userId, position]}
|
{#each Object.entries($friendsCursorPositions) as [userId, position]}
|
||||||
{@const dollConfig = getFriendDoll(userId)}
|
|
||||||
<div class="badge py-3 text-xs text-left flex flex-row gap-2">
|
<div class="badge py-3 text-xs text-left flex flex-row gap-2">
|
||||||
<span class="font-bold">{getFriendById(userId).name}</span>
|
<span class="font-bold">{getFriendById(userId).name}</span>
|
||||||
<div class="flex flex-col font-mono">
|
<div class="flex flex-col font-mono">
|
||||||
|
|||||||
3
src/types/bindings/AppMetadata.ts
Normal file
3
src/types/bindings/AppMetadata.ts
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||||
|
|
||||||
|
export type AppMetadata = { localized: string | null, unlocalized: string | null, };
|
||||||
Reference in New Issue
Block a user