mirror of
https://github.com/kmc7468/arkvault.git
synced 2026-02-04 16:16:55 +00:00
Compare commits
1 Commits
v0.6.0
...
174305ca1b
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
174305ca1b |
46
src/lib/components/atoms/RowVirtualizer.svelte
Normal file
46
src/lib/components/atoms/RowVirtualizer.svelte
Normal file
@@ -0,0 +1,46 @@
|
||||
<script lang="ts">
|
||||
import { createWindowVirtualizer } from "@tanstack/svelte-virtual";
|
||||
import { untrack, type Snippet } from "svelte";
|
||||
import type { ClassValue } from "svelte/elements";
|
||||
|
||||
interface Props {
|
||||
class?: ClassValue;
|
||||
count: number;
|
||||
item: Snippet<[index: number]>;
|
||||
itemHeight: (index: number) => number;
|
||||
placeholder?: Snippet;
|
||||
}
|
||||
|
||||
let { class: className, count, item, itemHeight, placeholder }: Props = $props();
|
||||
|
||||
const virtualizer = $derived(
|
||||
createWindowVirtualizer({
|
||||
count: untrack(() => count),
|
||||
estimateSize: itemHeight,
|
||||
}),
|
||||
);
|
||||
|
||||
const measureItem = (node: HTMLElement) => {
|
||||
$effect(() => $virtualizer.measureElement(node));
|
||||
};
|
||||
|
||||
$effect(() => $virtualizer.setOptions({ count }));
|
||||
</script>
|
||||
|
||||
<div class={["relative", className]}>
|
||||
<div style:height="{$virtualizer.getTotalSize()}px">
|
||||
{#each $virtualizer.getVirtualItems() as virtualItem (virtualItem.key)}
|
||||
<div
|
||||
class="absolute left-0 top-0 w-full"
|
||||
style:transform="translateY({virtualItem.start}px)"
|
||||
data-index={virtualItem.index}
|
||||
use:measureItem
|
||||
>
|
||||
{@render item(virtualItem.index)}
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
{#if placeholder && $virtualizer.getVirtualItems().length === 0}
|
||||
{@render placeholder()}
|
||||
{/if}
|
||||
</div>
|
||||
@@ -3,3 +3,4 @@ export * from "./buttons";
|
||||
export * from "./divs";
|
||||
export * from "./inputs";
|
||||
export { default as Modal } from "./Modal.svelte";
|
||||
export { default as RowVirtualizer } from "./RowVirtualizer.svelte";
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<script lang="ts">
|
||||
import { untrack } from "svelte";
|
||||
import { get, type Writable } from "svelte/store";
|
||||
import { CheckBox } from "$lib/components/atoms";
|
||||
import { CheckBox, RowVirtualizer } from "$lib/components/atoms";
|
||||
import { SubCategories, type SelectedCategory } from "$lib/components/molecules";
|
||||
import { getFileInfo, type FileInfo, type CategoryInfo } from "$lib/modules/filesystem";
|
||||
import { masterKeyStore } from "$lib/stores";
|
||||
@@ -89,19 +89,26 @@
|
||||
<p class="font-medium">하위 카테고리의 파일</p>
|
||||
</CheckBox>
|
||||
</div>
|
||||
<div class="space-y-1">
|
||||
{#key info}
|
||||
{#each files as { info, isRecursive }}
|
||||
<File
|
||||
{info}
|
||||
onclick={onFileClick}
|
||||
onRemoveClick={!isRecursive ? onFileRemoveClick : undefined}
|
||||
/>
|
||||
{:else}
|
||||
<p class="text-gray-500 text-center">이 카테고리에 추가된 파일이 없어요.</p>
|
||||
{/each}
|
||||
{/key}
|
||||
</div>
|
||||
{#key info}
|
||||
<RowVirtualizer
|
||||
count={files.length}
|
||||
itemHeight={(index) => 56 + (index + 1 < files.length ? 4 : 0)}
|
||||
>
|
||||
{#snippet item(index)}
|
||||
{@const { info, isRecursive } = files[index]!}
|
||||
<div class={[index + 1 < files.length && "pb-1"]}>
|
||||
<File
|
||||
{info}
|
||||
onclick={onFileClick}
|
||||
onRemoveClick={!isRecursive ? onFileRemoveClick : undefined}
|
||||
/>
|
||||
</div>
|
||||
{/snippet}
|
||||
{#snippet placeholder()}
|
||||
<p class="text-center text-gray-500">이 카테고리에 추가된 파일이 없어요.</p>
|
||||
{/snippet}
|
||||
</RowVirtualizer>
|
||||
{/key}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
<script lang="ts">
|
||||
import { createWindowVirtualizer } from "@tanstack/svelte-virtual";
|
||||
import { untrack } from "svelte";
|
||||
import { get, type Writable } from "svelte/store";
|
||||
import { FileThumbnailButton } from "$lib/components/atoms";
|
||||
import { FileThumbnailButton, RowVirtualizer } from "$lib/components/atoms";
|
||||
import type { FileInfo } from "$lib/modules/filesystem";
|
||||
import { formatDate, formatDateSortable, SortBy, sortEntries } from "$lib/utils";
|
||||
|
||||
@@ -17,25 +16,11 @@
|
||||
| { date?: undefined; contentType?: undefined; info: Writable<FileInfo | null> }
|
||||
| { date: Date; contentType: string; info: Writable<FileInfo | null> };
|
||||
type Row =
|
||||
| { type: "header"; key: string; label: string }
|
||||
| { type: "items"; key: string; items: FileEntry[] };
|
||||
| { type: "header"; label: string }
|
||||
| { type: "items"; items: FileEntry[]; isLast: boolean };
|
||||
|
||||
let filesWithDate: FileEntry[] = $state([]);
|
||||
let rows: Row[] = $state([]);
|
||||
let listElement: HTMLDivElement | undefined = $state();
|
||||
|
||||
const virtualizer = createWindowVirtualizer({
|
||||
count: 0,
|
||||
getItemKey: (index) => rows[index]!.key,
|
||||
estimateSize: () => 1000, // TODO
|
||||
});
|
||||
|
||||
const measureRow = (node: HTMLElement) => {
|
||||
$virtualizer.measureElement(node);
|
||||
return {
|
||||
update: () => $virtualizer.measureElement(node),
|
||||
};
|
||||
};
|
||||
|
||||
$effect(() => {
|
||||
filesWithDate = files.map((file) => {
|
||||
@@ -76,18 +61,19 @@
|
||||
|
||||
newRows.push({
|
||||
type: "header",
|
||||
key: `header-${date}`,
|
||||
label: formatDate(entries[0]!.date!),
|
||||
});
|
||||
newRows.push({
|
||||
type: "items",
|
||||
key: `items-${date}`,
|
||||
items: entries,
|
||||
});
|
||||
|
||||
for (let i = 0; i < entries.length; i += 4) {
|
||||
newRows.push({
|
||||
type: "items",
|
||||
items: entries.slice(i, i + 4),
|
||||
isLast: i + 4 >= entries.length,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
rows = newRows;
|
||||
$virtualizer.setOptions({ count: rows.length });
|
||||
};
|
||||
return untrack(() => {
|
||||
buildRows();
|
||||
@@ -110,29 +96,29 @@
|
||||
});
|
||||
</script>
|
||||
|
||||
<div bind:this={listElement} class="relative flex flex-grow flex-col">
|
||||
<div style="height: {$virtualizer.getTotalSize()}px;">
|
||||
{#each $virtualizer.getVirtualItems() as virtualRow (virtualRow.key)}
|
||||
{@const row = rows[virtualRow.index]!}
|
||||
<div
|
||||
use:measureRow
|
||||
data-index={virtualRow.index}
|
||||
class="absolute left-0 top-0 w-full"
|
||||
style="transform: translateY({virtualRow.start}px);"
|
||||
>
|
||||
{#if row.type === "header"}
|
||||
<p class="pb-2 font-medium">{row.label}</p>
|
||||
{:else}
|
||||
<div class="grid grid-cols-4 gap-1 pb-4">
|
||||
{#each row.items as { info }}
|
||||
<FileThumbnailButton {info} onclick={onFileClick} />
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
<RowVirtualizer
|
||||
count={rows.length}
|
||||
itemHeight={(index) =>
|
||||
rows[index]!.type === "header"
|
||||
? 32
|
||||
: Math.ceil(rows[index]!.items.length / 4) * 181 +
|
||||
(Math.ceil(rows[index]!.items.length / 4) - 1) * 4 +
|
||||
16}
|
||||
class="flex flex-grow flex-col"
|
||||
>
|
||||
{#snippet item(index)}
|
||||
{@const row = rows[index]!}
|
||||
{#if row.type === "header"}
|
||||
<p class="pb-2 font-medium">{row.label}</p>
|
||||
{:else}
|
||||
<div class={["grid grid-cols-4 gap-x-1", row.isLast ? "pb-4" : "pb-1"]}>
|
||||
{#each row.items as { info }}
|
||||
<FileThumbnailButton {info} onclick={onFileClick} />
|
||||
{/each}
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
{#if $virtualizer.getVirtualItems().length === 0}
|
||||
{/if}
|
||||
{/snippet}
|
||||
{#snippet placeholder()}
|
||||
<div class="flex h-full flex-grow items-center justify-center">
|
||||
<p class="text-gray-500">
|
||||
{#if files.length === 0}
|
||||
@@ -144,5 +130,5 @@
|
||||
{/if}
|
||||
</p>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
{/snippet}
|
||||
</RowVirtualizer>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<script lang="ts">
|
||||
import { untrack } from "svelte";
|
||||
import { get, type Writable } from "svelte/store";
|
||||
import { ActionEntryButton } from "$lib/components/atoms";
|
||||
import { ActionEntryButton, RowVirtualizer } from "$lib/components/atoms";
|
||||
import { DirectoryEntryLabel } from "$lib/components/molecules";
|
||||
import {
|
||||
getDirectoryInfo,
|
||||
@@ -127,13 +127,23 @@
|
||||
{#each subDirectories as { info }}
|
||||
<SubDirectory {info} onclick={onEntryClick} onOpenMenuClick={onEntryMenuClick} />
|
||||
{/each}
|
||||
{#each files as file}
|
||||
{#if file.type === "file"}
|
||||
<File info={file.info} onclick={onEntryClick} onOpenMenuClick={onEntryMenuClick} />
|
||||
{:else}
|
||||
<UploadingFile status={file.info} />
|
||||
{/if}
|
||||
{/each}
|
||||
{#if files.length > 0}
|
||||
<RowVirtualizer
|
||||
count={files.length}
|
||||
itemHeight={(index) => 48 + (index + 1 < files.length ? 4 : 0)}
|
||||
>
|
||||
{#snippet item(index)}
|
||||
{@const file = files[index]!}
|
||||
<div class={index + 1 < files.length ? "pb-1" : ""}>
|
||||
{#if file.type === "file"}
|
||||
<File info={file.info} onclick={onEntryClick} onOpenMenuClick={onEntryMenuClick} />
|
||||
{:else}
|
||||
<UploadingFile status={file.info} />
|
||||
{/if}
|
||||
</div>
|
||||
{/snippet}
|
||||
</RowVirtualizer>
|
||||
{/if}
|
||||
</div>
|
||||
{:else}
|
||||
<div class="flex flex-grow items-center justify-center">
|
||||
|
||||
Reference in New Issue
Block a user