mirror of
https://github.com/kmc7468/arkvault.git
synced 2026-02-03 23:56:53 +00:00
불필요하게 분리된 컴포넌트 삭제
This commit is contained in:
@@ -54,7 +54,7 @@
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
{#if placeholder && $virtualizer.getVirtualItems().length === 0}
|
||||
{#if placeholder && count === 0}
|
||||
{@render placeholder()}
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
@@ -1,93 +0,0 @@
|
||||
<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";
|
||||
import type { SelectedFile } from "./service";
|
||||
|
||||
import IconMoreVert from "~icons/material-symbols/more-vert";
|
||||
|
||||
interface Props {
|
||||
info: CategoryInfo;
|
||||
isFileRecursive: boolean | undefined;
|
||||
onFileClick: (file: SelectedFile) => void;
|
||||
onFileRemoveClick: (file: SelectedFile) => void;
|
||||
onSubCategoryClick: (subCategory: SelectedCategory) => void;
|
||||
onSubCategoryCreateClick: () => void;
|
||||
onSubCategoryMenuClick: (subCategory: SelectedCategory) => void;
|
||||
}
|
||||
|
||||
let {
|
||||
info,
|
||||
onFileClick,
|
||||
onFileRemoveClick,
|
||||
onSubCategoryClick,
|
||||
onSubCategoryCreateClick,
|
||||
onSubCategoryMenuClick,
|
||||
isFileRecursive = $bindable(),
|
||||
}: Props = $props();
|
||||
|
||||
let lastCategoryId = $state<CategoryInfo["id"] | undefined>();
|
||||
let lastIsFileRecursive = $state<boolean | undefined>();
|
||||
|
||||
let files = $derived(
|
||||
sortEntries(
|
||||
info.files
|
||||
?.map((file) => ({ name: file.name, details: file }))
|
||||
.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">
|
||||
<div class="space-y-4 bg-white p-4">
|
||||
{#if info.id !== "root"}
|
||||
<p class="text-lg font-bold text-gray-800">하위 카테고리</p>
|
||||
{/if}
|
||||
<SubCategories
|
||||
{info}
|
||||
{onSubCategoryClick}
|
||||
{onSubCategoryCreateClick}
|
||||
{onSubCategoryMenuClick}
|
||||
subCategoryMenuIcon={IconMoreVert}
|
||||
/>
|
||||
</div>
|
||||
{#if info.id !== "root"}
|
||||
<div class="space-y-4 bg-white p-4">
|
||||
<div class="flex items-center justify-between">
|
||||
<p class="text-lg font-bold text-gray-800">파일</p>
|
||||
<CheckBox bind:checked={isFileRecursive}>
|
||||
<p class="font-medium">하위 카테고리의 파일</p>
|
||||
</CheckBox>
|
||||
</div>
|
||||
<RowVirtualizer count={files.length} itemHeight={() => 48} itemGap={4}>
|
||||
{#snippet item(index)}
|
||||
{@const { details } = files[index]!}
|
||||
<File
|
||||
info={details}
|
||||
onclick={onFileClick}
|
||||
onRemoveClick={!details.isRecursive ? onFileRemoveClick : undefined}
|
||||
/>
|
||||
{/snippet}
|
||||
{#snippet placeholder()}
|
||||
<p class="text-center text-gray-500">이 카테고리에 추가된 파일이 없어요.</p>
|
||||
{/snippet}
|
||||
</RowVirtualizer>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
@@ -1,2 +0,0 @@
|
||||
export { default } from "./Category.svelte";
|
||||
export * from "./service";
|
||||
@@ -1,4 +0,0 @@
|
||||
export interface SelectedFile {
|
||||
id: number;
|
||||
name: string;
|
||||
}
|
||||
@@ -1,78 +0,0 @@
|
||||
<script lang="ts">
|
||||
import { FileThumbnailButton, RowVirtualizer } from "$lib/components/atoms";
|
||||
import type { SummarizedFileInfo } from "$lib/modules/filesystem";
|
||||
import { formatDate, formatDateSortable, SortBy, sortEntries } from "$lib/utils";
|
||||
|
||||
interface Props {
|
||||
files: SummarizedFileInfo[];
|
||||
onFileClick?: (file: SummarizedFileInfo) => void;
|
||||
}
|
||||
|
||||
let { files, onFileClick }: Props = $props();
|
||||
|
||||
type Row =
|
||||
| { type: "header"; label: string }
|
||||
| { type: "items"; files: SummarizedFileInfo[]; isLast: boolean };
|
||||
|
||||
let rows = $derived.by(() => {
|
||||
const groups = Map.groupBy(
|
||||
files.filter(
|
||||
(file) => file.contentType.startsWith("image/") || file.contentType.startsWith("video/"),
|
||||
),
|
||||
(file) => formatDateSortable(file.createdAt ?? file.lastModifiedAt),
|
||||
);
|
||||
return Array.from(groups.entries())
|
||||
.sort(([dateA], [dateB]) => dateB.localeCompare(dateA))
|
||||
.flatMap(([, entries]) => {
|
||||
const sortedEntries = [...entries];
|
||||
sortEntries(sortedEntries, SortBy.DATE_DESC);
|
||||
|
||||
return [
|
||||
{
|
||||
type: "header",
|
||||
label: formatDate(sortedEntries[0]!.createdAt ?? sortedEntries[0]!.lastModifiedAt),
|
||||
},
|
||||
...Array.from({ length: Math.ceil(sortedEntries.length / 4) }, (_, i) => {
|
||||
const start = i * 4;
|
||||
const end = start + 4;
|
||||
return {
|
||||
type: "items" as const,
|
||||
files: sortedEntries.slice(start, end),
|
||||
isLast: end >= sortedEntries.length,
|
||||
};
|
||||
}),
|
||||
] satisfies Row[];
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
||||
<RowVirtualizer
|
||||
count={rows.length}
|
||||
itemHeight={(index) =>
|
||||
rows[index]!.type === "header" ? 28 : 181 + (rows[index]!.isLast ? 16 : 4)}
|
||||
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.files as file (file.id)}
|
||||
<FileThumbnailButton info={file} onclick={onFileClick} />
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
{/snippet}
|
||||
{#snippet placeholder()}
|
||||
<div class="flex h-full flex-grow items-center justify-center">
|
||||
<p class="text-gray-500">
|
||||
{#if files.length === 0}
|
||||
업로드된 파일이 없어요.
|
||||
{:else}
|
||||
사진 또는 동영상이 없어요.
|
||||
{/if}
|
||||
</p>
|
||||
</div>
|
||||
{/snippet}
|
||||
</RowVirtualizer>
|
||||
@@ -1,4 +1 @@
|
||||
export * from "./Category";
|
||||
export { default as Category } from "./Category";
|
||||
export { default as Gallery } from "./Gallery.svelte";
|
||||
export * from "./modals";
|
||||
|
||||
@@ -1,15 +1,53 @@
|
||||
<script lang="ts">
|
||||
import { onMount } from "svelte";
|
||||
import { goto } from "$app/navigation";
|
||||
import { FullscreenDiv } from "$lib/components/atoms";
|
||||
import { FileThumbnailButton, FullscreenDiv, RowVirtualizer } from "$lib/components/atoms";
|
||||
import { TopBar } from "$lib/components/molecules";
|
||||
import { Gallery } from "$lib/components/organisms";
|
||||
import { bulkGetFileInfo, type MaybeFileInfo } from "$lib/modules/filesystem";
|
||||
import {
|
||||
bulkGetFileInfo,
|
||||
type MaybeFileInfo,
|
||||
type SummarizedFileInfo,
|
||||
} from "$lib/modules/filesystem";
|
||||
import { masterKeyStore } from "$lib/stores";
|
||||
import { formatDate, formatDateSortable, SortBy, sortEntries } from "$lib/utils";
|
||||
|
||||
let { data } = $props();
|
||||
|
||||
type Row =
|
||||
| { type: "header"; label: string }
|
||||
| { type: "items"; files: SummarizedFileInfo[]; isLast: boolean };
|
||||
|
||||
let files: MaybeFileInfo[] = $state([]);
|
||||
let rows: Row[] = $derived.by(() => {
|
||||
const groups = Map.groupBy(
|
||||
files
|
||||
.filter((file) => file.exists)
|
||||
.filter(
|
||||
(file) => file.contentType.startsWith("image/") || file.contentType.startsWith("video/"),
|
||||
),
|
||||
(file) => formatDateSortable(file.createdAt ?? file.lastModifiedAt),
|
||||
);
|
||||
return Array.from(groups.entries())
|
||||
.sort(([dateA], [dateB]) => dateB.localeCompare(dateA))
|
||||
.flatMap(([, entries]) => {
|
||||
const sortedEntries = sortEntries([...entries], SortBy.DATE_DESC);
|
||||
return [
|
||||
{
|
||||
type: "header",
|
||||
label: formatDate(sortedEntries[0]!.createdAt ?? sortedEntries[0]!.lastModifiedAt),
|
||||
},
|
||||
...Array.from({ length: Math.ceil(sortedEntries.length / 4) }, (_, i) => {
|
||||
const start = i * 4;
|
||||
const end = start + 4;
|
||||
return {
|
||||
type: "items" as const,
|
||||
files: sortedEntries.slice(start, end),
|
||||
isLast: end >= sortedEntries.length,
|
||||
};
|
||||
}),
|
||||
];
|
||||
});
|
||||
});
|
||||
|
||||
onMount(async () => {
|
||||
files = Array.from((await bulkGetFileInfo(data.files, $masterKeyStore?.get(1)?.key!)).values());
|
||||
@@ -22,8 +60,37 @@
|
||||
|
||||
<TopBar title="사진 및 동영상" />
|
||||
<FullscreenDiv>
|
||||
<Gallery
|
||||
files={files.filter((file) => file?.exists)}
|
||||
onFileClick={({ id }) => goto(`/file/${id}?from=gallery`)}
|
||||
<RowVirtualizer
|
||||
count={rows.length}
|
||||
itemHeight={(index) =>
|
||||
rows[index]!.type === "header" ? 28 : 181 + (rows[index]!.isLast ? 16 : 4)}
|
||||
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.files as file (file.id)}
|
||||
<FileThumbnailButton
|
||||
info={file}
|
||||
onclick={() => goto(`/file/${file.id}?from=gallery`)}
|
||||
/>
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
{/snippet}
|
||||
{#snippet placeholder()}
|
||||
<div class="flex h-full flex-grow items-center justify-center">
|
||||
<p class="text-gray-500">
|
||||
{#if files.length === 0}
|
||||
업로드된 파일이 없어요.
|
||||
{:else}
|
||||
사진 또는 동영상이 없어요.
|
||||
{/if}
|
||||
</p>
|
||||
</div>
|
||||
{/snippet}
|
||||
</RowVirtualizer>
|
||||
</FullscreenDiv>
|
||||
|
||||
@@ -1,13 +1,16 @@
|
||||
<script lang="ts">
|
||||
import { goto } from "$app/navigation";
|
||||
import { TopBar } from "$lib/components/molecules";
|
||||
import { Category, CategoryCreateModal } from "$lib/components/organisms";
|
||||
import { CheckBox, RowVirtualizer } from "$lib/components/atoms";
|
||||
import { SubCategories, TopBar } from "$lib/components/molecules";
|
||||
import { CategoryCreateModal } from "$lib/components/organisms";
|
||||
import { updateCategoryInfo } from "$lib/indexedDB";
|
||||
import { getCategoryInfo, type MaybeCategoryInfo } from "$lib/modules/filesystem";
|
||||
import { masterKeyStore } from "$lib/stores";
|
||||
import { HybridPromise } from "$lib/utils";
|
||||
import { HybridPromise, sortEntries } from "$lib/utils";
|
||||
import CategoryDeleteModal from "./CategoryDeleteModal.svelte";
|
||||
import CategoryMenuBottomSheet from "./CategoryMenuBottomSheet.svelte";
|
||||
import CategoryRenameModal from "./CategoryRenameModal.svelte";
|
||||
import File from "./File.svelte";
|
||||
import {
|
||||
createContext,
|
||||
requestCategoryCreation,
|
||||
@@ -16,6 +19,8 @@
|
||||
requestCategoryDeletion,
|
||||
} from "./service.svelte";
|
||||
|
||||
import IconMoreVert from "~icons/material-symbols/more-vert";
|
||||
|
||||
let { data } = $props();
|
||||
let context = createContext();
|
||||
|
||||
@@ -26,6 +31,30 @@
|
||||
let isCategoryRenameModalOpen = $state(false);
|
||||
let isCategoryDeleteModalOpen = $state(false);
|
||||
|
||||
let lastCategoryId: CategoryId | undefined = $state();
|
||||
let lastIsFileRecursive: boolean | undefined = $state();
|
||||
|
||||
let files = $derived(
|
||||
sortEntries(
|
||||
info?.files
|
||||
?.map((file) => ({ name: file.name, details: file }))
|
||||
.filter(({ details }) => info?.isFileRecursive || !details.isRecursive) ?? [],
|
||||
),
|
||||
);
|
||||
|
||||
$effect(() => {
|
||||
if (!info || info.id === "root" || info.isFileRecursive === undefined) return;
|
||||
if (lastCategoryId !== info.id) {
|
||||
lastCategoryId = info.id;
|
||||
lastIsFileRecursive = info.isFileRecursive;
|
||||
return;
|
||||
}
|
||||
if (lastIsFileRecursive === info.isFileRecursive) return;
|
||||
|
||||
lastIsFileRecursive = info.isFileRecursive;
|
||||
void updateCategoryInfo(info.id, { isFileRecursive: info.isFileRecursive });
|
||||
});
|
||||
|
||||
$effect(() => {
|
||||
HybridPromise.resolve(getCategoryInfo(data.id, $masterKeyStore?.get(1)?.key!)).then(
|
||||
(result) => {
|
||||
@@ -41,28 +70,58 @@
|
||||
<title>카테고리</title>
|
||||
</svelte:head>
|
||||
|
||||
{#if info?.exists}
|
||||
{#if info?.id !== "root"}
|
||||
<TopBar title={info?.name} />
|
||||
{/if}
|
||||
<div class="min-h-full bg-gray-100 pb-[5.5em]">
|
||||
{#if info?.exists}
|
||||
<div class="space-y-4">
|
||||
<div class="space-y-4 bg-white p-4">
|
||||
{#if info.id !== "root"}
|
||||
<TopBar title={info.name} />
|
||||
<p class="text-lg font-bold text-gray-800">하위 카테고리</p>
|
||||
{/if}
|
||||
<div class="min-h-full bg-gray-100 pb-[5.5em]">
|
||||
<Category
|
||||
bind:isFileRecursive={info.isFileRecursive}
|
||||
<SubCategories
|
||||
{info}
|
||||
onFileClick={({ id }) => goto(`/file/${id}?from=category`)}
|
||||
onFileRemoveClick={async ({ id }) => {
|
||||
await requestFileRemovalFromCategory(id, data.id as number);
|
||||
void getCategoryInfo(data.id, $masterKeyStore?.get(1)?.key!); // TODO: FIXME
|
||||
}}
|
||||
onSubCategoryClick={({ id }) => goto(`/category/${id}`)}
|
||||
onSubCategoryCreateClick={() => (isCategoryCreateModalOpen = true)}
|
||||
onSubCategoryMenuClick={(subCategory) => {
|
||||
context.selectedCategory = subCategory;
|
||||
isCategoryMenuBottomSheetOpen = true;
|
||||
}}
|
||||
subCategoryMenuIcon={IconMoreVert}
|
||||
/>
|
||||
</div>
|
||||
{/if}
|
||||
{#if info.id !== "root"}
|
||||
<div class="space-y-4 bg-white p-4">
|
||||
<div class="flex items-center justify-between">
|
||||
<p class="text-lg font-bold text-gray-800">파일</p>
|
||||
<CheckBox bind:checked={info.isFileRecursive}>
|
||||
<p class="font-medium">하위 카테고리의 파일</p>
|
||||
</CheckBox>
|
||||
</div>
|
||||
<RowVirtualizer count={files.length} itemHeight={() => 48} itemGap={4}>
|
||||
{#snippet item(index)}
|
||||
{@const { details } = files[index]!}
|
||||
<File
|
||||
info={details}
|
||||
onclick={({ id }) => goto(`/file/${id}?from=category`)}
|
||||
onRemoveClick={!details.isRecursive
|
||||
? async ({ id }) => {
|
||||
await requestFileRemovalFromCategory(id, data.id as number);
|
||||
void getCategoryInfo(data.id, $masterKeyStore?.get(1)?.key!); // TODO: FIXME
|
||||
}
|
||||
: undefined}
|
||||
/>
|
||||
{/snippet}
|
||||
{#snippet placeholder()}
|
||||
<p class="text-center text-gray-500">이 카테고리에 추가된 파일이 없어요.</p>
|
||||
{/snippet}
|
||||
</RowVirtualizer>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<CategoryCreateModal
|
||||
bind:isOpen={isCategoryCreateModalOpen}
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
import { DirectoryEntryLabel } from "$lib/components/molecules";
|
||||
import { getFileThumbnail } from "$lib/modules/file";
|
||||
import type { CategoryFileInfo } from "$lib/modules/filesystem";
|
||||
import type { SelectedFile } from "./service";
|
||||
import type { SelectedFile } from "./service.svelte";
|
||||
|
||||
import IconClose from "~icons/material-symbols/close";
|
||||
|
||||
@@ -5,6 +5,11 @@ import { trpc } from "$trpc/client";
|
||||
|
||||
export { requestCategoryCreation, requestFileRemovalFromCategory } from "$lib/services/category";
|
||||
|
||||
export interface SelectedFile {
|
||||
id: number;
|
||||
name: string;
|
||||
}
|
||||
|
||||
export const createContext = () => {
|
||||
const context = $state({
|
||||
selectedCategory: undefined as SelectedCategory | undefined,
|
||||
|
||||
@@ -95,11 +95,11 @@
|
||||
|
||||
<input bind:this={fileInput} onchange={uploadFile} type="file" multiple class="hidden" />
|
||||
|
||||
{#if info?.exists}
|
||||
<div class="flex h-full flex-col">
|
||||
<div class="flex h-full flex-col">
|
||||
{#if showTopBar}
|
||||
<TopBar title={info.name} class="flex-shrink-0" />
|
||||
<TopBar title={info?.name} class="flex-shrink-0" />
|
||||
{/if}
|
||||
{#if info?.exists}
|
||||
<div class={["flex flex-grow flex-col px-4 pb-4", !showTopBar && "pt-4"]}>
|
||||
<div class="flex gap-x-2">
|
||||
<UploadStatusCard onclick={() => goto("/file/uploads")} />
|
||||
@@ -123,8 +123,8 @@
|
||||
/>
|
||||
{/key}
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<FloatingButton
|
||||
icon={IconAdd}
|
||||
|
||||
Reference in New Issue
Block a user