mirror of
https://github.com/open-webui/open-webui.git
synced 2026-08-28 05:27:35 +00:00
refac
This commit is contained in:
parent
c1f914a626
commit
c93c6d6fc4
3 changed files with 108 additions and 52 deletions
|
|
@ -116,10 +116,6 @@
|
|||
let loadingDirs: Set<string> = new Set();
|
||||
let directoryMenu: { x: number; y: number } | null = null;
|
||||
|
||||
const invalidateVisibleRows = () => {
|
||||
entries = [...entries];
|
||||
};
|
||||
|
||||
/** Normalize Windows backslashes and collapse duplicate separators. */
|
||||
const normalizePath = (path: string) => path.replace(/\\/g, '/').replace(/\/{2,}/g, '/');
|
||||
|
||||
|
|
@ -145,26 +141,26 @@
|
|||
return normalizePath(`${parent}${child}`);
|
||||
};
|
||||
|
||||
const sortEntries = (items: FileEntry[]): FileEntry[] => {
|
||||
const sortEntries = (items: FileEntry[], mode: SortMode, asc: boolean): FileEntry[] => {
|
||||
return [...items].sort((a, b) => {
|
||||
// Directories always first
|
||||
if (a.type !== b.type) return a.type === 'directory' ? -1 : 1;
|
||||
if (sortBy === 'date') {
|
||||
if (mode === 'date') {
|
||||
if (a.modified !== undefined && b.modified !== undefined) {
|
||||
return sortAsc ? a.modified - b.modified : b.modified - a.modified;
|
||||
return asc ? a.modified - b.modified : b.modified - a.modified;
|
||||
}
|
||||
const cmp = a.name.localeCompare(b.name);
|
||||
return sortAsc ? cmp : -cmp;
|
||||
return asc ? cmp : -cmp;
|
||||
}
|
||||
if (sortBy === 'size') {
|
||||
if (mode === 'size') {
|
||||
if (a.size !== undefined && b.size !== undefined) {
|
||||
return sortAsc ? a.size - b.size : b.size - a.size;
|
||||
return asc ? a.size - b.size : b.size - a.size;
|
||||
}
|
||||
const cmp = a.name.localeCompare(b.name);
|
||||
return sortAsc ? cmp : -cmp;
|
||||
return asc ? cmp : -cmp;
|
||||
}
|
||||
const cmp = a.name.localeCompare(b.name);
|
||||
return sortAsc ? cmp : -cmp;
|
||||
return asc ? cmp : -cmp;
|
||||
});
|
||||
};
|
||||
|
||||
|
|
@ -175,16 +171,16 @@
|
|||
sortBy = mode;
|
||||
sortAsc = true;
|
||||
}
|
||||
invalidateVisibleRows();
|
||||
treeCache = new Map(treeCache);
|
||||
void refreshBrowser();
|
||||
};
|
||||
|
||||
const filterEntries = (items: FileEntry[]) =>
|
||||
showHidden ? items : items.filter((entry) => !entry.name.startsWith('.'));
|
||||
const filterEntries = (items: FileEntry[], hiddenVisible: boolean) =>
|
||||
hiddenVisible ? items : items.filter((entry) => !entry.name.startsWith('.'));
|
||||
|
||||
const entryPath = (parentPath: string, entry: FileEntry) =>
|
||||
entry.type === 'directory' ? asDirectoryPath(joinPath(parentPath, entry.name)) : joinPath(parentPath, entry.name);
|
||||
entry.type === 'directory'
|
||||
? asDirectoryPath(joinPath(parentPath, entry.name))
|
||||
: joinPath(parentPath, entry.name);
|
||||
|
||||
const withRowIndexes = (rows: Omit<BrowserRow, 'rowIndex'>[]): BrowserRow[] =>
|
||||
rows.map((row, rowIndex) => ({ ...row, rowIndex }));
|
||||
|
|
@ -192,19 +188,35 @@
|
|||
const buildVisibleRows = (
|
||||
items: FileEntry[],
|
||||
parentPath: string,
|
||||
expanded: Set<string>,
|
||||
cache: Map<string, FileEntry[]>,
|
||||
hiddenVisible: boolean,
|
||||
mode: SortMode,
|
||||
asc: boolean,
|
||||
depth = 0
|
||||
): Omit<BrowserRow, 'rowIndex'>[] =>
|
||||
sortEntries(filterEntries(items)).flatMap((entry) => {
|
||||
sortEntries(filterEntries(items, hiddenVisible), mode, asc).flatMap((entry) => {
|
||||
const fullPath = entryPath(parentPath, entry);
|
||||
const row = { ...entry, fullPath, parentPath, depth };
|
||||
const children =
|
||||
entry.type === 'directory' && expandedDirs.has(fullPath)
|
||||
? buildVisibleRows(treeCache.get(fullPath) ?? [], fullPath, depth + 1)
|
||||
entry.type === 'directory' && expanded.has(fullPath)
|
||||
? buildVisibleRows(
|
||||
cache.get(fullPath) ?? [],
|
||||
fullPath,
|
||||
expanded,
|
||||
cache,
|
||||
hiddenVisible,
|
||||
mode,
|
||||
asc,
|
||||
depth + 1
|
||||
)
|
||||
: [];
|
||||
return [row, ...children];
|
||||
});
|
||||
|
||||
$: visibleEntries = withRowIndexes(buildVisibleRows(entries, currentPath));
|
||||
$: visibleEntries = withRowIndexes(
|
||||
buildVisibleRows(entries, currentPath, expandedDirs, treeCache, showHidden, sortBy, sortAsc)
|
||||
);
|
||||
|
||||
// ── Navigation history ──────────────────────────────────────────────
|
||||
type NavEntry = { path: string; file: string | null };
|
||||
|
|
@ -465,18 +477,29 @@
|
|||
return isInsideFileRoot(path) ? asDirectoryPath(path) : fileRoot.path;
|
||||
};
|
||||
|
||||
const applyCwd = (cwd: TerminalCwd | null, preferredPath?: string) => {
|
||||
const rootFromCwd = (cwd: TerminalCwd | null, pathHint?: string) => {
|
||||
const cwdPath = cwd?.cwd ? asDirectoryPath(cwd.cwd) : null;
|
||||
const homePath = cwd?.home ? asDirectoryPath(cwd.home) : null;
|
||||
const preferredDirectory = preferredPath ? asDirectoryPath(preferredPath) : null;
|
||||
const pathForRoot =
|
||||
preferredDirectory && preferredDirectory !== '/' ? preferredDirectory : cwdPath;
|
||||
const homeRoot =
|
||||
homePath && pathForRoot && (pathForRoot === homePath || pathForRoot.startsWith(homePath))
|
||||
? { path: homePath, label: 'Home' }
|
||||
: undefined;
|
||||
const hintPath = pathHint ? asDirectoryPath(pathHint) : null;
|
||||
const rootPath = cwd?.root?.path ? asDirectoryPath(cwd.root.path) : null;
|
||||
setFileRoot(rootPath && rootPath !== '/' ? cwd?.root : (homeRoot ?? cwd?.root));
|
||||
|
||||
if (rootPath && rootPath !== '/') return cwd?.root;
|
||||
|
||||
const pathForHome = hintPath && hintPath !== '/' ? hintPath : cwdPath;
|
||||
if (
|
||||
homePath &&
|
||||
pathForHome &&
|
||||
(pathForHome === homePath || pathForHome.startsWith(homePath))
|
||||
) {
|
||||
return { path: homePath, label: 'Home' };
|
||||
}
|
||||
|
||||
return undefined;
|
||||
};
|
||||
|
||||
const applyCwd = (cwd: TerminalCwd | null, pathHint?: string) => {
|
||||
const cwdPath = cwd?.cwd ? asDirectoryPath(cwd.cwd) : null;
|
||||
setFileRoot(rootFromCwd(cwd, pathHint));
|
||||
const path = cwdPath ?? fileRoot?.path ?? '/';
|
||||
return clampToFileRoot(path);
|
||||
};
|
||||
|
|
@ -609,7 +632,6 @@
|
|||
next.delete(directory);
|
||||
expandedDirs = next;
|
||||
saveTreeState();
|
||||
invalidateVisibleRows();
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -624,7 +646,6 @@
|
|||
saveTreeState();
|
||||
toast.error($i18n.t('Failed to load folder'));
|
||||
} else {
|
||||
invalidateVisibleRows();
|
||||
treeCache = new Map(treeCache);
|
||||
}
|
||||
};
|
||||
|
|
@ -1140,20 +1161,21 @@
|
|||
if (!handledDisplayFile && terminal) {
|
||||
loading = true;
|
||||
|
||||
void (async () => {
|
||||
// Discover server features on initial mount
|
||||
const config = await getTerminalConfig(terminal.url, terminal.key);
|
||||
terminalEnabled = config?.features?.terminal !== false;
|
||||
void (async () => {
|
||||
// Discover server features on initial mount
|
||||
const config = await getTerminalConfig(terminal.url, terminal.key);
|
||||
terminalEnabled = config?.features?.terminal !== false;
|
||||
|
||||
const serverCwd = await getCwd(terminal.url, terminal.key, chatId ?? undefined);
|
||||
const serverPath = applyCwd(serverCwd, savedPath);
|
||||
if (chatId || savedPath === '/') {
|
||||
// Fetch session-specific cwd from the server (or global default for new chats)
|
||||
savedPath = serverPath;
|
||||
}
|
||||
savedPath = clampToFileRoot(savedPath);
|
||||
loadDir(savedPath, { restoreTree: true });
|
||||
})();
|
||||
const serverCwd = await getCwd(terminal.url, terminal.key, chatId ?? undefined);
|
||||
const useServerPath = !!chatId || savedPath === '/';
|
||||
const serverPath = applyCwd(serverCwd, useServerPath ? undefined : savedPath);
|
||||
if (useServerPath) {
|
||||
// Fetch session-specific cwd from the server (or global default for new chats)
|
||||
savedPath = serverPath;
|
||||
}
|
||||
savedPath = clampToFileRoot(savedPath);
|
||||
loadDir(savedPath, { restoreTree: true });
|
||||
})();
|
||||
}
|
||||
|
||||
mounted = true;
|
||||
|
|
|
|||
|
|
@ -48,7 +48,7 @@
|
|||
$: directoryPath = entryPath.endsWith('/') ? entryPath : `${entryPath}/`;
|
||||
$: writable = entry.writable !== false;
|
||||
$: canMutate = parentWritable && writable;
|
||||
$: rowIndent = `${12 + depth * 16}px`;
|
||||
$: rowIndent = `${8 + depth * 16}px`;
|
||||
|
||||
const formatRelativeTime = (epoch: number): string => {
|
||||
const diff = Math.floor(Date.now() / 1000) - epoch;
|
||||
|
|
@ -62,6 +62,7 @@
|
|||
|
||||
let dragOverFolder = false;
|
||||
let expandTimer: ReturnType<typeof setTimeout> | null = null;
|
||||
let menuOpen = false;
|
||||
|
||||
const clearExpandTimer = () => {
|
||||
if (!expandTimer) return;
|
||||
|
|
@ -209,7 +210,7 @@
|
|||
{#if entry.type === 'directory'}
|
||||
<button
|
||||
type="button"
|
||||
class="mr-1 flex w-3 shrink-0 items-center self-stretch justify-center text-gray-400 dark:text-gray-600 hover:text-gray-600 dark:hover:text-gray-400"
|
||||
class="mr-1.5 flex w-5 shrink-0 items-center self-stretch justify-center text-gray-400 dark:text-gray-600 hover:text-gray-600 dark:hover:text-gray-400"
|
||||
style="margin-left: {rowIndent};"
|
||||
on:click|stopPropagation={() => onToggleExpand(directoryPath)}
|
||||
aria-label={expanded ? $i18n.t('Collapse') : $i18n.t('Expand')}
|
||||
|
|
@ -222,7 +223,7 @@
|
|||
/>
|
||||
</button>
|
||||
{:else}
|
||||
<span class="mr-1 w-4 shrink-0 self-stretch" style="margin-left: {rowIndent};"></span>
|
||||
<span class="mr-1.5 w-5 shrink-0 self-stretch" style="margin-left: {rowIndent};"></span>
|
||||
{/if}
|
||||
|
||||
<button
|
||||
|
|
@ -291,7 +292,7 @@
|
|||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
<FileTypeIcon name={entry.name} type={entry.type} size={13} />
|
||||
<FileTypeIcon name={entry.name} type={entry.type} size={12} />
|
||||
{#if renaming}
|
||||
<!-- svelte-ignore a11y-click-events-have-key-events -->
|
||||
<input
|
||||
|
|
@ -333,7 +334,7 @@
|
|||
{/if}
|
||||
</button>
|
||||
|
||||
<Dropdown align="end" sideOffset={4}>
|
||||
<Dropdown bind:show={menuOpen} align="end" sideOffset={4}>
|
||||
<button
|
||||
class="shrink-0 flex h-5 w-5 items-center justify-center mr-1 rounded transition
|
||||
text-gray-400 hover:text-gray-600 dark:text-gray-500 dark:hover:text-gray-400
|
||||
|
|
@ -350,6 +351,7 @@
|
|||
class="select-none flex h-7 w-full items-center gap-2 rounded-lg px-2 text-xs hover:bg-gray-50/40 dark:hover:bg-white/4 transition"
|
||||
on:click={(e) => {
|
||||
e.stopPropagation();
|
||||
menuOpen = false;
|
||||
onOpen(entry);
|
||||
}}
|
||||
>
|
||||
|
|
@ -369,6 +371,7 @@
|
|||
class="select-none flex h-7 w-full items-center gap-2 rounded-lg px-2 text-xs hover:bg-gray-50/40 dark:hover:bg-white/4 transition"
|
||||
on:click={(e) => {
|
||||
e.stopPropagation();
|
||||
menuOpen = false;
|
||||
onToggleExpand(directoryPath);
|
||||
}}
|
||||
>
|
||||
|
|
@ -387,6 +390,7 @@
|
|||
class="select-none flex h-7 w-full items-center gap-2 rounded-lg px-2 text-xs hover:bg-gray-50/40 dark:hover:bg-white/4 transition"
|
||||
on:click={(e) => {
|
||||
e.stopPropagation();
|
||||
menuOpen = false;
|
||||
onDownload(entryPath);
|
||||
}}
|
||||
>
|
||||
|
|
@ -400,6 +404,7 @@
|
|||
class="select-none flex h-7 w-full items-center gap-2 rounded-lg px-2 text-xs hover:bg-gray-50/40 dark:hover:bg-white/4 transition"
|
||||
on:click={(e) => {
|
||||
e.stopPropagation();
|
||||
menuOpen = false;
|
||||
navigator.clipboard.writeText(entryPath).then(() => {
|
||||
toast.success($i18n.t('Path copied'));
|
||||
});
|
||||
|
|
@ -416,6 +421,7 @@
|
|||
on:click={(e) => {
|
||||
e.stopPropagation();
|
||||
if (!canMutate) return;
|
||||
menuOpen = false;
|
||||
startRename();
|
||||
}}
|
||||
>
|
||||
|
|
@ -430,6 +436,7 @@
|
|||
on:click={(e) => {
|
||||
e.stopPropagation();
|
||||
if (!canMutate) return;
|
||||
menuOpen = false;
|
||||
onDelete(entryPath.replace(/\/$/, ''), entry.name);
|
||||
}}
|
||||
>
|
||||
|
|
|
|||
|
|
@ -19,6 +19,7 @@
|
|||
let mounted = false;
|
||||
let initializedSlide = '';
|
||||
let hideThumbs = false;
|
||||
const slideShortcutKeys = ['ArrowUp', 'ArrowDown', 'ArrowLeft', 'ArrowRight'];
|
||||
|
||||
$: safeSlide = Math.min(Math.max(0, currentSlide), Math.max(0, slides.length - 1));
|
||||
$: selectedSlide = slides[safeSlide] ?? '';
|
||||
|
|
@ -45,6 +46,7 @@
|
|||
bounds: true,
|
||||
boundsPadding: 0.1,
|
||||
zoomSpeed: 0.065,
|
||||
filterKey: (e?: KeyboardEvent) => !!e && slideShortcutKeys.includes(e.key),
|
||||
beforeWheel: (e) => {
|
||||
if (!e.ctrlKey && !e.metaKey) return true;
|
||||
return false;
|
||||
|
|
@ -63,13 +65,36 @@
|
|||
};
|
||||
|
||||
const selectSlide = (index: number) => {
|
||||
currentSlide = index;
|
||||
const nextSlide = Math.min(Math.max(0, index), Math.max(0, slides.length - 1));
|
||||
if (nextSlide === safeSlide) return;
|
||||
|
||||
currentSlide = nextSlide;
|
||||
void tick().then(() => {
|
||||
updateFitScale();
|
||||
resetView();
|
||||
});
|
||||
};
|
||||
|
||||
const handleKeyDown = (e: KeyboardEvent) => {
|
||||
if (
|
||||
e.defaultPrevented ||
|
||||
e.altKey ||
|
||||
e.ctrlKey ||
|
||||
e.metaKey ||
|
||||
slides.length === 0 ||
|
||||
!slideShortcutKeys.includes(e.key)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
e.preventDefault();
|
||||
if (e.key === 'ArrowUp' || e.key === 'ArrowLeft') {
|
||||
selectSlide(safeSlide - 1);
|
||||
} else {
|
||||
selectSlide(safeSlide + 1);
|
||||
}
|
||||
};
|
||||
|
||||
const zoomIn = () => {
|
||||
if (!pzInstance || !stageEl) return;
|
||||
pzInstance.zoomTo(stageEl.clientWidth / 2, stageEl.clientHeight / 2, 1.25);
|
||||
|
|
@ -129,6 +154,8 @@
|
|||
});
|
||||
</script>
|
||||
|
||||
<svelte:window on:keydown={handleKeyDown} />
|
||||
|
||||
<div
|
||||
bind:this={rootEl}
|
||||
class="relative grid {hideThumbs
|
||||
|
|
@ -138,7 +165,7 @@
|
|||
<aside
|
||||
class={hideThumbs
|
||||
? 'hidden'
|
||||
: 'overflow-y-auto px-2.5 pt-3.5 pb-16 border-r border-gray-200/60 dark:border-white/10 bg-transparent'}
|
||||
: 'scrollbar-hidden overflow-y-auto px-2.5 pt-3.5 pb-16 border-r border-gray-200/60 dark:border-white/10 bg-transparent'}
|
||||
aria-label="Slides"
|
||||
>
|
||||
{#each slides as slide, index}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue