This commit is contained in:
Anthony Stirling
2026-01-30 00:49:38 +00:00
parent c879e54441
commit 93af41d74b
4 changed files with 160 additions and 11 deletions
@@ -44,8 +44,13 @@
"identifier": "fs:allow-write-file",
"allow": [{ "path": "**" }]
},
{
"identifier": "fs:allow-remove",
"allow": [{ "path": "**" }]
},
"dialog:default",
"dialog:allow-open",
"dialog:allow-save",
"opener:default",
"shell:allow-open"
]
@@ -22,6 +22,7 @@ import LightModeIcon from '@mui/icons-material/LightMode';
import { useSidebarContext } from '@app/contexts/SidebarContext';
import { RightRailButtonConfig, RightRailRenderContext, RightRailSection } from '@app/types/rightRail';
import { useRightRailTooltipSide } from '@app/hooks/useRightRailTooltipSide';
import { isTauri } from '@tauri-apps/api/core';
const SECTION_ORDER: RightRailSection[] = ['top', 'middle', 'bottom'];
@@ -151,16 +152,51 @@ export default function RightRail() {
return;
}
const filesToDownload = selectedFiles.length > 0 ? selectedFiles : activeFiles;
filesToDownload.forEach(file => {
const link = document.createElement('a');
link.href = URL.createObjectURL(file);
link.download = file.name;
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
URL.revokeObjectURL(link.href);
});
const filesToExport = selectedFiles.length > 0 ? selectedFiles : activeFiles;
// Desktop mode: Show "Save As" dialog for each file
if (isTauri()) {
try {
// @ts-ignore - Desktop module not available in proprietary build
const { showSaveDialog, saveToLocalPath } = await import('@desktop/services/localFileSaveService');
for (const file of filesToExport) {
const savePath = await showSaveDialog(file.name);
if (savePath) {
const result = await saveToLocalPath(file, savePath);
if (result.success) {
console.log(`[RightRail] Saved to: ${savePath}`);
} else {
console.error(`[RightRail] Failed to save: ${result.error}`);
alert(`Failed to save ${file.name}: ${result.error}`);
}
}
}
} catch (error) {
console.error('[RightRail] Save As error:', error);
// Fallback to browser download
filesToExport.forEach(file => {
const link = document.createElement('a');
link.href = URL.createObjectURL(file);
link.download = file.name;
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
URL.revokeObjectURL(link.href);
});
}
} else {
// Web mode: Regular download
filesToExport.forEach(file => {
const link = document.createElement('a');
link.href = URL.createObjectURL(file);
link.download = file.name;
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
URL.revokeObjectURL(link.href);
});
}
}, [
currentView,
selectedFiles,
@@ -174,11 +210,21 @@ export default function RightRail() {
if (currentView === 'pageEditor') {
return t('rightRail.exportAll', 'Export PDF');
}
// Desktop mode: "Save As" instead of "Download"
if (isTauri()) {
if (selectedCount > 0) {
return selectedCount === 1 ? 'Save As...' : `Save ${selectedCount} Files As...`;
}
return 'Save As...';
}
// Web mode: "Download"
if (selectedCount > 0) {
return terminology.downloadSelected;
}
return terminology.downloadAll;
}, [currentView, selectedCount, t]);
}, [currentView, selectedCount, t, terminology]);
return (
<div ref={sidebarRefs.rightRailRef} className="right-rail" data-sidebar="right-rail">
@@ -328,6 +328,47 @@ function FileContextInner({
addFiles: addRawFiles,
addStirlingFileStubs: addStirlingFileStubsAction,
removeFiles: async (fileIds: FileId[], deleteFromStorage?: boolean) => {
// Check if any files have localFilePath (desktop mode)
const filesWithLocalPaths: Array<{ id: FileId; path: string; name: string }> = [];
for (const fileId of fileIds) {
const stub = stateRef.current.files.byId[fileId];
if (stub?.localFilePath) {
filesWithLocalPaths.push({
id: fileId,
path: stub.localFilePath,
name: stub.name
});
}
}
// Ask user if they want to delete from disk (desktop only)
if (filesWithLocalPaths.length > 0) {
const fileList = filesWithLocalPaths.map(f => `• ${f.name}`).join('\n');
const message = filesWithLocalPaths.length === 1
? `Delete "${filesWithLocalPaths[0].name}" from disk?\n\nThis will permanently delete the file from:\n${filesWithLocalPaths[0].path}`
: `Delete ${filesWithLocalPaths.length} files from disk?\n\n${fileList}\n\nThis will permanently delete these files from your computer.`;
const shouldDeleteFromDisk = window.confirm(message);
if (shouldDeleteFromDisk) {
try {
// @ts-ignore - Desktop module not available in proprietary build
const { deleteLocalFile } = await import('@desktop/services/localFileSaveService');
for (const file of filesWithLocalPaths) {
const result = await deleteLocalFile(file.path);
if (result.success) {
console.log(`[FileContext] Deleted from disk: ${file.name}`);
} else {
console.warn(`[FileContext] Failed to delete from disk: ${file.name}`, result.error);
}
}
} catch (error) {
console.error('[FileContext] Failed to delete files from disk:', error);
}
}
}
// Remove from memory and cleanup resources
lifecycleManager.removeFiles(fileIds, stateRef);
@@ -43,6 +43,63 @@ export function shouldAutoSave(inputCount: number, outputCount: number): boolean
return isTauri() && inputCount === 1 && outputCount === 1;
}
/**
* Delete a file from local filesystem (Tauri desktop only)
*
* @param filePath - Absolute path to delete
* @returns Result indicating success or failure
*/
export async function deleteLocalFile(filePath: string): Promise<SaveResult> {
if (!isTauri()) {
return { success: false, error: "Not running in Tauri desktop app" };
}
try {
const { remove } = await import("@tauri-apps/plugin-fs");
await remove(filePath);
console.log(`[LocalFileDelete] Deleted: ${filePath}`);
return { success: true };
} catch (error) {
const message = error instanceof Error ? error.message : String(error);
console.error('[LocalFileDelete] Failed to delete:', message);
return { success: false, error: message };
}
}
/**
* Show native save dialog and return selected path
*
* @param defaultFilename - Suggested filename
* @param defaultDirectory - Optional default directory
* @returns Selected file path or null if cancelled
*/
export async function showSaveDialog(
defaultFilename: string,
defaultDirectory?: string
): Promise<string | null> {
if (!isTauri()) {
return null;
}
try {
const { save } = await import("@tauri-apps/plugin-dialog");
const selectedPath = await save({
defaultPath: defaultDirectory ? `${defaultDirectory}/${defaultFilename}` : defaultFilename,
filters: [{
name: 'PDF',
extensions: ['pdf']
}],
title: 'Save As'
});
return selectedPath;
} catch (error) {
console.error('[SaveDialog] Failed to show dialog:', error);
return null;
}
}
export interface MultiFileSaveResult {
success: boolean;
savedCount: number;