Add agent guide and improve drive navigation UX

This commit is contained in:
mixa
2026-03-02 23:14:01 +03:00
parent d6658100bd
commit 2fab944351
5 changed files with 252 additions and 33 deletions

View File

@@ -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
View 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

View File

@@ -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) => (