mirror of
https://github.com/spacedriveapp/spacedrive
synced 2024-07-04 12:13:27 +00:00
[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
This commit is contained in:
parent
88166cdfdd
commit
4078c360b4
6
.github/scripts/setup-system.ps1
vendored
6
.github/scripts/setup-system.ps1
vendored
|
@ -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 {
|
||||
|
|
15
Cargo.lock
generated
15
Cargo.lock
generated
|
@ -3756,9 +3756,9 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "libc"
|
||||
version = "0.2.144"
|
||||
version = "0.2.146"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2b00cc1c228a6782d0f076e7b232802e0c5689d41bb5df366f2a6b6621cfdfe1"
|
||||
checksum = "f92be4933c13fd498862a9e02a3055f8a8d9c039ce33db97306fd5a6caa7f29b"
|
||||
|
||||
[[package]]
|
||||
name = "libheif-rs"
|
||||
|
@ -6998,6 +6998,16 @@ dependencies = [
|
|||
"swift-rs",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "sd-desktop-windows"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"normpath",
|
||||
"thiserror",
|
||||
"windows 0.48.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "sd-ffmpeg"
|
||||
version = "0.1.0"
|
||||
|
@ -7634,6 +7644,7 @@ dependencies = [
|
|||
"sd-core",
|
||||
"sd-desktop-linux",
|
||||
"sd-desktop-macos",
|
||||
"sd-desktop-windows",
|
||||
"serde",
|
||||
"specta",
|
||||
"tauri",
|
||||
|
|
|
@ -29,12 +29,14 @@ pub enum Mode {
|
|||
}
|
||||
|
||||
fn terminal() -> Result<String> {
|
||||
// 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)
|
||||
}
|
||||
|
||||
|
|
|
@ -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<Mime, VecDeque<Handler>>);
|
||||
pub struct SystemApps(pub HashMap<Mime, BTreeSet<Handler>>);
|
||||
|
||||
impl SystemApps {
|
||||
pub fn get_handlers(&self, handler_type: HandlerType) -> VecDeque<Handler> {
|
||||
match handler_type {
|
||||
pub fn get_handlers(&self, handler_type: HandlerType) -> impl Iterator<Item = &Handler> {
|
||||
let mimes = match handler_type {
|
||||
HandlerType::Ext(ext) => {
|
||||
let mut handlers: HashSet<Handler> = 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<Handler> {
|
||||
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<impl Iterator<Item = (OsString, DesktopEntry)>> {
|
||||
pub fn get_entries() -> Result<impl Iterator<Item = DesktopEntry>> {
|
||||
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<Self> {
|
||||
let mut map = HashMap::<Mime, VecDeque<Handler>>::with_capacity(50);
|
||||
let mut map = HashMap::<Mime, BTreeSet<Handler>>::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))
|
||||
}
|
||||
|
|
15
apps/desktop/crates/windows/Cargo.toml
Normal file
15
apps/desktop/crates/windows/Cargo.toml
Normal file
|
@ -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"]
|
119
apps/desktop/crates/windows/src/lib.rs
Normal file
119
apps/desktop/crates/windows/src/lib.rs
Normal file
|
@ -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<Vec<IAssocHandler>> {
|
||||
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::<Vec<_>>();
|
||||
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::<Vec<_>>();
|
||||
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)
|
||||
}
|
|
@ -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 = [] }
|
||||
|
||||
|
|
|
@ -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<i32>,
|
||||
node: tauri::State<'_, Arc<Node>>,
|
||||
) -> Result<Vec<OpenWithApplication>, ()> {
|
||||
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::<Vec<_>>()
|
||||
} 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::<Vec<_>>()
|
||||
} else {
|
||||
vec![OpenWithApplication::Error(
|
||||
id,
|
||||
"File not found in database".into(),
|
||||
)]
|
||||
}
|
||||
name: entry.name,
|
||||
url: path,
|
||||
})
|
||||
.map_err(|e| {
|
||||
error!("{e:#?}");
|
||||
})
|
||||
})
|
||||
})
|
||||
.collect::<Result<Vec<_>, _>>()
|
||||
.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::<Vec<_>>()
|
||||
})
|
||||
.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<FileIdAndUrl>,
|
||||
node: tauri::State<'_, Arc<Node>>,
|
||||
) -> 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::<HashMap<_, _>>();
|
||||
let ids = url_by_id.keys().copied().collect::<Vec<_>>();
|
||||
|
||||
#[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::<Result<Vec<_>, _>>()
|
||||
.map(|_| ())
|
||||
})
|
||||
}
|
||||
|
|
|
@ -38,6 +38,6 @@ export function lockAppTheme(themeType: AppThemeType) {
|
|||
return invoke()<null>("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 }
|
||||
|
|
|
@ -34,7 +34,7 @@ const Items = ({
|
|||
}) => {
|
||||
const { library } = useLibraryContext();
|
||||
|
||||
const items = useQuery<any[]>(
|
||||
const items = useQuery<unknown>(
|
||||
['openWith', filePath.id],
|
||||
() => actions.getFilePathOpenWithApps(library.uuid, [filePath.id]),
|
||||
{ suspense: true }
|
||||
|
@ -42,25 +42,30 @@ const Items = ({
|
|||
|
||||
return (
|
||||
<>
|
||||
{items.data?.map((data) => (
|
||||
<ContextMenu.Item
|
||||
key={data.name}
|
||||
onClick={async () => {
|
||||
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}
|
||||
</ContextMenu.Item>
|
||||
)) ?? <p> No apps available </p>}
|
||||
{Array.isArray(items.data) && items.data.length > 0 ? (
|
||||
items.data.map((data, id) => (
|
||||
<ContextMenu.Item
|
||||
key={id}
|
||||
onClick={async () => {
|
||||
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}
|
||||
</ContextMenu.Item>
|
||||
))
|
||||
) : (
|
||||
<p className="w-full text-center text-sm text-gray-400"> No apps available </p>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -90,7 +90,11 @@ export const Categories = (props: { selected: Category; onSelectedChanged(c: Cat
|
|||
<motion.div
|
||||
onViewportEnter={() => 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}
|
||||
>
|
||||
|
|
|
@ -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<unknown>;
|
||||
openFilePathWith?(library: string, fileIdsAndAppUrls: [number, string][]): Promise<unknown>;
|
||||
lockAppTheme?(themeType: 'Auto' | 'Light' | 'Dark'): any;
|
||||
};
|
||||
|
||||
|
|
|
@ -184,7 +184,7 @@ export type MaybeNot<T> = 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.
|
||||
|
|
Loading…
Reference in a new issue