mirror of
https://github.com/spacedriveapp/spacedrive
synced 2024-07-04 12:13:27 +00:00
[ENG-1710] Context menu path bar (#2305)
* context menu on path bar * Open in new tab * locales * Update ExplorerPath.tsx
This commit is contained in:
parent
f48a91b124
commit
1b83a6fd8a
|
@ -78,18 +78,28 @@ const cache = createCache();
|
|||
|
||||
const routes = createRoutes(platform, cache);
|
||||
|
||||
type redirect = { pathname: string; search: string | undefined };
|
||||
|
||||
function AppInner() {
|
||||
const [tabs, setTabs] = useState(() => [createTab()]);
|
||||
const [selectedTabIndex, setSelectedTabIndex] = useState(0);
|
||||
|
||||
const selectedTab = tabs[selectedTabIndex]!;
|
||||
|
||||
function createTab() {
|
||||
function createTab(redirect?: redirect) {
|
||||
const history = createMemoryHistory();
|
||||
const router = createMemoryRouterWithHistory({ routes, history });
|
||||
|
||||
const id = Math.random().toString();
|
||||
|
||||
// for "Open in new tab"
|
||||
if (redirect) {
|
||||
router.navigate({
|
||||
pathname: redirect.pathname,
|
||||
search: redirect.search
|
||||
});
|
||||
}
|
||||
|
||||
const dispose = router.subscribe((event) => {
|
||||
// we don't care about non-idle events as those are artifacts of form mutations + suspense
|
||||
if (event.navigation.state !== 'idle') return;
|
||||
|
@ -165,13 +175,13 @@ function AppInner() {
|
|||
tabIndex: selectedTabIndex,
|
||||
setTabIndex: setSelectedTabIndex,
|
||||
tabs: tabs.map(({ router, title }) => ({ router, title })),
|
||||
createTab() {
|
||||
createTab(redirect?: redirect) {
|
||||
createTabPromise.current = createTabPromise.current.then(
|
||||
() =>
|
||||
new Promise((res) => {
|
||||
startTransition(() => {
|
||||
setTabs((tabs) => {
|
||||
const newTab = createTab();
|
||||
const newTab = createTab(redirect);
|
||||
const newTabs = [...tabs, newTab];
|
||||
|
||||
setSelectedTabIndex(newTabs.length - 1);
|
||||
|
|
|
@ -6,7 +6,7 @@ export const TabsContext = createContext<{
|
|||
tabIndex: number;
|
||||
setTabIndex: (i: number) => void;
|
||||
tabs: { router: Router; title: string }[];
|
||||
createTab(): void;
|
||||
createTab(redirect?: { pathname: string; search: string | undefined }): void;
|
||||
removeTab(index: number): void;
|
||||
} | null>(null);
|
||||
|
||||
|
|
|
@ -1,14 +1,23 @@
|
|||
import { CaretRight } from '@phosphor-icons/react';
|
||||
import { AppWindow, ArrowSquareOut, CaretRight, ClipboardText } from '@phosphor-icons/react';
|
||||
import {
|
||||
getExplorerItemData,
|
||||
getIndexedItemFilePath,
|
||||
useLibraryContext,
|
||||
useLibraryQuery
|
||||
} from '@sd/client';
|
||||
import { ContextMenu } from '@sd/ui';
|
||||
import clsx from 'clsx';
|
||||
import { memo, useMemo } from 'react';
|
||||
import { memo, useMemo, useState } from 'react';
|
||||
import { useNavigate } from 'react-router';
|
||||
import { createSearchParams } from 'react-router-dom';
|
||||
import { getExplorerItemData, getIndexedItemFilePath, useLibraryQuery } from '@sd/client';
|
||||
import { useTabsContext } from '~/TabsContext';
|
||||
import { Icon } from '~/components';
|
||||
import { useIsDark, useOperatingSystem } from '~/hooks';
|
||||
import { useIsDark, useLocale, useOperatingSystem } from '~/hooks';
|
||||
import { usePlatform } from '~/util/Platform';
|
||||
|
||||
import { useExplorerContext } from './Context';
|
||||
import { FileThumb } from './FilePath/Thumb';
|
||||
import { lookup } from './RevealInNativeExplorer';
|
||||
import { useExplorerDroppable } from './useExplorerDroppable';
|
||||
import { useExplorerSearchParams } from './util';
|
||||
|
||||
|
@ -17,7 +26,6 @@ export const PATH_BAR_HEIGHT = 32;
|
|||
export const ExplorerPath = memo(() => {
|
||||
const os = useOperatingSystem(true);
|
||||
const navigate = useNavigate();
|
||||
|
||||
const [{ path: searchPath }] = useExplorerSearchParams();
|
||||
const { parent: explorerParent, selectedItems } = useExplorerContext();
|
||||
|
||||
|
@ -117,6 +125,7 @@ export const ExplorerPath = memo(() => {
|
|||
<Path
|
||||
key={path.pathname}
|
||||
path={path}
|
||||
locationPath={location?.path ?? ''}
|
||||
onClick={() => handleOnClick(path)}
|
||||
disabled={path.pathname === (searchPath ?? (location && '/'))}
|
||||
/>
|
||||
|
@ -138,10 +147,32 @@ interface PathProps {
|
|||
path: { name: string; pathname: string; locationId?: number };
|
||||
onClick: () => void;
|
||||
disabled: boolean;
|
||||
locationPath: string;
|
||||
}
|
||||
|
||||
const Path = ({ path, onClick, disabled }: PathProps) => {
|
||||
const Path = ({ path, onClick, disabled, locationPath }: PathProps) => {
|
||||
const isDark = useIsDark();
|
||||
const { revealItems } = usePlatform();
|
||||
const { library } = useLibraryContext();
|
||||
const { t } = useLocale();
|
||||
const os = useOperatingSystem();
|
||||
const tabs = useTabsContext();
|
||||
const [contextMenuOpen, setContextMenuOpen] = useState(false);
|
||||
|
||||
const osFileBrowserName = lookup[os] ?? 'file manager';
|
||||
const pathValue = path.pathname.endsWith('/')
|
||||
? locationPath + path.pathname.substring(0, path.pathname.length - 1)
|
||||
: path.pathname;
|
||||
const osPath = os === 'windows' ? pathValue?.replace(/\//g, '\\') : pathValue;
|
||||
|
||||
// "Open in new tab" redirect
|
||||
const basePath = path.locationId ? `location/${path.locationId}` : `ephemeral/0-0`;
|
||||
const searchParam =
|
||||
path.pathname === '/' ? undefined : createSearchParams({ path: path.pathname });
|
||||
const redirect = {
|
||||
pathname: `${library.uuid}/${basePath}`,
|
||||
search: searchParam ? `${searchParam}` : undefined
|
||||
};
|
||||
|
||||
const { setDroppableRef, className, isDroppable } = useExplorerDroppable({
|
||||
data: {
|
||||
|
@ -155,21 +186,62 @@ const Path = ({ path, onClick, disabled }: PathProps) => {
|
|||
});
|
||||
|
||||
return (
|
||||
<button
|
||||
ref={setDroppableRef}
|
||||
className={clsx(
|
||||
'group flex items-center gap-1 rounded px-1 py-0.5',
|
||||
isDroppable && [isDark ? 'bg-app-button/70' : 'bg-app-darkerBox'],
|
||||
!disabled && [isDark ? 'hover:bg-app-button/70' : 'hover:bg-app-darkerBox'],
|
||||
className
|
||||
)}
|
||||
disabled={disabled}
|
||||
onClick={onClick}
|
||||
tabIndex={-1}
|
||||
<ContextMenu.Root
|
||||
onOpenChange={setContextMenuOpen}
|
||||
trigger={
|
||||
<button
|
||||
ref={setDroppableRef}
|
||||
className={clsx(
|
||||
'group flex items-center gap-1 rounded px-1 py-0.5',
|
||||
(isDroppable || contextMenuOpen) && [
|
||||
isDark ? 'bg-app-button/70' : 'bg-app-darkerBox'
|
||||
],
|
||||
!disabled && [isDark ? 'hover:bg-app-button/70' : 'hover:bg-app-darkerBox'],
|
||||
className
|
||||
)}
|
||||
disabled={disabled}
|
||||
onClick={onClick}
|
||||
tabIndex={-1}
|
||||
>
|
||||
<Icon name="Folder" size={16} alt="Folder" />
|
||||
<span className="max-w-xs truncate text-ink-dull">{path.name}</span>
|
||||
<CaretRight
|
||||
weight="bold"
|
||||
className="text-ink-dull group-last:hidden"
|
||||
size={10}
|
||||
/>
|
||||
</button>
|
||||
}
|
||||
>
|
||||
<Icon name="Folder" size={16} alt="Folder" />
|
||||
<span className="max-w-xs truncate text-ink-dull">{path.name}</span>
|
||||
<CaretRight weight="bold" className="text-ink-dull group-last:hidden" size={10} />
|
||||
</button>
|
||||
<ContextMenu.Item
|
||||
onClick={() => {
|
||||
if (!tabs) return null;
|
||||
tabs.createTab(redirect);
|
||||
}}
|
||||
label={t('open_in_new_tab')}
|
||||
icon={ArrowSquareOut}
|
||||
/>
|
||||
<ContextMenu.Item
|
||||
onClick={() => {
|
||||
if (!revealItems) return null;
|
||||
revealItems(library.uuid, [
|
||||
path.locationId
|
||||
? {
|
||||
Location: { id: path.locationId }
|
||||
}
|
||||
: {
|
||||
Ephemeral: { path: path.pathname }
|
||||
}
|
||||
]);
|
||||
}}
|
||||
label={t('revel_in_browser', { browser: osFileBrowserName })}
|
||||
icon={AppWindow}
|
||||
/>
|
||||
<ContextMenu.Item
|
||||
onClick={() => navigator.clipboard.writeText(osPath)}
|
||||
icon={ClipboardText}
|
||||
label={t("copy_as_path")}
|
||||
/>
|
||||
</ContextMenu.Root>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -6,7 +6,7 @@ import { useKeybindFactory } from '~/hooks/useKeybindFactory';
|
|||
import { NonEmptyArray } from '~/util';
|
||||
import { Platform, usePlatform } from '~/util/Platform';
|
||||
|
||||
const lookup: Record<string, string> = {
|
||||
export const lookup: Record<string, string> = {
|
||||
macOS: 'Finder',
|
||||
windows: 'Explorer'
|
||||
};
|
||||
|
|
|
@ -24,7 +24,7 @@ import {
|
|||
tw,
|
||||
usePopover
|
||||
} from '@sd/ui';
|
||||
import { useLocale } from '~/hooks';
|
||||
import { useLocale, useOperatingSystem } from '~/hooks';
|
||||
|
||||
import TopBarButton from '../TopBar/TopBarButton';
|
||||
|
||||
|
@ -34,6 +34,7 @@ export default function LocationOptions({ location, path }: { location: Location
|
|||
const navigate = useNavigate();
|
||||
|
||||
const { t } = useLocale();
|
||||
const os = useOperatingSystem();
|
||||
|
||||
const [copied, setCopied] = useState(false);
|
||||
|
||||
|
@ -48,6 +49,8 @@ export default function LocationOptions({ location, path }: { location: Location
|
|||
? currentPath.substring(0, currentPath.length - 1)
|
||||
: currentPath;
|
||||
|
||||
const osPath = os === 'windows' ? currentPath?.replace(/\//g, '\\') : currentPath;
|
||||
|
||||
return (
|
||||
<div className="opacity-30 group-hover:opacity-70">
|
||||
<IconContext.Provider value={{ size: 20, className: 'r-1 h-4 w-4 opacity-60' }}>
|
||||
|
@ -64,7 +67,7 @@ export default function LocationOptions({ location, path }: { location: Location
|
|||
<Input
|
||||
readOnly
|
||||
className="mb-2"
|
||||
value={currentPath ?? ''}
|
||||
value={osPath ?? ''}
|
||||
right={
|
||||
<Tooltip
|
||||
label={copied ? t('copied') : t('copy_path_to_clipboard')}
|
||||
|
|
|
@ -464,5 +464,6 @@
|
|||
"create_folder_success": "Created new folder: {{name}}",
|
||||
"empty_file": "Empty file",
|
||||
"new": "New",
|
||||
"text_file": "Text File"
|
||||
"text_file": "Text File",
|
||||
"open_in_new_tab": "Адкрыць у новай укладцы"
|
||||
}
|
||||
|
|
|
@ -464,5 +464,6 @@
|
|||
"size_kb": "kB",
|
||||
"size_mb": "MB",
|
||||
"size_tb": "TB",
|
||||
"text_file": "Textdatei"
|
||||
"text_file": "Textdatei",
|
||||
"open_in_new_tab": "In neuem Tab öffnen"
|
||||
}
|
||||
|
|
|
@ -298,10 +298,10 @@
|
|||
"new_folder": "Folder",
|
||||
"text_file": "Text File",
|
||||
"empty_file": "Empty file",
|
||||
"create_folder_error":"Error creating folder",
|
||||
"create_file_error":"Error creating file",
|
||||
"create_folder_success":"Created new folder: {{name}}",
|
||||
"create_file_success":"Created new file: {{name}}",
|
||||
"create_folder_error": "Error creating folder",
|
||||
"create_file_error": "Error creating file",
|
||||
"create_folder_success": "Created new folder: {{name}}",
|
||||
"create_file_success": "Created new file: {{name}}",
|
||||
"new_library": "New library",
|
||||
"new_location": "New location",
|
||||
"new_location_web_description": "As you are using the browser version of Spacedrive you will (for now) need to specify an absolute URL of a directory local to the remote node.",
|
||||
|
@ -464,5 +464,6 @@
|
|||
"your_local_network": "Your Local Network",
|
||||
"your_privacy": "Your Privacy",
|
||||
"pin": "Pin",
|
||||
"rescan": "Rescan"
|
||||
"rescan": "Rescan",
|
||||
"open_in_new_tab": "Open in new tab"
|
||||
}
|
||||
|
|
|
@ -464,5 +464,6 @@
|
|||
"create_folder_success": "Nouveau dossier créé : {{name}}",
|
||||
"empty_file": "Fichier vide",
|
||||
"new": "Nouveau",
|
||||
"text_file": "Fichier texte"
|
||||
"text_file": "Fichier texte",
|
||||
"open_in_new_tab": "Ouvrir dans un nouvel onglet"
|
||||
}
|
||||
|
|
|
@ -464,5 +464,6 @@
|
|||
"size_kb": "kB",
|
||||
"size_mb": "MB",
|
||||
"size_tb": "TBC",
|
||||
"text_file": "File di testo"
|
||||
"text_file": "File di testo",
|
||||
"open_in_new_tab": "Apri in una nuova scheda"
|
||||
}
|
||||
|
|
|
@ -464,5 +464,6 @@
|
|||
"create_folder_success": "新しいフォルダーを作成しました: {{name}}",
|
||||
"empty_file": "空のファイル",
|
||||
"new": "新しい",
|
||||
"text_file": "テキストファイル"
|
||||
"text_file": "テキストファイル",
|
||||
"open_in_new_tab": "新しいタブで開く"
|
||||
}
|
||||
|
|
|
@ -464,5 +464,6 @@
|
|||
"size_kb": "KB",
|
||||
"size_mb": "MB",
|
||||
"size_tb": "TB",
|
||||
"text_file": "Tekstbestand"
|
||||
"text_file": "Tekstbestand",
|
||||
"open_in_new_tab": "Openen in nieuw tabblad"
|
||||
}
|
||||
|
|
|
@ -464,5 +464,6 @@
|
|||
"create_folder_success": "Создана новая папка: {{name}}.",
|
||||
"empty_file": "Пустой файл",
|
||||
"new": "Новый",
|
||||
"text_file": "Текстовый файл"
|
||||
"text_file": "Текстовый файл",
|
||||
"open_in_new_tab": "Открыть в новой вкладке"
|
||||
}
|
||||
|
|
|
@ -464,5 +464,6 @@
|
|||
"size_kb": "kB",
|
||||
"size_mb": "MB",
|
||||
"size_tb": "TB",
|
||||
"text_file": "Metin dosyası"
|
||||
"text_file": "Metin dosyası",
|
||||
"open_in_new_tab": "Yeni sekmede aç"
|
||||
}
|
||||
|
|
|
@ -464,5 +464,6 @@
|
|||
"size_kb": "千字节",
|
||||
"size_mb": "MB",
|
||||
"size_tb": "结核病",
|
||||
"text_file": "文本文件"
|
||||
"text_file": "文本文件",
|
||||
"open_in_new_tab": "在新标签页中打开"
|
||||
}
|
||||
|
|
|
@ -464,5 +464,6 @@
|
|||
"size_kb": "千位元組",
|
||||
"size_mb": "MB",
|
||||
"size_tb": "結核病",
|
||||
"text_file": "文字檔案"
|
||||
"text_file": "文字檔案",
|
||||
"open_in_new_tab": "在新分頁中開啟"
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue