mirror of
https://github.com/NanmiCoder/cc-haha
synced 2026-07-20 13:53:32 +08:00
feat: use native folder dialog in Tauri, keep directory tree for web
DirectoryPicker now detects runtime environment: - Tauri desktop: opens native OS folder picker via @tauri-apps/plugin-dialog - Web browser: falls back to backend API directory tree browser Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
f9c42c3b40
commit
0d71d8acb6
@ -9,8 +9,12 @@ type Props = {
|
|||||||
|
|
||||||
type DirEntry = { name: string; path: string; isDirectory: boolean }
|
type DirEntry = { name: string; path: string; isDirectory: boolean }
|
||||||
|
|
||||||
|
function isTauriRuntime() {
|
||||||
|
return typeof window !== 'undefined' && ('__TAURI_INTERNALS__' in window || '__TAURI__' in window)
|
||||||
|
}
|
||||||
|
|
||||||
export function DirectoryPicker({ value, onChange }: Props) {
|
export function DirectoryPicker({ value, onChange }: Props) {
|
||||||
const [open, setOpen] = useState(false)
|
const [isOpen, setIsOpen] = useState(false)
|
||||||
const [mode, setMode] = useState<'recent' | 'browse'>('recent')
|
const [mode, setMode] = useState<'recent' | 'browse'>('recent')
|
||||||
const [projects, setProjects] = useState<RecentProject[]>([])
|
const [projects, setProjects] = useState<RecentProject[]>([])
|
||||||
const [browseEntries, setBrowseEntries] = useState<DirEntry[]>([])
|
const [browseEntries, setBrowseEntries] = useState<DirEntry[]>([])
|
||||||
@ -21,23 +25,23 @@ export function DirectoryPicker({ value, onChange }: Props) {
|
|||||||
|
|
||||||
// Close on outside click
|
// Close on outside click
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!open) return
|
if (!isOpen) return
|
||||||
const handleClick = (e: MouseEvent) => {
|
const handleClick = (e: MouseEvent) => {
|
||||||
if (ref.current && !ref.current.contains(e.target as Node)) setOpen(false)
|
if (ref.current && !ref.current.contains(e.target as Node)) setIsOpen(false)
|
||||||
}
|
}
|
||||||
document.addEventListener('mousedown', handleClick)
|
document.addEventListener('mousedown', handleClick)
|
||||||
return () => document.removeEventListener('mousedown', handleClick)
|
return () => document.removeEventListener('mousedown', handleClick)
|
||||||
}, [open])
|
}, [isOpen])
|
||||||
|
|
||||||
// Load recent projects when opened
|
// Load recent projects when opened
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!open || mode !== 'recent') return
|
if (!isOpen || mode !== 'recent') return
|
||||||
setLoading(true)
|
setLoading(true)
|
||||||
sessionsApi.getRecentProjects()
|
sessionsApi.getRecentProjects()
|
||||||
.then(({ projects }) => setProjects(projects))
|
.then(({ projects }) => setProjects(projects))
|
||||||
.catch(() => setProjects([]))
|
.catch(() => setProjects([]))
|
||||||
.finally(() => setLoading(false))
|
.finally(() => setLoading(false))
|
||||||
}, [open, mode])
|
}, [isOpen, mode])
|
||||||
|
|
||||||
const loadBrowseDir = async (path?: string) => {
|
const loadBrowseDir = async (path?: string) => {
|
||||||
setLoading(true)
|
setLoading(true)
|
||||||
@ -52,13 +56,30 @@ export function DirectoryPicker({ value, onChange }: Props) {
|
|||||||
|
|
||||||
const handleSelect = (path: string) => {
|
const handleSelect = (path: string) => {
|
||||||
onChange(path)
|
onChange(path)
|
||||||
setOpen(false)
|
setIsOpen(false)
|
||||||
setMode('recent')
|
setMode('recent')
|
||||||
}
|
}
|
||||||
|
|
||||||
const switchToBrowse = () => {
|
const handleChooseFolder = async () => {
|
||||||
setMode('browse')
|
if (isTauriRuntime()) {
|
||||||
loadBrowseDir(value || undefined)
|
// Desktop: native OS folder dialog
|
||||||
|
setIsOpen(false)
|
||||||
|
try {
|
||||||
|
const { open } = await import('@tauri-apps/plugin-dialog')
|
||||||
|
const selected = await open({
|
||||||
|
directory: true,
|
||||||
|
multiple: false,
|
||||||
|
title: 'Choose project folder',
|
||||||
|
})
|
||||||
|
if (selected) onChange(selected)
|
||||||
|
} catch (err) {
|
||||||
|
console.error('[DirectoryPicker] Failed to open folder dialog:', err)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Web browser: directory tree via backend API
|
||||||
|
setMode('browse')
|
||||||
|
loadBrowseDir(value || undefined)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Find selected project info
|
// Find selected project info
|
||||||
@ -69,10 +90,9 @@ export function DirectoryPicker({ value, onChange }: Props) {
|
|||||||
{/* Trigger — shows selected project chip or placeholder */}
|
{/* Trigger — shows selected project chip or placeholder */}
|
||||||
{value ? (
|
{value ? (
|
||||||
<button
|
<button
|
||||||
onClick={() => { setOpen(!open); setMode('recent') }}
|
onClick={() => { setIsOpen(!isOpen); setMode('recent') }}
|
||||||
className="flex items-center gap-2 px-3 py-1.5 bg-[var(--color-surface-container-low)] hover:bg-[var(--color-surface-hover)] rounded-full text-xs transition-colors border border-[var(--color-border)]"
|
className="flex items-center gap-2 px-3 py-1.5 bg-[var(--color-surface-container-low)] hover:bg-[var(--color-surface-hover)] rounded-full text-xs transition-colors border border-[var(--color-border)]"
|
||||||
>
|
>
|
||||||
{/* GitHub icon for git repos, folder for others */}
|
|
||||||
{selectedProject?.isGit ? (
|
{selectedProject?.isGit ? (
|
||||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="currentColor" className="text-[var(--color-text-secondary)]">
|
<svg width="16" height="16" viewBox="0 0 16 16" fill="currentColor" className="text-[var(--color-text-secondary)]">
|
||||||
<path d="M8 0C3.58 0 0 3.58 0 8c0 3.54 2.29 6.53 5.47 7.59.4.07.55-.17.55-.38 0-.19-.01-.82-.01-1.49-2.01.37-2.53-.49-2.69-.94-.09-.23-.48-.94-.82-1.13-.28-.15-.68-.52-.01-.53.63-.01 1.08.58 1.23.82.72 1.21 1.87.87 2.33.66.07-.52.28-.87.51-1.07-1.78-.2-3.64-.89-3.64-3.95 0-.87.31-1.59.82-2.15-.08-.2-.36-1.02.08-2.12 0 0 .67-.21 2.2.82.64-.18 1.32-.27 2-.27.68 0 1.36.09 2 .27 1.53-1.04 2.2-.82 2.2-.82.44 1.1.16 1.92.08 2.12.51.56.82 1.27.82 2.15 0 3.07-1.87 3.75-3.65 3.95.29.25.54.73.54 1.48 0 1.07-.01 1.93-.01 2.2 0 .21.15.46.55.38A8.013 8.013 0 0016 8c0-4.42-3.58-8-8-8z" />
|
<path d="M8 0C3.58 0 0 3.58 0 8c0 3.54 2.29 6.53 5.47 7.59.4.07.55-.17.55-.38 0-.19-.01-.82-.01-1.49-2.01.37-2.53-.49-2.69-.94-.09-.23-.48-.94-.82-1.13-.28-.15-.68-.52-.01-.53.63-.01 1.08.58 1.23.82.72 1.21 1.87.87 2.33.66.07-.52.28-.87.51-1.07-1.78-.2-3.64-.89-3.64-3.95 0-.87.31-1.59.82-2.15-.08-.2-.36-1.02.08-2.12 0 0 .67-.21 2.2.82.64-.18 1.32-.27 2-.27.68 0 1.36.09 2 .27 1.53-1.04 2.2-.82 2.2-.82.44 1.1.16 1.92.08 2.12.51.56.82 1.27.82 2.15 0 3.07-1.87 3.75-3.65 3.95.29.25.54.73.54 1.48 0 1.07-.01 1.93-.01 2.2 0 .21.15.46.55.38A8.013 8.013 0 0016 8c0-4.42-3.58-8-8-8z" />
|
||||||
@ -96,7 +116,7 @@ export function DirectoryPicker({ value, onChange }: Props) {
|
|||||||
</button>
|
</button>
|
||||||
) : (
|
) : (
|
||||||
<button
|
<button
|
||||||
onClick={() => { setOpen(!open); setMode('recent') }}
|
onClick={() => { setIsOpen(!isOpen); setMode('recent') }}
|
||||||
className="flex items-center gap-2 text-xs text-[var(--color-text-tertiary)] hover:text-[var(--color-text-secondary)] transition-colors"
|
className="flex items-center gap-2 text-xs text-[var(--color-text-tertiary)] hover:text-[var(--color-text-secondary)] transition-colors"
|
||||||
>
|
>
|
||||||
<span className="material-symbols-outlined text-[14px]">folder_open</span>
|
<span className="material-symbols-outlined text-[14px]">folder_open</span>
|
||||||
@ -105,10 +125,9 @@ export function DirectoryPicker({ value, onChange }: Props) {
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Dropdown */}
|
{/* Dropdown */}
|
||||||
{open && (
|
{isOpen && (
|
||||||
<div className="absolute left-0 bottom-full mb-2 w-[400px] z-50 bg-[var(--color-surface-container-lowest)] border border-[var(--color-border)] rounded-xl shadow-[var(--shadow-dropdown)] overflow-hidden">
|
<div className="absolute left-0 bottom-full mb-2 w-[400px] z-50 bg-[var(--color-surface-container-lowest)] border border-[var(--color-border)] rounded-xl shadow-[var(--shadow-dropdown)] overflow-hidden">
|
||||||
{mode === 'recent' ? (
|
{mode === 'recent' ? (
|
||||||
/* Recent projects mode */
|
|
||||||
<>
|
<>
|
||||||
<div className="px-4 py-2 text-[10px] font-bold uppercase tracking-widest text-[var(--color-outline)]">
|
<div className="px-4 py-2 text-[10px] font-bold uppercase tracking-widest text-[var(--color-outline)]">
|
||||||
Recent
|
Recent
|
||||||
@ -129,7 +148,6 @@ export function DirectoryPicker({ value, onChange }: Props) {
|
|||||||
isSelected ? 'bg-[var(--color-surface-selected)]' : ''
|
isSelected ? 'bg-[var(--color-surface-selected)]' : ''
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
{/* Icon */}
|
|
||||||
{project.isGit ? (
|
{project.isGit ? (
|
||||||
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="var(--color-text-secondary)" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" className="flex-shrink-0">
|
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="var(--color-text-secondary)" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" className="flex-shrink-0">
|
||||||
<circle cx="18" cy="18" r="3" /><circle cx="6" cy="6" r="3" />
|
<circle cx="18" cy="18" r="3" /><circle cx="6" cy="6" r="3" />
|
||||||
@ -138,8 +156,6 @@ export function DirectoryPicker({ value, onChange }: Props) {
|
|||||||
) : (
|
) : (
|
||||||
<span className="material-symbols-outlined text-[20px] text-[var(--color-text-secondary)] flex-shrink-0">folder</span>
|
<span className="material-symbols-outlined text-[20px] text-[var(--color-text-secondary)] flex-shrink-0">folder</span>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Info */}
|
|
||||||
<div className="flex-1 min-w-0">
|
<div className="flex-1 min-w-0">
|
||||||
<div className="text-sm font-semibold text-[var(--color-text-primary)] truncate">
|
<div className="text-sm font-semibold text-[var(--color-text-primary)] truncate">
|
||||||
{project.repoName || project.projectName}
|
{project.repoName || project.projectName}
|
||||||
@ -148,8 +164,6 @@ export function DirectoryPicker({ value, onChange }: Props) {
|
|||||||
{project.realPath}
|
{project.realPath}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Check mark */}
|
|
||||||
{isSelected && (
|
{isSelected && (
|
||||||
<span className="material-symbols-outlined text-[18px] text-[var(--color-brand)] flex-shrink-0" style={{ fontVariationSettings: "'FILL' 1" }}>
|
<span className="material-symbols-outlined text-[18px] text-[var(--color-brand)] flex-shrink-0" style={{ fontVariationSettings: "'FILL' 1" }}>
|
||||||
check
|
check
|
||||||
@ -164,7 +178,7 @@ export function DirectoryPicker({ value, onChange }: Props) {
|
|||||||
{/* Divider + Choose different folder */}
|
{/* Divider + Choose different folder */}
|
||||||
<div className="border-t border-[var(--color-border)]">
|
<div className="border-t border-[var(--color-border)]">
|
||||||
<button
|
<button
|
||||||
onClick={switchToBrowse}
|
onClick={handleChooseFolder}
|
||||||
className="w-full flex items-center gap-3 px-4 py-3 text-left hover:bg-[var(--color-surface-hover)] transition-colors"
|
className="w-full flex items-center gap-3 px-4 py-3 text-left hover:bg-[var(--color-surface-hover)] transition-colors"
|
||||||
>
|
>
|
||||||
<span className="material-symbols-outlined text-[20px] text-[var(--color-text-tertiary)]">create_new_folder</span>
|
<span className="material-symbols-outlined text-[20px] text-[var(--color-text-tertiary)]">create_new_folder</span>
|
||||||
@ -173,7 +187,7 @@ export function DirectoryPicker({ value, onChange }: Props) {
|
|||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
/* File browser mode */
|
/* Directory tree browser (web only) */
|
||||||
<>
|
<>
|
||||||
<div className="px-3 py-2 border-b border-[var(--color-border)] flex items-center gap-1 flex-wrap">
|
<div className="px-3 py-2 border-b border-[var(--color-border)] flex items-center gap-1 flex-wrap">
|
||||||
<button onClick={() => setMode('recent')} className="text-xs text-[var(--color-text-accent)] hover:underline mr-2">
|
<button onClick={() => setMode('recent')} className="text-xs text-[var(--color-text-accent)] hover:underline mr-2">
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user