파일 시스템 관련 상태와 변경 로직을 도메인 모듈로 통합하여 캐시가 자동으로 갱신되도록 개선하고 오류를 토스트로 표시하도록 변경

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
staticandClaude Opus 5.5 committed 2026-10-12 00:22:25 +09:00
1 parent 2ce73aa331
commit 4e82c5e556
67 files changed
+1091 -1082

No files matched your search

@@ -2,7 +2,8 @@
import { goto } from "$app/navigation";
import { BottomDiv, Button, FullscreenDiv, TextInput } from "#lib/components/atoms/index.js";
import { TitledDiv, TopBar } from "#lib/components/molecules/index.js";
import { requestPasswordChange } from "./service";
import { showToast, tryAction } from "#lib/client/toast.svelte.js";
import { changePassword as requestPasswordChange } from "#lib/features/auth/index.js";
let oldPassword = $state("");
let newPassword = $state("");
@@ -10,11 +11,16 @@
const changePassword = async () => {
if (newPassword !== confirmPassword) {
// TODO: Alert
showToast("새 비밀번호가 일치하지 않아요.");
return;
}
if (await requestPasswordChange(oldPassword, newPassword)) {
if (
await tryAction(
() => requestPasswordChange(oldPassword, newPassword),
"비밀번호를 바꾸지 못했어요. 기존 비밀번호를 확인해 주세요.",
)
) {
await goto("/menu");
}
};
@@ -1,11 +0,0 @@
import { trpc } from "#trpc/client.js";
export const requestPasswordChange = async (oldPassword: string, newPassword: string) => {
try {
await trpc.auth.changePassword.mutate({ oldPassword, newPassword });
return true;
} catch {
// TODO: Error Handling
return false;
}
};
+34 -30
View File
@@ -14,9 +14,10 @@
keyring,
requestClientRegistrationAndSessionUpgrade,
} from "#lib/client/keys/index.js";
import { requestLogout } from "#lib/services/auth.js";
import { requestDeletedFilesCleanup } from "#lib/services/file.js";
import { requestLogin } from "./service";
import { showErrorToast, showToast } from "#lib/client/toast.svelte.js";
import { login as requestLogin, logout as requestLogout } from "#lib/features/auth/index.js";
import { cleanupDeletedFileCaches } from "#lib/features/filesystem/index.js";
import { isTRPCClientError } from "#trpc/client.js";
let { data } = $props();
@@ -30,42 +31,45 @@
};
const upgradeSession = async (force: boolean) => {
try {
const [upgradeRes, upgradeError] = await requestClientRegistrationAndSessionUpgrade(force);
if (!force && upgradeError === "Already logged in") {
isForceLoginModalOpen = true;
return;
} else if (!upgradeRes) {
throw new Error("Failed to upgrade session");
}
const [upgradeRes, upgradeError] = await requestClientRegistrationAndSessionUpgrade(force);
if (!force && upgradeError === "Already logged in") {
isForceLoginModalOpen = true;
return;
} else if (!upgradeRes) {
showToast("로그인하지 못했어요.");
return;
}
// TODO: Multi-user support
// TODO: Multi-user support
if (keyring.hasMasterKeys || (await downloadMasterKeys())) {
await requestDeletedFilesCleanup();
await goto(data.redirectPath);
} else {
await redirect("/client/pending");
}
} catch (e) {
// TODO
throw e;
if (keyring.hasMasterKeys || (await downloadMasterKeys())) {
await cleanupDeletedFileCaches().catch(console.error);
await goto(data.redirectPath);
} else {
await redirect("/client/pending");
}
};
const login = async () => {
// TODO: Validation
if (!email || !password) {
showToast("이메일과 비밀번호를 입력해 주세요.");
return;
}
try {
if (!(await requestLogin(email, password))) throw new Error("Failed to login");
if (!keyring.clientKeys) return await redirect("/key/generate");
await upgradeSession(false);
await requestLogin(email, password);
} catch (e) {
// TODO: Alert
throw e;
if (isTRPCClientError(e) && e.data?.code === "UNAUTHORIZED") {
showToast("이메일 또는 비밀번호가 올바르지 않아요.");
} else {
showErrorToast(e, "로그인하지 못했어요.");
}
return;
}
if (!keyring.clientKeys) return await redirect("/key/generate");
await upgradeSession(false);
};
</script>
@@ -93,6 +97,6 @@
<ForceLoginModal
bind:isOpen={isForceLoginModalOpen}
oncancel={requestLogout}
oncancel={() => requestLogout().catch(console.error)}
onLoginClick={() => upgradeSession(true)}
/>
@@ -1,11 +0,0 @@
import { trpc } from "#trpc/client.js";
export const requestLogin = async (email: string, password: string) => {
try {
await trpc.auth.login.mutate({ email, password });
return true;
} catch {
// TODO: Error Handling
return false;
}
};
+60 -48
View File
@@ -5,20 +5,22 @@
import { page } from "$app/state";
import { FullscreenDiv } from "#lib/components/atoms/index.js";
import { Categories, IconEntryButton, TopBar } from "#lib/components/molecules/index.js";
import { getFileInfo, type MaybeFileInfo } from "#lib/modules/filesystem/index.js";
import { captureVideoThumbnail } from "#lib/client/media/thumbnail.js";
import { getFileDownloadState } from "#lib/client/transfer/index.js";
import { HybridPromise } from "#lib/utils/index.js";
import { showErrorToast, tryAction } from "#lib/client/toast.svelte.js";
import {
addFileToCategory,
getFileContent,
getFileStreamUrl,
removeFileFromCategory,
setFileFavorite,
uploadFileThumbnail,
useFile,
type DataKey,
type FileInfo,
} from "#lib/features/filesystem/index.js";
import AddToCategoryBottomSheet from "./AddToCategoryBottomSheet.svelte";
import DownloadStatus from "./DownloadStatus.svelte";
import {
requestFileRemovalFromCategory,
requestFileDownload,
requestThumbnailUpload,
requestFileAdditionToCategory,
requestVideoStream,
requestFavoriteToggle,
} from "./service";
import TopBarMenu from "./TopBarMenu.svelte";
import IconMoreVert from "~icons/material-symbols/more-vert";
@@ -28,7 +30,8 @@
let { data } = $props();
let info: MaybeFileInfo | undefined = $state();
const file = useFile(() => data.id);
let info = $derived(file.current);
let downloadState = $derived(getFileDownloadState(data.id));
let isMenuOpen = $state(false);
@@ -59,33 +62,43 @@
);
};
const updateThumbnail = async (dataKey: CryptoKey, dataKeyVersion: Date) => {
const thumbnail = await captureVideoThumbnail(videoElement!);
await requestThumbnailUpload(data.id, thumbnail, dataKey, dataKeyVersion);
};
const updateThumbnail = (dataKey: DataKey) =>
tryAction(async () => {
const thumbnail = await captureVideoThumbnail(videoElement!);
await uploadFileThumbnail(data.id, thumbnail, dataKey);
}, "썸네일을 설정하지 못했어요.");
const addToCategory = async (categoryId: number) => {
await requestFileAdditionToCategory(data.id, categoryId);
isAddToCategoryBottomSheetOpen = false;
void getFileInfo(data.id); // TODO: FIXME
if (
await tryAction(
() => addFileToCategory(data.id, categoryId),
"카테고리에 파일을 추가하지 못했어요.",
)
) {
isAddToCategoryBottomSheetOpen = false;
}
};
const removeFromCategory = async (categoryId: number) => {
await requestFileRemovalFromCategory(data.id, categoryId);
void getFileInfo(data.id); // TODO: FIXME
};
const removeFromCategory = (categoryId: number) =>
tryAction(
() => removeFileFromCategory(data.id, categoryId),
"카테고리에서 파일을 제거하지 못했어요.",
);
const toggleFavorite = async () => {
await requestFavoriteToggle(data.id, !!info?.isFavorite);
void getFileInfo(data.id); // TODO: FIXME
};
const toggleFavorite = () =>
tryAction(
() => setFileFavorite(info as FileInfo, !info?.isFavorite),
"즐겨찾기를 변경하지 못했어요.",
);
$effect(() => {
HybridPromise.resolve(getFileInfo(data.id)).then((result) => {
if (data.id === result.id) {
info = result;
}
const loadContent = (dataKey: CryptoKey, isLegacy: boolean) =>
getFileContent(data.id, dataKey, isLegacy).catch((e) => {
showErrorToast(e, "파일을 불러오지 못했어요.");
throw e;
});
$effect.pre(() => {
void data.id; // Reset the viewer when navigating to another file
isDownloadRequested = false;
viewerType = undefined;
});
@@ -103,25 +116,24 @@
if (!downloadState && !isDownloadRequested) {
isDownloadRequested = true;
if (viewerType === "video" && !info!.isLegacy) {
requestVideoStream(data.id, info!.dataKey!.key, contentType).then((streamUrl) => {
if (streamUrl) {
const { key } = info!.dataKey!;
const isLegacy = !!info!.isLegacy;
if (viewerType === "video" && !isLegacy) {
getFileStreamUrl(data.id, key, contentType)
.then((streamUrl) => {
videoStreamUrl = streamUrl;
} else {
requestFileDownload(data.id, info!.dataKey!.key, info!.isLegacy!).then((buffer) =>
updateViewer(buffer, contentType),
);
})
.catch(async () => {
// Fall back to downloading the whole file
await updateViewer(await loadContent(key, isLegacy), contentType);
});
} else {
void loadContent(key, isLegacy).then(async (buffer) => {
const blob = await updateViewer(buffer, contentType);
if (!viewerType) {
FileSaver.saveAs(blob, info!.name);
}
});
} else {
requestFileDownload(data.id, info!.dataKey!.key, info!.isLegacy!).then(
async (buffer) => {
const blob = await updateViewer(buffer, contentType);
if (!viewerType) {
FileSaver.saveAs(blob, info!.name);
}
},
);
}
}
});
@@ -192,7 +204,7 @@
></video>
<IconEntryButton
icon={IconCamera}
onclick={() => updateThumbnail(info?.dataKey?.key!, info?.dataKey?.version!)}
onclick={() => updateThumbnail(info!.dataKey!)}
class="w-full"
>
이 장면을 썸네일로 설정하기
@@ -2,9 +2,8 @@
import { BottomDiv, BottomSheet, Button, FullscreenDiv } from "#lib/components/atoms/index.js";
import { SubCategories } from "#lib/components/molecules/index.js";
import { CategoryCreateModal } from "#lib/components/organisms/index.js";
import { getCategoryInfo, type MaybeCategoryInfo } from "#lib/modules/filesystem/index.js";
import { HybridPromise } from "#lib/utils/index.js";
import { requestCategoryCreation } from "./service";
import { tryAction } from "#lib/client/toast.svelte.js";
import { createCategory, useCategory } from "#lib/features/filesystem/index.js";
interface Props {
onAddToCategoryClick: (categoryId: number) => void;
@@ -13,13 +12,15 @@
let { onAddToCategoryClick, isOpen = $bindable() }: Props = $props();
let categoryInfo: MaybeCategoryInfo | undefined = $state();
let categoryId: CategoryId = $state("root");
const category = useCategory(() => categoryId);
let categoryInfo = $derived(category.current);
let isCategoryCreateModalOpen = $state(false);
$effect(() => {
$effect.pre(() => {
if (isOpen) {
HybridPromise.resolve(getCategoryInfo("root")).then((result) => (categoryInfo = result));
categoryId = "root";
}
});
</script>
@@ -30,8 +31,7 @@
<SubCategories
class="py-4"
info={categoryInfo}
onSubCategoryClick={({ id }) =>
HybridPromise.resolve(getCategoryInfo(id)).then((result) => (categoryInfo = result))}
onSubCategoryClick={({ id }) => (categoryId = id)}
onSubCategoryCreateClick={() => (isCategoryCreateModalOpen = true)}
subCategoryCreatePosition="top"
/>
@@ -48,11 +48,6 @@
<CategoryCreateModal
bind:isOpen={isCategoryCreateModalOpen}
onCreateClick={async (name: string) => {
if (await requestCategoryCreation(name, categoryInfo!.id)) {
void getCategoryInfo(categoryInfo!.id); // TODO: FIXME
return true;
}
return false;
}}
onCreateClick={(name: string) =>
tryAction(() => createCategory(categoryId, name), "카테고리를 만들지 못했어요.")}
/>
@@ -1,64 +0,0 @@
import { storeFileThumbnailCache } from "#lib/client/cache/index.js";
import { prepareFileDecryption, getDecryptedFileUrl } from "#lib/client/serviceWorker.js";
import { requestFileThumbnailUpload } from "#lib/services/file.js";
import { trpc } from "#trpc/client.js";
export { requestCategoryCreation, requestFileRemovalFromCategory } from "#lib/services/category.js";
export { requestFileDownload } from "#lib/services/file.js";
export const requestVideoStream = async (
fileId: number,
dataKey: CryptoKey,
contentType: string,
) => {
const res = await fetch(`/api/file/${fileId}/download`, { method: "HEAD" });
if (!res.ok) return null;
const encContentSize = parseInt(res.headers.get("Content-Length") ?? "0", 10);
if (encContentSize <= 0) return null;
try {
await prepareFileDecryption(fileId, { isLegacy: false, dataKey, encContentSize, contentType });
return getDecryptedFileUrl(fileId);
} catch {
// TODO: Error Handling
return null;
}
};
export const requestThumbnailUpload = async (
fileId: number,
thumbnail: Blob,
dataKey: CryptoKey,
dataKeyVersion: Date,
) => {
const res = await requestFileThumbnailUpload(fileId, thumbnail, dataKey, dataKeyVersion);
if (!res) return false;
void thumbnail.arrayBuffer().then((buffer) => storeFileThumbnailCache(fileId, buffer));
return true;
};
export const requestFileAdditionToCategory = async (fileId: number, categoryId: number) => {
try {
await trpc.category.addFile.mutate({ id: categoryId, file: fileId });
return true;
} catch {
// TODO: Error Handling
return false;
}
};
export const requestFavoriteToggle = async (fileId: number, isFavorite: boolean) => {
try {
if (isFavorite) {
await trpc.favorites.removeFile.mutate({ id: fileId });
} else {
await trpc.favorites.addFile.mutate({ id: fileId });
}
return true;
} catch {
// TODO: Error Handling
return false;
}
};
@@ -7,7 +7,7 @@
clearDownloadedFiles,
type FileDownloadState,
} from "#lib/client/transfer/index.js";
import { bulkGetFileInfo, type MaybeFileInfo } from "#lib/modules/filesystem/index.js";
import { bulkGetFileInfo, type MaybeFileInfo } from "#lib/features/filesystem/index.js";
import File from "./File.svelte";
let downloadingFiles: { info: MaybeFileInfo; state: FileDownloadState }[] = $state([]);
@@ -1,6 +1,6 @@
<script lang="ts">
import type { FileDownloadState } from "#lib/client/transfer/index.js";
import type { SummarizedFileInfo } from "#lib/modules/filesystem/index.js";
import type { SummarizedFileInfo } from "#lib/features/filesystem/index.js";
import { formatNetworkSpeed } from "#lib/utils/index.js";
import IconCloud from "~icons/material-symbols/cloud";
+1 -1
View File
@@ -11,7 +11,7 @@
bulkGetFileInfo,
type MaybeFileInfo,
type SummarizedFileInfo,
} from "#lib/modules/filesystem/index.js";
} from "#lib/features/filesystem/index.js";
import { formatDate, formatDateSortable, SortBy, sortEntries } from "#lib/utils/index.js";
let { data } = $props();
+20 -19
View File
@@ -10,6 +10,7 @@
requestSessionUpgrade,
serializeClientKeys,
} from "#lib/client/keys/index.js";
import { showToast } from "#lib/client/toast.svelte.js";
import BeforeContinueBottomSheet from "./BeforeContinueBottomSheet.svelte";
import BeforeContinueModal from "./BeforeContinueModal.svelte";
@@ -33,26 +34,26 @@
};
const registerPubKeys = async () => {
try {
if (!(await requestClientRegistration())) throw new Error("Failed to register client");
await keyring.persistClientKeys();
if (!(await requestSessionUpgrade())[0]) throw new Error("Failed to upgrade session");
if (
!(await registerInitialMasterKeyAndHmacSecret(
data.masterKeyWrapped,
data.hmacSecretWrapped,
))
)
throw new Error("Failed to register initial MEK and HSK");
await goto("/client/pending?redirect=" + encodeURIComponent(data.redirectPath));
} catch (e) {
// TODO: Error handling
throw e;
if (!(await requestClientRegistration())) {
showToast("이 디바이스를 등록하지 못했어요.");
return;
}
await keyring.persistClientKeys();
if (!(await requestSessionUpgrade())[0]) {
showToast("로그인하지 못했어요.");
return;
}
if (
!(await registerInitialMasterKeyAndHmacSecret(data.masterKeyWrapped, data.hmacSecretWrapped))
) {
showToast("암호 키를 등록하지 못했어요.");
return;
}
await goto("/client/pending?redirect=" + encodeURIComponent(data.redirectPath));
};
</script>
@@ -13,7 +13,8 @@
registerInitialMasterKeyAndHmacSecret,
requestClientRegistrationAndSessionUpgrade,
} from "#lib/client/keys/index.js";
import { requestDeletedFilesCleanup } from "#lib/services/file.js";
import { showToast } from "#lib/client/toast.svelte.js";
import { cleanupDeletedFileCaches } from "#lib/features/filesystem/index.js";
import { gotoStateful } from "#lib/utils/index.js";
import Order from "./Order.svelte";
@@ -46,8 +47,6 @@
];
const generateKeys = async () => {
// TODO: Loading indicator
const { encryptKey, ...clientKeys } = await generateClientKeys();
const { masterKey, masterKeyWrapped } = await generateInitialMasterKey(encryptKey);
const { hmacSecretWrapped } = await generateInitialHmacSecret(masterKey);
@@ -66,7 +65,7 @@
isForceLoginModalOpen = true;
return;
} else if (!upgradeRes) {
// TODO: Error Handling
showToast("이 디바이스를 등록하지 못했어요.");
return;
}
@@ -78,11 +77,11 @@
await keyring.persistClientKeys();
if (!(await registerInitialMasterKeyAndHmacSecret(masterKeyWrapped, hmacSecretWrapped))) {
// TODO: Error Handling
showToast("암호 키를 등록하지 못했어요.");
return;
}
await requestDeletedFilesCleanup();
await cleanupDeletedFileCaches().catch(console.error);
await goto("/client/pending?redirect=" + encodeURIComponent(data.redirectPath));
};
@@ -90,10 +89,10 @@
const file = fileInput?.files?.[0];
if (!file) return;
if (await importClientKeys(await file.text())) {
if (await importClientKeys(await file.text()).catch(() => false)) {
await upgradeSession(false);
} else {
// TODO: Error Handling
showToast("올바른 암호 키 파일이 아니에요.");
}
fileInput!.value = "";
+20 -9
View File
@@ -5,17 +5,18 @@
import { slide } from "svelte/transition";
import { goto } from "$app/navigation";
import { Chip, FullscreenDiv, RowVirtualizer } from "#lib/components/atoms/index.js";
import { showErrorToast } from "#lib/client/toast.svelte.js";
import {
getDirectoryInfo,
type LocalCategoryInfo,
type MaybeDirectoryInfo,
} from "#lib/modules/filesystem/index.js";
import { HybridPromise, searchString, sortEntries } from "#lib/utils/index.js";
} from "#lib/features/filesystem/index.js";
import { search, type SearchFilter, type SearchResult } from "#lib/features/search/index.js";
import { searchString, sortEntries } from "#lib/utils/index.js";
import Directory from "./Directory.svelte";
import File from "./File.svelte";
import SearchBar from "./SearchBar.svelte";
import SelectCategoryBottomSheet from "./SelectCategoryBottomSheet.svelte";
import { requestSearch, type SearchFilter, type SearchResult } from "./service";
let { data } = $props();
@@ -121,10 +122,18 @@
$effect.pre(() => {
if (data.directoryId) {
HybridPromise.resolve(getDirectoryInfo(data.directoryId)).then((res) => {
const applyDirectoryInfo = (res: MaybeDirectoryInfo) => {
directoryInfo = res;
filters.searchInDirectory = res.exists;
});
};
// Apply synchronously if cached so that a restored snapshot isn't overwritten later
const res = getDirectoryInfo(data.directoryId);
if (res instanceof Promise) {
res.then(applyDirectoryInfo).catch(() => {});
} else {
applyDirectoryInfo(res);
}
} else {
directoryInfo = undefined;
filters.searchInDirectory = false;
@@ -148,13 +157,15 @@
}
if (hasAnyFilter) {
requestSearch({
search({
ancestorId: filters.searchInDirectory ? data.directoryId! : "root",
inFavorites: filters.searchInFavorites,
categories: filters.categories,
}).then((res) => {
serverResult = res;
});
})
.then((res) => {
serverResult = res;
})
.catch((e) => showErrorToast(e, "검색하지 못했어요."));
}
});
</script>
@@ -1,7 +1,7 @@
<script lang="ts">
import { ActionEntryButton } from "#lib/components/atoms/index.js";
import { DirectoryEntryLabel } from "#lib/components/molecules/index.js";
import type { SubDirectoryInfo } from "#lib/modules/filesystem/index.js";
import type { SubDirectoryInfo } from "#lib/features/filesystem/index.js";
interface Props {
info: SubDirectoryInfo;
+1 -1
View File
@@ -2,7 +2,7 @@
import { ActionEntryButton } from "#lib/components/atoms/index.js";
import { DirectoryEntryLabel } from "#lib/components/molecules/index.js";
import { getFileThumbnail } from "#lib/client/cache/index.js";
import type { SummarizedFileInfo } from "#lib/modules/filesystem/index.js";
import type { SummarizedFileInfo } from "#lib/features/filesystem/index.js";
import { formatDateTime } from "#lib/utils/index.js";
interface Props {
@@ -1,12 +1,7 @@
<script lang="ts">
import { BottomDiv, BottomSheet, Button, FullscreenDiv } from "#lib/components/atoms/index.js";
import { SubCategories } from "#lib/components/molecules/index.js";
import {
getCategoryInfo,
type LocalCategoryInfo,
type MaybeCategoryInfo,
} from "#lib/modules/filesystem/index.js";
import { HybridPromise } from "#lib/utils/index.js";
import { useCategory, type LocalCategoryInfo } from "#lib/features/filesystem/index.js";
interface Props {
isOpen: boolean;
@@ -16,11 +11,13 @@
let { isOpen = $bindable(), mode, onSelectCategoryClick }: Props = $props();
let categoryInfo: MaybeCategoryInfo | undefined = $state();
let categoryId: CategoryId = $state("root");
const category = useCategory(() => categoryId);
let categoryInfo = $derived(category.current);
$effect(() => {
$effect.pre(() => {
if (isOpen) {
HybridPromise.resolve(getCategoryInfo("root")).then((result) => (categoryInfo = result));
categoryId = "root";
}
});
</script>
@@ -31,8 +28,7 @@
<SubCategories
class="py-4"
info={categoryInfo}
onSubCategoryClick={({ id }) =>
HybridPromise.resolve(getCategoryInfo(id)).then((result) => (categoryInfo = result))}
onSubCategoryClick={({ id }) => (categoryId = id)}
/>
{#if categoryInfo.id !== "root"}
<BottomDiv>
-42
View File
@@ -1,42 +0,0 @@
import {
getDirectoryInfo,
getFileInfo,
type LocalDirectoryInfo,
type FileInfo,
type LocalCategoryInfo,
} from "#lib/modules/filesystem/index.js";
import { HybridPromise } from "#lib/utils/index.js";
import { trpc } from "#trpc/client.js";
export interface SearchFilter {
ancestorId: DirectoryId;
inFavorites: boolean;
categories: { info: LocalCategoryInfo; type: "include" | "exclude" }[];
}
export interface SearchResult {
directories: LocalDirectoryInfo[];
files: FileInfo[];
}
export const requestSearch = async (filter: SearchFilter) => {
const { directories: directoriesRaw, files: filesRaw } = await trpc.search.search.query({
ancestor: filter.ancestorId,
inFavorites: filter.inFavorites,
includeCategories: filter.categories
.filter(({ type }) => type === "include")
.map(({ info }) => info.id),
excludeCategories: filter.categories
.filter(({ type }) => type === "exclude")
.map(({ info }) => info.id),
});
const [directories, files] = await HybridPromise.all([
HybridPromise.all(
directoriesRaw.map((directory) =>
getDirectoryInfo(directory.id, { serverResponse: directory }),
),
),
HybridPromise.all(filesRaw.map((file) => getFileInfo(file.id, { serverResponse: file }))),
]);
return { directories, files } as SearchResult;
};
+1 -1
View File
@@ -7,7 +7,7 @@
getFileCacheIndex,
deleteFileCache as doDeleteFileCache,
} from "#lib/client/cache/index.js";
import { bulkGetFileInfo, type MaybeFileInfo } from "#lib/modules/filesystem/index.js";
import { bulkGetFileInfo, type MaybeFileInfo } from "#lib/features/filesystem/index.js";
import { formatFileSize } from "#lib/utils/index.js";
import File from "./File.svelte";
+1 -1
View File
@@ -1,6 +1,6 @@
<script lang="ts">
import type { FileCacheIndex } from "#lib/client/storage/indexedDB/index.js";
import type { MaybeFileInfo } from "#lib/modules/filesystem/index.js";
import type { MaybeFileInfo } from "#lib/features/filesystem/index.js";
import { formatDate, formatFileSize } from "#lib/utils/index.js";
import IconDraft from "~icons/material-symbols/draft";
@@ -4,7 +4,7 @@
import { BottomDiv, Button, FullscreenDiv } from "#lib/components/atoms/index.js";
import { IconEntryButton, TopBar } from "#lib/components/molecules/index.js";
import { deleteAllFileThumbnailCaches } from "#lib/client/cache/index.js";
import type { MaybeFileInfo } from "#lib/modules/filesystem/index.js";
import type { MaybeFileInfo } from "#lib/features/filesystem/index.js";
import { sortEntries } from "#lib/utils/index.js";
import File from "./File.svelte";
import {
@@ -11,7 +11,7 @@
<script lang="ts">
import { ActionEntryButton } from "#lib/components/atoms/index.js";
import { DirectoryEntryLabel } from "#lib/components/molecules/index.js";
import type { FileInfo } from "#lib/modules/filesystem/index.js";
import type { FileInfo } from "#lib/features/filesystem/index.js";
import { formatDateTime } from "#lib/utils/index.js";
import type { GenerationStatus } from "./service";
@@ -1,10 +1,13 @@
import { limitFunction } from "p-limit";
import { SvelteMap } from "svelte/reactivity";
import { storeFileThumbnailCache } from "#lib/client/cache/index.js";
import { generateThumbnail } from "#lib/client/media/thumbnail.js";
import { getFileInfo, type FileInfo } from "#lib/modules/filesystem/index.js";
import { requestFileDownload, requestFileThumbnailUpload } from "#lib/services/file.js";
import { HybridPromise, Scheduler } from "#lib/utils/index.js";
import {
getFileContent,
getFileInfo,
uploadFileThumbnail,
type FileInfo,
} from "#lib/features/filesystem/index.js";
import { Scheduler } from "#lib/utils/index.js";
import type { RouterOutputs } from "#trpc/router.server.js";
export type GenerationStatus =
@@ -28,34 +31,26 @@ export const clearThumbnailGenerationStatuses = () => {
export const requestMissingThumbnailFiles = async (
filesRaw: RouterOutputs["file"]["listWithoutThumbnail"],
) => {
const files = await HybridPromise.all(
const files = await Promise.all(
filesRaw.map((file) => getFileInfo(file.id, { serverResponse: file })),
);
return files;
};
const requestThumbnailUpload = limitFunction(
const generateAndUploadThumbnail = limitFunction(
async (fileInfo: FileInfo, fileBuffer: ArrayBuffer) => {
statuses.set(fileInfo.id, "generating");
const thumbnail = await generateThumbnail(
new Blob([fileBuffer], { type: fileInfo.contentType }),
);
if (!thumbnail) return false;
if (!thumbnail) {
throw new Error("Failed to generate thumbnail");
}
statuses.set(fileInfo.id, "uploading");
const res = await requestFileThumbnailUpload(
fileInfo.id,
thumbnail,
fileInfo.dataKey?.key!,
fileInfo.dataKey?.version!,
);
if (!res) return false;
await uploadFileThumbnail(fileInfo.id, thumbnail, fileInfo.dataKey!);
statuses.set(fileInfo.id, "uploaded");
void thumbnail.arrayBuffer().then((buffer) => storeFileThumbnailCache(fileInfo.id, buffer));
return true;
},
{ concurrency: 4 },
);
@@ -74,17 +69,13 @@ export const requestThumbnailGeneration = async (fileInfo: FileInfo) => {
await scheduler.schedule(
async () => {
statuses.set(fileInfo.id, "generation-pending");
file = await requestFileDownload(fileInfo.id, fileInfo.dataKey?.key!, fileInfo.isLegacy!);
file = await getFileContent(fileInfo.id, fileInfo.dataKey!.key, !!fileInfo.isLegacy);
return file.byteLength;
},
async () => {
if (!(await requestThumbnailUpload(fileInfo, file!))) {
statuses.set(fileInfo.id, "error");
}
},
() => generateAndUploadThumbnail(fileInfo, file!),
);
} catch (e) {
console.error(e);
statuses.set(fileInfo.id, "error");
throw e;
}
};
+37 -61
View File
@@ -3,36 +3,36 @@
import { CheckBox, RowVirtualizer } from "#lib/components/atoms/index.js";
import { SubCategories, TopBar } from "#lib/components/molecules/index.js";
import { CategoryCreateModal } from "#lib/components/organisms/index.js";
import { updateCategoryInfo } from "#lib/client/storage/indexedDB/index.js";
import { getCategoryInfo, type MaybeCategoryInfo } from "#lib/modules/filesystem/index.js";
import { HybridPromise, sortEntries } from "#lib/utils/index.js";
import { tryAction } from "#lib/client/toast.svelte.js";
import {
createCategory,
deleteCategory,
removeFileFromCategory,
renameCategory,
setCategoryFileRecursive,
useCategory,
type LocalCategoryInfo,
} from "#lib/features/filesystem/index.js";
import { sortEntries } from "#lib/utils/index.js";
import CategoryDeleteModal from "./CategoryDeleteModal.svelte";
import CategoryMenuBottomSheet from "./CategoryMenuBottomSheet.svelte";
import CategoryRenameModal from "./CategoryRenameModal.svelte";
import File from "./File.svelte";
import {
createContext,
requestCategoryCreation,
requestFileRemovalFromCategory,
requestCategoryRename,
requestCategoryDeletion,
} from "./service.svelte";
import { createContext } from "./service.svelte";
import IconMoreVert from "~icons/material-symbols/more-vert";
let { data } = $props();
let context = createContext();
let info: MaybeCategoryInfo | undefined = $state();
const category = useCategory(() => data.id);
let info = $derived(category.current);
let isCategoryCreateModalOpen = $state(false);
let isCategoryMenuBottomSheetOpen = $state(false);
let isCategoryRenameModalOpen = $state(false);
let isCategoryDeleteModalOpen = $state(false);
let lastCategoryId: CategoryId | undefined = $state();
let lastIsFileRecursive: boolean | undefined = $state();
let files = $derived(
sortEntries(
info?.files
@@ -40,27 +40,6 @@
.filter(({ details }) => info?.isFileRecursive || !details.isRecursive) ?? [],
),
);
$effect(() => {
if (!info || info.id === "root" || info.isFileRecursive === undefined) return;
if (lastCategoryId !== info.id) {
lastCategoryId = info.id;
lastIsFileRecursive = info.isFileRecursive;
return;
}
if (lastIsFileRecursive === info.isFileRecursive) return;
lastIsFileRecursive = info.isFileRecursive;
void updateCategoryInfo(info.id, { isFileRecursive: info.isFileRecursive });
});
$effect(() => {
HybridPromise.resolve(getCategoryInfo(data.id)).then((result) => {
if (data.id === result.id) {
info = result;
}
});
});
</script>
<svelte:head>
@@ -93,7 +72,12 @@
<div class="space-y-2 bg-white p-4">
<div class="flex items-center justify-between">
<p class="text-lg font-bold text-gray-800">파일</p>
<CheckBox bind:checked={info.isFileRecursive}>
<CheckBox
bind:checked={
() => info.isFileRecursive,
(value) => setCategoryFileRecursive(info as LocalCategoryInfo, value)
}
>
<p class="font-medium">하위 카테고리의 파일</p>
</CheckBox>
</div>
@@ -109,10 +93,11 @@
info={details}
onclick={({ id }) => goto(`/file/${id}?from=category`)}
onRemoveClick={!details.isRecursive
? async ({ id }) => {
await requestFileRemovalFromCategory(id, data.id as number);
void getCategoryInfo(data.id); // TODO: FIXME
}
? ({ id }) =>
tryAction(
() => removeFileFromCategory(id, data.id as number),
"카테고리에서 파일을 제거하지 못했어요.",
)
: undefined}
/>
{/snippet}
@@ -128,13 +113,8 @@
<CategoryCreateModal
bind:isOpen={isCategoryCreateModalOpen}
onCreateClick={async (name: string) => {
if (await requestCategoryCreation(name, data.id)) {
void getCategoryInfo(data.id); // TODO: FIXME
return true;
}
return false;
}}
onCreateClick={(name: string) =>
tryAction(() => createCategory(data.id, name), "카테고리를 만들지 못했어요.")}
/>
<CategoryMenuBottomSheet
@@ -150,21 +130,17 @@
/>
<CategoryRenameModal
bind:isOpen={isCategoryRenameModalOpen}
onRenameClick={async (newName: string) => {
if (await requestCategoryRename(context.selectedCategory!, newName)) {
void getCategoryInfo(data.id); // TODO: FIXME
return true;
}
return false;
}}
onRenameClick={(newName: string) =>
tryAction(
() => renameCategory({ ...context.selectedCategory!, parentId: data.id }, newName),
"이름을 바꾸지 못했어요.",
)}
/>
<CategoryDeleteModal
bind:isOpen={isCategoryDeleteModalOpen}
onDeleteClick={async () => {
if (await requestCategoryDeletion(context.selectedCategory!)) {
void getCategoryInfo(data.id); // TODO: FIXME
return true;
}
return false;
}}
onDeleteClick={() =>
tryAction(
() => deleteCategory({ ...context.selectedCategory!, parentId: data.id }),
"카테고리를 삭제하지 못했어요.",
)}
/>
@@ -2,7 +2,7 @@
import { ActionEntryButton } from "#lib/components/atoms/index.js";
import { DirectoryEntryLabel } from "#lib/components/molecules/index.js";
import { getFileThumbnail } from "#lib/client/cache/index.js";
import type { CategoryFileInfo } from "#lib/modules/filesystem/index.js";
import type { CategoryFileInfo } from "#lib/features/filesystem/index.js";
import { formatDateTime } from "#lib/utils/index.js";
import type { SelectedFile } from "./service.svelte";
@@ -1,9 +1,5 @@
import { getContext, setContext } from "svelte";
import type { SelectedCategory } from "#lib/components/molecules/index.js";
import { encryptString } from "#lib/shared/crypto/index.js";
import { trpc } from "#trpc/client.js";
export { requestCategoryCreation, requestFileRemovalFromCategory } from "#lib/services/category.js";
export interface SelectedFile {
id: number;
@@ -20,35 +16,3 @@ export const createContext = () => {
export const useContext = () => {
return getContext<ReturnType<typeof createContext>>("context");
};
export const requestCategoryRename = async (category: SelectedCategory, newName: string) => {
if (!category.dataKey) {
// TODO: Error Handling
return false;
}
const newNameEncrypted = await encryptString(newName, category.dataKey.key);
try {
await trpc.category.rename.mutate({
id: category.id,
dekVersion: category.dataKey.version,
name: newNameEncrypted.ciphertext,
nameIv: newNameEncrypted.iv,
});
return true;
} catch {
// TODO: Error Handling
return false;
}
};
export const requestCategoryDeletion = async (category: SelectedCategory) => {
try {
await trpc.category.delete.mutate({ id: category.id });
return true;
} catch {
// TODO: Error Handling
return false;
}
};
+29 -60
View File
@@ -3,8 +3,8 @@
import { page } from "$app/state";
import { FloatingButton } from "#lib/components/atoms/index.js";
import { TopBar } from "#lib/components/molecules/index.js";
import { getDirectoryInfo, type MaybeDirectoryInfo } from "#lib/modules/filesystem/index.js";
import { HybridPromise } from "#lib/utils/index.js";
import { tryAction } from "#lib/client/toast.svelte.js";
import { createDirectory, uploadFile, useDirectory } from "#lib/features/filesystem/index.js";
import DirectoryCreateModal from "./DirectoryCreateModal.svelte";
import DirectoryEntries from "./DirectoryEntries";
import DownloadStatusCard from "./DownloadStatusCard.svelte";
@@ -14,14 +14,7 @@
import EntryMenuBottomSheet from "./EntryMenuBottomSheet.svelte";
import EntryRenameModal from "./EntryRenameModal.svelte";
import UploadStatusCard from "./UploadStatusCard.svelte";
import {
createContext,
requestDirectoryCreation,
requestFileUpload,
requestEntryRename,
requestEntryDeletion,
requestFavoriteToggle,
} from "./service.svelte";
import { createContext, deleteEntry, renameEntry, toggleEntryFavorite } from "./service.svelte";
import IconSearch from "~icons/material-symbols/search";
import IconAdd from "~icons/material-symbols/add";
@@ -29,7 +22,8 @@
let { data } = $props();
let context = createContext();
let info: MaybeDirectoryInfo | undefined = $state();
const directory = useDirectory(() => data.id);
let info = $derived(directory.current);
let fileInput: HTMLInputElement | undefined = $state();
let duplicatedFile: File | undefined = $state();
let resolveForDuplicateFileModal: ((res: boolean) => void) | undefined = $state();
@@ -56,46 +50,33 @@
await goto(`/search${query ? `?${query}` : ""}`);
};
const uploadFile = () => {
const uploadFiles = () => {
const files = fileInput?.files;
if (!files || files.length === 0) return;
for (const file of files) {
requestFileUpload(file, data.id, () => {
return new Promise((resolve) => {
duplicatedFile = file;
resolveForDuplicateFileModal = resolve;
isDuplicateFileModalOpen = true;
});
})
.then((res) => {
if (!res) return;
// TODO: FIXME
void getDirectoryInfo(data.id);
})
.catch((e: Error) => {
// TODO: FIXME
console.error(e);
});
void tryAction(
() =>
uploadFile(file, data.id, () => {
return new Promise((resolve) => {
duplicatedFile = file;
resolveForDuplicateFileModal = resolve;
isDuplicateFileModalOpen = true;
});
}),
`'${file.name}' 파일을 업로드하지 못했어요.`,
);
}
fileInput!.value = "";
};
$effect(() => {
HybridPromise.resolve(getDirectoryInfo(data.id)).then((result) => {
if (data.id === result.id) {
info = result;
}
});
});
</script>
<svelte:head>
<title>내 파일</title>
</svelte:head>
<input bind:this={fileInput} onchange={uploadFile} type="file" multiple class="hidden" />
<input bind:this={fileInput} onchange={uploadFiles} type="file" multiple class="hidden" />
<div class="flex h-full flex-col">
<TopBar title={info?.name ?? "내 파일"} {showBackButton} class="shrink-0">
@@ -150,13 +131,8 @@
/>
<DirectoryCreateModal
bind:isOpen={isDirectoryCreateModalOpen}
onCreateClick={async (name) => {
if (await requestDirectoryCreation(name, data.id)) {
void getDirectoryInfo(data.id); // TODO: FIXME
return true;
}
return false;
}}
onCreateClick={(name) =>
tryAction(() => createDirectory(data.id, name), "폴더를 만들지 못했어요.")}
/>
<DuplicateFileModal
bind:isOpen={isDuplicateFileModalOpen}
@@ -182,29 +158,22 @@
isEntryDeleteModalOpen = true;
}}
onFavoriteClick={async () => {
if (await requestFavoriteToggle(context.selectedEntry!)) {
if (
await tryAction(
() => toggleEntryFavorite(context.selectedEntry!),
"즐겨찾기를 변경하지 못했어요.",
)
) {
isEntryMenuBottomSheetOpen = false;
void getDirectoryInfo(data.id); // TODO: FIXME
}
}}
/>
<EntryRenameModal
bind:isOpen={isEntryRenameModalOpen}
onRenameClick={async (newName: string) => {
if (await requestEntryRename(context.selectedEntry!, newName)) {
void getDirectoryInfo(data.id); // TODO: FIXME
return true;
}
return false;
}}
onRenameClick={(newName: string) =>
tryAction(() => renameEntry(context.selectedEntry!, newName), "이름을 바꾸지 못했어요.")}
/>
<EntryDeleteModal
bind:isOpen={isEntryDeleteModalOpen}
onDeleteClick={async () => {
if (await requestEntryDeletion(context.selectedEntry!)) {
void getDirectoryInfo(data.id); // TODO: FIXME
return true;
}
return false;
}}
onDeleteClick={() => tryAction(() => deleteEntry(context.selectedEntry!), "삭제하지 못했어요.")}
/>
@@ -2,7 +2,7 @@
import { ActionEntryButton, RowVirtualizer } from "#lib/components/atoms/index.js";
import { DirectoryEntryLabel } from "#lib/components/molecules/index.js";
import { getUploadingFiles, type LiveFileUploadState } from "#lib/client/transfer/index.js";
import type { DirectoryInfo } from "#lib/modules/filesystem/index.js";
import type { DirectoryInfo } from "#lib/features/filesystem/index.js";
import { sortEntries } from "#lib/utils/index.js";
import File from "./File.svelte";
import SubDirectory from "./SubDirectory.svelte";
@@ -2,7 +2,7 @@
import { ActionEntryButton } from "#lib/components/atoms/index.js";
import { DirectoryEntryLabel } from "#lib/components/molecules/index.js";
import { getFileThumbnail } from "#lib/client/cache/index.js";
import type { SummarizedFileInfo } from "#lib/modules/filesystem/index.js";
import type { SummarizedFileInfo } from "#lib/features/filesystem/index.js";
import { formatDateTime } from "#lib/utils/index.js";
import type { SelectedEntry } from "../service.svelte";
@@ -22,6 +22,7 @@
callback({
type: "file",
id: info.id,
parentId: info.parentId,
dataKey: info.dataKey,
name: info.name,
isFavorite: info.isFavorite,
@@ -1,7 +1,7 @@
<script lang="ts">
import { ActionEntryButton } from "#lib/components/atoms/index.js";
import { DirectoryEntryLabel } from "#lib/components/molecules/index.js";
import type { SubDirectoryInfo } from "#lib/modules/filesystem/index.js";
import type { SubDirectoryInfo } from "#lib/features/filesystem/index.js";
import type { SelectedEntry } from "../service.svelte";
import IconMoreVert from "~icons/material-symbols/more-vert";
@@ -18,6 +18,7 @@
callback({
type: "directory",
id: info.id,
parentId: info.parentId,
dataKey: info.dataKey,
name: info.name,
isFavorite: info.isFavorite,
@@ -1,19 +1,18 @@
import { getContext, setContext } from "svelte";
import {
storeFileCache,
deleteFileCache,
storeFileThumbnailCache,
deleteFileThumbnailCache,
} from "#lib/client/cache/index.js";
import { keyring } from "#lib/client/keys/index.js";
import { uploadFile } from "#lib/client/transfer/index.js";
import type { DataKey } from "#lib/modules/filesystem/index.js";
import { generateDataKey, wrapDataKey, encryptString } from "#lib/shared/crypto/index.js";
import { trpc } from "#trpc/client.js";
deleteDirectory,
deleteFile,
renameDirectory,
renameFile,
setDirectoryFavorite,
setFileFavorite,
type DataKey,
} from "#lib/features/filesystem/index.js";
export interface SelectedEntry {
type: "directory" | "file";
id: number;
parentId: DirectoryId;
dataKey: DataKey | undefined;
name: string;
isFavorite: boolean;
@@ -30,115 +29,26 @@ export const useContext = () => {
return getContext<ReturnType<typeof createContext>>("context");
};
export const requestDirectoryCreation = async (name: string, parentId: "root" | number) => {
const masterKey = keyring.activeMasterKey;
const { dataKey, dataKeyVersion } = await generateDataKey();
const nameEncrypted = await encryptString(name, dataKey);
try {
await trpc.directory.create.mutate({
parent: parentId,
mekVersion: masterKey.version,
dek: await wrapDataKey(dataKey, masterKey.key),
dekVersion: dataKeyVersion,
name: nameEncrypted.ciphertext,
nameIv: nameEncrypted.iv,
});
return true;
} catch {
// TODO: Error Handling
return false;
export const renameEntry = async (entry: SelectedEntry, newName: string) => {
if (entry.type === "directory") {
await renameDirectory(entry, newName);
} else {
await renameFile(entry, newName);
}
};
export const requestFileUpload = async (
file: File,
parentId: "root" | number,
onDuplicate: () => Promise<boolean>,
) => {
const res = await uploadFile(file, parentId, onDuplicate);
if (!res) return false;
if (res.fileBuffer) {
storeFileCache(res.fileId, res.fileBuffer); // Intended
}
if (res.thumbnailBuffer) {
storeFileThumbnailCache(res.fileId, res.thumbnailBuffer); // Intended
}
return true;
};
export const requestEntryRename = async (entry: SelectedEntry, newName: string) => {
if (!entry.dataKey) {
// TODO: Error Handling
return false;
}
const newNameEncrypted = await encryptString(newName, entry.dataKey.key);
try {
if (entry.type === "directory") {
await trpc.directory.rename.mutate({
id: entry.id,
dekVersion: entry.dataKey.version,
name: newNameEncrypted.ciphertext,
nameIv: newNameEncrypted.iv,
});
} else {
await trpc.file.rename.mutate({
id: entry.id,
dekVersion: entry.dataKey.version,
name: newNameEncrypted.ciphertext,
nameIv: newNameEncrypted.iv,
});
}
return true;
} catch {
// TODO: Error Handling
return false;
export const deleteEntry = async (entry: SelectedEntry) => {
if (entry.type === "directory") {
await deleteDirectory(entry);
} else {
await deleteFile(entry);
}
};
export const requestEntryDeletion = async (entry: SelectedEntry) => {
try {
if (entry.type === "directory") {
const { deletedFiles } = await trpc.directory.delete.mutate({ id: entry.id });
await Promise.all(
deletedFiles.flatMap((fileId) => [
deleteFileCache(fileId),
deleteFileThumbnailCache(fileId),
]),
);
} else {
await trpc.file.delete.mutate({ id: entry.id });
await Promise.all([deleteFileCache(entry.id), deleteFileThumbnailCache(entry.id)]);
}
return true;
} catch {
// TODO: Error Handling
return false;
}
};
export const requestFavoriteToggle = async (entry: SelectedEntry) => {
try {
if (entry.type === "directory") {
if (entry.isFavorite) {
await trpc.favorites.removeDirectory.mutate({ id: entry.id });
} else {
await trpc.favorites.addDirectory.mutate({ id: entry.id });
}
} else {
if (entry.isFavorite) {
await trpc.favorites.removeFile.mutate({ id: entry.id });
} else {
await trpc.favorites.addFile.mutate({ id: entry.id });
}
}
return true;
} catch {
// TODO: Error Handling
return false;
export const toggleEntryFavorite = async (entry: SelectedEntry) => {
if (entry.type === "directory") {
await setDirectoryFavorite(entry, !entry.isFavorite);
} else {
await setFileFavorite(entry, !entry.isFavorite);
}
};
+14 -6
View File
@@ -5,22 +5,30 @@
import { TopBar } from "#lib/components/molecules/index.js";
import Directory from "./Directory.svelte";
import File from "./File.svelte";
import { requestFavoriteEntries, requestRemoveFavorite, type FavoriteEntry } from "./service";
import { showErrorToast, tryAction } from "#lib/client/toast.svelte.js";
import {
getFavoriteEntries,
removeFavorite,
type FavoriteEntry,
} from "#lib/features/favorites/index.js";
import IconSearch from "~icons/material-symbols/search";
let { data } = $props();
let entries: FavoriteEntry[] = $state([]);
let isLoading = $state(true);
onMount(async () => {
entries = await requestFavoriteEntries(data.favorites);
isLoading = false;
try {
entries = await getFavoriteEntries();
} catch (e) {
showErrorToast(e, "즐겨찾기를 불러오지 못했어요.");
} finally {
isLoading = false;
}
});
const handleRemove = async (entry: FavoriteEntry) => {
if (await requestRemoveFavorite(entry.type, entry.details.id)) {
if (await tryAction(() => removeFavorite(entry), "즐겨찾기를 해제하지 못했어요.")) {
entries = entries.filter(
(e) => !(e.type === entry.type && e.details.id === entry.details.id),
);
-8
View File
@@ -1,8 +0,0 @@
import { trpc } from "#trpc/client.js";
import type { PageLoad } from "./$types";
export const load: PageLoad = async ({ parent }) => {
await parent(); // Wait for the key guard in the root layout
const favorites = await trpc.favorites.get.query();
return { favorites };
};
+1 -1
View File
@@ -1,7 +1,7 @@
<script lang="ts">
import { ActionEntryButton } from "#lib/components/atoms/index.js";
import { DirectoryEntryLabel } from "#lib/components/molecules/index.js";
import type { SubDirectoryInfo } from "#lib/modules/filesystem/index.js";
import type { SubDirectoryInfo } from "#lib/features/filesystem/index.js";
import IconClose from "~icons/material-symbols/close";
+1 -1
View File
@@ -2,7 +2,7 @@
import { ActionEntryButton } from "#lib/components/atoms/index.js";
import { DirectoryEntryLabel } from "#lib/components/molecules/index.js";
import { getFileThumbnail } from "#lib/client/cache/index.js";
import type { SummarizedFileInfo } from "#lib/modules/filesystem/index.js";
import type { SummarizedFileInfo } from "#lib/features/filesystem/index.js";
import { formatDateTime } from "#lib/utils/index.js";
import IconClose from "~icons/material-symbols/close";
-61
View File
@@ -1,61 +0,0 @@
import {
getDirectoryInfo,
getFileInfo,
type SummarizedFileInfo,
type SubDirectoryInfo,
type LocalDirectoryInfo,
} from "#lib/modules/filesystem/index.js";
import { HybridPromise, sortEntries } from "#lib/utils/index.js";
import { trpc } from "#trpc/client.js";
import type { RouterOutputs } from "#trpc/router.server.js";
export type FavoriteEntry =
| { type: "directory"; name: string; details: SubDirectoryInfo }
| { type: "file"; name: string; details: SummarizedFileInfo };
export const requestFavoriteEntries = async (favorites: RouterOutputs["favorites"]["get"]) => {
const [directories, files] = await HybridPromise.all([
HybridPromise.all(
favorites.directories.map((directory) =>
getDirectoryInfo(directory.id, {
serverResponse: { ...directory, isFavorite: true },
}),
),
),
HybridPromise.all(
favorites.files.map((file) =>
getFileInfo(file.id, { serverResponse: { ...file, isFavorite: true } }),
),
),
]);
return [
...sortEntries(
directories.map((directory): FavoriteEntry => ({
type: "directory",
name: directory.name!,
details: directory as LocalDirectoryInfo,
})),
),
...sortEntries(
files.map((file): FavoriteEntry => ({
type: "file",
name: file.name!,
details: file as SummarizedFileInfo,
})),
),
];
};
export const requestRemoveFavorite = async (type: "file" | "directory", id: number) => {
try {
if (type === "directory") {
await trpc.favorites.removeDirectory.mutate({ id });
} else {
await trpc.favorites.removeFile.mutate({ id });
}
return true;
} catch {
// TODO: Error Handling
return false;
}
};
+1 -1
View File
@@ -2,7 +2,7 @@
import { onMount } from "svelte";
import { goto } from "$app/navigation";
import { EntryButton, FileThumbnailButton } from "#lib/components/atoms/index.js";
import { type SummarizedFileInfo } from "#lib/modules/filesystem/index.js";
import { type SummarizedFileInfo } from "#lib/features/filesystem/index.js";
import { requestFreshMediaFilesRetrieval } from "./service";
let mediaFiles: SummarizedFileInfo[] = $state([]);
+3 -2
View File
@@ -1,7 +1,8 @@
<script lang="ts">
import { goto } from "$app/navigation";
import MenuEntryButton from "./MenuEntryButton.svelte";
import { requestLogout } from "./service";
import { tryAction } from "#lib/client/toast.svelte.js";
import { logout as requestLogout } from "#lib/features/auth/index.js";
import IconStorage from "~icons/material-symbols/storage";
import IconImage from "~icons/material-symbols/image";
@@ -11,7 +12,7 @@
let { data } = $props();
const logout = async () => {
if (await requestLogout()) {
if (await tryAction(requestLogout, "로그아웃하지 못했어요.")) {
await goto("/auth/login");
}
};
-1
View File
@@ -1 +0,0 @@
export { requestLogout } from "#lib/services/auth.js";
+3
View File
@@ -1,5 +1,6 @@
<script lang="ts">
import { getDownloadingFiles, getUploadingFiles } from "#lib/client/transfer/index.js";
import { Toasts } from "#lib/components/atoms/index.js";
import "../app.css";
let { children } = $props();
@@ -14,3 +15,5 @@
<svelte:window onbeforeunload={protectFileUploadAndDownload} />
{@render children()}
<Toasts />