diff --git a/.gitignore b/.gitignore index ca683dc5d4..03a65905e1 100644 --- a/.gitignore +++ b/.gitignore @@ -312,3 +312,4 @@ docs/type3/signatures/ # Local screenshot artifacts from *-screenshots.spec.ts frontend/editor/screenshots/ +frontend/editor/src-tauri/libs/.variant diff --git a/frontend/editor/public/locales/en-US/translation.toml b/frontend/editor/public/locales/en-US/translation.toml index 87d8ffb556..402142ea17 100644 --- a/frontend/editor/public/locales/en-US/translation.toml +++ b/frontend/editor/public/locales/en-US/translation.toml @@ -4321,7 +4321,8 @@ download = "Download" downloadAll = "Download all" downloadVersion = "Download this version" dropOverlay = "Drop files to upload" -dropOverlaySub = "Files start in Local. Use 'Move to' or 'Save to cloud' to organize them into a folder." +dropOverlaySub = "Files land in Local. Organize them into folders any time." +dropOverlaySubFolder = "They'll be added to this folder." duplicate = "Duplicate" file = "File" fileInfo = "File info" @@ -4334,12 +4335,19 @@ inPath = "in {{path}}" inWorkspace = "Open" inWorkspaceAria = "Already in workspace" loading = "Loading…" -localFoldersUnavailable = "Folders are cloud-only - save a file to the cloud to organize it." +localFolderManagedByDisk = "This folder is managed by its directory on disk." +moveAcrossKindsBlocked = "These folders live in different places, so one can't go inside the other." +moveIntoMountCloudSkipped_one = "{{count}} server file stayed in your files. It lives on the server, not on this disk." +moveIntoMountCloudSkipped_other = "{{count}} server files stayed in your files. They live on the server, not on this disk." +moveIntoMountFailed_one = "{{count}} file could not be written into the folder." +moveIntoMountFailed_other = "{{count}} files could not be written into the folder." +moveIntoVirtualCloudSkipped_one = "{{count}} server file was left in place. Server files can't live in browser-only folders." +moveIntoVirtualCloudSkipped_other = "{{count}} server files were left in place. Server files can't live in browser-only folders." moveSkippedRemote_one = "{{count}} file couldn't be moved on the server (no permission or already deleted)." moveSkippedRemote_other = "{{count}} files couldn't be moved on the server (no permission or already deleted)." moveTo = "Move to…" newFolder = "New folder" -newFolderStorageDisabled = "Server folder storage isn't enabled. Ask your admin to turn it on." +newFolderStorageDisabled = "Server folder storage isn't enabled." newFolderTabUnavailable = "Switch to All or Cloud to create folders." offlineNoFolderEdits = "Server folder sync unavailable - folder changes are disabled. Check sign-in and storage configuration." open = "Open" @@ -4347,6 +4355,7 @@ openVersionInWorkspace = "Open in workspace" originFilter = "Filter by source" refresh = "Refresh from server" remove = "Delete" +removeLocalFolder = "Remove (files stay on disk)" removeVersion = "Remove this version" rename = "Rename" renamed = "Renamed" @@ -4359,6 +4368,7 @@ selectAll = "Select all" selectAllHint = "Click to select all. Tip: hold Ctrl (or Cmd) to add files one at a time, Shift to select a range." selectedCount = "{{count}} selected" selectFile = "Select file {{name}}" +serverFolderNeedsConnection = "Sign in to Stirling Cloud or connect a self-hosted server to use server folders." shareDisabledHint = "File sharing isn't enabled on this server. Ask your admin to enable it." shareManage = "Manage sharing" showDetails = "Show details" @@ -4367,7 +4377,6 @@ summary_one = "{{count}} item" summary_other = "{{count}} items" tree = "Folders" upload = "Upload" -uploadedToLocal = "Uploaded files start in Local. Use 'Save to cloud' to put them in a folder." uploadFromMobile = "Upload from Mobile" versionActions = "Version actions" versionCollapse = "Collapse middle versions" @@ -4433,6 +4442,8 @@ title = "You haven't shared any files yet" [filesPage.error] actionFailed = "Could not {{action}}." actionFailedDetail = "Could not {{action}}: {{message}}" +addFolderFailed = "Could not add the folder." +addFolderFailedDetail = "Could not add the folder: {{message}}" cloudDeleteFailed_one = "Couldn't delete 1 file from the cloud." cloudDeleteFailed_other = "Couldn't delete {{count}} files from the cloud." deleteFolderFailed = "Could not delete folder." @@ -4445,8 +4456,14 @@ moveFilesFailed = "Could not move files." moveFilesFailedDetail = "Could not move files: {{message}}" moveFolderFailed = "Could not move folder." moveFolderFailedDetail = "Could not move folder: {{message}}" +openDiskFileFailed = "Could not open {{name}}." +openDiskFileFailedDetail = "Could not open {{name}}: {{message}}" +readFolderFailed = "Could not read the folder." +readFolderFailedDetail = "Could not read the folder: {{message}}" removeFilesFailed = "Could not remove files." removeFilesFailedDetail = "Could not remove files: {{message}}" +removeFolderFailed = "Could not remove folder." +removeFolderFailedDetail = "Could not remove folder: {{message}}" uploadFilesFailed = "Could not upload files." uploadFilesFailedDetail = "Could not upload files: {{message}}" @@ -4466,12 +4483,21 @@ activeCount = "{{count}} filters active" clearAll = "Clear filters" label = "Filters" +[filesPage.folderKind] +local = "Local folder" +virtual = "Browser folder" + [filesPage.folderName] cancel = "Cancel" error = "Could not save folder. Try again." label = "Folder name" placeholder = "Folder name" +[filesPage.folderOrigin] +diskHint = "A folder mounted from a directory on your disk" +serverHint = "A folder stored on the Stirling server" +virtualHint = "A folder that lives only in this browser" + [filesPage.moveDialog] cancel = "Cancel" confirm = "Move here" @@ -4485,10 +4511,16 @@ newFolderPlaceholder = "Folder name" newFolderToggle = "Create new folder…" title = "Move to folder" +[filesPage.newFolderMenu] +addExisting = "Add local folder" +server = "New folder on the server" +serverHint = "Synced to your account, available wherever you sign in." + [filesPage.origin] all = "All sources" cloud = "Cloud" cloudHint = "Stored on the Stirling server" +diskHint = "A file in the mounted folder on your disk" local = "Local" localHint = "Only stored in this browser" shared = "Shared" diff --git a/frontend/editor/src-tauri/capabilities/default.json b/frontend/editor/src-tauri/capabilities/default.json index 4147b69ce0..34d333a3de 100644 --- a/frontend/editor/src-tauri/capabilities/default.json +++ b/frontend/editor/src-tauri/capabilities/default.json @@ -39,6 +39,18 @@ "identifier": "fs:allow-read-file", "allow": [{ "path": "**" }] }, + { + "identifier": "fs:allow-read-dir", + "allow": [{ "path": "**" }] + }, + { + "identifier": "fs:allow-stat", + "allow": [{ "path": "**" }] + }, + { + "identifier": "fs:allow-mkdir", + "allow": [{ "path": "**" }] + }, { "identifier": "fs:allow-write-file", "allow": [{ "path": "**" }] diff --git a/frontend/editor/src/core/components/filesPage/FileGrid.tsx b/frontend/editor/src/core/components/filesPage/FileGrid.tsx index 334ad76805..8ab5bdbca9 100644 --- a/frontend/editor/src/core/components/filesPage/FileGrid.tsx +++ b/frontend/editor/src/core/components/filesPage/FileGrid.tsx @@ -20,7 +20,13 @@ import CreateNewFolderIcon from "@mui/icons-material/CreateNewFolder"; import SearchIcon from "@mui/icons-material/Search"; import { FileId } from "@app/types/file"; -import { FolderId, FolderRecord, ROOT_FOLDER_ID } from "@app/types/folder"; +import { + FolderId, + FolderRecord, + ROOT_FOLDER_ID, + folderKind, +} from "@app/types/folder"; +import type { DiskFileEntry } from "@app/services/localFolderContents"; import { useFolders } from "@app/contexts/FolderContext"; import { usePolicyFileBadges } from "@app/hooks/usePolicyFileBadges"; import { StirlingFileStub } from "@app/types/fileContext"; @@ -36,20 +42,64 @@ import { FileOriginBadge } from "@app/components/filesPage/FileOriginBadge"; import { FolderThumbnail } from "@app/components/filesPage/FolderThumbnail"; import { findFolderIcon } from "@app/components/filesPage/folderIcons"; import { FolderAppearancePicker } from "@app/components/filesPage/FolderAppearancePicker"; -import { useLazyThumbnail } from "@app/hooks/useLazyThumbnail"; +import { + useLazyThumbnail, + useDiskThumbnail, +} from "@app/hooks/useLazyThumbnail"; import { useFileActionIcons } from "@app/hooks/useFileActionIcons"; import { useFileActionTerminology } from "@app/hooks/useFileActionTerminology"; import type { FilesPageSortMode } from "@app/contexts/FilesPageContext"; import { OpenInNewWindowMenuItem } from "@app/components/filesPage/OpenInNewWindowMenuItem"; +/** + * The origin badge a folder wears, mirroring the one its files would: a server folder + * is Cloud, a virtual folder is Local (this browser), a mounted folder is On disk. + */ +function useFolderOriginBadge(folder: FolderRecord): { + origin: "cloud" | "local"; + tooltip: string; +} { + const { t } = useTranslation(); + switch (folderKind(folder)) { + case "virtual": + return { + origin: "local", + tooltip: t( + "filesPage.folderOrigin.virtualHint", + "A folder that lives only in this browser", + ), + }; + case "local": + return { + // Same mark as a virtual folder: what matters is that it lives on + // this device, not which corner of it. The tooltip says which. + origin: "local", + tooltip: t( + "filesPage.folderOrigin.diskHint", + "A folder mounted from a directory on your disk", + ), + }; + default: + return { + origin: "cloud", + tooltip: t( + "filesPage.folderOrigin.serverHint", + "A folder stored on the Stirling server", + ), + }; + } +} + export type FilesPageViewMode = "grid" | "list"; export interface FilesPageEntry { - kind: "folder" | "file"; + kind: "folder" | "file" | "diskFile"; folder?: FolderRecord; /** Number of files inside this folder (folder entries only). */ folderFileCount?: number; file?: StirlingFileStub; + /** A file read straight off a mounted directory (kind "diskFile"). */ + disk?: DiskFileEntry; /** Parent breadcrumb path for search results outside the current folder. */ parentPath?: string; } @@ -66,6 +116,7 @@ interface FileGridProps { onOpenFolder: (id: FolderId) => void; /** "Add to workspace". */ onOpenFile: (file: StirlingFileStub) => void; + onOpenDiskFile?: (entry: DiskFileEntry) => void; onMoveFiles: ( fileIds: FileId[], targetFolderId: FolderId | null, @@ -383,6 +434,7 @@ function GridView(props: FileGridProps) { onSelectFile, onOpenFolder, onOpenFile, + onOpenDiskFile, onMoveFiles, onMoveFolder, onRenameFolder, @@ -414,6 +466,15 @@ function GridView(props: FileGridProps) { /> ); } + if (entry.kind === "diskFile" && entry.disk) { + return ( + onOpenDiskFile?.(entry.disk!)} + /> + ); + } if (entry.kind === "file" && entry.file) { return (