컴포넌트를 도메인과 무관한 UI 컴포넌트와 도메인 컴포넌트로 재구성하고 중복된 파일 목록 컴포넌트 통합 및 모듈 간 의존성 방향을 ESLint로 강제

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
staticandClaude Opus 5.5 committed 2026-10-12 00:30:10 +09:00
1 parent 4e82c5e556
commit 877cf9c42e
94 files changed
+398 -518

No files matched your search

+25
View File
@@ -13,6 +13,20 @@ const svelteConfig = (await loadConfig("./", { traverse: false }))?.config;
const gitignorePath = fileURLToPath(new URL("./.gitignore", import.meta.url));
// Restricts the direction of dependencies between modules
const defineImportBoundaries = (boundaries) =>
Object.entries(boundaries).map(([files, regex]) => ({
files: [files],
rules: {
"no-restricted-imports": [
"error",
{
patterns: [{ regex, message: "Violates the dependency direction between modules." }],
},
],
},
}));
export default defineConfig(
includeIgnoreFile(gitignorePath),
js.configs.recommended,
@@ -37,6 +51,17 @@ export default defineConfig(
"no-undef": "off",
},
},
...defineImportBoundaries({
// Pure code shared by the server, the client, and the service worker
"src/lib/shared/**": "^(#lib/(?!shared/)|#trpc/|#workers/|\\$app/|svelte($|/))",
"src/lib/server/**": "^(#lib/(client|features|ui)/|#trpc/)",
// Infrastructure of the browser
"src/lib/client/**": "^#lib/(server|features|ui)/",
// Domain-agnostic components
"src/lib/ui/**": "^(#lib/(server|client|features)/|#trpc/)",
"src/lib/features/**": "^#lib/server/",
"src/service-worker/**": "^(#lib/(?!shared/|client/storage/opfs\\.js$)|#trpc/|#workers/)",
}),
{
files: ["**/*.svelte", "**/*.svelte.ts", "**/*.svelte.js"],
languageOptions: {
+5 -2
View File
@@ -1,7 +1,6 @@
/* eslint-disable svelte/prefer-svelte-reactivity -- Lookup tables of reactive entries */
import { LRUCache } from "lru-cache";
import { deleteDirectory, deleteFile, readFile, writeFile } from "#lib/client/storage/opfs.js";
import type { SummarizedFileInfo } from "#lib/features/filesystem/index.js";
import { decryptChunk, encodeToBase64 } from "#lib/shared/crypto/index.js";
export interface Thumbnail {
@@ -33,7 +32,11 @@ const fetchFromServer = async (fileId: number, dataKey: CryptoKey) => {
return getThumbnailUrl(thumbnailBuffer);
};
export const getFileThumbnail = (file: SummarizedFileInfo) => {
export const getFileThumbnail = (file: {
id: number;
contentType: string;
dataKey?: { key: CryptoKey };
}) => {
if (!(file.contentType.startsWith("image/") || file.contentType.startsWith("video/"))) {
return undefined;
}
@@ -1,6 +0,0 @@
export { default as ActionEntryButton } from "./ActionEntryButton.svelte";
export { default as Button } from "./Button.svelte";
export { default as EntryButton } from "./EntryButton.svelte";
export { default as FileThumbnailButton } from "./FileThumbnailButton.svelte";
export { default as FloatingButton } from "./FloatingButton.svelte";
export { default as TextButton } from "./TextButton.svelte";
-3
View File
@@ -1,3 +0,0 @@
export { default as AdaptiveDiv } from "./AdaptiveDiv.svelte";
export { default as BottomDiv } from "./BottomDiv.svelte";
export { default as FullscreenDiv } from "./FullscreenDiv.svelte";
-8
View File
@@ -1,8 +0,0 @@
export { default as BottomSheet } from "./BottomSheet.svelte";
export * from "./buttons";
export { default as Chip } from "./Chip.svelte";
export * from "./divs";
export * from "./inputs";
export { default as Modal } from "./Modal.svelte";
export { default as RowVirtualizer } from "./RowVirtualizer.svelte";
export { default as Toasts } from "./Toasts.svelte";
-2
View File
@@ -1,2 +0,0 @@
export { default as CheckBox } from "./CheckBox.svelte";
export { default as TextInput } from "./TextInput.svelte";
-9
View File
@@ -1,9 +0,0 @@
export * from "./ActionModal.svelte";
export { default as ActionModal } from "./ActionModal.svelte";
export * from "./Categories.svelte";
export { default as Categories } from "./Categories.svelte";
export { default as IconEntryButton } from "./IconEntryButton.svelte";
export * from "./labels";
export { default as SubCategories } from "./SubCategories.svelte";
export { default as TitledDiv } from "./TitledDiv.svelte";
export { default as TopBar } from "./TopBar.svelte";
@@ -1,4 +0,0 @@
export { default as CategoryLabel } from "./CategoryLabel.svelte";
export { default as DirectoryEntryLabel } from "./DirectoryEntryLabel.svelte";
export { default as IconLabel } from "./IconLabel.svelte";
export { default as TitleLabel } from "./TitleLabel.svelte";
-1
View File
@@ -1 +0,0 @@
export * from "./modals";
@@ -1,4 +0,0 @@
export { default as CategoryCreateModal } from "./CategoryCreateModal.svelte";
export { default as ForceLoginModal } from "./ForceLoginModal.svelte";
export { default as RenameModal } from "./RenameModal.svelte";
export { default as TextInputModal } from "./TextInputModal.svelte";
@@ -1,5 +1,5 @@
<script lang="ts">
import { ActionModal } from "#lib/components/molecules/index.js";
import { ActionModal } from "#lib/ui/index.js";
interface Props {
isOpen: boolean;
@@ -0,0 +1 @@
export { default as ForceLoginModal } from "./ForceLoginModal.svelte";
@@ -1,25 +1,6 @@
import { showToast } from "#lib/ui/index.js";
import { isTRPCClientError } from "#trpc/client.js";
export interface Toast {
id: number;
message: string;
}
const TOAST_DURATION = 3000;
let toasts: Toast[] = $state([]);
let nextId = 0;
export const getToasts = () => toasts;
export const showToast = (message: string) => {
const toast = { id: nextId++, message };
toasts.push(toast);
setTimeout(() => {
toasts = toasts.filter(({ id }) => id !== toast.id);
}, TOAST_DURATION);
};
const getErrorMessage = (e: unknown, fallbackMessage: string) => {
if (isTRPCClientError(e)) {
switch (e.data?.code) {
@@ -1,5 +1,5 @@
<script module lang="ts">
import type { DataKey } from "#lib/features/filesystem/index.js";
import type { DataKey, SubCategoryInfo } from "#lib/features/filesystem/index.js";
export interface SelectedCategory {
id: number;
@@ -11,9 +11,7 @@
<script lang="ts">
import type { Component } from "svelte";
import type { SvelteHTMLElements } from "svelte/elements";
import { ActionEntryButton } from "#lib/components/atoms/index.js";
import { CategoryLabel } from "#lib/components/molecules/index.js";
import type { SubCategoryInfo } from "#lib/features/filesystem/index.js";
import { ActionEntryButton, CategoryLabel } from "#lib/ui/index.js";
import { sortEntries } from "#lib/utils/index.js";
interface Props {
@@ -1,5 +1,5 @@
<script lang="ts">
import { TextInputModal } from "#lib/components/organisms/index.js";
import { TextInputModal } from "#lib/ui/index.js";
interface Props {
isOpen: boolean;
@@ -0,0 +1,25 @@
<script lang="ts">
import type { Component } from "svelte";
import type { SvelteHTMLElements } from "svelte/elements";
import { ActionEntryButton, DirectoryEntryLabel } from "#lib/ui/index.js";
interface Props {
action?: {
icon: Component<SvelteHTMLElements["svg"]>;
onclick: () => void;
};
info: { name: string; isFavorite?: boolean };
onclick?: () => void;
}
let { action, info, onclick }: Props = $props();
</script>
<ActionEntryButton
class="h-14"
{onclick}
actionButtonIcon={action?.icon}
onActionButtonClick={action?.onclick}
>
<DirectoryEntryLabel type="directory" name={info.name} isFavorite={info.isFavorite} />
</ActionEntryButton>
@@ -0,0 +1,40 @@
<script lang="ts">
import type { Component } from "svelte";
import type { ClassValue, SvelteHTMLElements } from "svelte/elements";
import { getFileThumbnail } from "#lib/client/cache/index.js";
import { ActionEntryButton, DirectoryEntryLabel } from "#lib/ui/index.js";
import { formatDateTime } from "#lib/utils/index.js";
import type { SummarizedFileInfo } from "../types";
interface Props {
action?: {
icon: Component<SvelteHTMLElements["svg"]>;
onclick: () => void;
class?: ClassValue;
};
info: SummarizedFileInfo;
onclick?: () => void;
showThumbnail?: boolean;
subtext?: string;
}
let { action, info, onclick, showThumbnail = true, subtext }: Props = $props();
let thumbnail = $derived(showThumbnail ? getFileThumbnail(info) : undefined);
</script>
<ActionEntryButton
class="h-14"
{onclick}
actionButtonIcon={action?.icon}
actionButtonClass={action?.class}
onActionButtonClick={action?.onclick}
>
<DirectoryEntryLabel
type="file"
thumbnail={thumbnail?.url}
name={info.name}
subtext={subtext ?? formatDateTime(info.createdAt ?? info.lastModifiedAt)}
isFavorite={info.isFavorite}
/>
</ActionEntryButton>
@@ -1,12 +1,9 @@
<script lang="ts">
import type { Component } from "svelte";
import type { ClassValue, SvelteHTMLElements } from "svelte/elements";
import {
Categories,
IconEntryButton,
type SelectedCategory,
} from "#lib/components/molecules/index.js";
import { Categories, type SelectedCategory } from "#lib/features/filesystem/components/index.js";
import type { CategoryInfo } from "#lib/features/filesystem/index.js";
import { IconEntryButton } from "#lib/ui/index.js";
import IconAddCircle from "~icons/material-symbols/add-circle";
@@ -0,0 +1,7 @@
export * from "./Categories.svelte";
export { default as Categories } from "./Categories.svelte";
export { default as CategoryCreateModal } from "./CategoryCreateModal.svelte";
export { default as DirectoryEntry } from "./DirectoryEntry.svelte";
export { default as FileEntry } from "./FileEntry.svelte";
export { default as FileThumbnailButton } from "./FileThumbnailButton.svelte";
export { default as SubCategories } from "./SubCategories.svelte";
@@ -50,7 +50,7 @@
</div>
<style lang="postcss">
@reference "../../../../app.css";
@reference "../../../app.css";
#container:active:not(:has(#action-button:active)) {
@apply bg-gray-100;
@@ -1,7 +1,7 @@
<script lang="ts">
import type { Component } from "svelte";
import type { ClassValue, SvelteHTMLElements } from "svelte/elements";
import { AdaptiveDiv } from "#lib/components/atoms/index.js";
import { AdaptiveDiv } from "#lib/ui/index.js";
interface Props {
class: ClassValue;
@@ -1,8 +1,7 @@
<script lang="ts">
import type { Component, Snippet } from "svelte";
import type { ClassValue, SvelteHTMLElements } from "svelte/elements";
import { EntryButton } from "#lib/components/atoms/index.js";
import { IconLabel } from "#lib/components/molecules/index.js";
import { EntryButton, IconLabel } from "#lib/ui/index.js";
interface Props {
children: Snippet;
+28
View File
@@ -0,0 +1,28 @@
export { default as ActionEntryButton } from "./buttons/ActionEntryButton.svelte";
export { default as Button } from "./buttons/Button.svelte";
export { default as EntryButton } from "./buttons/EntryButton.svelte";
export { default as FloatingButton } from "./buttons/FloatingButton.svelte";
export { default as IconEntryButton } from "./buttons/IconEntryButton.svelte";
export { default as TextButton } from "./buttons/TextButton.svelte";
export { default as CheckBox } from "./inputs/CheckBox.svelte";
export { default as Chip } from "./inputs/Chip.svelte";
export { default as TextInput } from "./inputs/TextInput.svelte";
export { default as CategoryLabel } from "./labels/CategoryLabel.svelte";
export { default as DirectoryEntryLabel } from "./labels/DirectoryEntryLabel.svelte";
export { default as IconLabel } from "./labels/IconLabel.svelte";
export { default as TitleLabel } from "./labels/TitleLabel.svelte";
export { default as AdaptiveDiv } from "./layout/AdaptiveDiv.svelte";
export { default as BottomDiv } from "./layout/BottomDiv.svelte";
export { default as FullscreenDiv } from "./layout/FullscreenDiv.svelte";
export { default as TitledDiv } from "./layout/TitledDiv.svelte";
export { default as TopBar } from "./layout/TopBar.svelte";
export * from "./overlays/ActionModal.svelte";
export { default as ActionModal } from "./overlays/ActionModal.svelte";
export { default as BottomSheet } from "./overlays/BottomSheet.svelte";
export { default as Modal } from "./overlays/Modal.svelte";
export * from "./overlays/overlayGroup.svelte";
export { default as RenameModal } from "./overlays/RenameModal.svelte";
export { default as TextInputModal } from "./overlays/TextInputModal.svelte";
export * from "./overlays/toast.svelte";
export { default as Toasts } from "./overlays/Toasts.svelte";
export { default as RowVirtualizer } from "./RowVirtualizer.svelte";
File renamed without changes.
@@ -29,7 +29,7 @@
</div>
<style lang="postcss">
@reference "../../../../app.css";
@reference "../../../app.css";
input:focus,
input:not(:placeholder-shown) {
@@ -1,6 +1,6 @@
<script lang="ts">
import type { ClassValue } from "svelte/elements";
import { IconLabel } from "#lib/components/molecules/index.js";
import { IconLabel } from "#lib/ui/index.js";
import IconCategory from "~icons/material-symbols/category";
@@ -1,6 +1,6 @@
<script lang="ts">
import type { ClassValue } from "svelte/elements";
import { IconLabel } from "#lib/components/molecules/index.js";
import { IconLabel } from "#lib/ui/index.js";
import IconFolder from "~icons/material-symbols/folder";
import IconDriveFolderUpload from "~icons/material-symbols/drive-folder-upload";
@@ -1,7 +1,7 @@
<script lang="ts">
import type { Component, Snippet } from "svelte";
import type { ClassValue, SvelteHTMLElements } from "svelte/elements";
import { TitleLabel } from "#lib/components/molecules/index.js";
import { TitleLabel } from "#lib/ui/index.js";
interface Props {
children?: Snippet;
File renamed without changes.
@@ -4,7 +4,7 @@
<script lang="ts">
import type { Snippet } from "svelte";
import { Button, Modal } from "#lib/components/atoms/index.js";
import { Button, Modal } from "#lib/ui/index.js";
interface Props {
cancelText?: string;
@@ -2,7 +2,7 @@
import type { Snippet } from "svelte";
import type { ClassValue } from "svelte/elements";
import { fade, fly } from "svelte/transition";
import { AdaptiveDiv } from "#lib/components/atoms/index.js";
import { AdaptiveDiv } from "#lib/ui/index.js";
interface Props {
children: Snippet;
@@ -2,7 +2,7 @@
import type { Snippet } from "svelte";
import type { ClassValue } from "svelte/elements";
import { fade } from "svelte/transition";
import { AdaptiveDiv } from "#lib/components/atoms/index.js";
import { AdaptiveDiv } from "#lib/ui/index.js";
interface Props {
children: Snippet;
@@ -1,5 +1,5 @@
<script lang="ts">
import { TextInputModal } from "#lib/components/organisms/index.js";
import { TextInputModal } from "#lib/ui/index.js";
interface Props {
isOpen: boolean;
@@ -1,6 +1,5 @@
<script lang="ts">
import { TextInput } from "#lib/components/atoms/index.js";
import { ActionModal, type ConfirmHandler } from "#lib/components/molecules/index.js";
import { TextInput, ActionModal, type ConfirmHandler } from "#lib/ui/index.js";
interface Props {
defaultValue?: string;
@@ -1,6 +1,6 @@
<script lang="ts">
import { fly } from "svelte/transition";
import { getToasts } from "#lib/client/toast.svelte.js";
import { getToasts } from "./toast.svelte";
</script>
<div class="pointer-events-none fixed inset-x-0 top-4 z-50 flex flex-col items-center gap-y-2 px-4">
@@ -0,0 +1,27 @@
// Manages overlays (modals and bottom sheets) of which at most one is open at a time
export class OverlayGroup<T extends string> {
current: T | undefined = $state();
open(name: T) {
this.current = name;
}
close() {
this.current = undefined;
}
// Getter and setter for `bind:isOpen={getter, setter}`
getter(name: T) {
return () => this.current === name;
}
setter(name: T) {
return (isOpen: boolean) => {
if (isOpen) {
this.current = name;
} else if (this.current === name) {
this.current = undefined;
}
};
}
}
+19
View File
@@ -0,0 +1,19 @@
export interface Toast {
id: number;
message: string;
}
const TOAST_DURATION = 3000;
let toasts: Toast[] = $state([]);
let nextId = 0;
export const getToasts = () => toasts;
export const showToast = (message: string) => {
const toast = { id: nextId++, message };
toasts.push(toast);
setTimeout(() => {
toasts = toasts.filter(({ id }) => id !== toast.id);
}, TOAST_DURATION);
};
+1 -1
View File
@@ -1,5 +1,5 @@
<script lang="ts">
import { AdaptiveDiv } from "#lib/components/atoms/index.js";
import { AdaptiveDiv } from "#lib/ui/index.js";
let { children } = $props();
</script>
@@ -1,9 +1,16 @@
<script lang="ts">
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 { showToast, tryAction } from "#lib/client/toast.svelte.js";
import { changePassword as requestPasswordChange } from "#lib/features/auth/index.js";
import { tryAction } from "#lib/features/errors.js";
import {
BottomDiv,
Button,
FullscreenDiv,
TextInput,
TitledDiv,
TopBar,
showToast,
} from "#lib/ui/index.js";
let oldPassword = $state("");
let newPassword = $state("");
+12 -11
View File
@@ -1,22 +1,23 @@
<script lang="ts">
import { goto } from "$app/navigation";
import {
downloadMasterKeys,
keyring,
requestClientRegistrationAndSessionUpgrade,
} from "#lib/client/keys/index.js";
import { ForceLoginModal } from "#lib/features/auth/components/index.js";
import { login as requestLogin, logout as requestLogout } from "#lib/features/auth/index.js";
import { showErrorToast } from "#lib/features/errors.js";
import { cleanupDeletedFileCaches } from "#lib/features/filesystem/index.js";
import {
BottomDiv,
Button,
FullscreenDiv,
TextButton,
TextInput,
} from "#lib/components/atoms/index.js";
import { TitledDiv } from "#lib/components/molecules/index.js";
import { ForceLoginModal } from "#lib/components/organisms/index.js";
import {
downloadMasterKeys,
keyring,
requestClientRegistrationAndSessionUpgrade,
} from "#lib/client/keys/index.js";
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";
TitledDiv,
showToast,
} from "#lib/ui/index.js";
import { isTRPCClientError } from "#trpc/client.js";
let { data } = $props();
@@ -1,13 +1,12 @@
<script lang="ts">
import { onMount } from "svelte";
import { goto } from "$app/navigation";
import { FullscreenDiv } from "#lib/components/atoms/index.js";
import { TitledDiv } from "#lib/components/molecules/index.js";
import {
downloadMasterKeys,
generatePublicKeyFingerprint,
keyring,
} from "#lib/client/keys/index.js";
import { FullscreenDiv, TitledDiv } from "#lib/ui/index.js";
import IconFingerprint from "~icons/material-symbols/fingerprint";
@@ -3,11 +3,10 @@
import { untrack } from "svelte";
import { goto } from "$app/navigation";
import { page } from "$app/state";
import { FullscreenDiv } from "#lib/components/atoms/index.js";
import { Categories, IconEntryButton, TopBar } from "#lib/components/molecules/index.js";
import { captureVideoThumbnail } from "#lib/client/media/thumbnail.js";
import { getFileDownloadState } from "#lib/client/transfer/index.js";
import { showErrorToast, tryAction } from "#lib/client/toast.svelte.js";
import { showErrorToast, tryAction } from "#lib/features/errors.js";
import { Categories } from "#lib/features/filesystem/components/index.js";
import {
addFileToCategory,
getFileContent,
@@ -19,6 +18,7 @@
type DataKey,
type FileInfo,
} from "#lib/features/filesystem/index.js";
import { FullscreenDiv, IconEntryButton, TopBar } from "#lib/ui/index.js";
import AddToCategoryBottomSheet from "./AddToCategoryBottomSheet.svelte";
import DownloadStatus from "./DownloadStatus.svelte";
import TopBarMenu from "./TopBarMenu.svelte";
@@ -1,9 +1,8 @@
<script lang="ts">
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 { tryAction } from "#lib/client/toast.svelte.js";
import { tryAction } from "#lib/features/errors.js";
import { SubCategories, CategoryCreateModal } from "#lib/features/filesystem/components/index.js";
import { createCategory, useCategory } from "#lib/features/filesystem/index.js";
import { BottomDiv, BottomSheet, Button, FullscreenDiv } from "#lib/ui/index.js";
interface Props {
onAddToCategoryClick: (categoryId: number) => void;
@@ -1,13 +1,12 @@
<script lang="ts">
import { onMount } from "svelte";
import { FullscreenDiv } from "#lib/components/atoms/index.js";
import { TopBar } from "#lib/components/molecules/index.js";
import {
getDownloadingFiles,
clearDownloadedFiles,
type FileDownloadState,
} from "#lib/client/transfer/index.js";
import { bulkGetFileInfo, type MaybeFileInfo } from "#lib/features/filesystem/index.js";
import { FullscreenDiv, TopBar } from "#lib/ui/index.js";
import File from "./File.svelte";
let downloadingFiles: { info: MaybeFileInfo; state: FileDownloadState }[] = $state([]);
@@ -1,7 +1,6 @@
<script lang="ts">
import { FullscreenDiv } from "#lib/components/atoms/index.js";
import { TopBar } from "#lib/components/molecules/index.js";
import { getUploadingFiles, clearUploadedFiles } from "#lib/client/transfer/index.js";
import { FullscreenDiv, TopBar } from "#lib/ui/index.js";
import File from "./File.svelte";
const uploadingFiles = getUploadingFiles();
+2 -6
View File
@@ -1,17 +1,13 @@
<script lang="ts">
import { onMount } from "svelte";
import { goto } from "$app/navigation";
import {
FileThumbnailButton,
FullscreenDiv,
RowVirtualizer,
} from "#lib/components/atoms/index.js";
import { TopBar } from "#lib/components/molecules/index.js";
import { FileThumbnailButton } from "#lib/features/filesystem/components/index.js";
import {
bulkGetFileInfo,
type MaybeFileInfo,
type SummarizedFileInfo,
} from "#lib/features/filesystem/index.js";
import { FullscreenDiv, RowVirtualizer, TopBar } from "#lib/ui/index.js";
import { formatDate, formatDateSortable, SortBy, sortEntries } from "#lib/utils/index.js";
let { data } = $props();
@@ -1,8 +1,6 @@
<script lang="ts">
import FileSaver from "file-saver";
import { goto } from "$app/navigation";
import { BottomDiv, Button, FullscreenDiv, TextButton } from "#lib/components/atoms/index.js";
import { TitledDiv } from "#lib/components/molecules/index.js";
import {
keyring,
registerInitialMasterKeyAndHmacSecret,
@@ -10,7 +8,14 @@
requestSessionUpgrade,
serializeClientKeys,
} from "#lib/client/keys/index.js";
import { showToast } from "#lib/client/toast.svelte.js";
import {
BottomDiv,
Button,
FullscreenDiv,
TextButton,
TitledDiv,
showToast,
} from "#lib/ui/index.js";
import BeforeContinueBottomSheet from "./BeforeContinueBottomSheet.svelte";
import BeforeContinueModal from "./BeforeContinueModal.svelte";
@@ -1,5 +1,5 @@
<script lang="ts">
import { BottomDiv, BottomSheet, Button, FullscreenDiv } from "#lib/components/atoms/index.js";
import { BottomDiv, BottomSheet, Button, FullscreenDiv } from "#lib/ui/index.js";
interface Props {
onRetryClick: () => void;
@@ -1,5 +1,5 @@
<script lang="ts">
import { ActionModal } from "#lib/components/molecules/index.js";
import { ActionModal } from "#lib/ui/index.js";
interface Props {
isOpen: boolean;
@@ -1,9 +1,6 @@
<script lang="ts">
import { onMount } from "svelte";
import { goto } from "$app/navigation";
import { BottomDiv, Button, FullscreenDiv, TextButton } from "#lib/components/atoms/index.js";
import { TitledDiv } from "#lib/components/molecules/index.js";
import { ForceLoginModal } from "#lib/components/organisms/index.js";
import {
generateClientKeys,
generateInitialHmacSecret,
@@ -13,8 +10,16 @@
registerInitialMasterKeyAndHmacSecret,
requestClientRegistrationAndSessionUpgrade,
} from "#lib/client/keys/index.js";
import { showToast } from "#lib/client/toast.svelte.js";
import { ForceLoginModal } from "#lib/features/auth/components/index.js";
import { cleanupDeletedFileCaches } from "#lib/features/filesystem/index.js";
import {
BottomDiv,
Button,
FullscreenDiv,
TextButton,
TitledDiv,
showToast,
} from "#lib/ui/index.js";
import { gotoStateful } from "#lib/utils/index.js";
import Order from "./Order.svelte";
+5 -6
View File
@@ -4,17 +4,16 @@
import { untrack } from "svelte";
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 { showErrorToast } from "#lib/features/errors.js";
import { DirectoryEntry, FileEntry } from "#lib/features/filesystem/components/index.js";
import {
getDirectoryInfo,
type LocalCategoryInfo,
type MaybeDirectoryInfo,
} from "#lib/features/filesystem/index.js";
import { search, type SearchFilter, type SearchResult } from "#lib/features/search/index.js";
import { Chip, FullscreenDiv, RowVirtualizer } from "#lib/ui/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";
@@ -236,9 +235,9 @@
{#snippet item(index)}
{@const info = result[index]!}
{#if info.type === "directory"}
<Directory {info} onclick={() => goto(`/directory/${info.id}?from=search`)} />
<DirectoryEntry {info} onclick={() => goto(`/directory/${info.id}?from=search`)} />
{:else}
<File {info} onclick={() => goto(`/file/${info.id}?from=search`)} />
<FileEntry {info} onclick={() => goto(`/file/${info.id}?from=search`)} />
{/if}
{/snippet}
</RowVirtualizer>
@@ -1,16 +0,0 @@
<script lang="ts">
import { ActionEntryButton } from "#lib/components/atoms/index.js";
import { DirectoryEntryLabel } from "#lib/components/molecules/index.js";
import type { SubDirectoryInfo } from "#lib/features/filesystem/index.js";
interface Props {
info: SubDirectoryInfo;
onclick: () => void;
}
let { info, onclick }: Props = $props();
</script>
<ActionEntryButton class="h-14" {onclick}>
<DirectoryEntryLabel type="directory" name={info.name} isFavorite={info.isFavorite} />
</ActionEntryButton>
@@ -1,26 +0,0 @@
<script lang="ts">
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/features/filesystem/index.js";
import { formatDateTime } from "#lib/utils/index.js";
interface Props {
info: SummarizedFileInfo;
onclick: () => void;
}
let { info, onclick }: Props = $props();
let thumbnail = $derived(getFileThumbnail(info));
</script>
<ActionEntryButton class="h-14" {onclick}>
<DirectoryEntryLabel
type="file"
thumbnail={thumbnail?.url}
name={info.name}
subtext={formatDateTime(info.createdAt ?? info.lastModifiedAt)}
isFavorite={info.isFavorite}
/>
</ActionEntryButton>
@@ -1,7 +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 { SubCategories } from "#lib/features/filesystem/components/index.js";
import { useCategory, type LocalCategoryInfo } from "#lib/features/filesystem/index.js";
import { BottomDiv, BottomSheet, Button, FullscreenDiv } from "#lib/ui/index.js";
interface Props {
isOpen: boolean;
+2 -3
View File
@@ -1,13 +1,12 @@
<script lang="ts">
import { onMount } from "svelte";
import { FullscreenDiv } from "#lib/components/atoms/index.js";
import { TopBar } from "#lib/components/molecules/index.js";
import type { FileCacheIndex } from "#lib/client/storage/indexedDB/index.js";
import {
getFileCacheIndex,
deleteFileCache as doDeleteFileCache,
} from "#lib/client/cache/index.js";
import type { FileCacheIndex } from "#lib/client/storage/indexedDB/index.js";
import { bulkGetFileInfo, type MaybeFileInfo } from "#lib/features/filesystem/index.js";
import { FullscreenDiv, TopBar } from "#lib/ui/index.js";
import { formatFileSize } from "#lib/utils/index.js";
import File from "./File.svelte";
@@ -1,12 +1,11 @@
<script lang="ts">
import { onMount } from "svelte";
import { goto } from "$app/navigation";
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 { FileEntry } from "#lib/features/filesystem/components/index.js";
import type { MaybeFileInfo } from "#lib/features/filesystem/index.js";
import { BottomDiv, Button, FullscreenDiv, IconEntryButton, TopBar } from "#lib/ui/index.js";
import { sortEntries } from "#lib/utils/index.js";
import File from "./File.svelte";
import {
getThumbnailGenerationStatus,
clearThumbnailGenerationStatuses,
@@ -15,10 +14,19 @@
type GenerationStatus,
} from "./service";
import IconCamera from "~icons/material-symbols/camera";
import IconDelete from "~icons/material-symbols/delete";
let { data } = $props();
const statusTexts = {
queued: "대기 중",
"generation-pending": "준비 중",
generating: "생성하는 중",
uploading: "업로드하는 중",
error: "실패",
} as const;
let fileInfos: MaybeFileInfo[] = $state([]);
let files = $derived(
fileInfos
@@ -69,11 +77,18 @@
<div class="space-y-2">
{#each files as { info, status } (info.id)}
{#if info.exists}
<File
<FileEntry
{info}
{status}
onclick={({ id }) => goto(`/file/${id}`)}
onGenerateThumbnailClick={requestThumbnailGeneration}
showThumbnail={false}
subtext={status && statusTexts[status]}
onclick={() => goto(`/file/${info.id}`)}
action={!status || status === "error"
? {
icon: IconCamera,
onclick: () => requestThumbnailGeneration(info),
class: "text-gray-800",
}
: undefined}
/>
{/if}
{/each}
@@ -1,41 +0,0 @@
<script module lang="ts">
const subtexts = {
queued: "대기 중",
"generation-pending": "준비 중",
generating: "생성하는 중",
uploading: "업로드하는 중",
error: "실패",
} as const;
</script>
<script lang="ts">
import { ActionEntryButton } from "#lib/components/atoms/index.js";
import { DirectoryEntryLabel } from "#lib/components/molecules/index.js";
import type { FileInfo } from "#lib/features/filesystem/index.js";
import { formatDateTime } from "#lib/utils/index.js";
import type { GenerationStatus } from "./service";
import IconCamera from "~icons/material-symbols/camera";
interface Props {
info: FileInfo;
onclick: (file: FileInfo) => void;
onGenerateThumbnailClick: (file: FileInfo) => void;
status: Exclude<GenerationStatus, "uploaded"> | undefined;
}
let { info, onclick, onGenerateThumbnailClick, status }: Props = $props();
</script>
<ActionEntryButton
class="h-14"
onclick={() => onclick(info)}
actionButtonIcon={!status || status === "error" ? IconCamera : undefined}
onActionButtonClick={() => onGenerateThumbnailClick(info)}
actionButtonClass="text-gray-800"
>
{@const subtext = status
? subtexts[status]
: formatDateTime(info.createdAt ?? info.lastModifiedAt)}
<DirectoryEntryLabel type="file" name={info.name} {subtext} />
</ActionEntryButton>
+1 -1
View File
@@ -1,5 +1,5 @@
<script lang="ts">
import { AdaptiveDiv } from "#lib/components/atoms/index.js";
import { AdaptiveDiv } from "#lib/ui/index.js";
import BottomBar from "./BottomBar.svelte";
let { children } = $props();
+1 -1
View File
@@ -1,7 +1,7 @@
<script lang="ts">
import { goto } from "$app/navigation";
import { page } from "$app/state";
import { AdaptiveDiv } from "#lib/components/atoms/index.js";
import { AdaptiveDiv } from "#lib/ui/index.js";
import IconHome from "~icons/material-symbols/home";
import IconFolder from "~icons/material-symbols/folder";
+30 -31
View File
@@ -1,9 +1,11 @@
<script lang="ts">
import { goto } from "$app/navigation";
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 { tryAction } from "#lib/client/toast.svelte.js";
import { tryAction } from "#lib/features/errors.js";
import {
FileEntry,
SubCategories,
CategoryCreateModal,
} from "#lib/features/filesystem/components/index.js";
import {
createCategory,
deleteCategory,
@@ -13,13 +15,14 @@
useCategory,
type LocalCategoryInfo,
} from "#lib/features/filesystem/index.js";
import { CheckBox, OverlayGroup, RowVirtualizer, TopBar } from "#lib/ui/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 } from "./service.svelte";
import IconClose from "~icons/material-symbols/close";
import IconMoreVert from "~icons/material-symbols/more-vert";
let { data } = $props();
@@ -28,10 +31,9 @@
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);
const overlays = new OverlayGroup<
"categoryCreate" | "categoryMenu" | "categoryRename" | "categoryDelete"
>();
let files = $derived(
sortEntries(
@@ -59,11 +61,11 @@
<SubCategories
{info}
onSubCategoryClick={({ id }) => goto(`/category/${id}`)}
onSubCategoryCreateClick={() => (isCategoryCreateModalOpen = true)}
onSubCategoryCreateClick={() => overlays.open("categoryCreate")}
subCategoryCreatePosition="bottom"
onSubCategoryMenuClick={(subCategory) => {
context.selectedCategory = subCategory;
isCategoryMenuBottomSheetOpen = true;
overlays.open("categoryMenu");
}}
subCategoryMenuIcon={IconMoreVert}
/>
@@ -89,15 +91,18 @@
>
{#snippet item(index)}
{@const { details } = files[index]!}
<File
<FileEntry
info={details}
onclick={({ id }) => goto(`/file/${id}?from=category`)}
onRemoveClick={!details.isRecursive
? ({ id }) =>
tryAction(
() => removeFileFromCategory(id, data.id as number),
"카테고리에서 파일을 제거하지 못했어요.",
)
onclick={() => goto(`/file/${details.id}?from=category`)}
action={!details.isRecursive
? {
icon: IconClose,
onclick: () =>
tryAction(
() => removeFileFromCategory(details.id, data.id as number),
"카테고리에서 파일을 제거하지 못했어요.",
),
}
: undefined}
/>
{/snippet}
@@ -112,24 +117,18 @@
</div>
<CategoryCreateModal
bind:isOpen={isCategoryCreateModalOpen}
bind:isOpen={overlays.getter("categoryCreate"), overlays.setter("categoryCreate")}
onCreateClick={(name: string) =>
tryAction(() => createCategory(data.id, name), "카테고리를 만들지 못했어요.")}
/>
<CategoryMenuBottomSheet
bind:isOpen={isCategoryMenuBottomSheetOpen}
onRenameClick={() => {
isCategoryMenuBottomSheetOpen = false;
isCategoryRenameModalOpen = true;
}}
onDeleteClick={() => {
isCategoryMenuBottomSheetOpen = false;
isCategoryDeleteModalOpen = true;
}}
bind:isOpen={overlays.getter("categoryMenu"), overlays.setter("categoryMenu")}
onRenameClick={() => overlays.open("categoryRename")}
onDeleteClick={() => overlays.open("categoryDelete")}
/>
<CategoryRenameModal
bind:isOpen={isCategoryRenameModalOpen}
bind:isOpen={overlays.getter("categoryRename"), overlays.setter("categoryRename")}
onRenameClick={(newName: string) =>
tryAction(
() => renameCategory({ ...context.selectedCategory!, parentId: data.id }, newName),
@@ -137,7 +136,7 @@
)}
/>
<CategoryDeleteModal
bind:isOpen={isCategoryDeleteModalOpen}
bind:isOpen={overlays.getter("categoryDelete"), overlays.setter("categoryDelete")}
onDeleteClick={() =>
tryAction(
() => deleteCategory({ ...context.selectedCategory!, parentId: data.id }),
@@ -1,5 +1,5 @@
<script lang="ts">
import { ActionModal } from "#lib/components/molecules/index.js";
import { ActionModal } from "#lib/ui/index.js";
import { truncateString } from "#lib/utils/index.js";
import { useContext } from "./service.svelte";
@@ -1,6 +1,5 @@
<script lang="ts">
import { BottomSheet } from "#lib/components/atoms/index.js";
import { CategoryLabel, IconEntryButton } from "#lib/components/molecules/index.js";
import { BottomSheet, CategoryLabel, IconEntryButton } from "#lib/ui/index.js";
import { useContext } from "./service.svelte";
import IconEdit from "~icons/material-symbols/edit";
@@ -1,5 +1,5 @@
<script lang="ts">
import { RenameModal } from "#lib/components/organisms/index.js";
import { RenameModal } from "#lib/ui/index.js";
import { useContext } from "./service.svelte";
interface Props {
@@ -1,35 +0,0 @@
<script lang="ts">
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/features/filesystem/index.js";
import { formatDateTime } from "#lib/utils/index.js";
import type { SelectedFile } from "./service.svelte";
import IconClose from "~icons/material-symbols/close";
interface Props {
info: CategoryFileInfo;
onclick: (file: SelectedFile) => void;
onRemoveClick?: (file: SelectedFile) => void;
}
let { info, onclick, onRemoveClick }: Props = $props();
let thumbnail = $derived(getFileThumbnail(info));
</script>
<ActionEntryButton
class="h-14"
onclick={() => onclick(info)}
actionButtonIcon={onRemoveClick && IconClose}
onActionButtonClick={() => onRemoveClick?.(info)}
>
<DirectoryEntryLabel
type="file"
thumbnail={thumbnail?.url}
name={info.name}
subtext={formatDateTime(info.createdAt ?? info.lastModifiedAt)}
isFavorite={info.isFavorite}
/>
</ActionEntryButton>
@@ -1,18 +1,20 @@
import { getContext, setContext } from "svelte";
import type { SelectedCategory } from "#lib/components/molecules/index.js";
import { createContext as createSvelteContext } from "svelte";
import type { SelectedCategory } from "#lib/features/filesystem/components/index.js";
export interface SelectedFile {
id: number;
name: string;
}
interface Context {
selectedCategory?: SelectedCategory;
}
const [getContext, setContext] = createSvelteContext<Context>();
export const createContext = () => {
const context = $state({
selectedCategory: undefined as SelectedCategory | undefined,
});
return setContext("context", context);
const context: Context = $state({});
return setContext(context);
};
export const useContext = () => {
return getContext<ReturnType<typeof createContext>>("context");
};
export const useContext = getContext;
+18 -30
View File
@@ -1,10 +1,9 @@
<script lang="ts">
import { goto } from "$app/navigation";
import { page } from "$app/state";
import { FloatingButton } from "#lib/components/atoms/index.js";
import { TopBar } from "#lib/components/molecules/index.js";
import { tryAction } from "#lib/client/toast.svelte.js";
import { tryAction } from "#lib/features/errors.js";
import { createDirectory, uploadFile, useDirectory } from "#lib/features/filesystem/index.js";
import { FloatingButton, OverlayGroup, TopBar } from "#lib/ui/index.js";
import DirectoryCreateModal from "./DirectoryCreateModal.svelte";
import DirectoryEntries from "./DirectoryEntries";
import DownloadStatusCard from "./DownloadStatusCard.svelte";
@@ -28,14 +27,12 @@
let duplicatedFile: File | undefined = $state();
let resolveForDuplicateFileModal: ((res: boolean) => void) | undefined = $state();
let isEntryCreateBottomSheetOpen = $state(false);
let isDirectoryCreateModalOpen = $state(false);
const overlays = new OverlayGroup<
"entryCreate" | "directoryCreate" | "entryMenu" | "entryRename" | "entryDelete"
>();
// Shown independently as it is requested while uploading in the background
let isDuplicateFileModalOpen = $state(false);
let isEntryMenuBottomSheetOpen = $state(false);
let isEntryRenameModalOpen = $state(false);
let isEntryDeleteModalOpen = $state(false);
let showParentEntry = $derived(
["file", "search", "favorite"].includes(page.url.searchParams.get("from") ?? ""),
);
@@ -96,7 +93,7 @@
onEntryClick={({ type, id }) => goto(`/${type}/${id}`)}
onEntryMenuClick={(entry) => {
context.selectedEntry = entry;
isEntryMenuBottomSheetOpen = true;
overlays.open("entryMenu");
}}
showParentEntry={showParentEntry && data.id !== "root"}
onParentClick={() =>
@@ -114,23 +111,20 @@
<FloatingButton
icon={IconAdd}
onclick={() => {
isEntryCreateBottomSheetOpen = true;
overlays.open("entryCreate");
}}
class="right-4 bottom-24"
/>
<EntryCreateBottomSheet
bind:isOpen={isEntryCreateBottomSheetOpen}
onDirectoryCreateClick={() => {
isEntryCreateBottomSheetOpen = false;
isDirectoryCreateModalOpen = true;
}}
bind:isOpen={overlays.getter("entryCreate"), overlays.setter("entryCreate")}
onDirectoryCreateClick={() => overlays.open("directoryCreate")}
onFileUploadClick={() => {
isEntryCreateBottomSheetOpen = false;
overlays.close();
fileInput?.click();
}}
/>
<DirectoryCreateModal
bind:isOpen={isDirectoryCreateModalOpen}
bind:isOpen={overlays.getter("directoryCreate"), overlays.setter("directoryCreate")}
onCreateClick={(name) =>
tryAction(() => createDirectory(data.id, name), "폴더를 만들지 못했어요.")}
/>
@@ -148,15 +142,9 @@
/>
<EntryMenuBottomSheet
bind:isOpen={isEntryMenuBottomSheetOpen}
onRenameClick={() => {
isEntryMenuBottomSheetOpen = false;
isEntryRenameModalOpen = true;
}}
onDeleteClick={() => {
isEntryMenuBottomSheetOpen = false;
isEntryDeleteModalOpen = true;
}}
bind:isOpen={overlays.getter("entryMenu"), overlays.setter("entryMenu")}
onRenameClick={() => overlays.open("entryRename")}
onDeleteClick={() => overlays.open("entryDelete")}
onFavoriteClick={async () => {
if (
await tryAction(
@@ -164,16 +152,16 @@
"즐겨찾기를 변경하지 못했어요.",
)
) {
isEntryMenuBottomSheetOpen = false;
overlays.close();
}
}}
/>
<EntryRenameModal
bind:isOpen={isEntryRenameModalOpen}
bind:isOpen={overlays.getter("entryRename"), overlays.setter("entryRename")}
onRenameClick={(newName: string) =>
tryAction(() => renameEntry(context.selectedEntry!, newName), "이름을 바꾸지 못했어요.")}
/>
<EntryDeleteModal
bind:isOpen={isEntryDeleteModalOpen}
bind:isOpen={overlays.getter("entryDelete"), overlays.setter("entryDelete")}
onDeleteClick={() => tryAction(() => deleteEntry(context.selectedEntry!), "삭제하지 못했어요.")}
/>
@@ -1,5 +1,5 @@
<script lang="ts">
import { TextInputModal } from "#lib/components/organisms/index.js";
import { TextInputModal } from "#lib/ui/index.js";
interface Props {
isOpen: boolean;
@@ -1,14 +1,18 @@
<script lang="ts">
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/features/filesystem/index.js";
import { DirectoryEntry, FileEntry } from "#lib/features/filesystem/components/index.js";
import type {
DirectoryInfo,
SubDirectoryInfo,
SummarizedFileInfo,
} from "#lib/features/filesystem/index.js";
import { ActionEntryButton, RowVirtualizer, DirectoryEntryLabel } from "#lib/ui/index.js";
import { sortEntries } from "#lib/utils/index.js";
import File from "./File.svelte";
import SubDirectory from "./SubDirectory.svelte";
import UploadingFile from "./UploadingFile.svelte";
import type { SelectedEntry } from "../service.svelte";
import IconMoreVert from "~icons/material-symbols/more-vert";
interface Props {
info: DirectoryInfo;
onEntryClick: (entry: SelectedEntry) => void;
@@ -35,6 +39,11 @@
<T extends Exclude<Entry["type"], "parent">>(type: T) =>
(details: Extract<Entry, { type: T }>["details"]) => ({ type, name: details.name, details });
const toSelectedEntry = (
type: SelectedEntry["type"],
{ id, parentId, dataKey, name, isFavorite }: SubDirectoryInfo | SummarizedFileInfo,
): SelectedEntry => ({ type, id, parentId, dataKey, name, isFavorite });
let entries = $derived([
...(showParentEntry ? ([{ type: "parent" }] as const) : []),
...sortEntries(info.subDirectories.map(toEntry("directory"))),
@@ -63,13 +72,19 @@
<DirectoryEntryLabel type="parent-directory" name=".." />
</ActionEntryButton>
{:else if entry.type === "directory"}
<SubDirectory
{@const selected = toSelectedEntry("directory", entry.details)}
<DirectoryEntry
info={entry.details}
onclick={onEntryClick}
onOpenMenuClick={onEntryMenuClick}
onclick={() => onEntryClick(selected)}
action={{ icon: IconMoreVert, onclick: () => onEntryMenuClick(selected) }}
/>
{:else if entry.type === "file"}
<File info={entry.details} onclick={onEntryClick} onOpenMenuClick={onEntryMenuClick} />
{@const selected = toSelectedEntry("file", entry.details)}
<FileEntry
info={entry.details}
onclick={() => onEntryClick(selected)}
action={{ icon: IconMoreVert, onclick: () => onEntryMenuClick(selected) }}
/>
{:else}
<UploadingFile state={entry.details} />
{/if}
@@ -1,46 +0,0 @@
<script lang="ts">
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/features/filesystem/index.js";
import { formatDateTime } from "#lib/utils/index.js";
import type { SelectedEntry } from "../service.svelte";
import IconMoreVert from "~icons/material-symbols/more-vert";
interface Props {
info: SummarizedFileInfo;
onclick: (entry: SelectedEntry) => void;
onOpenMenuClick: (entry: SelectedEntry) => void;
}
let { info, onclick, onOpenMenuClick }: Props = $props();
let thumbnail = $derived(getFileThumbnail(info));
const action = (callback: typeof onclick) => {
callback({
type: "file",
id: info.id,
parentId: info.parentId,
dataKey: info.dataKey,
name: info.name,
isFavorite: info.isFavorite,
});
};
</script>
<ActionEntryButton
class="h-14"
onclick={() => action(onclick)}
actionButtonIcon={IconMoreVert}
onActionButtonClick={() => action(onOpenMenuClick)}
>
<DirectoryEntryLabel
type="file"
thumbnail={thumbnail?.url}
name={info.name}
subtext={formatDateTime(info.createdAt ?? info.lastModifiedAt)}
isFavorite={info.isFavorite}
/>
</ActionEntryButton>
@@ -1,36 +0,0 @@
<script lang="ts">
import { ActionEntryButton } from "#lib/components/atoms/index.js";
import { DirectoryEntryLabel } from "#lib/components/molecules/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";
interface Props {
info: SubDirectoryInfo;
onclick: (entry: SelectedEntry) => void;
onOpenMenuClick: (entry: SelectedEntry) => void;
}
let { info, onclick, onOpenMenuClick }: Props = $props();
const action = (callback: typeof onclick) => {
callback({
type: "directory",
id: info.id,
parentId: info.parentId,
dataKey: info.dataKey,
name: info.name,
isFavorite: info.isFavorite,
});
};
</script>
<ActionEntryButton
class="h-14"
onclick={() => action(onclick)}
actionButtonIcon={IconMoreVert}
onActionButtonClick={() => action(onOpenMenuClick)}
>
<DirectoryEntryLabel type="directory" name={info.name} isFavorite={info.isFavorite} />
</ActionEntryButton>
@@ -1,5 +1,5 @@
<script lang="ts">
import { ActionModal } from "#lib/components/molecules/index.js";
import { ActionModal } from "#lib/ui/index.js";
import { truncateString } from "#lib/utils/index.js";
interface Props {
@@ -1,6 +1,5 @@
<script lang="ts">
import { BottomSheet } from "#lib/components/atoms/index.js";
import { IconEntryButton } from "#lib/components/molecules/index.js";
import { BottomSheet, IconEntryButton } from "#lib/ui/index.js";
import IconCreateNewFolder from "~icons/material-symbols/create-new-folder";
import IconUploadFile from "~icons/material-symbols/upload-file";
@@ -1,5 +1,5 @@
<script lang="ts">
import { ActionModal } from "#lib/components/molecules/index.js";
import { ActionModal } from "#lib/ui/index.js";
import { truncateString } from "#lib/utils/index.js";
import { useContext } from "./service.svelte";
@@ -1,6 +1,5 @@
<script lang="ts">
import { BottomSheet } from "#lib/components/atoms/index.js";
import { DirectoryEntryLabel, IconEntryButton } from "#lib/components/molecules/index.js";
import { BottomSheet, DirectoryEntryLabel, IconEntryButton } from "#lib/ui/index.js";
import { useContext } from "./service.svelte";
import IconFavorite from "~icons/material-symbols/favorite";
@@ -1,5 +1,5 @@
<script lang="ts">
import { RenameModal } from "#lib/components/organisms/index.js";
import { RenameModal } from "#lib/ui/index.js";
import { useContext } from "./service.svelte";
interface Props {
@@ -1,4 +1,4 @@
import { getContext, setContext } from "svelte";
import { createContext as createSvelteContext } from "svelte";
import {
deleteDirectory,
deleteFile,
@@ -18,16 +18,18 @@ export interface SelectedEntry {
isFavorite: boolean;
}
interface Context {
selectedEntry?: SelectedEntry;
}
const [getContext, setContext] = createSvelteContext<Context>();
export const createContext = () => {
const context = $state({
selectedEntry: undefined as SelectedEntry | undefined,
});
return setContext("context", context);
const context: Context = $state({});
return setContext(context);
};
export const useContext = () => {
return getContext<ReturnType<typeof createContext>>("context");
};
export const useContext = getContext;
export const renameEntry = async (entry: SelectedEntry, newName: string) => {
if (entry.type === "directory") {
+8 -9
View File
@@ -1,17 +1,16 @@
<script lang="ts">
import { onMount } from "svelte";
import { goto } from "$app/navigation";
import { RowVirtualizer } from "#lib/components/atoms/index.js";
import { TopBar } from "#lib/components/molecules/index.js";
import Directory from "./Directory.svelte";
import File from "./File.svelte";
import { showErrorToast, tryAction } from "#lib/client/toast.svelte.js";
import { showErrorToast, tryAction } from "#lib/features/errors.js";
import {
getFavoriteEntries,
removeFavorite,
type FavoriteEntry,
} from "#lib/features/favorites/index.js";
import { DirectoryEntry, FileEntry } from "#lib/features/filesystem/components/index.js";
import { RowVirtualizer, TopBar } from "#lib/ui/index.js";
import IconClose from "~icons/material-symbols/close";
import IconSearch from "~icons/material-symbols/search";
let entries: FavoriteEntry[] = $state([]);
@@ -68,16 +67,16 @@
{#snippet item(index)}
{@const entry = entries[index]!}
{#if entry.type === "directory"}
<Directory
<DirectoryEntry
info={entry.details}
onclick={() => handleClick(entry)}
onRemoveClick={() => handleRemove(entry)}
action={{ icon: IconClose, onclick: () => handleRemove(entry) }}
/>
{:else}
<File
<FileEntry
info={entry.details}
onclick={() => handleClick(entry)}
onRemoveClick={() => handleRemove(entry)}
action={{ icon: IconClose, onclick: () => handleRemove(entry) }}
/>
{/if}
{/snippet}
@@ -1,24 +0,0 @@
<script lang="ts">
import { ActionEntryButton } from "#lib/components/atoms/index.js";
import { DirectoryEntryLabel } from "#lib/components/molecules/index.js";
import type { SubDirectoryInfo } from "#lib/features/filesystem/index.js";
import IconClose from "~icons/material-symbols/close";
interface Props {
info: SubDirectoryInfo;
onclick: () => void;
onRemoveClick: () => void;
}
let { info, onclick, onRemoveClick }: Props = $props();
</script>
<ActionEntryButton
class="h-14"
{onclick}
actionButtonIcon={IconClose}
onActionButtonClick={onRemoveClick}
>
<DirectoryEntryLabel type="directory" name={info.name} isFavorite />
</ActionEntryButton>
-34
View File
@@ -1,34 +0,0 @@
<script lang="ts">
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/features/filesystem/index.js";
import { formatDateTime } from "#lib/utils/index.js";
import IconClose from "~icons/material-symbols/close";
interface Props {
info: SummarizedFileInfo;
onclick: () => void;
onRemoveClick: () => void;
}
let { info, onclick, onRemoveClick }: Props = $props();
let thumbnail = $derived(getFileThumbnail(info));
</script>
<ActionEntryButton
class="h-14"
{onclick}
actionButtonIcon={IconClose}
onActionButtonClick={onRemoveClick}
>
<DirectoryEntryLabel
type="file"
thumbnail={thumbnail?.url}
name={info.name}
subtext={formatDateTime(info.createdAt ?? info.lastModifiedAt)}
isFavorite
/>
</ActionEntryButton>
+2 -1
View File
@@ -1,8 +1,9 @@
<script lang="ts">
import { onMount } from "svelte";
import { goto } from "$app/navigation";
import { EntryButton, FileThumbnailButton } from "#lib/components/atoms/index.js";
import { FileThumbnailButton } from "#lib/features/filesystem/components/index.js";
import { type SummarizedFileInfo } from "#lib/features/filesystem/index.js";
import { EntryButton } from "#lib/ui/index.js";
import { requestFreshMediaFilesRetrieval } from "./service";
let mediaFiles: SummarizedFileInfo[] = $state([]);
+1 -1
View File
@@ -1,8 +1,8 @@
<script lang="ts">
import { goto } from "$app/navigation";
import MenuEntryButton from "./MenuEntryButton.svelte";
import { tryAction } from "#lib/client/toast.svelte.js";
import { logout as requestLogout } from "#lib/features/auth/index.js";
import { tryAction } from "#lib/features/errors.js";
import IconStorage from "~icons/material-symbols/storage";
import IconImage from "~icons/material-symbols/image";
@@ -1,7 +1,7 @@
<script lang="ts">
import type { Component, Snippet } from "svelte";
import type { ClassValue, SvelteHTMLElements } from "svelte/elements";
import { IconEntryButton } from "#lib/components/molecules/index.js";
import { IconEntryButton } from "#lib/ui/index.js";
interface Props {
children: Snippet;
+1 -1
View File
@@ -1,6 +1,6 @@
<script lang="ts">
import { getDownloadingFiles, getUploadingFiles } from "#lib/client/transfer/index.js";
import { Toasts } from "#lib/components/atoms/index.js";
import { Toasts } from "#lib/ui/index.js";
import "../app.css";
let { children } = $props();