From 4078c360b497fd4cd372e59f419da3bbe4ac59f0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?V=C3=ADtor=20Vasconcellos?= Date: Sat, 17 Jun 2023 02:23:45 -0300 Subject: [PATCH] [ENG-594] `Open With` Windows + fixes (#945) * Windows `Open With` WIP - Listing applications capable of hanling a file type is working - Openning a file with a selected application is failing with unspecified error HRESULT(0x80004005) for some reason * Fix file not opening due to COM not being initialized - Fix `no apps available` style * Remove unwrap * Fix `Open With` due to changes in main * Fix macOS `Open With` * Fix Windows `Open With` due to changes in main - Sort linux `Open With` entries, to ensure consistent app order * Fix macOS again * Update core.ts * Fix windows CI being rate limited * Clippy * Fix CoUninitialize not being called * minor formatting * Implement feedback - Improve performance of listing apps that can handle a certain file type in Linux * Fix broken feedback change - Small perf improvement to windows crate * Some improvements to windows crate --- .github/scripts/setup-system.ps1 | 6 +- Cargo.lock | Bin 241262 -> 241418 bytes .../desktop/crates/linux/src/desktop_entry.rs | 6 +- apps/desktop/crates/linux/src/system.rs | 62 ++-- apps/desktop/crates/windows/Cargo.toml | 15 + apps/desktop/crates/windows/src/lib.rs | 119 ++++++++ apps/desktop/src-tauri/Cargo.toml | 3 + apps/desktop/src-tauri/src/file.rs | 281 ++++++++++-------- apps/desktop/src/commands.ts | 2 +- .../Explorer/File/ContextMenu/OpenWith.tsx | 45 +-- .../app/$libraryId/overview/Categories.tsx | 6 +- interface/util/Platform.tsx | 4 +- packages/client/src/core.ts | 2 +- 13 files changed, 369 insertions(+), 182 deletions(-) create mode 100644 apps/desktop/crates/windows/Cargo.toml create mode 100644 apps/desktop/crates/windows/src/lib.rs diff --git a/.github/scripts/setup-system.ps1 b/.github/scripts/setup-system.ps1 index f4c5f4129..9f19711f1 100644 --- a/.github/scripts/setup-system.ps1 +++ b/.github/scripts/setup-system.ps1 @@ -178,6 +178,8 @@ https://learn.microsoft.com/windows/package-manager/winget/ Write-Host Write-Host 'Installing Visual Studio Build Tools...' -ForegroundColor Yellow Write-Host 'This will take some time as it involves downloading several gigabytes of data....' -ForegroundColor Cyan + winget install -e --accept-source-agreements --force --disable-interactivity --id Microsoft.VisualStudio.2022.BuildTools ` + --override 'updateall --quiet --wait' # Force install because BuildTools is itself a package manager, so let it decide if something needs to be installed or not winget install -e --accept-source-agreements --force --disable-interactivity --id Microsoft.VisualStudio.2022.BuildTools ` --override '--wait --quiet --add Microsoft.VisualStudio.Workload.VCTools --includeRecommended' @@ -206,6 +208,8 @@ https://learn.microsoft.com/windows/package-manager/winget/ $LASTEXITCODE = 0 } + # TODO: Install Strawberry perl, required by debug build of openssl-sys + Write-Host Write-Host 'Installing NodeJS...' -ForegroundColor Yellow # Check if Node.JS is already installed and if it's compatible with the project @@ -341,7 +345,7 @@ while ($page -gt 0) { $_.workflow_runs | ForEach-Object { $artifactPath = ( - (Invoke-RestMethod -Uri ($_.artifacts_url | Out-String) -Method Get).artifacts ` + (Invoke-RestMethodGithub -Uri ($_.artifacts_url | Out-String) -Method Get).artifacts ` | Where-Object { $_.name -eq "ffmpeg-${ffmpegVersion}-x86_64" } | ForEach-Object { diff --git a/Cargo.lock b/Cargo.lock index 9af823d43373356c38c5d0c659594aaba00a9c09..9352e47136a1fae5e56a2ed348e2b7ba1db115cd 100644 GIT binary patch delta 178 zcmaENhp+1%U&9tgp2W!)`vM%&ERB*lZ(ol8L}FTW_aAh9H4`oU$4!qfMsGjmP0|0}os bQWMiUV}TT1nC|k-yp;U%?Umb@zJ3P)1GGPg delta 106 zcmV-w0G0oW-VW~E4uG@)3}KUTOi({EVlXgcV=-egGB{y2H#jn6FlI0}Hf1+rGBYwb zFfwH@V>LE7Ib<|3Vq!I9W-~T6W-?(mVm3B1F=J+AW@VT0aRL~Jvta?Zvta@vA-7tC M0;@B(9Jd1Q_bRd@vj6}9 diff --git a/apps/desktop/crates/linux/src/desktop_entry.rs b/apps/desktop/crates/linux/src/desktop_entry.rs index 2040d551d..98b583932 100644 --- a/apps/desktop/crates/linux/src/desktop_entry.rs +++ b/apps/desktop/crates/linux/src/desktop_entry.rs @@ -29,12 +29,14 @@ pub enum Mode { } fn terminal() -> Result { + // TODO: Attemtp to read x-terminal-emulator bin (Debian/Ubuntu spec for setting default terminal) SystemApps::get_entries() .ok() .and_then(|mut entries| { - entries.find(|(_handler, entry)| entry.categories.contains_key("TerminalEmulator")) + entries + .find(|DesktopEntry { categories, .. }| categories.contains_key("TerminalEmulator")) }) - .map(|e| e.1.exec) + .map(|e| e.exec) .ok_or(Error::NoTerminal) } diff --git a/apps/desktop/crates/linux/src/system.rs b/apps/desktop/crates/linux/src/system.rs index b462f2464..7ea6fd07d 100644 --- a/apps/desktop/crates/linux/src/system.rs +++ b/apps/desktop/crates/linux/src/system.rs @@ -1,7 +1,7 @@ use std::{ - collections::{HashMap, HashSet, VecDeque}, + collections::{BTreeSet, HashMap}, convert::TryFrom, - ffi::OsString, + ffi::OsStr, }; use mime::Mime; @@ -10,49 +10,53 @@ use xdg_mime::SharedMimeInfo; use crate::{DesktopEntry, Handler, HandlerType, Result}; #[derive(Debug, Default, Clone)] -pub struct SystemApps(pub HashMap>); +pub struct SystemApps(pub HashMap>); impl SystemApps { - pub fn get_handlers(&self, handler_type: HandlerType) -> VecDeque { - match handler_type { + pub fn get_handlers(&self, handler_type: HandlerType) -> impl Iterator { + let mimes = match handler_type { HandlerType::Ext(ext) => { - let mut handlers: HashSet = HashSet::new(); - for mime in SharedMimeInfo::new().get_mime_types_from_file_name(ext.as_str()) { - if let Some(mime_handlers) = self.0.get(&mime) { - for handler in mime_handlers { - handlers.insert(handler.clone()); - } - } - } - handlers.into_iter().collect() + SharedMimeInfo::new().get_mime_types_from_file_name(ext.as_str()) + } + HandlerType::Mime(mime) => vec![mime], + }; + + let mut handlers: BTreeSet<&Handler> = BTreeSet::new(); + for mime in mimes { + if let Some(mime_handlers) = self.0.get(&mime) { + handlers.extend(mime_handlers.iter()); } - HandlerType::Mime(mime) => self.0.get(&mime).unwrap_or(&VecDeque::new()).clone(), } + + handlers.into_iter() } - pub fn get_handler(&self, handler_type: HandlerType) -> Option { - Some(self.get_handlers(handler_type).get(0)?.clone()) + pub fn get_handler(&self, handler_type: HandlerType) -> Option<&Handler> { + self.get_handlers(handler_type).next() } - pub fn get_entries() -> Result> { + pub fn get_entries() -> Result> { Ok(xdg::BaseDirectories::new()? .list_data_files_once("applications") .into_iter() - .filter(|p| p.extension().and_then(|x| x.to_str()) == Some("desktop")) - .filter_map(|p| Some((p.file_name()?.to_owned(), DesktopEntry::try_from(&p).ok()?)))) + .filter(|p| p.extension().map_or(false, |x| x == OsStr::new("desktop"))) + .filter_map(|p| DesktopEntry::try_from(&p).ok())) } pub fn populate() -> Result { - let mut map = HashMap::>::with_capacity(50); + let mut map = HashMap::>::with_capacity(50); - Self::get_entries()?.for_each(|(_, entry)| { - let (file_name, mimes) = (entry.file_name, entry.mimes); - mimes.into_iter().for_each(|mime| { - map.entry(mime) - .or_default() - .push_back(Handler::assume_valid(file_name.clone())); - }); - }); + Self::get_entries()?.for_each( + |DesktopEntry { + mimes, file_name, .. + }| { + mimes.into_iter().for_each(|mime| { + map.entry(mime) + .or_default() + .insert(Handler::assume_valid(file_name.clone())); + }); + }, + ); Ok(Self(map)) } diff --git a/apps/desktop/crates/windows/Cargo.toml b/apps/desktop/crates/windows/Cargo.toml new file mode 100644 index 000000000..b09ecacb7 --- /dev/null +++ b/apps/desktop/crates/windows/Cargo.toml @@ -0,0 +1,15 @@ +[package] +name = "sd-desktop-windows" +version = "0.1.0" +license = { workspace = true } +repository = { workspace = true } +edition = { workspace = true } + +[dependencies] +thiserror = "1.0.40" +normpath = "1.1.1" +libc = "0.2.146" + +[dependencies.windows] +version = "0.48" +features = ["Win32_UI_Shell", "Win32_System_Com"] diff --git a/apps/desktop/crates/windows/src/lib.rs b/apps/desktop/crates/windows/src/lib.rs new file mode 100644 index 000000000..80e47f2ee --- /dev/null +++ b/apps/desktop/crates/windows/src/lib.rs @@ -0,0 +1,119 @@ +#![cfg(target_os = "windows")] + +use std::{ + ffi::{OsStr, OsString}, + os::windows::ffi::OsStrExt, + path::Path, +}; + +use normpath::PathExt; +use windows::{ + core::{HSTRING, PCWSTR}, + Win32::{ + System::Com::{ + CoInitializeEx, CoUninitialize, IDataObject, COINIT_APARTMENTTHREADED, + COINIT_DISABLE_OLE1DDE, + }, + UI::Shell::{ + BHID_DataObject, IAssocHandler, IShellItem, SHAssocEnumHandlers, + SHCreateItemFromParsingName, ASSOC_FILTER_RECOMMENDED, + }, + }, +}; + +pub use windows::core::{Error, Result}; + +// Based on: https://github.com/Byron/trash-rs/blob/841bc1388959ab3be4f05ad1a90b03aa6bcaea67/src/windows.rs#L212-L258 +struct CoInitializer {} +impl CoInitializer { + fn new() -> CoInitializer { + let hr = unsafe { CoInitializeEx(None, COINIT_APARTMENTTHREADED | COINIT_DISABLE_OLE1DDE) }; + if hr.is_err() { + panic!("Call to CoInitializeEx failed. HRESULT: {:?}.", hr); + } + CoInitializer {} + } +} + +thread_local! { + static CO_INITIALIZER: CoInitializer = { + unsafe { libc::atexit(atexit_handler) }; + CoInitializer::new() + }; +} + +extern "C" fn atexit_handler() { + unsafe { + CoUninitialize(); + } +} + +fn ensure_com_initialized() { + CO_INITIALIZER.with(|_| {}); +} + +// Use SHAssocEnumHandlers to get the list of apps associated with a file extension. +// https://learn.microsoft.com/en-us/windows/win32/api/shobjidl_core/nf-shobjidl_core-shassocenumhandlers +pub fn list_apps_associated_with_ext(ext: &OsStr) -> Result> { + if ext.is_empty() { + return Ok(Vec::new()); + } + + // SHAssocEnumHandlers requires the extension to be prefixed with a dot + let ext = { + // Get first charact from ext + let ext_bytes = ext.encode_wide().collect::>(); + if ext_bytes[0] != '.' as u16 { + let mut prefixed_ext = OsString::from("."); + prefixed_ext.push(ext); + prefixed_ext + } else { + ext.to_os_string() + } + }; + + let assoc_handlers = + unsafe { SHAssocEnumHandlers(&HSTRING::from(ext), ASSOC_FILTER_RECOMMENDED) }?; + + let mut vec = Vec::new(); + loop { + let mut rgelt = [None; 1]; + let mut pceltfetched = 0; + unsafe { assoc_handlers.Next(&mut rgelt, Some(&mut pceltfetched)) }?; + + if pceltfetched == 0 { + break; + } + + if let [Some(handler)] = rgelt { + vec.push(handler); + } + } + + Ok(vec) +} + +pub fn open_file_path_with(path: &Path, url: &str) -> Result<()> { + ensure_com_initialized(); + + let ext = path.extension().ok_or(Error::OK)?; + for handler in list_apps_associated_with_ext(ext)?.iter() { + let name = unsafe { handler.GetName()?.to_string()? }; + if name == url { + let path = path.normalize_virtually().map_err(|_| Error::OK)?; + let wide_path = path + .as_os_str() + .encode_wide() + .chain(std::iter::once(0)) + .collect::>(); + let factory: IShellItem = + unsafe { SHCreateItemFromParsingName(PCWSTR(wide_path.as_ptr()), None) }?; + let data: IDataObject = unsafe { factory.BindToHandler(None, &BHID_DataObject) }?; + unsafe { handler.Invoke(&data) }?; + + return Ok(()); + } + } + + Err(Error::OK) +} diff --git a/apps/desktop/src-tauri/Cargo.toml b/apps/desktop/src-tauri/Cargo.toml index 84dbe0287..11a7f3b2f 100644 --- a/apps/desktop/src-tauri/Cargo.toml +++ b/apps/desktop/src-tauri/Cargo.toml @@ -39,6 +39,9 @@ sd-desktop-linux = { path = "../crates/linux" } [target.'cfg(target_os = "macos")'.dependencies] sd-desktop-macos = { path = "../crates/macos" } +[target.'cfg(target_os = "windows")'.dependencies] +sd-desktop-windows = { path = "../crates/windows" } + [build-dependencies] tauri-build = { version = "1.3.0", features = [] } diff --git a/apps/desktop/src-tauri/src/file.rs b/apps/desktop/src-tauri/src/file.rs index d9d6dfc90..b9632f3b4 100644 --- a/apps/desktop/src-tauri/src/file.rs +++ b/apps/desktop/src-tauri/src/file.rs @@ -50,59 +50,53 @@ pub async fn open_file_path( } #[derive(Serialize, Type)] -#[serde(tag = "t", content = "c")] -#[allow(dead_code)] -pub enum OpenWithApplication { - File { - id: i32, - name: String, - #[cfg(target_os = "linux")] - url: std::path::PathBuf, - #[cfg(not(target_os = "linux"))] - url: String, - }, - Error(i32, String), +pub struct OpenWithApplication { + id: i32, + name: String, + #[cfg(target_os = "linux")] + url: std::path::PathBuf, + #[cfg(not(target_os = "linux"))] + url: String, } #[tauri::command(async)] #[specta::specta] -#[allow(unused_variables)] pub async fn get_file_path_open_with_apps( library: uuid::Uuid, ids: Vec, node: tauri::State<'_, Arc>, ) -> Result, ()> { - let Some(library) = node.library_manager.get_library(library).await else { - return Err(()) - }; + let Some(library) = node.library_manager.get_library(library).await + else { + return Ok(vec![]); + }; - let Ok(paths) = library.get_file_paths(ids).await.map_err(|e| {error!("{e:#?}");}) - else { - return Err(()); - }; + let Ok(paths) = library + .get_file_paths(ids).await + .map_err(|e| {error!("{e:#?}");}) + else { + return Ok(vec![]); + }; #[cfg(target_os = "macos")] return Ok(paths .into_iter() .flat_map(|(id, path)| { - if let Some(path) = path { - unsafe { - sd_desktop_macos::get_open_with_applications(&path.to_str().unwrap().into()) - } + let Some(path) = path + else { + error!("File not found in database"); + return vec![]; + }; + + unsafe { sd_desktop_macos::get_open_with_applications(&path.to_str().unwrap().into()) } .as_slice() .iter() - .map(|app| OpenWithApplication::File { + .map(|app| OpenWithApplication { id, name: app.name.to_string(), url: app.url.to_string(), }) .collect::>() - } else { - vec![OpenWithApplication::Error( - id, - "File not found in database".into(), - )] - } }) .collect()); @@ -111,133 +105,170 @@ pub async fn get_file_path_open_with_apps( use sd_desktop_linux::{DesktopEntry, HandlerType, SystemApps}; // TODO: cache this, and only update when the underlying XDG desktop apps changes - let system_apps = SystemApps::populate().map_err(|_| ())?; + let Ok(system_apps) = SystemApps::populate() + .map_err(|e| { error!("{e:#?}"); }) + else { + return Ok(vec![]); + }; return Ok(paths .into_iter() .flat_map(|(id, path)| { - if let Some(path) = path { - let Some(name) = path.file_name() - .and_then(|name| name.to_str()) - .map(|name| name.to_string()) + let Some(path) = path else { - return vec![OpenWithApplication::Error( - id, - "Failed to extract file name".into(), - )] + error!("File not found in database"); + return vec![]; }; - system_apps - .get_handlers(HandlerType::Ext(name)) - .iter() - .map(|handler| { - handler - .get_path() - .map(|path| { - DesktopEntry::try_from(&path) - .map(|entry| OpenWithApplication::File { - id, - name: entry.name, - url: path, - }) - .unwrap_or_else(|e| { - error!("{e:#?}"); - OpenWithApplication::Error( - id, - "Failed to parse desktop entry".into(), - ) - }) - }) - .unwrap_or_else(|e| { - error!("{e:#?}"); - OpenWithApplication::Error( + let Some(name) = path.file_name() + .and_then(|name| name.to_str()) + .map(|name| name.to_string()) + else { + error!("Failed to extract file name"); + return vec![]; + }; + + system_apps + .get_handlers(HandlerType::Ext(name)) + .map(|handler| { + handler + .get_path() + .map_err(|e| { + error!("{e:#?}"); + }) + .and_then(|path| { + DesktopEntry::try_from(&path) + // TODO: Ignore desktop entries that have commands that don't exist/aren't available in path + .map(|entry| OpenWithApplication { id, - "Failed to get path from desktop entry".into(), - ) - }) - }) - .collect::>() - } else { - vec![OpenWithApplication::Error( - id, - "File not found in database".into(), - )] - } + name: entry.name, + url: path, + }) + .map_err(|e| { + error!("{e:#?}"); + }) + }) + }) + .collect::, _>>() + .unwrap_or(vec![]) }) .collect()); } + #[cfg(windows)] + return Ok(paths + .into_iter() + .flat_map(|(id, path)| { + let Some(path) = path + else { + error!("File not found in database"); + return vec![]; + }; + + let Some(ext) = path.extension() + else { + error!("Failed to extract file extension"); + return vec![]; + }; + + sd_desktop_windows::list_apps_associated_with_ext(ext) + .map_err(|e| { + error!("{e:#?}"); + }) + .map(|handlers| { + handlers + .iter() + .filter_map(|handler| { + let (Ok(name), Ok(url)) = ( + unsafe { handler.GetUIName() }.map_err(|e| { error!("{e:#?}");}) + .and_then(|name| unsafe { name.to_string() } + .map_err(|e| { error!("{e:#?}");})), + unsafe { handler.GetName() }.map_err(|e| { error!("{e:#?}");}) + .and_then(|name| unsafe { name.to_string() } + .map_err(|e| { error!("{e:#?}");})), + ) else { + error!("Failed to get handler info"); + return None + }; + + Some(OpenWithApplication { id, name, url }) + }) + .collect::>() + }) + .unwrap_or(vec![]) + }) + .collect()); + #[allow(unreachable_code)] - Err(()) + Ok(vec![]) } type FileIdAndUrl = (i32, String); #[tauri::command(async)] #[specta::specta] -#[allow(unused_variables)] pub async fn open_file_path_with( library: uuid::Uuid, file_ids_and_urls: Vec, node: tauri::State<'_, Arc>, ) -> Result<(), ()> { - let Some(library) = node.library_manager.get_library(library).await else { - return Err(()) - }; + let Some(library) = node.library_manager.get_library(library).await + else { + return Err(()) + }; let url_by_id = file_ids_and_urls.into_iter().collect::>(); let ids = url_by_id.keys().copied().collect::>(); - #[cfg(target_os = "macos")] - { - library - .get_file_paths(ids) - .await - .map(|paths| { - paths.iter().for_each(|(id, path)| { - if let Some(path) = path { + library + .get_file_paths(ids) + .await + .map_err(|e| { + error!("{e:#?}"); + }) + .and_then(|paths| { + paths + .iter() + .map(|(id, path)| { + let (Some(path), Some(url)) = ( + #[cfg(windows)] + path.as_ref(), + #[cfg(not(windows))] + path.as_ref().and_then(|path| path.to_str()), + url_by_id.get(id) + ) + else { + error!("File not found in database"); + return Err(()); + }; + + #[cfg(target_os = "macos")] + return { unsafe { sd_desktop_macos::open_file_path_with( - &path.to_str().unwrap().into(), - &url_by_id - .get(id) - .expect("we just created this hashmap") - .as_str() - .into(), + &path.into(), + &url.as_str().into(), ) - } - } - }) - }) - .map_err(|e| { - error!("{e:#?}"); - }) - } + }; + Ok(()) + }; - #[cfg(target_os = "linux")] - { - library - .get_file_paths(ids) - .await - .map(|paths| { - paths.iter().for_each(|(id, path)| { - if let Some(path) = path.as_ref().and_then(|path| path.to_str()) { - if let Err(e) = sd_desktop_linux::Handler::assume_valid( - url_by_id - .get(id) - .expect("we just created this hashmap") - .as_str() - .into(), - ) + #[cfg(target_os = "linux")] + return sd_desktop_linux::Handler::assume_valid(url.into()) .open(&[path]) - { + .map_err(|e| { error!("{e:#?}"); - } - } + }); + + #[cfg(windows)] + return sd_desktop_windows::open_file_path_with(path, url).map_err(|e| { + error!("{e:#?}"); + }); + + #[allow(unreachable_code)] + Err(()) }) - }) - .map_err(|e| { - error!("{e:#?}"); - }) - } + .collect::, _>>() + .map(|_| ()) + }) } diff --git a/apps/desktop/src/commands.ts b/apps/desktop/src/commands.ts index 7102021f7..b03e0fbe4 100644 --- a/apps/desktop/src/commands.ts +++ b/apps/desktop/src/commands.ts @@ -38,6 +38,6 @@ export function lockAppTheme(themeType: AppThemeType) { return invoke()("lock_app_theme", { themeType }) } -export type OpenWithApplication = { t: "File"; c: { id: number; name: string; url: string } } | { t: "Error"; c: [number, string] } +export type OpenWithApplication = { id: number; name: string; url: string } export type AppThemeType = "Auto" | "Light" | "Dark" export type OpenFilePathResult = { t: "NoLibrary" } | { t: "NoFile"; c: number } | { t: "OpenError"; c: [number, string] } | { t: "AllGood"; c: number } | { t: "Internal"; c: string } diff --git a/interface/app/$libraryId/Explorer/File/ContextMenu/OpenWith.tsx b/interface/app/$libraryId/Explorer/File/ContextMenu/OpenWith.tsx index 7a00c2584..d94a6f417 100644 --- a/interface/app/$libraryId/Explorer/File/ContextMenu/OpenWith.tsx +++ b/interface/app/$libraryId/Explorer/File/ContextMenu/OpenWith.tsx @@ -34,7 +34,7 @@ const Items = ({ }) => { const { library } = useLibraryContext(); - const items = useQuery( + const items = useQuery( ['openWith', filePath.id], () => actions.getFilePathOpenWithApps(library.uuid, [filePath.id]), { suspense: true } @@ -42,25 +42,30 @@ const Items = ({ return ( <> - {items.data?.map((data) => ( - { - try { - await actions.openFilePathWith(library.uuid, [ - (filePath.id, data.c.url) - ]); - } catch { - showAlertDialog({ - title: 'Error', - value: `Failed to open file, with: ${data.url}` - }); - } - }} - > - {data.name} - - )) ??

No apps available

} + {Array.isArray(items.data) && items.data.length > 0 ? ( + items.data.map((data, id) => ( + { + try { + await actions.openFilePathWith(library.uuid, [ + [filePath.id, data.url] + ]); + } catch (e) { + console.error(e); + showAlertDialog({ + title: 'Error', + value: `Failed to open file, with: ${data.url}` + }); + } + }} + > + {data.name} + + )) + ) : ( +

No apps available

+ )} ); }; diff --git a/interface/app/$libraryId/overview/Categories.tsx b/interface/app/$libraryId/overview/Categories.tsx index cccfc3009..79e30a917 100644 --- a/interface/app/$libraryId/overview/Categories.tsx +++ b/interface/app/$libraryId/overview/Categories.tsx @@ -90,7 +90,11 @@ export const Categories = (props: { selected: Category; onSelectedChanged(c: Cat lastCategoryVisibleHandler(index)} onViewportLeave={() => lastCategoryVisibleHandler(index)} - viewport={{ root: ref, margin: '0% -120px 0% 0%' }} + viewport={{ + root: ref, + // WARNING: Edge breaks if the values are not postfixed with px or % + margin: '0% -120px 0% 0%' + }} className="min-w-fit" key={category} > diff --git a/interface/util/Platform.tsx b/interface/util/Platform.tsx index 63808d29d..b99a52468 100644 --- a/interface/util/Platform.tsx +++ b/interface/util/Platform.tsx @@ -25,8 +25,8 @@ export type Platform = { openLogsDir?(): void; // Opens a file path with a given ID openFilePath?(library: string, ids: number[]): any; - getFilePathOpenWithApps?(library: string, ids: number[]): any; - openFilePathWith?(library: string, fileIdsAndAppUrls: [number, string][]): any; + getFilePathOpenWithApps?(library: string, ids: number[]): Promise; + openFilePathWith?(library: string, fileIdsAndAppUrls: [number, string][]): Promise; lockAppTheme?(themeType: 'Auto' | 'Light' | 'Dark'): any; }; diff --git a/packages/client/src/core.ts b/packages/client/src/core.ts index c03c1fd95..d35d711b5 100644 --- a/packages/client/src/core.ts +++ b/packages/client/src/core.ts @@ -184,7 +184,7 @@ export type MaybeNot = T | { not: T } export type MediaData = { id: number; pixel_width: number | null; pixel_height: number | null; longitude: number | null; latitude: number | null; fps: number | null; capture_device_make: string | null; capture_device_model: string | null; capture_device_software: string | null; duration_seconds: number | null; codecs: string | null; streams: number | null } -export type Node = { id: number; pub_id: number[]; name: string; platform: number; version: string | null; last_seen: string; timezone: string | null; date_created: string } +export type Node = { id: number; pub_id: number[]; name: string; platform: number; date_created: string } /** * NodeConfig is the configuration for a node. This is shared between all libraries and is stored in a JSON file on disk.