updates
This commit is contained in:
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user