mirror of
https://github.com/kmc7468/arkvault.git
synced 2026-02-04 08:06:56 +00:00
갤러리 페이지 구현
This commit is contained in:
@@ -21,6 +21,7 @@
|
|||||||
"@sveltejs/adapter-node": "^5.4.0",
|
"@sveltejs/adapter-node": "^5.4.0",
|
||||||
"@sveltejs/kit": "^2.49.2",
|
"@sveltejs/kit": "^2.49.2",
|
||||||
"@sveltejs/vite-plugin-svelte": "^6.2.1",
|
"@sveltejs/vite-plugin-svelte": "^6.2.1",
|
||||||
|
"@tanstack/svelte-virtual": "^3.13.13",
|
||||||
"@trpc/client": "^11.8.1",
|
"@trpc/client": "^11.8.1",
|
||||||
"@types/file-saver": "^2.0.7",
|
"@types/file-saver": "^2.0.7",
|
||||||
"@types/ms": "^0.7.34",
|
"@types/ms": "^0.7.34",
|
||||||
|
|||||||
18
pnpm-lock.yaml
generated
18
pnpm-lock.yaml
generated
@@ -54,6 +54,9 @@ importers:
|
|||||||
'@sveltejs/vite-plugin-svelte':
|
'@sveltejs/vite-plugin-svelte':
|
||||||
specifier: ^6.2.1
|
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))
|
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':
|
'@trpc/client':
|
||||||
specifier: ^11.8.1
|
specifier: ^11.8.1
|
||||||
version: 11.8.1(@trpc/server@11.8.1(typescript@5.9.3))(typescript@5.9.3)
|
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
|
svelte: ^5.0.0
|
||||||
vite: ^6.3.0 || ^7.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':
|
'@trpc/client@11.8.1':
|
||||||
resolution: {integrity: sha512-L/SJFGanr9xGABmuDoeXR4xAdHJmsXsiF9OuH+apecJ+8sUITzVT1EPeqp0ebqA6lBhEl5pPfg3rngVhi/h60Q==}
|
resolution: {integrity: sha512-L/SJFGanr9xGABmuDoeXR4xAdHJmsXsiF9OuH+apecJ+8sUITzVT1EPeqp0ebqA6lBhEl5pPfg3rngVhi/h60Q==}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
@@ -2367,6 +2378,13 @@ snapshots:
|
|||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- supports-color
|
- 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)':
|
'@trpc/client@11.8.1(@trpc/server@11.8.1(typescript@5.9.3))(typescript@5.9.3)':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@trpc/server': 11.8.1(typescript@5.9.3)
|
'@trpc/server': 11.8.1(typescript@5.9.3)
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
import type { SvelteHTMLElements } from "svelte/elements";
|
import type { SvelteHTMLElements } from "svelte/elements";
|
||||||
import { get, type Writable } from "svelte/store";
|
import { get, type Writable } from "svelte/store";
|
||||||
import type { CategoryInfo } from "$lib/modules/filesystem";
|
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 Category from "./Category.svelte";
|
||||||
import type { SelectedCategory } from "./service";
|
import type { SelectedCategory } from "./service";
|
||||||
|
|
||||||
|
|||||||
128
src/lib/components/molecules/Gallery/Gallery.svelte
Normal file
128
src/lib/components/molecules/Gallery/Gallery.svelte
Normal file
@@ -0,0 +1,128 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { createWindowVirtualizer } from "@tanstack/svelte-virtual";
|
||||||
|
import { untrack } from "svelte";
|
||||||
|
import { get, type Writable } from "svelte/store";
|
||||||
|
import type { FileInfo } from "$lib/modules/filesystem";
|
||||||
|
import { formatDate, formatDateSortable, SortBy, sortEntries } from "$lib/utils";
|
||||||
|
import Thumbnail from "./Thumbnail.svelte";
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
files: Writable<FileInfo | null>[];
|
||||||
|
onFileClick?: (file: FileInfo) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
let { files, onFileClick }: Props = $props();
|
||||||
|
|
||||||
|
type FileEntry = { date?: Date; info: Writable<FileInfo | null> };
|
||||||
|
type Row =
|
||||||
|
| { 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) => {
|
||||||
|
const { createdAt, lastModifiedAt } = get(file) ?? {};
|
||||||
|
return { date: createdAt ?? lastModifiedAt, info: file };
|
||||||
|
});
|
||||||
|
|
||||||
|
const buildRows = () => {
|
||||||
|
const map = new Map<string, FileEntry[]>();
|
||||||
|
|
||||||
|
for (const file of filesWithDate) {
|
||||||
|
if (!file.date) continue;
|
||||||
|
|
||||||
|
const date = formatDateSortable(file.date);
|
||||||
|
const entries = map.get(date) ?? [];
|
||||||
|
entries.push(file);
|
||||||
|
map.set(date, entries);
|
||||||
|
}
|
||||||
|
|
||||||
|
const newRows: Row[] = [];
|
||||||
|
const sortedDates = Array.from(map.keys()).sort((a, b) => b.localeCompare(a));
|
||||||
|
for (const date of sortedDates) {
|
||||||
|
const entries = map.get(date)!;
|
||||||
|
sortEntries(entries, SortBy.DATE_DESC);
|
||||||
|
|
||||||
|
newRows.push({
|
||||||
|
type: "header",
|
||||||
|
key: `header-${date}`,
|
||||||
|
label: formatDate(entries[0]!.date!),
|
||||||
|
});
|
||||||
|
newRows.push({
|
||||||
|
type: "items",
|
||||||
|
key: `items-${date}`,
|
||||||
|
items: entries,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
rows = newRows;
|
||||||
|
$virtualizer.setOptions({ count: rows.length });
|
||||||
|
};
|
||||||
|
return untrack(() => {
|
||||||
|
buildRows();
|
||||||
|
|
||||||
|
const unsubscribes = filesWithDate.map((file) =>
|
||||||
|
file.info.subscribe((value) => {
|
||||||
|
const newDate = value?.createdAt ?? value?.lastModifiedAt;
|
||||||
|
if (file.date?.getTime() === newDate?.getTime()) return;
|
||||||
|
file.date = newDate;
|
||||||
|
buildRows();
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
return () => unsubscribes.forEach((unsubscribe) => unsubscribe());
|
||||||
|
});
|
||||||
|
});
|
||||||
|
</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 }}
|
||||||
|
<Thumbnail {info} onclick={onFileClick} />
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
{/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}
|
||||||
|
업로드된 파일이 없어요.
|
||||||
|
{:else if filesWithDate.length === 0}
|
||||||
|
파일 목록을 불러오고 있어요.
|
||||||
|
{:else}
|
||||||
|
사진 또는 동영상이 없어요.
|
||||||
|
{/if}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
42
src/lib/components/molecules/Gallery/Thumbnail.svelte
Normal file
42
src/lib/components/molecules/Gallery/Thumbnail.svelte
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import type { Writable } from "svelte/store";
|
||||||
|
import type { FileInfo } from "$lib/modules/filesystem";
|
||||||
|
import { requestFileThumbnailDownload } from "$lib/services/file";
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
info: Writable<FileInfo | null>;
|
||||||
|
onclick?: (file: FileInfo) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
let { info, onclick }: Props = $props();
|
||||||
|
|
||||||
|
let thumbnail: string | undefined = $state();
|
||||||
|
|
||||||
|
$effect(() => {
|
||||||
|
if ($info) {
|
||||||
|
requestFileThumbnailDownload($info.id, $info.dataKey)
|
||||||
|
.then((thumbnailUrl) => {
|
||||||
|
thumbnail = thumbnailUrl ?? undefined;
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
// TODO: Error Handling
|
||||||
|
thumbnail = undefined;
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
thumbnail = undefined;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
{#if $info}
|
||||||
|
<button
|
||||||
|
onclick={() => onclick?.($info)}
|
||||||
|
class="relative aspect-square w-full overflow-hidden rounded transition active:scale-95 active:brightness-90"
|
||||||
|
>
|
||||||
|
{#if thumbnail}
|
||||||
|
<img src={thumbnail} alt={$info.name} class="h-full w-full object-cover" />
|
||||||
|
{:else}
|
||||||
|
<div class="h-full w-full bg-gray-100"></div>
|
||||||
|
{/if}
|
||||||
|
</button>
|
||||||
|
{/if}
|
||||||
1
src/lib/components/molecules/Gallery/index.ts
Normal file
1
src/lib/components/molecules/Gallery/index.ts
Normal file
@@ -0,0 +1 @@
|
|||||||
|
export { default as Gallery } from "./Gallery.svelte";
|
||||||
@@ -2,6 +2,7 @@ export * from "./ActionModal.svelte";
|
|||||||
export { default as ActionModal } from "./ActionModal.svelte";
|
export { default as ActionModal } from "./ActionModal.svelte";
|
||||||
export * from "./Categories";
|
export * from "./Categories";
|
||||||
export { default as Categories } from "./Categories";
|
export { default as Categories } from "./Categories";
|
||||||
|
export * from "./Gallery";
|
||||||
export { default as IconEntryButton } from "./IconEntryButton.svelte";
|
export { default as IconEntryButton } from "./IconEntryButton.svelte";
|
||||||
export * from "./labels";
|
export * from "./labels";
|
||||||
export { default as SubCategories } from "./SubCategories.svelte";
|
export { default as SubCategories } from "./SubCategories.svelte";
|
||||||
|
|||||||
@@ -4,8 +4,8 @@
|
|||||||
import { CheckBox } from "$lib/components/atoms";
|
import { CheckBox } from "$lib/components/atoms";
|
||||||
import { SubCategories, type SelectedCategory } from "$lib/components/molecules";
|
import { SubCategories, type SelectedCategory } from "$lib/components/molecules";
|
||||||
import { getFileInfo, type FileInfo, type CategoryInfo } from "$lib/modules/filesystem";
|
import { getFileInfo, type FileInfo, type CategoryInfo } from "$lib/modules/filesystem";
|
||||||
import { SortBy, sortEntries } from "$lib/modules/util";
|
|
||||||
import { masterKeyStore } from "$lib/stores";
|
import { masterKeyStore } from "$lib/stores";
|
||||||
|
import { SortBy, sortEntries } from "$lib/utils";
|
||||||
import File from "./File.svelte";
|
import File from "./File.svelte";
|
||||||
import type { SelectedFile } from "./service";
|
import type { SelectedFile } from "./service";
|
||||||
|
|
||||||
|
|||||||
@@ -7,6 +7,13 @@ export const formatDate = (date: Date) => {
|
|||||||
return `${year}. ${month}. ${day}.`;
|
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) => {
|
export const formatDateTime = (date: Date) => {
|
||||||
const dateFormatted = formatDate(date);
|
const dateFormatted = formatDate(date);
|
||||||
const hours = date.getHours();
|
const hours = date.getHours();
|
||||||
@@ -32,32 +39,3 @@ export const truncateString = (str: string, maxLength = 20) => {
|
|||||||
if (str.length <= maxLength) return str;
|
if (str.length <= maxLength) return str;
|
||||||
return `${str.slice(0, maxLength)}...`;
|
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 = <T extends { name?: string }>(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));
|
|
||||||
};
|
|
||||||
@@ -1 +1,3 @@
|
|||||||
|
export * from "./format";
|
||||||
export * from "./gotoStateful";
|
export * from "./gotoStateful";
|
||||||
|
export * from "./sort";
|
||||||
|
|||||||
57
src/lib/utils/sort.ts
Normal file
57
src/lib/utils/sort.ts
Normal file
@@ -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 = <T extends SortEntry>(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);
|
||||||
|
};
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import type { Writable } from "svelte/store";
|
import type { Writable } from "svelte/store";
|
||||||
import { formatNetworkSpeed } from "$lib/modules/util";
|
|
||||||
import { isFileDownloading, type FileDownloadStatus } from "$lib/stores";
|
import { isFileDownloading, type FileDownloadStatus } from "$lib/stores";
|
||||||
|
import { formatNetworkSpeed } from "$lib/utils";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
status?: Writable<FileDownloadStatus>;
|
status?: Writable<FileDownloadStatus>;
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { get, type Writable } from "svelte/store";
|
import { get, type Writable } from "svelte/store";
|
||||||
import { getFileInfo, type FileInfo } from "$lib/modules/filesystem";
|
import { getFileInfo, type FileInfo } from "$lib/modules/filesystem";
|
||||||
import { formatNetworkSpeed } from "$lib/modules/util";
|
|
||||||
import { masterKeyStore, type FileDownloadStatus } from "$lib/stores";
|
import { masterKeyStore, type FileDownloadStatus } from "$lib/stores";
|
||||||
|
import { formatNetworkSpeed } from "$lib/utils";
|
||||||
|
|
||||||
import IconCloud from "~icons/material-symbols/cloud";
|
import IconCloud from "~icons/material-symbols/cloud";
|
||||||
import IconCloudDownload from "~icons/material-symbols/cloud-download";
|
import IconCloudDownload from "~icons/material-symbols/cloud-download";
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import type { Writable } from "svelte/store";
|
import type { Writable } from "svelte/store";
|
||||||
import { formatNetworkSpeed } from "$lib/modules/util";
|
|
||||||
import type { FileUploadStatus } from "$lib/stores";
|
import type { FileUploadStatus } from "$lib/stores";
|
||||||
|
import { formatNetworkSpeed } from "$lib/utils";
|
||||||
|
|
||||||
import IconPending from "~icons/material-symbols/pending";
|
import IconPending from "~icons/material-symbols/pending";
|
||||||
import IconLockClock from "~icons/material-symbols/lock-clock";
|
import IconLockClock from "~icons/material-symbols/lock-clock";
|
||||||
|
|||||||
25
src/routes/(fullscreen)/gallery/+page.svelte
Normal file
25
src/routes/(fullscreen)/gallery/+page.svelte
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import type { Writable } from "svelte/store";
|
||||||
|
import { goto } from "$app/navigation";
|
||||||
|
import { FullscreenDiv } from "$lib/components/atoms";
|
||||||
|
import { Gallery, TopBar } from "$lib/components/molecules";
|
||||||
|
import { getFileInfo, type FileInfo } from "$lib/modules/filesystem";
|
||||||
|
import { masterKeyStore } from "$lib/stores";
|
||||||
|
|
||||||
|
let { data } = $props();
|
||||||
|
|
||||||
|
let files: Writable<FileInfo | null>[] = $state([]);
|
||||||
|
|
||||||
|
$effect(() => {
|
||||||
|
files = data.files.map((file) => getFileInfo(file, $masterKeyStore?.get(1)?.key!));
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<svelte:head>
|
||||||
|
<title>사진 및 동영상</title>
|
||||||
|
</svelte:head>
|
||||||
|
|
||||||
|
<TopBar title="사진 및 동영상" />
|
||||||
|
<FullscreenDiv>
|
||||||
|
<Gallery {files} onFileClick={({ id }) => goto(`/file/${id}`)} />
|
||||||
|
</FullscreenDiv>
|
||||||
7
src/routes/(fullscreen)/gallery/+page.ts
Normal file
7
src/routes/(fullscreen)/gallery/+page.ts
Normal 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 };
|
||||||
|
};
|
||||||
@@ -6,8 +6,8 @@
|
|||||||
import type { FileCacheIndex } from "$lib/indexedDB";
|
import type { FileCacheIndex } from "$lib/indexedDB";
|
||||||
import { getFileCacheIndex, deleteFileCache as doDeleteFileCache } from "$lib/modules/file";
|
import { getFileCacheIndex, deleteFileCache as doDeleteFileCache } from "$lib/modules/file";
|
||||||
import { getFileInfo, type FileInfo } from "$lib/modules/filesystem";
|
import { getFileInfo, type FileInfo } from "$lib/modules/filesystem";
|
||||||
import { formatFileSize } from "$lib/modules/util";
|
|
||||||
import { masterKeyStore } from "$lib/stores";
|
import { masterKeyStore } from "$lib/stores";
|
||||||
|
import { formatFileSize } from "$lib/utils";
|
||||||
import File from "./File.svelte";
|
import File from "./File.svelte";
|
||||||
|
|
||||||
interface FileCache {
|
interface FileCache {
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
import type { Writable } from "svelte/store";
|
import type { Writable } from "svelte/store";
|
||||||
import type { FileCacheIndex } from "$lib/indexedDB";
|
import type { FileCacheIndex } from "$lib/indexedDB";
|
||||||
import type { FileInfo } from "$lib/modules/filesystem";
|
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 IconDraft from "~icons/material-symbols/draft";
|
||||||
import IconScanDelete from "~icons/material-symbols/scan-delete";
|
import IconScanDelete from "~icons/material-symbols/scan-delete";
|
||||||
|
|||||||
@@ -1,13 +1,7 @@
|
|||||||
import { error } from "@sveltejs/kit";
|
|
||||||
import { trpc } from "$trpc/client";
|
import { trpc } from "$trpc/client";
|
||||||
import type { PageLoad } from "./$types";
|
import type { PageLoad } from "./$types";
|
||||||
|
|
||||||
export const load: PageLoad = async ({ fetch }) => {
|
export const load: PageLoad = async ({ fetch }) => {
|
||||||
try {
|
|
||||||
const files = await trpc(fetch).file.listWithoutThumbnail.query();
|
const files = await trpc(fetch).file.listWithoutThumbnail.query();
|
||||||
return { files };
|
return { files };
|
||||||
} catch {
|
|
||||||
// TODO: Error Handling
|
|
||||||
error(500, "Internal server error");
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -14,7 +14,7 @@
|
|||||||
import { ActionEntryButton } from "$lib/components/atoms";
|
import { ActionEntryButton } from "$lib/components/atoms";
|
||||||
import { DirectoryEntryLabel } from "$lib/components/molecules";
|
import { DirectoryEntryLabel } from "$lib/components/molecules";
|
||||||
import type { FileInfo } from "$lib/modules/filesystem";
|
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 type { GenerationStatus } from "./service.svelte";
|
||||||
|
|
||||||
import IconCamera from "~icons/material-symbols/camera";
|
import IconCamera from "~icons/material-symbols/camera";
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { ActionModal } from "$lib/components/molecules";
|
import { ActionModal } from "$lib/components/molecules";
|
||||||
import { truncateString } from "$lib/modules/util";
|
import { truncateString } from "$lib/utils";
|
||||||
import { useContext } from "./service.svelte";
|
import { useContext } from "./service.svelte";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
|
|||||||
@@ -7,13 +7,13 @@
|
|||||||
type DirectoryInfo,
|
type DirectoryInfo,
|
||||||
type FileInfo,
|
type FileInfo,
|
||||||
} from "$lib/modules/filesystem";
|
} from "$lib/modules/filesystem";
|
||||||
import { SortBy, sortEntries } from "$lib/modules/util";
|
|
||||||
import {
|
import {
|
||||||
fileUploadStatusStore,
|
fileUploadStatusStore,
|
||||||
isFileUploading,
|
isFileUploading,
|
||||||
masterKeyStore,
|
masterKeyStore,
|
||||||
type FileUploadStatus,
|
type FileUploadStatus,
|
||||||
} from "$lib/stores";
|
} from "$lib/stores";
|
||||||
|
import { SortBy, sortEntries } from "$lib/utils";
|
||||||
import File from "./File.svelte";
|
import File from "./File.svelte";
|
||||||
import SubDirectory from "./SubDirectory.svelte";
|
import SubDirectory from "./SubDirectory.svelte";
|
||||||
import UploadingFile from "./UploadingFile.svelte";
|
import UploadingFile from "./UploadingFile.svelte";
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
import { ActionEntryButton } from "$lib/components/atoms";
|
import { ActionEntryButton } from "$lib/components/atoms";
|
||||||
import { DirectoryEntryLabel } from "$lib/components/molecules";
|
import { DirectoryEntryLabel } from "$lib/components/molecules";
|
||||||
import type { FileInfo } from "$lib/modules/filesystem";
|
import type { FileInfo } from "$lib/modules/filesystem";
|
||||||
import { formatDateTime } from "$lib/modules/util";
|
import { formatDateTime } from "$lib/utils";
|
||||||
import { requestFileThumbnailDownload } from "./service";
|
import { requestFileThumbnailDownload } from "./service";
|
||||||
import type { SelectedEntry } from "../service.svelte";
|
import type { SelectedEntry } from "../service.svelte";
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import type { Writable } from "svelte/store";
|
import type { Writable } from "svelte/store";
|
||||||
import { formatNetworkSpeed } from "$lib/modules/util";
|
|
||||||
import { isFileUploading, type FileUploadStatus } from "$lib/stores";
|
import { isFileUploading, type FileUploadStatus } from "$lib/stores";
|
||||||
|
import { formatNetworkSpeed } from "$lib/utils";
|
||||||
|
|
||||||
import IconDraft from "~icons/material-symbols/draft";
|
import IconDraft from "~icons/material-symbols/draft";
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { ActionModal } from "$lib/components/molecules";
|
import { ActionModal } from "$lib/components/molecules";
|
||||||
import { truncateString } from "$lib/modules/util";
|
import { truncateString } from "$lib/utils";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
file: File | undefined;
|
file: File | undefined;
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { ActionModal } from "$lib/components/molecules";
|
import { ActionModal } from "$lib/components/molecules";
|
||||||
import { truncateString } from "$lib/modules/util";
|
import { truncateString } from "$lib/utils";
|
||||||
import { useContext } from "./service.svelte";
|
import { useContext } from "./service.svelte";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
|
|||||||
@@ -1,13 +1,7 @@
|
|||||||
import { error } from "@sveltejs/kit";
|
|
||||||
import { trpc } from "$trpc/client";
|
import { trpc } from "$trpc/client";
|
||||||
import type { PageLoad } from "./$types";
|
import type { PageLoad } from "./$types";
|
||||||
|
|
||||||
export const load: PageLoad = async ({ fetch }) => {
|
export const load: PageLoad = async ({ fetch }) => {
|
||||||
try {
|
|
||||||
const { nickname } = await trpc(fetch).user.get.query();
|
const { nickname } = await trpc(fetch).user.get.query();
|
||||||
return { nickname };
|
return { nickname };
|
||||||
} catch {
|
|
||||||
// TODO: Error Handling
|
|
||||||
error(500, "Internal server error");
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user