diff --git a/package.json b/package.json
index ce46336..bd44d2c 100644
--- a/package.json
+++ b/package.json
@@ -21,6 +21,7 @@
"@sveltejs/adapter-node": "^5.4.0",
"@sveltejs/kit": "^2.49.2",
"@sveltejs/vite-plugin-svelte": "^6.2.1",
+ "@tanstack/svelte-virtual": "^3.13.13",
"@trpc/client": "^11.8.1",
"@types/file-saver": "^2.0.7",
"@types/ms": "^0.7.34",
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index e3de21a..9503e44 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -54,6 +54,9 @@ importers:
'@sveltejs/vite-plugin-svelte':
specifier: ^6.2.1
version: 6.2.1(svelte@5.46.1)(vite@7.3.0(@types/node@25.0.3)(jiti@1.21.7)(yaml@2.8.0))
+ '@tanstack/svelte-virtual':
+ specifier: ^3.13.13
+ version: 3.13.13(svelte@5.46.1)
'@trpc/client':
specifier: ^11.8.1
version: 11.8.1(@trpc/server@11.8.1(typescript@5.9.3))(typescript@5.9.3)
@@ -610,6 +613,14 @@ packages:
svelte: ^5.0.0
vite: ^6.3.0 || ^7.0.0
+ '@tanstack/svelte-virtual@3.13.13':
+ resolution: {integrity: sha512-VDOvbRw3R+XBQdFodEJ4E7AOmEyo3Bmr4zL4DLVnJ0fxICdbvY5F5t8zSwJ4f7lqjckXi0yKFzY8WBtjaNbsGQ==}
+ peerDependencies:
+ svelte: ^3.48.0 || ^4.0.0 || ^5.0.0
+
+ '@tanstack/virtual-core@3.13.13':
+ resolution: {integrity: sha512-uQFoSdKKf5S8k51W5t7b2qpfkyIbdHMzAn+AMQvHPxKUPeo1SsGaA4JRISQT87jm28b7z8OEqPcg1IOZagQHcA==}
+
'@trpc/client@11.8.1':
resolution: {integrity: sha512-L/SJFGanr9xGABmuDoeXR4xAdHJmsXsiF9OuH+apecJ+8sUITzVT1EPeqp0ebqA6lBhEl5pPfg3rngVhi/h60Q==}
peerDependencies:
@@ -2367,6 +2378,13 @@ snapshots:
transitivePeerDependencies:
- supports-color
+ '@tanstack/svelte-virtual@3.13.13(svelte@5.46.1)':
+ dependencies:
+ '@tanstack/virtual-core': 3.13.13
+ svelte: 5.46.1
+
+ '@tanstack/virtual-core@3.13.13': {}
+
'@trpc/client@11.8.1(@trpc/server@11.8.1(typescript@5.9.3))(typescript@5.9.3)':
dependencies:
'@trpc/server': 11.8.1(typescript@5.9.3)
diff --git a/src/lib/components/molecules/Categories/Categories.svelte b/src/lib/components/molecules/Categories/Categories.svelte
index a11313e..54368c6 100644
--- a/src/lib/components/molecules/Categories/Categories.svelte
+++ b/src/lib/components/molecules/Categories/Categories.svelte
@@ -3,7 +3,7 @@
import type { SvelteHTMLElements } from "svelte/elements";
import { get, type Writable } from "svelte/store";
import type { CategoryInfo } from "$lib/modules/filesystem";
- import { SortBy, sortEntries } from "$lib/modules/util";
+ import { SortBy, sortEntries } from "$lib/utils";
import Category from "./Category.svelte";
import type { SelectedCategory } from "./service";
diff --git a/src/lib/components/molecules/Gallery/Gallery.svelte b/src/lib/components/molecules/Gallery/Gallery.svelte
new file mode 100644
index 0000000..1bcb335
--- /dev/null
+++ b/src/lib/components/molecules/Gallery/Gallery.svelte
@@ -0,0 +1,128 @@
+
+
+
+
+ {#each $virtualizer.getVirtualItems() as virtualRow (virtualRow.key)}
+ {@const row = rows[virtualRow.index]!}
+
+ {#if row.type === "header"}
+
{row.label}
+ {:else}
+
+ {#each row.items as { info }}
+
+ {/each}
+
+ {/if}
+
+ {/each}
+
+ {#if $virtualizer.getVirtualItems().length === 0}
+
+
+ {#if files.length === 0}
+ 업로드된 파일이 없어요.
+ {:else if filesWithDate.length === 0}
+ 파일 목록을 불러오고 있어요.
+ {:else}
+ 사진 또는 동영상이 없어요.
+ {/if}
+
+
+ {/if}
+
diff --git a/src/lib/components/molecules/Gallery/Thumbnail.svelte b/src/lib/components/molecules/Gallery/Thumbnail.svelte
new file mode 100644
index 0000000..463419a
--- /dev/null
+++ b/src/lib/components/molecules/Gallery/Thumbnail.svelte
@@ -0,0 +1,42 @@
+
+
+{#if $info}
+
+{/if}
diff --git a/src/lib/components/molecules/Gallery/index.ts b/src/lib/components/molecules/Gallery/index.ts
new file mode 100644
index 0000000..9a269ab
--- /dev/null
+++ b/src/lib/components/molecules/Gallery/index.ts
@@ -0,0 +1 @@
+export { default as Gallery } from "./Gallery.svelte";
diff --git a/src/lib/components/molecules/index.ts b/src/lib/components/molecules/index.ts
index 8edc84a..b11ed5f 100644
--- a/src/lib/components/molecules/index.ts
+++ b/src/lib/components/molecules/index.ts
@@ -2,6 +2,7 @@ export * from "./ActionModal.svelte";
export { default as ActionModal } from "./ActionModal.svelte";
export * from "./Categories";
export { default as Categories } from "./Categories";
+export * from "./Gallery";
export { default as IconEntryButton } from "./IconEntryButton.svelte";
export * from "./labels";
export { default as SubCategories } from "./SubCategories.svelte";
diff --git a/src/lib/components/organisms/Category/Category.svelte b/src/lib/components/organisms/Category/Category.svelte
index 295bf99..ce3abcd 100644
--- a/src/lib/components/organisms/Category/Category.svelte
+++ b/src/lib/components/organisms/Category/Category.svelte
@@ -4,8 +4,8 @@
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 { SortBy, sortEntries } from "$lib/modules/util";
import { masterKeyStore } from "$lib/stores";
+ import { SortBy, sortEntries } from "$lib/utils";
import File from "./File.svelte";
import type { SelectedFile } from "./service";
diff --git a/src/lib/modules/util.ts b/src/lib/utils/format.ts
similarity index 64%
rename from src/lib/modules/util.ts
rename to src/lib/utils/format.ts
index 3fff89d..ce3b35c 100644
--- a/src/lib/modules/util.ts
+++ b/src/lib/utils/format.ts
@@ -7,6 +7,13 @@ export const formatDate = (date: Date) => {
return `${year}. ${month}. ${day}.`;
};
+export const formatDateSortable = (date: Date) => {
+ const year = date.getFullYear();
+ const month = pad2(date.getMonth() + 1);
+ const day = pad2(date.getDate());
+ return `${year}${month}${day}`;
+};
+
export const formatDateTime = (date: Date) => {
const dateFormatted = formatDate(date);
const hours = date.getHours();
@@ -32,32 +39,3 @@ export const truncateString = (str: string, maxLength = 20) => {
if (str.length <= maxLength) return str;
return `${str.slice(0, maxLength)}...`;
};
-
-export enum SortBy {
- NAME_ASC,
- NAME_DESC,
-}
-
-type SortFunc = (a?: string, b?: string) => number;
-
-const collator = new Intl.Collator(undefined, { numeric: true, sensitivity: "base" });
-
-const sortByNameAsc: SortFunc = (a, b) => {
- if (a && b) return collator.compare(a, b);
- if (a) return -1;
- if (b) return 1;
- return 0;
-};
-
-const sortByNameDesc: SortFunc = (a, b) => -sortByNameAsc(a, b);
-
-export const sortEntries = (entries: T[], sortBy: SortBy) => {
- let sortFunc: SortFunc;
- if (sortBy === SortBy.NAME_ASC) {
- sortFunc = sortByNameAsc;
- } else {
- sortFunc = sortByNameDesc;
- }
-
- entries.sort((a, b) => sortFunc(a.name, b.name));
-};
diff --git a/src/lib/utils/index.ts b/src/lib/utils/index.ts
index 4c24322..1db9577 100644
--- a/src/lib/utils/index.ts
+++ b/src/lib/utils/index.ts
@@ -1 +1,3 @@
+export * from "./format";
export * from "./gotoStateful";
+export * from "./sort";
diff --git a/src/lib/utils/sort.ts b/src/lib/utils/sort.ts
new file mode 100644
index 0000000..2385e55
--- /dev/null
+++ b/src/lib/utils/sort.ts
@@ -0,0 +1,57 @@
+interface SortEntry {
+ name?: string;
+ date?: Date;
+}
+
+export enum SortBy {
+ NAME_ASC,
+ NAME_DESC,
+ DATE_ASC,
+ DATE_DESC,
+}
+
+type SortFunc = (a: SortEntry, b: SortEntry) => number;
+
+const collator = new Intl.Collator(undefined, { numeric: true, sensitivity: "base" });
+
+const sortByNameAsc: SortFunc = ({ name: a }, { name: b }) => {
+ if (a && b) return collator.compare(a, b);
+ if (a) return -1;
+ if (b) return 1;
+ return 0;
+};
+
+const sortByNameDesc: SortFunc = (a, b) => -sortByNameAsc(a, b);
+
+const sortByDateAsc: SortFunc = ({ date: a }, { date: b }) => {
+ if (a && b) return a.getTime() - b.getTime();
+ if (a) return -1;
+ if (b) return 1;
+ return 0;
+};
+
+const sortByDateDesc: SortFunc = (a, b) => -sortByDateAsc(a, b);
+
+export const sortEntries = (entries: T[], sortBy: SortBy) => {
+ let sortFunc: SortFunc;
+
+ switch (sortBy) {
+ case SortBy.NAME_ASC:
+ sortFunc = sortByNameAsc;
+ break;
+ case SortBy.NAME_DESC:
+ sortFunc = sortByNameDesc;
+ break;
+ case SortBy.DATE_ASC:
+ sortFunc = sortByDateAsc;
+ break;
+ case SortBy.DATE_DESC:
+ sortFunc = sortByDateDesc;
+ break;
+ default:
+ const exhaustive: never = sortBy;
+ sortFunc = exhaustive;
+ }
+
+ entries.sort(sortFunc);
+};
diff --git a/src/routes/(fullscreen)/file/[id]/DownloadStatus.svelte b/src/routes/(fullscreen)/file/[id]/DownloadStatus.svelte
index 21774cd..150669e 100644
--- a/src/routes/(fullscreen)/file/[id]/DownloadStatus.svelte
+++ b/src/routes/(fullscreen)/file/[id]/DownloadStatus.svelte
@@ -1,7 +1,7 @@
+
+
+ 사진 및 동영상
+
+
+
+
+ goto(`/file/${id}`)} />
+
diff --git a/src/routes/(fullscreen)/gallery/+page.ts b/src/routes/(fullscreen)/gallery/+page.ts
new file mode 100644
index 0000000..1a241c5
--- /dev/null
+++ b/src/routes/(fullscreen)/gallery/+page.ts
@@ -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 };
+};
diff --git a/src/routes/(fullscreen)/settings/cache/+page.svelte b/src/routes/(fullscreen)/settings/cache/+page.svelte
index af375c2..cf8192d 100644
--- a/src/routes/(fullscreen)/settings/cache/+page.svelte
+++ b/src/routes/(fullscreen)/settings/cache/+page.svelte
@@ -6,8 +6,8 @@
import type { FileCacheIndex } from "$lib/indexedDB";
import { getFileCacheIndex, deleteFileCache as doDeleteFileCache } from "$lib/modules/file";
import { getFileInfo, type FileInfo } from "$lib/modules/filesystem";
- import { formatFileSize } from "$lib/modules/util";
import { masterKeyStore } from "$lib/stores";
+ import { formatFileSize } from "$lib/utils";
import File from "./File.svelte";
interface FileCache {
diff --git a/src/routes/(fullscreen)/settings/cache/File.svelte b/src/routes/(fullscreen)/settings/cache/File.svelte
index f21445b..581d144 100644
--- a/src/routes/(fullscreen)/settings/cache/File.svelte
+++ b/src/routes/(fullscreen)/settings/cache/File.svelte
@@ -2,7 +2,7 @@
import type { Writable } from "svelte/store";
import type { FileCacheIndex } from "$lib/indexedDB";
import type { FileInfo } from "$lib/modules/filesystem";
- import { formatDate, formatFileSize } from "$lib/modules/util";
+ import { formatDate, formatFileSize } from "$lib/utils";
import IconDraft from "~icons/material-symbols/draft";
import IconScanDelete from "~icons/material-symbols/scan-delete";
diff --git a/src/routes/(fullscreen)/settings/thumbnail/+page.ts b/src/routes/(fullscreen)/settings/thumbnail/+page.ts
index f435b72..4d5520c 100644
--- a/src/routes/(fullscreen)/settings/thumbnail/+page.ts
+++ b/src/routes/(fullscreen)/settings/thumbnail/+page.ts
@@ -1,13 +1,7 @@
-import { error } from "@sveltejs/kit";
import { trpc } from "$trpc/client";
import type { PageLoad } from "./$types";
export const load: PageLoad = async ({ fetch }) => {
- try {
- const files = await trpc(fetch).file.listWithoutThumbnail.query();
- return { files };
- } catch {
- // TODO: Error Handling
- error(500, "Internal server error");
- }
+ const files = await trpc(fetch).file.listWithoutThumbnail.query();
+ return { files };
};
diff --git a/src/routes/(fullscreen)/settings/thumbnail/File.svelte b/src/routes/(fullscreen)/settings/thumbnail/File.svelte
index 8977fc7..93c23ad 100644
--- a/src/routes/(fullscreen)/settings/thumbnail/File.svelte
+++ b/src/routes/(fullscreen)/settings/thumbnail/File.svelte
@@ -14,7 +14,7 @@
import { ActionEntryButton } from "$lib/components/atoms";
import { DirectoryEntryLabel } from "$lib/components/molecules";
import type { FileInfo } from "$lib/modules/filesystem";
- import { formatDateTime } from "$lib/modules/util";
+ import { formatDateTime } from "$lib/utils";
import type { GenerationStatus } from "./service.svelte";
import IconCamera from "~icons/material-symbols/camera";
diff --git a/src/routes/(main)/category/[[id]]/CategoryDeleteModal.svelte b/src/routes/(main)/category/[[id]]/CategoryDeleteModal.svelte
index 5e792d1..289b971 100644
--- a/src/routes/(main)/category/[[id]]/CategoryDeleteModal.svelte
+++ b/src/routes/(main)/category/[[id]]/CategoryDeleteModal.svelte
@@ -1,6 +1,6 @@