refactor: web stores => managers (1)

This commit is contained in:
Daniel Dietzler 2025-04-28 13:20:00 +02:00
parent 205260d31c
commit d1e62c3736
No known key found for this signature in database
GPG Key ID: A1C0B97CD8E18DFF
37 changed files with 196 additions and 143 deletions

View File

@ -1,7 +1,6 @@
<script lang="ts"> <script lang="ts">
import SelectAllAssets from '$lib/components/photos-page/actions/select-all-assets.svelte'; import SelectAllAssets from '$lib/components/photos-page/actions/select-all-assets.svelte';
import { assetViewingStore } from '$lib/stores/asset-viewing.store'; import { assetViewingStore } from '$lib/stores/asset-viewing.store';
import { dragAndDropFilesStore } from '$lib/stores/drag-and-drop-files.store';
import { fileUploadHandler, openFileUploadDialog } from '$lib/utils/file-uploader'; import { fileUploadHandler, openFileUploadDialog } from '$lib/utils/file-uploader';
import type { AlbumResponseDto, SharedLinkResponseDto, UserResponseDto } from '@immich/sdk'; import type { AlbumResponseDto, SharedLinkResponseDto, UserResponseDto } from '@immich/sdk';
import { AssetStore } from '$lib/stores/assets-store.svelte'; import { AssetStore } from '$lib/stores/assets-store.svelte';
@ -20,6 +19,7 @@
import { t } from 'svelte-i18n'; import { t } from 'svelte-i18n';
import { onDestroy } from 'svelte'; import { onDestroy } from 'svelte';
import { AssetInteraction } from '$lib/stores/asset-interaction.svelte'; import { AssetInteraction } from '$lib/stores/asset-interaction.svelte';
import { dragAndDropManager } from '$lib/managers/drag-and-drop.manager.svelte';
interface Props { interface Props {
sharedLink: SharedLinkResponseDto; sharedLink: SharedLinkResponseDto;
@ -38,10 +38,10 @@
const assetInteraction = new AssetInteraction(); const assetInteraction = new AssetInteraction();
dragAndDropFilesStore.subscribe((value) => { $effect(() => {
if (value.isDragging && value.files.length > 0) { if (dragAndDropManager.isDragging && dragAndDropManager.files.length > 0) {
handlePromiseError(fileUploadHandler(value.files, album.id)); handlePromiseError(fileUploadHandler(dragAndDropManager.files, album.id));
dragAndDropFilesStore.set({ isDragging: false, files: [] }); dragAndDropManager.reset();
} }
}); });
</script> </script>

View File

@ -5,7 +5,6 @@
import NextAssetAction from '$lib/components/asset-viewer/actions/next-asset-action.svelte'; import NextAssetAction from '$lib/components/asset-viewer/actions/next-asset-action.svelte';
import PreviousAssetAction from '$lib/components/asset-viewer/actions/previous-asset-action.svelte'; import PreviousAssetAction from '$lib/components/asset-viewer/actions/previous-asset-action.svelte';
import { AssetAction, ProjectionType } from '$lib/constants'; import { AssetAction, ProjectionType } from '$lib/constants';
import { updateNumberOfComments } from '$lib/stores/activity.store';
import { closeEditorCofirm } from '$lib/stores/asset-editor.store'; import { closeEditorCofirm } from '$lib/stores/asset-editor.store';
import { assetViewingStore } from '$lib/stores/asset-viewing.store'; import { assetViewingStore } from '$lib/stores/asset-viewing.store';
import { isShowDetail } from '$lib/stores/preferences.store'; import { isShowDetail } from '$lib/stores/preferences.store';
@ -47,6 +46,7 @@
import PhotoViewer from './photo-viewer.svelte'; import PhotoViewer from './photo-viewer.svelte';
import SlideshowBar from './slideshow-bar.svelte'; import SlideshowBar from './slideshow-bar.svelte';
import VideoViewer from './video-wrapper-viewer.svelte'; import VideoViewer from './video-wrapper-viewer.svelte';
import { activityManager } from '$lib/managers/activity.manager.svelte';
type HasAsset = boolean; type HasAsset = boolean;
@ -137,12 +137,12 @@
const handleAddComment = () => { const handleAddComment = () => {
numberOfComments++; numberOfComments++;
updateNumberOfComments(1); activityManager.updateNumberOfComments(1);
}; };
const handleRemoveComment = () => { const handleRemoveComment = () => {
numberOfComments--; numberOfComments--;
updateNumberOfComments(-1); activityManager.updateNumberOfComments(-1);
}; };
const handleFavorite = async () => { const handleFavorite = async () => {

View File

@ -46,7 +46,7 @@
import AlbumListItemDetails from './album-list-item-details.svelte'; import AlbumListItemDetails from './album-list-item-details.svelte';
import Portal from '$lib/components/shared-components/portal/portal.svelte'; import Portal from '$lib/components/shared-components/portal/portal.svelte';
import { getMetadataSearchQuery } from '$lib/utils/metadata-search'; import { getMetadataSearchQuery } from '$lib/utils/metadata-search';
import { isFaceEditMode } from '$lib/stores/face-edit.svelte'; import { faceManager } from '$lib/managers/face.manager.svelte';
interface Props { interface Props {
asset: AssetResponseDto; asset: AssetResponseDto;
@ -207,7 +207,7 @@
padding="1" padding="1"
size="20" size="20"
buttonSize="32" buttonSize="32"
onclick={() => (isFaceEditMode.value = !isFaceEditMode.value)} onclick={() => (faceManager.isEditMode = !faceManager.isEditMode)}
/> />
{#if people.length > 0 || unassignedFaces.length > 0} {#if people.length > 0 || unassignedFaces.length > 0}

View File

@ -1,5 +1,5 @@
<script lang="ts"> <script lang="ts">
import { type DownloadProgress, downloadManager, downloadStore } from '$lib/stores/download-store.svelte'; import { downloadManager, type DownloadProgress } from '$lib/managers/download.manager.svelte';
import { locale } from '$lib/stores/preferences.store'; import { locale } from '$lib/stores/preferences.store';
import { fly, slide } from 'svelte/transition'; import { fly, slide } from 'svelte/transition';
import { getByteUnitString } from '../../utils/byte-units'; import { getByteUnitString } from '../../utils/byte-units';
@ -13,15 +13,15 @@
}; };
</script> </script>
{#if downloadStore.isDownloading} {#if downloadManager.isDownloading}
<div <div
transition:fly={{ x: -100, duration: 350 }} transition:fly={{ x: -100, duration: 350 }}
class="fixed bottom-10 left-2 z-[10000] max-h-[270px] w-[315px] rounded-2xl border bg-immich-bg p-4 text-sm shadow-sm" class="fixed bottom-10 left-2 z-[10000] max-h-[270px] w-[315px] rounded-2xl border bg-immich-bg p-4 text-sm shadow-sm"
> >
<p class="mb-2 text-xs text-gray-500">{$t('downloading').toUpperCase()}</p> <p class="mb-2 text-xs text-gray-500">{$t('downloading').toUpperCase()}</p>
<div class="my-2 mb-2 flex max-h-[200px] flex-col overflow-y-auto text-sm"> <div class="my-2 mb-2 flex max-h-[200px] flex-col overflow-y-auto text-sm">
{#each Object.keys(downloadStore.assets) as downloadKey (downloadKey)} {#each Object.keys(downloadManager.assets) as downloadKey (downloadKey)}
{@const download = downloadStore.assets[downloadKey]} {@const download = downloadManager.assets[downloadKey]}
<div class="mb-2 flex place-items-center" transition:slide> <div class="mb-2 flex place-items-center" transition:slide>
<div class="w-full pr-10"> <div class="w-full pr-10">
<div class="flex place-items-center justify-between gap-2 text-xs font-medium"> <div class="flex place-items-center justify-between gap-2 text-xs font-medium">

View File

@ -2,7 +2,6 @@
import ImageThumbnail from '$lib/components/assets/thumbnail/image-thumbnail.svelte'; import ImageThumbnail from '$lib/components/assets/thumbnail/image-thumbnail.svelte';
import { dialogController } from '$lib/components/shared-components/dialog/dialog'; import { dialogController } from '$lib/components/shared-components/dialog/dialog';
import { notificationController } from '$lib/components/shared-components/notification/notification'; import { notificationController } from '$lib/components/shared-components/notification/notification';
import { isFaceEditMode } from '$lib/stores/face-edit.svelte';
import { getPeopleThumbnailUrl } from '$lib/utils'; import { getPeopleThumbnailUrl } from '$lib/utils';
import { getAllPeople, createFace, type PersonResponseDto } from '@immich/sdk'; import { getAllPeople, createFace, type PersonResponseDto } from '@immich/sdk';
import { Button, Input } from '@immich/ui'; import { Button, Input } from '@immich/ui';
@ -10,6 +9,7 @@
import { onMount } from 'svelte'; import { onMount } from 'svelte';
import { assetViewingStore } from '$lib/stores/asset-viewing.store'; import { assetViewingStore } from '$lib/stores/asset-viewing.store';
import { handleError } from '$lib/utils/handle-error'; import { handleError } from '$lib/utils/handle-error';
import { faceManager } from '$lib/managers/face.manager.svelte';
interface Props { interface Props {
htmlElement: HTMLImageElement | HTMLVideoElement; htmlElement: HTMLImageElement | HTMLVideoElement;
@ -140,7 +140,7 @@
}; };
const cancel = () => { const cancel = () => {
isFaceEditMode.value = false; faceManager.isEditMode = false;
}; };
const getPeople = async () => { const getPeople = async () => {
@ -303,7 +303,7 @@
} catch (error) { } catch (error) {
handleError(error, 'Error tagging face'); handleError(error, 'Error tagging face');
} finally { } finally {
isFaceEditMode.value = false; faceManager.isEditMode = false;
} }
}; };
</script> </script>

View File

@ -20,7 +20,7 @@
import { handleError } from '$lib/utils/handle-error'; import { handleError } from '$lib/utils/handle-error';
import FaceEditor from '$lib/components/asset-viewer/face-editor/face-editor.svelte'; import FaceEditor from '$lib/components/asset-viewer/face-editor/face-editor.svelte';
import { photoViewerImgElement } from '$lib/stores/assets-store.svelte'; import { photoViewerImgElement } from '$lib/stores/assets-store.svelte';
import { isFaceEditMode } from '$lib/stores/face-edit.svelte'; import { faceManager } from '$lib/managers/face.manager.svelte';
interface Props { interface Props {
asset: AssetResponseDto; asset: AssetResponseDto;
@ -109,7 +109,7 @@
}; };
$effect(() => { $effect(() => {
if (isFaceEditMode.value && $photoZoomState.currentZoom > 1) { if (faceManager.isEditMode && $photoZoomState.currentZoom > 1) {
zoomToggle(); zoomToggle();
} }
}); });
@ -235,7 +235,7 @@
{/each} {/each}
</div> </div>
{#if isFaceEditMode.value} {#if faceManager.isEditMode}
<FaceEditor htmlElement={$photoViewerImgElement} {containerWidth} {containerHeight} assetId={asset.id} /> <FaceEditor htmlElement={$photoViewerImgElement} {containerWidth} {containerHeight} assetId={asset.id} />
{/if} {/if}
{/if} {/if}

View File

@ -9,8 +9,8 @@
import type { SwipeCustomEvent } from 'svelte-gestures'; import type { SwipeCustomEvent } from 'svelte-gestures';
import { fade } from 'svelte/transition'; import { fade } from 'svelte/transition';
import { t } from 'svelte-i18n'; import { t } from 'svelte-i18n';
import { isFaceEditMode } from '$lib/stores/face-edit.svelte';
import FaceEditor from '$lib/components/asset-viewer/face-editor/face-editor.svelte'; import FaceEditor from '$lib/components/asset-viewer/face-editor/face-editor.svelte';
import { faceManager } from '$lib/managers/face.manager.svelte';
interface Props { interface Props {
assetId: string; assetId: string;
@ -94,7 +94,7 @@
let containerHeight = $state(0); let containerHeight = $state(0);
$effect(() => { $effect(() => {
if (isFaceEditMode.value) { if (faceManager.isEditMode) {
videoPlayer?.pause(); videoPlayer?.pause();
} }
}); });
@ -141,7 +141,7 @@
</div> </div>
{/if} {/if}
{#if isFaceEditMode.value} {#if faceManager.isEditMode}
<FaceEditor htmlElement={videoPlayer} {containerWidth} {containerHeight} {assetId} /> <FaceEditor htmlElement={videoPlayer} {containerWidth} {containerHeight} {assetId} />
{/if} {/if}
</div> </div>

View File

@ -2,7 +2,6 @@
import { goto } from '$app/navigation'; import { goto } from '$app/navigation';
import type { Action } from '$lib/components/asset-viewer/actions/action'; import type { Action } from '$lib/components/asset-viewer/actions/action';
import { AppRoute, AssetAction } from '$lib/constants'; import { AppRoute, AssetAction } from '$lib/constants';
import { dragAndDropFilesStore } from '$lib/stores/drag-and-drop-files.store';
import { getKey, handlePromiseError } from '$lib/utils'; import { getKey, handlePromiseError } from '$lib/utils';
import { downloadArchive } from '$lib/utils/asset-utils'; import { downloadArchive } from '$lib/utils/asset-utils';
import { fileUploadHandler, openFileUploadDialog } from '$lib/utils/file-uploader'; import { fileUploadHandler, openFileUploadDialog } from '$lib/utils/file-uploader';
@ -22,6 +21,7 @@
import type { Viewport } from '$lib/stores/assets-store.svelte'; import type { Viewport } from '$lib/stores/assets-store.svelte';
import { t } from 'svelte-i18n'; import { t } from 'svelte-i18n';
import { AssetInteraction } from '$lib/stores/asset-interaction.svelte'; import { AssetInteraction } from '$lib/stores/asset-interaction.svelte';
import { dragAndDropManager } from '$lib/managers/drag-and-drop.manager.svelte';
interface Props { interface Props {
sharedLink: SharedLinkResponseDto; sharedLink: SharedLinkResponseDto;
@ -35,10 +35,10 @@
let assets = $derived(sharedLink.assets); let assets = $derived(sharedLink.assets);
dragAndDropFilesStore.subscribe((value) => { $effect(() => {
if (value.isDragging && value.files.length > 0) { if (dragAndDropManager.isDragging && dragAndDropManager.files.length > 0) {
handlePromiseError(handleUploadAssets(value.files)); handlePromiseError(handleUploadAssets(dragAndDropManager.files));
dragAndDropFilesStore.set({ isDragging: false, files: [] }); dragAndDropManager.reset();
} }
}); });

View File

@ -6,7 +6,7 @@
type Padding, type Padding,
} from '$lib/components/elements/buttons/circle-icon-button.svelte'; } from '$lib/components/elements/buttons/circle-icon-button.svelte';
import ContextMenu from '$lib/components/shared-components/context-menu/context-menu.svelte'; import ContextMenu from '$lib/components/shared-components/context-menu/context-menu.svelte';
import { optionClickCallbackStore, selectedIdStore } from '$lib/stores/context-menu.store'; import { contextMenuManager } from '$lib/managers/context-menu.manager.svelte';
import { import {
getContextMenuPositionFromBoundingRect, getContextMenuPositionFromBoundingRect,
getContextMenuPositionFromEvent, getContextMenuPositionFromEvent,
@ -97,7 +97,7 @@
} }
focusButton(); focusButton();
isOpen = false; isOpen = false;
$selectedIdStore = undefined; contextMenuManager.selectedId = undefined;
}; };
const handleOptionClick = () => { const handleOptionClick = () => {
@ -124,7 +124,7 @@
$effect(() => { $effect(() => {
if (isOpen) { if (isOpen) {
$optionClickCallbackStore = handleOptionClick; contextMenuManager.optionClickCallback = handleOptionClick;
} }
}); });
</script> </script>
@ -139,8 +139,8 @@
isOpen, isOpen,
onEscape, onEscape,
openDropdown, openDropdown,
selectedId: $selectedIdStore, selectedId: contextMenuManager.selectedId,
selectionChanged: (id) => ($selectedIdStore = id), selectionChanged: (id) => (contextMenuManager.selectedId = id),
}} }}
onresize={onResize} onresize={onResize}
{...restProps} {...restProps}
@ -178,7 +178,7 @@
<ContextMenu <ContextMenu
{...contextMenuPosition} {...contextMenuPosition}
{direction} {direction}
ariaActiveDescendant={$selectedIdStore} ariaActiveDescendant={contextMenuManager.selectedId}
ariaLabelledBy={buttonId} ariaLabelledBy={buttonId}
bind:menuElement={menuContainer} bind:menuElement={menuContainer}
id={menuId} id={menuId}

View File

@ -1,9 +1,9 @@
<script lang="ts"> <script lang="ts">
import Icon from '$lib/components/elements/icon.svelte'; import Icon from '$lib/components/elements/icon.svelte';
import { generateId } from '$lib/utils/generate-id'; import { generateId } from '$lib/utils/generate-id';
import { optionClickCallbackStore, selectedIdStore } from '$lib/stores/context-menu.store';
import type { Shortcut } from '$lib/actions/shortcut'; import type { Shortcut } from '$lib/actions/shortcut';
import { shortcutLabel as computeShortcutLabel, shortcut as bindShortcut } from '$lib/actions/shortcut'; import { shortcutLabel as computeShortcutLabel, shortcut as bindShortcut } from '$lib/actions/shortcut';
import { contextMenuManager } from '$lib/managers/context-menu.manager.svelte';
interface Props { interface Props {
text: string; text: string;
@ -29,10 +29,10 @@
let id: string = generateId(); let id: string = generateId();
let isActive = $derived($selectedIdStore === id); let isActive = $derived(contextMenuManager.selectedId === id);
const handleClick = () => { const handleClick = () => {
$optionClickCallbackStore?.(); contextMenuManager.optionClickCallback?.();
onClick(); onClick();
}; };
@ -51,8 +51,8 @@
<li <li
{id} {id}
onclick={handleClick} onclick={handleClick}
onmouseover={() => ($selectedIdStore = id)} onmouseover={() => (contextMenuManager.selectedId = id)}
onmouseleave={() => ($selectedIdStore = undefined)} onmouseleave={() => (contextMenuManager.selectedId = undefined)}
class="w-full p-4 text-left text-sm font-medium {textColor} focus:outline-none focus:ring-2 focus:ring-inset cursor-pointer border-gray-200 flex gap-2 items-center {isActive class="w-full p-4 text-left text-sm font-medium {textColor} focus:outline-none focus:ring-2 focus:ring-inset cursor-pointer border-gray-200 flex gap-2 items-center {isActive
? activeColor ? activeColor
: 'bg-slate-100'}" : 'bg-slate-100'}"

View File

@ -4,7 +4,7 @@
import { shortcuts } from '$lib/actions/shortcut'; import { shortcuts } from '$lib/actions/shortcut';
import { generateId } from '$lib/utils/generate-id'; import { generateId } from '$lib/utils/generate-id';
import { contextMenuNavigation } from '$lib/actions/context-menu-navigation'; import { contextMenuNavigation } from '$lib/actions/context-menu-navigation';
import { optionClickCallbackStore, selectedIdStore } from '$lib/stores/context-menu.store'; import { contextMenuManager } from '$lib/managers/context-menu.manager.svelte';
interface Props { interface Props {
title: string; title: string;
@ -60,7 +60,7 @@
if (isOpen && menuContainer) { if (isOpen && menuContainer) {
triggerElement = document.activeElement as HTMLElement; triggerElement = document.activeElement as HTMLElement;
menuContainer.focus(); menuContainer.focus();
$optionClickCallbackStore = closeContextMenu; contextMenuManager.optionClickCallback = closeContextMenu;
} }
}); });
@ -77,8 +77,8 @@
closeDropdown: closeContextMenu, closeDropdown: closeContextMenu,
container: menuContainer, container: menuContainer,
isOpen, isOpen,
selectedId: $selectedIdStore, selectedId: contextMenuManager.selectedId,
selectionChanged: (id) => ($selectedIdStore = id), selectionChanged: (id) => (contextMenuManager.selectedId = id),
}} }}
use:shortcuts={[ use:shortcuts={[
{ {
@ -96,7 +96,7 @@
{direction} {direction}
{x} {x}
{y} {y}
ariaActiveDescendant={$selectedIdStore} ariaActiveDescendant={contextMenuManager.selectedId}
ariaLabel={title} ariaLabel={title}
bind:menuElement={menuContainer} bind:menuElement={menuContainer}
id={menuId} id={menuId}

View File

@ -1,7 +1,7 @@
<script lang="ts"> <script lang="ts">
import { page } from '$app/state'; import { page } from '$app/state';
import { shouldIgnoreEvent } from '$lib/actions/shortcut'; import { shouldIgnoreEvent } from '$lib/actions/shortcut';
import { dragAndDropFilesStore } from '$lib/stores/drag-and-drop-files.store'; import { dragAndDropManager } from '$lib/managers/drag-and-drop.manager.svelte';
import { fileUploadHandler } from '$lib/utils/file-uploader'; import { fileUploadHandler } from '$lib/utils/file-uploader';
import { isAlbumsRoute, isSharedLinkRoute } from '$lib/utils/navigation'; import { isAlbumsRoute, isSharedLinkRoute } from '$lib/utils/navigation';
import { t } from 'svelte-i18n'; import { t } from 'svelte-i18n';
@ -124,7 +124,8 @@
const filesArray: File[] = Array.from<File>(files); const filesArray: File[] = Array.from<File>(files);
if (isShare) { if (isShare) {
dragAndDropFilesStore.set({ isDragging: true, files: filesArray }); dragAndDropManager.isDragging = true;
dragAndDropManager.files = filesArray;
} else { } else {
await fileUploadHandler(filesArray, albumId); await fileUploadHandler(filesArray, albumId);
} }

View File

@ -10,7 +10,7 @@
import ImmichLogo from '$lib/components/shared-components/immich-logo.svelte'; import ImmichLogo from '$lib/components/shared-components/immich-logo.svelte';
import SearchBar from '$lib/components/shared-components/search-bar/search-bar.svelte'; import SearchBar from '$lib/components/shared-components/search-bar/search-bar.svelte';
import { AppRoute } from '$lib/constants'; import { AppRoute } from '$lib/constants';
import { authManager } from '$lib/stores/auth-manager.svelte'; import { authManager } from '$lib/managers/auth-manager.svelte';
import { mobileDevice } from '$lib/stores/mobile-device.svelte'; import { mobileDevice } from '$lib/stores/mobile-device.svelte';
import { featureFlags } from '$lib/stores/server-config.store'; import { featureFlags } from '$lib/stores/server-config.store';
import { sidebarStore } from '$lib/stores/sidebar.svelte'; import { sidebarStore } from '$lib/stores/sidebar.svelte';

View File

@ -0,0 +1,17 @@
class ActivityManager {
#numberOfComments = $state<number>(0);
get numberOfComments() {
return this.#numberOfComments;
}
set numberOfComments(number: number) {
this.#numberOfComments = number;
}
updateNumberOfComments(addOrRemove: 1 | -1) {
this.#numberOfComments += addOrRemove;
}
}
export const activityManager = new ActivityManager();

View File

@ -1,6 +1,6 @@
import { goto } from '$app/navigation'; import { goto } from '$app/navigation';
import { AppRoute } from '$lib/constants'; import { AppRoute } from '$lib/constants';
import { eventManager } from '$lib/stores/event-manager.svelte'; import { eventManager } from '$lib/managers/event.manager.svelte';
import { logout } from '@immich/sdk'; import { logout } from '@immich/sdk';
class AuthManager { class AuthManager {

View File

@ -0,0 +1,22 @@
class ContextMenuManager {
#selectedId = $state<string | undefined>(undefined);
#optionClickCallback = $state<(() => void) | undefined>(undefined);
get selectedId() {
return this.#selectedId;
}
set selectedId(id: string | undefined) {
this.#selectedId = id;
}
get optionClickCallback() {
return this.#optionClickCallback;
}
set optionClickCallback(callback: (() => void) | undefined) {
this.#optionClickCallback = callback;
}
}
export const contextMenuManager = new ContextMenuManager();

View File

@ -0,0 +1,47 @@
export interface DownloadProgress {
progress: number;
total: number;
percentage: number;
abort: AbortController | null;
}
class DownloadManager {
#assets = $state<Record<string, DownloadProgress>>({});
#isDownloading = $derived(Object.keys(this.#assets).length > 0);
#update(key: string, value: Partial<DownloadProgress>) {
if (!this.#assets[key]) {
this.#assets[key] = { progress: 0, total: 0, percentage: 0, abort: null };
}
const item = this.#assets[key];
Object.assign(item, value);
item.percentage = Math.min(Math.floor((item.progress / item.total) * 100), 100);
}
get assets() {
return this.#assets;
}
get isDownloading() {
return this.#isDownloading;
}
add(key: string, total: number, abort?: AbortController) {
this.#update(key, { total, abort });
}
clear(key: string) {
delete this.#assets[key];
}
update(key: string, progress: number, total?: number) {
const download: Partial<DownloadProgress> = { progress };
if (total !== undefined) {
download.total = total;
}
this.#update(key, download);
}
}
export const downloadManager = new DownloadManager();

View File

@ -0,0 +1,27 @@
class DragAndDropManager {
#isDragging = $state<boolean>(false);
#files = $state<File[]>([]);
get isDragging() {
return this.#isDragging;
}
get files() {
return this.#files;
}
set isDragging(isDragging: boolean) {
this.#isDragging = isDragging;
}
set files(files: File[]) {
this.#files = files;
}
reset() {
this.#isDragging = false;
this.#files = [];
}
}
export const dragAndDropManager = new DragAndDropManager();

View File

@ -0,0 +1,13 @@
class FaceManager {
#isEditMode = $state(false);
get isEditMode() {
return this.#isEditMode;
}
set isEditMode(isEditMode: boolean) {
this.#isEditMode = isEditMode;
}
}
export const faceManager = new FaceManager();

View File

@ -1,11 +0,0 @@
import { writable } from 'svelte/store';
export const numberOfComments = writable<number>(0);
export const setNumberOfComments = (number: number) => {
numberOfComments.set(number);
};
export const updateNumberOfComments = (addOrRemove: 1 | -1) => {
numberOfComments.update((n) => n + addOrRemove);
};

View File

@ -1,6 +0,0 @@
import { writable } from 'svelte/store';
const selectedIdStore = writable<string | undefined>(undefined);
const optionClickCallbackStore = writable<(() => void) | undefined>(undefined);
export { optionClickCallbackStore, selectedIdStore };

View File

@ -1,51 +0,0 @@
export interface DownloadProgress {
progress: number;
total: number;
percentage: number;
abort: AbortController | null;
}
class DownloadStore {
assets = $state<Record<string, DownloadProgress>>({});
isDownloading = $derived(Object.keys(this.assets).length > 0);
#update(key: string, value: Partial<DownloadProgress> | null) {
if (value === null) {
delete this.assets[key];
return;
}
if (!this.assets[key]) {
this.assets[key] = { progress: 0, total: 0, percentage: 0, abort: null };
}
const item = this.assets[key];
Object.assign(item, value);
item.percentage = Math.min(Math.floor((item.progress / item.total) * 100), 100);
}
add(key: string, total: number, abort?: AbortController) {
this.#update(key, { total, abort });
}
clear(key: string) {
this.#update(key, null);
}
update(key: string, progress: number, total?: number) {
const download: Partial<DownloadProgress> = { progress };
if (total !== undefined) {
download.total = total;
}
this.#update(key, download);
}
}
export const downloadStore = new DownloadStore();
export const downloadManager = {
add: (key: string, total: number, abort?: AbortController) => downloadStore.add(key, total, abort),
clear: (key: string) => downloadStore.clear(key),
update: (key: string, progress: number, total?: number) => downloadStore.update(key, progress, total),
};

View File

@ -1,7 +0,0 @@
//store to track the state of the drag and drop and the files
import { writable } from 'svelte/store';
export const dragAndDropFilesStore = writable({
isDragging: false as boolean,
files: [] as File[],
});

View File

@ -1 +0,0 @@
export const isFaceEditMode = $state({ value: false });

View File

@ -1,4 +1,4 @@
import { eventManager } from '$lib/stores/event-manager.svelte'; import { eventManager } from '$lib/managers/event.manager.svelte';
import { import {
getAssetsByOriginalPath, getAssetsByOriginalPath,
getUniqueOriginalPaths, getUniqueOriginalPaths,

View File

@ -1,4 +1,4 @@
import { eventManager } from '$lib/stores/event-manager.svelte'; import { eventManager } from '$lib/managers/event.manager.svelte';
import { asLocalTimeISO } from '$lib/utils/date-time'; import { asLocalTimeISO } from '$lib/utils/date-time';
import { import {
type AssetResponseDto, type AssetResponseDto,

View File

@ -1,4 +1,4 @@
import { eventManager } from '$lib/stores/event-manager.svelte'; import { eventManager } from '$lib/managers/event.manager.svelte';
class SearchStore { class SearchStore {
savedSearchTerms = $state<string[]>([]); savedSearchTerms = $state<string[]>([]);

View File

@ -1,4 +1,4 @@
import { eventManager } from '$lib/stores/event-manager.svelte'; import { eventManager } from '$lib/managers/event.manager.svelte';
import { purchaseStore } from '$lib/stores/purchase.store'; import { purchaseStore } from '$lib/stores/purchase.store';
import { type UserAdminResponseDto, type UserPreferencesResponseDto } from '@immich/sdk'; import { type UserAdminResponseDto, type UserPreferencesResponseDto } from '@immich/sdk';
import { writable } from 'svelte/store'; import { writable } from 'svelte/store';

View File

@ -1,4 +1,4 @@
import { eventManager } from '$lib/stores/event-manager.svelte'; import { eventManager } from '$lib/managers/event.manager.svelte';
import type { import type {
AlbumResponseDto, AlbumResponseDto,
ServerAboutResponseDto, ServerAboutResponseDto,

View File

@ -1,4 +1,4 @@
import { authManager } from '$lib/stores/auth-manager.svelte'; import { authManager } from '$lib/managers/auth-manager.svelte';
import { createEventEmitter } from '$lib/utils/eventemitter'; import { createEventEmitter } from '$lib/utils/eventemitter';
import type { AssetResponseDto, ServerVersionResponseDto } from '@immich/sdk'; import type { AssetResponseDto, ServerVersionResponseDto } from '@immich/sdk';
import { io, type Socket } from 'socket.io-client'; import { io, type Socket } from 'socket.io-client';

View File

@ -3,9 +3,9 @@ import FormatBoldMessage from '$lib/components/i18n/format-bold-message.svelte';
import type { InterpolationValues } from '$lib/components/i18n/format-message'; import type { InterpolationValues } from '$lib/components/i18n/format-message';
import { NotificationType, notificationController } from '$lib/components/shared-components/notification/notification'; import { NotificationType, notificationController } from '$lib/components/shared-components/notification/notification';
import { AppRoute } from '$lib/constants'; import { AppRoute } from '$lib/constants';
import { downloadManager } from '$lib/managers/download.manager.svelte';
import type { AssetInteraction } from '$lib/stores/asset-interaction.svelte'; import type { AssetInteraction } from '$lib/stores/asset-interaction.svelte';
import { assetsSnapshot, isSelectingAllAssets, type AssetStore } from '$lib/stores/assets-store.svelte'; import { assetsSnapshot, isSelectingAllAssets, type AssetStore } from '$lib/stores/assets-store.svelte';
import { downloadManager } from '$lib/stores/download-store.svelte';
import { preferences } from '$lib/stores/user.store'; import { preferences } from '$lib/stores/user.store';
import { downloadRequest, getKey, withError } from '$lib/utils'; import { downloadRequest, getKey, withError } from '$lib/utils';
import { createAlbum } from '$lib/utils/album-utils'; import { createAlbum } from '$lib/utils/album-utils';

View File

@ -34,7 +34,6 @@
} from '$lib/components/shared-components/notification/notification'; } from '$lib/components/shared-components/notification/notification';
import UserAvatar from '$lib/components/shared-components/user-avatar.svelte'; import UserAvatar from '$lib/components/shared-components/user-avatar.svelte';
import { AppRoute, AlbumPageViewMode } from '$lib/constants'; import { AppRoute, AlbumPageViewMode } from '$lib/constants';
import { numberOfComments, setNumberOfComments, updateNumberOfComments } from '$lib/stores/activity.store';
import { assetViewingStore } from '$lib/stores/asset-viewing.store'; import { assetViewingStore } from '$lib/stores/asset-viewing.store';
import { AssetStore } from '$lib/stores/assets-store.svelte'; import { AssetStore } from '$lib/stores/assets-store.svelte';
import { SlideshowNavigation, SlideshowState, slideshowStore } from '$lib/stores/slideshow.store'; import { SlideshowNavigation, SlideshowState, slideshowStore } from '$lib/stores/slideshow.store';
@ -87,6 +86,7 @@
import { confirmAlbumDelete } from '$lib/utils/album-utils'; import { confirmAlbumDelete } from '$lib/utils/album-utils';
import TagAction from '$lib/components/photos-page/actions/tag-action.svelte'; import TagAction from '$lib/components/photos-page/actions/tag-action.svelte';
import { AssetInteraction } from '$lib/stores/asset-interaction.svelte'; import { AssetInteraction } from '$lib/stores/asset-interaction.svelte';
import { activityManager } from '$lib/managers/activity.manager.svelte';
interface Props { interface Props {
data: PageData; data: PageData;
@ -191,7 +191,7 @@
const getNumberOfComments = async () => { const getNumberOfComments = async () => {
try { try {
const { comments } = await getActivityStatistics({ albumId: album.id }); const { comments } = await getActivityStatistics({ albumId: album.id });
setNumberOfComments(comments); activityManager.numberOfComments = comments;
} catch (error) { } catch (error) {
handleError(error, $t('errors.cant_get_number_of_comments')); handleError(error, $t('errors.cant_get_number_of_comments'));
} }
@ -398,7 +398,7 @@
let albumId = $derived(album.id); let albumId = $derived(album.id);
$effect(() => { $effect(() => {
if (!album.isActivityEnabled && $numberOfComments === 0) { if (!album.isActivityEnabled && activityManager.numberOfComments === 0) {
isShowActivity = false; isShowActivity = false;
} }
}); });
@ -420,7 +420,9 @@
let isOwned = $derived($user.id == album.ownerId); let isOwned = $derived($user.id == album.ownerId);
let showActivityStatus = $derived( let showActivityStatus = $derived(
album.albumUsers.length > 0 && !$showAssetViewer && (album.isActivityEnabled || $numberOfComments > 0), album.albumUsers.length > 0 &&
!$showAssetViewer &&
(album.isActivityEnabled || activityManager.numberOfComments > 0),
); );
let isEditor = $derived( let isEditor = $derived(
album.albumUsers.find(({ user: { id } }) => id === $user.id)?.role === AlbumUserRole.Editor || album.albumUsers.find(({ user: { id } }) => id === $user.id)?.role === AlbumUserRole.Editor ||
@ -712,7 +714,7 @@
<ActivityStatus <ActivityStatus
disabled={!album.isActivityEnabled} disabled={!album.isActivityEnabled}
{isLiked} {isLiked}
numberOfComments={$numberOfComments} numberOfComments={activityManager.numberOfComments}
onFavorite={handleFavorite} onFavorite={handleFavorite}
onOpenActivityTab={handleOpenAndCloseActivityTab} onOpenActivityTab={handleOpenAndCloseActivityTab}
/> />
@ -735,8 +737,8 @@
albumId={album.id} albumId={album.id}
{isLiked} {isLiked}
bind:reactions bind:reactions
onAddComment={() => updateNumberOfComments(1)} onAddComment={() => activityManager.updateNumberOfComments(1)}
onDeleteComment={() => updateNumberOfComments(-1)} onDeleteComment={() => activityManager.updateNumberOfComments(-1)}
onDeleteLike={() => (isLiked = null)} onDeleteLike={() => (isLiked = null)}
onClose={handleOpenAndCloseActivityTab} onClose={handleOpenAndCloseActivityTab}
/> />

View File

@ -20,10 +20,10 @@
import ButtonContextMenu from '$lib/components/shared-components/context-menu/button-context-menu.svelte'; import ButtonContextMenu from '$lib/components/shared-components/context-menu/button-context-menu.svelte';
import EmptyPlaceholder from '$lib/components/shared-components/empty-placeholder.svelte'; import EmptyPlaceholder from '$lib/components/shared-components/empty-placeholder.svelte';
import { AssetAction } from '$lib/constants'; import { AssetAction } from '$lib/constants';
import { faceManager } from '$lib/managers/face.manager.svelte';
import { AssetInteraction } from '$lib/stores/asset-interaction.svelte'; import { AssetInteraction } from '$lib/stores/asset-interaction.svelte';
import { assetViewingStore } from '$lib/stores/asset-viewing.store'; import { assetViewingStore } from '$lib/stores/asset-viewing.store';
import { AssetStore } from '$lib/stores/assets-store.svelte'; import { AssetStore } from '$lib/stores/assets-store.svelte';
import { isFaceEditMode } from '$lib/stores/face-edit.svelte';
import { preferences, user } from '$lib/stores/user.store'; import { preferences, user } from '$lib/stores/user.store';
import { import {
updateStackedAssetInTimeline, updateStackedAssetInTimeline,
@ -76,7 +76,7 @@
}; };
beforeNavigate(() => { beforeNavigate(() => {
isFaceEditMode.value = false; faceManager.isEditMode = false;
}); });
</script> </script>

View File

@ -7,7 +7,6 @@
NotificationType, NotificationType,
notificationController, notificationController,
} from '$lib/components/shared-components/notification/notification'; } from '$lib/components/shared-components/notification/notification';
import { downloadManager } from '$lib/stores/download-store.svelte';
import { locale } from '$lib/stores/preferences.store'; import { locale } from '$lib/stores/preferences.store';
import { copyToClipboard } from '$lib/utils'; import { copyToClipboard } from '$lib/utils';
import { downloadBlob } from '$lib/utils/asset-utils'; import { downloadBlob } from '$lib/utils/asset-utils';
@ -17,6 +16,7 @@
import { mdiCheckAll, mdiContentCopy, mdiDownload, mdiRefresh, mdiWrench } from '@mdi/js'; import { mdiCheckAll, mdiContentCopy, mdiDownload, mdiRefresh, mdiWrench } from '@mdi/js';
import { t } from 'svelte-i18n'; import { t } from 'svelte-i18n';
import type { PageData } from './$types'; import type { PageData } from './$types';
import { downloadManager } from '$lib/managers/download.manager.svelte';
interface Props { interface Props {
data: PageData; data: PageData;

View File

@ -22,7 +22,6 @@
import SettingAccordionState from '$lib/components/shared-components/settings/setting-accordion-state.svelte'; import SettingAccordionState from '$lib/components/shared-components/settings/setting-accordion-state.svelte';
import SettingAccordion from '$lib/components/shared-components/settings/setting-accordion.svelte'; import SettingAccordion from '$lib/components/shared-components/settings/setting-accordion.svelte';
import { QueryParameter } from '$lib/constants'; import { QueryParameter } from '$lib/constants';
import { downloadManager } from '$lib/stores/download-store.svelte';
import { featureFlags } from '$lib/stores/server-config.store'; import { featureFlags } from '$lib/stores/server-config.store';
import { copyToClipboard } from '$lib/utils'; import { copyToClipboard } from '$lib/utils';
import { downloadBlob } from '$lib/utils/asset-utils'; import { downloadBlob } from '$lib/utils/asset-utils';
@ -53,6 +52,7 @@
import type { Component } from 'svelte'; import type { Component } from 'svelte';
import type { SettingsComponentProps } from '$lib/components/admin-page/settings/admin-settings'; import type { SettingsComponentProps } from '$lib/components/admin-page/settings/admin-settings';
import SearchBar from '$lib/components/elements/search-bar.svelte'; import SearchBar from '$lib/components/elements/search-bar.svelte';
import { downloadManager } from '$lib/managers/download.manager.svelte';
interface Props { interface Props {
data: PageData; data: PageData;

View File

@ -1,6 +1,6 @@
<script lang="ts"> <script lang="ts">
import AuthPageLayout from '$lib/components/layouts/AuthPageLayout.svelte'; import AuthPageLayout from '$lib/components/layouts/AuthPageLayout.svelte';
import { authManager } from '$lib/stores/auth-manager.svelte'; import { authManager } from '$lib/managers/auth-manager.svelte';
import { user } from '$lib/stores/user.store'; import { user } from '$lib/stores/user.store';
import { updateMyUser } from '@immich/sdk'; import { updateMyUser } from '@immich/sdk';
import { Alert, Button, Field, HelperText, PasswordInput, Stack, Text } from '@immich/ui'; import { Alert, Button, Field, HelperText, PasswordInput, Stack, Text } from '@immich/ui';