diff --git a/openless-all/app/package-lock.json b/openless-all/app/package-lock.json index 76fb2f0e..983be211 100644 --- a/openless-all/app/package-lock.json +++ b/openless-all/app/package-lock.json @@ -9,11 +9,9 @@ "version": "2.0.0-Beta.1", "dependencies": { "@base-ui/react": "^1.6.0", - "@formkit/auto-animate": "^0.9.0", "@shadcn/react": "^0.2.0", "@tailwindcss/vite": "^4.3.2", "@tauri-apps/api": "^2.1.1", - "@tauri-apps/plugin-autostart": "^2.5.1", "@tauri-apps/plugin-dialog": "^2.7.2", "@tauri-apps/plugin-shell": "^2.3.5", "@tauri-apps/plugin-updater": "^2.10.1", @@ -1226,12 +1224,6 @@ "integrity": "sha512-RiB/yIh78pcIxl6lLMG0CgBXAZ2Y0eVHqMPYugu+9U0AeT6YBeiJpf7lbdJNIugFP5SIjwNRgo4DhR1Qxi26Gg==", "license": "MIT" }, - "node_modules/@formkit/auto-animate": { - "version": "0.9.0", - "resolved": "https://registry.npmjs.org/@formkit/auto-animate/-/auto-animate-0.9.0.tgz", - "integrity": "sha512-VhP4zEAacXS3dfTpJpJ88QdLqMTcabMg0jwpOSxZ/VzfQVfl3GkZSCZThhGC5uhq/TxPHPzW0dzr4H9Bb1OgKA==", - "license": "MIT" - }, "node_modules/@hono/node-server": { "version": "1.19.14", "resolved": "https://registry.npmjs.org/@hono/node-server/-/node-server-1.19.14.tgz", @@ -2223,15 +2215,6 @@ "node": ">= 10" } }, - "node_modules/@tauri-apps/plugin-autostart": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/@tauri-apps/plugin-autostart/-/plugin-autostart-2.5.1.tgz", - "integrity": "sha512-zS/xx7yzveCcotkA+8TqkI2lysmG2wvQXv2HGAVExITmnFfHAdj1arGsbbfs3o6EktRHf6l34pJxc3YGG2mg7w==", - "license": "MIT OR Apache-2.0", - "dependencies": { - "@tauri-apps/api": "^2.8.0" - } - }, "node_modules/@tauri-apps/plugin-dialog": { "version": "2.7.2", "resolved": "https://registry.npmjs.org/@tauri-apps/plugin-dialog/-/plugin-dialog-2.7.2.tgz", diff --git a/openless-all/app/package.json b/openless-all/app/package.json index 2eb0ca1d..5494533a 100644 --- a/openless-all/app/package.json +++ b/openless-all/app/package.json @@ -34,11 +34,9 @@ }, "dependencies": { "@base-ui/react": "^1.6.0", - "@formkit/auto-animate": "^0.9.0", "@shadcn/react": "^0.2.0", "@tailwindcss/vite": "^4.3.2", "@tauri-apps/api": "^2.1.1", - "@tauri-apps/plugin-autostart": "^2.5.1", "@tauri-apps/plugin-dialog": "^2.7.2", "@tauri-apps/plugin-shell": "^2.3.5", "@tauri-apps/plugin-updater": "^2.10.1", diff --git a/openless-all/app/src-tauri/src/android/types.rs b/openless-all/app/src-tauri/src/android/types.rs deleted file mode 100644 index 49d8bbc3..00000000 --- a/openless-all/app/src-tauri/src/android/types.rs +++ /dev/null @@ -1,380 +0,0 @@ -//! Android-specific preference types and status payloads. - -use serde::{Deserialize, Serialize}; - -#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)] -#[serde(rename_all = "camelCase")] -pub enum AndroidInsertStrategy { - Auto, - Ime, - Accessibility, - Clipboard, -} - -#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)] -#[serde(rename_all = "camelCase")] -pub enum AndroidOverlayTrigger { - Background, - Keyboard, - Always, -} - -impl AndroidOverlayTrigger { - pub fn normalized(self) -> Self { - match self { - AndroidOverlayTrigger::Keyboard => AndroidOverlayTrigger::Background, - trigger => trigger, - } - } -} - -#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)] -#[serde(rename_all = "snake_case")] -pub enum AndroidOverlayActivationMode { - Tap, - LongPress, -} - -#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)] -#[serde(rename_all = "snake_case")] -pub enum AndroidOverlayLeftSwipeAction { - Translation, - StylePack, -} - -#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)] -#[serde(rename_all = "snake_case")] -pub enum AndroidOverlayCancelSwipeDirection { - Up, - Down, -} - -#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)] -#[serde(rename_all = "camelCase")] -pub enum AndroidAccessibilityState { - Enabled, - NotEnabled, - NotAndroid, -} - -#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] -#[serde(rename_all = "camelCase")] -pub struct AndroidAccessibilityStatus { - pub state: AndroidAccessibilityState, - pub enabled: bool, - #[serde(default)] - pub operational: bool, - #[serde(default)] - pub message: String, - pub message_key: String, -} - -#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)] -#[serde(rename_all = "camelCase")] -pub enum AndroidShizukuState { - NotInstalled, - NotRunning, - NotAuthorized, - Authorized, - BinderDead, - NotAndroid, -} - -#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] -#[serde(rename_all = "camelCase")] -pub struct AndroidAccessibilityDiagnosis { - pub registered: bool, - pub operational: bool, - #[serde(default)] - pub message: String, - pub message_key: String, -} - -#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] -#[serde(rename_all = "camelCase")] -pub struct AndroidShizukuStatus { - pub state: AndroidShizukuState, - #[serde(default)] - pub message: String, - pub message_key: String, - pub accessibility: AndroidAccessibilityDiagnosis, - #[serde(default, skip_serializing_if = "Option::is_none")] - pub last_permission_message_key: Option, -} - -#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)] -#[serde(rename_all = "camelCase")] -pub enum AndroidAccessibilityRecoveryOutcome { - Success, - WriteRejected, - ServiceNotBound, - ShizukuUnavailable, - UserNotConfirmed, - ShellFailed, -} - -#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] -#[serde(rename_all = "camelCase")] -pub struct AndroidAccessibilityRecoveryResult { - pub outcome: AndroidAccessibilityRecoveryOutcome, - #[serde(default)] - pub message: String, - pub message_key: String, -} - -pub fn default_android_insert_strategy() -> AndroidInsertStrategy { - AndroidInsertStrategy::Accessibility -} - -pub fn default_android_overlay_trigger() -> AndroidOverlayTrigger { - AndroidOverlayTrigger::Background -} - -pub fn default_android_overlay_activation_mode() -> AndroidOverlayActivationMode { - AndroidOverlayActivationMode::Tap -} - -pub fn default_android_overlay_left_swipe_action() -> AndroidOverlayLeftSwipeAction { - AndroidOverlayLeftSwipeAction::Translation -} - -pub fn default_android_overlay_cancel_swipe_direction() -> AndroidOverlayCancelSwipeDirection { - AndroidOverlayCancelSwipeDirection::Up -} - -pub fn default_android_overlay_size_dp() -> u32 { - 72 -} - -pub fn normalize_android_insert_strategy(strategy: AndroidInsertStrategy) -> AndroidInsertStrategy { - match strategy { - AndroidInsertStrategy::Auto | AndroidInsertStrategy::Ime => { - AndroidInsertStrategy::Accessibility - } - strategy => strategy, - } -} - -pub fn normalize_android_overlay_size_dp(size_dp: u32) -> u32 { - size_dp.clamp(48, 120) -} - -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub enum AndroidOverlaySettingsAction { - None, - RefreshLayout, - Transition { - from: AndroidOverlayTrigger, - to: AndroidOverlayTrigger, - }, -} - -pub fn classify_android_overlay_settings_change( - previous: &super::UserPreferences, - next: &super::UserPreferences, -) -> AndroidOverlaySettingsAction { - let trigger_changed = - previous.android_overlay_trigger.normalized() != next.android_overlay_trigger.normalized(); - let size_changed = normalize_android_overlay_size_dp(previous.android_overlay_size_dp) - != normalize_android_overlay_size_dp(next.android_overlay_size_dp); - - if trigger_changed { - return AndroidOverlaySettingsAction::Transition { - from: previous.android_overlay_trigger.normalized(), - to: next.android_overlay_trigger.normalized(), - }; - } - - if size_changed { - return AndroidOverlaySettingsAction::RefreshLayout; - } - - AndroidOverlaySettingsAction::None -} - -#[cfg(test)] -mod android_overlay_tests { - use super::*; - use crate::types::UserPreferences; - - fn overlay_prefs( - trigger: AndroidOverlayTrigger, - size_dp: u32, - activation: AndroidOverlayActivationMode, - ) -> UserPreferences { - let mut prefs = UserPreferences::default(); - prefs.android_overlay_trigger = trigger; - prefs.android_overlay_size_dp = size_dp; - prefs.android_overlay_activation_mode = activation; - prefs - } - - #[test] - fn size_only_change_returns_refresh_layout() { - let previous = overlay_prefs( - AndroidOverlayTrigger::Always, - 72, - AndroidOverlayActivationMode::Tap, - ); - let next = overlay_prefs( - AndroidOverlayTrigger::Always, - 96, - AndroidOverlayActivationMode::Tap, - ); - assert_eq!( - classify_android_overlay_settings_change(&previous, &next), - AndroidOverlaySettingsAction::RefreshLayout, - ); - } - - #[test] - fn trigger_only_change_returns_transition() { - let previous = overlay_prefs( - AndroidOverlayTrigger::Background, - 72, - AndroidOverlayActivationMode::Tap, - ); - let next = overlay_prefs( - AndroidOverlayTrigger::Always, - 72, - AndroidOverlayActivationMode::Tap, - ); - assert_eq!( - classify_android_overlay_settings_change(&previous, &next), - AndroidOverlaySettingsAction::Transition { - from: AndroidOverlayTrigger::Background, - to: AndroidOverlayTrigger::Always, - }, - ); - } - - #[test] - fn trigger_and_size_change_returns_transition_only() { - let previous = overlay_prefs( - AndroidOverlayTrigger::Background, - 72, - AndroidOverlayActivationMode::Tap, - ); - let next = overlay_prefs( - AndroidOverlayTrigger::Always, - 96, - AndroidOverlayActivationMode::Tap, - ); - assert_eq!( - classify_android_overlay_settings_change(&previous, &next), - AndroidOverlaySettingsAction::Transition { - from: AndroidOverlayTrigger::Background, - to: AndroidOverlayTrigger::Always, - }, - ); - } - - #[test] - fn activation_only_change_returns_none() { - let previous = overlay_prefs( - AndroidOverlayTrigger::Always, - 72, - AndroidOverlayActivationMode::Tap, - ); - let next = overlay_prefs( - AndroidOverlayTrigger::Always, - 72, - AndroidOverlayActivationMode::LongPress, - ); - assert_eq!( - classify_android_overlay_settings_change(&previous, &next), - AndroidOverlaySettingsAction::None, - ); - } - - #[test] - fn out_of_bounds_size_200_to_120_returns_none_after_normalize() { - let previous = overlay_prefs( - AndroidOverlayTrigger::Always, - 200, - AndroidOverlayActivationMode::Tap, - ); - let next = overlay_prefs( - AndroidOverlayTrigger::Always, - 120, - AndroidOverlayActivationMode::Tap, - ); - assert_eq!( - classify_android_overlay_settings_change(&previous, &next), - AndroidOverlaySettingsAction::None, - ); - } - - #[test] - fn out_of_bounds_size_below_min_normalizes_to_same_returns_none() { - let previous = overlay_prefs( - AndroidOverlayTrigger::Always, - 30, - AndroidOverlayActivationMode::Tap, - ); - let next = overlay_prefs( - AndroidOverlayTrigger::Always, - 48, - AndroidOverlayActivationMode::Tap, - ); - assert_eq!( - classify_android_overlay_settings_change(&previous, &next), - AndroidOverlaySettingsAction::None, - ); - } - - #[test] - fn identical_normalized_size_returns_none() { - let previous = overlay_prefs( - AndroidOverlayTrigger::Always, - 72, - AndroidOverlayActivationMode::Tap, - ); - let next = overlay_prefs( - AndroidOverlayTrigger::Always, - 72, - AndroidOverlayActivationMode::Tap, - ); - assert_eq!( - classify_android_overlay_settings_change(&previous, &next), - AndroidOverlaySettingsAction::None, - ); - } - - #[test] - fn keyboard_trigger_normalizes_to_background_for_transition() { - let previous = overlay_prefs( - AndroidOverlayTrigger::Keyboard, - 72, - AndroidOverlayActivationMode::Tap, - ); - let next = overlay_prefs( - AndroidOverlayTrigger::Always, - 72, - AndroidOverlayActivationMode::Tap, - ); - assert_eq!( - classify_android_overlay_settings_change(&previous, &next), - AndroidOverlaySettingsAction::Transition { - from: AndroidOverlayTrigger::Background, - to: AndroidOverlayTrigger::Always, - }, - ); - } -} - -#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)] -#[serde(rename_all = "camelCase")] -pub enum AndroidOverlayPermissionState { - Granted, - NotGranted, - NotAndroid, -} - -#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] -#[serde(rename_all = "camelCase")] -pub struct AndroidOverlayStatus { - pub permission: AndroidOverlayPermissionState, - pub overlay_visible: bool, - pub message: String, -} diff --git a/openless-all/app/src-tauri/src/polish/output_cleaning.rs b/openless-all/app/src-tauri/src/polish/output_cleaning.rs deleted file mode 100644 index 4cf3a496..00000000 --- a/openless-all/app/src-tauri/src/polish/output_cleaning.rs +++ /dev/null @@ -1,3 +0,0 @@ -//! Compatibility re-export of shared LLM output sanitization. - -pub(crate) use openless_core::output_cleaning::*; diff --git a/openless-all/app/src-tauri/src/polish/prompt_compose.rs b/openless-all/app/src-tauri/src/polish/prompt_compose.rs deleted file mode 100644 index 4b920aee..00000000 --- a/openless-all/app/src-tauri/src/polish/prompt_compose.rs +++ /dev/null @@ -1,7 +0,0 @@ -//! Tauri compatibility re-exports for framework-independent prompt composition. - -pub(crate) use openless_core::{ - assemble_polish_system_prompt, build_hotword_block, compose_hotword_block_preview, - compose_polish_prompts, compose_qa_system_prompt, compose_system_prompt, - compose_translate_prompts, context_premise, PolishSystemPromptAssembly, -}; diff --git a/openless-all/app/src-tauri/src/remote_server/lan_addresses.rs b/openless-all/app/src-tauri/src/remote_server/lan_addresses.rs deleted file mode 100644 index 23370a30..00000000 --- a/openless-all/app/src-tauri/src/remote_server/lan_addresses.rs +++ /dev/null @@ -1,404 +0,0 @@ -use std::fs::{self, File, OpenOptions}; -use std::io::{self, Read, Write}; -use std::net::{IpAddr, Ipv4Addr, SocketAddr, UdpSocket}; -use std::path::Path; -use std::sync::atomic::{AtomicBool, Ordering}; -use std::sync::mpsc; -use std::thread; -use std::time::Duration; - -use uuid::Uuid; - -const CACHE_FILE: &str = "remote-input-ips-v1.txt"; -const MAX_CACHE_BYTES: u64 = 1024; -const MAX_CACHE_IPS: usize = 32; -const ENUMERATION_TIMEOUT: Duration = Duration::from_secs(1); -const ROUTE_DESTINATIONS: [&str; 4] = [ - "8.8.8.8:80", - "1.1.1.1:80", - "192.168.8.1:80", - "192.168.1.1:80", -]; - -static ENUMERATION_IN_FLIGHT: AtomicBool = AtomicBool::new(false); - -#[derive(Debug, Clone, PartialEq, Eq)] -pub(crate) struct LanAddressSnapshot { - pub(crate) ips: Vec, - pub(crate) stale: bool, -} - -pub(crate) fn normalize_lan_ips(ips: I) -> Vec -where - I: IntoIterator, -{ - let mut normalized = ips.into_iter().filter(is_private_lan).collect::>(); - normalized.sort_unstable(); - normalized.dedup(); - normalized.truncate(MAX_CACHE_IPS); - normalized -} - -pub(crate) fn discover_lan_addresses(config_dir: Option<&Path>) -> LanAddressSnapshot { - let live_ips = enumerate_interfaces_with_timeout(); - if let Some(ips) = live_ips.as_ref().filter(|ips| !ips.is_empty()) { - if let Some(dir) = config_dir { - if let Err(error) = persist_cached_ips(dir, ips) { - log::warn!("[remote-input] persist LAN IP cache failed: {error}"); - } - } - } - let cached_ips = if live_ips.as_ref().is_some_and(|ips| !ips.is_empty()) { - None - } else { - config_dir.and_then(load_cached_ips) - }; - let route_ips = if live_ips.as_ref().is_some_and(|ips| !ips.is_empty()) - || cached_ips.as_ref().is_some_and(|ips| !ips.is_empty()) - { - Vec::new() - } else { - local_lan_ipv4s_from_route() - }; - let snapshot = choose_snapshot(live_ips, cached_ips, route_ips); - if snapshot.stale { - log::info!("[remote-input] using cached LAN IPs after interface discovery timeout"); - } - snapshot -} - -fn choose_snapshot( - live_ips: Option>, - cached_ips: Option>, - route_ips: Vec, -) -> LanAddressSnapshot { - if let Some(ips) = live_ips.filter(|ips| !ips.is_empty()) { - return LanAddressSnapshot { ips, stale: false }; - } - if let Some(ips) = cached_ips.filter(|ips| !ips.is_empty()) { - return LanAddressSnapshot { ips, stale: true }; - } - LanAddressSnapshot { - ips: normalize_lan_ips(route_ips), - stale: false, - } -} - -fn is_private_lan(ip: &Ipv4Addr) -> bool { - let octets = ip.octets(); - !ip.is_loopback() - && !ip.is_link_local() - && ((octets[0] == 192 && octets[1] == 168) - || octets[0] == 10 - || (octets[0] == 172 && (16..=31).contains(&octets[1]))) -} - -fn enumerate_interfaces_with_timeout() -> Option> { - if ENUMERATION_IN_FLIGHT - .compare_exchange(false, true, Ordering::AcqRel, Ordering::Acquire) - .is_err() - { - return None; - } - - let (tx, rx) = mpsc::sync_channel(1); - let worker = thread::Builder::new() - .name("openless-lan-addresses".to_string()) - .spawn(move || { - struct ResetInFlight; - - impl Drop for ResetInFlight { - fn drop(&mut self) { - ENUMERATION_IN_FLIGHT.store(false, Ordering::Release); - } - } - - let _reset = ResetInFlight; - let result = enumerate_interfaces(); - let _ = tx.send(result); - }); - - if worker.is_err() { - ENUMERATION_IN_FLIGHT.store(false, Ordering::Release); - return None; - } - - match rx.recv_timeout(ENUMERATION_TIMEOUT) { - Ok(Ok(ips)) if !ips.is_empty() => Some(ips), - Ok(Ok(_)) | Ok(Err(_)) | Err(_) => None, - } -} - -fn enumerate_interfaces() -> Result, String> { - let ifaces = local_ip_address::list_afinet_netifas().map_err(|error| error.to_string())?; - let ips = ifaces.into_iter().filter_map(|(_, ip)| match ip { - IpAddr::V4(ip) => Some(ip), - IpAddr::V6(_) => None, - }); - Ok(normalize_lan_ips(ips)) -} - -fn local_lan_ipv4s_from_route() -> Vec { - let mut ips = Vec::new(); - for destination in ROUTE_DESTINATIONS { - let Ok(socket) = UdpSocket::bind("0.0.0.0:0") else { - continue; - }; - let _ = socket.set_write_timeout(Some(Duration::from_millis(200))); - if socket.connect(destination).is_err() { - continue; - } - if let Ok(SocketAddr::V4(address)) = socket.local_addr() { - ips.push(*address.ip()); - } - } - normalize_lan_ips(ips) -} - -fn cache_path(config_dir: &Path) -> std::path::PathBuf { - config_dir.join(CACHE_FILE) -} - -fn load_cached_ips(config_dir: &Path) -> Option> { - let path = cache_path(config_dir); - let mut file = File::open(path).ok()?; - let mut contents = Vec::new(); - std::io::Read::by_ref(&mut file) - .take(MAX_CACHE_BYTES + 1) - .read_to_end(&mut contents) - .ok()?; - if contents.len() as u64 > MAX_CACHE_BYTES { - return None; - } - parse_cached_ips(&contents).ok() -} - -fn parse_cached_ips(contents: &[u8]) -> Result, String> { - let text = std::str::from_utf8(contents).map_err(|_| "cache is not UTF-8".to_string())?; - let mut ips = Vec::new(); - for line in text.lines() { - if line.trim().is_empty() { - return Err("cache contains an empty line".to_string()); - } - if ips.len() >= MAX_CACHE_IPS { - return Err("cache contains too many addresses".to_string()); - } - let ip = line - .trim() - .parse::() - .map_err(|_| "cache contains an invalid IPv4 address".to_string())?; - if !is_private_lan(&ip) { - return Err("cache contains a non-private IPv4 address".to_string()); - } - ips.push(ip); - } - let normalized = normalize_lan_ips(ips); - if normalized.is_empty() { - return Err("cache contains no LAN addresses".to_string()); - } - Ok(normalized) -} - -fn persist_cached_ips(config_dir: &Path, ips: &[Ipv4Addr]) -> io::Result<()> { - fs::create_dir_all(config_dir)?; - let path = cache_path(config_dir); - let temp_path = path.with_file_name(format!(".{CACHE_FILE}.tmp-{}", Uuid::new_v4().simple())); - let result = (|| { - let mut options = OpenOptions::new(); - options.write(true).create_new(true); - #[cfg(unix)] - { - use std::os::unix::fs::OpenOptionsExt; - options.mode(0o600); - } - let mut file = options.open(&temp_path)?; - for ip in ips { - writeln!(file, "{ip}")?; - } - file.sync_all()?; - drop(file); - replace_cache_file(&temp_path, &path) - })(); - if result.is_err() { - let _ = fs::remove_file(&temp_path); - } - result -} - -#[cfg(unix)] -fn replace_cache_file(temp_path: &Path, path: &Path) -> io::Result<()> { - fs::rename(temp_path, path) -} - -#[cfg(target_os = "windows")] -fn replace_cache_file(temp_path: &Path, path: &Path) -> io::Result<()> { - use windows::core::PCWSTR; - use windows::Win32::Storage::FileSystem::{ - MoveFileExW, ReplaceFileW, MOVEFILE_REPLACE_EXISTING, MOVEFILE_WRITE_THROUGH, - REPLACE_FILE_FLAGS, - }; - - let replacement = windows_wide_path(temp_path); - let destination = windows_wide_path(path); - if path.exists() { - let backup = - path.with_file_name(format!(".{CACHE_FILE}.backup-{}", Uuid::new_v4().simple())); - let backup_wide = windows_wide_path(&backup); - let replaced = unsafe { - ReplaceFileW( - PCWSTR(destination.as_ptr()), - PCWSTR(replacement.as_ptr()), - PCWSTR(backup_wide.as_ptr()), - REPLACE_FILE_FLAGS(0), - None, - None, - ) - }; - replaced.map_err(windows_io_error)?; - let _ = fs::remove_file(backup); - Ok(()) - } else { - unsafe { - MoveFileExW( - PCWSTR(replacement.as_ptr()), - PCWSTR(destination.as_ptr()), - MOVEFILE_REPLACE_EXISTING | MOVEFILE_WRITE_THROUGH, - ) - } - .map_err(windows_io_error) - } -} - -#[cfg(not(any(unix, target_os = "windows")))] -fn replace_cache_file(temp_path: &Path, path: &Path) -> io::Result<()> { - fs::rename(temp_path, path) -} - -#[cfg(target_os = "windows")] -fn windows_wide_path(path: &Path) -> Vec { - use std::os::windows::ffi::OsStrExt; - - path.as_os_str().encode_wide().chain(Some(0)).collect() -} - -#[cfg(target_os = "windows")] -fn windows_io_error(error: windows::core::Error) -> io::Error { - io::Error::new(io::ErrorKind::Other, error) -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn normalize_lan_ips_filters_private_addresses_and_deduplicates() { - let actual = normalize_lan_ips([ - Ipv4Addr::new(192, 168, 1, 20), - Ipv4Addr::new(8, 8, 8, 8), - Ipv4Addr::new(10, 0, 0, 2), - Ipv4Addr::new(192, 168, 1, 20), - Ipv4Addr::new(172, 16, 4, 9), - Ipv4Addr::new(127, 0, 0, 1), - ]); - - assert_eq!( - actual, - vec![ - Ipv4Addr::new(10, 0, 0, 2), - Ipv4Addr::new(172, 16, 4, 9), - Ipv4Addr::new(192, 168, 1, 20), - ] - ); - } - - #[test] - fn parse_cached_ips_rejects_public_or_malformed_entries() { - assert!(parse_cached_ips(b"8.8.8.8\n").is_err()); - assert!(parse_cached_ips(b"not-an-ip\n").is_err()); - assert!(parse_cached_ips(b"10.0.0.2\n\n10.0.0.3\n").is_err()); - } - - #[test] - fn parse_cached_ips_accepts_bounded_private_entries() { - assert_eq!( - parse_cached_ips(b"192.168.1.20\n10.0.0.2\n192.168.1.20\n").unwrap(), - vec![Ipv4Addr::new(10, 0, 0, 2), Ipv4Addr::new(192, 168, 1, 20)] - ); - } - - #[test] - fn choose_snapshot_prefers_live_then_cache_then_route() { - let live = vec![Ipv4Addr::new(10, 0, 0, 2)]; - let cached = vec![Ipv4Addr::new(192, 168, 1, 20)]; - let route = vec![Ipv4Addr::new(172, 16, 0, 3)]; - - assert_eq!( - choose_snapshot(Some(live.clone()), Some(cached.clone()), route.clone()), - LanAddressSnapshot { - ips: live, - stale: false, - } - ); - assert_eq!( - choose_snapshot(None, Some(cached.clone()), route.clone()), - LanAddressSnapshot { - ips: cached, - stale: true, - } - ); - assert_eq!( - choose_snapshot(None, None, route.clone()), - LanAddressSnapshot { - ips: route, - stale: false, - } - ); - assert_eq!( - choose_snapshot(None, None, Vec::new()), - LanAddressSnapshot { - ips: Vec::new(), - stale: false, - } - ); - } - - #[test] - fn cache_rejects_more_than_the_maximum_number_of_entries() { - let contents = (0..=MAX_CACHE_IPS) - .map(|_| "10.0.0.2") - .collect::>() - .join("\n"); - assert!(parse_cached_ips(contents.as_bytes()).is_err()); - } - - #[test] - fn oversized_cache_file_is_rejected_before_parsing() { - let directory = std::env::temp_dir().join(format!( - "openless-lan-addresses-oversized-{}", - Uuid::new_v4().simple() - )); - fs::create_dir_all(&directory).unwrap(); - fs::write( - cache_path(&directory), - vec![b'1'; (MAX_CACHE_BYTES + 1) as usize], - ) - .unwrap(); - - assert_eq!(load_cached_ips(&directory), None); - let _ = fs::remove_dir_all(directory); - } - - #[test] - fn cached_ips_round_trip_through_atomic_file() { - let directory = std::env::temp_dir().join(format!( - "openless-lan-addresses-test-{}", - Uuid::new_v4().simple() - )); - let ips = vec![Ipv4Addr::new(10, 0, 0, 2), Ipv4Addr::new(192, 168, 1, 20)]; - - persist_cached_ips(&directory, &ips).unwrap(); - - assert_eq!(load_cached_ips(&directory), Some(ips)); - let _ = fs::remove_dir_all(directory); - } -} diff --git a/openless-all/app/src/components/chat/index.ts b/openless-all/app/src/components/chat/index.ts deleted file mode 100644 index 647fd8d2..00000000 --- a/openless-all/app/src/components/chat/index.ts +++ /dev/null @@ -1,64 +0,0 @@ -// 统一聊天面板组件库出口。 -// -// 官方 shadcn base-rhea 组件(ui/ 目录源码按官方 Manual 安装 1:1 内嵌,行为来自 -// @shadcn/react;两个面板 = 官方 message-scroller-demo 同款骨架): -// · 骨架 Card* / Empty* / InputGroup* —— 对话框壳、空状态、输入组。 -// · 滚动框 MessageScroller* —— 两个面板共用的「扫描框」。 -// · 消息 Message*/Bubble* —— 划词追问(QA)的对话形态(带头像)。 -// · 标记 Marker* —— Less Computer 的工具调用 / 上下文压缩标签形态(无头像)。 -// 本仓库组装层:ThinkingOrb/OrbAvatar(胶囊同款思考动画)、UserAvatar(GitHub)、 -// AssistantMarkdown(流式 markdown)、useChatPanelLifecycle(出现/消失动画信号)。 - -export { Button, buttonVariants } from './ui/button'; -export { Input } from './ui/input'; -export { Textarea } from './ui/textarea'; -export { Spinner } from './ui/spinner'; -export { - Card, - CardAction, - CardContent, - CardDescription, - CardFooter, - CardHeader, - CardTitle, -} from './ui/card'; -export { - Empty, - EmptyContent, - EmptyDescription, - EmptyHeader, - EmptyMedia, - EmptyTitle, -} from './ui/empty'; -export { - InputGroup, - InputGroupAddon, - InputGroupButton, - InputGroupInput, - InputGroupText, - InputGroupTextarea, -} from './ui/input-group'; -export { - MessageScroller, - MessageScrollerButton, - MessageScrollerContent, - MessageScrollerItem, - MessageScrollerProvider, - MessageScrollerViewport, - useMessageScroller, - useMessageScrollerScrollable, - useMessageScrollerVisibility, -} from './ui/message-scroller'; -export { - Message, - MessageAvatar, - MessageContent, - MessageFooter, - MessageGroup, - MessageHeader, -} from './ui/message'; -export { Bubble, BubbleContent, BubbleGroup } from './ui/bubble'; -export { Marker, MarkerContent, MarkerIcon, markerVariants } from './ui/marker'; -export { ThinkingOrb, OrbAvatar, UserAvatar, useGithubLogin } from './avatars'; -export { AssistantMarkdown } from './markdown'; -export { useChatPanelLifecycle } from './lifecycle'; diff --git a/openless-all/app/src/components/ui/SegSimple.tsx b/openless-all/app/src/components/ui/SegSimple.tsx deleted file mode 100644 index f75fef9c..00000000 --- a/openless-all/app/src/components/ui/SegSimple.tsx +++ /dev/null @@ -1,33 +0,0 @@ -// SegSimple — segmented control used in the Settings modal sub-sections. - -import { useState } from 'react'; -import { segmentedTrackStyle } from '../../pages/settings/shared'; - -interface SegSimpleProps { - options: string[]; - active: string; -} - -export function SegSimple({ options, active }: SegSimpleProps) { - const [v, setV] = useState(active); - return ( -
- {options.map((o) => ( - - ))} -
- ); -} diff --git a/openless-all/app/src/components/ui/SwitchLite.tsx b/openless-all/app/src/components/ui/SwitchLite.tsx deleted file mode 100644 index 56181127..00000000 --- a/openless-all/app/src/components/ui/SwitchLite.tsx +++ /dev/null @@ -1,32 +0,0 @@ -// SwitchLite — small toggle used in the Settings modal sub-sections. - -import { useState } from 'react'; - -interface SwitchLiteProps { - on?: boolean; -} - -export function SwitchLite({ on: initial = false }: SwitchLiteProps) { - const [on, setOn] = useState(initial); - return ( - - ); -} diff --git a/openless-all/app/src/lib/androidMicrophonePermission.ts b/openless-all/app/src/lib/androidMicrophonePermission.ts deleted file mode 100644 index 74490bd6..00000000 --- a/openless-all/app/src/lib/androidMicrophonePermission.ts +++ /dev/null @@ -1 +0,0 @@ -export * from '@android/lib/androidMicrophonePermission'; diff --git a/openless-all/app/src/pages/settings/SelectionPolishSection.tsx b/openless-all/app/src/pages/settings/SelectionPolishSection.tsx deleted file mode 100644 index 4e443aed..00000000 --- a/openless-all/app/src/pages/settings/SelectionPolishSection.tsx +++ /dev/null @@ -1,82 +0,0 @@ -// 通用 → 选区润色:这是与录音输入并列的独立入口,快捷键、交付方式均不再藏在快捷键列表中。 - -import type { PlatformCapabilities, SelectionPolishOutputMode } from '../../lib/types'; -import { useEffect, useState } from 'react'; -import { useTranslation } from 'react-i18next'; -import { ShortcutRecorder } from '../../components/ShortcutRecorder'; -import { defaultSelectionPolishShortcut } from '../../lib/hotkey'; -import { setSelectionPolishHotkey } from '../../lib/ipc'; -import { getPlatformCapabilities } from '../../lib/platform'; -import { useHotkeySettings } from '../../state/HotkeySettingsContext'; -import { Card } from '../_atoms'; -import { SectionTitle, SettingRow, chipSelectedStyle, segmentedTrackStyle } from './shared'; - -const outputOptions: Array<{ value: SelectionPolishOutputMode }> = [ - { value: 'directReplace' }, - { value: 'previewConfirm' }, -]; - -export function SelectionPolishSection() { - const { t } = useTranslation(); - const { prefs, capability, refresh, updatePrefs } = useHotkeySettings(); - const [platformCaps, setPlatformCaps] = useState(null); - - useEffect(() => { void getPlatformCapabilities().then(setPlatformCaps); }, []); - - // 选区润色的安全替换:Windows 用前台窗口/焦点控件校验,macOS 用前台应用 + - // 选区文本指纹校验;两者都具备后才提供设置入口(Linux 热键接入后同样可用)。 - if (!prefs || !capability || !platformCaps?.supportsDesktopHotkey) return null; - - return ( - - - {t('settings.selectionPolish.title', '选区润色')} - - - { - await setSelectionPolishHotkey(binding); - await refresh(); - }} - onDisable={async () => { - await setSelectionPolishHotkey(null); - await refresh(); - }} - // 「重置」= 恢复默认快捷键,等价于旧版独立「启用」按钮。 - onReset={async () => { - await setSelectionPolishHotkey(defaultSelectionPolishShortcut()); - await refresh(); - }} - /> - - -
- {outputOptions.map(option => { - const selected = prefs.selectionPolishOutputMode === option.value; - return ( - - ); - })} -
-
-
- ); -} diff --git a/openless-all/app/src/types/tauri-plugin-autostart.d.ts b/openless-all/app/src/types/tauri-plugin-autostart.d.ts deleted file mode 100644 index c543a7f2..00000000 --- a/openless-all/app/src/types/tauri-plugin-autostart.d.ts +++ /dev/null @@ -1,5 +0,0 @@ -declare module '@tauri-apps/plugin-autostart' { - export function enable(): Promise; - export function disable(): Promise; - export function isEnabled(): Promise; -}