mirror of
https://github.com/kmc7468/arkvault.git
synced 2026-02-04 08:06:56 +00:00
Compare commits
2 Commits
d98be331ad
...
d1f9018213
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d1f9018213 | ||
|
|
2e3cd4f8a2 |
@@ -8,10 +8,11 @@
|
||||
count: number;
|
||||
item: Snippet<[index: number]>;
|
||||
itemHeight: (index: number) => number;
|
||||
itemGap?: number;
|
||||
placeholder?: Snippet;
|
||||
}
|
||||
|
||||
let { class: className, count, item, itemHeight, placeholder }: Props = $props();
|
||||
let { class: className, count, item, itemHeight, itemGap, placeholder }: Props = $props();
|
||||
|
||||
let element: HTMLElement | undefined = $state();
|
||||
let scrollMargin = $state(0);
|
||||
@@ -20,6 +21,7 @@
|
||||
createWindowVirtualizer({
|
||||
count,
|
||||
estimateSize: itemHeight,
|
||||
gap: itemGap,
|
||||
scrollMargin,
|
||||
}),
|
||||
);
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
<script lang="ts">
|
||||
import { CheckBox, RowVirtualizer } from "$lib/components/atoms";
|
||||
import { SubCategories, type SelectedCategory } from "$lib/components/molecules";
|
||||
import { updateCategoryInfo } from "$lib/indexedDB";
|
||||
import type { CategoryInfo } from "$lib/modules/filesystem";
|
||||
import { sortEntries } from "$lib/utils";
|
||||
import File from "./File.svelte";
|
||||
@@ -28,6 +29,9 @@
|
||||
isFileRecursive = $bindable(),
|
||||
}: Props = $props();
|
||||
|
||||
let lastCategoryId = $state<CategoryInfo["id"] | undefined>();
|
||||
let lastIsFileRecursive = $state<boolean | undefined>();
|
||||
|
||||
let files = $derived(
|
||||
sortEntries(
|
||||
info.files
|
||||
@@ -35,6 +39,19 @@
|
||||
.filter(({ details }) => isFileRecursive || !details.isRecursive) ?? [],
|
||||
),
|
||||
);
|
||||
|
||||
$effect(() => {
|
||||
if (info.id === "root" || isFileRecursive === undefined) return;
|
||||
if (lastCategoryId !== info.id) {
|
||||
lastCategoryId = info.id;
|
||||
lastIsFileRecursive = isFileRecursive;
|
||||
return;
|
||||
}
|
||||
if (lastIsFileRecursive === isFileRecursive) return;
|
||||
|
||||
lastIsFileRecursive = isFileRecursive;
|
||||
void updateCategoryInfo(info.id, { isFileRecursive });
|
||||
});
|
||||
</script>
|
||||
|
||||
<div class="space-y-4">
|
||||
@@ -58,19 +75,14 @@
|
||||
<p class="font-medium">하위 카테고리의 파일</p>
|
||||
</CheckBox>
|
||||
</div>
|
||||
<RowVirtualizer
|
||||
count={files.length}
|
||||
itemHeight={(index) => 48 + (index + 1 < files.length ? 4 : 0)}
|
||||
>
|
||||
<RowVirtualizer count={files.length} itemHeight={() => 48} itemGap={4}>
|
||||
{#snippet item(index)}
|
||||
{@const { details } = files[index]!}
|
||||
<div class={[index + 1 < files.length && "pb-1"]}>
|
||||
<File
|
||||
info={details}
|
||||
onclick={onFileClick}
|
||||
onRemoveClick={!details.isRecursive ? onFileRemoveClick : undefined}
|
||||
/>
|
||||
</div>
|
||||
<File
|
||||
info={details}
|
||||
onclick={onFileClick}
|
||||
onRemoveClick={!details.isRecursive ? onFileRemoveClick : undefined}
|
||||
/>
|
||||
{/snippet}
|
||||
{#snippet placeholder()}
|
||||
<p class="text-center text-gray-500">이 카테고리에 추가된 파일이 없어요.</p>
|
||||
|
||||
@@ -46,7 +46,7 @@ const isFileUploading = (status: FileUploadState["status"]) =>
|
||||
|
||||
export const getUploadingFiles = (parentId?: DirectoryId) => {
|
||||
return uploadingFiles.filter(
|
||||
(file): file is LiveFileUploadState =>
|
||||
(file) =>
|
||||
(parentId === undefined || file.parentId === parentId) && isFileUploading(file.status),
|
||||
);
|
||||
};
|
||||
|
||||
@@ -52,25 +52,76 @@ const fetchFromServer = async (id: CategoryId, masterKey: CryptoKey) => {
|
||||
metadata,
|
||||
subCategories: subCategoriesRaw,
|
||||
files: filesRaw,
|
||||
} = await trpc().category.get.query({ id });
|
||||
const [subCategories, files] = await Promise.all([
|
||||
Promise.all(
|
||||
subCategoriesRaw.map(async (category) => ({
|
||||
} = await trpc().category.get.query({ id, recurse: true });
|
||||
const subCategories = await Promise.all(
|
||||
subCategoriesRaw.map(async (category) => {
|
||||
const decrypted = await decryptCategoryMetadata(category, masterKey);
|
||||
const existing = await IndexedDB.getCategoryInfo(category.id);
|
||||
await IndexedDB.storeCategoryInfo({
|
||||
id: category.id,
|
||||
...(await decryptCategoryMetadata(category, masterKey)),
|
||||
})),
|
||||
),
|
||||
filesRaw
|
||||
? Promise.all(
|
||||
filesRaw.map(async (file) => ({
|
||||
parentId: id,
|
||||
name: decrypted.name,
|
||||
files: existing?.files ?? [],
|
||||
isFileRecursive: existing?.isFileRecursive ?? false,
|
||||
});
|
||||
return {
|
||||
id: category.id,
|
||||
...decrypted,
|
||||
};
|
||||
}),
|
||||
);
|
||||
|
||||
const existingFiles = filesRaw
|
||||
? await IndexedDB.bulkGetFileInfos(filesRaw.map((file) => file.id))
|
||||
: [];
|
||||
const files = filesRaw
|
||||
? await Promise.all(
|
||||
filesRaw.map(async (file, index) => {
|
||||
const decrypted = await decryptFileMetadata(file, masterKey);
|
||||
const existing = existingFiles[index];
|
||||
if (existing) {
|
||||
const categoryIds = file.isRecursive
|
||||
? existing.categoryIds
|
||||
: Array.from(new Set([...existing.categoryIds, id as number]));
|
||||
await IndexedDB.storeFileInfo({
|
||||
id: file.id,
|
||||
parentId: existing.parentId,
|
||||
contentType: file.contentType,
|
||||
name: decrypted.name,
|
||||
createdAt: decrypted.createdAt,
|
||||
lastModifiedAt: decrypted.lastModifiedAt,
|
||||
categoryIds,
|
||||
});
|
||||
}
|
||||
return {
|
||||
id: file.id,
|
||||
contentType: file.contentType,
|
||||
isRecursive: file.isRecursive,
|
||||
...(await decryptFileMetadata(file, masterKey)),
|
||||
})),
|
||||
)
|
||||
: undefined,
|
||||
]);
|
||||
...decrypted,
|
||||
};
|
||||
}),
|
||||
)
|
||||
: undefined;
|
||||
|
||||
const decryptedMetadata = metadata
|
||||
? await decryptCategoryMetadata(metadata, masterKey)
|
||||
: undefined;
|
||||
if (id !== "root" && metadata && decryptedMetadata) {
|
||||
const existingCategory = await IndexedDB.getCategoryInfo(id);
|
||||
await IndexedDB.storeCategoryInfo({
|
||||
id: id as number,
|
||||
parentId: metadata.parent,
|
||||
name: decryptedMetadata.name,
|
||||
files:
|
||||
files?.map((file) => ({
|
||||
id: file.id,
|
||||
isRecursive: file.isRecursive,
|
||||
})) ??
|
||||
existingCategory?.files ??
|
||||
[],
|
||||
isFileRecursive: existingCategory?.isFileRecursive ?? false,
|
||||
});
|
||||
}
|
||||
|
||||
if (id === "root") {
|
||||
return {
|
||||
@@ -84,7 +135,7 @@ const fetchFromServer = async (id: CategoryId, masterKey: CryptoKey) => {
|
||||
exists: true as const,
|
||||
subCategories,
|
||||
files,
|
||||
...(await decryptCategoryMetadata(metadata!, masterKey)),
|
||||
...decryptedMetadata!,
|
||||
};
|
||||
}
|
||||
} catch (e) {
|
||||
|
||||
@@ -39,22 +39,52 @@ const fetchFromServer = async (id: DirectoryId, masterKey: CryptoKey) => {
|
||||
subDirectories: subDirectoriesRaw,
|
||||
files: filesRaw,
|
||||
} = await trpc().directory.get.query({ id });
|
||||
const [subDirectories, files] = await Promise.all([
|
||||
const existingFiles = await IndexedDB.bulkGetFileInfos(filesRaw.map((file) => file.id));
|
||||
const [subDirectories, files, decryptedMetadata] = await Promise.all([
|
||||
Promise.all(
|
||||
subDirectoriesRaw.map(async (directory) => ({
|
||||
id: directory.id,
|
||||
...(await decryptDirectoryMetadata(directory, masterKey)),
|
||||
})),
|
||||
subDirectoriesRaw.map(async (directory) => {
|
||||
const decrypted = await decryptDirectoryMetadata(directory, masterKey);
|
||||
await IndexedDB.storeDirectoryInfo({
|
||||
id: directory.id,
|
||||
parentId: id,
|
||||
name: decrypted.name,
|
||||
});
|
||||
return {
|
||||
id: directory.id,
|
||||
...decrypted,
|
||||
};
|
||||
}),
|
||||
),
|
||||
Promise.all(
|
||||
filesRaw.map(async (file) => ({
|
||||
id: file.id,
|
||||
contentType: file.contentType,
|
||||
...(await decryptFileMetadata(file, masterKey)),
|
||||
})),
|
||||
filesRaw.map(async (file, index) => {
|
||||
const decrypted = await decryptFileMetadata(file, masterKey);
|
||||
await IndexedDB.storeFileInfo({
|
||||
id: file.id,
|
||||
parentId: id,
|
||||
contentType: file.contentType,
|
||||
name: decrypted.name,
|
||||
createdAt: decrypted.createdAt,
|
||||
lastModifiedAt: decrypted.lastModifiedAt,
|
||||
categoryIds: existingFiles[index]?.categoryIds ?? [],
|
||||
});
|
||||
return {
|
||||
id: file.id,
|
||||
contentType: file.contentType,
|
||||
...decrypted,
|
||||
};
|
||||
}),
|
||||
),
|
||||
metadata ? decryptDirectoryMetadata(metadata, masterKey) : undefined,
|
||||
]);
|
||||
|
||||
if (id !== "root" && metadata && decryptedMetadata) {
|
||||
await IndexedDB.storeDirectoryInfo({
|
||||
id,
|
||||
parentId: metadata.parent,
|
||||
name: decryptedMetadata.name,
|
||||
});
|
||||
}
|
||||
|
||||
if (id === "root") {
|
||||
return {
|
||||
id,
|
||||
@@ -69,7 +99,7 @@ const fetchFromServer = async (id: DirectoryId, masterKey: CryptoKey) => {
|
||||
parentId: metadata!.parent,
|
||||
subDirectories,
|
||||
files,
|
||||
...(await decryptDirectoryMetadata(metadata!, masterKey)),
|
||||
...decryptedMetadata!,
|
||||
};
|
||||
}
|
||||
} catch (e) {
|
||||
|
||||
@@ -66,15 +66,26 @@ const bulkFetchFromIndexedDB = async (ids: number[]) => {
|
||||
const fetchFromServer = async (id: number, masterKey: CryptoKey) => {
|
||||
try {
|
||||
const { categories: categoriesRaw, ...metadata } = await trpc().file.get.query({ id });
|
||||
const [categories] = await Promise.all([
|
||||
const [categories, decryptedMetadata] = await Promise.all([
|
||||
Promise.all(
|
||||
categoriesRaw.map(async (category) => ({
|
||||
id: category.id,
|
||||
...(await decryptCategoryMetadata(category, masterKey)),
|
||||
})),
|
||||
),
|
||||
decryptFileMetadata(metadata, masterKey),
|
||||
]);
|
||||
|
||||
await IndexedDB.storeFileInfo({
|
||||
id,
|
||||
parentId: metadata.parent,
|
||||
contentType: metadata.contentType,
|
||||
name: decryptedMetadata.name,
|
||||
createdAt: decryptedMetadata.createdAt,
|
||||
lastModifiedAt: decryptedMetadata.lastModifiedAt,
|
||||
categoryIds: categories.map((category) => category.id),
|
||||
});
|
||||
|
||||
return {
|
||||
id,
|
||||
exists: true as const,
|
||||
@@ -82,7 +93,7 @@ const fetchFromServer = async (id: number, masterKey: CryptoKey) => {
|
||||
contentType: metadata.contentType,
|
||||
contentIv: metadata.contentIv,
|
||||
categories,
|
||||
...(await decryptFileMetadata(metadata, masterKey)),
|
||||
...decryptedMetadata,
|
||||
};
|
||||
} catch (e) {
|
||||
if (isTRPCClientError(e) && e.data?.code === "NOT_FOUND") {
|
||||
@@ -97,12 +108,25 @@ const bulkFetchFromServer = async (ids: number[], masterKey: CryptoKey) => {
|
||||
const filesRaw = await trpc().file.bulkGet.query({ ids });
|
||||
const files = await Promise.all(
|
||||
filesRaw.map(async (file) => {
|
||||
const categories = await Promise.all(
|
||||
file.categories.map(async (category) => ({
|
||||
id: category.id,
|
||||
...(await decryptCategoryMetadata(category, masterKey)),
|
||||
})),
|
||||
);
|
||||
const [categories, decryptedMetadata] = await Promise.all([
|
||||
Promise.all(
|
||||
file.categories.map(async (category) => ({
|
||||
id: category.id,
|
||||
...(await decryptCategoryMetadata(category, masterKey)),
|
||||
})),
|
||||
),
|
||||
decryptFileMetadata(file, masterKey),
|
||||
]);
|
||||
|
||||
await IndexedDB.storeFileInfo({
|
||||
id: file.id,
|
||||
parentId: file.parent,
|
||||
contentType: file.contentType,
|
||||
name: decryptedMetadata.name,
|
||||
createdAt: decryptedMetadata.createdAt,
|
||||
lastModifiedAt: decryptedMetadata.lastModifiedAt,
|
||||
categoryIds: categories.map((category) => category.id),
|
||||
});
|
||||
return {
|
||||
id: file.id,
|
||||
exists: true as const,
|
||||
@@ -110,7 +134,7 @@ const bulkFetchFromServer = async (ids: number[], masterKey: CryptoKey) => {
|
||||
contentType: file.contentType,
|
||||
contentIv: file.contentIv,
|
||||
categories,
|
||||
...(await decryptFileMetadata(file, masterKey)),
|
||||
...decryptedMetadata,
|
||||
};
|
||||
}),
|
||||
);
|
||||
|
||||
@@ -1,18 +1,29 @@
|
||||
<script lang="ts">
|
||||
import { onMount } from "svelte";
|
||||
import { FullscreenDiv } from "$lib/components/atoms";
|
||||
import { TopBar } from "$lib/components/molecules";
|
||||
import { getDownloadingFiles, clearDownloadedFiles } from "$lib/modules/file";
|
||||
import { bulkGetFileInfo } from "$lib/modules/filesystem";
|
||||
import {
|
||||
getDownloadingFiles,
|
||||
clearDownloadedFiles,
|
||||
type FileDownloadState,
|
||||
} from "$lib/modules/file";
|
||||
import { bulkGetFileInfo, type MaybeFileInfo } from "$lib/modules/filesystem";
|
||||
import { masterKeyStore } from "$lib/stores";
|
||||
import File from "./File.svelte";
|
||||
|
||||
const downloadingFiles = getDownloadingFiles();
|
||||
const filesPromise = $derived(
|
||||
bulkGetFileInfo(
|
||||
downloadingFiles.map(({ id }) => id),
|
||||
let downloadingFiles: { info: MaybeFileInfo; state: FileDownloadState }[] = $state([]);
|
||||
|
||||
onMount(async () => {
|
||||
const states = getDownloadingFiles();
|
||||
const infos = await bulkGetFileInfo(
|
||||
states.map(({ id }) => id),
|
||||
$masterKeyStore?.get(1)?.key!,
|
||||
),
|
||||
);
|
||||
);
|
||||
downloadingFiles = states.map((state) => ({
|
||||
info: infos.get(state.id)!,
|
||||
state,
|
||||
}));
|
||||
});
|
||||
|
||||
$effect(() => clearDownloadedFiles);
|
||||
</script>
|
||||
@@ -23,14 +34,11 @@
|
||||
|
||||
<TopBar />
|
||||
<FullscreenDiv>
|
||||
{#await filesPromise then files}
|
||||
<div class="space-y-2 pb-4">
|
||||
{#each downloadingFiles as state}
|
||||
{@const info = files.get(state.id)!}
|
||||
{#if info.exists}
|
||||
<File {state} {info} />
|
||||
{/if}
|
||||
{/each}
|
||||
</div>
|
||||
{/await}
|
||||
<div class="space-y-2 pb-4">
|
||||
{#each downloadingFiles as { info, state } (info.id)}
|
||||
{#if info.exists}
|
||||
<File {info} {state} />
|
||||
{/if}
|
||||
{/each}
|
||||
</div>
|
||||
</FullscreenDiv>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<script lang="ts">
|
||||
import type { FileDownloadState } from "$lib/modules/file";
|
||||
import type { FileInfo } from "$lib/modules/filesystem";
|
||||
import type { SummarizedFileInfo } from "$lib/modules/filesystem";
|
||||
import { formatNetworkSpeed } from "$lib/utils";
|
||||
|
||||
import IconCloud from "~icons/material-symbols/cloud";
|
||||
@@ -11,7 +11,7 @@
|
||||
import IconError from "~icons/material-symbols/error";
|
||||
|
||||
interface Props {
|
||||
info: FileInfo;
|
||||
info: SummarizedFileInfo;
|
||||
state: FileDownloadState;
|
||||
}
|
||||
|
||||
|
||||
@@ -33,47 +33,38 @@
|
||||
|
||||
const toEntry =
|
||||
<T extends Exclude<Entry["type"], "parent">>(type: T) =>
|
||||
(details: Extract<Entry, { type: T }>["details"]) => ({
|
||||
type,
|
||||
name: details.name,
|
||||
details,
|
||||
});
|
||||
(details: Extract<Entry, { type: T }>["details"]) => ({ type, name: details.name, details });
|
||||
|
||||
let entries = $derived([
|
||||
...(showParentEntry ? ([{ type: "parent" }] as const) : []),
|
||||
...sortEntries(info.subDirectories.map(toEntry("directory"))),
|
||||
...sortEntries([
|
||||
...info.files.map(toEntry("file")),
|
||||
...getUploadingFiles(info.id).map(toEntry("uploading-file")),
|
||||
...(getUploadingFiles(info.id) as LiveFileUploadState[]).map(toEntry("uploading-file")),
|
||||
]),
|
||||
]);
|
||||
</script>
|
||||
|
||||
{#if entries.length > 0}
|
||||
<div class="pb-[4.5rem]">
|
||||
<RowVirtualizer
|
||||
count={entries.length}
|
||||
itemHeight={(index) => 56 + (index + 1 < entries.length ? 4 : 0)}
|
||||
>
|
||||
<RowVirtualizer count={entries.length} itemHeight={() => 56} itemGap={4}>
|
||||
{#snippet item(index)}
|
||||
{@const entry = entries[index]!}
|
||||
<div class={index + 1 < entries.length ? "pb-1" : ""}>
|
||||
{#if entry.type === "parent"}
|
||||
<ActionEntryButton class="h-14" onclick={onParentClick}>
|
||||
<DirectoryEntryLabel type="parent-directory" name=".." />
|
||||
</ActionEntryButton>
|
||||
{:else if entry.type === "directory"}
|
||||
<SubDirectory
|
||||
info={entry.details}
|
||||
onclick={onEntryClick}
|
||||
onOpenMenuClick={onEntryMenuClick}
|
||||
/>
|
||||
{:else if entry.type === "file"}
|
||||
<File info={entry.details} onclick={onEntryClick} onOpenMenuClick={onEntryMenuClick} />
|
||||
{:else}
|
||||
<UploadingFile state={entry.details} />
|
||||
{/if}
|
||||
</div>
|
||||
{#if entry.type === "parent"}
|
||||
<ActionEntryButton class="h-14" onclick={onParentClick}>
|
||||
<DirectoryEntryLabel type="parent-directory" name=".." />
|
||||
</ActionEntryButton>
|
||||
{:else if entry.type === "directory"}
|
||||
<SubDirectory
|
||||
info={entry.details}
|
||||
onclick={onEntryClick}
|
||||
onOpenMenuClick={onEntryMenuClick}
|
||||
/>
|
||||
{:else if entry.type === "file"}
|
||||
<File info={entry.details} onclick={onEntryClick} onOpenMenuClick={onEntryMenuClick} />
|
||||
{:else}
|
||||
<UploadingFile state={entry.details} />
|
||||
{/if}
|
||||
{/snippet}
|
||||
</RowVirtualizer>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user