From 06de37916964daeb5a4a57afb717d6c29371ee5e Mon Sep 17 00:00:00 2001 From: Brendan Allan Date: Tue, 27 Jun 2023 17:34:53 +0200 Subject: [PATCH] [ENG-816, ENG-821] Re-implement reveal in finder + ContextMenu overhaul (#1029) * mostly there * native opening working * more * cleanup * reorganise * remove unnecessary import * uncomment some stuff * spacing * store quickview ref inside provider * fix linting * clippy --------- Co-authored-by: Utku <74243531+utkubakir@users.noreply.github.com> Co-authored-by: Jamie Pine <32987599+jamiepine@users.noreply.github.com> --- Cargo.lock | 25 + apps/desktop/src-tauri/Cargo.toml | 5 +- apps/desktop/src-tauri/src/file.rs | 86 ++- apps/desktop/src-tauri/src/main.rs | 3 +- apps/desktop/src/App.tsx | 18 +- apps/desktop/src/commands.ts | 11 +- core/prisma/schema.prisma | 2 +- interface/app/$libraryId/Explorer/Context.tsx | 35 ++ .../app/$libraryId/Explorer/ContextMenu.tsx | 200 ------- .../ContextMenu/FilePath/CutCopyItems.tsx | 74 +++ .../Explorer/ContextMenu/FilePath/Items.tsx | 210 +++++++ .../FilePath}/OpenWith.tsx | 1 + .../Explorer/ContextMenu/FilePath/index.tsx | 77 +++ .../Explorer/ContextMenu/Location/index.tsx | 29 + .../Explorer/ContextMenu/Object/Items.tsx | 56 ++ .../Explorer/ContextMenu/Object/index.tsx | 58 ++ .../Explorer/ContextMenu/SharedItems.tsx | 130 +++++ .../$libraryId/Explorer/ContextMenu/index.tsx | 19 + .../$libraryId/Explorer/DismissibleNotice.tsx | 2 +- .../$libraryId/Explorer/File/ContextMenu.tsx | 407 -------------- .../{File => FilePath}/DecryptDialog.tsx | 0 .../{File => FilePath}/DeleteDialog.tsx | 0 .../{File => FilePath}/EncryptDialog.tsx | 0 .../{File => FilePath}/EraseDialog.tsx | 0 .../{File => FilePath}/RenameTextBox.tsx | 0 .../{File => FilePath}/Thumb.module.scss | 0 .../Explorer/{File => FilePath}/Thumb.tsx | 27 +- .../$libraryId/Explorer/Inspector/index.tsx | 5 +- .../app/$libraryId/Explorer/OptionsPanel.tsx | 9 +- .../$libraryId/Explorer/ParentContextMenu.tsx | 165 ++++++ .../Explorer/QuickPreview/Context.tsx | 26 + .../{ => QuickPreview}/QuickPreview.tsx | 4 +- .../Explorer/QuickPreview/index.tsx | 131 +++++ .../$libraryId/Explorer/TopBarOptions.tsx} | 61 +- .../app/$libraryId/Explorer/View/GridView.tsx | 4 +- .../app/$libraryId/Explorer/View/ListView.tsx | 11 +- .../$libraryId/Explorer/View/MediaView.tsx | 4 +- .../Explorer/View/RenamableItemText.tsx | 4 +- .../app/$libraryId/Explorer/View/index.tsx | 532 +++++++++--------- .../$libraryId/Explorer/config.ts} | 0 interface/app/$libraryId/Explorer/index.tsx | 25 +- interface/app/$libraryId/Explorer/store.ts | 86 +++ interface/app/$libraryId/Explorer/util.ts | 24 +- interface/app/$libraryId/Layout/index.tsx | 15 +- interface/app/$libraryId/location/$id.tsx | 51 +- .../$libraryId/location/LocationOptions.tsx | 13 +- interface/app/$libraryId/node/$id.tsx | 37 +- interface/app/$libraryId/overview/data.ts | 2 +- interface/app/$libraryId/overview/index.tsx | 24 +- interface/app/$libraryId/search.tsx | 35 +- interface/app/$libraryId/tag/$id.tsx | 43 +- interface/hooks/index.ts | 4 - interface/hooks/useExplorerItemData.ts | 22 - interface/hooks/useExplorerStore.tsx | 88 --- interface/hooks/useKeyDeleteFile.tsx | 2 +- interface/hooks/useKeybindFactory.ts | 4 + interface/util/Platform.tsx | 7 +- 57 files changed, 1693 insertions(+), 1220 deletions(-) create mode 100644 interface/app/$libraryId/Explorer/Context.tsx delete mode 100644 interface/app/$libraryId/Explorer/ContextMenu.tsx create mode 100644 interface/app/$libraryId/Explorer/ContextMenu/FilePath/CutCopyItems.tsx create mode 100644 interface/app/$libraryId/Explorer/ContextMenu/FilePath/Items.tsx rename interface/app/$libraryId/Explorer/{File/ContextMenu => ContextMenu/FilePath}/OpenWith.tsx (99%) create mode 100644 interface/app/$libraryId/Explorer/ContextMenu/FilePath/index.tsx create mode 100644 interface/app/$libraryId/Explorer/ContextMenu/Location/index.tsx create mode 100644 interface/app/$libraryId/Explorer/ContextMenu/Object/Items.tsx create mode 100644 interface/app/$libraryId/Explorer/ContextMenu/Object/index.tsx create mode 100644 interface/app/$libraryId/Explorer/ContextMenu/SharedItems.tsx create mode 100644 interface/app/$libraryId/Explorer/ContextMenu/index.tsx delete mode 100644 interface/app/$libraryId/Explorer/File/ContextMenu.tsx rename interface/app/$libraryId/Explorer/{File => FilePath}/DecryptDialog.tsx (100%) rename interface/app/$libraryId/Explorer/{File => FilePath}/DeleteDialog.tsx (100%) rename interface/app/$libraryId/Explorer/{File => FilePath}/EncryptDialog.tsx (100%) rename interface/app/$libraryId/Explorer/{File => FilePath}/EraseDialog.tsx (100%) rename interface/app/$libraryId/Explorer/{File => FilePath}/RenameTextBox.tsx (100%) rename interface/app/$libraryId/Explorer/{File => FilePath}/Thumb.module.scss (100%) rename interface/app/$libraryId/Explorer/{File => FilePath}/Thumb.tsx (95%) create mode 100644 interface/app/$libraryId/Explorer/ParentContextMenu.tsx create mode 100644 interface/app/$libraryId/Explorer/QuickPreview/Context.tsx rename interface/app/$libraryId/Explorer/{ => QuickPreview}/QuickPreview.tsx (97%) create mode 100644 interface/app/$libraryId/Explorer/QuickPreview/index.tsx rename interface/{hooks/useExplorerTopBarOptions.tsx => app/$libraryId/Explorer/TopBarOptions.tsx} (74%) rename interface/{hooks/useExplorerConfigStore.ts => app/$libraryId/Explorer/config.ts} (100%) create mode 100644 interface/app/$libraryId/Explorer/store.ts delete mode 100644 interface/hooks/useExplorerItemData.ts delete mode 100644 interface/hooks/useExplorerStore.tsx create mode 100644 interface/hooks/useKeybindFactory.ts diff --git a/Cargo.lock b/Cargo.lock index 216e97c29..756ca7d2a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1763,6 +1763,17 @@ dependencies = [ "regex", ] +[[package]] +name = "dbus" +version = "0.9.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bb21987b9fb1613058ba3843121dd18b163b254d8a6e797e144cbac14d96d1b" +dependencies = [ + "libc", + "libdbus-sys", + "winapi", +] + [[package]] name = "deps-generator" version = "0.0.0" @@ -3837,6 +3848,16 @@ version = "0.2.146" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f92be4933c13fd498862a9e02a3055f8a8d9c039ce33db97306fd5a6caa7f29b" +[[package]] +name = "libdbus-sys" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06085512b750d640299b79be4bad3d2fa90a9c00b1fd9e1b46364f66f0485c72" +dependencies = [ + "cc", + "pkg-config", +] + [[package]] name = "libheif-rs" version = "0.19.2" @@ -5192,7 +5213,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6c62dcb6174f9cb326eac248f07e955d5d559c272730b6c03e396b443b562788" dependencies = [ "bstr", + "dbus", "normpath", + "url", "winapi", ] @@ -7794,12 +7817,14 @@ dependencies = [ "httpz 0.0.3", "opener", "percent-encoding", + "prisma-client-rust", "rand 0.8.5", "rspc", "sd-core", "sd-desktop-linux", "sd-desktop-macos", "sd-desktop-windows", + "sd-prisma", "serde", "specta", "tauri", diff --git a/apps/desktop/src-tauri/Cargo.toml b/apps/desktop/src-tauri/Cargo.toml index 84b0f6bad..9ecffc668 100644 --- a/apps/desktop/src-tauri/Cargo.toml +++ b/apps/desktop/src-tauri/Cargo.toml @@ -26,11 +26,14 @@ tracing = "0.1.37" serde = "1.0.163" percent-encoding = "2.2.0" http = "0.2.9" -opener = "0.6.1" +opener = { version = "0.6.1", features = ["reveal"] } specta = { workspace = true } tauri-specta = { workspace = true, features = ["typescript"] } uuid = { version = "1.3.3", features = ["serde"] } +prisma-client-rust = { workspace = true } +sd-prisma = { path = "../../../crates/prisma" } + [target.'cfg(target_os = "linux")'.dependencies] axum = { version = "0.6.18", features = ["headers", "query"] } rand = "0.8.5" diff --git a/apps/desktop/src-tauri/src/file.rs b/apps/desktop/src-tauri/src/file.rs index b9632f3b4..f96c79e4e 100644 --- a/apps/desktop/src-tauri/src/file.rs +++ b/apps/desktop/src-tauri/src/file.rs @@ -1,10 +1,18 @@ -use std::{collections::HashMap, sync::Arc}; +use std::{ + collections::{BTreeSet, HashMap}, + sync::Arc, +}; -use sd_core::Node; +use sd_core::{ + prisma::{file_path, location}, + Node, +}; use serde::Serialize; use specta::Type; use tracing::error; +type NodeState<'a> = tauri::State<'a, Arc>; + #[derive(Serialize, Type)] #[serde(tag = "t", content = "c")] pub enum OpenFilePathResult { @@ -17,7 +25,7 @@ pub enum OpenFilePathResult { #[tauri::command(async)] #[specta::specta] -pub async fn open_file_path( +pub async fn open_file_paths( library: uuid::Uuid, ids: Vec, node: tauri::State<'_, Arc>, @@ -64,7 +72,7 @@ pub struct OpenWithApplication { pub async fn get_file_path_open_with_apps( library: uuid::Uuid, ids: Vec, - node: tauri::State<'_, Arc>, + node: NodeState<'_>, ) -> Result, ()> { let Some(library) = node.library_manager.get_library(library).await else { @@ -210,7 +218,7 @@ type FileIdAndUrl = (i32, String); pub async fn open_file_path_with( library: uuid::Uuid, file_ids_and_urls: Vec, - node: tauri::State<'_, Arc>, + node: NodeState<'_>, ) -> Result<(), ()> { let Some(library) = node.library_manager.get_library(library).await else { @@ -272,3 +280,71 @@ pub async fn open_file_path_with( .map(|_| ()) }) } + +#[derive(specta::Type, serde::Deserialize)] +pub enum RevealItem { + Location { id: location::id::Type }, + FilePath { id: file_path::id::Type }, +} + +#[tauri::command(async)] +#[specta::specta] +pub async fn reveal_items( + library: uuid::Uuid, + items: Vec, + node: NodeState<'_>, +) -> Result<(), ()> { + let Some(library) = node.library_manager.get_library(library).await + else { + return Err(()) + }; + + let (paths, locations): (Vec<_>, Vec<_>) = + items + .into_iter() + .fold((vec![], vec![]), |(mut paths, mut locations), item| { + match item { + RevealItem::FilePath { id } => paths.push(id), + RevealItem::Location { id } => locations.push(id), + } + + (paths, locations) + }); + + let mut paths_to_open = BTreeSet::new(); + + if !paths.is_empty() { + paths_to_open.extend( + library + .get_file_paths(paths) + .await + .unwrap_or_default() + .into_values() + .flatten(), + ); + } + + if !locations.is_empty() { + paths_to_open.extend( + library + .db + .location() + .find_many(vec![ + location::node_id::equals(Some(library.node_local_id)), + location::id::in_vec(locations), + ]) + .select(location::select!({ path })) + .exec() + .await + .unwrap_or_default() + .into_iter() + .flat_map(|location| location.path.map(Into::into)), + ); + } + + for path in paths_to_open { + opener::reveal(path).ok(); + } + + Ok(()) +} diff --git a/apps/desktop/src-tauri/src/main.rs b/apps/desktop/src-tauri/src/main.rs index bcd166a9c..63940e67b 100644 --- a/apps/desktop/src-tauri/src/main.rs +++ b/apps/desktop/src-tauri/src/main.rs @@ -167,9 +167,10 @@ async fn main() -> tauri::Result<()> { app_ready, reset_spacedrive, open_logs_dir, - file::open_file_path, + file::open_file_paths, file::get_file_path_open_with_apps, file::open_file_path_with, + file::reveal_items, theme::lock_app_theme ]) .build(tauri::generate_context!())?; diff --git a/apps/desktop/src/App.tsx b/apps/desktop/src/App.tsx index a3ab50f26..deaa9e7c5 100644 --- a/apps/desktop/src/App.tsx +++ b/apps/desktop/src/App.tsx @@ -17,14 +17,7 @@ import { } from '@sd/interface'; import { getSpacedropState } from '@sd/interface/hooks/useSpacedropState'; import '@sd/ui/style'; -import { - appReady, - getFilePathOpenWithApps, - lockAppTheme, - openFilePath, - openFilePathWith, - openLogsDir -} from './commands'; +import * as commands from './commands'; // TODO: Bring this back once upstream is fixed up. // const client = hooks.createClient({ @@ -81,12 +74,7 @@ const platform: Platform = { openFilePickerDialog: () => dialog.open(), saveFilePickerDialog: () => dialog.save(), showDevtools: () => invoke('show_devtools'), - openPath: (path) => shell.open(path), - openLogsDir, - openFilePath, - getFilePathOpenWithApps, - openFilePathWith, - lockAppTheme + ...commands }; const queryClient = new QueryClient(); @@ -96,7 +84,7 @@ const router = createBrowserRouter(routes); export default function App() { useEffect(() => { // This tells Tauri to show the current window because it's finished loading - appReady(); + commands.appReady(); }, []); useEffect(() => { diff --git a/apps/desktop/src/commands.ts b/apps/desktop/src/commands.ts index b03e0fbe4..3d136f3ef 100644 --- a/apps/desktop/src/commands.ts +++ b/apps/desktop/src/commands.ts @@ -22,8 +22,8 @@ export function openLogsDir() { return invoke()("open_logs_dir") } -export function openFilePath(library: string, ids: number[]) { - return invoke()("open_file_path", { library,ids }) +export function openFilePaths(library: string, ids: number[]) { + return invoke()("open_file_paths", { library,ids }) } export function getFilePathOpenWithApps(library: string, ids: number[]) { @@ -34,10 +34,15 @@ export function openFilePathWith(library: string, fileIdsAndUrls: ([number, stri return invoke()("open_file_path_with", { library,fileIdsAndUrls }) } +export function revealItems(library: string, items: RevealItem[]) { + return invoke()("reveal_items", { library,items }) +} + export function lockAppTheme(themeType: AppThemeType) { return invoke()("lock_app_theme", { themeType }) } +export type OpenFilePathResult = { t: "NoLibrary" } | { t: "NoFile"; c: number } | { t: "OpenError"; c: [number, string] } | { t: "AllGood"; c: number } | { t: "Internal"; c: string } +export type RevealItem = { Location: { id: number } } | { FilePath: { id: number } } export type OpenWithApplication = { id: number; name: string; url: string } export type AppThemeType = "Auto" | "Light" | "Dark" -export type OpenFilePathResult = { t: "NoLibrary" } | { t: "NoFile"; c: number } | { t: "OpenError"; c: [number, string] } | { t: "AllGood"; c: number } | { t: "Internal"; c: string } diff --git a/core/prisma/schema.prisma b/core/prisma/schema.prisma index fc90fb598..01273b7fb 100644 --- a/core/prisma/schema.prisma +++ b/core/prisma/schema.prisma @@ -6,7 +6,7 @@ datasource db { generator client { provider = "cargo prisma" output = "../../crates/prisma/src/prisma.rs" - module_path = "crate::prisma" + module_path = "sd_prisma::prisma" } generator sync { diff --git a/interface/app/$libraryId/Explorer/Context.tsx b/interface/app/$libraryId/Explorer/Context.tsx new file mode 100644 index 000000000..e4114dc5d --- /dev/null +++ b/interface/app/$libraryId/Explorer/Context.tsx @@ -0,0 +1,35 @@ +import { createContext, useContext } from 'react'; +import { FilePath, Location, NodeState, Tag } from '@sd/client'; + +export type ExplorerParent = + | { + type: 'Location'; + location: Location; + subPath?: FilePath; + } + | { + type: 'Tag'; + tag: Tag; + } + | { + type: 'Node'; + node: NodeState; + }; + +interface ExplorerContext { + parent?: ExplorerParent; +} + +/** + * Context that must wrap anything to do with the explorer. + * This includes explorer views, the inspector, and top bar items. +*/ +export const ExplorerContext = createContext(null); + +export const useExplorerContext = () => { + const ctx = useContext(ExplorerContext); + + if (ctx === null) throw new Error('ExplorerContext.Provider not found!'); + + return ctx; +}; diff --git a/interface/app/$libraryId/Explorer/ContextMenu.tsx b/interface/app/$libraryId/Explorer/ContextMenu.tsx deleted file mode 100644 index 38a0fa84d..000000000 --- a/interface/app/$libraryId/Explorer/ContextMenu.tsx +++ /dev/null @@ -1,200 +0,0 @@ -import { Clipboard, FileX, Image, Plus, Repeat, Share, ShieldCheck } from 'phosphor-react'; -import { PropsWithChildren, useMemo } from 'react'; -import { useBridgeQuery, useLibraryContext, useLibraryMutation, useLibraryQuery } from '@sd/client'; -import { ContextMenu as CM, ModifierKeys } from '@sd/ui'; -import { showAlertDialog } from '~/components'; -import { getExplorerStore, useExplorerStore, useOperatingSystem } from '~/hooks'; -import { usePlatform } from '~/util/Platform'; -import { keybindForOs } from '~/util/keybinds'; -import { useExplorerSearchParams } from './util'; - -export const OpenInNativeExplorer = () => { - const os = useOperatingSystem(); - const keybind = keybindForOs(os); - - const osFileBrowserName = useMemo(() => { - if (os === 'macOS') { - return 'Finder'; - } else if (os === 'windows') { - return 'Explorer'; - } else { - return 'file manager'; - } - }, [os]); - - const { openPath } = usePlatform(); - - let { locationId } = useExplorerStore(); - if (locationId == null) locationId = -1; - - const { library } = useLibraryContext(); - const { data: node } = useBridgeQuery(['nodeState']); - const { data: location } = useLibraryQuery(['locations.get', locationId]); - const [{ path: subPath }] = useExplorerSearchParams(); - - // Disable for remote nodes, as opening directories in a remote node is a more complex task - if (!(openPath && location?.path && node?.id && library.config.node_id === node.id)) - return null; - const path = location.path + (subPath ? subPath : ''); - - return ( - <> - openPath(path)} - /> - - - - ); -}; - -export default (props: PropsWithChildren) => { - const os = useOperatingSystem(); - const keybind = keybindForOs(os); - const [{ path: currentPath }] = useExplorerSearchParams(); - const { locationId, cutCopyState } = useExplorerStore(); - - const generateThumbsForLocation = useLibraryMutation('jobs.generateThumbsForLocation'); - const objectValidator = useLibraryMutation('jobs.objectValidator'); - const rescanLocation = useLibraryMutation('locations.fullRescan'); - const copyFiles = useLibraryMutation('files.copyFiles'); - const cutFiles = useLibraryMutation('files.cutFiles'); - - return ( - - - - { - e.preventDefault(); - - navigator.share?.({ - title: 'Spacedrive', - text: 'Check out this cool app', - url: 'https://spacedrive.com' - }); - }} - disabled - /> - - - - {locationId && ( - <> - { - try { - await rescanLocation.mutateAsync(locationId); - } catch (error) { - showAlertDialog({ - title: 'Error', - value: `Failed to re-index location, due to an error: ${error}` - }); - } - }} - label="Re-index" - icon={Repeat} - /> - - - ); -}; diff --git a/interface/app/$libraryId/Explorer/ContextMenu/FilePath/CutCopyItems.tsx b/interface/app/$libraryId/Explorer/ContextMenu/FilePath/CutCopyItems.tsx new file mode 100644 index 000000000..84fed0bd7 --- /dev/null +++ b/interface/app/$libraryId/Explorer/ContextMenu/FilePath/CutCopyItems.tsx @@ -0,0 +1,74 @@ +import { Copy, Scissors } from 'phosphor-react'; +import { FilePath, useLibraryMutation } from '@sd/client'; +import { ContextMenu, ModifierKeys } from '@sd/ui'; +import { showAlertDialog } from '~/components'; +import { useKeybindFactory } from '~/hooks/useKeybindFactory'; +import { getExplorerStore } from '../../store'; +import { useExplorerSearchParams } from '../../util'; + +interface Props { + locationId: number; + filePath: FilePath; +} + +export const CutCopyItems = ({ locationId, filePath }: Props) => { + const keybind = useKeybindFactory(); + const [{ path }] = useExplorerSearchParams(); + + const copyFiles = useLibraryMutation('files.copyFiles'); + + return ( + <> + { + getExplorerStore().cutCopyState = { + sourceParentPath: path ?? '/', + sourceLocationId: locationId, + sourcePathId: filePath.id, + actionType: 'Cut', + active: true + }; + }} + icon={Scissors} + /> + + { + getExplorerStore().cutCopyState = { + sourceParentPath: path ?? '/', + sourceLocationId: locationId, + sourcePathId: filePath.id, + actionType: 'Copy', + active: true + }; + }} + icon={Copy} + /> + + { + try { + await copyFiles.mutateAsync({ + source_location_id: locationId, + sources_file_path_ids: [filePath.id], + target_location_id: locationId, + target_location_relative_directory_path: path ?? '/', + target_file_name_suffix: ' copy' + }); + } catch (error) { + showAlertDialog({ + title: 'Error', + value: `Failed to duplcate file, due to an error: ${error}` + }); + } + }} + /> + + ); +}; diff --git a/interface/app/$libraryId/Explorer/ContextMenu/FilePath/Items.tsx b/interface/app/$libraryId/Explorer/ContextMenu/FilePath/Items.tsx new file mode 100644 index 000000000..36c94c6e3 --- /dev/null +++ b/interface/app/$libraryId/Explorer/ContextMenu/FilePath/Items.tsx @@ -0,0 +1,210 @@ +import { Image, Package, Trash, TrashSimple } from 'phosphor-react'; +import { FilePath, useLibraryContext, useLibraryMutation } from '@sd/client'; +import { ContextMenu, ModifierKeys, dialogManager } from '@sd/ui'; +import { showAlertDialog } from '~/components'; +import { useKeybindFactory } from '~/hooks/useKeybindFactory'; +import { usePlatform } from '~/util/Platform'; +import DeleteDialog from '../../FilePath/DeleteDialog'; +import EraseDialog from '../../FilePath/EraseDialog'; +import OpenWith from './OpenWith'; + +export * from './CutCopyItems'; + +interface FilePathProps { + filePath: FilePath; +} + +export const Delete = ({ filePath }: FilePathProps) => { + const keybind = useKeybindFactory(); + + const locationId = filePath.location_id; + + return ( + <> + {locationId != null && ( + + dialogManager.create((dp) => ( + + )) + } + /> + )} + + ); +}; + +export const Compress = (_: FilePathProps) => { + const keybind = useKeybindFactory(); + + return ( + + ); +}; + +export const Crypto = (_: FilePathProps) => { + return ( + <> + {/* { + if (keyManagerUnlocked && hasMountedKeys) { + dialogManager.create((dp) => ( + + )); + } else if (!keyManagerUnlocked) { + showAlertDialog({ + title: 'Key manager locked', + value: 'The key manager is currently locked. Please unlock it and try again.' + }); + } else if (!hasMountedKeys) { + showAlertDialog({ + title: 'No mounted keys', + value: 'No mounted keys were found. Please mount a key and try again.' + }); + } + }} + /> */} + {/* should only be shown if the file is a valid spacedrive-encrypted file (preferably going from the magic bytes) */} + {/* { + if (keyManagerUnlocked) { + dialogManager.create((dp) => ( + + )); + } else { + showAlertDialog({ + title: 'Key manager locked', + value: 'The key manager is currently locked. Please unlock it and try again.' + }); + } + }} + /> */} + + ); +}; + +export const SecureDelete = ({ filePath }: FilePathProps) => { + const locationId = filePath.location_id; + + return ( + <> + {locationId && ( + + dialogManager.create((dp) => ( + + )) + } + disabled + /> + )} + + ); +}; + +export const ParentFolderActions = ({ + filePath, + locationId +}: FilePathProps & { locationId: number }) => { + const fullRescan = useLibraryMutation('locations.fullRescan'); + const generateThumbnails = useLibraryMutation('jobs.generateThumbsForLocation'); + + return ( + <> + { + try { + await fullRescan.mutateAsync(locationId); + } catch (error) { + showAlertDialog({ + title: 'Error', + value: `Failed to rescan location, due to an error: ${error}` + }); + } + }} + label="Rescan Directory" + icon={Package} + /> + { + try { + await generateThumbnails.mutateAsync({ + id: locationId, + path: filePath.materialized_path ?? '/' + }); + } catch (error) { + showAlertDialog({ + title: 'Error', + value: `Failed to generate thumbnails, due to an error: ${error}` + }); + } + }} + label="Regen Thumbnails" + icon={Image} + /> + + ); +}; + +export const OpenOrDownload = ({ filePath }: { filePath: FilePath }) => { + const keybind = useKeybindFactory(); + const { platform, openFilePaths: openFilePath } = usePlatform(); + const updateAccessTime = useLibraryMutation('files.updateAccessTime'); + + const { library } = useLibraryContext(); + + if (platform === 'web') return ; + else + return ( + <> + {openFilePath && ( + { + if (filePath.object_id) + updateAccessTime + .mutateAsync(filePath.object_id) + .catch(console.error); + + try { + await openFilePath(library.uuid, [filePath.id]); + } catch (error) { + showAlertDialog({ + title: 'Error', + value: `Failed to open file, due to an error: ${error}` + }); + } + }} + /> + )} + + + ); +}; diff --git a/interface/app/$libraryId/Explorer/File/ContextMenu/OpenWith.tsx b/interface/app/$libraryId/Explorer/ContextMenu/FilePath/OpenWith.tsx similarity index 99% rename from interface/app/$libraryId/Explorer/File/ContextMenu/OpenWith.tsx rename to interface/app/$libraryId/Explorer/ContextMenu/FilePath/OpenWith.tsx index d94a6f417..fc8432b9a 100644 --- a/interface/app/$libraryId/Explorer/File/ContextMenu/OpenWith.tsx +++ b/interface/app/$libraryId/Explorer/ContextMenu/FilePath/OpenWith.tsx @@ -10,6 +10,7 @@ export default (props: { filePath: FilePath }) => { if (!getFilePathOpenWithApps || !openFilePathWith) return null; if (props.filePath.is_dir) return null; + return ( diff --git a/interface/app/$libraryId/Explorer/ContextMenu/FilePath/index.tsx b/interface/app/$libraryId/Explorer/ContextMenu/FilePath/index.tsx new file mode 100644 index 000000000..be8d88f93 --- /dev/null +++ b/interface/app/$libraryId/Explorer/ContextMenu/FilePath/index.tsx @@ -0,0 +1,77 @@ +import { Plus } from 'phosphor-react'; +import { ExplorerItem } from '@sd/client'; +import { ContextMenu } from '@sd/ui'; +import { useExplorerContext } from '../../Context'; +import { FilePathItems, ObjectItems, SharedItems } from '../../ContextMenu'; + +interface Props { + data: Extract; +} + +export default ({ data }: Props) => { + const filePath = data.item; + const { object } = filePath; + + const { parent } = useExplorerContext(); + + // const keyManagerUnlocked = useLibraryQuery(['keys.isUnlocked']).data ?? false; + // const mountedKeys = useLibraryQuery(['keys.listMounted']); + // const hasMountedKeys = mountedKeys.data?.length ?? 0 > 0; + + return ( + <> + + + + + + + + + + + + + + + {object && } + + {parent?.type === 'Location' && ( + + )} + + + + + + + + + + {object && } + + + + + + + {object && } + + {parent?.type === 'Location' && ( + + )} + + + + + + + + + ); +}; diff --git a/interface/app/$libraryId/Explorer/ContextMenu/Location/index.tsx b/interface/app/$libraryId/Explorer/ContextMenu/Location/index.tsx new file mode 100644 index 000000000..085c18c4e --- /dev/null +++ b/interface/app/$libraryId/Explorer/ContextMenu/Location/index.tsx @@ -0,0 +1,29 @@ +import { ExplorerItem } from "@sd/client"; +import { ContextMenu } from '@sd/ui'; +import { SharedItems } from ".."; + +interface Props { + data: Extract; +} + +export default ({ data }: Props) => { + const location = data.item; + + return <> + + + + + + + + + + + + + + +} diff --git a/interface/app/$libraryId/Explorer/ContextMenu/Object/Items.tsx b/interface/app/$libraryId/Explorer/ContextMenu/Object/Items.tsx new file mode 100644 index 000000000..4e3c0deab --- /dev/null +++ b/interface/app/$libraryId/Explorer/ContextMenu/Object/Items.tsx @@ -0,0 +1,56 @@ +import { ArrowBendUpRight, TagSimple } from 'phosphor-react'; +import { FilePath, ObjectKind, Object as ObjectType, useLibraryMutation } from '@sd/client'; +import { ContextMenu } from '@sd/ui'; +import { showAlertDialog } from '~/components'; +import AssignTagMenuItems from '../../AssignTagMenuItems'; + +export const RemoveFromRecents = ({ object }: { object: ObjectType }) => { + const removeFromRecents = useLibraryMutation('files.removeAccessTime'); + + return ( + <> + {object.date_accessed !== null && ( + { + try { + await removeFromRecents.mutateAsync([object.id]); + } catch (error) { + showAlertDialog({ + title: 'Error', + value: `Failed to remove file from recents, due to an error: ${error}` + }); + } + }} + /> + )} + + ); +}; + +export const AssignTag = ({ object }: { object: ObjectType }) => ( + + + +); + +const ObjectConversions: Record = { + [ObjectKind.Image]: ['PNG', 'WebP', 'Gif'], + [ObjectKind.Video]: ['MP4', 'MOV', 'AVI'] +}; + +export const ConvertObject = ({ object, filePath }: { object: ObjectType; filePath: FilePath }) => { + const { kind } = object; + + return ( + <> + {kind !== null && [ObjectKind.Image, ObjectKind.Video].includes(kind as ObjectKind) && ( + + {ObjectConversions[kind]?.map((ext) => ( + + ))} + + )} + + ); +}; diff --git a/interface/app/$libraryId/Explorer/ContextMenu/Object/index.tsx b/interface/app/$libraryId/Explorer/ContextMenu/Object/index.tsx new file mode 100644 index 000000000..ab96e1eb3 --- /dev/null +++ b/interface/app/$libraryId/Explorer/ContextMenu/Object/index.tsx @@ -0,0 +1,58 @@ +import { Plus } from 'phosphor-react'; +import { ExplorerItem } from '@sd/client'; +import { ContextMenu } from '@sd/ui'; +import { FilePathItems, ObjectItems, SharedItems } from '..'; + +interface Props { + data: Extract; +} + +export default ({ data }: Props) => { + const object = data.item; + const filePath = data.item.file_paths[0]; + + return ( + <> + {filePath && } + + + + + + + + + + {filePath && ( + + )} + + + + + + + + {(object || filePath) && } + + {object && } + + {filePath && ( + + + + + + )} + + {filePath && ( + <> + + + + )} + + ); +}; diff --git a/interface/app/$libraryId/Explorer/ContextMenu/SharedItems.tsx b/interface/app/$libraryId/Explorer/ContextMenu/SharedItems.tsx new file mode 100644 index 000000000..de24257aa --- /dev/null +++ b/interface/app/$libraryId/Explorer/ContextMenu/SharedItems.tsx @@ -0,0 +1,130 @@ +import { FileX, Share as ShareIcon } from 'phosphor-react'; +import { useMemo } from 'react'; +import { ExplorerItem, FilePath, useLibraryContext } from '@sd/client'; +import { ContextMenu, ModifierKeys } from '@sd/ui'; +import { useOperatingSystem } from '~/hooks'; +import { useKeybindFactory } from '~/hooks/useKeybindFactory'; +import { usePlatform } from '~/util/Platform'; +import { useExplorerViewContext } from '../ViewContext'; +import { getExplorerStore, useExplorerStore } from '../store'; + +export const OpenQuickView = ({ item }: { item: ExplorerItem }) => { + const keybind = useKeybindFactory(); + + return ( + (getExplorerStore().quickViewObject = item)} + /> + ); +}; + +export const Details = () => { + const { showInspector } = useExplorerStore(); + const keybind = useKeybindFactory(); + + return ( + <> + {!showInspector && ( + (getExplorerStore().showInspector = true)} + /> + )} + + ); +}; + +export const Rename = () => { + const explorerStore = useExplorerStore(); + const keybind = useKeybindFactory(); + const explorerView = useExplorerViewContext(); + + return ( + <> + {explorerStore.layoutMode !== 'media' && ( + explorerView.setIsRenaming(true)} + /> + )} + + ); +}; + +export const RevealInNativeExplorer = (props: { locationId: number } | { filePath: FilePath }) => { + const os = useOperatingSystem(); + const keybind = useKeybindFactory(); + const { revealItems } = usePlatform(); + const { library } = useLibraryContext(); + + const osFileBrowserName = useMemo(() => { + const lookup: Record = { + macOS: 'Finder', + windows: 'Explorer' + }; + + return lookup[os] ?? 'file manager'; + }, [os]); + + return ( + <> + {revealItems && (console.log(props), revealItems(library.uuid, ['filePath' in props ? { + FilePath: { + id: props.filePath.id, + } + } : { + Location: { + id: props.locationId + } + }]))} + />} + + ); +}; + +export const Deselect = () => { + const { cutCopyState } = useExplorerStore(); + + return ( +