Add agent guide and improve drive navigation UX
This commit is contained in:
@@ -2,9 +2,9 @@
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
||||
<link rel="icon" type="image/svg+xml" href="/filez.svg" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>frontend</title>
|
||||
<title>FileZ</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
|
||||
10
frontend/public/filez.svg
Normal file
10
frontend/public/filez.svg
Normal file
@@ -0,0 +1,10 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64" role="img" aria-label="FileZ">
|
||||
<defs>
|
||||
<linearGradient id="g" x1="0" y1="0" x2="1" y2="1">
|
||||
<stop offset="0" stop-color="#34d399"/>
|
||||
<stop offset="1" stop-color="#0ea5e9"/>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<rect x="6" y="6" width="52" height="52" rx="12" fill="#111827"/>
|
||||
<path d="M18 20h28v6L26 44h20v6H18v-6l20-18H18z" fill="url(#g)"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 423 B |
@@ -111,6 +111,8 @@ const dict: Record<Lang, Record<string, string>> = {
|
||||
archives: 'Archives',
|
||||
tagged: 'Tagged',
|
||||
recent: 'Recent',
|
||||
recentFolders: 'Recent folders',
|
||||
clearHistory: 'Clear history',
|
||||
tags: 'Tags',
|
||||
addTag: 'Add tag',
|
||||
openInBrowser: 'Open in browser',
|
||||
@@ -191,6 +193,8 @@ const dict: Record<Lang, Record<string, string>> = {
|
||||
archives: 'Архивы',
|
||||
tagged: 'С тегами',
|
||||
recent: 'Недавние',
|
||||
recentFolders: 'Недавние папки',
|
||||
clearHistory: 'Очистить историю',
|
||||
tags: 'Теги',
|
||||
addTag: 'Добавить тег',
|
||||
openInBrowser: 'Открыть в браузере',
|
||||
@@ -243,12 +247,16 @@ function fileExt(name: string): string {
|
||||
return name.slice(i + 1).toLowerCase()
|
||||
}
|
||||
|
||||
function parentPathOf(p: string): string {
|
||||
const n = p.trim()
|
||||
if (!n || n === '/') return '/'
|
||||
const parts = n.split('/').filter(Boolean)
|
||||
if (parts.length <= 1) return '/'
|
||||
return '/' + parts.slice(0, -1).join('/')
|
||||
function drivePathFromLocation(): string {
|
||||
const sp = new URLSearchParams(window.location.search)
|
||||
const q = sp.get('p') || '/'
|
||||
const clean = ('/' + q).replace(/\/+/g, '/')
|
||||
return clean === '//' ? '/' : clean
|
||||
}
|
||||
|
||||
function driveUrl(p: string): string {
|
||||
if (!p || p === '/') return '/drive'
|
||||
return `/drive?p=${encodeURIComponent(p)}`
|
||||
}
|
||||
|
||||
function fileKind(file: FileEntry): string {
|
||||
@@ -371,16 +379,53 @@ export default function App() {
|
||||
const fileInputRef = useRef<HTMLInputElement>(null)
|
||||
const folderInputRef = useRef<HTMLInputElement>(null)
|
||||
const searchRef = useRef<HTMLInputElement>(null)
|
||||
const driveHistoryRef = useRef<string[]>(['/'])
|
||||
const driveHistoryIndexRef = useRef(0)
|
||||
|
||||
const effectiveTheme = useMemo(() => user?.theme ?? 'dracula', [user?.theme])
|
||||
const effectiveMode = useMemo(() => (user?.colorMode && user.colorMode !== 'auto' ? user.colorMode : detectMode()), [user?.colorMode])
|
||||
const t = (k: string) => dict[lang][k] ?? k
|
||||
|
||||
useEffect(() => {
|
||||
const onPop = () => setRoute(routeFromPath(window.location.pathname))
|
||||
const onPop = () => {
|
||||
const nextRoute = routeFromPath(window.location.pathname)
|
||||
setRoute(nextRoute)
|
||||
if (nextRoute === 'drive' && user) {
|
||||
const nextPath = drivePathFromLocation()
|
||||
const idx = driveHistoryRef.current.lastIndexOf(nextPath)
|
||||
if (idx >= 0) {
|
||||
driveHistoryIndexRef.current = idx
|
||||
} else {
|
||||
driveHistoryRef.current = [...driveHistoryRef.current, nextPath]
|
||||
driveHistoryIndexRef.current = driveHistoryRef.current.length - 1
|
||||
}
|
||||
void loadFiles(nextPath, { syncUrl: false, trackHistory: false })
|
||||
}
|
||||
}
|
||||
window.addEventListener('popstate', onPop)
|
||||
return () => window.removeEventListener('popstate', onPop)
|
||||
}, [])
|
||||
}, [user])
|
||||
|
||||
useEffect(() => {
|
||||
const onAuxClick = (e: MouseEvent) => {
|
||||
if (route !== 'drive' || !user) return
|
||||
if (e.button === 3) {
|
||||
e.preventDefault()
|
||||
const nextIdx = driveHistoryIndexRef.current - 1
|
||||
if (nextIdx < 0) return
|
||||
driveHistoryIndexRef.current = nextIdx
|
||||
void loadFiles(driveHistoryRef.current[nextIdx], { replace: true, trackHistory: false })
|
||||
} else if (e.button === 4) {
|
||||
e.preventDefault()
|
||||
const nextIdx = driveHistoryIndexRef.current + 1
|
||||
if (nextIdx >= driveHistoryRef.current.length) return
|
||||
driveHistoryIndexRef.current = nextIdx
|
||||
void loadFiles(driveHistoryRef.current[nextIdx], { replace: true, trackHistory: false })
|
||||
}
|
||||
}
|
||||
document.addEventListener('auxclick', onAuxClick, true)
|
||||
return () => document.removeEventListener('auxclick', onAuxClick, true)
|
||||
}, [route, user])
|
||||
|
||||
useEffect(() => {
|
||||
document.documentElement.dataset.theme = effectiveTheme
|
||||
@@ -407,20 +452,6 @@ export default function App() {
|
||||
return () => window.removeEventListener('keydown', onKey)
|
||||
}, [route, path])
|
||||
|
||||
useEffect(() => {
|
||||
const onMouseUp = (e: MouseEvent) => {
|
||||
if (route !== 'drive' || !user) return
|
||||
if (e.button !== 3) return
|
||||
e.preventDefault()
|
||||
const up = parentPathOf(path)
|
||||
if (up !== path) {
|
||||
void loadFiles(up)
|
||||
}
|
||||
}
|
||||
window.addEventListener('mouseup', onMouseUp)
|
||||
return () => window.removeEventListener('mouseup', onMouseUp)
|
||||
}, [route, user, path])
|
||||
|
||||
useEffect(() => {
|
||||
void bootstrap()
|
||||
}, [])
|
||||
@@ -477,7 +508,10 @@ export default function App() {
|
||||
try {
|
||||
const me = await api<User>('/api/auth/me')
|
||||
setUser(me)
|
||||
await loadFiles('/')
|
||||
const initial = routeFromPath(window.location.pathname) === 'drive' ? drivePathFromLocation() : '/'
|
||||
driveHistoryRef.current = [initial]
|
||||
driveHistoryIndexRef.current = 0
|
||||
await loadFiles(initial, { syncUrl: routeFromPath(window.location.pathname) !== 'drive' })
|
||||
} catch {
|
||||
setUser(null)
|
||||
}
|
||||
@@ -491,16 +525,39 @@ export default function App() {
|
||||
}
|
||||
|
||||
function navigate(next: Route) {
|
||||
const target = next === 'admin' ? '/admin' : next === 'drive' ? '/drive' : '/'
|
||||
const target = next === 'admin' ? '/admin' : next === 'drive' ? driveUrl(path) : '/'
|
||||
window.history.pushState(null, '', target)
|
||||
setRoute(next)
|
||||
setErr('')
|
||||
}
|
||||
|
||||
async function loadFiles(nextPath: string) {
|
||||
async function loadFiles(nextPath: string, opts?: { syncUrl?: boolean; replace?: boolean; trackHistory?: boolean }) {
|
||||
const data = await api<{ path: string; entries: FileEntry[] }>(`/api/files?path=${encodeURIComponent(nextPath)}`)
|
||||
setPath(data.path)
|
||||
setFiles([...data.entries])
|
||||
|
||||
if (opts?.trackHistory !== false) {
|
||||
const current = driveHistoryRef.current
|
||||
const idx = driveHistoryIndexRef.current
|
||||
const head = current.slice(0, idx + 1)
|
||||
if (head[head.length - 1] !== data.path) {
|
||||
head.push(data.path)
|
||||
}
|
||||
driveHistoryRef.current = head
|
||||
driveHistoryIndexRef.current = head.length - 1
|
||||
}
|
||||
|
||||
if (opts?.syncUrl !== false && route === 'drive') {
|
||||
const target = driveUrl(data.path)
|
||||
const current = window.location.pathname + window.location.search
|
||||
if (current !== target) {
|
||||
if (opts?.replace) {
|
||||
window.history.replaceState(null, '', target)
|
||||
} else {
|
||||
window.history.pushState(null, '', target)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function loadUsers() {
|
||||
@@ -1161,6 +1218,14 @@ export default function App() {
|
||||
setPaintSelect({ active: true, value: true })
|
||||
setSelectedPaths((prev) => ({ ...prev, [f.path]: true }))
|
||||
}}
|
||||
onDoubleClick={(e) => {
|
||||
if (isSelectionIgnoredTarget(e.target)) return
|
||||
if (f.isDir) {
|
||||
void loadFiles(f.path)
|
||||
} else {
|
||||
void openFile(f)
|
||||
}
|
||||
}}
|
||||
onDragStart={(e) => {
|
||||
const paths = selectedPaths[f.path] ? selectedList : [f.path]
|
||||
setDraggingPaths(paths)
|
||||
@@ -1213,11 +1278,7 @@ export default function App() {
|
||||
<div className="min-w-0">
|
||||
<div className="flex items-center gap-2">
|
||||
<KindIcon file={f} />
|
||||
{f.isDir ? (
|
||||
<button type="button" className="truncate text-left text-sm font-medium hover:underline" onClick={() => void loadFiles(f.path)}>{f.name}</button>
|
||||
) : (
|
||||
<button type="button" className="truncate text-left text-sm font-medium hover:underline" onClick={() => void openFile(f)}>{f.name}</button>
|
||||
)}
|
||||
<p className="truncate text-left text-sm font-medium hover:underline">{f.name}</p>
|
||||
</div>
|
||||
<div className="mt-2 flex flex-wrap items-center gap-1">
|
||||
{(f.tags ?? []).map((tag) => (
|
||||
|
||||
Reference in New Issue
Block a user