mirror of
https://github.com/kmc7468/arkvault.git
synced 2026-02-04 08:06:56 +00:00
파일 페이지에서의 네트워크 호출 최적화
This commit is contained in:
@@ -1,14 +1,14 @@
|
||||
<script lang="ts">
|
||||
import FileSaver from "file-saver";
|
||||
import { untrack } from "svelte";
|
||||
import { get, type Writable } from "svelte/store";
|
||||
import { goto } from "$app/navigation";
|
||||
import { page } from "$app/state";
|
||||
import { FullscreenDiv } from "$lib/components/atoms";
|
||||
import { Categories, IconEntryButton, TopBar } from "$lib/components/molecules";
|
||||
import { getFileInfo, type FileInfo } from "$lib/modules/filesystem";
|
||||
import { getFileInfo, type FileInfo } from "$lib/modules/filesystem2.svelte";
|
||||
import { captureVideoThumbnail } from "$lib/modules/thumbnail";
|
||||
import { fileDownloadStatusStore, isFileDownloading, masterKeyStore } from "$lib/stores";
|
||||
import { getFileDownloadState } from "$lib/modules/file";
|
||||
import { masterKeyStore } from "$lib/stores";
|
||||
import AddToCategoryBottomSheet from "./AddToCategoryBottomSheet.svelte";
|
||||
import DownloadStatus from "./DownloadStatus.svelte";
|
||||
import {
|
||||
@@ -26,19 +26,13 @@
|
||||
|
||||
let { data } = $props();
|
||||
|
||||
let info: Writable<FileInfo | null> | undefined = $state();
|
||||
// let categories: Writable<CategoryInfo | null>[] = $state([]);
|
||||
let infoPromise: Promise<FileInfo | null> | undefined = $state();
|
||||
let info: FileInfo | null = $state(null);
|
||||
let downloadState = $derived(getFileDownloadState(data.id));
|
||||
|
||||
let isMenuOpen = $state(false);
|
||||
let isAddToCategoryBottomSheetOpen = $state(false);
|
||||
|
||||
let downloadStatus = $derived(
|
||||
$fileDownloadStatusStore.find((statusStore) => {
|
||||
const { id, status } = get(statusStore);
|
||||
return id === data.id && isFileDownloading(status);
|
||||
}),
|
||||
);
|
||||
|
||||
let isDownloadRequested = $state(false);
|
||||
let viewerType: "image" | "video" | undefined = $state();
|
||||
let fileBlob: Blob | undefined = $state();
|
||||
@@ -71,28 +65,27 @@
|
||||
const addToCategory = async (categoryId: number) => {
|
||||
await requestFileAdditionToCategory(data.id, categoryId);
|
||||
isAddToCategoryBottomSheetOpen = false;
|
||||
info = getFileInfo(data.id, $masterKeyStore?.get(1)?.key!); // TODO: FIXME
|
||||
infoPromise = getFileInfo(data.id, $masterKeyStore?.get(1)?.key!); // TODO: FIXME
|
||||
};
|
||||
|
||||
const removeFromCategory = async (categoryId: number) => {
|
||||
await requestFileRemovalFromCategory(data.id, categoryId);
|
||||
info = getFileInfo(data.id, $masterKeyStore?.get(1)?.key!); // TODO: FIXME
|
||||
infoPromise = getFileInfo(data.id, $masterKeyStore?.get(1)?.key!); // TODO: FIXME
|
||||
};
|
||||
|
||||
$effect(() => {
|
||||
info = getFileInfo(data.id, $masterKeyStore?.get(1)?.key!);
|
||||
infoPromise = getFileInfo(data.id, $masterKeyStore?.get(1)?.key!).then((fileInfo) => {
|
||||
info = fileInfo;
|
||||
return fileInfo;
|
||||
});
|
||||
info = null;
|
||||
isDownloadRequested = false;
|
||||
viewerType = undefined;
|
||||
});
|
||||
|
||||
// $effect(() => {
|
||||
// categories =
|
||||
// $info?.categoryIds.map((id) => getCategoryInfo(id, $masterKeyStore?.get(1)?.key!)) ?? [];
|
||||
// });
|
||||
|
||||
$effect(() => {
|
||||
if ($info && $info.dataKey && $info.contentIv) {
|
||||
const contentType = $info.contentType;
|
||||
if (info?.dataKey) {
|
||||
const contentType = info.contentType;
|
||||
if (contentType.startsWith("image")) {
|
||||
viewerType = "image";
|
||||
} else if (contentType.startsWith("video")) {
|
||||
@@ -100,24 +93,24 @@
|
||||
}
|
||||
|
||||
untrack(() => {
|
||||
if (!downloadStatus && !isDownloadRequested) {
|
||||
if (!downloadState && !isDownloadRequested) {
|
||||
isDownloadRequested = true;
|
||||
requestFileDownload(data.id, $info.contentIv!, $info.dataKey!).then(async (buffer) => {
|
||||
const blob = await updateViewer(buffer, contentType);
|
||||
if (!viewerType) {
|
||||
FileSaver.saveAs(blob, $info.name);
|
||||
}
|
||||
});
|
||||
requestFileDownload(data.id, info!.contentIv!, info!.dataKey!.key).then(
|
||||
async (buffer) => {
|
||||
const blob = await updateViewer(buffer, contentType);
|
||||
if (!viewerType) {
|
||||
FileSaver.saveAs(blob, info!.name);
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
$effect(() => {
|
||||
if ($info && $downloadStatus?.status === "decrypted") {
|
||||
untrack(
|
||||
() => !isDownloadRequested && updateViewer($downloadStatus.result!, $info.contentType),
|
||||
);
|
||||
if (info && downloadState?.status === "decrypted") {
|
||||
untrack(() => !isDownloadRequested && updateViewer(downloadState.result!, info!.contentType));
|
||||
}
|
||||
});
|
||||
|
||||
@@ -128,84 +121,88 @@
|
||||
<title>파일</title>
|
||||
</svelte:head>
|
||||
|
||||
<TopBar title={$info?.name}>
|
||||
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
||||
<!-- svelte-ignore a11y_click_events_have_key_events -->
|
||||
<div onclick={(e) => e.stopPropagation()}>
|
||||
<button
|
||||
onclick={() => (isMenuOpen = !isMenuOpen)}
|
||||
class="w-[2.3rem] flex-shrink-0 rounded-full p-1 active:bg-black active:bg-opacity-[0.04]"
|
||||
>
|
||||
<IconMoreVert class="text-2xl" />
|
||||
</button>
|
||||
<TopBarMenu
|
||||
bind:isOpen={isMenuOpen}
|
||||
directoryId={["category", "gallery"].includes(page.url.searchParams.get("from") ?? "")
|
||||
? $info?.parentId
|
||||
: undefined}
|
||||
{fileBlob}
|
||||
filename={$info?.name}
|
||||
/>
|
||||
</div>
|
||||
</TopBar>
|
||||
<FullscreenDiv>
|
||||
<div class="space-y-4 pb-4">
|
||||
<DownloadStatus status={downloadStatus} />
|
||||
{#if $info && viewerType}
|
||||
<div class="flex w-full justify-center">
|
||||
{#snippet viewerLoading(message: string)}
|
||||
<p class="text-gray-500">{message}</p>
|
||||
{/snippet}
|
||||
{#if info}
|
||||
<TopBar title={info.name}>
|
||||
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
||||
<!-- svelte-ignore a11y_click_events_have_key_events -->
|
||||
<div onclick={(e) => e.stopPropagation()}>
|
||||
<button
|
||||
onclick={() => (isMenuOpen = !isMenuOpen)}
|
||||
class="w-[2.3rem] flex-shrink-0 rounded-full p-1 active:bg-black active:bg-opacity-[0.04]"
|
||||
>
|
||||
<IconMoreVert class="text-2xl" />
|
||||
</button>
|
||||
<TopBarMenu
|
||||
bind:isOpen={isMenuOpen}
|
||||
directoryId={["category", "gallery"].includes(page.url.searchParams.get("from") ?? "")
|
||||
? info.parentId
|
||||
: undefined}
|
||||
{fileBlob}
|
||||
filename={info.name}
|
||||
/>
|
||||
</div>
|
||||
</TopBar>
|
||||
<FullscreenDiv>
|
||||
<div class="space-y-4 pb-4">
|
||||
{#if downloadState}
|
||||
<DownloadStatus state={downloadState} />
|
||||
{/if}
|
||||
{#if viewerType}
|
||||
<div class="flex w-full justify-center">
|
||||
{#snippet viewerLoading(message: string)}
|
||||
<p class="text-gray-500">{message}</p>
|
||||
{/snippet}
|
||||
|
||||
{#if viewerType === "image"}
|
||||
{#if fileBlobUrl}
|
||||
<img src={fileBlobUrl} alt={$info.name} onerror={convertHeicToJpeg} />
|
||||
{:else}
|
||||
{@render viewerLoading("이미지를 불러오고 있어요.")}
|
||||
{#if viewerType === "image"}
|
||||
{#if fileBlobUrl}
|
||||
<img src={fileBlobUrl} alt={info.name} onerror={convertHeicToJpeg} />
|
||||
{:else}
|
||||
{@render viewerLoading("이미지를 불러오고 있어요.")}
|
||||
{/if}
|
||||
{:else if viewerType === "video"}
|
||||
{#if fileBlobUrl}
|
||||
<div class="flex flex-col space-y-2">
|
||||
<!-- svelte-ignore a11y_media_has_caption -->
|
||||
<video bind:this={videoElement} src={fileBlobUrl} controls muted></video>
|
||||
<IconEntryButton
|
||||
icon={IconCamera}
|
||||
onclick={() => updateThumbnail(info?.dataKey?.key!, info?.dataKey?.version!)}
|
||||
class="w-full"
|
||||
>
|
||||
이 장면을 썸네일로 설정하기
|
||||
</IconEntryButton>
|
||||
</div>
|
||||
{:else}
|
||||
{@render viewerLoading("비디오를 불러오고 있어요.")}
|
||||
{/if}
|
||||
{/if}
|
||||
{:else if viewerType === "video"}
|
||||
{#if fileBlobUrl}
|
||||
<div class="flex flex-col space-y-2">
|
||||
<!-- svelte-ignore a11y_media_has_caption -->
|
||||
<video bind:this={videoElement} src={fileBlobUrl} controls muted></video>
|
||||
<IconEntryButton
|
||||
icon={IconCamera}
|
||||
onclick={() => updateThumbnail($info.dataKey!, $info.dataKeyVersion!)}
|
||||
class="w-full"
|
||||
>
|
||||
이 장면을 썸네일로 설정하기
|
||||
</IconEntryButton>
|
||||
</div>
|
||||
{:else}
|
||||
{@render viewerLoading("비디오를 불러오고 있어요.")}
|
||||
{/if}
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
<div class="space-y-2">
|
||||
<p class="text-lg font-bold">카테고리</p>
|
||||
<div class="space-y-1">
|
||||
<!-- <Categories
|
||||
{categories}
|
||||
categoryMenuIcon={IconClose}
|
||||
onCategoryClick={({ id }) => goto(`/category/${id}`)}
|
||||
onCategoryMenuClick={({ id }) => removeFromCategory(id)}
|
||||
/> -->
|
||||
<IconEntryButton
|
||||
icon={IconAddCircle}
|
||||
onclick={() => (isAddToCategoryBottomSheetOpen = true)}
|
||||
class="h-12 w-full"
|
||||
iconClass="text-gray-600"
|
||||
textClass="text-gray-700"
|
||||
>
|
||||
카테고리에 추가하기
|
||||
</IconEntryButton>
|
||||
</div>
|
||||
{/if}
|
||||
<div class="space-y-2">
|
||||
<p class="text-lg font-bold">카테고리</p>
|
||||
<div class="space-y-1">
|
||||
<Categories
|
||||
categories={info.categories}
|
||||
categoryMenuIcon={IconClose}
|
||||
onCategoryClick={({ id }) => goto(`/category/${id}`)}
|
||||
onCategoryMenuClick={({ id }) => removeFromCategory(id)}
|
||||
/>
|
||||
<IconEntryButton
|
||||
icon={IconAddCircle}
|
||||
onclick={() => (isAddToCategoryBottomSheetOpen = true)}
|
||||
class="h-12 w-full"
|
||||
iconClass="text-gray-600"
|
||||
textClass="text-gray-700"
|
||||
>
|
||||
카테고리에 추가하기
|
||||
</IconEntryButton>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</FullscreenDiv>
|
||||
</FullscreenDiv>
|
||||
|
||||
<AddToCategoryBottomSheet
|
||||
bind:isOpen={isAddToCategoryBottomSheetOpen}
|
||||
onAddToCategoryClick={addToCategory}
|
||||
/>
|
||||
<AddToCategoryBottomSheet
|
||||
bind:isOpen={isAddToCategoryBottomSheetOpen}
|
||||
onAddToCategoryClick={addToCategory}
|
||||
/>
|
||||
{/if}
|
||||
|
||||
@@ -1,32 +1,31 @@
|
||||
<script lang="ts">
|
||||
import type { Writable } from "svelte/store";
|
||||
import { isFileDownloading, type FileDownloadStatus } from "$lib/stores";
|
||||
import { isFileDownloading, type FileDownloadState } from "$lib/modules/file";
|
||||
import { formatNetworkSpeed } from "$lib/utils";
|
||||
|
||||
interface Props {
|
||||
status?: Writable<FileDownloadStatus>;
|
||||
state: FileDownloadState;
|
||||
}
|
||||
|
||||
let { status }: Props = $props();
|
||||
let { state }: Props = $props();
|
||||
</script>
|
||||
|
||||
{#if $status && isFileDownloading($status.status)}
|
||||
{#if isFileDownloading(state.status)}
|
||||
<div class="w-full rounded-xl bg-gray-100 p-3">
|
||||
<p class="font-medium">
|
||||
{#if $status.status === "download-pending"}
|
||||
{#if state.status === "download-pending"}
|
||||
다운로드를 기다리는 중
|
||||
{:else if $status.status === "downloading"}
|
||||
{:else if state.status === "downloading"}
|
||||
다운로드하는 중
|
||||
{:else if $status.status === "decryption-pending"}
|
||||
{:else if state.status === "decryption-pending"}
|
||||
복호화를 기다리는 중
|
||||
{:else if $status.status === "decrypting"}
|
||||
{:else if state.status === "decrypting"}
|
||||
복호화하는 중
|
||||
{/if}
|
||||
</p>
|
||||
<p class="text-xs">
|
||||
{#if $status.status === "downloading"}
|
||||
{#if state.status === "downloading"}
|
||||
전송됨
|
||||
{Math.floor(($status.progress ?? 0) * 100)}% · {formatNetworkSpeed(($status.rate ?? 0) * 8)}
|
||||
{Math.floor((state.progress ?? 0) * 100)}% · {formatNetworkSpeed((state.rate ?? 0) * 8)}
|
||||
{/if}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -1,19 +1,21 @@
|
||||
<script lang="ts">
|
||||
import { get } from "svelte/store";
|
||||
import { FullscreenDiv } from "$lib/components/atoms";
|
||||
import { TopBar } from "$lib/components/molecules";
|
||||
import { fileDownloadStatusStore, isFileDownloading } from "$lib/stores";
|
||||
import { getFileInfo } from "$lib/modules/filesystem2.svelte";
|
||||
import { getDownloadingFiles, clearDownloadedFiles } from "$lib/modules/file";
|
||||
import { masterKeyStore } from "$lib/stores";
|
||||
import File from "./File.svelte";
|
||||
|
||||
let downloadingFiles = $derived(
|
||||
$fileDownloadStatusStore.filter((status) => isFileDownloading(get(status).status)),
|
||||
let downloadingFilesPromise = $derived(
|
||||
Promise.all(
|
||||
getDownloadingFiles().map(async (file) => ({
|
||||
state: file,
|
||||
fileInfo: await getFileInfo(file.id, $masterKeyStore?.get(1)?.key!),
|
||||
})),
|
||||
),
|
||||
);
|
||||
|
||||
$effect(() => () => {
|
||||
$fileDownloadStatusStore = $fileDownloadStatusStore.filter((status) =>
|
||||
isFileDownloading(get(status).status),
|
||||
);
|
||||
});
|
||||
$effect(() => clearDownloadedFiles);
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
@@ -22,9 +24,9 @@
|
||||
|
||||
<TopBar />
|
||||
<FullscreenDiv>
|
||||
<div class="space-y-2 pb-4">
|
||||
{#each downloadingFiles as status}
|
||||
<File {status} />
|
||||
{#await downloadingFilesPromise then downloadingFiles}
|
||||
{#each downloadingFiles as { state, fileInfo }}
|
||||
<File {state} info={fileInfo} />
|
||||
{/each}
|
||||
</div>
|
||||
{/await}
|
||||
</FullscreenDiv>
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
<script lang="ts">
|
||||
import { get, type Writable } from "svelte/store";
|
||||
import { getFileInfo, type FileInfo } from "$lib/modules/filesystem";
|
||||
import { masterKeyStore, type FileDownloadStatus } from "$lib/stores";
|
||||
import type { FileDownloadState } from "$lib/modules/file";
|
||||
import type { FileInfo } from "$lib/modules/filesystem2.svelte";
|
||||
import { formatNetworkSpeed } from "$lib/utils";
|
||||
|
||||
import IconCloud from "~icons/material-symbols/cloud";
|
||||
@@ -12,56 +11,49 @@
|
||||
import IconError from "~icons/material-symbols/error";
|
||||
|
||||
interface Props {
|
||||
status: Writable<FileDownloadStatus>;
|
||||
info: FileInfo;
|
||||
state: FileDownloadState;
|
||||
}
|
||||
|
||||
let { status }: Props = $props();
|
||||
|
||||
let fileInfo: Writable<FileInfo | null> | undefined = $state();
|
||||
|
||||
$effect(() => {
|
||||
fileInfo = getFileInfo(get(status).id, $masterKeyStore?.get(1)?.key!);
|
||||
});
|
||||
let { info, state }: Props = $props();
|
||||
</script>
|
||||
|
||||
{#if $fileInfo}
|
||||
<div class="flex h-14 items-center gap-x-4 p-2">
|
||||
<div class="flex-shrink-0 text-lg text-gray-600">
|
||||
{#if $status.status === "download-pending"}
|
||||
<IconCloud />
|
||||
{:else if $status.status === "downloading"}
|
||||
<IconCloudDownload />
|
||||
{:else if $status.status === "decryption-pending"}
|
||||
<IconLock />
|
||||
{:else if $status.status === "decrypting"}
|
||||
<IconLockClock />
|
||||
{:else if $status.status === "decrypted"}
|
||||
<IconCheckCircle class="text-green-500" />
|
||||
{:else if $status.status === "error"}
|
||||
<IconError class="text-red-500" />
|
||||
{/if}
|
||||
</div>
|
||||
<div class="flex-grow overflow-hidden">
|
||||
<p title={$fileInfo.name} class="truncate font-medium">
|
||||
{$fileInfo.name}
|
||||
</p>
|
||||
<p class="text-xs text-gray-800">
|
||||
{#if $status.status === "download-pending"}
|
||||
다운로드를 기다리는 중
|
||||
{:else if $status.status === "downloading"}
|
||||
전송됨
|
||||
{Math.floor(($status.progress ?? 0) * 100)}% ·
|
||||
{formatNetworkSpeed(($status.rate ?? 0) * 8)}
|
||||
{:else if $status.status === "decryption-pending"}
|
||||
복호화를 기다리는 중
|
||||
{:else if $status.status === "decrypting"}
|
||||
복호화하는 중
|
||||
{:else if $status.status === "decrypted"}
|
||||
다운로드 완료
|
||||
{:else if $status.status === "error"}
|
||||
다운로드 실패
|
||||
{/if}
|
||||
</p>
|
||||
</div>
|
||||
<div class="flex h-14 items-center gap-x-4 p-2">
|
||||
<div class="flex-shrink-0 text-lg text-gray-600">
|
||||
{#if state.status === "download-pending"}
|
||||
<IconCloud />
|
||||
{:else if state.status === "downloading"}
|
||||
<IconCloudDownload />
|
||||
{:else if state.status === "decryption-pending"}
|
||||
<IconLock />
|
||||
{:else if state.status === "decrypting"}
|
||||
<IconLockClock />
|
||||
{:else if state.status === "decrypted"}
|
||||
<IconCheckCircle class="text-green-500" />
|
||||
{:else if state.status === "error"}
|
||||
<IconError class="text-red-500" />
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
<div class="flex-grow overflow-hidden">
|
||||
<p title={info.name} class="truncate font-medium">
|
||||
{info.name}
|
||||
</p>
|
||||
<p class="text-xs text-gray-800">
|
||||
{#if state.status === "download-pending"}
|
||||
다운로드를 기다리는 중
|
||||
{:else if state.status === "downloading"}
|
||||
전송됨
|
||||
{Math.floor((state.progress ?? 0) * 100)}% ·
|
||||
{formatNetworkSpeed((state.rate ?? 0) * 8)}
|
||||
{:else if state.status === "decryption-pending"}
|
||||
복호화를 기다리는 중
|
||||
{:else if state.status === "decrypting"}
|
||||
복호화하는 중
|
||||
{:else if state.status === "decrypted"}
|
||||
다운로드 완료
|
||||
{:else if state.status === "error"}
|
||||
다운로드 실패
|
||||
{/if}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,18 +1,20 @@
|
||||
<script lang="ts">
|
||||
import type { Writable } from "svelte/store";
|
||||
import { onMount } from "svelte";
|
||||
import { goto } from "$app/navigation";
|
||||
import { FullscreenDiv } from "$lib/components/atoms";
|
||||
import { TopBar } from "$lib/components/molecules";
|
||||
import { Gallery } from "$lib/components/organisms";
|
||||
import { getFileInfo, type FileInfo } from "$lib/modules/filesystem";
|
||||
import { getFileInfo, type FileInfo } from "$lib/modules/filesystem2.svelte";
|
||||
import { masterKeyStore } from "$lib/stores";
|
||||
|
||||
let { data } = $props();
|
||||
|
||||
let files: Writable<FileInfo | null>[] = $state([]);
|
||||
let files: (FileInfo | null)[] = $state([]);
|
||||
|
||||
$effect(() => {
|
||||
files = data.files.map((file) => getFileInfo(file, $masterKeyStore?.get(1)?.key!));
|
||||
onMount(async () => {
|
||||
files = await Promise.all(
|
||||
data.files.map((file) => getFileInfo(file, $masterKeyStore?.get(1)?.key!)),
|
||||
);
|
||||
});
|
||||
</script>
|
||||
|
||||
@@ -22,5 +24,8 @@
|
||||
|
||||
<TopBar title="사진 및 동영상" />
|
||||
<FullscreenDiv>
|
||||
<Gallery {files} onFileClick={({ id }) => goto(`/file/${id}?from=gallery`)} />
|
||||
<Gallery
|
||||
files={files.filter((file) => !!file)}
|
||||
onFileClick={({ id }) => goto(`/file/${id}?from=gallery`)}
|
||||
/>
|
||||
</FullscreenDiv>
|
||||
|
||||
@@ -1,18 +1,17 @@
|
||||
<script lang="ts">
|
||||
import { onMount } from "svelte";
|
||||
import type { Writable } from "svelte/store";
|
||||
import { FullscreenDiv } from "$lib/components/atoms";
|
||||
import { TopBar } from "$lib/components/molecules";
|
||||
import type { FileCacheIndex } from "$lib/indexedDB";
|
||||
import { getFileCacheIndex, deleteFileCache as doDeleteFileCache } from "$lib/modules/file";
|
||||
import { getFileInfo, type FileInfo } from "$lib/modules/filesystem";
|
||||
import { getFileInfo, type FileInfo } from "$lib/modules/filesystem2.svelte";
|
||||
import { masterKeyStore } from "$lib/stores";
|
||||
import { formatFileSize } from "$lib/utils";
|
||||
import File from "./File.svelte";
|
||||
|
||||
interface FileCache {
|
||||
index: FileCacheIndex;
|
||||
fileInfo: Writable<FileInfo | null>;
|
||||
fileInfo: FileInfo | null;
|
||||
}
|
||||
|
||||
let fileCache: FileCache[] | undefined = $state();
|
||||
@@ -23,13 +22,14 @@
|
||||
fileCache = fileCache?.filter(({ index }) => index.fileId !== fileId);
|
||||
};
|
||||
|
||||
onMount(() => {
|
||||
fileCache = getFileCacheIndex()
|
||||
.map((index) => ({
|
||||
onMount(async () => {
|
||||
fileCache = await Promise.all(
|
||||
getFileCacheIndex().map(async (index) => ({
|
||||
index,
|
||||
fileInfo: getFileInfo(index.fileId, $masterKeyStore?.get(1)?.key!),
|
||||
}))
|
||||
.sort((a, b) => a.index.lastRetrievedAt.getTime() - b.index.lastRetrievedAt.getTime());
|
||||
fileInfo: await getFileInfo(index.fileId, $masterKeyStore?.get(1)?.key!),
|
||||
})),
|
||||
);
|
||||
fileCache.sort((a, b) => a.index.lastRetrievedAt.getTime() - b.index.lastRetrievedAt.getTime());
|
||||
});
|
||||
|
||||
$effect(() => {
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
<script lang="ts">
|
||||
import type { Writable } from "svelte/store";
|
||||
import type { FileCacheIndex } from "$lib/indexedDB";
|
||||
import type { FileInfo } from "$lib/modules/filesystem";
|
||||
import type { SummarizedFileInfo } from "$lib/modules/filesystem2.svelte";
|
||||
import { formatDate, formatFileSize } from "$lib/utils";
|
||||
|
||||
import IconDraft from "~icons/material-symbols/draft";
|
||||
@@ -10,7 +9,7 @@
|
||||
|
||||
interface Props {
|
||||
index: FileCacheIndex;
|
||||
info: Writable<FileInfo | null>;
|
||||
info: SummarizedFileInfo | null;
|
||||
onDeleteClick: (fileId: number) => void;
|
||||
}
|
||||
|
||||
@@ -18,7 +17,7 @@
|
||||
</script>
|
||||
|
||||
<div class="flex h-14 items-center gap-x-4 p-2">
|
||||
{#if $info}
|
||||
{#if info}
|
||||
<div class="flex-shrink-0 rounded-full bg-blue-100 p-1 text-xl">
|
||||
<IconDraft class="text-blue-400" />
|
||||
</div>
|
||||
@@ -28,8 +27,8 @@
|
||||
</div>
|
||||
{/if}
|
||||
<div class="flex-grow overflow-hidden">
|
||||
{#if $info}
|
||||
<p title={$info.name} class="truncate font-medium">{$info.name}</p>
|
||||
{#if info}
|
||||
<p title={info.name} class="truncate font-medium">{info.name}</p>
|
||||
{:else}
|
||||
<p class="font-medium">삭제된 파일</p>
|
||||
{/if}
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
import { BottomDiv, Button, FullscreenDiv } from "$lib/components/atoms";
|
||||
import { IconEntryButton, TopBar } from "$lib/components/molecules";
|
||||
import { deleteAllFileThumbnailCaches } from "$lib/modules/file";
|
||||
import { getFileInfo } from "$lib/modules/filesystem";
|
||||
import { getFileInfo } from "$lib/modules/filesystem2.svelte";
|
||||
import { masterKeyStore } from "$lib/stores";
|
||||
import File from "./File.svelte";
|
||||
import {
|
||||
@@ -20,19 +20,20 @@
|
||||
|
||||
const generateAllThumbnails = () => {
|
||||
persistentStates.files.forEach(({ info }) => {
|
||||
const fileInfo = get(info);
|
||||
if (fileInfo) {
|
||||
requestThumbnailGeneration(fileInfo);
|
||||
if (info) {
|
||||
requestThumbnailGeneration(info);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
onMount(() => {
|
||||
persistentStates.files = data.files.map((fileId) => ({
|
||||
id: fileId,
|
||||
info: getFileInfo(fileId, $masterKeyStore?.get(1)?.key!),
|
||||
status: getGenerationStatus(fileId),
|
||||
}));
|
||||
onMount(async () => {
|
||||
persistentStates.files = await Promise.all(
|
||||
data.files.map(async (fileId) => ({
|
||||
id: fileId,
|
||||
info: await getFileInfo(fileId, $masterKeyStore?.get(1)?.key!),
|
||||
status: getGenerationStatus(fileId),
|
||||
})),
|
||||
);
|
||||
});
|
||||
</script>
|
||||
|
||||
|
||||
@@ -13,34 +13,32 @@
|
||||
import type { Writable } from "svelte/store";
|
||||
import { ActionEntryButton } from "$lib/components/atoms";
|
||||
import { DirectoryEntryLabel } from "$lib/components/molecules";
|
||||
import type { FileInfo } from "$lib/modules/filesystem";
|
||||
import type { FileInfo } from "$lib/modules/filesystem2.svelte";
|
||||
import { formatDateTime } from "$lib/utils";
|
||||
import type { GenerationStatus } from "./service.svelte";
|
||||
|
||||
import IconCamera from "~icons/material-symbols/camera";
|
||||
|
||||
interface Props {
|
||||
info: Writable<FileInfo | null>;
|
||||
onclick: (selectedFile: FileInfo) => void;
|
||||
onGenerateThumbnailClick: (selectedFile: FileInfo) => void;
|
||||
info: FileInfo;
|
||||
onclick: (file: FileInfo) => void;
|
||||
onGenerateThumbnailClick: (file: FileInfo) => void;
|
||||
generationStatus?: Writable<GenerationStatus>;
|
||||
}
|
||||
|
||||
let { info, onclick, onGenerateThumbnailClick, generationStatus }: Props = $props();
|
||||
</script>
|
||||
|
||||
{#if $info}
|
||||
<ActionEntryButton
|
||||
class="h-14"
|
||||
onclick={() => onclick($info)}
|
||||
actionButtonIcon={!$generationStatus || $generationStatus === "error" ? IconCamera : undefined}
|
||||
onActionButtonClick={() => onGenerateThumbnailClick($info)}
|
||||
actionButtonClass="text-gray-800"
|
||||
>
|
||||
{@const subtext =
|
||||
$generationStatus && $generationStatus !== "uploaded"
|
||||
? subtexts[$generationStatus]
|
||||
: formatDateTime($info.createdAt ?? $info.lastModifiedAt)}
|
||||
<DirectoryEntryLabel type="file" name={$info.name} {subtext} />
|
||||
</ActionEntryButton>
|
||||
{/if}
|
||||
<ActionEntryButton
|
||||
class="h-14"
|
||||
onclick={() => onclick(info)}
|
||||
actionButtonIcon={!$generationStatus || $generationStatus === "error" ? IconCamera : undefined}
|
||||
onActionButtonClick={() => onGenerateThumbnailClick(info)}
|
||||
actionButtonClass="text-gray-800"
|
||||
>
|
||||
{@const subtext =
|
||||
$generationStatus && $generationStatus !== "uploaded"
|
||||
? subtexts[$generationStatus]
|
||||
: formatDateTime(info.createdAt ?? info.lastModifiedAt)}
|
||||
<DirectoryEntryLabel type="file" name={info.name} {subtext} />
|
||||
</ActionEntryButton>
|
||||
|
||||
@@ -2,7 +2,7 @@ import { limitFunction } from "p-limit";
|
||||
import { get, writable, type Writable } from "svelte/store";
|
||||
import { encryptData } from "$lib/modules/crypto";
|
||||
import { storeFileThumbnailCache } from "$lib/modules/file";
|
||||
import type { FileInfo } from "$lib/modules/filesystem";
|
||||
import type { FileInfo } from "$lib/modules/filesystem2.svelte";
|
||||
import { generateThumbnail as doGenerateThumbnail } from "$lib/modules/thumbnail";
|
||||
import { requestFileDownload, requestFileThumbnailUpload } from "$lib/services/file";
|
||||
|
||||
@@ -17,7 +17,7 @@ export type GenerationStatus =
|
||||
|
||||
interface File {
|
||||
id: number;
|
||||
info: Writable<FileInfo | null>;
|
||||
info: FileInfo;
|
||||
status?: Writable<GenerationStatus>;
|
||||
}
|
||||
|
||||
@@ -129,7 +129,11 @@ export const requestThumbnailGeneration = async (fileInfo: FileInfo) => {
|
||||
|
||||
let fileSize = 0;
|
||||
try {
|
||||
const file = await requestFileDownload(fileInfo.id, fileInfo.contentIv!, fileInfo.dataKey!);
|
||||
const file = await requestFileDownload(
|
||||
fileInfo.id,
|
||||
fileInfo.contentIv!,
|
||||
fileInfo.dataKey?.key!,
|
||||
);
|
||||
fileSize = file.byteLength;
|
||||
|
||||
memoryUsage += fileSize;
|
||||
@@ -141,11 +145,11 @@ export const requestThumbnailGeneration = async (fileInfo: FileInfo) => {
|
||||
status,
|
||||
file,
|
||||
fileInfo.contentType,
|
||||
fileInfo.dataKey!,
|
||||
fileInfo.dataKey?.key!,
|
||||
);
|
||||
if (
|
||||
!thumbnail ||
|
||||
!(await requestThumbnailUpload(status, fileInfo.id, fileInfo.dataKeyVersion!, thumbnail))
|
||||
!(await requestThumbnailUpload(status, fileInfo.id, fileInfo.dataKey?.version!, thumbnail))
|
||||
) {
|
||||
status.set("error");
|
||||
}
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
<script lang="ts">
|
||||
import { untrack } from "svelte";
|
||||
import { get, type Writable } from "svelte/store";
|
||||
import { fileDownloadStatusStore, isFileDownloading, type FileDownloadStatus } from "$lib/stores";
|
||||
import { getDownloadingFiles } from "$lib/modules/file";
|
||||
|
||||
interface Props {
|
||||
onclick: () => void;
|
||||
@@ -9,23 +7,7 @@
|
||||
|
||||
let { onclick }: Props = $props();
|
||||
|
||||
let downloadingFiles: Writable<FileDownloadStatus>[] = $state([]);
|
||||
|
||||
$effect(() => {
|
||||
downloadingFiles = $fileDownloadStatusStore.filter((status) =>
|
||||
isFileDownloading(get(status).status),
|
||||
);
|
||||
return untrack(() => {
|
||||
const unsubscribes = downloadingFiles.map((downloadingFile) =>
|
||||
downloadingFile.subscribe(({ status }) => {
|
||||
if (!isFileDownloading(status)) {
|
||||
downloadingFiles = downloadingFiles.filter((file) => file !== downloadingFile);
|
||||
}
|
||||
}),
|
||||
);
|
||||
return () => unsubscribes.forEach((unsubscribe) => unsubscribe());
|
||||
});
|
||||
});
|
||||
let downloadingFiles = $derived(getDownloadingFiles());
|
||||
</script>
|
||||
|
||||
{#if downloadingFiles.length > 0}
|
||||
|
||||
@@ -1,17 +1,18 @@
|
||||
<script lang="ts">
|
||||
import type { Writable } from "svelte/store";
|
||||
import { onMount } from "svelte";
|
||||
import { goto } from "$app/navigation";
|
||||
import { EntryButton, FileThumbnailButton } from "$lib/components/atoms";
|
||||
import { getFileInfo, type FileInfo } from "$lib/modules/filesystem";
|
||||
import { getFileInfo, type FileInfo } from "$lib/modules/filesystem2.svelte";
|
||||
import { masterKeyStore } from "$lib/stores";
|
||||
import { requestFreshMediaFilesRetrieval } from "./service";
|
||||
|
||||
let mediaFiles: Writable<FileInfo | null>[] = $state([]);
|
||||
let mediaFiles: (FileInfo | null)[] = $state([]);
|
||||
|
||||
$effect(() => {
|
||||
requestFreshMediaFilesRetrieval().then((files) => {
|
||||
mediaFiles = files.map(({ id }) => getFileInfo(id, $masterKeyStore?.get(1)?.key!));
|
||||
});
|
||||
onMount(async () => {
|
||||
const files = await requestFreshMediaFilesRetrieval();
|
||||
mediaFiles = await Promise.all(
|
||||
files.map(({ id }) => getFileInfo(id, $masterKeyStore?.get(1)?.key!)),
|
||||
);
|
||||
});
|
||||
</script>
|
||||
|
||||
@@ -28,7 +29,9 @@
|
||||
{#if mediaFiles.length > 0}
|
||||
<div class="grid grid-cols-4 gap-2 p-2">
|
||||
{#each mediaFiles as file}
|
||||
<FileThumbnailButton info={file} onclick={({ id }) => goto(`/file/${id}`)} />
|
||||
{#if file}
|
||||
<FileThumbnailButton info={file} onclick={({ id }) => goto(`/file/${id}`)} />
|
||||
{/if}
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
@@ -1,23 +1,14 @@
|
||||
<script lang="ts">
|
||||
import { onMount } from "svelte";
|
||||
import { get } from "svelte/store";
|
||||
import { goto as svelteGoto } from "$app/navigation";
|
||||
import { getUploadingFiles } from "$lib/modules/file";
|
||||
import {
|
||||
fileDownloadStatusStore,
|
||||
isFileDownloading,
|
||||
clientKeyStore,
|
||||
masterKeyStore,
|
||||
} from "$lib/stores";
|
||||
import { getDownloadingFiles, getUploadingFiles } from "$lib/modules/file";
|
||||
import { clientKeyStore, masterKeyStore } from "$lib/stores";
|
||||
import "../app.css";
|
||||
|
||||
let { children } = $props();
|
||||
|
||||
const protectFileUploadAndDownload = (e: BeforeUnloadEvent) => {
|
||||
if (
|
||||
getUploadingFiles().length > 0 ||
|
||||
$fileDownloadStatusStore.some((status) => isFileDownloading(get(status).status))
|
||||
) {
|
||||
if (getDownloadingFiles().length > 0 || getUploadingFiles().length > 0) {
|
||||
e.preventDefault();
|
||||
}
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user