diff --git a/packages/apps/file-explorer/src/components/FileExplorer.tsx b/packages/apps/file-explorer/src/components/FileExplorer.tsx index 6b35d205..02fb7738 100644 --- a/packages/apps/file-explorer/src/components/FileExplorer.tsx +++ b/packages/apps/file-explorer/src/components/FileExplorer.tsx @@ -1,7 +1,7 @@ import { ChangeEventHandler, FC, KeyboardEventHandler, useCallback, useEffect, useState } from "react"; import styles from "./FileExplorer.module.css"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; -import { faArrowUp, faCaretLeft, faCaretRight, faCircleInfo, faCog, faDesktop, faFileLines, faHouse, faImage, faPlus, faSearch, faTrash } from "@fortawesome/free-solid-svg-icons"; +import { faArrowUp, faCaretLeft, faCaretRight, faCircleInfo, faCog, faDesktop, faFileLines, faHouse, faImage, faPlus, faSearch, faTrash, faUpload } from "@fortawesome/free-solid-svg-icons"; import { QuickAccessButton } from "./QuickAccessButton"; import { ImportButton } from "./ImportButton"; import { Actions, ClickAction, CODE_EXTENSIONS, DialogBox, DirectoryList, Divider, FileEventHandler, FolderEventHandler, ModalProps, ModalsConfig, OnSelectionChangeParams, useAlert, useContextMenu, useHistory, useSystemManager, useVirtualRoot, useWindowedModal, useWindowsManager, utilStyles, Vector2, VirtualFile, VirtualFolder, VirtualFolderLink, VirtualRoot, WindowProps } from "@prozilla-os/core"; @@ -41,6 +41,11 @@ export function FileExplorer({ app, path: startPath, selectorMode, Footer, onSel } if (windowsManager != null) (file as VirtualFile).open(windowsManager); }}/> + {(props.triggerParams as VirtualFile)?.isDownloadable() && + { + (file as VirtualFile).download(); + }}/> + } { (file as VirtualFile).delete(); }}/> diff --git a/packages/core/src/features/_utils/browser.utils.ts b/packages/core/src/features/_utils/browser.utils.ts index 5097007f..9cf8f3c2 100644 --- a/packages/core/src/features/_utils/browser.utils.ts +++ b/packages/core/src/features/_utils/browser.utils.ts @@ -117,4 +117,17 @@ export function removeBaseUrl(url: string) { export function copyToClipboard(string: string, onSuccess?: (value: void) => void, onFail?: (value: void) => void) { void navigator.clipboard.writeText(string).then(onSuccess, onFail); +} + +export function downloadUrl(url: string, name: string) { + // Create invisible anchor element with download URL + const anchor = document.createElement("a"); + anchor.href = url; + anchor.download = name; + anchor.style.display = "none"; + + // Click anchor element + document.body.appendChild(anchor); + anchor.click(); + document.body.removeChild(anchor); } \ No newline at end of file diff --git a/packages/core/src/features/virtual-drive/file/virtualFile.ts b/packages/core/src/features/virtual-drive/file/virtualFile.ts index 22e7c818..59e2075c 100644 --- a/packages/core/src/features/virtual-drive/file/virtualFile.ts +++ b/packages/core/src/features/virtual-drive/file/virtualFile.ts @@ -1,4 +1,5 @@ import { FILE_SCHEMES, IMAGE_EXTENSIONS } from "../../../constants/virtualDrive.const"; +import { downloadUrl } from "../../_utils"; import { WindowsManager } from "../../windows/windowsManager"; import { VirtualBase, VirtualBaseJson } from "../virtualBase"; @@ -187,6 +188,34 @@ export class VirtualFile extends VirtualBase { return `${type} file (.${this.extension.toLowerCase()})`.trim(); } + download() { + if (!this.isDownloadable()) { + return; + } + + try { + if (this.source != null) { + downloadUrl(this.source, this.id); + } else if (this.content != null) { + const blob = new Blob([this.content], { type: "text/plain" }); + const url = window.URL.createObjectURL(blob); + downloadUrl(url, this.id); + window.URL.revokeObjectURL(url); + } + } catch (error) { + console.error("Error while downloading file:", error); + } + } + + isDownloadable(): boolean { + if (this.content != null) { + return true; + } else if (this.source != null) { + return !this.source.startsWith(FILE_SCHEMES.external) && !this.source.startsWith(FILE_SCHEMES.app); + } + return false; + } + toJSON(): VirtualFileJson | null { // Don't return file if it can't or hasn't been edited if (!this.canBeEdited || (this.editedByUser == null || !this.editedByUser))