diff --git a/apps/desktop/src/App.tsx b/apps/desktop/src/App.tsx index 68ba78be9..eb2f39b73 100644 --- a/apps/desktop/src/App.tsx +++ b/apps/desktop/src/App.tsx @@ -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); diff --git a/interface/TabsContext.tsx b/interface/TabsContext.tsx index 528af4c4b..fa94629f9 100644 --- a/interface/TabsContext.tsx +++ b/interface/TabsContext.tsx @@ -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); diff --git a/interface/app/$libraryId/Explorer/ExplorerPath.tsx b/interface/app/$libraryId/Explorer/ExplorerPath.tsx index dbecaac3b..0c4ce5094 100644 --- a/interface/app/$libraryId/Explorer/ExplorerPath.tsx +++ b/interface/app/$libraryId/Explorer/ExplorerPath.tsx @@ -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(() => { 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 ( - + } > - - {path.name} - - + { + if (!tabs) return null; + tabs.createTab(redirect); + }} + label={t('open_in_new_tab')} + icon={ArrowSquareOut} + /> + { + 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} + /> + navigator.clipboard.writeText(osPath)} + icon={ClipboardText} + label={t("copy_as_path")} + /> + ); }; diff --git a/interface/app/$libraryId/Explorer/RevealInNativeExplorer.tsx b/interface/app/$libraryId/Explorer/RevealInNativeExplorer.tsx index 7726d4617..7a16eb117 100644 --- a/interface/app/$libraryId/Explorer/RevealInNativeExplorer.tsx +++ b/interface/app/$libraryId/Explorer/RevealInNativeExplorer.tsx @@ -6,7 +6,7 @@ import { useKeybindFactory } from '~/hooks/useKeybindFactory'; import { NonEmptyArray } from '~/util'; import { Platform, usePlatform } from '~/util/Platform'; -const lookup: Record = { +export const lookup: Record = { macOS: 'Finder', windows: 'Explorer' }; diff --git a/interface/app/$libraryId/location/LocationOptions.tsx b/interface/app/$libraryId/location/LocationOptions.tsx index 2c7ed2aa4..0cecce2e6 100644 --- a/interface/app/$libraryId/location/LocationOptions.tsx +++ b/interface/app/$libraryId/location/LocationOptions.tsx @@ -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 (
@@ -64,7 +67,7 @@ export default function LocationOptions({ location, path }: { location: Location