Add save actions and cross-platform file manager support (#848)
* files-download-feature * feat: add save option to file directory context menus and viewer * fix: open files in the system file manager
This commit is contained in:
@@ -13,6 +13,7 @@ import {
|
||||
RiMore2Fill,
|
||||
RiRefreshLine,
|
||||
RiSearchLine,
|
||||
RiDownloadLine,
|
||||
} from '@remixicon/react';
|
||||
|
||||
import { toast } from '@/components/ui';
|
||||
@@ -44,7 +45,7 @@ import { useGitStatus } from '@/stores/useGitStore';
|
||||
import { useDirectoryShowHidden } from '@/lib/directoryShowHidden';
|
||||
import { useFilesViewShowGitignored } from '@/lib/filesViewShowGitignored';
|
||||
import { copyTextToClipboard } from '@/lib/clipboard';
|
||||
import { cn } from '@/lib/utils';
|
||||
import { cn, getRevealLabel } from '@/lib/utils';
|
||||
import { opencodeClient } from '@/lib/opencode/client';
|
||||
import { FileTypeIcon } from '@/components/icons/FileTypeIcon';
|
||||
import { getContextFileOpenFailureMessage, validateContextFileOpen } from '@/lib/contextFileOpenGuard';
|
||||
@@ -132,6 +133,7 @@ interface FileRowProps {
|
||||
canDelete: boolean;
|
||||
canReveal: boolean;
|
||||
};
|
||||
downloadFile?: (path: string) => Promise<void>;
|
||||
contextMenuPath: string | null;
|
||||
setContextMenuPath: (path: string | null) => void;
|
||||
onSelect: (node: FileNode) => void;
|
||||
@@ -147,6 +149,7 @@ const FileRow: React.FC<FileRowProps> = ({
|
||||
status,
|
||||
badge,
|
||||
permissions,
|
||||
downloadFile,
|
||||
contextMenuPath,
|
||||
setContextMenuPath,
|
||||
onSelect,
|
||||
@@ -244,9 +247,17 @@ const FileRow: React.FC<FileRowProps> = ({
|
||||
}}>
|
||||
<RiFileCopyLine className="mr-2 h-4 w-4" /> Copy Path
|
||||
</DropdownMenuItem>
|
||||
{!isDir && downloadFile && (
|
||||
<DropdownMenuItem onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
void downloadFile(node.path);
|
||||
}}>
|
||||
<RiDownloadLine className="mr-2 h-4 w-4" /> Save
|
||||
</DropdownMenuItem>
|
||||
)}
|
||||
{canReveal && (
|
||||
<DropdownMenuItem onClick={(e) => { e.stopPropagation(); onRevealPath(node.path); }}>
|
||||
<RiFolderReceivedLine className="mr-2 h-4 w-4" /> Reveal in Finder
|
||||
<RiFolderReceivedLine className="mr-2 h-4 w-4" /> {getRevealLabel()}
|
||||
</DropdownMenuItem>
|
||||
)}
|
||||
{isDir && (canCreateFile || canCreateFolder) && (
|
||||
@@ -749,6 +760,7 @@ export const SidebarFilesTree: React.FC = () => {
|
||||
status={!isDir ? getFileStatus(node.path) : undefined}
|
||||
badge={isDir ? getFolderBadge(node.path) : undefined}
|
||||
permissions={{ canRename, canCreateFile, canCreateFolder, canDelete, canReveal }}
|
||||
downloadFile={files.downloadFile}
|
||||
contextMenuPath={contextMenuPath}
|
||||
setContextMenuPath={setContextMenuPath}
|
||||
onSelect={handleOpenFile}
|
||||
|
||||
@@ -26,6 +26,7 @@ import {
|
||||
RiFileTransferLine,
|
||||
RiCodeSSlashLine,
|
||||
RiNodeTree,
|
||||
RiDownloadLine,
|
||||
} from '@remixicon/react';
|
||||
import { toast } from '@/components/ui';
|
||||
import { copyTextToClipboard } from '@/lib/clipboard';
|
||||
@@ -58,7 +59,7 @@ import {
|
||||
import { useDebouncedValue } from '@/hooks/useDebouncedValue';
|
||||
import { useFileSearchStore } from '@/stores/useFileSearchStore';
|
||||
import { useDeviceInfo } from '@/lib/device';
|
||||
import { cn, getModifierLabel, hasModifier } from '@/lib/utils';
|
||||
import { cn, getModifierLabel, getRevealLabel, hasModifier } from '@/lib/utils';
|
||||
import { getLanguageFromExtension, getImageMimeType, isImageFile } from '@/lib/toolHelpers';
|
||||
import { useRuntimeAPIs } from '@/hooks/useRuntimeAPIs';
|
||||
import { EditorView } from '@codemirror/view';
|
||||
@@ -292,6 +293,7 @@ interface FileRowProps {
|
||||
canDelete: boolean;
|
||||
canReveal: boolean;
|
||||
};
|
||||
downloadFile?: (path: string) => Promise<void>;
|
||||
contextMenuPath: string | null;
|
||||
setContextMenuPath: (path: string | null) => void;
|
||||
onSelect: (node: FileNode) => void;
|
||||
@@ -308,6 +310,7 @@ const FileRow: React.FC<FileRowProps> = ({
|
||||
status,
|
||||
badge,
|
||||
permissions,
|
||||
downloadFile,
|
||||
contextMenuPath,
|
||||
setContextMenuPath,
|
||||
onSelect,
|
||||
@@ -414,9 +417,17 @@ const FileRow: React.FC<FileRowProps> = ({
|
||||
}}>
|
||||
<RiFileCopyLine className="mr-2 h-4 w-4" /> Copy Path
|
||||
</DropdownMenuItem>
|
||||
{!isDir && downloadFile && (
|
||||
<DropdownMenuItem onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
void downloadFile(node.path);
|
||||
}}>
|
||||
<RiDownloadLine className="mr-2 h-4 w-4" /> Save
|
||||
</DropdownMenuItem>
|
||||
)}
|
||||
{canReveal && (
|
||||
<DropdownMenuItem onClick={(e) => { e.stopPropagation(); onRevealPath(node.path); }}>
|
||||
<RiFolderReceivedLine className="mr-2 h-4 w-4" /> Reveal in Finder
|
||||
<RiFolderReceivedLine className="mr-2 h-4 w-4" /> {getRevealLabel()}
|
||||
</DropdownMenuItem>
|
||||
)}
|
||||
{isDir && (canCreateFile || canCreateFolder) && (
|
||||
@@ -1638,6 +1649,7 @@ export const FilesView: React.FC<FilesViewProps> = ({ mode = 'full' }) => {
|
||||
status={!isDir ? getFileStatus(node.path) : undefined}
|
||||
badge={isDir ? getFolderBadge(node.path) : undefined}
|
||||
permissions={{ canRename, canCreateFile, canCreateFolder, canDelete, canReveal }}
|
||||
downloadFile={files.downloadFile}
|
||||
contextMenuPath={contextMenuPath}
|
||||
setContextMenuPath={setContextMenuPath}
|
||||
onSelect={handleSelectFile}
|
||||
@@ -2426,6 +2438,22 @@ export const FilesView: React.FC<FilesViewProps> = ({ mode = 'full' }) => {
|
||||
</Button>
|
||||
)}
|
||||
|
||||
{files.downloadFile && (
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => {
|
||||
const fn = files.downloadFile;
|
||||
if (fn) void fn(selectedFile.path);
|
||||
}}
|
||||
className="h-6 w-6 p-0"
|
||||
title="Save file"
|
||||
aria-label="Save file"
|
||||
>
|
||||
<RiDownloadLine className="h-4 w-4" />
|
||||
</Button>
|
||||
)}
|
||||
|
||||
{exitFullscreenOnly ? (
|
||||
<Button
|
||||
variant="ghost"
|
||||
|
||||
Reference in New Issue
Block a user