diff --git a/clients/desktop/src-tauri/Cargo.toml b/clients/desktop/src-tauri/Cargo.toml index f6b13a4..a346bca 100644 --- a/clients/desktop/src-tauri/Cargo.toml +++ b/clients/desktop/src-tauri/Cargo.toml @@ -42,8 +42,10 @@ windows = { version = "0.58", features = [ "Win32_System_CorrelationVector", # gate fuer CF_CALLBACK_INFO / CfExecute / CfConnectSyncRoot "Win32_UI_Shell", "Win32_Security", + "Win32_System_Registry", ] } widestring = "1" +winreg = "0.52" # Linux: FUSE-basiertes Virtual-Filesystem (optional, cargo build --features linux_fuse) [target.'cfg(target_os = "linux")'.dependencies] diff --git a/clients/desktop/src-tauri/src/cloud_files/mod.rs b/clients/desktop/src-tauri/src/cloud_files/mod.rs index 52e8a5e..ca18248 100644 --- a/clients/desktop/src-tauri/src/cloud_files/mod.rs +++ b/clients/desktop/src-tauri/src/cloud_files/mod.rs @@ -43,6 +43,8 @@ pub enum SyncState { #[cfg(windows)] pub mod windows; +#[cfg(windows)] +pub mod shell_integration; #[cfg(all(target_os = "linux", feature = "linux_fuse"))] pub mod linux; pub mod sync_loop; diff --git a/clients/desktop/src-tauri/src/cloud_files/shell_integration.rs b/clients/desktop/src-tauri/src/cloud_files/shell_integration.rs new file mode 100644 index 0000000..0e24bab --- /dev/null +++ b/clients/desktop/src-tauri/src/cloud_files/shell_integration.rs @@ -0,0 +1,143 @@ +//! Explorer-Sidebar-Integration fuer Windows (ohne Admin-Rechte). +//! +//! Registriert den Sync-Ordner als Shell-Namespace-Extension unter +//! HKEY_CURRENT_USER, sodass er mit eigenem Icon in der Navigation +//! des Datei-Explorers erscheint (wie OneDrive/Dropbox). +//! +//! Anders als die eigentliche Cloud Files API ist das reine Registry- +//! Kosmetik - der Ordner funktioniert auch ohne Sidebar-Eintrag, +//! nur sieht man ihn dann nicht in der linken Leiste. + +#![cfg(windows)] + +use std::path::Path; +use winreg::enums::*; +use winreg::RegKey; + +// Stabile GUID fuer Mini-Cloud - gleiche wie in windows.rs als ProviderId. +const CLSID_GUID: &str = "{4D696E69-436C-6F75-6444-7566667944AB}"; + +// Standard-CLSID fuer "Generic Shell Folder Implementation". +const SHELL_FOLDER_CLSID: &str = "{0E5AAE11-A475-4c5b-AB00-C66DE400274E}"; + +/// Registriere den Mount-Ordner in der Explorer-Navigation. +/// `icon_source`: Pfad zu ICO oder EXE mit Icon-Index (z.B. "C:\\app.exe,0") +pub fn install( + display_name: &str, + mount_point: &Path, + icon_source: &str, +) -> Result<(), String> { + let hkcu = RegKey::predef(HKEY_CURRENT_USER); + + // 1) CLSID-Eintrag unter Software\Classes\CLSID\{GUID} + let clsid_path = format!("Software\\Classes\\CLSID\\{}", CLSID_GUID); + let (clsid, _) = hkcu + .create_subkey(&clsid_path) + .map_err(|e| format!("create CLSID: {e}"))?; + clsid + .set_value("", &display_name.to_string()) + .map_err(|e| format!("set displayname: {e}"))?; + clsid + .set_value("System.IsPinnedToNameSpaceTree", &1u32) + .map_err(|e| format!("set pinned: {e}"))?; + clsid + .set_value("SortOrderIndex", &0x42u32) + .map_err(|e| format!("set sortorder: {e}"))?; + + // 2) DefaultIcon + let (icon_key, _) = clsid + .create_subkey("DefaultIcon") + .map_err(|e| format!("create DefaultIcon: {e}"))?; + icon_key + .set_value("", &icon_source.to_string()) + .map_err(|e| format!("set icon: {e}"))?; + + // 3) InProcServer32 -> shell32.dll (Standard Shell-Folder-Host) + let (inproc, _) = clsid + .create_subkey("InProcServer32") + .map_err(|e| format!("create InProcServer32: {e}"))?; + inproc + .set_value("", &"%SystemRoot%\\system32\\shell32.dll".to_string()) + .map_err(|e| format!("set shell32: {e}"))?; + inproc + .set_value("ThreadingModel", &"Both".to_string()) + .map_err(|e| format!("set threading: {e}"))?; + + // 4) Instance -> zeigt auf generischen Shell-Folder + let (instance, _) = clsid + .create_subkey("Instance") + .map_err(|e| format!("create Instance: {e}"))?; + instance + .set_value("CLSID", &SHELL_FOLDER_CLSID.to_string()) + .map_err(|e| format!("set inst clsid: {e}"))?; + + let (pb, _) = instance + .create_subkey("InitPropertyBag") + .map_err(|e| format!("create InitPropertyBag: {e}"))?; + pb.set_value("Attributes", &0x11u32) + .map_err(|e| format!("set attrs pb: {e}"))?; + pb.set_value( + "TargetFolderPath", + &mount_point.to_string_lossy().into_owned(), + ) + .map_err(|e| format!("set target: {e}"))?; + + // 5) ShellFolder-Flags + let (sf, _) = clsid + .create_subkey("ShellFolder") + .map_err(|e| format!("create ShellFolder: {e}"))?; + sf.set_value("FolderValueFlags", &0x28u32) + .map_err(|e| format!("set folderflags: {e}"))?; + sf.set_value("Attributes", &0xF080004Du32) + .map_err(|e| format!("set attrs sf: {e}"))?; + + // 6) In die Navigation einhaengen + let ns_path = format!( + "Software\\Microsoft\\Windows\\CurrentVersion\\Explorer\\Desktop\\NameSpace\\{}", + CLSID_GUID + ); + let (ns, _) = hkcu + .create_subkey(&ns_path) + .map_err(|e| format!("create NameSpace: {e}"))?; + ns.set_value("", &display_name.to_string()) + .map_err(|e| format!("set ns name: {e}"))?; + + // 7) Explorer informieren (SHChangeNotify) + notify_shell(); + + Ok(()) +} + +/// Entferne die Shell-Integration wieder. +pub fn uninstall() -> Result<(), String> { + let hkcu = RegKey::predef(HKEY_CURRENT_USER); + + let ns_path = format!( + "Software\\Microsoft\\Windows\\CurrentVersion\\Explorer\\Desktop\\NameSpace\\{}", + CLSID_GUID + ); + let _ = hkcu.delete_subkey_all(&ns_path); + + let clsid_path = format!("Software\\Classes\\CLSID\\{}", CLSID_GUID); + let _ = hkcu.delete_subkey_all(&clsid_path); + + notify_shell(); + Ok(()) +} + +/// Teilt Explorer mit, dass sich die Shell-Namespace-Liste geaendert hat. +/// Ohne das sieht man den neuen Eintrag erst nach Explorer-Neustart. +fn notify_shell() { + use windows::Win32::UI::Shell::{SHChangeNotify, SHCNE_ASSOCCHANGED, SHCNF_IDLIST}; + unsafe { + SHChangeNotify(SHCNE_ASSOCCHANGED, SHCNF_IDLIST, None, None); + } +} + +/// Standard-Icon-Quelle: die laufende .exe mit Index 0. +pub fn default_icon_source() -> String { + std::env::current_exe() + .ok() + .and_then(|p| p.to_str().map(|s| format!("{},0", s))) + .unwrap_or_else(|| "%SystemRoot%\\system32\\imageres.dll,2".to_string()) +} diff --git a/clients/desktop/src-tauri/src/cloud_files/windows.rs b/clients/desktop/src-tauri/src/cloud_files/windows.rs index 5d9c9f7..c1144d1 100644 --- a/clients/desktop/src-tauri/src/cloud_files/windows.rs +++ b/clients/desktop/src-tauri/src/cloud_files/windows.rs @@ -136,11 +136,22 @@ pub fn register_sync_root( log_msg(mount_point, "CfRegisterSyncRoot OK"); connect_callbacks(mount_point)?; log_msg(mount_point, "callbacks connected"); + + // Explorer-Sidebar-Eintrag mit Wolken-Icon + let icon = super::shell_integration::default_icon_source(); + match super::shell_integration::install(provider_name, mount_point, &icon) { + Ok(()) => log_msg(mount_point, "shell integration installed"), + Err(e) => log_err(mount_point, &format!("shell integration FAILED: {e}")), + } + Ok(()) } pub fn unregister_sync_root(mount_point: &PathBuf) -> Result<(), String> { - disconnect_callbacks()?; + // Shell-Eintrag zuerst entfernen (schlaegt nie fehl). + let _ = super::shell_integration::uninstall(); + + let _ = disconnect_callbacks(); let path_wide = U16CString::from_str(mount_point.to_string_lossy().as_ref()) .map_err(|e| e.to_string())?; unsafe {