mirror of
https://github.com/NanmiCoder/cc-haha
synced 2026-07-17 13:13:35 +08:00
feat(desktop): add reusable open-with menu + items builder (IDE/Finder/browser)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
c99472222a
commit
55b9ccc276
@ -1,17 +0,0 @@
|
||||
import '@testing-library/jest-dom'
|
||||
import { fireEvent, render, screen } from '@testing-library/react'
|
||||
import { describe, expect, it, vi } from 'vitest'
|
||||
import { OpenWithMenu } from './OpenWithMenu'
|
||||
|
||||
describe('OpenWithMenu', () => {
|
||||
it('offers in-app and system browser choices', () => {
|
||||
const onInApp = vi.fn(); const onSystem = vi.fn()
|
||||
render(<OpenWithMenu url="https://example.com" onOpenInApp={onInApp} onOpenSystem={onSystem} />)
|
||||
fireEvent.click(screen.getByLabelText('打开方式'))
|
||||
fireEvent.click(screen.getByText('应用内浏览器'))
|
||||
expect(onInApp).toHaveBeenCalledWith('https://example.com')
|
||||
fireEvent.click(screen.getByLabelText('打开方式'))
|
||||
fireEvent.click(screen.getByText('系统浏览器'))
|
||||
expect(onSystem).toHaveBeenCalledWith('https://example.com')
|
||||
})
|
||||
})
|
||||
@ -1,21 +0,0 @@
|
||||
import { useState } from 'react'
|
||||
import { ChevronDown } from 'lucide-react'
|
||||
|
||||
type Props = { url: string; onOpenInApp: (url: string) => void; onOpenSystem: (url: string) => void }
|
||||
|
||||
export function OpenWithMenu({ url, onOpenInApp, onOpenSystem }: Props) {
|
||||
const [open, setOpen] = useState(false)
|
||||
return (
|
||||
<span className="relative inline-block">
|
||||
<button aria-label="打开方式" onClick={() => setOpen((v) => !v)} className="inline-flex items-center gap-0.5 text-xs">
|
||||
<ChevronDown size={12} />
|
||||
</button>
|
||||
{open && (
|
||||
<div className="absolute z-10 mt-1 rounded-md border border-[var(--color-border)] bg-[var(--color-surface)] py-1 text-xs shadow">
|
||||
<button className="block w-full px-3 py-1 text-left hover:bg-[var(--color-surface-hover)]" onClick={() => { setOpen(false); onOpenInApp(url) }}>应用内浏览器</button>
|
||||
<button className="block w-full px-3 py-1 text-left hover:bg-[var(--color-surface-hover)]" onClick={() => { setOpen(false); onOpenSystem(url) }}>系统浏览器</button>
|
||||
</div>
|
||||
)}
|
||||
</span>
|
||||
)
|
||||
}
|
||||
71
desktop/src/components/common/OpenWithMenu.test.tsx
Normal file
71
desktop/src/components/common/OpenWithMenu.test.tsx
Normal file
@ -0,0 +1,71 @@
|
||||
import '@testing-library/jest-dom'
|
||||
import { fireEvent, render, screen } from '@testing-library/react'
|
||||
import { describe, expect, it, vi } from 'vitest'
|
||||
import { OpenWithMenu } from './OpenWithMenu'
|
||||
import type { OpenWithItem } from '../../lib/openWithItems'
|
||||
|
||||
const anchor = { top: 100, bottom: 110, left: 20, right: 120 }
|
||||
|
||||
function makeItems(onSelect1 = vi.fn(), onSelect2 = vi.fn(), onSelect3 = vi.fn()): OpenWithItem[] {
|
||||
return [
|
||||
{ id: 'in-app', label: 'In-app browser', icon: 'in-app-browser', onSelect: onSelect1 },
|
||||
{ id: 'system', label: 'System browser', icon: 'system', onSelect: onSelect2 },
|
||||
{ id: 'preview', label: 'Workspace preview', icon: 'preview', onSelect: onSelect3 },
|
||||
]
|
||||
}
|
||||
|
||||
describe('OpenWithMenu', () => {
|
||||
it('renders all item labels', () => {
|
||||
const onClose = vi.fn()
|
||||
render(<OpenWithMenu items={makeItems()} anchor={anchor} onClose={onClose} />)
|
||||
expect(screen.getByText('In-app browser')).toBeInTheDocument()
|
||||
expect(screen.getByText('System browser')).toBeInTheDocument()
|
||||
expect(screen.getByText('Workspace preview')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('renders a menu role', () => {
|
||||
const onClose = vi.fn()
|
||||
render(<OpenWithMenu items={makeItems()} anchor={anchor} onClose={onClose} />)
|
||||
expect(screen.getByRole('menu')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('renders menuitems for each item', () => {
|
||||
const onClose = vi.fn()
|
||||
render(<OpenWithMenu items={makeItems()} anchor={anchor} onClose={onClose} />)
|
||||
const menuItems = screen.getAllByRole('menuitem')
|
||||
expect(menuItems).toHaveLength(3)
|
||||
})
|
||||
|
||||
it('clicking an item calls its onSelect and onClose', () => {
|
||||
const onClose = vi.fn()
|
||||
const onSelect1 = vi.fn()
|
||||
render(<OpenWithMenu items={makeItems(onSelect1)} anchor={anchor} onClose={onClose} />)
|
||||
fireEvent.click(screen.getByText('In-app browser'))
|
||||
expect(onSelect1).toHaveBeenCalledTimes(1)
|
||||
expect(onClose).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
|
||||
it('clicking the second item calls its onSelect and onClose', () => {
|
||||
const onClose = vi.fn()
|
||||
const onSelect2 = vi.fn()
|
||||
render(<OpenWithMenu items={makeItems(vi.fn(), onSelect2)} anchor={anchor} onClose={onClose} />)
|
||||
fireEvent.click(screen.getByText('System browser'))
|
||||
expect(onSelect2).toHaveBeenCalledTimes(1)
|
||||
expect(onClose).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
|
||||
it('pressing Escape calls onClose', () => {
|
||||
const onClose = vi.fn()
|
||||
render(<OpenWithMenu items={makeItems()} anchor={anchor} onClose={onClose} />)
|
||||
fireEvent.keyDown(document, { key: 'Escape' })
|
||||
expect(onClose).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
|
||||
it('mousedown outside the menu calls onClose', () => {
|
||||
const onClose = vi.fn()
|
||||
render(<OpenWithMenu items={makeItems()} anchor={anchor} onClose={onClose} />)
|
||||
// Simulate mousedown on document body (outside the menu)
|
||||
fireEvent.mouseDown(document.body)
|
||||
expect(onClose).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
})
|
||||
52
desktop/src/components/common/OpenWithMenu.tsx
Normal file
52
desktop/src/components/common/OpenWithMenu.tsx
Normal file
@ -0,0 +1,52 @@
|
||||
import { createPortal } from 'react-dom'
|
||||
import { useEffect, useRef } from 'react'
|
||||
import { Globe, ExternalLink, FileText } from 'lucide-react'
|
||||
import { TargetIcon } from './TargetIcon'
|
||||
import type { OpenWithItem } from '../../lib/openWithItems'
|
||||
|
||||
type Props = {
|
||||
items: OpenWithItem[]
|
||||
anchor: { top: number; bottom: number; left: number; right: number }
|
||||
onClose: () => void
|
||||
}
|
||||
|
||||
function ItemIcon({ item }: { item: OpenWithItem }) {
|
||||
if ((item.icon === 'ide' || item.icon === 'file-manager') && item.target) return <TargetIcon target={item.target} size={20} />
|
||||
if (item.icon === 'in-app-browser') return <Globe size={18} strokeWidth={1.9} />
|
||||
if (item.icon === 'preview') return <FileText size={18} strokeWidth={1.9} />
|
||||
return <ExternalLink size={18} strokeWidth={1.9} />
|
||||
}
|
||||
|
||||
export function OpenWithMenu({ items, anchor, onClose }: Props) {
|
||||
const ref = useRef<HTMLDivElement>(null)
|
||||
useEffect(() => {
|
||||
const onDown = (e: MouseEvent) => { if (!ref.current?.contains(e.target as Node)) onClose() }
|
||||
const onKey = (e: KeyboardEvent) => { if (e.key === 'Escape') onClose() }
|
||||
document.addEventListener('mousedown', onDown)
|
||||
document.addEventListener('keydown', onKey)
|
||||
return () => { document.removeEventListener('mousedown', onDown); document.removeEventListener('keydown', onKey) }
|
||||
}, [onClose])
|
||||
|
||||
return createPortal(
|
||||
<div
|
||||
ref={ref}
|
||||
role="menu"
|
||||
className="fixed z-50 min-w-[220px] overflow-hidden rounded-[12px] border border-[var(--color-border)] bg-[var(--color-surface)] py-1 shadow-[var(--shadow-dropdown)]"
|
||||
style={{ top: anchor.bottom + 6, left: Math.min(anchor.left, window.innerWidth - 240) }}
|
||||
>
|
||||
{items.map((item) => (
|
||||
<button
|
||||
key={item.id}
|
||||
type="button"
|
||||
role="menuitem"
|
||||
onClick={() => { item.onSelect(); onClose() }}
|
||||
className="flex w-full items-center gap-3 px-3 py-2.5 text-left text-sm font-medium text-[var(--color-text-primary)] transition-colors hover:bg-[var(--color-surface-hover)]"
|
||||
>
|
||||
<span className="flex h-6 w-6 items-center justify-center text-[var(--color-text-secondary)]"><ItemIcon item={item} /></span>
|
||||
<span className="min-w-0 truncate">{item.label}</span>
|
||||
</button>
|
||||
))}
|
||||
</div>,
|
||||
document.body,
|
||||
)
|
||||
}
|
||||
34
desktop/src/components/common/TargetIcon.tsx
Normal file
34
desktop/src/components/common/TargetIcon.tsx
Normal file
@ -0,0 +1,34 @@
|
||||
import { useEffect, useState } from 'react'
|
||||
import { Code2, FolderOpen } from 'lucide-react'
|
||||
import type { OpenTarget } from '../../stores/openTargetStore'
|
||||
|
||||
export function getFallbackIcon(kind: 'ide' | 'file_manager', size = 17) {
|
||||
if (kind === 'file_manager') {
|
||||
return <FolderOpen size={size} strokeWidth={1.9} />
|
||||
}
|
||||
return <Code2 size={size} strokeWidth={1.9} />
|
||||
}
|
||||
|
||||
export function TargetIcon({ target, size = 18 }: { target: OpenTarget; size?: number }) {
|
||||
const [failed, setFailed] = useState(false)
|
||||
|
||||
useEffect(() => {
|
||||
setFailed(false)
|
||||
}, [target.iconUrl])
|
||||
|
||||
if (target.iconUrl && !failed) {
|
||||
return (
|
||||
<img
|
||||
src={target.iconUrl}
|
||||
alt=""
|
||||
aria-hidden="true"
|
||||
draggable={false}
|
||||
onError={() => setFailed(true)}
|
||||
className="block shrink-0 object-contain"
|
||||
style={{ width: size, height: size }}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
return getFallbackIcon(target.kind, Math.max(16, size - 1))
|
||||
}
|
||||
@ -1,44 +1,14 @@
|
||||
import { useEffect, useMemo, useRef, useState } from 'react'
|
||||
import { createPortal } from 'react-dom'
|
||||
import { ChevronDown, Code2, FolderOpen } from 'lucide-react'
|
||||
import { ChevronDown } from 'lucide-react'
|
||||
import { useTranslation } from '../../i18n'
|
||||
import { useOpenTargetStore, type OpenTarget } from '../../stores/openTargetStore'
|
||||
import { useOpenTargetStore } from '../../stores/openTargetStore'
|
||||
import { TargetIcon } from '../common/TargetIcon'
|
||||
|
||||
type Props = {
|
||||
path: string | null | undefined
|
||||
}
|
||||
|
||||
function getFallbackIcon(kind: 'ide' | 'file_manager', size = 17) {
|
||||
if (kind === 'file_manager') {
|
||||
return <FolderOpen size={size} strokeWidth={1.9} />
|
||||
}
|
||||
return <Code2 size={size} strokeWidth={1.9} />
|
||||
}
|
||||
|
||||
function TargetIcon({ target, size = 18 }: { target: OpenTarget; size?: number }) {
|
||||
const [failed, setFailed] = useState(false)
|
||||
|
||||
useEffect(() => {
|
||||
setFailed(false)
|
||||
}, [target.iconUrl])
|
||||
|
||||
if (target.iconUrl && !failed) {
|
||||
return (
|
||||
<img
|
||||
src={target.iconUrl}
|
||||
alt=""
|
||||
aria-hidden="true"
|
||||
draggable={false}
|
||||
onError={() => setFailed(true)}
|
||||
className="block shrink-0 object-contain"
|
||||
style={{ width: size, height: size }}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
return getFallbackIcon(target.kind, Math.max(16, size - 1))
|
||||
}
|
||||
|
||||
export function OpenProjectMenu({ path }: Props) {
|
||||
const t = useTranslation()
|
||||
const targets = useOpenTargetStore((state) => state.targets)
|
||||
|
||||
@ -91,6 +91,14 @@ export const en = {
|
||||
'openProject.openIn': 'Open in {target}',
|
||||
'openProject.openFailed': 'Could not open project',
|
||||
|
||||
// ─── Open With ─────────────────────────────────────
|
||||
'openWith.inAppBrowser': 'In-app browser',
|
||||
'openWith.systemBrowser': 'System browser',
|
||||
'openWith.systemDefault': 'Open with system default',
|
||||
'openWith.workspacePreview': 'Workspace preview',
|
||||
'openWith.openInTarget': 'Open in {target}',
|
||||
'openWith.revealInTarget': 'Reveal in {target}',
|
||||
|
||||
// ─── Workspace Panel ───────────────────────────────
|
||||
'workspace.changedFiles': 'Changed files',
|
||||
'workspace.allFiles': 'All files',
|
||||
|
||||
@ -93,6 +93,14 @@ export const zh: Record<TranslationKey, string> = {
|
||||
'openProject.openIn': '用 {target} 打开',
|
||||
'openProject.openFailed': '无法打开项目',
|
||||
|
||||
// ─── Open With ─────────────────────────────────────
|
||||
'openWith.inAppBrowser': '应用内浏览器',
|
||||
'openWith.systemBrowser': '系统浏览器',
|
||||
'openWith.systemDefault': '系统默认打开',
|
||||
'openWith.workspacePreview': '工作台预览',
|
||||
'openWith.openInTarget': '用 {target} 打开',
|
||||
'openWith.revealInTarget': '在 {target} 中显示',
|
||||
|
||||
// ─── Workspace Panel ───────────────────────────────
|
||||
'workspace.changedFiles': '已更改文件',
|
||||
'workspace.allFiles': '所有文件',
|
||||
|
||||
115
desktop/src/lib/openWithItems.test.ts
Normal file
115
desktop/src/lib/openWithItems.test.ts
Normal file
@ -0,0 +1,115 @@
|
||||
import { describe, expect, it, vi } from 'vitest'
|
||||
import { buildOpenWithItems, type OpenWithContext, type OpenWithDeps } from './openWithItems'
|
||||
import type { OpenTarget } from '../stores/openTargetStore'
|
||||
|
||||
function makeT() {
|
||||
return (key: string, vars?: Record<string, string>) =>
|
||||
vars?.target != null ? `${key}:${vars.target}` : key
|
||||
}
|
||||
|
||||
function makeDeps(overrides?: Partial<OpenWithDeps>): OpenWithDeps {
|
||||
return {
|
||||
openInAppBrowser: vi.fn(),
|
||||
openSystem: vi.fn(),
|
||||
openWorkspacePreview: vi.fn(),
|
||||
openTarget: vi.fn(),
|
||||
t: makeT(),
|
||||
...overrides,
|
||||
}
|
||||
}
|
||||
|
||||
const ideTarget: OpenTarget = { id: 'code', kind: 'ide', label: 'VS Code', icon: 'vscode', platform: 'darwin' }
|
||||
const fmTarget: OpenTarget = { id: 'finder', kind: 'file_manager', label: 'Finder', icon: 'finder', platform: 'darwin' }
|
||||
|
||||
describe('buildOpenWithItems – url context', () => {
|
||||
it('returns exactly [in-app, system] for a url context', () => {
|
||||
const deps = makeDeps()
|
||||
const ctx: OpenWithContext = { kind: 'url', url: 'https://example.com' }
|
||||
const items = buildOpenWithItems(ctx, [], deps)
|
||||
expect(items.map((i) => i.id)).toEqual(['in-app', 'system'])
|
||||
})
|
||||
|
||||
it('in-app calls openInAppBrowser with url', () => {
|
||||
const deps = makeDeps()
|
||||
const ctx: OpenWithContext = { kind: 'url', url: 'https://example.com' }
|
||||
const items = buildOpenWithItems(ctx, [], deps)
|
||||
items[0]!.onSelect()
|
||||
expect(deps.openInAppBrowser).toHaveBeenCalledWith('https://example.com')
|
||||
expect(deps.openSystem).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('system calls openSystem with url', () => {
|
||||
const deps = makeDeps()
|
||||
const ctx: OpenWithContext = { kind: 'url', url: 'https://example.com' }
|
||||
const items = buildOpenWithItems(ctx, [], deps)
|
||||
items[1]!.onSelect()
|
||||
expect(deps.openSystem).toHaveBeenCalledWith('https://example.com')
|
||||
expect(deps.openInAppBrowser).not.toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
|
||||
describe('buildOpenWithItems – file context with targets', () => {
|
||||
it('returns [preview, ide:code, fm:finder, system] ids', () => {
|
||||
const deps = makeDeps()
|
||||
const ctx: OpenWithContext = { kind: 'file', absolutePath: '/w/a.md', relPath: 'a.md', previewable: true }
|
||||
const items = buildOpenWithItems(ctx, [ideTarget, fmTarget], deps)
|
||||
expect(items.map((i) => i.id)).toEqual(['preview', 'ide:code', 'fm:finder', 'system'])
|
||||
})
|
||||
|
||||
it('preview calls openWorkspacePreview with relPath', () => {
|
||||
const deps = makeDeps()
|
||||
const ctx: OpenWithContext = { kind: 'file', absolutePath: '/w/a.md', relPath: 'a.md', previewable: true }
|
||||
const items = buildOpenWithItems(ctx, [ideTarget, fmTarget], deps)
|
||||
const preview = items.find((i) => i.id === 'preview')!
|
||||
preview.onSelect()
|
||||
expect(deps.openWorkspacePreview).toHaveBeenCalledWith('a.md')
|
||||
})
|
||||
|
||||
it('ide:code calls openTarget with correct args', () => {
|
||||
const deps = makeDeps()
|
||||
const ctx: OpenWithContext = { kind: 'file', absolutePath: '/w/a.md', relPath: 'a.md', previewable: true }
|
||||
const items = buildOpenWithItems(ctx, [ideTarget, fmTarget], deps)
|
||||
const ideItem = items.find((i) => i.id === 'ide:code')!
|
||||
ideItem.onSelect()
|
||||
expect(deps.openTarget).toHaveBeenCalledWith('code', '/w/a.md')
|
||||
})
|
||||
|
||||
it('system calls openSystem with absolutePath', () => {
|
||||
const deps = makeDeps()
|
||||
const ctx: OpenWithContext = { kind: 'file', absolutePath: '/w/a.md', relPath: 'a.md', previewable: true }
|
||||
const items = buildOpenWithItems(ctx, [ideTarget, fmTarget], deps)
|
||||
const sys = items.find((i) => i.id === 'system')!
|
||||
sys.onSelect()
|
||||
expect(deps.openSystem).toHaveBeenCalledWith('/w/a.md')
|
||||
})
|
||||
|
||||
it('ide item carries the target object', () => {
|
||||
const deps = makeDeps()
|
||||
const ctx: OpenWithContext = { kind: 'file', absolutePath: '/w/a.md', relPath: 'a.md', previewable: true }
|
||||
const items = buildOpenWithItems(ctx, [ideTarget, fmTarget], deps)
|
||||
const ideItem = items.find((i) => i.id === 'ide:code')!
|
||||
expect(ideItem.target).toBe(ideTarget)
|
||||
})
|
||||
})
|
||||
|
||||
describe('buildOpenWithItems – file context with inAppBrowserUrl (no previewable)', () => {
|
||||
it('returns [in-app, system] ids for no targets + inAppBrowserUrl', () => {
|
||||
const deps = makeDeps()
|
||||
const ctx: OpenWithContext = {
|
||||
kind: 'file',
|
||||
absolutePath: '/w/page.html',
|
||||
inAppBrowserUrl: 'http://127.0.0.1:4321/preview-fs/s1/page.html',
|
||||
}
|
||||
const items = buildOpenWithItems(ctx, [], deps)
|
||||
expect(items.map((i) => i.id)).toEqual(['in-app', 'system'])
|
||||
})
|
||||
|
||||
it('in-app calls openInAppBrowser with inAppBrowserUrl', () => {
|
||||
const deps = makeDeps()
|
||||
const inAppBrowserUrl = 'http://127.0.0.1:4321/preview-fs/s1/page.html'
|
||||
const ctx: OpenWithContext = { kind: 'file', absolutePath: '/w/page.html', inAppBrowserUrl }
|
||||
const items = buildOpenWithItems(ctx, [], deps)
|
||||
items.find((i) => i.id === 'in-app')!.onSelect()
|
||||
expect(deps.openInAppBrowser).toHaveBeenCalledWith(inAppBrowserUrl)
|
||||
})
|
||||
})
|
||||
48
desktop/src/lib/openWithItems.ts
Normal file
48
desktop/src/lib/openWithItems.ts
Normal file
@ -0,0 +1,48 @@
|
||||
import type { OpenTarget } from '../stores/openTargetStore'
|
||||
|
||||
export type OpenWithIcon = 'in-app-browser' | 'system' | 'ide' | 'file-manager' | 'preview'
|
||||
|
||||
export type OpenWithItem = {
|
||||
id: string
|
||||
label: string
|
||||
icon: OpenWithIcon
|
||||
target?: OpenTarget // present for ide/file-manager items (to render its favicon)
|
||||
onSelect: () => void
|
||||
}
|
||||
|
||||
export type OpenWithDeps = {
|
||||
openInAppBrowser: (url: string) => void
|
||||
openSystem: (urlOrPath: string) => void
|
||||
openWorkspacePreview: (relPath: string) => void
|
||||
openTarget: (targetId: string, absolutePath: string) => void
|
||||
t: (key: string, vars?: Record<string, string>) => string
|
||||
}
|
||||
|
||||
export type OpenWithContext =
|
||||
| { kind: 'url'; url: string }
|
||||
| { kind: 'file'; absolutePath: string; relPath?: string; previewable?: boolean; inAppBrowserUrl?: string }
|
||||
|
||||
export function buildOpenWithItems(ctx: OpenWithContext, targets: OpenTarget[], deps: OpenWithDeps): OpenWithItem[] {
|
||||
const items: OpenWithItem[] = []
|
||||
if (ctx.kind === 'url') {
|
||||
items.push({ id: 'in-app', label: deps.t('openWith.inAppBrowser'), icon: 'in-app-browser', onSelect: () => deps.openInAppBrowser(ctx.url) })
|
||||
items.push({ id: 'system', label: deps.t('openWith.systemBrowser'), icon: 'system', onSelect: () => deps.openSystem(ctx.url) })
|
||||
return items
|
||||
}
|
||||
if (ctx.previewable && ctx.relPath != null) {
|
||||
const relPath = ctx.relPath
|
||||
items.push({ id: 'preview', label: deps.t('openWith.workspacePreview'), icon: 'preview', onSelect: () => deps.openWorkspacePreview(relPath) })
|
||||
}
|
||||
if (ctx.inAppBrowserUrl) {
|
||||
const url = ctx.inAppBrowserUrl
|
||||
items.push({ id: 'in-app', label: deps.t('openWith.inAppBrowser'), icon: 'in-app-browser', onSelect: () => deps.openInAppBrowser(url) })
|
||||
}
|
||||
for (const target of targets.filter((x) => x.kind === 'ide')) {
|
||||
items.push({ id: `ide:${target.id}`, label: deps.t('openWith.openInTarget', { target: target.label }), icon: 'ide', target, onSelect: () => deps.openTarget(target.id, ctx.absolutePath) })
|
||||
}
|
||||
for (const target of targets.filter((x) => x.kind === 'file_manager')) {
|
||||
items.push({ id: `fm:${target.id}`, label: deps.t('openWith.revealInTarget', { target: target.label }), icon: 'file-manager', target, onSelect: () => deps.openTarget(target.id, ctx.absolutePath) })
|
||||
}
|
||||
items.push({ id: 'system', label: deps.t('openWith.systemDefault'), icon: 'system', onSelect: () => deps.openSystem(ctx.absolutePath) })
|
||||
return items
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user