mirror of
https://github.com/nimbold/Firelink.git
synced 2026-08-28 03:27:12 +00:00
fix: repair download open and reveal interactions
This commit is contained in:
+31
-52
@@ -1,6 +1,6 @@
|
|||||||
use std::ffi::OsString;
|
use std::ffi::OsString;
|
||||||
use std::path::{Component, Path, PathBuf};
|
use std::path::{Component, Path, PathBuf};
|
||||||
use std::process::Command;
|
use tauri_plugin_opener::OpenerExt;
|
||||||
use tauri_plugin_store::StoreExt;
|
use tauri_plugin_store::StoreExt;
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
@@ -8,35 +8,18 @@ pub async fn reveal_in_file_manager(
|
|||||||
app_handle: tauri::AppHandle,
|
app_handle: tauri::AppHandle,
|
||||||
path: String,
|
path: String,
|
||||||
) -> Result<(), String> {
|
) -> Result<(), String> {
|
||||||
let path = authorize_download_path(&app_handle, &path, DownloadAsset::Primary)?;
|
let primary = authorize_download_path(&app_handle, &path, DownloadAsset::Primary)?;
|
||||||
|
let path = existing_download_asset(&primary).ok_or_else(|| {
|
||||||
|
format!(
|
||||||
|
"Downloaded file or partial file is missing: {}",
|
||||||
|
primary.display()
|
||||||
|
)
|
||||||
|
})?;
|
||||||
|
|
||||||
#[cfg(target_os = "macos")]
|
app_handle
|
||||||
{
|
.opener()
|
||||||
Command::new("open")
|
.reveal_item_in_dir(path.to_string_lossy().as_ref())
|
||||||
.arg("-R")
|
.map_err(|e| format!("Failed to reveal file: {}", e))?;
|
||||||
.arg(&path)
|
|
||||||
.spawn()
|
|
||||||
.map_err(|e| format!("Failed to reveal file: {}", e))?;
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(target_os = "windows")]
|
|
||||||
{
|
|
||||||
Command::new("explorer.exe")
|
|
||||||
.arg(format!("/select,\"{}\"", path.display()))
|
|
||||||
.spawn()
|
|
||||||
.map_err(|e| format!("Failed to reveal file: {}", e))?;
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(not(any(target_os = "macos", target_os = "windows")))]
|
|
||||||
{
|
|
||||||
let parent = path
|
|
||||||
.parent()
|
|
||||||
.ok_or("Download path has no parent directory")?;
|
|
||||||
Command::new("xdg-open")
|
|
||||||
.arg(parent)
|
|
||||||
.spawn()
|
|
||||||
.map_err(|e| format!("Failed to reveal file: {}", e))?;
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
@@ -47,31 +30,14 @@ pub async fn open_downloaded_file(
|
|||||||
path: String,
|
path: String,
|
||||||
) -> Result<(), String> {
|
) -> Result<(), String> {
|
||||||
let path = authorize_download_path(&app_handle, &path, DownloadAsset::Primary)?;
|
let path = authorize_download_path(&app_handle, &path, DownloadAsset::Primary)?;
|
||||||
|
if !path.exists() {
|
||||||
#[cfg(target_os = "macos")]
|
return Err(format!("Downloaded file is missing: {}", path.display()));
|
||||||
{
|
|
||||||
Command::new("open")
|
|
||||||
.arg(&path)
|
|
||||||
.spawn()
|
|
||||||
.map_err(|e| format!("Failed to open file: {}", e))?;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(target_os = "windows")]
|
app_handle
|
||||||
{
|
.opener()
|
||||||
Command::new("cmd")
|
.open_path(path.to_string_lossy().as_ref(), None::<String>)
|
||||||
.args(["/c", "start", ""])
|
.map_err(|e| format!("Failed to open file: {}", e))?;
|
||||||
.arg(&path)
|
|
||||||
.spawn()
|
|
||||||
.map_err(|e| format!("Failed to open file: {}", e))?;
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(not(any(target_os = "macos", target_os = "windows")))]
|
|
||||||
{
|
|
||||||
Command::new("xdg-open")
|
|
||||||
.arg(&path)
|
|
||||||
.spawn()
|
|
||||||
.map_err(|e| format!("Failed to open file: {}", e))?;
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
@@ -205,6 +171,19 @@ fn canonicalize_with_missing_leaf(path: &Path) -> Result<PathBuf, String> {
|
|||||||
Ok(canonical)
|
Ok(canonical)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn existing_download_asset(primary: &Path) -> Option<PathBuf> {
|
||||||
|
[
|
||||||
|
primary.to_path_buf(),
|
||||||
|
append_suffix(primary, ".aria2"),
|
||||||
|
append_suffix(primary, ".part"),
|
||||||
|
]
|
||||||
|
.into_iter()
|
||||||
|
.find(|candidate| {
|
||||||
|
std::fs::symlink_metadata(candidate)
|
||||||
|
.is_ok_and(|metadata| metadata.is_file() && !metadata.file_type().is_symlink())
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
fn append_suffix(path: &Path, suffix: &str) -> PathBuf {
|
fn append_suffix(path: &Path, suffix: &str) -> PathBuf {
|
||||||
let mut value: OsString = path.as_os_str().to_owned();
|
let mut value: OsString = path.as_os_str().to_owned();
|
||||||
value.push(suffix);
|
value.push(suffix);
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ interface DownloadItemProps {
|
|||||||
setContextMenu: (menu: { x: number; y: number; id: string }) => void;
|
setContextMenu: (menu: { x: number; y: number; id: string }) => void;
|
||||||
handlePause: (id: string) => void;
|
handlePause: (id: string) => void;
|
||||||
handleResume: (item: DownloadItemType) => void;
|
handleResume: (item: DownloadItemType) => void;
|
||||||
|
handleDoubleClick: (item: DownloadItemType) => void;
|
||||||
getCategoryIcon: (category: string) => React.ReactNode;
|
getCategoryIcon: (category: string) => React.ReactNode;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -21,6 +22,7 @@ export const DownloadItem = React.memo<DownloadItemProps>(({
|
|||||||
setContextMenu,
|
setContextMenu,
|
||||||
handlePause,
|
handlePause,
|
||||||
handleResume,
|
handleResume,
|
||||||
|
handleDoubleClick,
|
||||||
getCategoryIcon,
|
getCategoryIcon,
|
||||||
}) => {
|
}) => {
|
||||||
const download = useDownloadStore(state => state.downloads.find(d => d.id === downloadId));
|
const download = useDownloadStore(state => state.downloads.find(d => d.id === downloadId));
|
||||||
@@ -70,6 +72,7 @@ export const DownloadItem = React.memo<DownloadItemProps>(({
|
|||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
setContextMenu({ x: e.clientX, y: e.clientY, id: download.id });
|
setContextMenu({ x: e.clientX, y: e.clientY, id: download.id });
|
||||||
}}
|
}}
|
||||||
|
onDoubleClick={() => handleDoubleClick(download)}
|
||||||
>
|
>
|
||||||
<div className="download-file-cell">
|
<div className="download-file-cell">
|
||||||
<span className="shrink-0 text-text-muted">
|
<span className="shrink-0 text-text-muted">
|
||||||
@@ -168,7 +171,10 @@ export const DownloadItem = React.memo<DownloadItemProps>(({
|
|||||||
{download.dateAdded ? new Date(download.dateAdded).toLocaleDateString() : '-'}
|
{download.dateAdded ? new Date(download.dateAdded).toLocaleDateString() : '-'}
|
||||||
</span>
|
</span>
|
||||||
|
|
||||||
<div className="hidden group-hover:flex items-center justify-end gap-0.5 w-full ml-auto">
|
<div
|
||||||
|
className="hidden group-hover:flex items-center justify-end gap-0.5 w-full ml-auto"
|
||||||
|
onDoubleClick={(e) => e.stopPropagation()}
|
||||||
|
>
|
||||||
{download.status === 'queued' && queueIndex !== -1 && (
|
{download.status === 'queued' && queueIndex !== -1 && (
|
||||||
<>
|
<>
|
||||||
<button
|
<button
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ export const DownloadTable: React.FC<DownloadTableProps> = ({ filter }) => {
|
|||||||
const isMac = navigator.userAgent.includes('Mac');
|
const isMac = navigator.userAgent.includes('Mac');
|
||||||
|
|
||||||
const [contextMenu, setContextMenu] = useState<{ x: number; y: number; id: string } | null>(null);
|
const [contextMenu, setContextMenu] = useState<{ x: number; y: number; id: string } | null>(null);
|
||||||
|
const [interactionError, setInteractionError] = useState('');
|
||||||
const [columnWidths, setColumnWidths] = useState([340, 100, 220, 100, 80, 170]);
|
const [columnWidths, setColumnWidths] = useState([340, 100, 220, 100, 80, 170]);
|
||||||
const columnMinimums = [0, 58, 92, 58, 48, 112];
|
const columnMinimums = [0, 58, 92, 58, 48, 112];
|
||||||
const tableGridTemplate = columnWidths.map((width, index) => `minmax(${columnMinimums[index]}px, ${width}fr)`).join(' ');
|
const tableGridTemplate = columnWidths.map((width, index) => `minmax(${columnMinimums[index]}px, ${width}fr)`).join(' ');
|
||||||
@@ -50,6 +51,12 @@ export const DownloadTable: React.FC<DownloadTableProps> = ({ filter }) => {
|
|||||||
return () => window.removeEventListener('click', handleCloseMenu);
|
return () => window.removeEventListener('click', handleCloseMenu);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!interactionError) return;
|
||||||
|
const timeout = window.setTimeout(() => setInteractionError(''), 5000);
|
||||||
|
return () => window.clearTimeout(timeout);
|
||||||
|
}, [interactionError]);
|
||||||
|
|
||||||
const resolvePath = async (dir: string, file: string) => {
|
const resolvePath = async (dir: string, file: string) => {
|
||||||
let resolvedDir = dir;
|
let resolvedDir = dir;
|
||||||
if (dir.startsWith('~/')) {
|
if (dir.startsWith('~/')) {
|
||||||
@@ -62,6 +69,60 @@ export const DownloadTable: React.FC<DownloadTableProps> = ({ filter }) => {
|
|||||||
return resolvedDir + separator + file;
|
return resolvedDir + separator + file;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const showInteractionError = (message: string, error: unknown) => {
|
||||||
|
const detail = typeof error === 'string' ? error : error instanceof Error ? error.message : String(error);
|
||||||
|
setInteractionError(`${message}: ${detail}`);
|
||||||
|
};
|
||||||
|
|
||||||
|
const getDownloadPath = async (item: DownloadItem) => {
|
||||||
|
const fileName = item.fileName?.trim();
|
||||||
|
if (!fileName) return null;
|
||||||
|
return resolvePath(item.destination || '~/Downloads', fileName);
|
||||||
|
};
|
||||||
|
|
||||||
|
const openProperties = (id: string) => {
|
||||||
|
useDownloadStore.getState().setSelectedPropertiesDownloadId(id);
|
||||||
|
};
|
||||||
|
|
||||||
|
const openDownloadFile = async (item: DownloadItem) => {
|
||||||
|
const fullPath = await getDownloadPath(item);
|
||||||
|
if (!fullPath) {
|
||||||
|
openProperties(item.id);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await invoke('open_downloaded_file', { path: fullPath });
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Failed to open file:", error);
|
||||||
|
showInteractionError('Could not open downloaded file', error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const revealDownloadFile = async (item: DownloadItem) => {
|
||||||
|
const fullPath = await getDownloadPath(item);
|
||||||
|
if (!fullPath) {
|
||||||
|
openProperties(item.id);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await invoke('reveal_in_file_manager', { path: fullPath });
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Failed to show in Finder:", error);
|
||||||
|
showInteractionError('Could not show download in Finder', error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDownloadDoubleClick = (item: DownloadItem) => {
|
||||||
|
if (item.status === 'completed') {
|
||||||
|
void openDownloadFile(item);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
openProperties(item.id);
|
||||||
|
};
|
||||||
|
|
||||||
const filteredDownloads = downloads.filter((d: DownloadItem) => {
|
const filteredDownloads = downloads.filter((d: DownloadItem) => {
|
||||||
if (filter.startsWith('queue:')) {
|
if (filter.startsWith('queue:')) {
|
||||||
return d.queueId === filter.replace('queue:', '');
|
return d.queueId === filter.replace('queue:', '');
|
||||||
@@ -217,6 +278,7 @@ export const DownloadTable: React.FC<DownloadTableProps> = ({ filter }) => {
|
|||||||
setContextMenu={setContextMenu}
|
setContextMenu={setContextMenu}
|
||||||
handlePause={handlePause}
|
handlePause={handlePause}
|
||||||
handleResume={handleResume}
|
handleResume={handleResume}
|
||||||
|
handleDoubleClick={handleDownloadDoubleClick}
|
||||||
getCategoryIcon={getCategoryIcon}
|
getCategoryIcon={getCategoryIcon}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
@@ -251,28 +313,18 @@ export const DownloadTable: React.FC<DownloadTableProps> = ({ filter }) => {
|
|||||||
<button
|
<button
|
||||||
onClick={async () => {
|
onClick={async () => {
|
||||||
setContextMenu(null);
|
setContextMenu(null);
|
||||||
try {
|
await openDownloadFile(contextItem);
|
||||||
const fullPath = await resolvePath(contextItem.destination || '~/Downloads', contextItem.fileName);
|
|
||||||
await invoke('open_downloaded_file', { path: fullPath });
|
|
||||||
} catch (e) {
|
|
||||||
console.error("Failed to open file:", e);
|
|
||||||
}
|
|
||||||
}}
|
}}
|
||||||
className="w-full text-left px-3 py-2 hover:bg-item-hover transition-colors"
|
className="w-full text-left px-3 py-2 hover:bg-item-hover transition-colors"
|
||||||
>
|
>
|
||||||
Open File
|
Open
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<button
|
<button
|
||||||
onClick={async () => {
|
onClick={async () => {
|
||||||
setContextMenu(null);
|
setContextMenu(null);
|
||||||
try {
|
await revealDownloadFile(contextItem);
|
||||||
const fullPath = await resolvePath(contextItem.destination || '~/Downloads', contextItem.fileName);
|
|
||||||
await invoke('reveal_in_file_manager', { path: fullPath });
|
|
||||||
} catch (e) {
|
|
||||||
console.error("Failed to show in folder:", e);
|
|
||||||
}
|
|
||||||
}}
|
}}
|
||||||
className="w-full text-left px-3 py-2 hover:bg-item-hover transition-colors"
|
className="w-full text-left px-3 py-2 hover:bg-item-hover transition-colors"
|
||||||
>
|
>
|
||||||
@@ -357,10 +409,10 @@ export const DownloadTable: React.FC<DownloadTableProps> = ({ filter }) => {
|
|||||||
<div className="h-[1px] bg-border-modal/60 my-1.5 mx-2"></div>
|
<div className="h-[1px] bg-border-modal/60 my-1.5 mx-2"></div>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setContextMenu(null);
|
setContextMenu(null);
|
||||||
useDownloadStore.getState().setSelectedPropertiesDownloadId(contextItem.id);
|
openProperties(contextItem.id);
|
||||||
}}
|
}}
|
||||||
className="w-full text-left px-3 py-2 hover:bg-item-hover transition-colors"
|
className="w-full text-left px-3 py-2 hover:bg-item-hover transition-colors"
|
||||||
>
|
>
|
||||||
Properties
|
Properties
|
||||||
@@ -368,6 +420,12 @@ export const DownloadTable: React.FC<DownloadTableProps> = ({ filter }) => {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{interactionError && (
|
||||||
|
<div className="app-toast fixed bottom-5 left-1/2 z-50 -translate-x-1/2 px-4 py-2 text-[12px]">
|
||||||
|
{interactionError}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user