mirror of
https://github.com/kmc7468/arkvault.git
synced 2025-12-14 22:08:45 +00:00
heic 파일에 대한 썸네일 지원 추가 및 카테고리 페이지에서도 파일의 썸네일이 표시되도록 개선
This commit is contained in:
@@ -25,9 +25,9 @@
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
{#snippet iconSnippet()}
|
{#snippet iconSnippet()}
|
||||||
<div class="flex h-10 w-10 items-center justify-center overflow-y-hidden text-xl">
|
<div class="flex h-10 w-10 items-center justify-center text-xl">
|
||||||
{#if thumbnail}
|
{#if thumbnail}
|
||||||
<img src={thumbnail} alt={name} loading="lazy" />
|
<img src={thumbnail} alt={name} loading="lazy" class="aspect-square rounded object-cover" />
|
||||||
{:else if type === "directory"}
|
{:else if type === "directory"}
|
||||||
<IconFolder />
|
<IconFolder />
|
||||||
{:else}
|
{:else}
|
||||||
|
|||||||
@@ -2,8 +2,9 @@
|
|||||||
import type { Writable } from "svelte/store";
|
import type { Writable } from "svelte/store";
|
||||||
import { ActionEntryButton } from "$lib/components/atoms";
|
import { ActionEntryButton } from "$lib/components/atoms";
|
||||||
import { DirectoryEntryLabel } from "$lib/components/molecules";
|
import { DirectoryEntryLabel } from "$lib/components/molecules";
|
||||||
|
import { getFileThumbnail } from "$lib/modules/file";
|
||||||
import type { FileInfo } from "$lib/modules/filesystem";
|
import type { FileInfo } from "$lib/modules/filesystem";
|
||||||
import type { SelectedFile } from "./service";
|
import { requestFileThumbnailDownload, type SelectedFile } from "./service";
|
||||||
|
|
||||||
import IconClose from "~icons/material-symbols/close";
|
import IconClose from "~icons/material-symbols/close";
|
||||||
|
|
||||||
@@ -15,6 +16,8 @@
|
|||||||
|
|
||||||
let { info, onclick, onRemoveClick }: Props = $props();
|
let { info, onclick, onRemoveClick }: Props = $props();
|
||||||
|
|
||||||
|
let thumbnail: string | undefined = $state();
|
||||||
|
|
||||||
const openFile = () => {
|
const openFile = () => {
|
||||||
const { id, dataKey, dataKeyVersion, name } = $info as FileInfo;
|
const { id, dataKey, dataKeyVersion, name } = $info as FileInfo;
|
||||||
if (!dataKey || !dataKeyVersion) return; // TODO: Error handling
|
if (!dataKey || !dataKeyVersion) return; // TODO: Error handling
|
||||||
@@ -28,6 +31,24 @@
|
|||||||
|
|
||||||
onRemoveClick!({ id, dataKey, dataKeyVersion, name });
|
onRemoveClick!({ id, dataKey, dataKeyVersion, name });
|
||||||
};
|
};
|
||||||
|
|
||||||
|
$effect(() => {
|
||||||
|
if ($info?.dataKey) {
|
||||||
|
getFileThumbnail($info.id)
|
||||||
|
.then(
|
||||||
|
(thumbnailUrl) => thumbnailUrl || requestFileThumbnailDownload($info.id, $info.dataKey!),
|
||||||
|
)
|
||||||
|
.then((thumbnailUrl) => {
|
||||||
|
thumbnail = thumbnailUrl ?? undefined;
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
// TODO: Error Handling
|
||||||
|
thumbnail = undefined;
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
thumbnail = undefined;
|
||||||
|
}
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
{#if $info}
|
{#if $info}
|
||||||
@@ -37,6 +58,6 @@
|
|||||||
actionButtonIcon={onRemoveClick && IconClose}
|
actionButtonIcon={onRemoveClick && IconClose}
|
||||||
onActionButtonClick={removeFile}
|
onActionButtonClick={removeFile}
|
||||||
>
|
>
|
||||||
<DirectoryEntryLabel type="file" name={$info.name} />
|
<DirectoryEntryLabel type="file" {thumbnail} name={$info.name} />
|
||||||
</ActionEntryButton>
|
</ActionEntryButton>
|
||||||
{/if}
|
{/if}
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
export { requestFileThumbnailDownload } from "$lib/services/file";
|
||||||
|
|
||||||
export interface SelectedFile {
|
export interface SelectedFile {
|
||||||
id: number;
|
id: number;
|
||||||
dataKey: CryptoKey;
|
dataKey: CryptoKey;
|
||||||
|
|||||||
@@ -81,7 +81,11 @@ const extractExifDateTime = (fileBuffer: ArrayBuffer) => {
|
|||||||
const generateThumbnail = async (file: File, fileType: string) => {
|
const generateThumbnail = async (file: File, fileType: string) => {
|
||||||
let url;
|
let url;
|
||||||
try {
|
try {
|
||||||
if (fileType.startsWith("image/")) {
|
if (fileType === "image/heic") {
|
||||||
|
const { default: heic2any } = await import("heic2any");
|
||||||
|
url = URL.createObjectURL((await heic2any({ blob: file, toType: "image/png" })) as Blob);
|
||||||
|
return await generateImageThumbnail(url);
|
||||||
|
} else if (fileType.startsWith("image/")) {
|
||||||
url = URL.createObjectURL(file);
|
url = URL.createObjectURL(file);
|
||||||
return await generateImageThumbnail(url);
|
return await generateImageThumbnail(url);
|
||||||
} else if (fileType.startsWith("video/")) {
|
} else if (fileType.startsWith("video/")) {
|
||||||
|
|||||||
21
src/lib/services/file.ts
Normal file
21
src/lib/services/file.ts
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
import { callGetApi } from "$lib/hooks";
|
||||||
|
import { decryptData } from "$lib/modules/crypto";
|
||||||
|
import { storeFileThumbnail } from "$lib/modules/file";
|
||||||
|
import { getThumbnailUrl } from "$lib/modules/thumbnail";
|
||||||
|
import type { FileThumbnailInfoResponse } from "$lib/server/schemas";
|
||||||
|
|
||||||
|
export const requestFileThumbnailDownload = async (fileId: number, dataKey: CryptoKey) => {
|
||||||
|
let res = await callGetApi(`/api/file/${fileId}/thumbnail`);
|
||||||
|
if (!res.ok) return null;
|
||||||
|
|
||||||
|
const { contentIv: thumbnailEncryptedIv }: FileThumbnailInfoResponse = await res.json();
|
||||||
|
|
||||||
|
res = await callGetApi(`/api/file/${fileId}/thumbnail/download`);
|
||||||
|
if (!res.ok) return null;
|
||||||
|
|
||||||
|
const thumbnailEncrypted = await res.arrayBuffer();
|
||||||
|
const thumbnail = await decryptData(thumbnailEncrypted, thumbnailEncryptedIv, dataKey);
|
||||||
|
|
||||||
|
storeFileThumbnail(fileId, thumbnail); // Intended
|
||||||
|
return getThumbnailUrl(thumbnail);
|
||||||
|
};
|
||||||
@@ -21,7 +21,16 @@ export const generateThumbnail = limitFunction(
|
|||||||
async (fileBuffer: ArrayBuffer, fileType: string) => {
|
async (fileBuffer: ArrayBuffer, fileType: string) => {
|
||||||
let url;
|
let url;
|
||||||
try {
|
try {
|
||||||
if (fileType.startsWith("image/")) {
|
if (fileType === "image/heic") {
|
||||||
|
const { default: heic2any } = await import("heic2any");
|
||||||
|
url = URL.createObjectURL(
|
||||||
|
(await heic2any({
|
||||||
|
blob: new Blob([fileBuffer], { type: fileType }),
|
||||||
|
toType: "image/png",
|
||||||
|
})) as Blob,
|
||||||
|
);
|
||||||
|
return await generateImageThumbnail(url);
|
||||||
|
} else if (fileType.startsWith("image/")) {
|
||||||
url = URL.createObjectURL(new Blob([fileBuffer], { type: fileType }));
|
url = URL.createObjectURL(new Blob([fileBuffer], { type: fileType }));
|
||||||
return await generateImageThumbnail(url);
|
return await generateImageThumbnail(url);
|
||||||
} else if (fileType.startsWith("video/")) {
|
} else if (fileType.startsWith("video/")) {
|
||||||
|
|||||||
@@ -13,8 +13,8 @@
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
{#if isFileUploading($status.status)}
|
{#if isFileUploading($status.status)}
|
||||||
<div class="flex h-14 items-center gap-x-4 p-2">
|
<div class="flex h-14 gap-x-4 p-2">
|
||||||
<div class="flex-shrink-0 text-lg">
|
<div class="flex h-10 w-10 flex-shrink-0 items-center justify-center text-xl">
|
||||||
<IconDraft class="text-gray-600" />
|
<IconDraft class="text-gray-600" />
|
||||||
</div>
|
</div>
|
||||||
<div class="flex flex-grow flex-col overflow-hidden text-gray-800">
|
<div class="flex flex-grow flex-col overflow-hidden text-gray-800">
|
||||||
|
|||||||
@@ -1,21 +1 @@
|
|||||||
import { callGetApi } from "$lib/hooks";
|
export { requestFileThumbnailDownload } from "$lib/services/file";
|
||||||
import { decryptData } from "$lib/modules/crypto";
|
|
||||||
import { storeFileThumbnail } from "$lib/modules/file";
|
|
||||||
import { getThumbnailUrl } from "$lib/modules/thumbnail";
|
|
||||||
import type { FileThumbnailInfoResponse } from "$lib/server/schemas";
|
|
||||||
|
|
||||||
export const requestFileThumbnailDownload = async (fileId: number, dataKey: CryptoKey) => {
|
|
||||||
let res = await callGetApi(`/api/file/${fileId}/thumbnail`);
|
|
||||||
if (!res.ok) return null;
|
|
||||||
|
|
||||||
const { contentIv: thumbnailEncryptedIv }: FileThumbnailInfoResponse = await res.json();
|
|
||||||
|
|
||||||
res = await callGetApi(`/api/file/${fileId}/thumbnail/download`);
|
|
||||||
if (!res.ok) return null;
|
|
||||||
|
|
||||||
const thumbnailEncrypted = await res.arrayBuffer();
|
|
||||||
const thumbnail = await decryptData(thumbnailEncrypted, thumbnailEncryptedIv, dataKey);
|
|
||||||
|
|
||||||
storeFileThumbnail(fileId, thumbnail); // Intended
|
|
||||||
return getThumbnailUrl(thumbnail);
|
|
||||||
};
|
|
||||||
|
|||||||
Reference in New Issue
Block a user