[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:
ameer2468 2024-04-10 03:03:18 +03:00 committed by GitHub
parent f48a91b124
commit 1b83a6fd8a
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
16 changed files with 139 additions and 43 deletions

View file

@ -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);

View file

@ -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);

View file

@ -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>
);
};

View file

@ -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'
};

View file

@ -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')}

View file

@ -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": "Адкрыць у новай укладцы"
}

View file

@ -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"
}

View file

@ -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"
}

View file

@ -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"
}

View file

@ -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"
}

View file

@ -464,5 +464,6 @@
"create_folder_success": "新しいフォルダーを作成しました: {{name}}",
"empty_file": "空のファイル",
"new": "新しい",
"text_file": "テキストファイル"
"text_file": "テキストファイル",
"open_in_new_tab": "新しいタブで開く"
}

View file

@ -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"
}

View file

@ -464,5 +464,6 @@
"create_folder_success": "Создана новая папка: {{name}}.",
"empty_file": "Пустой файл",
"new": "Новый",
"text_file": "Текстовый файл"
"text_file": "Текстовый файл",
"open_in_new_tab": "Открыть в новой вкладке"
}

View file

@ -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ç"
}

View file

@ -464,5 +464,6 @@
"size_kb": "千字节",
"size_mb": "MB",
"size_tb": "结核病",
"text_file": "文本文件"
"text_file": "文本文件",
"open_in_new_tab": "在新标签页中打开"
}

View file

@ -464,5 +464,6 @@
"size_kb": "千位元組",
"size_mb": "MB",
"size_tb": "結核病",
"text_file": "文字檔案"
"text_file": "文字檔案",
"open_in_new_tab": "在新分頁中開啟"
}