mirror of
https://github.com/spacedriveapp/spacedrive
synced 2024-07-07 04:23:29 +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
|
||||||
Write-Host 'Installing Visual Studio Build Tools...' -ForegroundColor Yellow
|
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
|
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
|
# 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 `
|
winget install -e --accept-source-agreements --force --disable-interactivity --id Microsoft.VisualStudio.2022.BuildTools `
|
||||||
--override '--wait --quiet --add Microsoft.VisualStudio.Workload.VCTools --includeRecommended'
|
--override '--wait --quiet --add Microsoft.VisualStudio.Workload.VCTools --includeRecommended'
|
||||||
|
@ -206,6 +208,8 @@ https://learn.microsoft.com/windows/package-manager/winget/
|
||||||
$LASTEXITCODE = 0
|
$LASTEXITCODE = 0
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# TODO: Install Strawberry perl, required by debug build of openssl-sys
|
||||||
|
|
||||||
Write-Host
|
Write-Host
|
||||||
Write-Host 'Installing NodeJS...' -ForegroundColor Yellow
|
Write-Host 'Installing NodeJS...' -ForegroundColor Yellow
|
||||||
# Check if Node.JS is already installed and if it's compatible with the project
|
# 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 {
|
$_.workflow_runs | ForEach-Object {
|
||||||
$artifactPath = (
|
$artifactPath = (
|
||||||
(Invoke-RestMethod -Uri ($_.artifacts_url | Out-String) -Method Get).artifacts `
|
(Invoke-RestMethodGithub -Uri ($_.artifacts_url | Out-String) -Method Get).artifacts `
|
||||||
| Where-Object {
|
| Where-Object {
|
||||||
$_.name -eq "ffmpeg-${ffmpegVersion}-x86_64"
|
$_.name -eq "ffmpeg-${ffmpegVersion}-x86_64"
|
||||||
} | ForEach-Object {
|
} | ForEach-Object {
|
||||||
|
|
15
Cargo.lock
generated
15
Cargo.lock
generated
|
@ -3756,9 +3756,9 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "libc"
|
name = "libc"
|
||||||
version = "0.2.144"
|
version = "0.2.146"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "2b00cc1c228a6782d0f076e7b232802e0c5689d41bb5df366f2a6b6621cfdfe1"
|
checksum = "f92be4933c13fd498862a9e02a3055f8a8d9c039ce33db97306fd5a6caa7f29b"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "libheif-rs"
|
name = "libheif-rs"
|
||||||
|
@ -6998,6 +6998,16 @@ dependencies = [
|
||||||
"swift-rs",
|
"swift-rs",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "sd-desktop-windows"
|
||||||
|
version = "0.1.0"
|
||||||
|
dependencies = [
|
||||||
|
"libc",
|
||||||
|
"normpath",
|
||||||
|
"thiserror",
|
||||||
|
"windows 0.48.0",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "sd-ffmpeg"
|
name = "sd-ffmpeg"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
|
@ -7634,6 +7644,7 @@ dependencies = [
|
||||||
"sd-core",
|
"sd-core",
|
||||||
"sd-desktop-linux",
|
"sd-desktop-linux",
|
||||||
"sd-desktop-macos",
|
"sd-desktop-macos",
|
||||||
|
"sd-desktop-windows",
|
||||||
"serde",
|
"serde",
|
||||||
"specta",
|
"specta",
|
||||||
"tauri",
|
"tauri",
|
||||||
|
|
|
@ -29,12 +29,14 @@ pub enum Mode {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn terminal() -> Result<String> {
|
fn terminal() -> Result<String> {
|
||||||
|
// TODO: Attemtp to read x-terminal-emulator bin (Debian/Ubuntu spec for setting default terminal)
|
||||||
SystemApps::get_entries()
|
SystemApps::get_entries()
|
||||||
.ok()
|
.ok()
|
||||||
.and_then(|mut entries| {
|
.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)
|
.ok_or(Error::NoTerminal)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
use std::{
|
use std::{
|
||||||
collections::{HashMap, HashSet, VecDeque},
|
collections::{BTreeSet, HashMap},
|
||||||
convert::TryFrom,
|
convert::TryFrom,
|
||||||
ffi::OsString,
|
ffi::OsStr,
|
||||||
};
|
};
|
||||||
|
|
||||||
use mime::Mime;
|
use mime::Mime;
|
||||||
|
@ -10,49 +10,53 @@ use xdg_mime::SharedMimeInfo;
|
||||||
use crate::{DesktopEntry, Handler, HandlerType, Result};
|
use crate::{DesktopEntry, Handler, HandlerType, Result};
|
||||||
|
|
||||||
#[derive(Debug, Default, Clone)]
|
#[derive(Debug, Default, Clone)]
|
||||||
pub struct SystemApps(pub HashMap<Mime, VecDeque<Handler>>);
|
pub struct SystemApps(pub HashMap<Mime, BTreeSet<Handler>>);
|
||||||
|
|
||||||
impl SystemApps {
|
impl SystemApps {
|
||||||
pub fn get_handlers(&self, handler_type: HandlerType) -> VecDeque<Handler> {
|
pub fn get_handlers(&self, handler_type: HandlerType) -> impl Iterator<Item = &Handler> {
|
||||||
match handler_type {
|
let mimes = match handler_type {
|
||||||
HandlerType::Ext(ext) => {
|
HandlerType::Ext(ext) => {
|
||||||
let mut handlers: HashSet<Handler> = HashSet::new();
|
SharedMimeInfo::new().get_mime_types_from_file_name(ext.as_str())
|
||||||
for mime in SharedMimeInfo::new().get_mime_types_from_file_name(ext.as_str()) {
|
}
|
||||||
if let Some(mime_handlers) = self.0.get(&mime) {
|
HandlerType::Mime(mime) => vec![mime],
|
||||||
for handler in mime_handlers {
|
};
|
||||||
handlers.insert(handler.clone());
|
|
||||||
}
|
let mut handlers: BTreeSet<&Handler> = BTreeSet::new();
|
||||||
}
|
for mime in mimes {
|
||||||
}
|
if let Some(mime_handlers) = self.0.get(&mime) {
|
||||||
handlers.into_iter().collect()
|
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> {
|
pub fn get_handler(&self, handler_type: HandlerType) -> Option<&Handler> {
|
||||||
Some(self.get_handlers(handler_type).get(0)?.clone())
|
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()?
|
Ok(xdg::BaseDirectories::new()?
|
||||||
.list_data_files_once("applications")
|
.list_data_files_once("applications")
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.filter(|p| p.extension().and_then(|x| x.to_str()) == Some("desktop"))
|
.filter(|p| p.extension().map_or(false, |x| x == OsStr::new("desktop")))
|
||||||
.filter_map(|p| Some((p.file_name()?.to_owned(), DesktopEntry::try_from(&p).ok()?))))
|
.filter_map(|p| DesktopEntry::try_from(&p).ok()))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn populate() -> Result<Self> {
|
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)| {
|
Self::get_entries()?.for_each(
|
||||||
let (file_name, mimes) = (entry.file_name, entry.mimes);
|
|DesktopEntry {
|
||||||
mimes.into_iter().for_each(|mime| {
|
mimes, file_name, ..
|
||||||
map.entry(mime)
|
}| {
|
||||||
.or_default()
|
mimes.into_iter().for_each(|mime| {
|
||||||
.push_back(Handler::assume_valid(file_name.clone()));
|
map.entry(mime)
|
||||||
});
|
.or_default()
|
||||||
});
|
.insert(Handler::assume_valid(file_name.clone()));
|
||||||
|
});
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
Ok(Self(map))
|
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]
|
[target.'cfg(target_os = "macos")'.dependencies]
|
||||||
sd-desktop-macos = { path = "../crates/macos" }
|
sd-desktop-macos = { path = "../crates/macos" }
|
||||||
|
|
||||||
|
[target.'cfg(target_os = "windows")'.dependencies]
|
||||||
|
sd-desktop-windows = { path = "../crates/windows" }
|
||||||
|
|
||||||
[build-dependencies]
|
[build-dependencies]
|
||||||
tauri-build = { version = "1.3.0", features = [] }
|
tauri-build = { version = "1.3.0", features = [] }
|
||||||
|
|
||||||
|
|
|
@ -50,59 +50,53 @@ pub async fn open_file_path(
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Serialize, Type)]
|
#[derive(Serialize, Type)]
|
||||||
#[serde(tag = "t", content = "c")]
|
pub struct OpenWithApplication {
|
||||||
#[allow(dead_code)]
|
id: i32,
|
||||||
pub enum OpenWithApplication {
|
name: String,
|
||||||
File {
|
#[cfg(target_os = "linux")]
|
||||||
id: i32,
|
url: std::path::PathBuf,
|
||||||
name: String,
|
#[cfg(not(target_os = "linux"))]
|
||||||
#[cfg(target_os = "linux")]
|
url: String,
|
||||||
url: std::path::PathBuf,
|
|
||||||
#[cfg(not(target_os = "linux"))]
|
|
||||||
url: String,
|
|
||||||
},
|
|
||||||
Error(i32, String),
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tauri::command(async)]
|
#[tauri::command(async)]
|
||||||
#[specta::specta]
|
#[specta::specta]
|
||||||
#[allow(unused_variables)]
|
|
||||||
pub async fn get_file_path_open_with_apps(
|
pub async fn get_file_path_open_with_apps(
|
||||||
library: uuid::Uuid,
|
library: uuid::Uuid,
|
||||||
ids: Vec<i32>,
|
ids: Vec<i32>,
|
||||||
node: tauri::State<'_, Arc<Node>>,
|
node: tauri::State<'_, Arc<Node>>,
|
||||||
) -> Result<Vec<OpenWithApplication>, ()> {
|
) -> Result<Vec<OpenWithApplication>, ()> {
|
||||||
let Some(library) = node.library_manager.get_library(library).await else {
|
let Some(library) = node.library_manager.get_library(library).await
|
||||||
return Err(())
|
else {
|
||||||
};
|
return Ok(vec![]);
|
||||||
|
};
|
||||||
|
|
||||||
let Ok(paths) = library.get_file_paths(ids).await.map_err(|e| {error!("{e:#?}");})
|
let Ok(paths) = library
|
||||||
else {
|
.get_file_paths(ids).await
|
||||||
return Err(());
|
.map_err(|e| {error!("{e:#?}");})
|
||||||
};
|
else {
|
||||||
|
return Ok(vec![]);
|
||||||
|
};
|
||||||
|
|
||||||
#[cfg(target_os = "macos")]
|
#[cfg(target_os = "macos")]
|
||||||
return Ok(paths
|
return Ok(paths
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.flat_map(|(id, path)| {
|
.flat_map(|(id, path)| {
|
||||||
if let Some(path) = path {
|
let Some(path) = path
|
||||||
unsafe {
|
else {
|
||||||
sd_desktop_macos::get_open_with_applications(&path.to_str().unwrap().into())
|
error!("File not found in database");
|
||||||
}
|
return vec![];
|
||||||
|
};
|
||||||
|
|
||||||
|
unsafe { sd_desktop_macos::get_open_with_applications(&path.to_str().unwrap().into()) }
|
||||||
.as_slice()
|
.as_slice()
|
||||||
.iter()
|
.iter()
|
||||||
.map(|app| OpenWithApplication::File {
|
.map(|app| OpenWithApplication {
|
||||||
id,
|
id,
|
||||||
name: app.name.to_string(),
|
name: app.name.to_string(),
|
||||||
url: app.url.to_string(),
|
url: app.url.to_string(),
|
||||||
})
|
})
|
||||||
.collect::<Vec<_>>()
|
.collect::<Vec<_>>()
|
||||||
} else {
|
|
||||||
vec![OpenWithApplication::Error(
|
|
||||||
id,
|
|
||||||
"File not found in database".into(),
|
|
||||||
)]
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
.collect());
|
.collect());
|
||||||
|
|
||||||
|
@ -111,133 +105,170 @@ pub async fn get_file_path_open_with_apps(
|
||||||
use sd_desktop_linux::{DesktopEntry, HandlerType, SystemApps};
|
use sd_desktop_linux::{DesktopEntry, HandlerType, SystemApps};
|
||||||
|
|
||||||
// TODO: cache this, and only update when the underlying XDG desktop apps changes
|
// 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
|
return Ok(paths
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.flat_map(|(id, path)| {
|
.flat_map(|(id, path)| {
|
||||||
if let Some(path) = path {
|
let Some(path) = path
|
||||||
let Some(name) = path.file_name()
|
|
||||||
.and_then(|name| name.to_str())
|
|
||||||
.map(|name| name.to_string())
|
|
||||||
else {
|
else {
|
||||||
return vec![OpenWithApplication::Error(
|
error!("File not found in database");
|
||||||
id,
|
return vec![];
|
||||||
"Failed to extract file name".into(),
|
|
||||||
)]
|
|
||||||
};
|
};
|
||||||
|
|
||||||
system_apps
|
let Some(name) = path.file_name()
|
||||||
.get_handlers(HandlerType::Ext(name))
|
.and_then(|name| name.to_str())
|
||||||
.iter()
|
.map(|name| name.to_string())
|
||||||
.map(|handler| {
|
else {
|
||||||
handler
|
error!("Failed to extract file name");
|
||||||
.get_path()
|
return vec![];
|
||||||
.map(|path| {
|
};
|
||||||
DesktopEntry::try_from(&path)
|
|
||||||
.map(|entry| OpenWithApplication::File {
|
system_apps
|
||||||
id,
|
.get_handlers(HandlerType::Ext(name))
|
||||||
name: entry.name,
|
.map(|handler| {
|
||||||
url: path,
|
handler
|
||||||
})
|
.get_path()
|
||||||
.unwrap_or_else(|e| {
|
.map_err(|e| {
|
||||||
error!("{e:#?}");
|
error!("{e:#?}");
|
||||||
OpenWithApplication::Error(
|
})
|
||||||
id,
|
.and_then(|path| {
|
||||||
"Failed to parse desktop entry".into(),
|
DesktopEntry::try_from(&path)
|
||||||
)
|
// TODO: Ignore desktop entries that have commands that don't exist/aren't available in path
|
||||||
})
|
.map(|entry| OpenWithApplication {
|
||||||
})
|
|
||||||
.unwrap_or_else(|e| {
|
|
||||||
error!("{e:#?}");
|
|
||||||
OpenWithApplication::Error(
|
|
||||||
id,
|
id,
|
||||||
"Failed to get path from desktop entry".into(),
|
name: entry.name,
|
||||||
)
|
url: path,
|
||||||
})
|
})
|
||||||
})
|
.map_err(|e| {
|
||||||
.collect::<Vec<_>>()
|
error!("{e:#?}");
|
||||||
} else {
|
})
|
||||||
vec![OpenWithApplication::Error(
|
})
|
||||||
id,
|
})
|
||||||
"File not found in database".into(),
|
.collect::<Result<Vec<_>, _>>()
|
||||||
)]
|
.unwrap_or(vec![])
|
||||||
}
|
|
||||||
})
|
})
|
||||||
.collect());
|
.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)]
|
#[allow(unreachable_code)]
|
||||||
Err(())
|
Ok(vec![])
|
||||||
}
|
}
|
||||||
|
|
||||||
type FileIdAndUrl = (i32, String);
|
type FileIdAndUrl = (i32, String);
|
||||||
|
|
||||||
#[tauri::command(async)]
|
#[tauri::command(async)]
|
||||||
#[specta::specta]
|
#[specta::specta]
|
||||||
#[allow(unused_variables)]
|
|
||||||
pub async fn open_file_path_with(
|
pub async fn open_file_path_with(
|
||||||
library: uuid::Uuid,
|
library: uuid::Uuid,
|
||||||
file_ids_and_urls: Vec<FileIdAndUrl>,
|
file_ids_and_urls: Vec<FileIdAndUrl>,
|
||||||
node: tauri::State<'_, Arc<Node>>,
|
node: tauri::State<'_, Arc<Node>>,
|
||||||
) -> Result<(), ()> {
|
) -> Result<(), ()> {
|
||||||
let Some(library) = node.library_manager.get_library(library).await else {
|
let Some(library) = node.library_manager.get_library(library).await
|
||||||
return Err(())
|
else {
|
||||||
};
|
return Err(())
|
||||||
|
};
|
||||||
|
|
||||||
let url_by_id = file_ids_and_urls.into_iter().collect::<HashMap<_, _>>();
|
let url_by_id = file_ids_and_urls.into_iter().collect::<HashMap<_, _>>();
|
||||||
let ids = url_by_id.keys().copied().collect::<Vec<_>>();
|
let ids = url_by_id.keys().copied().collect::<Vec<_>>();
|
||||||
|
|
||||||
#[cfg(target_os = "macos")]
|
library
|
||||||
{
|
.get_file_paths(ids)
|
||||||
library
|
.await
|
||||||
.get_file_paths(ids)
|
.map_err(|e| {
|
||||||
.await
|
error!("{e:#?}");
|
||||||
.map(|paths| {
|
})
|
||||||
paths.iter().for_each(|(id, path)| {
|
.and_then(|paths| {
|
||||||
if let Some(path) = path {
|
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 {
|
unsafe {
|
||||||
sd_desktop_macos::open_file_path_with(
|
sd_desktop_macos::open_file_path_with(
|
||||||
&path.to_str().unwrap().into(),
|
&path.into(),
|
||||||
&url_by_id
|
&url.as_str().into(),
|
||||||
.get(id)
|
|
||||||
.expect("we just created this hashmap")
|
|
||||||
.as_str()
|
|
||||||
.into(),
|
|
||||||
)
|
)
|
||||||
}
|
};
|
||||||
}
|
Ok(())
|
||||||
})
|
};
|
||||||
})
|
|
||||||
.map_err(|e| {
|
|
||||||
error!("{e:#?}");
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(target_os = "linux")]
|
#[cfg(target_os = "linux")]
|
||||||
{
|
return sd_desktop_linux::Handler::assume_valid(url.into())
|
||||||
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(),
|
|
||||||
)
|
|
||||||
.open(&[path])
|
.open(&[path])
|
||||||
{
|
.map_err(|e| {
|
||||||
error!("{e:#?}");
|
error!("{e:#?}");
|
||||||
}
|
});
|
||||||
}
|
|
||||||
|
#[cfg(windows)]
|
||||||
|
return sd_desktop_windows::open_file_path_with(path, url).map_err(|e| {
|
||||||
|
error!("{e:#?}");
|
||||||
|
});
|
||||||
|
|
||||||
|
#[allow(unreachable_code)]
|
||||||
|
Err(())
|
||||||
})
|
})
|
||||||
})
|
.collect::<Result<Vec<_>, _>>()
|
||||||
.map_err(|e| {
|
.map(|_| ())
|
||||||
error!("{e:#?}");
|
})
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -38,6 +38,6 @@ export function lockAppTheme(themeType: AppThemeType) {
|
||||||
return invoke()<null>("lock_app_theme", { themeType })
|
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 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 }
|
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 { library } = useLibraryContext();
|
||||||
|
|
||||||
const items = useQuery<any[]>(
|
const items = useQuery<unknown>(
|
||||||
['openWith', filePath.id],
|
['openWith', filePath.id],
|
||||||
() => actions.getFilePathOpenWithApps(library.uuid, [filePath.id]),
|
() => actions.getFilePathOpenWithApps(library.uuid, [filePath.id]),
|
||||||
{ suspense: true }
|
{ suspense: true }
|
||||||
|
@ -42,25 +42,30 @@ const Items = ({
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{items.data?.map((data) => (
|
{Array.isArray(items.data) && items.data.length > 0 ? (
|
||||||
<ContextMenu.Item
|
items.data.map((data, id) => (
|
||||||
key={data.name}
|
<ContextMenu.Item
|
||||||
onClick={async () => {
|
key={id}
|
||||||
try {
|
onClick={async () => {
|
||||||
await actions.openFilePathWith(library.uuid, [
|
try {
|
||||||
(filePath.id, data.c.url)
|
await actions.openFilePathWith(library.uuid, [
|
||||||
]);
|
[filePath.id, data.url]
|
||||||
} catch {
|
]);
|
||||||
showAlertDialog({
|
} catch (e) {
|
||||||
title: 'Error',
|
console.error(e);
|
||||||
value: `Failed to open file, with: ${data.url}`
|
showAlertDialog({
|
||||||
});
|
title: 'Error',
|
||||||
}
|
value: `Failed to open file, with: ${data.url}`
|
||||||
}}
|
});
|
||||||
>
|
}
|
||||||
{data.name}
|
}}
|
||||||
</ContextMenu.Item>
|
>
|
||||||
)) ?? <p> No apps available </p>}
|
{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
|
<motion.div
|
||||||
onViewportEnter={() => lastCategoryVisibleHandler(index)}
|
onViewportEnter={() => lastCategoryVisibleHandler(index)}
|
||||||
onViewportLeave={() => 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"
|
className="min-w-fit"
|
||||||
key={category}
|
key={category}
|
||||||
>
|
>
|
||||||
|
|
|
@ -25,8 +25,8 @@ export type Platform = {
|
||||||
openLogsDir?(): void;
|
openLogsDir?(): void;
|
||||||
// Opens a file path with a given ID
|
// Opens a file path with a given ID
|
||||||
openFilePath?(library: string, ids: number[]): any;
|
openFilePath?(library: string, ids: number[]): any;
|
||||||
getFilePathOpenWithApps?(library: string, ids: number[]): any;
|
getFilePathOpenWithApps?(library: string, ids: number[]): Promise<unknown>;
|
||||||
openFilePathWith?(library: string, fileIdsAndAppUrls: [number, string][]): any;
|
openFilePathWith?(library: string, fileIdsAndAppUrls: [number, string][]): Promise<unknown>;
|
||||||
lockAppTheme?(themeType: 'Auto' | 'Light' | 'Dark'): any;
|
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 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.
|
* 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