Files
spacedrive/interface/app/$libraryId/Explorer/ContextMenu/FilePath/Items.tsx
nikec 717e01ef1f [ENG-300] Explorer multi-select (#1197)
* grid

* Improved multi-select, grid list & view offset. Added gap option & app frame.

* List view multi-select

* Include multi-select in overview, fix page ref type

* Add gap to options panel

* Fix drag

* Update categories z-index

* going pretty well

* fix a couple bugs

* fix another bug :)

* minor improvements

* Separate grid activeItem

* extra comments

* um akshully don't ref during render

* show thumbnails yay

* cleanup

* Clean up

* Fix ranges

* here it is

* fix cols drag

* don't enforce selecto context

* explorer view selectable

* Update index.tsx

* Context menu support for multi-select (#1187)

* here it is

* stopPropagation

* cut copy multiple

---------

Co-authored-by: nikec <nikec.job@gmail.com>

* explorer view selectable

* Update index.tsx

* items Map

* fix renamable

* Update inspector

* Hide tag assign if empty

* fix merge

* cleanup

* fix un-rendered drag select

* fix double click quick preview

* update thumbnail

* mostly handle multiple select in keybindings

* fix ts

* remove another todo

* move useItemAs hooks to @sd/client

* fix thumb controls

* multi-select double click

* cleaner?

* smaller gap

---------

Co-authored-by: Jamie Pine <ijamespine@me.com>
Co-authored-by: Brendan Allan <brendonovich@outlook.com>
Co-authored-by: Jamie Pine <32987599+jamiepine@users.noreply.github.com>
Co-authored-by: Utku <74243531+utkubakir@users.noreply.github.com>
Co-authored-by: Ericson "Fogo" Soares <ericson.ds999@gmail.com>
2023-08-15 08:23:41 +00:00

279 lines
7.3 KiB
TypeScript

import { Image, Package, Trash, TrashSimple } from 'phosphor-react';
import { libraryClient, useLibraryContext, useLibraryMutation } from '@sd/client';
import { ContextMenu, ModifierKeys, dialogManager } from '@sd/ui';
import { showAlertDialog } from '~/components';
import { useKeybindFactory } from '~/hooks/useKeybindFactory';
import { isNonEmpty } from '~/util';
import { usePlatform } from '~/util/Platform';
import { useExplorerContext } from '../../Context';
import { CopyAsPathBase } from '../../CopyAsPath';
import DeleteDialog from '../../FilePath/DeleteDialog';
import EraseDialog from '../../FilePath/EraseDialog';
import { Conditional, ConditionalItem } from '../ConditionalItem';
import { useContextMenuContext } from '../context';
import OpenWith from './OpenWith';
export * from './CutCopyItems';
export const Delete = new ConditionalItem({
useCondition: () => {
const { selectedFilePaths } = useContextMenuContext();
if (!isNonEmpty(selectedFilePaths)) return null;
const locationId = selectedFilePaths[0].location_id;
if (locationId === null) return null;
return { selectedFilePaths, locationId };
},
Component: ({ selectedFilePaths, locationId }) => {
const keybind = useKeybindFactory();
return (
<ContextMenu.Item
icon={Trash}
label="Delete"
variant="danger"
keybind={keybind([ModifierKeys.Control], ['Delete'])}
onClick={() =>
dialogManager.create((dp) => (
<DeleteDialog
{...dp}
locationId={locationId}
pathIds={selectedFilePaths.map((p) => p.id)}
/>
))
}
/>
);
}
});
export const CopyAsPath = new ConditionalItem({
useCondition: () => {
const { selectedFilePaths } = useContextMenuContext();
if (!isNonEmpty(selectedFilePaths) || selectedFilePaths.length > 1) return null;
return { selectedFilePaths };
},
Component: ({ selectedFilePaths }) => (
<CopyAsPathBase
getPath={() => libraryClient.query(['files.getPath', selectedFilePaths[0].id])}
/>
)
});
export const Compress = new ConditionalItem({
useCondition: () => {
const { selectedFilePaths } = useContextMenuContext();
if (!isNonEmpty(selectedFilePaths)) return null;
return { selectedFilePaths };
},
Component: ({ selectedFilePaths: _ }) => {
const keybind = useKeybindFactory();
return (
<ContextMenu.Item
label="Compress"
icon={Package}
keybind={keybind([ModifierKeys.Control], ['B'])}
disabled
/>
);
}
});
export const Crypto = new ConditionalItem({
useCondition: () => {
const { selectedFilePaths } = useContextMenuContext();
if (!isNonEmpty(selectedFilePaths)) return null;
return { selectedFilePaths };
},
Component: ({ selectedFilePaths: _ }) => {
return (
<>
{/* <ContextMenu.Item
label="Encrypt"
icon={LockSimple}
keybind="⌘E"
onClick={() => {
if (keyManagerUnlocked && hasMountedKeys) {
dialogManager.create((dp) => (
<EncryptDialog
{...dp}
location_id={store.locationId!}
path_id={data.item.id}
/>
));
} 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) */}
{/* <ContextMenu.Item
label="Decrypt"
icon={LockSimpleOpen}
keybind="⌘D"
onClick={() => {
if (keyManagerUnlocked) {
dialogManager.create((dp) => (
<DecryptDialog
{...dp}
location_id={store.locationId!}
path_id={data.item.id}
/>
));
} else {
showAlertDialog({
title: 'Key manager locked',
value: 'The key manager is currently locked. Please unlock it and try again.'
});
}
}}
/> */}
</>
);
}
});
export const SecureDelete = new ConditionalItem({
useCondition: () => {
const { selectedFilePaths } = useContextMenuContext();
if (!isNonEmpty(selectedFilePaths)) return null;
const locationId = selectedFilePaths[0].location_id;
if (locationId === null) return null;
return { locationId, selectedFilePaths };
},
Component: ({ locationId, selectedFilePaths }) => (
<ContextMenu.Item
variant="danger"
label="Secure delete"
icon={TrashSimple}
onClick={() =>
dialogManager.create((dp) => (
<EraseDialog {...dp} locationId={locationId} filePaths={selectedFilePaths} />
))
}
disabled
/>
)
});
export const ParentFolderActions = new ConditionalItem({
useCondition: () => {
const { parent } = useExplorerContext();
if (parent?.type !== 'Location') return null;
return { parent };
},
Component: ({ parent }) => {
const { selectedFilePaths } = useContextMenuContext();
const fullRescan = useLibraryMutation('locations.fullRescan');
const generateThumbnails = useLibraryMutation('jobs.generateThumbsForLocation');
return (
<>
<ContextMenu.Item
onClick={async () => {
try {
await fullRescan.mutateAsync({
location_id: parent.location.id,
reidentify_objects: false
});
} catch (error) {
showAlertDialog({
title: 'Error',
value: `Failed to rescan location, due to an error: ${error}`
});
}
}}
label="Rescan Directory"
icon={Package}
/>
<ContextMenu.Item
onClick={async () => {
try {
await generateThumbnails.mutateAsync({
id: parent.location.id,
path: selectedFilePaths[0]?.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 = new ConditionalItem({
useCondition: () => {
const { selectedFilePaths } = useContextMenuContext();
const { openFilePaths } = usePlatform();
if (!openFilePaths || !isNonEmpty(selectedFilePaths)) return null;
return { openFilePaths, selectedFilePaths };
},
Component: ({ openFilePaths, selectedFilePaths }) => {
const keybind = useKeybindFactory();
const { platform } = usePlatform();
const updateAccessTime = useLibraryMutation('files.updateAccessTime');
const { library } = useLibraryContext();
if (platform === 'web') return <ContextMenu.Item label="Download" />;
else
return (
<>
<ContextMenu.Item
label="Open"
keybind={keybind([ModifierKeys.Control], ['O'])}
onClick={async () => {
if (selectedFilePaths.length < 1) return;
updateAccessTime
.mutateAsync(
selectedFilePaths.map((p) => p.object_id!).filter(Boolean)
)
.catch(console.error);
try {
await openFilePaths(
library.uuid,
selectedFilePaths.map((p) => p.id)
);
} catch (error) {
showAlertDialog({
title: 'Error',
value: `Failed to open file, due to an error: ${error}`
});
}
}}
/>
<Conditional items={[OpenWith]} />
</>
);
}
});