Merge pull request #13 from kmc7468/dev

v0.5.1
This commit is contained in:
static
2025-07-12 19:56:12 +09:00
committed by GitHub
7 changed files with 121 additions and 38 deletions

View File

@@ -1,7 +1,7 @@
{ {
"name": "arkvault", "name": "arkvault",
"private": true, "private": true,
"version": "0.5.0", "version": "0.5.1",
"type": "module", "type": "module",
"scripts": { "scripts": {
"dev": "vite dev", "dev": "vite dev",

View File

@@ -25,6 +25,7 @@ interface CategoryInfo {
parentId: CategoryId; parentId: CategoryId;
name: string; name: string;
files: { id: number; isRecursive: boolean }[]; files: { id: number; isRecursive: boolean }[];
isFileRecursive: boolean;
} }
const filesystem = new Dexie("filesystem") as Dexie & { const filesystem = new Dexie("filesystem") as Dexie & {
@@ -33,11 +34,21 @@ const filesystem = new Dexie("filesystem") as Dexie & {
category: EntityTable<CategoryInfo, "id">; category: EntityTable<CategoryInfo, "id">;
}; };
filesystem.version(2).stores({ filesystem
directory: "id, parentId", .version(3)
file: "id, parentId", .stores({
category: "id, parentId", directory: "id, parentId",
}); file: "id, parentId",
category: "id, parentId",
})
.upgrade(async (trx) => {
await trx
.table("category")
.toCollection()
.modify((category) => {
category.isFileRecursive = false;
});
});
export const getDirectoryInfos = async (parentId: DirectoryId) => { export const getDirectoryInfos = async (parentId: DirectoryId) => {
return await filesystem.directory.where({ parentId }).toArray(); return await filesystem.directory.where({ parentId }).toArray();
@@ -87,6 +98,10 @@ export const storeCategoryInfo = async (categoryInfo: CategoryInfo) => {
await filesystem.category.put(categoryInfo); await filesystem.category.put(categoryInfo);
}; };
export const updateCategoryInfo = async (id: number, changes: { isFileRecursive?: boolean }) => {
await filesystem.category.update(id, changes);
};
export const deleteCategoryInfo = async (id: number) => { export const deleteCategoryInfo = async (id: number) => {
await filesystem.category.delete(id); await filesystem.category.delete(id);
}; };

View File

@@ -12,6 +12,7 @@ import {
getCategoryInfos as getCategoryInfosFromIndexedDB, getCategoryInfos as getCategoryInfosFromIndexedDB,
getCategoryInfo as getCategoryInfoFromIndexedDB, getCategoryInfo as getCategoryInfoFromIndexedDB,
storeCategoryInfo, storeCategoryInfo,
updateCategoryInfo as updateCategoryInfoInIndexedDB,
deleteCategoryInfo, deleteCategoryInfo,
type DirectoryId, type DirectoryId,
type CategoryId, type CategoryId,
@@ -62,6 +63,7 @@ export type CategoryInfo =
name?: undefined; name?: undefined;
subCategoryIds: number[]; subCategoryIds: number[];
files?: undefined; files?: undefined;
isFileRecursive?: undefined;
} }
| { | {
id: number; id: number;
@@ -70,6 +72,7 @@ export type CategoryInfo =
name: string; name: string;
subCategoryIds: number[]; subCategoryIds: number[];
files: { id: number; isRecursive: boolean }[]; files: { id: number; isRecursive: boolean }[];
isFileRecursive: boolean;
}; };
const directoryInfoStore = new Map<DirectoryId, Writable<DirectoryInfo | null>>(); const directoryInfoStore = new Map<DirectoryId, Writable<DirectoryInfo | null>>();
@@ -255,7 +258,13 @@ const fetchCategoryInfoFromIndexedDB = async (
info.set({ id, subCategoryIds }); info.set({ id, subCategoryIds });
} else { } else {
if (!category) return; if (!category) return;
info.set({ id, name: category.name, subCategoryIds, files: category.files }); info.set({
id,
name: category.name,
subCategoryIds,
files: category.files,
isFileRecursive: category.isFileRecursive,
});
} }
}; };
@@ -288,20 +297,28 @@ const fetchCategoryInfoFromServer = async (
const { files }: CategoryFileListResponse = await res.json(); const { files }: CategoryFileListResponse = await res.json();
const filesMapped = files.map(({ file, isRecursive }) => ({ id: file, isRecursive })); const filesMapped = files.map(({ file, isRecursive }) => ({ id: file, isRecursive }));
let isFileRecursive: boolean | undefined = undefined;
info.set({ info.update((value) => {
id, const newValue = {
dataKey, isFileRecursive: false,
dataKeyVersion: new Date(metadata!.dekVersion), ...value,
name, id,
subCategoryIds: subCategories, dataKey,
files: filesMapped, dataKeyVersion: new Date(metadata!.dekVersion),
name,
subCategoryIds: subCategories,
files: filesMapped,
};
isFileRecursive = newValue.isFileRecursive;
return newValue;
}); });
await storeCategoryInfo({ await storeCategoryInfo({
id, id,
parentId: metadata!.parent, parentId: metadata!.parent,
name, name,
files: filesMapped, files: filesMapped,
isFileRecursive: isFileRecursive!,
}); });
} }
}; };
@@ -327,3 +344,17 @@ export const getCategoryInfo = (categoryId: CategoryId, masterKey: CryptoKey) =>
fetchCategoryInfo(categoryId, info, masterKey); // Intended fetchCategoryInfo(categoryId, info, masterKey); // Intended
return info; return info;
}; };
export const updateCategoryInfo = async (
categoryId: number,
changes: { isFileRecursive?: boolean },
) => {
await updateCategoryInfoInIndexedDB(categoryId, changes);
categoryInfoStore.get(categoryId)?.update((value) => {
if (!value) return value;
if (changes.isFileRecursive !== undefined) {
value.isFileRecursive = changes.isFileRecursive;
}
return value;
});
};

View File

@@ -32,7 +32,7 @@ const capture = (
drawer(context, scaledWidth, scaledHeight); drawer(context, scaledWidth, scaledHeight);
canvas.toBlob((blob) => { canvas.toBlob((blob) => {
if (blob) { if (blob && blob.type === "image/webp") {
resolve(blob); resolve(blob);
} else { } else {
reject(new Error("Failed to generate thumbnail")); reject(new Error("Failed to generate thumbnail"));
@@ -83,18 +83,26 @@ const generateVideoThumbnail = (videoUrl: string, time = 0) => {
export const generateThumbnail = async (fileBuffer: ArrayBuffer, fileType: string) => { export const generateThumbnail = async (fileBuffer: ArrayBuffer, fileType: string) => {
let url; let url;
try { try {
if (fileType === "image/heic") { if (fileType.startsWith("image/")) {
const { default: heic2any } = await import("heic2any"); const fileBlob = new Blob([fileBuffer], { type: fileType });
url = URL.createObjectURL( url = URL.createObjectURL(fileBlob);
(await heic2any({
blob: new Blob([fileBuffer], { type: fileType }), try {
toType: "image/png", return await generateImageThumbnail(url);
})) as Blob, } catch {
); URL.revokeObjectURL(url);
return await generateImageThumbnail(url); url = undefined;
} else if (fileType.startsWith("image/")) {
url = URL.createObjectURL(new Blob([fileBuffer], { type: fileType })); if (fileType === "image/heic") {
return await generateImageThumbnail(url); const { default: heic2any } = await import("heic2any");
url = URL.createObjectURL(
(await heic2any({ blob: fileBlob, toType: "image/png" })) as Blob,
);
return await generateImageThumbnail(url);
} else {
return null;
}
}
} else if (fileType.startsWith("video/")) { } else if (fileType.startsWith("video/")) {
url = URL.createObjectURL(new Blob([fileBuffer], { type: fileType })); url = URL.createObjectURL(new Blob([fileBuffer], { type: fileType }));
return await generateVideoThumbnail(url); return await generateVideoThumbnail(url);

View File

@@ -6,8 +6,14 @@
let oldPassword = $state(""); let oldPassword = $state("");
let newPassword = $state(""); let newPassword = $state("");
let confirmPassword = $state("");
const changePassword = async () => { const changePassword = async () => {
if (newPassword !== confirmPassword) {
// TODO: Alert
return;
}
if (await requestPasswordChange(oldPassword, newPassword)) { if (await requestPasswordChange(oldPassword, newPassword)) {
await goto("/menu"); await goto("/menu");
} }
@@ -30,6 +36,7 @@
<TextInput bind:value={oldPassword} placeholder="기존 비밀번호" type="password" /> <TextInput bind:value={oldPassword} placeholder="기존 비밀번호" type="password" />
<TextInput bind:value={newPassword} placeholder="새 비밀번호" type="password" /> <TextInput bind:value={newPassword} placeholder="새 비밀번호" type="password" />
<TextInput bind:value={confirmPassword} placeholder="새 비밀번호 확인" type="password" />
</TitledDiv> </TitledDiv>
<BottomDiv> <BottomDiv>
<Button onclick={changePassword} class="w-full">비밀번호 바꾸기</Button> <Button onclick={changePassword} class="w-full">비밀번호 바꾸기</Button>

View File

@@ -43,22 +43,31 @@
let isDownloadRequested = $state(false); let isDownloadRequested = $state(false);
let viewerType: "image" | "video" | undefined = $state(); let viewerType: "image" | "video" | undefined = $state();
let fileBlobUrl: string | undefined = $state(); let fileBlobUrl: string | undefined = $state();
let heicBlob: Blob | undefined = $state();
let videoElement: HTMLVideoElement | undefined = $state(); let videoElement: HTMLVideoElement | undefined = $state();
const updateViewer = async (buffer: ArrayBuffer, contentType: string) => { const updateViewer = async (buffer: ArrayBuffer, contentType: string) => {
const fileBlob = new Blob([buffer], { type: contentType }); const fileBlob = new Blob([buffer], { type: contentType });
if (contentType === "image/heic") { if (viewerType) {
const { default: heic2any } = await import("heic2any");
fileBlobUrl = URL.createObjectURL(
(await heic2any({ blob: fileBlob, toType: "image/jpeg" })) as Blob,
);
} else if (viewerType) {
fileBlobUrl = URL.createObjectURL(fileBlob); fileBlobUrl = URL.createObjectURL(fileBlob);
heicBlob = contentType === "image/heic" ? fileBlob : undefined;
} }
return fileBlob; return fileBlob;
}; };
const convertHeicToJpeg = async () => {
if (!heicBlob) return;
URL.revokeObjectURL(fileBlobUrl!);
fileBlobUrl = undefined;
const { default: heic2any } = await import("heic2any");
fileBlobUrl = URL.createObjectURL(
(await heic2any({ blob: heicBlob, toType: "image/jpeg" })) as Blob,
);
heicBlob = undefined;
};
const updateThumbnail = async (dataKey: CryptoKey, dataKeyVersion: Date) => { const updateThumbnail = async (dataKey: CryptoKey, dataKeyVersion: Date) => {
const thumbnail = await captureVideoThumbnail(videoElement!); const thumbnail = await captureVideoThumbnail(videoElement!);
await requestThumbnailUpload(data.id, thumbnail, dataKey, dataKeyVersion); await requestThumbnailUpload(data.id, thumbnail, dataKey, dataKeyVersion);
@@ -136,7 +145,7 @@
{#if viewerType === "image"} {#if viewerType === "image"}
{#if fileBlobUrl} {#if fileBlobUrl}
<img src={fileBlobUrl} alt={$info.name} /> <img src={fileBlobUrl} alt={$info.name} onerror={convertHeicToJpeg} />
{:else} {:else}
{@render viewerLoading("이미지를 불러오고 있어요.")} {@render viewerLoading("이미지를 불러오고 있어요.")}
{/if} {/if}

View File

@@ -3,7 +3,7 @@
import { goto } from "$app/navigation"; import { goto } from "$app/navigation";
import { TopBar } from "$lib/components/molecules"; import { TopBar } from "$lib/components/molecules";
import { Category, CategoryCreateModal } from "$lib/components/organisms"; import { Category, CategoryCreateModal } from "$lib/components/organisms";
import { getCategoryInfo, type CategoryInfo } from "$lib/modules/filesystem"; import { getCategoryInfo, updateCategoryInfo, type CategoryInfo } from "$lib/modules/filesystem";
import { masterKeyStore } from "$lib/stores"; import { masterKeyStore } from "$lib/stores";
import CategoryDeleteModal from "./CategoryDeleteModal.svelte"; import CategoryDeleteModal from "./CategoryDeleteModal.svelte";
import CategoryMenuBottomSheet from "./CategoryMenuBottomSheet.svelte"; import CategoryMenuBottomSheet from "./CategoryMenuBottomSheet.svelte";
@@ -21,7 +21,7 @@
let info: Writable<CategoryInfo | null> | undefined = $state(); let info: Writable<CategoryInfo | null> | undefined = $state();
let isFileRecursive = $state(false); let isFileRecursive: boolean | undefined = $state();
let isCategoryCreateModalOpen = $state(false); let isCategoryCreateModalOpen = $state(false);
let isCategoryMenuBottomSheetOpen = $state(false); let isCategoryMenuBottomSheetOpen = $state(false);
@@ -30,6 +30,19 @@
$effect(() => { $effect(() => {
info = getCategoryInfo(data.id, $masterKeyStore?.get(1)?.key!); info = getCategoryInfo(data.id, $masterKeyStore?.get(1)?.key!);
isFileRecursive = undefined;
});
$effect(() => {
if ($info && isFileRecursive === undefined) {
isFileRecursive = $info.isFileRecursive ?? false;
}
});
$effect(() => {
if (data.id !== "root" && $info?.isFileRecursive !== isFileRecursive) {
updateCategoryInfo(data.id as number, { isFileRecursive });
}
}); });
</script> </script>
@@ -41,7 +54,7 @@
<TopBar title={$info?.name} /> <TopBar title={$info?.name} />
{/if} {/if}
<div class="min-h-full bg-gray-100 pb-[5.5em]"> <div class="min-h-full bg-gray-100 pb-[5.5em]">
{#if $info} {#if $info && isFileRecursive !== undefined}
<Category <Category
bind:isFileRecursive bind:isFileRecursive
info={$info} info={$info}