7 Commits

Author SHA1 Message Date
static
90ac5ba4c3 Merge pull request #15 from kmc7468/dev
v0.6.0
2025-12-27 14:22:26 +09:00
static
dfffa004ac Merge pull request #13 from kmc7468/dev
v0.5.1
2025-07-12 19:56:12 +09:00
static
0cd55a413d Merge pull request #12 from kmc7468/dev
v0.5.0
2025-07-12 06:01:08 +09:00
static
361d966a59 Merge pull request #10 from kmc7468/dev
v0.4.0
2025-01-30 21:06:50 +09:00
static
aef43b8bfa Merge pull request #6 from kmc7468/dev
v0.3.0
2025-01-18 13:29:09 +09:00
static
7f128cccf6 Merge pull request #5 from kmc7468/dev
v0.2.0
2025-01-13 03:53:14 +09:00
static
a198e5f6dc Merge pull request #2 from kmc7468/dev
v0.1.0
2025-01-09 06:24:31 +09:00
16 changed files with 114 additions and 164 deletions

View File

@@ -1,46 +0,0 @@
<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>

View File

@@ -3,4 +3,3 @@ export * from "./buttons";
export * from "./divs";
export * from "./inputs";
export { default as Modal } from "./Modal.svelte";
export { default as RowVirtualizer } from "./RowVirtualizer.svelte";

View File

@@ -1,7 +1,7 @@
<script lang="ts">
import { untrack } from "svelte";
import { get, type Writable } from "svelte/store";
import { CheckBox, RowVirtualizer } from "$lib/components/atoms";
import { CheckBox } 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,26 +89,19 @@
<p class="font-medium">하위 카테고리의 파일</p>
</CheckBox>
</div>
{#key info}
<RowVirtualizer
count={files.length}
itemHeight={(index) => 48 + (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 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>
</div>
{/if}
</div>

View File

@@ -1,7 +1,8 @@
<script lang="ts">
import { createWindowVirtualizer } from "@tanstack/svelte-virtual";
import { untrack } from "svelte";
import { get, type Writable } from "svelte/store";
import { FileThumbnailButton, RowVirtualizer } from "$lib/components/atoms";
import { FileThumbnailButton } from "$lib/components/atoms";
import type { FileInfo } from "$lib/modules/filesystem";
import { formatDate, formatDateSortable, SortBy, sortEntries } from "$lib/utils";
@@ -16,11 +17,25 @@
| { date?: undefined; contentType?: undefined; info: Writable<FileInfo | null> }
| { date: Date; contentType: string; info: Writable<FileInfo | null> };
type Row =
| { type: "header"; label: string }
| { type: "items"; items: FileEntry[]; isLast: boolean };
| { type: "header"; key: string; label: string }
| { type: "items"; key: string; items: FileEntry[] };
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) => {
@@ -61,19 +76,18 @@
newRows.push({
type: "header",
key: `header-${date}`,
label: formatDate(entries[0]!.date!),
});
for (let i = 0; i < entries.length; i += 4) {
newRows.push({
type: "items",
items: entries.slice(i, i + 4),
isLast: i + 4 >= entries.length,
});
}
newRows.push({
type: "items",
key: `items-${date}`,
items: entries,
});
}
rows = newRows;
$virtualizer.setOptions({ count: rows.length });
};
return untrack(() => {
buildRows();
@@ -96,29 +110,29 @@
});
</script>
<RowVirtualizer
count={rows.length}
itemHeight={(index) =>
rows[index]!.type === "header"
? 28
: 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 text-sm 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 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}
</div>
{/if}
{/snippet}
{#snippet placeholder()}
{/each}
</div>
{#if $virtualizer.getVirtualItems().length === 0}
<div class="flex h-full flex-grow items-center justify-center">
<p class="text-gray-500">
{#if files.length === 0}
@@ -130,5 +144,5 @@
{/if}
</p>
</div>
{/snippet}
</RowVirtualizer>
{/if}
</div>

View File

@@ -1,3 +1,4 @@
import { TRPCClientError } from "@trpc/client";
import { get, writable, type Writable } from "svelte/store";
import {
getDirectoryInfos as getDirectoryInfosFromIndexedDB,
@@ -17,7 +18,7 @@ import {
type CategoryId,
} from "$lib/indexedDB";
import { unwrapDataKey, decryptString } from "$lib/modules/crypto";
import { trpc, isTRPCClientError } from "$trpc/client";
import { trpc } from "$trpc/client";
export type DirectoryInfo =
| {
@@ -113,7 +114,7 @@ const fetchDirectoryInfoFromServer = async (
try {
data = await trpc().directory.get.query({ id });
} catch (e) {
if (isTRPCClientError(e) && e.data?.code === "NOT_FOUND") {
if (e instanceof TRPCClientError && e.data?.code === "NOT_FOUND") {
info.set(null);
await deleteDirectoryInfo(id as number);
return;
@@ -186,7 +187,7 @@ const fetchFileInfoFromServer = async (
try {
metadata = await trpc().file.get.query({ id });
} catch (e) {
if (isTRPCClientError(e) && e.data?.code === "NOT_FOUND") {
if (e instanceof TRPCClientError && e.data?.code === "NOT_FOUND") {
info.set(null);
await deleteFileInfo(id);
return;
@@ -282,7 +283,7 @@ const fetchCategoryInfoFromServer = async (
try {
data = await trpc().category.get.query({ id });
} catch (e) {
if (isTRPCClientError(e) && e.data?.code === "NOT_FOUND") {
if (e instanceof TRPCClientError && e.data?.code === "NOT_FOUND") {
info.set(null);
await deleteCategoryInfo(id as number);
return;

View File

@@ -1,5 +1,6 @@
import { TRPCClientError } from "@trpc/client";
import { encodeToBase64, decryptChallenge, signMessageRSA } from "$lib/modules/crypto";
import { trpc, isTRPCClientError } from "$trpc/client";
import { trpc } from "$trpc/client";
export const requestSessionUpgrade = async (
encryptKeyBase64: string,
@@ -15,7 +16,7 @@ export const requestSessionUpgrade = async (
sigPubKey: verifyKeyBase64,
}));
} catch (e) {
if (isTRPCClientError(e) && e.data?.code === "FORBIDDEN") {
if (e instanceof TRPCClientError && e.data?.code === "FORBIDDEN") {
return [false, "Unregistered client"] as const;
}
return [false] as const;
@@ -30,7 +31,7 @@ export const requestSessionUpgrade = async (
force,
});
} catch (e) {
if (isTRPCClientError(e) && e.data?.code === "CONFLICT") {
if (e instanceof TRPCClientError && e.data?.code === "CONFLICT") {
return [false, "Already logged in"] as const;
}
return [false] as const;

View File

@@ -1,3 +1,4 @@
import { TRPCClientError } from "@trpc/client";
import { storeMasterKeys } from "$lib/indexedDB";
import {
encodeToBase64,
@@ -10,7 +11,7 @@ import {
} from "$lib/modules/crypto";
import { requestSessionUpgrade } from "$lib/services/auth";
import { masterKeyStore, type ClientKeys } from "$lib/stores";
import { trpc, isTRPCClientError } from "$trpc/client";
import { trpc } from "$trpc/client";
export const requestClientRegistration = async (
encryptKeyBase64: string,
@@ -111,7 +112,10 @@ export const requestInitialMasterKeyAndHmacSecretRegistration = async (
mekSig: await signMasterKeyWrapped(masterKeyWrapped, 1, signKey),
});
} catch (e) {
if (isTRPCClientError(e) && (e.data?.code === "FORBIDDEN" || e.data?.code === "CONFLICT")) {
if (
e instanceof TRPCClientError &&
(e.data?.code === "FORBIDDEN" || e.data?.code === "CONFLICT")
) {
return true;
}
// TODO: Error Handling

View File

@@ -1,7 +0,0 @@
import { createCaller } from "$trpc/router.server";
import type { PageServerLoad } from "./$types";
export const load: PageServerLoad = async (event) => {
const files = await createCaller(event).file.list();
return { files };
};

View File

@@ -0,0 +1,7 @@
import { trpc } from "$trpc/client";
import type { PageLoad } from "./$types";
export const load: PageLoad = async ({ fetch }) => {
const files = await trpc(fetch).file.list.query();
return { files };
};

View File

@@ -1,7 +0,0 @@
import { createCaller } from "$trpc/router.server";
import type { PageServerLoad } from "./$types";
export const load: PageServerLoad = async (event) => {
const files = await createCaller(event).file.listWithoutThumbnail();
return { files };
};

View File

@@ -0,0 +1,7 @@
import { trpc } from "$trpc/client";
import type { PageLoad } from "./$types";
export const load: PageLoad = async ({ fetch }) => {
const files = await trpc(fetch).file.listWithoutThumbnail.query();
return { files };
};

View File

@@ -1,7 +1,7 @@
<script lang="ts">
import { untrack } from "svelte";
import { get, type Writable } from "svelte/store";
import { ActionEntryButton, RowVirtualizer } from "$lib/components/atoms";
import { ActionEntryButton } from "$lib/components/atoms";
import { DirectoryEntryLabel } from "$lib/components/molecules";
import {
getDirectoryInfo,
@@ -127,23 +127,13 @@
{#each subDirectories as { info }}
<SubDirectory {info} onclick={onEntryClick} onOpenMenuClick={onEntryMenuClick} />
{/each}
{#if files.length > 0}
<RowVirtualizer
count={files.length}
itemHeight={(index) => 56 + (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}
{#each files as file}
{#if file.type === "file"}
<File info={file.info} onclick={onEntryClick} onOpenMenuClick={onEntryMenuClick} />
{:else}
<UploadingFile status={file.info} />
{/if}
{/each}
</div>
{:else}
<div class="flex flex-grow items-center justify-center">

View File

@@ -21,16 +21,14 @@
<div class="min-h-full space-y-4 bg-gray-100 px-4 pb-[5.5em] pt-4">
<p class="px-2 text-2xl font-bold text-gray-800">ArkVault</p>
<div class="rounded-xl bg-white p-2">
<div class="space-y-2 rounded-xl bg-white px-2 pb-4 pt-2">
<EntryButton onclick={() => goto("/gallery")} class="w-full">
<p class="text-left font-semibold">사진 및 동영상</p>
</EntryButton>
{#if mediaFiles.length > 0}
<div class="grid grid-cols-4 gap-2 p-2">
{#each mediaFiles as file}
<FileThumbnailButton info={file} onclick={({ id }) => goto(`/file/${id}`)} />
{/each}
</div>
{/if}
<div class="grid grid-cols-4 gap-2 px-2">
{#each mediaFiles as file}
<FileThumbnailButton info={file} onclick={({ id }) => goto(`/file/${id}`)} />
{/each}
</div>
</div>
</div>

View File

@@ -1,7 +0,0 @@
import { createCaller } from "$trpc/router.server";
import type { PageServerLoad } from "./$types";
export const load: PageServerLoad = async (event) => {
const { nickname } = await createCaller(event).user.get();
return { nickname };
};

View File

@@ -0,0 +1,7 @@
import { trpc } from "$trpc/client";
import type { PageLoad } from "./$types";
export const load: PageLoad = async ({ fetch }) => {
const { nickname } = await trpc(fetch).user.get.query();
return { nickname };
};

View File

@@ -1,4 +1,4 @@
import { createTRPCClient, httpBatchLink, TRPCClientError } from "@trpc/client";
import { createTRPCClient, httpBatchLink } from "@trpc/client";
import superjson from "superjson";
import { browser } from "$app/environment";
import type { AppRouter } from "./router.server";
@@ -24,7 +24,3 @@ export const trpc = (fetch = globalThis.fetch) => {
}
return client;
};
export const isTRPCClientError = (e: unknown): e is TRPCClientError<AppRouter> => {
return e instanceof TRPCClientError;
};