mirror of
https://github.com/kmc7468/arkvault.git
synced 2025-12-14 22:08:45 +00:00
기존에 제작된 모달들을 ActionModal 컴포넌트 기반으로 재구성
This commit is contained in:
@@ -23,13 +23,16 @@
|
|||||||
<!-- svelte-ignore a11y_click_events_have_key_events -->
|
<!-- svelte-ignore a11y_click_events_have_key_events -->
|
||||||
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
||||||
<div onclick={closeBottomSheet} class="fixed inset-0 z-10 flex items-end justify-center">
|
<div onclick={closeBottomSheet} class="fixed inset-0 z-10 flex items-end justify-center">
|
||||||
<div class="absolute inset-0 bg-black bg-opacity-50" transition:fade={{ duration: 100 }}></div>
|
<div
|
||||||
|
class="absolute inset-0 bg-black bg-opacity-50"
|
||||||
|
transition:fade|global={{ duration: 100 }}
|
||||||
|
></div>
|
||||||
<div class="z-20 w-full">
|
<div class="z-20 w-full">
|
||||||
<AdaptiveDiv>
|
<AdaptiveDiv>
|
||||||
<div
|
<div
|
||||||
onclick={(e) => e.stopPropagation()}
|
onclick={(e) => e.stopPropagation()}
|
||||||
class="flex max-h-[70vh] min-h-[30vh] overflow-y-auto rounded-t-2xl bg-white px-4"
|
class="flex max-h-[70vh] min-h-[30vh] overflow-y-auto rounded-t-2xl bg-white px-4"
|
||||||
transition:fly={{ y: 100, duration: 200 }}
|
transition:fly|global={{ y: 100, duration: 200 }}
|
||||||
>
|
>
|
||||||
{@render children?.()}
|
{@render children?.()}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -20,7 +20,7 @@
|
|||||||
<div
|
<div
|
||||||
onclick={onclose || (() => (isOpen = false))}
|
onclick={onclose || (() => (isOpen = false))}
|
||||||
class="fixed inset-0 z-10 bg-black bg-opacity-50"
|
class="fixed inset-0 z-10 bg-black bg-opacity-50"
|
||||||
transition:fade={{ duration: 100 }}
|
transition:fade|global={{ duration: 100 }}
|
||||||
>
|
>
|
||||||
<AdaptiveDiv class="h-full">
|
<AdaptiveDiv class="h-full">
|
||||||
<div class="flex h-full items-center justify-center px-4">
|
<div class="flex h-full items-center justify-center px-4">
|
||||||
|
|||||||
@@ -3,10 +3,10 @@
|
|||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
isOpen: boolean;
|
isOpen: boolean;
|
||||||
oncreate: (name: string) => Promise<boolean>;
|
onCreateClick: (name: string) => Promise<boolean>;
|
||||||
}
|
}
|
||||||
|
|
||||||
let { isOpen = $bindable(), oncreate }: Props = $props();
|
let { isOpen = $bindable(), onCreateClick }: Props = $props();
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<TextInputModal
|
<TextInputModal
|
||||||
@@ -14,5 +14,5 @@
|
|||||||
title="새 카테고리"
|
title="새 카테고리"
|
||||||
placeholder="카테고리 이름"
|
placeholder="카테고리 이름"
|
||||||
submitText="만들기"
|
submitText="만들기"
|
||||||
onSubmitClick={oncreate}
|
onSubmitClick={onCreateClick}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -28,6 +28,11 @@ export const formatNetworkSpeed = (speed: number) => {
|
|||||||
return `${(speed / 1000 / 1000 / 1000).toFixed(1)} Gbps`;
|
return `${(speed / 1000 / 1000 / 1000).toFixed(1)} Gbps`;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const truncateString = (str: string, maxLength = 20) => {
|
||||||
|
if (str.length <= maxLength) return str;
|
||||||
|
return `${str.slice(0, maxLength)}...`;
|
||||||
|
};
|
||||||
|
|
||||||
export enum SortBy {
|
export enum SortBy {
|
||||||
NAME_ASC,
|
NAME_ASC,
|
||||||
NAME_DESC,
|
NAME_DESC,
|
||||||
|
|||||||
@@ -50,7 +50,7 @@
|
|||||||
|
|
||||||
<CategoryCreateModal
|
<CategoryCreateModal
|
||||||
bind:isOpen={isCategoryCreateModalOpen}
|
bind:isOpen={isCategoryCreateModalOpen}
|
||||||
oncreate={async (name: string) => {
|
onCreateClick={async (name: string) => {
|
||||||
if (await requestCategoryCreation(name, $category!.id, $masterKeyStore?.get(1)!)) {
|
if (await requestCategoryCreation(name, $category!.id, $masterKeyStore?.get(1)!)) {
|
||||||
category = getCategoryInfo($category!.id, $masterKeyStore?.get(1)?.key!); // TODO: FIXME
|
category = getCategoryInfo($category!.id, $masterKeyStore?.get(1)?.key!); // TODO: FIXME
|
||||||
return true;
|
return true;
|
||||||
|
|||||||
@@ -1,23 +1,20 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { Button, Modal } from "$lib/components/atoms";
|
import { ActionModal } from "$lib/components/molecules";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
onContinueClick: () => void;
|
|
||||||
isOpen: boolean;
|
isOpen: boolean;
|
||||||
|
onContinueClick: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
let { onContinueClick, isOpen = $bindable() }: Props = $props();
|
let { isOpen = $bindable(), onContinueClick }: Props = $props();
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<Modal bind:isOpen>
|
<ActionModal
|
||||||
<div class="space-y-4">
|
bind:isOpen
|
||||||
<div class="space-y-2 break-keep">
|
title="내보내지 않고 계속할까요?"
|
||||||
<p class="text-xl font-bold">내보내지 않고 계속할까요?</p>
|
cancelText="아니요"
|
||||||
<p>암호 키 파일은 유출 방지를 위해 이 화면에서만 저장할 수 있어요.</p>
|
confirmText="계속할게요"
|
||||||
</div>
|
onConfirmClick={onContinueClick}
|
||||||
<div class="flex gap-x-2">
|
>
|
||||||
<Button color="gray" onclick={() => (isOpen = false)} class="flex-1">아니요</Button>
|
<p>암호 키 파일은 유출 방지를 위해 이 화면에서만 저장할 수 있어요.</p>
|
||||||
<Button onclick={onContinueClick} class="flex-1">계속할게요</Button>
|
</ActionModal>
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</Modal>
|
|
||||||
|
|||||||
@@ -2,31 +2,32 @@
|
|||||||
import type { Writable } from "svelte/store";
|
import type { Writable } from "svelte/store";
|
||||||
import { goto } from "$app/navigation";
|
import { goto } from "$app/navigation";
|
||||||
import { TopBar } from "$lib/components";
|
import { TopBar } from "$lib/components";
|
||||||
import { CategoryCreateModal, RenameModal } from "$lib/components/organisms";
|
import { CategoryCreateModal } from "$lib/components/organisms";
|
||||||
import { getCategoryInfo, type CategoryInfo } from "$lib/modules/filesystem";
|
import { getCategoryInfo, type CategoryInfo } from "$lib/modules/filesystem";
|
||||||
import type { SelectedCategory } from "$lib/molecules/Categories";
|
|
||||||
import Category from "$lib/organisms/Category";
|
import Category from "$lib/organisms/Category";
|
||||||
import { masterKeyStore } from "$lib/stores";
|
import { masterKeyStore } from "$lib/stores";
|
||||||
|
import CategoryDeleteModal from "./CategoryDeleteModal.svelte";
|
||||||
import CategoryMenuBottomSheet from "./CategoryMenuBottomSheet.svelte";
|
import CategoryMenuBottomSheet from "./CategoryMenuBottomSheet.svelte";
|
||||||
import DeleteCategoryModal from "./DeleteCategoryModal.svelte";
|
import CategoryRenameModal from "./CategoryRenameModal.svelte";
|
||||||
import {
|
import {
|
||||||
|
createContext,
|
||||||
requestCategoryCreation,
|
requestCategoryCreation,
|
||||||
requestFileRemovalFromCategory,
|
requestFileRemovalFromCategory,
|
||||||
requestCategoryRename,
|
requestCategoryRename,
|
||||||
requestCategoryDeletion,
|
requestCategoryDeletion,
|
||||||
} from "./service";
|
} from "./service.svelte";
|
||||||
|
|
||||||
let { data } = $props();
|
let { data } = $props();
|
||||||
|
let context = createContext();
|
||||||
|
|
||||||
let info: Writable<CategoryInfo | null> | undefined = $state();
|
let info: Writable<CategoryInfo | null> | undefined = $state();
|
||||||
let selectedSubCategory: SelectedCategory | undefined = $state();
|
|
||||||
|
|
||||||
let isFileRecursive = $state(false);
|
let isFileRecursive = $state(false);
|
||||||
|
|
||||||
let isCategoryCreateModalOpen = $state(false);
|
let isCategoryCreateModalOpen = $state(false);
|
||||||
let isSubCategoryMenuBottomSheetOpen = $state(false);
|
let isCategoryMenuBottomSheetOpen = $state(false);
|
||||||
let isCategoryRenameModalOpen = $state(false);
|
let isCategoryRenameModalOpen = $state(false);
|
||||||
let isDeleteCategoryModalOpen = $state(false);
|
let isCategoryDeleteModalOpen = $state(false);
|
||||||
|
|
||||||
$effect(() => {
|
$effect(() => {
|
||||||
info = getCategoryInfo(data.id, $masterKeyStore?.get(1)?.key!);
|
info = getCategoryInfo(data.id, $masterKeyStore?.get(1)?.key!);
|
||||||
@@ -54,8 +55,8 @@
|
|||||||
onSubCategoryClick={({ id }) => goto(`/category/${id}`)}
|
onSubCategoryClick={({ id }) => goto(`/category/${id}`)}
|
||||||
onSubCategoryCreateClick={() => (isCategoryCreateModalOpen = true)}
|
onSubCategoryCreateClick={() => (isCategoryCreateModalOpen = true)}
|
||||||
onSubCategoryMenuClick={(subCategory) => {
|
onSubCategoryMenuClick={(subCategory) => {
|
||||||
selectedSubCategory = subCategory;
|
context.selectedCategory = subCategory;
|
||||||
isSubCategoryMenuBottomSheetOpen = true;
|
isCategoryMenuBottomSheetOpen = true;
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
{/if}
|
{/if}
|
||||||
@@ -64,7 +65,7 @@
|
|||||||
|
|
||||||
<CategoryCreateModal
|
<CategoryCreateModal
|
||||||
bind:isOpen={isCategoryCreateModalOpen}
|
bind:isOpen={isCategoryCreateModalOpen}
|
||||||
oncreate={async (name: string) => {
|
onCreateClick={async (name: string) => {
|
||||||
if (await requestCategoryCreation(name, data.id, $masterKeyStore?.get(1)!)) {
|
if (await requestCategoryCreation(name, data.id, $masterKeyStore?.get(1)!)) {
|
||||||
info = getCategoryInfo(data.id, $masterKeyStore?.get(1)?.key!); // TODO: FIXME
|
info = getCategoryInfo(data.id, $masterKeyStore?.get(1)?.key!); // TODO: FIXME
|
||||||
return true;
|
return true;
|
||||||
@@ -74,35 +75,30 @@
|
|||||||
/>
|
/>
|
||||||
|
|
||||||
<CategoryMenuBottomSheet
|
<CategoryMenuBottomSheet
|
||||||
bind:isOpen={isSubCategoryMenuBottomSheetOpen}
|
bind:isOpen={isCategoryMenuBottomSheetOpen}
|
||||||
bind:selectedCategory={selectedSubCategory}
|
|
||||||
onRenameClick={() => {
|
onRenameClick={() => {
|
||||||
isSubCategoryMenuBottomSheetOpen = false;
|
isCategoryMenuBottomSheetOpen = false;
|
||||||
isCategoryRenameModalOpen = true;
|
isCategoryRenameModalOpen = true;
|
||||||
}}
|
}}
|
||||||
onDeleteClick={() => {
|
onDeleteClick={() => {
|
||||||
isSubCategoryMenuBottomSheetOpen = false;
|
isCategoryMenuBottomSheetOpen = false;
|
||||||
isDeleteCategoryModalOpen = true;
|
isCategoryDeleteModalOpen = true;
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<RenameModal
|
<CategoryRenameModal
|
||||||
bind:isOpen={isCategoryRenameModalOpen}
|
bind:isOpen={isCategoryRenameModalOpen}
|
||||||
onbeforeclose={() => (selectedSubCategory = undefined)}
|
|
||||||
originalName={selectedSubCategory?.name}
|
|
||||||
onRenameClick={async (newName: string) => {
|
onRenameClick={async (newName: string) => {
|
||||||
if (await requestCategoryRename(selectedSubCategory!, newName)) {
|
if (await requestCategoryRename(context.selectedCategory!, newName)) {
|
||||||
info = getCategoryInfo(data.id, $masterKeyStore?.get(1)?.key!); // TODO: FIXME
|
info = getCategoryInfo(data.id, $masterKeyStore?.get(1)?.key!); // TODO: FIXME
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<DeleteCategoryModal
|
<CategoryDeleteModal
|
||||||
bind:isOpen={isDeleteCategoryModalOpen}
|
bind:isOpen={isCategoryDeleteModalOpen}
|
||||||
bind:selectedCategory={selectedSubCategory}
|
|
||||||
onDeleteClick={async () => {
|
onDeleteClick={async () => {
|
||||||
if (selectedSubCategory) {
|
if (await requestCategoryDeletion(context.selectedCategory!)) {
|
||||||
await requestCategoryDeletion(selectedSubCategory);
|
|
||||||
info = getCategoryInfo(data.id, $masterKeyStore?.get(1)?.key!); // TODO: FIXME
|
info = getCategoryInfo(data.id, $masterKeyStore?.get(1)?.key!); // TODO: FIXME
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|||||||
29
src/routes/(main)/category/[[id]]/CategoryDeleteModal.svelte
Normal file
29
src/routes/(main)/category/[[id]]/CategoryDeleteModal.svelte
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { ActionModal } from "$lib/components/molecules";
|
||||||
|
import { truncateString } from "$lib/modules/util";
|
||||||
|
import { useContext } from "./service.svelte";
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
isOpen: boolean;
|
||||||
|
onDeleteClick: () => Promise<boolean>;
|
||||||
|
}
|
||||||
|
|
||||||
|
let { isOpen = $bindable(), onDeleteClick }: Props = $props();
|
||||||
|
let context = useContext();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
{#if context.selectedCategory}
|
||||||
|
{@const { name } = context.selectedCategory}
|
||||||
|
<ActionModal
|
||||||
|
bind:isOpen
|
||||||
|
title="'{truncateString(name)}' 카테고리를 삭제할까요?"
|
||||||
|
cancelText="아니요"
|
||||||
|
confirmText="삭제할게요"
|
||||||
|
onConfirmClick={onDeleteClick}
|
||||||
|
>
|
||||||
|
<p>
|
||||||
|
모든 하위 카테고리도 함께 삭제돼요. <br />
|
||||||
|
하지만 카테고리에 추가된 파일들은 삭제되지 않아요.
|
||||||
|
</p>
|
||||||
|
</ActionModal>
|
||||||
|
{/if}
|
||||||
@@ -1,36 +1,26 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { BottomSheet } from "$lib/components";
|
import { BottomSheet } from "$lib/components";
|
||||||
import { EntryButton } from "$lib/components/atoms";
|
import { EntryButton } from "$lib/components/atoms";
|
||||||
import type { SelectedCategory } from "$lib/molecules/Categories";
|
import { useContext } from "./service.svelte";
|
||||||
|
|
||||||
import IconCategory from "~icons/material-symbols/category";
|
import IconCategory from "~icons/material-symbols/category";
|
||||||
import IconEdit from "~icons/material-symbols/edit";
|
import IconEdit from "~icons/material-symbols/edit";
|
||||||
import IconDelete from "~icons/material-symbols/delete";
|
import IconDelete from "~icons/material-symbols/delete";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
onRenameClick: () => void;
|
|
||||||
onDeleteClick: () => void;
|
|
||||||
isOpen: boolean;
|
isOpen: boolean;
|
||||||
selectedCategory: SelectedCategory | undefined;
|
onDeleteClick: () => void;
|
||||||
|
onRenameClick: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
let {
|
let { isOpen = $bindable(), onDeleteClick, onRenameClick }: Props = $props();
|
||||||
onRenameClick,
|
let context = useContext();
|
||||||
onDeleteClick,
|
|
||||||
isOpen = $bindable(),
|
|
||||||
selectedCategory = $bindable(),
|
|
||||||
}: Props = $props();
|
|
||||||
|
|
||||||
const closeBottomSheet = () => {
|
|
||||||
isOpen = false;
|
|
||||||
selectedCategory = undefined;
|
|
||||||
};
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<BottomSheet bind:isOpen onclose={closeBottomSheet}>
|
{#if context.selectedCategory}
|
||||||
<div class="w-full py-4">
|
{@const { name } = context.selectedCategory}
|
||||||
{#if selectedCategory}
|
<BottomSheet bind:isOpen>
|
||||||
{@const { name } = selectedCategory}
|
<div class="w-full py-4">
|
||||||
<div class="flex h-12 items-center gap-x-4 p-2">
|
<div class="flex h-12 items-center gap-x-4 p-2">
|
||||||
<div class="flex-shrink-0 text-lg">
|
<div class="flex-shrink-0 text-lg">
|
||||||
<IconCategory />
|
<IconCategory />
|
||||||
@@ -40,18 +30,18 @@
|
|||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="my-2 h-px w-full bg-gray-200"></div>
|
<div class="my-2 h-px w-full bg-gray-200"></div>
|
||||||
{/if}
|
<EntryButton onclick={onRenameClick} class="w-full">
|
||||||
<EntryButton onclick={onRenameClick} class="w-full">
|
<div class="flex h-8 items-center gap-x-4">
|
||||||
<div class="flex h-8 items-center gap-x-4">
|
<IconEdit class="text-lg" />
|
||||||
<IconEdit class="text-lg" />
|
<p class="font-medium">이름 바꾸기</p>
|
||||||
<p class="font-medium">이름 바꾸기</p>
|
</div>
|
||||||
</div>
|
</EntryButton>
|
||||||
</EntryButton>
|
<EntryButton onclick={onDeleteClick} class="w-full">
|
||||||
<EntryButton onclick={onDeleteClick} class="w-full">
|
<div class="flex h-8 items-center gap-x-4 text-red-500">
|
||||||
<div class="flex h-8 items-center gap-x-4 text-red-500">
|
<IconDelete class="text-lg" />
|
||||||
<IconDelete class="text-lg" />
|
<p class="font-medium">삭제하기</p>
|
||||||
<p class="font-medium">삭제하기</p>
|
</div>
|
||||||
</div>
|
</EntryButton>
|
||||||
</EntryButton>
|
</div>
|
||||||
</div>
|
</BottomSheet>
|
||||||
</BottomSheet>
|
{/if}
|
||||||
|
|||||||
17
src/routes/(main)/category/[[id]]/CategoryRenameModal.svelte
Normal file
17
src/routes/(main)/category/[[id]]/CategoryRenameModal.svelte
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { RenameModal } from "$lib/components/organisms";
|
||||||
|
import { useContext } from "./service.svelte";
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
isOpen: boolean;
|
||||||
|
onRenameClick: (newName: string) => Promise<boolean>;
|
||||||
|
}
|
||||||
|
|
||||||
|
let { isOpen = $bindable(), onRenameClick }: Props = $props();
|
||||||
|
let context = useContext();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
{#if context.selectedCategory}
|
||||||
|
{@const { name } = context.selectedCategory}
|
||||||
|
<RenameModal bind:isOpen originalName={name} {onRenameClick} />
|
||||||
|
{/if}
|
||||||
@@ -1,47 +0,0 @@
|
|||||||
<script lang="ts">
|
|
||||||
import { Button, Modal } from "$lib/components/atoms";
|
|
||||||
import type { SelectedCategory } from "$lib/molecules/Categories";
|
|
||||||
|
|
||||||
interface Props {
|
|
||||||
onDeleteClick: () => Promise<boolean>;
|
|
||||||
isOpen: boolean;
|
|
||||||
selectedCategory: SelectedCategory | undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
let { onDeleteClick, isOpen = $bindable(), selectedCategory = $bindable() }: Props = $props();
|
|
||||||
|
|
||||||
const closeModal = () => {
|
|
||||||
isOpen = false;
|
|
||||||
selectedCategory = undefined;
|
|
||||||
};
|
|
||||||
|
|
||||||
const deleteEntry = async () => {
|
|
||||||
// TODO: Validation
|
|
||||||
|
|
||||||
if (await onDeleteClick()) {
|
|
||||||
closeModal();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<Modal bind:isOpen onclose={closeModal}>
|
|
||||||
{#if selectedCategory}
|
|
||||||
{@const { name } = selectedCategory}
|
|
||||||
{@const nameShort = name.length > 20 ? `${name.slice(0, 20)}...` : name}
|
|
||||||
<div class="space-y-4">
|
|
||||||
<div class="space-y-2 break-keep">
|
|
||||||
<p class="text-xl font-bold">
|
|
||||||
'{nameShort}' 카테고리를 삭제할까요?
|
|
||||||
</p>
|
|
||||||
<p>
|
|
||||||
모든 하위 카테고리도 함께 삭제돼요. <br />
|
|
||||||
하지만 카테고리에 추가된 파일들은 삭제되지 않아요.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<div class="flex gap-x-2">
|
|
||||||
<Button color="gray" onclick={closeModal} class="flex-1">아니요</Button>
|
|
||||||
<Button onclick={deleteEntry} class="flex-1">삭제할게요</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{/if}
|
|
||||||
</Modal>
|
|
||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { getContext, setContext } from "svelte";
|
||||||
import { callPostApi } from "$lib/hooks";
|
import { callPostApi } from "$lib/hooks";
|
||||||
import { encryptString } from "$lib/modules/crypto";
|
import { encryptString } from "$lib/modules/crypto";
|
||||||
import type { SelectedCategory } from "$lib/molecules/Categories";
|
import type { SelectedCategory } from "$lib/molecules/Categories";
|
||||||
@@ -5,6 +6,17 @@ import type { CategoryRenameRequest } from "$lib/server/schemas";
|
|||||||
|
|
||||||
export { requestCategoryCreation, requestFileRemovalFromCategory } from "$lib/services/category";
|
export { requestCategoryCreation, requestFileRemovalFromCategory } from "$lib/services/category";
|
||||||
|
|
||||||
|
export const createContext = () => {
|
||||||
|
const context = $state({
|
||||||
|
selectedCategory: undefined as SelectedCategory | undefined,
|
||||||
|
});
|
||||||
|
return setContext("context", context);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useContext = () => {
|
||||||
|
return getContext<ReturnType<typeof createContext>>("context");
|
||||||
|
};
|
||||||
|
|
||||||
export const requestCategoryRename = async (category: SelectedCategory, newName: string) => {
|
export const requestCategoryRename = async (category: SelectedCategory, newName: string) => {
|
||||||
const newNameEncrypted = await encryptString(newName, category.dataKey);
|
const newNameEncrypted = await encryptString(newName, category.dataKey);
|
||||||
|
|
||||||
@@ -4,49 +4,43 @@
|
|||||||
import { goto } from "$app/navigation";
|
import { goto } from "$app/navigation";
|
||||||
import { TopBar } from "$lib/components";
|
import { TopBar } from "$lib/components";
|
||||||
import { FloatingButton } from "$lib/components/atoms";
|
import { FloatingButton } from "$lib/components/atoms";
|
||||||
import { RenameModal } from "$lib/components/organisms";
|
|
||||||
import { getDirectoryInfo, type DirectoryInfo } from "$lib/modules/filesystem";
|
import { getDirectoryInfo, type DirectoryInfo } from "$lib/modules/filesystem";
|
||||||
import { masterKeyStore, hmacSecretStore } from "$lib/stores";
|
import { masterKeyStore, hmacSecretStore } from "$lib/stores";
|
||||||
import CreateBottomSheet from "./CreateBottomSheet.svelte";
|
import DirectoryCreateModal from "./DirectoryCreateModal.svelte";
|
||||||
import CreateDirectoryModal from "./CreateDirectoryModal.svelte";
|
|
||||||
import DeleteDirectoryEntryModal from "./DeleteDirectoryEntryModal.svelte";
|
|
||||||
import DirectoryEntries from "./DirectoryEntries";
|
import DirectoryEntries from "./DirectoryEntries";
|
||||||
import DirectoryEntryMenuBottomSheet from "./DirectoryEntryMenuBottomSheet.svelte";
|
|
||||||
import DownloadStatusCard from "./DownloadStatusCard.svelte";
|
import DownloadStatusCard from "./DownloadStatusCard.svelte";
|
||||||
import DuplicateFileModal from "./DuplicateFileModal.svelte";
|
import DuplicateFileModal from "./DuplicateFileModal.svelte";
|
||||||
|
import EntryCreateBottomSheet from "./EntryCreateBottomSheet.svelte";
|
||||||
|
import EntryDeleteModal from "./EntryDeleteModal.svelte";
|
||||||
|
import EntryMenuBottomSheet from "./EntryMenuBottomSheet.svelte";
|
||||||
|
import EntryRenameModal from "./EntryRenameModal.svelte";
|
||||||
import UploadStatusCard from "./UploadStatusCard.svelte";
|
import UploadStatusCard from "./UploadStatusCard.svelte";
|
||||||
import {
|
import {
|
||||||
|
createContext,
|
||||||
requestHmacSecretDownload,
|
requestHmacSecretDownload,
|
||||||
requestDirectoryCreation,
|
requestDirectoryCreation,
|
||||||
requestFileUpload,
|
requestFileUpload,
|
||||||
requestDirectoryEntryRename,
|
requestEntryRename,
|
||||||
requestDirectoryEntryDeletion,
|
requestEntryDeletion,
|
||||||
type SelectedDirectoryEntry,
|
} from "./service.svelte";
|
||||||
} from "./service";
|
|
||||||
|
|
||||||
import IconAdd from "~icons/material-symbols/add";
|
import IconAdd from "~icons/material-symbols/add";
|
||||||
|
|
||||||
let { data } = $props();
|
let { data } = $props();
|
||||||
|
let context = createContext();
|
||||||
|
|
||||||
let info: Writable<DirectoryInfo | null> | undefined = $state();
|
let info: Writable<DirectoryInfo | null> | undefined = $state();
|
||||||
let fileInput: HTMLInputElement | undefined = $state();
|
let fileInput: HTMLInputElement | undefined = $state();
|
||||||
let resolveForDuplicateFileModal: ((res: boolean) => void) | undefined = $state();
|
|
||||||
let duplicatedFile: File | undefined = $state();
|
let duplicatedFile: File | undefined = $state();
|
||||||
let selectedEntry: SelectedDirectoryEntry | undefined = $state();
|
let resolveForDuplicateFileModal: ((res: boolean) => void) | undefined = $state();
|
||||||
|
|
||||||
let isCreateBottomSheetOpen = $state(false);
|
let isEntryCreateBottomSheetOpen = $state(false);
|
||||||
let isCreateDirectoryModalOpen = $state(false);
|
let isDirectoryCreateModalOpen = $state(false);
|
||||||
let isDuplicateFileModalOpen = $state(false);
|
let isDuplicateFileModalOpen = $state(false);
|
||||||
|
|
||||||
let isDirectoryEntryMenuBottomSheetOpen = $state(false);
|
let isEntryMenuBottomSheetOpen = $state(false);
|
||||||
let isDirectoryEntryRenameModalOpen = $state(false);
|
let isEntryRenameModalOpen = $state(false);
|
||||||
let isDeleteDirectoryEntryModalOpen = $state(false);
|
let isEntryDeleteModalOpen = $state(false);
|
||||||
|
|
||||||
const createDirectory = async (name: string) => {
|
|
||||||
await requestDirectoryCreation(name, data.id, $masterKeyStore?.get(1)!);
|
|
||||||
isCreateDirectoryModalOpen = false;
|
|
||||||
info = getDirectoryInfo(data.id, $masterKeyStore?.get(1)?.key!); // TODO: FIXME
|
|
||||||
};
|
|
||||||
|
|
||||||
const uploadFile = () => {
|
const uploadFile = () => {
|
||||||
const files = fileInput?.files;
|
const files = fileInput?.files;
|
||||||
@@ -55,22 +49,19 @@
|
|||||||
for (const file of files) {
|
for (const file of files) {
|
||||||
requestFileUpload(file, data.id, $hmacSecretStore?.get(1)!, $masterKeyStore?.get(1)!, () => {
|
requestFileUpload(file, data.id, $hmacSecretStore?.get(1)!, $masterKeyStore?.get(1)!, () => {
|
||||||
return new Promise((resolve) => {
|
return new Promise((resolve) => {
|
||||||
resolveForDuplicateFileModal = resolve;
|
|
||||||
duplicatedFile = file;
|
duplicatedFile = file;
|
||||||
|
resolveForDuplicateFileModal = resolve;
|
||||||
isDuplicateFileModalOpen = true;
|
isDuplicateFileModalOpen = true;
|
||||||
});
|
});
|
||||||
})
|
})
|
||||||
.then((res) => {
|
.then((res) => {
|
||||||
if (!res) return;
|
if (!res) return;
|
||||||
|
|
||||||
// TODO: FIXME
|
// TODO: FIXME
|
||||||
info = getDirectoryInfo(data.id, $masterKeyStore?.get(1)?.key!);
|
info = getDirectoryInfo(data.id, $masterKeyStore?.get(1)?.key!);
|
||||||
window.alert(`'${file.name}' 파일이 업로드되었어요.`);
|
|
||||||
})
|
})
|
||||||
.catch((e: Error) => {
|
.catch((e: Error) => {
|
||||||
// TODO: FIXME
|
// TODO: FIXME
|
||||||
console.error(e);
|
console.error(e);
|
||||||
window.alert(`'${file.name}' 파일 업로드에 실패했어요.\n${e.message}`);
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -110,8 +101,8 @@
|
|||||||
info={$info}
|
info={$info}
|
||||||
onEntryClick={({ type, id }) => goto(`/${type}/${id}`)}
|
onEntryClick={({ type, id }) => goto(`/${type}/${id}`)}
|
||||||
onEntryMenuClick={(entry) => {
|
onEntryMenuClick={(entry) => {
|
||||||
selectedEntry = entry;
|
context.selectedEntry = entry;
|
||||||
isDirectoryEntryMenuBottomSheetOpen = true;
|
isEntryMenuBottomSheetOpen = true;
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
{/key}
|
{/key}
|
||||||
@@ -122,68 +113,71 @@
|
|||||||
<FloatingButton
|
<FloatingButton
|
||||||
icon={IconAdd}
|
icon={IconAdd}
|
||||||
onclick={() => {
|
onclick={() => {
|
||||||
isCreateBottomSheetOpen = true;
|
isEntryCreateBottomSheetOpen = true;
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<CreateBottomSheet
|
<EntryCreateBottomSheet
|
||||||
bind:isOpen={isCreateBottomSheetOpen}
|
bind:isOpen={isEntryCreateBottomSheetOpen}
|
||||||
onDirectoryCreateClick={() => {
|
onDirectoryCreateClick={() => {
|
||||||
isCreateBottomSheetOpen = false;
|
isEntryCreateBottomSheetOpen = false;
|
||||||
isCreateDirectoryModalOpen = true;
|
isDirectoryCreateModalOpen = true;
|
||||||
}}
|
}}
|
||||||
onFileUploadClick={() => {
|
onFileUploadClick={() => {
|
||||||
isCreateBottomSheetOpen = false;
|
isEntryCreateBottomSheetOpen = false;
|
||||||
fileInput?.click();
|
fileInput?.click();
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<CreateDirectoryModal bind:isOpen={isCreateDirectoryModalOpen} onCreateClick={createDirectory} />
|
<DirectoryCreateModal
|
||||||
<DuplicateFileModal
|
bind:isOpen={isDirectoryCreateModalOpen}
|
||||||
bind:isOpen={isDuplicateFileModalOpen}
|
onCreateClick={async (name) => {
|
||||||
file={duplicatedFile}
|
if (await requestDirectoryCreation(name, data.id, $masterKeyStore?.get(1)!)) {
|
||||||
onclose={() => {
|
|
||||||
resolveForDuplicateFileModal?.(false);
|
|
||||||
resolveForDuplicateFileModal = undefined;
|
|
||||||
duplicatedFile = undefined;
|
|
||||||
isDuplicateFileModalOpen = false;
|
|
||||||
}}
|
|
||||||
onDuplicateClick={() => {
|
|
||||||
resolveForDuplicateFileModal?.(true);
|
|
||||||
resolveForDuplicateFileModal = undefined;
|
|
||||||
duplicatedFile = undefined;
|
|
||||||
isDuplicateFileModalOpen = false;
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<DirectoryEntryMenuBottomSheet
|
|
||||||
bind:isOpen={isDirectoryEntryMenuBottomSheetOpen}
|
|
||||||
bind:selectedEntry
|
|
||||||
onRenameClick={() => {
|
|
||||||
isDirectoryEntryMenuBottomSheetOpen = false;
|
|
||||||
isDirectoryEntryRenameModalOpen = true;
|
|
||||||
}}
|
|
||||||
onDeleteClick={() => {
|
|
||||||
isDirectoryEntryMenuBottomSheetOpen = false;
|
|
||||||
isDeleteDirectoryEntryModalOpen = true;
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<RenameModal
|
|
||||||
bind:isOpen={isDirectoryEntryRenameModalOpen}
|
|
||||||
onbeforeclose={() => (selectedEntry = undefined)}
|
|
||||||
originalName={selectedEntry?.name}
|
|
||||||
onRenameClick={async (newName: string) => {
|
|
||||||
if (await requestDirectoryEntryRename(selectedEntry!, newName)) {
|
|
||||||
info = getDirectoryInfo(data.id, $masterKeyStore?.get(1)?.key!); // TODO: FIXME
|
info = getDirectoryInfo(data.id, $masterKeyStore?.get(1)?.key!); // TODO: FIXME
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<DeleteDirectoryEntryModal
|
<DuplicateFileModal
|
||||||
bind:isOpen={isDeleteDirectoryEntryModalOpen}
|
bind:isOpen={isDuplicateFileModalOpen}
|
||||||
bind:selectedEntry
|
file={duplicatedFile}
|
||||||
onDeleteClick={async () => {
|
onbeforeclose={() => {
|
||||||
await requestDirectoryEntryDeletion(selectedEntry!);
|
resolveForDuplicateFileModal?.(false);
|
||||||
info = getDirectoryInfo(data.id, $masterKeyStore?.get(1)?.key!); // TODO: FIXME
|
isDuplicateFileModalOpen = false;
|
||||||
return true;
|
}}
|
||||||
|
onUploadClick={() => {
|
||||||
|
resolveForDuplicateFileModal?.(true);
|
||||||
|
isDuplicateFileModalOpen = false;
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<EntryMenuBottomSheet
|
||||||
|
bind:isOpen={isEntryMenuBottomSheetOpen}
|
||||||
|
onRenameClick={() => {
|
||||||
|
isEntryMenuBottomSheetOpen = false;
|
||||||
|
isEntryRenameModalOpen = true;
|
||||||
|
}}
|
||||||
|
onDeleteClick={() => {
|
||||||
|
isEntryMenuBottomSheetOpen = false;
|
||||||
|
isEntryDeleteModalOpen = true;
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<EntryRenameModal
|
||||||
|
bind:isOpen={isEntryRenameModalOpen}
|
||||||
|
onRenameClick={async (newName: string) => {
|
||||||
|
if (await requestEntryRename(context.selectedEntry!, newName)) {
|
||||||
|
info = getDirectoryInfo(data.id, $masterKeyStore?.get(1)?.key!); // TODO: FIXME
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<EntryDeleteModal
|
||||||
|
bind:isOpen={isEntryDeleteModalOpen}
|
||||||
|
onDeleteClick={async () => {
|
||||||
|
if (await requestEntryDeletion(context.selectedEntry!)) {
|
||||||
|
info = getDirectoryInfo(data.id, $masterKeyStore?.get(1)?.key!); // TODO: FIXME
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -1,28 +0,0 @@
|
|||||||
<script lang="ts">
|
|
||||||
import { Button, Modal, TextInput } from "$lib/components/atoms";
|
|
||||||
|
|
||||||
interface Props {
|
|
||||||
onCreateClick: (name: string) => void;
|
|
||||||
isOpen: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
let { onCreateClick, isOpen = $bindable() }: Props = $props();
|
|
||||||
|
|
||||||
let name = $state("");
|
|
||||||
|
|
||||||
const closeModal = () => {
|
|
||||||
name = "";
|
|
||||||
isOpen = false;
|
|
||||||
};
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<Modal bind:isOpen onclose={closeModal}>
|
|
||||||
<p class="text-xl font-bold">새 폴더</p>
|
|
||||||
<div class="mt-2 flex w-full">
|
|
||||||
<TextInput bind:value={name} placeholder="폴더 이름" />
|
|
||||||
</div>
|
|
||||||
<div class="mt-7 flex gap-x-2">
|
|
||||||
<Button color="gray" onclick={closeModal} class="flex-1">닫기</Button>
|
|
||||||
<Button onclick={() => onCreateClick(name)} class="flex-1">만들기</Button>
|
|
||||||
</div>
|
|
||||||
</Modal>
|
|
||||||
@@ -1,55 +0,0 @@
|
|||||||
<script lang="ts">
|
|
||||||
import { Button, Modal } from "$lib/components/atoms";
|
|
||||||
import type { SelectedDirectoryEntry } from "./service";
|
|
||||||
|
|
||||||
interface Props {
|
|
||||||
onDeleteClick: () => Promise<boolean>;
|
|
||||||
isOpen: boolean;
|
|
||||||
selectedEntry: SelectedDirectoryEntry | undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
let { onDeleteClick, isOpen = $bindable(), selectedEntry = $bindable() }: Props = $props();
|
|
||||||
|
|
||||||
const closeModal = () => {
|
|
||||||
isOpen = false;
|
|
||||||
selectedEntry = undefined;
|
|
||||||
};
|
|
||||||
|
|
||||||
const deleteEntry = async () => {
|
|
||||||
// TODO: Validation
|
|
||||||
|
|
||||||
if (await onDeleteClick()) {
|
|
||||||
closeModal();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<Modal bind:isOpen onclose={closeModal}>
|
|
||||||
{#if selectedEntry}
|
|
||||||
{@const { type, name } = selectedEntry}
|
|
||||||
{@const nameShort = name.length > 20 ? `${name.slice(0, 20)}...` : name}
|
|
||||||
<div class="space-y-4">
|
|
||||||
<div class="space-y-2 break-keep">
|
|
||||||
<p class="text-xl font-bold">
|
|
||||||
{#if type === "directory"}
|
|
||||||
'{nameShort}' 폴더를 삭제할까요?
|
|
||||||
{:else}
|
|
||||||
'{nameShort}' 파일을 삭제할까요?
|
|
||||||
{/if}
|
|
||||||
</p>
|
|
||||||
<p>
|
|
||||||
{#if type === "directory"}
|
|
||||||
삭제한 폴더는 복구할 수 없어요. <br />
|
|
||||||
폴더 안의 모든 파일과 폴더도 함께 삭제돼요.
|
|
||||||
{:else}
|
|
||||||
삭제한 파일은 복구할 수 없어요.
|
|
||||||
{/if}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<div class="flex gap-x-2">
|
|
||||||
<Button color="gray" onclick={closeModal} class="flex-1">아니요</Button>
|
|
||||||
<Button onclick={deleteEntry} class="flex-1">삭제할게요</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{/if}
|
|
||||||
</Modal>
|
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { TextInputModal } from "$lib/components/organisms";
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
isOpen: boolean;
|
||||||
|
onCreateClick: (name: string) => Promise<boolean>;
|
||||||
|
}
|
||||||
|
|
||||||
|
let { isOpen = $bindable(), onCreateClick }: Props = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<TextInputModal
|
||||||
|
bind:isOpen
|
||||||
|
title="새 폴더"
|
||||||
|
placeholder="폴더 이름"
|
||||||
|
submitText="만들기"
|
||||||
|
onSubmitClick={onCreateClick}
|
||||||
|
/>
|
||||||
@@ -17,12 +17,12 @@
|
|||||||
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";
|
||||||
import type { SelectedDirectoryEntry } from "../service";
|
import type { SelectedEntry } from "../service.svelte";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
info: DirectoryInfo;
|
info: DirectoryInfo;
|
||||||
onEntryClick: (entry: SelectedDirectoryEntry) => void;
|
onEntryClick: (entry: SelectedEntry) => void;
|
||||||
onEntryMenuClick: (entry: SelectedDirectoryEntry) => void;
|
onEntryMenuClick: (entry: SelectedEntry) => void;
|
||||||
sortBy?: SortBy;
|
sortBy?: SortBy;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -2,15 +2,15 @@
|
|||||||
import type { Writable } from "svelte/store";
|
import type { Writable } from "svelte/store";
|
||||||
import type { FileInfo } from "$lib/modules/filesystem";
|
import type { FileInfo } from "$lib/modules/filesystem";
|
||||||
import { formatDateTime } from "$lib/modules/util";
|
import { formatDateTime } from "$lib/modules/util";
|
||||||
import type { SelectedDirectoryEntry } from "../service";
|
import type { SelectedEntry } from "../service.svelte";
|
||||||
|
|
||||||
import IconDraft from "~icons/material-symbols/draft";
|
import IconDraft from "~icons/material-symbols/draft";
|
||||||
import IconMoreVert from "~icons/material-symbols/more-vert";
|
import IconMoreVert from "~icons/material-symbols/more-vert";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
info: Writable<FileInfo | null>;
|
info: Writable<FileInfo | null>;
|
||||||
onclick: (selectedEntry: SelectedDirectoryEntry) => void;
|
onclick: (selectedEntry: SelectedEntry) => void;
|
||||||
onOpenMenuClick: (selectedEntry: SelectedDirectoryEntry) => void;
|
onOpenMenuClick: (selectedEntry: SelectedEntry) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
let { info, onclick, onOpenMenuClick }: Props = $props();
|
let { info, onclick, onOpenMenuClick }: Props = $props();
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import type { Writable } from "svelte/store";
|
import type { Writable } from "svelte/store";
|
||||||
import type { DirectoryInfo } from "$lib/modules/filesystem";
|
import type { DirectoryInfo } from "$lib/modules/filesystem";
|
||||||
import type { SelectedDirectoryEntry } from "../service";
|
import type { SelectedEntry } from "../service.svelte";
|
||||||
|
|
||||||
import IconFolder from "~icons/material-symbols/folder";
|
import IconFolder from "~icons/material-symbols/folder";
|
||||||
import IconMoreVert from "~icons/material-symbols/more-vert";
|
import IconMoreVert from "~icons/material-symbols/more-vert";
|
||||||
@@ -10,8 +10,8 @@
|
|||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
info: Writable<DirectoryInfo | null>;
|
info: Writable<DirectoryInfo | null>;
|
||||||
onclick: (selectedEntry: SelectedDirectoryEntry) => void;
|
onclick: (selectedEntry: SelectedEntry) => void;
|
||||||
onOpenMenuClick: (selectedEntry: SelectedDirectoryEntry) => void;
|
onOpenMenuClick: (selectedEntry: SelectedEntry) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
let { info, onclick, onOpenMenuClick }: Props = $props();
|
let { info, onclick, onOpenMenuClick }: Props = $props();
|
||||||
|
|||||||
@@ -1,62 +0,0 @@
|
|||||||
<script lang="ts">
|
|
||||||
import { BottomSheet } from "$lib/components";
|
|
||||||
import { EntryButton } from "$lib/components/atoms";
|
|
||||||
import type { SelectedDirectoryEntry } from "./service";
|
|
||||||
|
|
||||||
import IconFolder from "~icons/material-symbols/folder";
|
|
||||||
import IconDraft from "~icons/material-symbols/draft";
|
|
||||||
import IconEdit from "~icons/material-symbols/edit";
|
|
||||||
import IconDelete from "~icons/material-symbols/delete";
|
|
||||||
|
|
||||||
interface Props {
|
|
||||||
onRenameClick: () => void;
|
|
||||||
onDeleteClick: () => void;
|
|
||||||
isOpen: boolean;
|
|
||||||
selectedEntry: SelectedDirectoryEntry | undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
let {
|
|
||||||
onRenameClick,
|
|
||||||
onDeleteClick,
|
|
||||||
isOpen = $bindable(),
|
|
||||||
selectedEntry = $bindable(),
|
|
||||||
}: Props = $props();
|
|
||||||
|
|
||||||
const closeBottomSheet = () => {
|
|
||||||
isOpen = false;
|
|
||||||
selectedEntry = undefined;
|
|
||||||
};
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<BottomSheet bind:isOpen onclose={closeBottomSheet}>
|
|
||||||
<div class="w-full py-4">
|
|
||||||
{#if selectedEntry}
|
|
||||||
{@const { type, name } = selectedEntry}
|
|
||||||
<div class="flex h-12 items-center gap-x-4 p-2">
|
|
||||||
<div class="flex-shrink-0 text-lg">
|
|
||||||
{#if type === "directory"}
|
|
||||||
<IconFolder />
|
|
||||||
{:else}
|
|
||||||
<IconDraft class="text-blue-400" />
|
|
||||||
{/if}
|
|
||||||
</div>
|
|
||||||
<p title={name} class="flex-grow truncate font-semibold">
|
|
||||||
{name}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<div class="my-2 h-px w-full bg-gray-200"></div>
|
|
||||||
{/if}
|
|
||||||
<EntryButton onclick={onRenameClick} class="w-full">
|
|
||||||
<div class="flex h-8 items-center gap-x-4">
|
|
||||||
<IconEdit class="text-lg" />
|
|
||||||
<p class="font-medium">이름 바꾸기</p>
|
|
||||||
</div>
|
|
||||||
</EntryButton>
|
|
||||||
<EntryButton onclick={onDeleteClick} class="w-full">
|
|
||||||
<div class="flex h-8 items-center gap-x-4 text-red-500">
|
|
||||||
<IconDelete class="text-lg" />
|
|
||||||
<p class="font-medium">삭제하기</p>
|
|
||||||
</div>
|
|
||||||
</EntryButton>
|
|
||||||
</div>
|
|
||||||
</BottomSheet>
|
|
||||||
@@ -1,29 +1,27 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { Button, Modal } from "$lib/components/atoms";
|
import { ActionModal } from "$lib/components/molecules";
|
||||||
|
import { truncateString } from "$lib/modules/util";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
file: File | undefined;
|
file: File | undefined;
|
||||||
onclose: () => void;
|
|
||||||
onDuplicateClick: () => void;
|
|
||||||
isOpen: boolean;
|
isOpen: boolean;
|
||||||
|
onbeforeclose: () => void;
|
||||||
|
onUploadClick: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
let { file, onclose, onDuplicateClick, isOpen = $bindable() }: Props = $props();
|
let { file, isOpen = $bindable(), onbeforeclose, onUploadClick }: Props = $props();
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<Modal bind:isOpen {onclose}>
|
{#if file}
|
||||||
{#if file}
|
{@const { name } = file}
|
||||||
{@const { name } = file}
|
<ActionModal
|
||||||
{@const nameShort = name.length > 20 ? `${name.slice(0, 20)}...` : name}
|
bind:isOpen
|
||||||
<div class="space-y-4">
|
{onbeforeclose}
|
||||||
<div class="space-y-2 break-keep">
|
title="'{truncateString(name)}' 파일이 있어요."
|
||||||
<p class="text-xl font-bold">'{nameShort}' 파일이 있어요.</p>
|
cancelText="아니요"
|
||||||
<p>예전에 이미 업로드된 파일이에요. 그래도 업로드할까요?</p>
|
confirmText="업로드할게요"
|
||||||
</div>
|
onConfirmClick={onUploadClick}
|
||||||
<div class="flex gap-x-2">
|
>
|
||||||
<Button color="gray" onclick={onclose} class="flex-1">아니요</Button>
|
<p>예전에 이미 업로드된 파일이에요. 그래도 업로드할까요?</p>
|
||||||
<Button onclick={onDuplicateClick} class="flex-1">업로드할게요</Button>
|
</ActionModal>
|
||||||
</div>
|
{/if}
|
||||||
</div>
|
|
||||||
{/if}
|
|
||||||
</Modal>
|
|
||||||
|
|||||||
@@ -6,12 +6,12 @@
|
|||||||
import IconUploadFile from "~icons/material-symbols/upload-file";
|
import IconUploadFile from "~icons/material-symbols/upload-file";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
|
isOpen: boolean;
|
||||||
onDirectoryCreateClick: () => void;
|
onDirectoryCreateClick: () => void;
|
||||||
onFileUploadClick: () => void;
|
onFileUploadClick: () => void;
|
||||||
isOpen: boolean;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
let { onDirectoryCreateClick, onFileUploadClick, isOpen = $bindable() }: Props = $props();
|
let { isOpen = $bindable(), onDirectoryCreateClick, onFileUploadClick }: Props = $props();
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<BottomSheet bind:isOpen>
|
<BottomSheet bind:isOpen>
|
||||||
33
src/routes/(main)/directory/[[id]]/EntryDeleteModal.svelte
Normal file
33
src/routes/(main)/directory/[[id]]/EntryDeleteModal.svelte
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { ActionModal } from "$lib/components/molecules";
|
||||||
|
import { truncateString } from "$lib/modules/util";
|
||||||
|
import { useContext } from "./service.svelte";
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
isOpen: boolean;
|
||||||
|
onDeleteClick: () => Promise<boolean>;
|
||||||
|
}
|
||||||
|
|
||||||
|
let { isOpen = $bindable(), onDeleteClick }: Props = $props();
|
||||||
|
let context = useContext();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
{#if context.selectedEntry}
|
||||||
|
{@const { name, type } = context.selectedEntry}
|
||||||
|
<ActionModal
|
||||||
|
bind:isOpen
|
||||||
|
title="'{truncateString(name)}' {type === 'directory' ? '폴더를' : '파일을'} 삭제할까요?"
|
||||||
|
cancelText="아니요"
|
||||||
|
confirmText="삭제할게요"
|
||||||
|
onConfirmClick={onDeleteClick}
|
||||||
|
>
|
||||||
|
<p>
|
||||||
|
{#if type === "directory"}
|
||||||
|
삭제한 폴더는 복구할 수 없어요. <br />
|
||||||
|
폴더 안의 모든 파일과 폴더도 함께 삭제돼요.
|
||||||
|
{:else}
|
||||||
|
삭제한 파일은 복구할 수 없어요.
|
||||||
|
{/if}
|
||||||
|
</p>
|
||||||
|
</ActionModal>
|
||||||
|
{/if}
|
||||||
@@ -0,0 +1,52 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { BottomSheet } from "$lib/components";
|
||||||
|
import { EntryButton } from "$lib/components/atoms";
|
||||||
|
import { useContext } from "./service.svelte";
|
||||||
|
|
||||||
|
import IconFolder from "~icons/material-symbols/folder";
|
||||||
|
import IconDraft from "~icons/material-symbols/draft";
|
||||||
|
import IconEdit from "~icons/material-symbols/edit";
|
||||||
|
import IconDelete from "~icons/material-symbols/delete";
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
isOpen: boolean;
|
||||||
|
onDeleteClick: () => void;
|
||||||
|
onRenameClick: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
let { isOpen = $bindable(), onDeleteClick, onRenameClick }: Props = $props();
|
||||||
|
let context = useContext();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
{#if context.selectedEntry}
|
||||||
|
{@const { name, type } = context.selectedEntry}
|
||||||
|
<BottomSheet bind:isOpen>
|
||||||
|
<div class="w-full py-4">
|
||||||
|
<div class="flex h-12 items-center gap-x-4 p-2">
|
||||||
|
<div class="flex-shrink-0 text-lg">
|
||||||
|
{#if type === "directory"}
|
||||||
|
<IconFolder />
|
||||||
|
{:else}
|
||||||
|
<IconDraft class="text-blue-400" />
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
<p title={name} class="flex-grow truncate font-semibold">
|
||||||
|
{name}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div class="my-2 h-px w-full bg-gray-200"></div>
|
||||||
|
<EntryButton onclick={onRenameClick} class="w-full">
|
||||||
|
<div class="flex h-8 items-center gap-x-4">
|
||||||
|
<IconEdit class="text-lg" />
|
||||||
|
<p class="font-medium">이름 바꾸기</p>
|
||||||
|
</div>
|
||||||
|
</EntryButton>
|
||||||
|
<EntryButton onclick={onDeleteClick} class="w-full">
|
||||||
|
<div class="flex h-8 items-center gap-x-4 text-red-500">
|
||||||
|
<IconDelete class="text-lg" />
|
||||||
|
<p class="font-medium">삭제하기</p>
|
||||||
|
</div>
|
||||||
|
</EntryButton>
|
||||||
|
</div>
|
||||||
|
</BottomSheet>
|
||||||
|
{/if}
|
||||||
17
src/routes/(main)/directory/[[id]]/EntryRenameModal.svelte
Normal file
17
src/routes/(main)/directory/[[id]]/EntryRenameModal.svelte
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { RenameModal } from "$lib/components/organisms";
|
||||||
|
import { useContext } from "./service.svelte";
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
isOpen: boolean;
|
||||||
|
onRenameClick: (newName: string) => Promise<boolean>;
|
||||||
|
}
|
||||||
|
|
||||||
|
let { isOpen = $bindable(), onRenameClick }: Props = $props();
|
||||||
|
let context = useContext();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
{#if context.selectedEntry}
|
||||||
|
{@const { name } = context.selectedEntry}
|
||||||
|
<RenameModal bind:isOpen originalName={name} {onRenameClick} />
|
||||||
|
{/if}
|
||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { getContext, setContext } from "svelte";
|
||||||
import { callGetApi, callPostApi } from "$lib/hooks";
|
import { callGetApi, callPostApi } from "$lib/hooks";
|
||||||
import { storeHmacSecrets } from "$lib/indexedDB";
|
import { storeHmacSecrets } from "$lib/indexedDB";
|
||||||
import { generateDataKey, wrapDataKey, unwrapHmacSecret, encryptString } from "$lib/modules/crypto";
|
import { generateDataKey, wrapDataKey, unwrapHmacSecret, encryptString } from "$lib/modules/crypto";
|
||||||
@@ -11,7 +12,7 @@ import type {
|
|||||||
} from "$lib/server/schemas";
|
} from "$lib/server/schemas";
|
||||||
import { hmacSecretStore, type MasterKey, type HmacSecret } from "$lib/stores";
|
import { hmacSecretStore, type MasterKey, type HmacSecret } from "$lib/stores";
|
||||||
|
|
||||||
export interface SelectedDirectoryEntry {
|
export interface SelectedEntry {
|
||||||
type: "directory" | "file";
|
type: "directory" | "file";
|
||||||
id: number;
|
id: number;
|
||||||
dataKey: CryptoKey;
|
dataKey: CryptoKey;
|
||||||
@@ -19,6 +20,17 @@ export interface SelectedDirectoryEntry {
|
|||||||
name: string;
|
name: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const createContext = () => {
|
||||||
|
const context = $state({
|
||||||
|
selectedEntry: undefined as SelectedEntry | undefined,
|
||||||
|
});
|
||||||
|
return setContext("context", context);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useContext = () => {
|
||||||
|
return getContext<ReturnType<typeof createContext>>("context");
|
||||||
|
};
|
||||||
|
|
||||||
export const requestHmacSecretDownload = async (masterKey: CryptoKey) => {
|
export const requestHmacSecretDownload = async (masterKey: CryptoKey) => {
|
||||||
// TODO: MEK rotation
|
// TODO: MEK rotation
|
||||||
|
|
||||||
@@ -46,7 +58,8 @@ export const requestDirectoryCreation = async (
|
|||||||
) => {
|
) => {
|
||||||
const { dataKey, dataKeyVersion } = await generateDataKey();
|
const { dataKey, dataKeyVersion } = await generateDataKey();
|
||||||
const nameEncrypted = await encryptString(name, dataKey);
|
const nameEncrypted = await encryptString(name, dataKey);
|
||||||
await callPostApi<DirectoryCreateRequest>("/api/directory/create", {
|
|
||||||
|
const res = await callPostApi<DirectoryCreateRequest>("/api/directory/create", {
|
||||||
parent: parentId,
|
parent: parentId,
|
||||||
mekVersion: masterKey.version,
|
mekVersion: masterKey.version,
|
||||||
dek: await wrapDataKey(dataKey, masterKey.key),
|
dek: await wrapDataKey(dataKey, masterKey.key),
|
||||||
@@ -54,6 +67,7 @@ export const requestDirectoryCreation = async (
|
|||||||
name: nameEncrypted.ciphertext,
|
name: nameEncrypted.ciphertext,
|
||||||
nameIv: nameEncrypted.iv,
|
nameIv: nameEncrypted.iv,
|
||||||
});
|
});
|
||||||
|
return res.ok;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const requestFileUpload = async (
|
export const requestFileUpload = async (
|
||||||
@@ -66,10 +80,7 @@ export const requestFileUpload = async (
|
|||||||
return await uploadFile(file, parentId, hmacSecret, masterKey, onDuplicate);
|
return await uploadFile(file, parentId, hmacSecret, masterKey, onDuplicate);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const requestDirectoryEntryRename = async (
|
export const requestEntryRename = async (entry: SelectedEntry, newName: string) => {
|
||||||
entry: SelectedDirectoryEntry,
|
|
||||||
newName: string,
|
|
||||||
) => {
|
|
||||||
const newNameEncrypted = await encryptString(newName, entry.dataKey);
|
const newNameEncrypted = await encryptString(newName, entry.dataKey);
|
||||||
|
|
||||||
let res;
|
let res;
|
||||||
@@ -89,7 +100,7 @@ export const requestDirectoryEntryRename = async (
|
|||||||
return res.ok;
|
return res.ok;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const requestDirectoryEntryDeletion = async (entry: SelectedDirectoryEntry) => {
|
export const requestEntryDeletion = async (entry: SelectedEntry) => {
|
||||||
const res = await callPostApi(`/api/${entry.type}/${entry.id}/delete`);
|
const res = await callPostApi(`/api/${entry.type}/${entry.id}/delete`);
|
||||||
if (!res.ok) return false;
|
if (!res.ok) return false;
|
||||||
|
|
||||||
Reference in New Issue
Block a user