Merge pull request #506 from AtalayaLabs/claude/frontend-performance-analysis-iwtyx1
This commit is contained in:
@@ -6,7 +6,7 @@
|
|||||||
import { searchFiles } from '$lib/api/endpoints/search';
|
import { searchFiles } from '$lib/api/endpoints/search';
|
||||||
import { fileInlineUrl } from '$lib/api/endpoints/files';
|
import { fileInlineUrl } from '$lib/api/endpoints/files';
|
||||||
import type { FileItem, FolderItem } from '$lib/api/types';
|
import type { FileItem, FolderItem } from '$lib/api/types';
|
||||||
import CommandPalette from '$lib/components/CommandPalette.svelte';
|
import { lazyComponent } from '$lib/composables/lazyComponent.svelte';
|
||||||
import Icon from '$lib/icons/Icon.svelte';
|
import Icon from '$lib/icons/Icon.svelte';
|
||||||
import { iconNameFromClass } from '$lib/utils/display';
|
import { iconNameFromClass } from '$lib/utils/display';
|
||||||
import { userInitials, avatarColorIndex } from '$lib/utils/avatar';
|
import { userInitials, avatarColorIndex } from '$lib/utils/avatar';
|
||||||
@@ -19,6 +19,10 @@
|
|||||||
|
|
||||||
let { children }: { children: Snippet } = $props();
|
let { children }: { children: Snippet } = $props();
|
||||||
|
|
||||||
|
// The command palette is loaded on its first Cmd/Ctrl+K and mounted open.
|
||||||
|
// Until then its ~400-line module stays out of the initial bundle.
|
||||||
|
const palette = lazyComponent(() => import('$lib/components/CommandPalette.svelte'));
|
||||||
|
|
||||||
interface NavLink {
|
interface NavLink {
|
||||||
href: string;
|
href: string;
|
||||||
label: string;
|
label: string;
|
||||||
@@ -232,6 +236,13 @@
|
|||||||
<svelte:window
|
<svelte:window
|
||||||
onclick={closeMenus}
|
onclick={closeMenus}
|
||||||
onkeydown={(e) => {
|
onkeydown={(e) => {
|
||||||
|
// First Cmd/Ctrl+K loads the palette and mounts it open; once mounted,
|
||||||
|
// the palette's own handler takes over toggling/closing.
|
||||||
|
if ((e.metaKey || e.ctrlKey) && e.key.toLowerCase() === 'k' && !palette.component) {
|
||||||
|
e.preventDefault();
|
||||||
|
void palette.load();
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (e.key !== 'Escape') return;
|
if (e.key !== 'Escape') return;
|
||||||
if (aboutOpen) aboutOpen = false;
|
if (aboutOpen) aboutOpen = false;
|
||||||
else if (searchActive) closeMobileSearch();
|
else if (searchActive) closeMobileSearch();
|
||||||
@@ -692,7 +703,10 @@
|
|||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
<CommandPalette />
|
{#if palette.component}
|
||||||
|
{@const CommandPalette = palette.component}
|
||||||
|
<CommandPalette autoOpen />
|
||||||
|
{/if}
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
/* Body becomes the sidebar+main flex row only while the shell is mounted. */
|
/* Body becomes the sidebar+main flex row only while the shell is mounted. */
|
||||||
|
|||||||
@@ -17,6 +17,10 @@
|
|||||||
run: () => void;
|
run: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// `autoOpen` lets a lazy host (AppShell) mount us already-open on the first
|
||||||
|
// Cmd/Ctrl+K, since our own key listener only exists once we're mounted.
|
||||||
|
let { autoOpen = false }: { autoOpen?: boolean } = $props();
|
||||||
|
|
||||||
let open = $state(false);
|
let open = $state(false);
|
||||||
// Drives the enter animation: flipped on after mount so the overlay/panel
|
// Drives the enter animation: flipped on after mount so the overlay/panel
|
||||||
// transition from their initial (faded/offset) state.
|
// transition from their initial (faded/offset) state.
|
||||||
@@ -30,6 +34,18 @@
|
|||||||
// Element focused before the palette opened, restored on close.
|
// Element focused before the palette opened, restored on close.
|
||||||
let prevFocus: HTMLElement | null = null;
|
let prevFocus: HTMLElement | null = null;
|
||||||
|
|
||||||
|
// When mounted already-open (autoOpen), run the same enter sequence the
|
||||||
|
// keyboard path uses. Guarded so it fires once, not on every reopen.
|
||||||
|
let didAutoOpen = false;
|
||||||
|
$effect(() => {
|
||||||
|
if (didAutoOpen || !autoOpen) return;
|
||||||
|
didAutoOpen = true;
|
||||||
|
open = true;
|
||||||
|
prevFocus = document.activeElement as HTMLElement | null;
|
||||||
|
requestAnimationFrame(() => (entered = true));
|
||||||
|
queueMicrotask(() => input?.focus());
|
||||||
|
});
|
||||||
|
|
||||||
function close() {
|
function close() {
|
||||||
open = false;
|
open = false;
|
||||||
entered = false;
|
entered = false;
|
||||||
|
|||||||
@@ -0,0 +1,39 @@
|
|||||||
|
/**
|
||||||
|
* Defer loading a heavy Svelte component until it is first needed.
|
||||||
|
*
|
||||||
|
* The dynamic `import()` puts the component in its own chunk, keeping it out of
|
||||||
|
* the initial bundle. The component type is inferred from the module's default
|
||||||
|
* export, so binding and prop typing at the call site stay fully checked. Call
|
||||||
|
* `load()` right before the component is shown, then render it once `component`
|
||||||
|
* is non-null:
|
||||||
|
*
|
||||||
|
* ```svelte
|
||||||
|
* const viewer = lazyComponent(() => import('$lib/components/FileViewer.svelte'));
|
||||||
|
* $effect(() => { if (open) void viewer.load(); });
|
||||||
|
* …
|
||||||
|
* {#if viewer.component}
|
||||||
|
* {@const Viewer = viewer.component}
|
||||||
|
* <Viewer bind:open {file} />
|
||||||
|
* {/if}
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
export function lazyComponent<C>(loader: () => Promise<{ default: C }>) {
|
||||||
|
let component = $state<C | null>(null);
|
||||||
|
let pending: Promise<void> | null = null;
|
||||||
|
|
||||||
|
return {
|
||||||
|
get component() {
|
||||||
|
return component;
|
||||||
|
},
|
||||||
|
/** Idempotent: kicks off the import once, resolves when the chunk is ready. */
|
||||||
|
load(): Promise<void> {
|
||||||
|
if (component) return Promise.resolve();
|
||||||
|
if (!pending) {
|
||||||
|
pending = loader().then((mod) => {
|
||||||
|
component = mod.default;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return pending;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -31,14 +31,16 @@ export class OwnerCache {
|
|||||||
|
|
||||||
/** Resolve every not-yet-cached id in parallel; nullish ids are skipped. */
|
/** Resolve every not-yet-cached id in parallel; nullish ids are skipped. */
|
||||||
async resolve(ids: Iterable<string | null | undefined>): Promise<void> {
|
async resolve(ids: Iterable<string | null | undefined>): Promise<void> {
|
||||||
const unique = [...new Set([...ids].filter((id): id is string => !!id))];
|
const pending = [...new Set([...ids].filter((id): id is string => !!id))].filter(
|
||||||
await Promise.all(
|
(id) => !this.#names[id]
|
||||||
unique.map(async (id) => {
|
|
||||||
if (this.#names[id]) return;
|
|
||||||
const name = await this.#resolver(id);
|
|
||||||
this.#names = { ...this.#names, [id]: name };
|
|
||||||
})
|
|
||||||
);
|
);
|
||||||
|
if (pending.length === 0) return;
|
||||||
|
const resolved = await Promise.all(
|
||||||
|
pending.map(async (id) => [id, await this.#resolver(id)] as const)
|
||||||
|
);
|
||||||
|
// One reactive assignment for the whole batch instead of one per id, so a
|
||||||
|
// large resolve doesn't spread-copy the record N times (and re-run derives N times).
|
||||||
|
this.#names = { ...this.#names, ...Object.fromEntries(resolved) };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -96,10 +96,15 @@ class FilesStore {
|
|||||||
this.selection = new Set();
|
this.selection = new Set();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Soft ceiling so the per-item toggle can't grow the set without bound.
|
||||||
|
// (Bulk "select all" lives in the views and intentionally isn't capped —
|
||||||
|
// silently dropping ids there would break batch delete/move.)
|
||||||
|
static readonly MAX_SELECTION = 10_000;
|
||||||
|
|
||||||
toggleSelected(id: string): void {
|
toggleSelected(id: string): void {
|
||||||
const next = new Set(this.selection);
|
const next = new Set(this.selection);
|
||||||
if (next.has(id)) next.delete(id);
|
if (next.has(id)) next.delete(id);
|
||||||
else next.add(id);
|
else if (next.size < FilesStore.MAX_SELECTION) next.add(id);
|
||||||
this.selection = next;
|
this.selection = next;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -320,7 +320,7 @@
|
|||||||
try {
|
try {
|
||||||
migration = await getMigration();
|
migration = await getMigration();
|
||||||
if (migration.status === 'running') {
|
if (migration.status === 'running') {
|
||||||
if (!migrationTimer) migrationTimer = setInterval(loadMigration, 2000);
|
if (!migrationTimer) migrationTimer = setInterval(loadMigration, 5000);
|
||||||
} else {
|
} else {
|
||||||
stopMigrationPoll();
|
stopMigrationPoll();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,7 +17,7 @@
|
|||||||
import { renameFile, deleteFile } from '$lib/api/endpoints/files';
|
import { renameFile, deleteFile } from '$lib/api/endpoints/files';
|
||||||
import { renameFolder, deleteFolder } from '$lib/api/endpoints/folders';
|
import { renameFolder, deleteFolder } from '$lib/api/endpoints/folders';
|
||||||
import type { FileItem } from '$lib/api/types';
|
import type { FileItem } from '$lib/api/types';
|
||||||
import FileViewer from '$lib/components/FileViewer.svelte';
|
import { lazyComponent } from '$lib/composables/lazyComponent.svelte';
|
||||||
import MoveDialog from '$lib/components/MoveDialog.svelte';
|
import MoveDialog from '$lib/components/MoveDialog.svelte';
|
||||||
import ShareDialog from '$lib/components/ShareDialog.svelte';
|
import ShareDialog from '$lib/components/ShareDialog.svelte';
|
||||||
import ResourceList, {
|
import ResourceList, {
|
||||||
@@ -123,6 +123,13 @@
|
|||||||
let viewerOpen = $state(false);
|
let viewerOpen = $state(false);
|
||||||
let viewerFile = $state<FileItem | null>(null);
|
let viewerFile = $state<FileItem | null>(null);
|
||||||
|
|
||||||
|
// The file preview is loaded the first time a file is opened, keeping its
|
||||||
|
// module out of this route's initial chunk.
|
||||||
|
const fileViewer = lazyComponent(() => import('$lib/components/FileViewer.svelte'));
|
||||||
|
$effect(() => {
|
||||||
|
if (viewerOpen) void fileViewer.load();
|
||||||
|
});
|
||||||
|
|
||||||
function open(entry: ResourceEntry) {
|
function open(entry: ResourceEntry) {
|
||||||
if (entry.kind === 'folder') {
|
if (entry.kind === 'folder') {
|
||||||
goto(`/files/${entry.id}`);
|
goto(`/files/${entry.id}`);
|
||||||
@@ -305,7 +312,10 @@
|
|||||||
{/snippet}
|
{/snippet}
|
||||||
</ResourceList>
|
</ResourceList>
|
||||||
|
|
||||||
<FileViewer bind:open={viewerOpen} file={viewerFile} />
|
{#if fileViewer.component}
|
||||||
|
{@const FileViewer = fileViewer.component}
|
||||||
|
<FileViewer bind:open={viewerOpen} file={viewerFile} />
|
||||||
|
{/if}
|
||||||
<MoveDialog
|
<MoveDialog
|
||||||
bind:open={moveOpen}
|
bind:open={moveOpen}
|
||||||
item={moveTarget}
|
item={moveTarget}
|
||||||
|
|||||||
@@ -37,12 +37,11 @@
|
|||||||
import { apiFetch } from '$lib/api/client';
|
import { apiFetch } from '$lib/api/client';
|
||||||
import { getCsrfHeaders } from '$lib/api/csrf';
|
import { getCsrfHeaders } from '$lib/api/csrf';
|
||||||
import type { FileItem, FolderItem, ItemType } from '$lib/api/types';
|
import type { FileItem, FolderItem, ItemType } from '$lib/api/types';
|
||||||
import FileViewer from '$lib/components/FileViewer.svelte';
|
|
||||||
import ListToolbar from '$lib/components/ListToolbar.svelte';
|
import ListToolbar from '$lib/components/ListToolbar.svelte';
|
||||||
import VirtualList from '$lib/components/VirtualList.svelte';
|
import VirtualList from '$lib/components/VirtualList.svelte';
|
||||||
import MoveDialog from '$lib/components/MoveDialog.svelte';
|
import MoveDialog from '$lib/components/MoveDialog.svelte';
|
||||||
import ShareDialog from '$lib/components/ShareDialog.svelte';
|
import ShareDialog from '$lib/components/ShareDialog.svelte';
|
||||||
import WopiEditor from '$lib/components/WopiEditor.svelte';
|
import { lazyComponent } from '$lib/composables/lazyComponent.svelte';
|
||||||
import { t } from '$lib/i18n/index.svelte';
|
import { t } from '$lib/i18n/index.svelte';
|
||||||
import { confirmDialog, promptDialog } from '$lib/stores/dialogs.svelte';
|
import { confirmDialog, promptDialog } from '$lib/stores/dialogs.svelte';
|
||||||
import { files as filesStore } from '$lib/stores/files.svelte';
|
import { files as filesStore } from '$lib/stores/files.svelte';
|
||||||
@@ -59,6 +58,12 @@
|
|||||||
import { formatDate, iconNameFromClass } from '$lib/utils/display';
|
import { formatDate, iconNameFromClass } from '$lib/utils/display';
|
||||||
import { gridColumns } from '$lib/utils/grid';
|
import { gridColumns } from '$lib/utils/grid';
|
||||||
|
|
||||||
|
// File preview and the WOPI editor are heavy and only appear on demand, so
|
||||||
|
// their modules load the first time the user opens one (see the effects that
|
||||||
|
// call `.load()` when `viewerOpen` / `wopiOpen` flip true).
|
||||||
|
const fileViewer = lazyComponent(() => import('$lib/components/FileViewer.svelte'));
|
||||||
|
const wopiEditor = lazyComponent(() => import('$lib/components/WopiEditor.svelte'));
|
||||||
|
|
||||||
// The URL rest param is the trail of folder ids from home's children down.
|
// The URL rest param is the trail of folder ids from home's children down.
|
||||||
// /files → home root; /files/a/b → folder b inside a inside home.
|
// /files → home root; /files/a/b → folder b inside a inside home.
|
||||||
const pathSegments = $derived((page.params.path ?? '').split('/').filter((s) => s.length > 0));
|
const pathSegments = $derived((page.params.path ?? '').split('/').filter((s) => s.length > 0));
|
||||||
@@ -806,6 +811,13 @@
|
|||||||
let wopiOpen = $state(false);
|
let wopiOpen = $state(false);
|
||||||
let wopiAction = $state<'edit' | 'view'>('edit');
|
let wopiAction = $state<'edit' | 'view'>('edit');
|
||||||
let wopiFile = $state<{ id: string; name: string } | null>(null);
|
let wopiFile = $state<{ id: string; name: string } | null>(null);
|
||||||
|
|
||||||
|
// Pull in the on-demand modules the moment they're first needed; after that
|
||||||
|
// the chunk is cached and the component stays mounted (controlled by `open`).
|
||||||
|
$effect(() => {
|
||||||
|
if (viewerOpen) void fileViewer.load();
|
||||||
|
if (wopiOpen) void wopiEditor.load();
|
||||||
|
});
|
||||||
// Editability of the current context-menu target file, resolved async.
|
// Editability of the current context-menu target file, resolved async.
|
||||||
let ctxCanEditWopi = $state(false);
|
let ctxCanEditWopi = $state(false);
|
||||||
|
|
||||||
@@ -1636,13 +1648,19 @@
|
|||||||
item={actionTarget}
|
item={actionTarget}
|
||||||
onshared={(id) => (sharedIds = new Set(sharedIds).add(id))}
|
onshared={(id) => (sharedIds = new Set(sharedIds).add(id))}
|
||||||
/>
|
/>
|
||||||
<FileViewer bind:open={viewerOpen} file={viewerFile} />
|
{#if fileViewer.component}
|
||||||
<WopiEditor
|
{@const FileViewer = fileViewer.component}
|
||||||
|
<FileViewer bind:open={viewerOpen} file={viewerFile} />
|
||||||
|
{/if}
|
||||||
|
{#if wopiEditor.component}
|
||||||
|
{@const WopiEditor = wopiEditor.component}
|
||||||
|
<WopiEditor
|
||||||
bind:open={wopiOpen}
|
bind:open={wopiOpen}
|
||||||
fileId={wopiFile?.id ?? null}
|
fileId={wopiFile?.id ?? null}
|
||||||
fileName={wopiFile?.name ?? ''}
|
fileName={wopiFile?.name ?? ''}
|
||||||
action={wopiAction}
|
action={wopiAction}
|
||||||
/>
|
/>
|
||||||
|
{/if}
|
||||||
|
|
||||||
{#if ctxOpen && ctxTarget}
|
{#if ctxOpen && ctxTarget}
|
||||||
<div
|
<div
|
||||||
|
|||||||
@@ -1,10 +1,8 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import Button from '$lib/components/Button.svelte';
|
import Button from '$lib/components/Button.svelte';
|
||||||
import EmptyState from '$lib/components/EmptyState.svelte';
|
import EmptyState from '$lib/components/EmptyState.svelte';
|
||||||
import PeopleView from '$lib/components/PeopleView.svelte';
|
|
||||||
import PhotoLightbox from '$lib/components/PhotoLightbox.svelte';
|
|
||||||
import PlacesMap from '$lib/components/PlacesMap.svelte';
|
|
||||||
import VirtualRows from '$lib/components/VirtualRows.svelte';
|
import VirtualRows from '$lib/components/VirtualRows.svelte';
|
||||||
|
import { lazyComponent } from '$lib/composables/lazyComponent.svelte';
|
||||||
import { useSelection } from '$lib/composables/useSelection.svelte';
|
import { useSelection } from '$lib/composables/useSelection.svelte';
|
||||||
import { errorToast } from '$lib/utils/errors';
|
import { errorToast } from '$lib/utils/errors';
|
||||||
import { onMount } from 'svelte';
|
import { onMount } from 'svelte';
|
||||||
@@ -24,6 +22,13 @@
|
|||||||
|
|
||||||
type Tab = 'moments' | 'places' | 'people';
|
type Tab = 'moments' | 'places' | 'people';
|
||||||
let tab = $state<Tab>('moments');
|
let tab = $state<Tab>('moments');
|
||||||
|
|
||||||
|
// The lightbox, the (maplibre-backed) places map and the people view are all
|
||||||
|
// heavy and off the initial path, so each loads on first use: the lightbox
|
||||||
|
// when a photo is opened, the map/people views when their tab is selected.
|
||||||
|
const photoLightbox = lazyComponent(() => import('$lib/components/PhotoLightbox.svelte'));
|
||||||
|
const placesMap = lazyComponent(() => import('$lib/components/PlacesMap.svelte'));
|
||||||
|
const peopleView = lazyComponent(() => import('$lib/components/PeopleView.svelte'));
|
||||||
let peopleAvailable = $state(false);
|
let peopleAvailable = $state(false);
|
||||||
|
|
||||||
let items = $state<PhotoItem[]>([]);
|
let items = $state<PhotoItem[]>([]);
|
||||||
@@ -44,6 +49,12 @@
|
|||||||
const selected = useSelection();
|
const selected = useSelection();
|
||||||
let lightbox = $state(-1); // index into `items`, -1 = closed
|
let lightbox = $state(-1); // index into `items`, -1 = closed
|
||||||
|
|
||||||
|
$effect(() => {
|
||||||
|
if (lightbox >= 0) void photoLightbox.load();
|
||||||
|
if (tab === 'places') void placesMap.load();
|
||||||
|
else if (tab === 'people') void peopleView.load();
|
||||||
|
});
|
||||||
|
|
||||||
/** Client-generated video frame thumbnails (file id → data/URL). */
|
/** Client-generated video frame thumbnails (file id → data/URL). */
|
||||||
let videoThumbs = $state<Record<string, string>>({});
|
let videoThumbs = $state<Record<string, string>>({});
|
||||||
|
|
||||||
@@ -290,11 +301,16 @@
|
|||||||
['large', 800, 800]
|
['large', 800, 800]
|
||||||
];
|
];
|
||||||
let previewData = '';
|
let previewData = '';
|
||||||
for (const [size, w, h] of SIZES) {
|
// Render the blobs and push all three sizes in parallel; `previewData`
|
||||||
|
// is captured before its upload so the local preview shows even if that
|
||||||
|
// upload fails (allSettled swallows per-size failures, as before).
|
||||||
|
await Promise.allSettled(
|
||||||
|
SIZES.map(async ([size, w, h]) => {
|
||||||
const blob = await bitmapToBlob(bitmap, w, h);
|
const blob = await bitmapToBlob(bitmap, w, h);
|
||||||
if (size === 'preview') previewData = await blobToDataUrl(blob);
|
if (size === 'preview') previewData = await blobToDataUrl(blob);
|
||||||
await uploadThumbnail(file.id, size, blob).catch(() => {});
|
await uploadThumbnail(file.id, size, blob);
|
||||||
}
|
})
|
||||||
|
);
|
||||||
if (previewData) videoThumbs = { ...videoThumbs, [file.id]: previewData };
|
if (previewData) videoThumbs = { ...videoThumbs, [file.id]: previewData };
|
||||||
} catch {
|
} catch {
|
||||||
// Keep the generic play badge on failure.
|
// Keep the generic play badge on failure.
|
||||||
@@ -487,11 +503,20 @@
|
|||||||
<div bind:this={sentinel} class="sentinel" aria-hidden="true"></div>
|
<div bind:this={sentinel} class="sentinel" aria-hidden="true"></div>
|
||||||
{#if loading}<p class="status">{t('common.loading', 'Loading…')}</p>{/if}
|
{#if loading}<p class="status">{t('common.loading', 'Loading…')}</p>{/if}
|
||||||
|
|
||||||
|
{#if photoLightbox.component}
|
||||||
|
{@const PhotoLightbox = photoLightbox.component}
|
||||||
<PhotoLightbox {items} bind:index={lightbox} onDelete={onDeletePhoto} />
|
<PhotoLightbox {items} bind:index={lightbox} onDelete={onDeletePhoto} />
|
||||||
|
{/if}
|
||||||
{:else if tab === 'places'}
|
{:else if tab === 'places'}
|
||||||
|
{#if placesMap.component}
|
||||||
|
{@const PlacesMap = placesMap.component}
|
||||||
<PlacesMap />
|
<PlacesMap />
|
||||||
|
{/if}
|
||||||
{:else if tab === 'people'}
|
{:else if tab === 'people'}
|
||||||
|
{#if peopleView.component}
|
||||||
|
{@const PeopleView = peopleView.component}
|
||||||
<PeopleView />
|
<PeopleView />
|
||||||
|
{/if}
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
{#snippet tile(photo: PhotoItem, sizeStyle?: string)}
|
{#snippet tile(photo: PhotoItem, sizeStyle?: string)}
|
||||||
|
|||||||
@@ -17,7 +17,7 @@
|
|||||||
import { fileDownloadUrl, renameFile, deleteFile } from '$lib/api/endpoints/files';
|
import { fileDownloadUrl, renameFile, deleteFile } from '$lib/api/endpoints/files';
|
||||||
import { renameFolder, deleteFolder } from '$lib/api/endpoints/folders';
|
import { renameFolder, deleteFolder } from '$lib/api/endpoints/folders';
|
||||||
import type { FileItem, ItemType } from '$lib/api/types';
|
import type { FileItem, ItemType } from '$lib/api/types';
|
||||||
import FileViewer from '$lib/components/FileViewer.svelte';
|
import { lazyComponent } from '$lib/composables/lazyComponent.svelte';
|
||||||
import MoveDialog from '$lib/components/MoveDialog.svelte';
|
import MoveDialog from '$lib/components/MoveDialog.svelte';
|
||||||
import ShareDialog from '$lib/components/ShareDialog.svelte';
|
import ShareDialog from '$lib/components/ShareDialog.svelte';
|
||||||
import ResourceList, {
|
import ResourceList, {
|
||||||
@@ -134,6 +134,13 @@
|
|||||||
let viewerOpen = $state(false);
|
let viewerOpen = $state(false);
|
||||||
let viewerFile = $state<FileItem | null>(null);
|
let viewerFile = $state<FileItem | null>(null);
|
||||||
|
|
||||||
|
// The file preview is loaded the first time a file is opened, keeping its
|
||||||
|
// module out of this route's initial chunk.
|
||||||
|
const fileViewer = lazyComponent(() => import('$lib/components/FileViewer.svelte'));
|
||||||
|
$effect(() => {
|
||||||
|
if (viewerOpen) void fileViewer.load();
|
||||||
|
});
|
||||||
|
|
||||||
function open(entry: ResourceEntry) {
|
function open(entry: ResourceEntry) {
|
||||||
if (entry.kind === 'folder') {
|
if (entry.kind === 'folder') {
|
||||||
goto(`/files/${entry.id}`);
|
goto(`/files/${entry.id}`);
|
||||||
@@ -349,7 +356,10 @@
|
|||||||
{/snippet}
|
{/snippet}
|
||||||
</ResourceList>
|
</ResourceList>
|
||||||
|
|
||||||
<FileViewer bind:open={viewerOpen} file={viewerFile} />
|
{#if fileViewer.component}
|
||||||
|
{@const FileViewer = fileViewer.component}
|
||||||
|
<FileViewer bind:open={viewerOpen} file={viewerFile} />
|
||||||
|
{/if}
|
||||||
<MoveDialog
|
<MoveDialog
|
||||||
bind:open={moveOpen}
|
bind:open={moveOpen}
|
||||||
item={moveTarget}
|
item={moveTarget}
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
import { onMount } from 'svelte';
|
import { onMount } from 'svelte';
|
||||||
import { fetchSharedWithMe, type IncomingGrantItem } from '$lib/api/endpoints/grants';
|
import { fetchSharedWithMe, type IncomingGrantItem } from '$lib/api/endpoints/grants';
|
||||||
import type { FileItem } from '$lib/api/types';
|
import type { FileItem } from '$lib/api/types';
|
||||||
import FileViewer from '$lib/components/FileViewer.svelte';
|
import { lazyComponent } from '$lib/composables/lazyComponent.svelte';
|
||||||
import ResourceList, { type ResourceEntry } from '$lib/components/ResourceList.svelte';
|
import ResourceList, { type ResourceEntry } from '$lib/components/ResourceList.svelte';
|
||||||
import { t } from '$lib/i18n/index.svelte';
|
import { t } from '$lib/i18n/index.svelte';
|
||||||
|
|
||||||
@@ -48,6 +48,13 @@
|
|||||||
let viewerOpen = $state(false);
|
let viewerOpen = $state(false);
|
||||||
let viewerFile = $state<FileItem | null>(null);
|
let viewerFile = $state<FileItem | null>(null);
|
||||||
|
|
||||||
|
// The file preview is loaded the first time a file is opened, keeping its
|
||||||
|
// module out of this route's initial chunk.
|
||||||
|
const fileViewer = lazyComponent(() => import('$lib/components/FileViewer.svelte'));
|
||||||
|
$effect(() => {
|
||||||
|
if (viewerOpen) void fileViewer.load();
|
||||||
|
});
|
||||||
|
|
||||||
function open(entry: ResourceEntry) {
|
function open(entry: ResourceEntry) {
|
||||||
if (entry.kind === 'folder') {
|
if (entry.kind === 'folder') {
|
||||||
goto(`/files/${entry.id}`);
|
goto(`/files/${entry.id}`);
|
||||||
@@ -76,4 +83,7 @@
|
|||||||
onopen={open}
|
onopen={open}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<FileViewer bind:open={viewerOpen} file={viewerFile} />
|
{#if fileViewer.component}
|
||||||
|
{@const FileViewer = fileViewer.component}
|
||||||
|
<FileViewer bind:open={viewerOpen} file={viewerFile} />
|
||||||
|
{/if}
|
||||||
|
|||||||
Reference in New Issue
Block a user