mirror of
https://github.com/samsonjs/immich.git
synced 2026-04-27 15:07:45 +00:00
refactor: favorite action (#25121)
This commit is contained in:
parent
78229baeab
commit
5bb3492616
17 changed files with 111 additions and 102 deletions
|
|
@ -726,8 +726,8 @@ importers:
|
||||||
specifier: file:../open-api/typescript-sdk
|
specifier: file:../open-api/typescript-sdk
|
||||||
version: link:../open-api/typescript-sdk
|
version: link:../open-api/typescript-sdk
|
||||||
'@immich/ui':
|
'@immich/ui':
|
||||||
specifier: ^0.52.0
|
specifier: ^0.53.3
|
||||||
version: 0.52.0(@sveltejs/kit@2.49.2(@opentelemetry/api@1.9.0)(@sveltejs/vite-plugin-svelte@6.2.1(svelte@5.46.1)(vite@7.3.0(@types/node@25.0.3)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(yaml@2.8.2)))(svelte@5.46.1)(vite@7.3.0(@types/node@25.0.3)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(yaml@2.8.2)))(svelte@5.46.1)
|
version: 0.53.3(@sveltejs/kit@2.49.2(@opentelemetry/api@1.9.0)(@sveltejs/vite-plugin-svelte@6.2.1(svelte@5.46.1)(vite@7.3.0(@types/node@25.0.3)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(yaml@2.8.2)))(svelte@5.46.1)(vite@7.3.0(@types/node@25.0.3)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(yaml@2.8.2)))(svelte@5.46.1)
|
||||||
'@mapbox/mapbox-gl-rtl-text':
|
'@mapbox/mapbox-gl-rtl-text':
|
||||||
specifier: 0.2.3
|
specifier: 0.2.3
|
||||||
version: 0.2.3(mapbox-gl@1.13.3)
|
version: 0.2.3(mapbox-gl@1.13.3)
|
||||||
|
|
@ -3075,8 +3075,8 @@ packages:
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
svelte: ^5.0.0
|
svelte: ^5.0.0
|
||||||
|
|
||||||
'@immich/ui@0.52.0':
|
'@immich/ui@0.53.3':
|
||||||
resolution: {integrity: sha512-ECQIE5qYNpe7Q5+hifIGUDaRQXBkPOp9dvZaHELWWzAGIhbwG+mUYwMpUgU2TO7fV5u8XU6nHyBuC055zApiWQ==}
|
resolution: {integrity: sha512-Ax7ctU9KIZgET58+PoMQnf1XDOIH76Xa341TXDfLwF96F3fQZ/v4TA7Ycb6hmTwIYGU9arIgqGqQDbuuNxc2vA==}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
svelte: ^5.0.0
|
svelte: ^5.0.0
|
||||||
|
|
||||||
|
|
@ -15078,7 +15078,7 @@ snapshots:
|
||||||
dependencies:
|
dependencies:
|
||||||
svelte: 5.46.1
|
svelte: 5.46.1
|
||||||
|
|
||||||
'@immich/ui@0.52.0(@sveltejs/kit@2.49.2(@opentelemetry/api@1.9.0)(@sveltejs/vite-plugin-svelte@6.2.1(svelte@5.46.1)(vite@7.3.0(@types/node@25.0.3)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(yaml@2.8.2)))(svelte@5.46.1)(vite@7.3.0(@types/node@25.0.3)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(yaml@2.8.2)))(svelte@5.46.1)':
|
'@immich/ui@0.53.3(@sveltejs/kit@2.49.2(@opentelemetry/api@1.9.0)(@sveltejs/vite-plugin-svelte@6.2.1(svelte@5.46.1)(vite@7.3.0(@types/node@25.0.3)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(yaml@2.8.2)))(svelte@5.46.1)(vite@7.3.0(@types/node@25.0.3)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(yaml@2.8.2)))(svelte@5.46.1)':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@immich/svelte-markdown-preprocess': 0.1.0(svelte@5.46.1)
|
'@immich/svelte-markdown-preprocess': 0.1.0(svelte@5.46.1)
|
||||||
'@internationalized/date': 3.10.0
|
'@internationalized/date': 3.10.0
|
||||||
|
|
|
||||||
|
|
@ -27,7 +27,7 @@
|
||||||
"@formatjs/icu-messageformat-parser": "^3.0.0",
|
"@formatjs/icu-messageformat-parser": "^3.0.0",
|
||||||
"@immich/justified-layout-wasm": "^0.4.3",
|
"@immich/justified-layout-wasm": "^0.4.3",
|
||||||
"@immich/sdk": "file:../open-api/typescript-sdk",
|
"@immich/sdk": "file:../open-api/typescript-sdk",
|
||||||
"@immich/ui": "^0.52.0",
|
"@immich/ui": "^0.53.3",
|
||||||
"@mapbox/mapbox-gl-rtl-text": "0.2.3",
|
"@mapbox/mapbox-gl-rtl-text": "0.2.3",
|
||||||
"@mdi/js": "^7.4.47",
|
"@mdi/js": "^7.4.47",
|
||||||
"@photo-sphere-viewer/core": "^5.14.0",
|
"@photo-sphere-viewer/core": "^5.14.0",
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,6 @@
|
||||||
const { title, icon, color = 'secondary', onAction } = $derived(action);
|
const { title, icon, color = 'secondary', onAction } = $derived(action);
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
{#if action.$if?.() ?? true}
|
{#if icon && (action.$if?.() ?? true)}
|
||||||
<IconButton variant="ghost" shape="round" {color} {icon} aria-label={title} onclick={() => onAction(action)} />
|
<IconButton variant="ghost" shape="round" {color} {icon} aria-label={title} onclick={() => onAction(action)} />
|
||||||
{/if}
|
{/if}
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,6 @@
|
||||||
const { title, icon, onAction } = $derived(action);
|
const { title, icon, onAction } = $derived(action);
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
{#if action.$if?.() ?? true}
|
{#if icon && (action.$if?.() ?? true)}
|
||||||
<IconButton {size} shape="round" color="primary" {icon} aria-label={title} onclick={() => onAction(action)} />
|
<IconButton {size} shape="round" color="primary" {icon} aria-label={title} onclick={() => onAction(action)} />
|
||||||
{/if}
|
{/if}
|
||||||
|
|
|
||||||
|
|
@ -5,8 +5,6 @@ import type { AlbumResponseDto, AssetResponseDto, PersonResponseDto, StackRespon
|
||||||
type ActionMap = {
|
type ActionMap = {
|
||||||
[AssetAction.ARCHIVE]: { asset: TimelineAsset };
|
[AssetAction.ARCHIVE]: { asset: TimelineAsset };
|
||||||
[AssetAction.UNARCHIVE]: { asset: TimelineAsset };
|
[AssetAction.UNARCHIVE]: { asset: TimelineAsset };
|
||||||
[AssetAction.FAVORITE]: { asset: TimelineAsset };
|
|
||||||
[AssetAction.UNFAVORITE]: { asset: TimelineAsset };
|
|
||||||
[AssetAction.TRASH]: { asset: TimelineAsset };
|
[AssetAction.TRASH]: { asset: TimelineAsset };
|
||||||
[AssetAction.DELETE]: { asset: TimelineAsset };
|
[AssetAction.DELETE]: { asset: TimelineAsset };
|
||||||
[AssetAction.RESTORE]: { asset: TimelineAsset };
|
[AssetAction.RESTORE]: { asset: TimelineAsset };
|
||||||
|
|
|
||||||
|
|
@ -1,51 +0,0 @@
|
||||||
<script lang="ts">
|
|
||||||
import { shortcut } from '$lib/actions/shortcut';
|
|
||||||
import { AssetAction } from '$lib/constants';
|
|
||||||
import { handleError } from '$lib/utils/handle-error';
|
|
||||||
import { toTimelineAsset } from '$lib/utils/timeline-util';
|
|
||||||
import { updateAsset, type AssetResponseDto } from '@immich/sdk';
|
|
||||||
import { IconButton, toastManager } from '@immich/ui';
|
|
||||||
import { mdiHeart, mdiHeartOutline } from '@mdi/js';
|
|
||||||
import { t } from 'svelte-i18n';
|
|
||||||
import type { OnAction } from './action';
|
|
||||||
|
|
||||||
interface Props {
|
|
||||||
asset: AssetResponseDto;
|
|
||||||
onAction: OnAction;
|
|
||||||
}
|
|
||||||
|
|
||||||
let { asset, onAction }: Props = $props();
|
|
||||||
|
|
||||||
const toggleFavorite = async () => {
|
|
||||||
try {
|
|
||||||
const data = await updateAsset({
|
|
||||||
id: asset.id,
|
|
||||||
updateAssetDto: {
|
|
||||||
isFavorite: !asset.isFavorite,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
asset = { ...asset, isFavorite: data.isFavorite };
|
|
||||||
|
|
||||||
onAction({
|
|
||||||
type: asset.isFavorite ? AssetAction.FAVORITE : AssetAction.UNFAVORITE,
|
|
||||||
asset: toTimelineAsset(asset),
|
|
||||||
});
|
|
||||||
|
|
||||||
toastManager.success(asset.isFavorite ? $t('added_to_favorites') : $t('removed_from_favorites'));
|
|
||||||
} catch (error) {
|
|
||||||
handleError(error, $t('errors.unable_to_add_remove_favorites', { values: { favorite: asset.isFavorite } }));
|
|
||||||
}
|
|
||||||
};
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<svelte:document use:shortcut={{ shortcut: { key: 'f' }, onShortcut: toggleFavorite }} />
|
|
||||||
|
|
||||||
<IconButton
|
|
||||||
color="secondary"
|
|
||||||
shape="round"
|
|
||||||
variant="ghost"
|
|
||||||
icon={asset.isFavorite ? mdiHeart : mdiHeartOutline}
|
|
||||||
aria-label={asset.isFavorite ? $t('unfavorite') : $t('to_favorite')}
|
|
||||||
onclick={toggleFavorite}
|
|
||||||
/>
|
|
||||||
|
|
@ -8,7 +8,6 @@
|
||||||
import ArchiveAction from '$lib/components/asset-viewer/actions/archive-action.svelte';
|
import ArchiveAction from '$lib/components/asset-viewer/actions/archive-action.svelte';
|
||||||
import DeleteAction from '$lib/components/asset-viewer/actions/delete-action.svelte';
|
import DeleteAction from '$lib/components/asset-viewer/actions/delete-action.svelte';
|
||||||
import DownloadAction from '$lib/components/asset-viewer/actions/download-action.svelte';
|
import DownloadAction from '$lib/components/asset-viewer/actions/download-action.svelte';
|
||||||
import FavoriteAction from '$lib/components/asset-viewer/actions/favorite-action.svelte';
|
|
||||||
import KeepThisDeleteOthersAction from '$lib/components/asset-viewer/actions/keep-this-delete-others.svelte';
|
import KeepThisDeleteOthersAction from '$lib/components/asset-viewer/actions/keep-this-delete-others.svelte';
|
||||||
import RatingAction from '$lib/components/asset-viewer/actions/rating-action.svelte';
|
import RatingAction from '$lib/components/asset-viewer/actions/rating-action.svelte';
|
||||||
import RemoveAssetFromStack from '$lib/components/asset-viewer/actions/remove-asset-from-stack.svelte';
|
import RemoveAssetFromStack from '$lib/components/asset-viewer/actions/remove-asset-from-stack.svelte';
|
||||||
|
|
@ -28,7 +27,7 @@
|
||||||
import { photoViewerImgElement } from '$lib/stores/assets-store.svelte';
|
import { photoViewerImgElement } from '$lib/stores/assets-store.svelte';
|
||||||
import { user } from '$lib/stores/user.store';
|
import { user } from '$lib/stores/user.store';
|
||||||
import { photoZoomState } from '$lib/stores/zoom-image.store';
|
import { photoZoomState } from '$lib/stores/zoom-image.store';
|
||||||
import { getAssetJobName, getSharedLink } from '$lib/utils';
|
import { getAssetJobName, getSharedLink, withoutIcons } from '$lib/utils';
|
||||||
import type { OnUndoDelete } from '$lib/utils/actions';
|
import type { OnUndoDelete } from '$lib/utils/actions';
|
||||||
import { canCopyImageToClipboard } from '$lib/utils/asset-utils';
|
import { canCopyImageToClipboard } from '$lib/utils/asset-utils';
|
||||||
import { toTimelineAsset } from '$lib/utils/timeline-util';
|
import { toTimelineAsset } from '$lib/utils/timeline-util';
|
||||||
|
|
@ -105,6 +104,7 @@
|
||||||
|
|
||||||
const Close: ActionItem = {
|
const Close: ActionItem = {
|
||||||
title: $t('go_back'),
|
title: $t('go_back'),
|
||||||
|
type: $t('assets'),
|
||||||
icon: mdiArrowLeft,
|
icon: mdiArrowLeft,
|
||||||
$if: () => !!onClose,
|
$if: () => !!onClose,
|
||||||
onAction: () => onClose?.(),
|
onAction: () => onClose?.(),
|
||||||
|
|
@ -113,7 +113,9 @@
|
||||||
|
|
||||||
const { Cast } = $derived(getGlobalActions($t));
|
const { Cast } = $derived(getGlobalActions($t));
|
||||||
|
|
||||||
const { Share, Offline, PlayMotionPhoto, StopMotionPhoto, Info } = $derived(getAssetActions($t, asset));
|
const { Share, Offline, Favorite, Unfavorite, PlayMotionPhoto, StopMotionPhoto, Info } = $derived(
|
||||||
|
getAssetActions($t, asset),
|
||||||
|
);
|
||||||
|
|
||||||
// $: showEditorButton =
|
// $: showEditorButton =
|
||||||
// isOwner &&
|
// isOwner &&
|
||||||
|
|
@ -128,7 +130,7 @@
|
||||||
|
|
||||||
<CommandPaletteDefaultProvider
|
<CommandPaletteDefaultProvider
|
||||||
name={$t('assets')}
|
name={$t('assets')}
|
||||||
actions={[Close, Share, Offline, PlayMotionPhoto, StopMotionPhoto, Info]}
|
actions={withoutIcons([Close, Share, Offline, Favorite, Unfavorite, PlayMotionPhoto, StopMotionPhoto, Info])}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
|
|
@ -172,9 +174,10 @@
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
<ActionButton action={Info} />
|
<ActionButton action={Info} />
|
||||||
|
<ActionButton action={Favorite} />
|
||||||
|
<ActionButton action={Unfavorite} />
|
||||||
|
|
||||||
{#if isOwner}
|
{#if isOwner}
|
||||||
<FavoriteAction {asset} {onAction} />
|
|
||||||
<RatingAction {asset} {onAction} />
|
<RatingAction {asset} {onAction} />
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -350,15 +350,6 @@
|
||||||
selectedEditType = type;
|
selectedEditType = type;
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleAssetReplace = async ({ oldAssetId, newAssetId }: { oldAssetId: string; newAssetId: string }) => {
|
|
||||||
if (oldAssetId !== asset.id) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
await new Promise((promise) => setTimeout(promise, 500));
|
|
||||||
await goto(`${AppRoute.PHOTOS}/${newAssetId}`);
|
|
||||||
};
|
|
||||||
|
|
||||||
let isFullScreen = $derived(fullscreenElement !== null);
|
let isFullScreen = $derived(fullscreenElement !== null);
|
||||||
|
|
||||||
$effect(() => {
|
$effect(() => {
|
||||||
|
|
@ -391,9 +382,24 @@
|
||||||
preloadManager.preload(cursor.nextAsset);
|
preloadManager.preload(cursor.nextAsset);
|
||||||
preloadManager.preload(cursor.previousAsset);
|
preloadManager.preload(cursor.previousAsset);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const onAssetReplace = async ({ oldAssetId, newAssetId }: { oldAssetId: string; newAssetId: string }) => {
|
||||||
|
if (oldAssetId !== asset.id) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await new Promise((promise) => setTimeout(promise, 500));
|
||||||
|
await goto(`${AppRoute.PHOTOS}/${newAssetId}`);
|
||||||
|
};
|
||||||
|
|
||||||
|
const onAssetUpdate = (update: AssetResponseDto) => {
|
||||||
|
if (asset.id === update.id) {
|
||||||
|
asset = update;
|
||||||
|
}
|
||||||
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<OnEvents onAssetReplace={handleAssetReplace} />
|
<OnEvents {onAssetReplace} {onAssetUpdate} />
|
||||||
|
|
||||||
<svelte:document bind:fullscreenElement />
|
<svelte:document bind:fullscreenElement />
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -39,13 +39,7 @@
|
||||||
timelineManager?: TimelineManager;
|
timelineManager?: TimelineManager;
|
||||||
options?: TimelineManagerOptions;
|
options?: TimelineManagerOptions;
|
||||||
assetInteraction: AssetInteraction;
|
assetInteraction: AssetInteraction;
|
||||||
removeAction?:
|
removeAction?: AssetAction.UNARCHIVE | AssetAction.ARCHIVE | AssetAction.SET_VISIBILITY_TIMELINE | null;
|
||||||
| AssetAction.UNARCHIVE
|
|
||||||
| AssetAction.ARCHIVE
|
|
||||||
| AssetAction.FAVORITE
|
|
||||||
| AssetAction.UNFAVORITE
|
|
||||||
| AssetAction.SET_VISIBILITY_TIMELINE
|
|
||||||
| null;
|
|
||||||
withStacked?: boolean;
|
withStacked?: boolean;
|
||||||
showArchiveIcon?: boolean;
|
showArchiveIcon?: boolean;
|
||||||
isShared?: boolean;
|
isShared?: boolean;
|
||||||
|
|
|
||||||
|
|
@ -25,13 +25,7 @@
|
||||||
album?: AlbumResponseDto;
|
album?: AlbumResponseDto;
|
||||||
person?: PersonResponseDto;
|
person?: PersonResponseDto;
|
||||||
|
|
||||||
removeAction?:
|
removeAction?: AssetAction.UNARCHIVE | AssetAction.ARCHIVE | AssetAction.SET_VISIBILITY_TIMELINE | null;
|
||||||
| AssetAction.UNARCHIVE
|
|
||||||
| AssetAction.ARCHIVE
|
|
||||||
| AssetAction.FAVORITE
|
|
||||||
| AssetAction.UNFAVORITE
|
|
||||||
| AssetAction.SET_VISIBILITY_TIMELINE
|
|
||||||
| null;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
let {
|
let {
|
||||||
|
|
@ -141,8 +135,6 @@
|
||||||
switch (action.type) {
|
switch (action.type) {
|
||||||
case AssetAction.ARCHIVE:
|
case AssetAction.ARCHIVE:
|
||||||
case AssetAction.UNARCHIVE:
|
case AssetAction.UNARCHIVE:
|
||||||
case AssetAction.FAVORITE:
|
|
||||||
case AssetAction.UNFAVORITE:
|
|
||||||
case AssetAction.ADD: {
|
case AssetAction.ADD: {
|
||||||
timelineManager.upsertAssets([action.asset]);
|
timelineManager.upsertAssets([action.asset]);
|
||||||
break;
|
break;
|
||||||
|
|
|
||||||
|
|
@ -3,8 +3,6 @@ export const UUID_REGEX = /^[\dA-Fa-f]{8}(?:\b-[\dA-Fa-f]{4}){3}\b-[\dA-Fa-f]{12
|
||||||
export enum AssetAction {
|
export enum AssetAction {
|
||||||
ARCHIVE = 'archive',
|
ARCHIVE = 'archive',
|
||||||
UNARCHIVE = 'unarchive',
|
UNARCHIVE = 'unarchive',
|
||||||
FAVORITE = 'favorite',
|
|
||||||
UNFAVORITE = 'unfavorite',
|
|
||||||
TRASH = 'trash',
|
TRASH = 'trash',
|
||||||
DELETE = 'delete',
|
DELETE = 'delete',
|
||||||
RESTORE = 'restore',
|
RESTORE = 'restore',
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@ import type { ReleaseEvent } from '$lib/types';
|
||||||
import type {
|
import type {
|
||||||
AlbumResponseDto,
|
AlbumResponseDto,
|
||||||
ApiKeyResponseDto,
|
ApiKeyResponseDto,
|
||||||
|
AssetResponseDto,
|
||||||
LibraryResponseDto,
|
LibraryResponseDto,
|
||||||
LoginResponseDto,
|
LoginResponseDto,
|
||||||
QueueResponseDto,
|
QueueResponseDto,
|
||||||
|
|
@ -24,6 +25,7 @@ export type Events = {
|
||||||
ApiKeyUpdate: [ApiKeyResponseDto];
|
ApiKeyUpdate: [ApiKeyResponseDto];
|
||||||
ApiKeyDelete: [ApiKeyResponseDto];
|
ApiKeyDelete: [ApiKeyResponseDto];
|
||||||
|
|
||||||
|
AssetUpdate: [AssetResponseDto];
|
||||||
AssetReplace: [{ oldAssetId: string; newAssetId: string }];
|
AssetReplace: [{ oldAssetId: string; newAssetId: string }];
|
||||||
|
|
||||||
AlbumUpdate: [AlbumResponseDto];
|
AlbumUpdate: [AlbumResponseDto];
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
import { VirtualScrollManager } from '$lib/managers/VirtualScrollManager/VirtualScrollManager.svelte';
|
import { VirtualScrollManager } from '$lib/managers/VirtualScrollManager/VirtualScrollManager.svelte';
|
||||||
import { authManager } from '$lib/managers/auth-manager.svelte';
|
import { authManager } from '$lib/managers/auth-manager.svelte';
|
||||||
|
import { eventManager } from '$lib/managers/event-manager.svelte';
|
||||||
import { GroupInsertionCache } from '$lib/managers/timeline-manager/group-insertion-cache.svelte';
|
import { GroupInsertionCache } from '$lib/managers/timeline-manager/group-insertion-cache.svelte';
|
||||||
import { updateIntersectionMonthGroup } from '$lib/managers/timeline-manager/internal/intersection-support.svelte';
|
import { updateIntersectionMonthGroup } from '$lib/managers/timeline-manager/internal/intersection-support.svelte';
|
||||||
import { updateGeometry } from '$lib/managers/timeline-manager/internal/layout-support.svelte';
|
import { updateGeometry } from '$lib/managers/timeline-manager/internal/layout-support.svelte';
|
||||||
|
|
@ -93,6 +94,7 @@ export class TimelineManager extends VirtualScrollManager {
|
||||||
#updatingIntersections = false;
|
#updatingIntersections = false;
|
||||||
#scrollableElement: HTMLElement | undefined = $state();
|
#scrollableElement: HTMLElement | undefined = $state();
|
||||||
#showAssetOwners = new PersistedLocalStorage<boolean>('album-show-asset-owners', false);
|
#showAssetOwners = new PersistedLocalStorage<boolean>('album-show-asset-owners', false);
|
||||||
|
#unsubscribes: Array<() => void> = [];
|
||||||
|
|
||||||
get showAssetOwners() {
|
get showAssetOwners() {
|
||||||
return this.#showAssetOwners.current;
|
return this.#showAssetOwners.current;
|
||||||
|
|
@ -108,6 +110,12 @@ export class TimelineManager extends VirtualScrollManager {
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
super();
|
super();
|
||||||
|
|
||||||
|
const onAssetUpdate = (asset: AssetResponseDto) => this.upsertAssets([toTimelineAsset(asset)]);
|
||||||
|
|
||||||
|
eventManager.on('AssetUpdate', onAssetUpdate);
|
||||||
|
|
||||||
|
this.#unsubscribes.push(() => eventManager.off('AssetUpdate', onAssetUpdate));
|
||||||
}
|
}
|
||||||
|
|
||||||
override get scrollTop(): number {
|
override get scrollTop(): number {
|
||||||
|
|
@ -269,6 +277,11 @@ export class TimelineManager extends VirtualScrollManager {
|
||||||
public override destroy() {
|
public override destroy() {
|
||||||
this.disconnect();
|
this.disconnect();
|
||||||
this.isInitialized = false;
|
this.isInitialized = false;
|
||||||
|
|
||||||
|
for (const unsubscribe of this.#unsubscribes) {
|
||||||
|
unsubscribe();
|
||||||
|
}
|
||||||
|
|
||||||
super.destroy();
|
super.destroy();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -3,10 +3,14 @@ import { eventManager } from '$lib/managers/event-manager.svelte';
|
||||||
import SharedLinkCreateModal from '$lib/modals/SharedLinkCreateModal.svelte';
|
import SharedLinkCreateModal from '$lib/modals/SharedLinkCreateModal.svelte';
|
||||||
import { user as authUser } from '$lib/stores/user.store';
|
import { user as authUser } from '$lib/stores/user.store';
|
||||||
import { openFileUploadDialog } from '$lib/utils/file-uploader';
|
import { openFileUploadDialog } from '$lib/utils/file-uploader';
|
||||||
import { AssetVisibility, copyAsset, deleteAssets, type AssetResponseDto } from '@immich/sdk';
|
import { handleError } from '$lib/utils/handle-error';
|
||||||
import { modalManager, type ActionItem } from '@immich/ui';
|
import { getFormatter } from '$lib/utils/i18n';
|
||||||
|
import { AssetVisibility, copyAsset, deleteAssets, updateAsset, type AssetResponseDto } from '@immich/sdk';
|
||||||
|
import { modalManager, toastManager, type ActionItem } from '@immich/ui';
|
||||||
import {
|
import {
|
||||||
mdiAlertOutline,
|
mdiAlertOutline,
|
||||||
|
mdiHeart,
|
||||||
|
mdiHeartOutline,
|
||||||
mdiInformationOutline,
|
mdiInformationOutline,
|
||||||
mdiMotionPauseOutline,
|
mdiMotionPauseOutline,
|
||||||
mdiMotionPlayOutline,
|
mdiMotionPlayOutline,
|
||||||
|
|
@ -16,9 +20,13 @@ import type { MessageFormatter } from 'svelte-i18n';
|
||||||
import { get } from 'svelte/store';
|
import { get } from 'svelte/store';
|
||||||
|
|
||||||
export const getAssetActions = ($t: MessageFormatter, asset: AssetResponseDto) => {
|
export const getAssetActions = ($t: MessageFormatter, asset: AssetResponseDto) => {
|
||||||
|
const currentAuthUser = get(authUser);
|
||||||
|
const isOwner = !!(currentAuthUser && currentAuthUser.id === asset.ownerId);
|
||||||
|
|
||||||
const Share: ActionItem = {
|
const Share: ActionItem = {
|
||||||
title: $t('share'),
|
title: $t('share'),
|
||||||
icon: mdiShareVariantOutline,
|
icon: mdiShareVariantOutline,
|
||||||
|
type: $t('assets'),
|
||||||
$if: () => !!(get(authUser) && !asset.isTrashed && asset.visibility !== AssetVisibility.Locked),
|
$if: () => !!(get(authUser) && !asset.isTrashed && asset.visibility !== AssetVisibility.Locked),
|
||||||
onAction: () => modalManager.show(SharedLinkCreateModal, { assetIds: [asset.id] }),
|
onAction: () => modalManager.show(SharedLinkCreateModal, { assetIds: [asset.id] }),
|
||||||
};
|
};
|
||||||
|
|
@ -26,6 +34,7 @@ export const getAssetActions = ($t: MessageFormatter, asset: AssetResponseDto) =
|
||||||
const PlayMotionPhoto: ActionItem = {
|
const PlayMotionPhoto: ActionItem = {
|
||||||
title: $t('play_motion_photo'),
|
title: $t('play_motion_photo'),
|
||||||
icon: mdiMotionPlayOutline,
|
icon: mdiMotionPlayOutline,
|
||||||
|
type: $t('assets'),
|
||||||
$if: () => !!asset.livePhotoVideoId && !assetViewerManager.isPlayingMotionPhoto,
|
$if: () => !!asset.livePhotoVideoId && !assetViewerManager.isPlayingMotionPhoto,
|
||||||
onAction: () => {
|
onAction: () => {
|
||||||
assetViewerManager.isPlayingMotionPhoto = true;
|
assetViewerManager.isPlayingMotionPhoto = true;
|
||||||
|
|
@ -35,15 +44,35 @@ export const getAssetActions = ($t: MessageFormatter, asset: AssetResponseDto) =
|
||||||
const StopMotionPhoto: ActionItem = {
|
const StopMotionPhoto: ActionItem = {
|
||||||
title: $t('stop_motion_photo'),
|
title: $t('stop_motion_photo'),
|
||||||
icon: mdiMotionPauseOutline,
|
icon: mdiMotionPauseOutline,
|
||||||
|
type: $t('assets'),
|
||||||
$if: () => !!asset.livePhotoVideoId && assetViewerManager.isPlayingMotionPhoto,
|
$if: () => !!asset.livePhotoVideoId && assetViewerManager.isPlayingMotionPhoto,
|
||||||
onAction: () => {
|
onAction: () => {
|
||||||
assetViewerManager.isPlayingMotionPhoto = false;
|
assetViewerManager.isPlayingMotionPhoto = false;
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const Favorite: ActionItem = {
|
||||||
|
title: $t('to_favorite'),
|
||||||
|
icon: mdiHeartOutline,
|
||||||
|
type: $t('assets'),
|
||||||
|
$if: () => isOwner && !asset.isFavorite,
|
||||||
|
onAction: () => handleFavorite(asset),
|
||||||
|
shortcuts: [{ key: 'f' }],
|
||||||
|
};
|
||||||
|
|
||||||
|
const Unfavorite: ActionItem = {
|
||||||
|
title: $t('unfavorite'),
|
||||||
|
icon: mdiHeart,
|
||||||
|
type: $t('assets'),
|
||||||
|
$if: () => isOwner && asset.isFavorite,
|
||||||
|
onAction: () => handleUnfavorite(asset),
|
||||||
|
shortcuts: [{ key: 'f' }],
|
||||||
|
};
|
||||||
|
|
||||||
const Offline: ActionItem = {
|
const Offline: ActionItem = {
|
||||||
title: $t('asset_offline'),
|
title: $t('asset_offline'),
|
||||||
icon: mdiAlertOutline,
|
icon: mdiAlertOutline,
|
||||||
|
type: $t('assets'),
|
||||||
color: 'danger',
|
color: 'danger',
|
||||||
$if: () => !!asset.isOffline,
|
$if: () => !!asset.isOffline,
|
||||||
onAction: () => assetViewerManager.toggleDetailPanel(),
|
onAction: () => assetViewerManager.toggleDetailPanel(),
|
||||||
|
|
@ -52,12 +81,37 @@ export const getAssetActions = ($t: MessageFormatter, asset: AssetResponseDto) =
|
||||||
const Info: ActionItem = {
|
const Info: ActionItem = {
|
||||||
title: $t('info'),
|
title: $t('info'),
|
||||||
icon: mdiInformationOutline,
|
icon: mdiInformationOutline,
|
||||||
|
type: $t('assets'),
|
||||||
$if: () => asset.hasMetadata,
|
$if: () => asset.hasMetadata,
|
||||||
onAction: () => assetViewerManager.toggleDetailPanel(),
|
onAction: () => assetViewerManager.toggleDetailPanel(),
|
||||||
shortcuts: [{ key: 'i' }],
|
shortcuts: [{ key: 'i' }],
|
||||||
};
|
};
|
||||||
|
|
||||||
return { Share, PlayMotionPhoto, StopMotionPhoto, Offline, Info };
|
return { Share, Offline, Info, Favorite, Unfavorite, PlayMotionPhoto, StopMotionPhoto };
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleFavorite = async (asset: AssetResponseDto) => {
|
||||||
|
const $t = await getFormatter();
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await updateAsset({ id: asset.id, updateAssetDto: { isFavorite: true } });
|
||||||
|
toastManager.success($t('added_to_favorites'));
|
||||||
|
eventManager.emit('AssetUpdate', response);
|
||||||
|
} catch (error) {
|
||||||
|
handleError(error, $t('errors.unable_to_add_remove_favorites', { values: { favorite: asset.isFavorite } }));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleUnfavorite = async (asset: AssetResponseDto) => {
|
||||||
|
const $t = await getFormatter();
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await updateAsset({ id: asset.id, updateAssetDto: { isFavorite: false } });
|
||||||
|
toastManager.success($t('removed_from_favorites'));
|
||||||
|
eventManager.emit('AssetUpdate', response);
|
||||||
|
} catch (error) {
|
||||||
|
handleError(error, $t('errors.unable_to_add_remove_favorites', { values: { favorite: asset.isFavorite } }));
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
export const handleReplaceAsset = async (oldAssetId: string) => {
|
export const handleReplaceAsset = async (oldAssetId: string) => {
|
||||||
|
|
|
||||||
|
|
@ -26,7 +26,7 @@ import {
|
||||||
type SharedLinkResponseDto,
|
type SharedLinkResponseDto,
|
||||||
type UserResponseDto,
|
type UserResponseDto,
|
||||||
} from '@immich/sdk';
|
} from '@immich/sdk';
|
||||||
import { toastManager } from '@immich/ui';
|
import { toastManager, type ActionItem } from '@immich/ui';
|
||||||
import { mdiCogRefreshOutline, mdiDatabaseRefreshOutline, mdiHeadSyncOutline, mdiImageRefreshOutline } from '@mdi/js';
|
import { mdiCogRefreshOutline, mdiDatabaseRefreshOutline, mdiHeadSyncOutline, mdiImageRefreshOutline } from '@mdi/js';
|
||||||
import { init, register, t } from 'svelte-i18n';
|
import { init, register, t } from 'svelte-i18n';
|
||||||
import { derived, get } from 'svelte/store';
|
import { derived, get } from 'svelte/store';
|
||||||
|
|
@ -440,3 +440,6 @@ export const getReleaseType = (
|
||||||
};
|
};
|
||||||
|
|
||||||
export const semverToName = ({ major, minor, patch }: ServerVersionResponseDto) => `v${major}.${minor}.${patch}`;
|
export const semverToName = ({ major, minor, patch }: ServerVersionResponseDto) => `v${major}.${minor}.${patch}`;
|
||||||
|
|
||||||
|
export const withoutIcons = (actions: ActionItem[]): ActionItem[] =>
|
||||||
|
actions.map((action) => ({ ...action, icon: undefined }));
|
||||||
|
|
|
||||||
|
|
@ -16,7 +16,6 @@
|
||||||
import TagAction from '$lib/components/timeline/actions/TagAction.svelte';
|
import TagAction from '$lib/components/timeline/actions/TagAction.svelte';
|
||||||
import AssetSelectControlBar from '$lib/components/timeline/AssetSelectControlBar.svelte';
|
import AssetSelectControlBar from '$lib/components/timeline/AssetSelectControlBar.svelte';
|
||||||
import Timeline from '$lib/components/timeline/Timeline.svelte';
|
import Timeline from '$lib/components/timeline/Timeline.svelte';
|
||||||
import { AssetAction } from '$lib/constants';
|
|
||||||
import { TimelineManager } from '$lib/managers/timeline-manager/timeline-manager.svelte';
|
import { TimelineManager } from '$lib/managers/timeline-manager/timeline-manager.svelte';
|
||||||
import { AssetInteraction } from '$lib/stores/asset-interaction.svelte';
|
import { AssetInteraction } from '$lib/stores/asset-interaction.svelte';
|
||||||
import { preferences } from '$lib/stores/user.store';
|
import { preferences } from '$lib/stores/user.store';
|
||||||
|
|
@ -55,7 +54,6 @@
|
||||||
bind:timelineManager
|
bind:timelineManager
|
||||||
{options}
|
{options}
|
||||||
{assetInteraction}
|
{assetInteraction}
|
||||||
removeAction={AssetAction.UNFAVORITE}
|
|
||||||
onEscape={handleEscape}
|
onEscape={handleEscape}
|
||||||
>
|
>
|
||||||
{#snippet empty()}
|
{#snippet empty()}
|
||||||
|
|
|
||||||
|
|
@ -145,7 +145,6 @@
|
||||||
icon: mdiThemeLightDark,
|
icon: mdiThemeLightDark,
|
||||||
onAction: () => themeManager.toggleTheme(),
|
onAction: () => themeManager.toggleTheme(),
|
||||||
shortcuts: { shift: true, key: 't' },
|
shortcuts: { shift: true, key: 't' },
|
||||||
isGlobal: true,
|
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|
@ -181,7 +180,7 @@
|
||||||
icon: mdiServer,
|
icon: mdiServer,
|
||||||
onAction: () => goto(AppRoute.ADMIN_STATS),
|
onAction: () => goto(AppRoute.ADMIN_STATS),
|
||||||
},
|
},
|
||||||
].map((route) => ({ ...route, type: $t('page'), isGlobal: true, $if: () => $user?.isAdmin }));
|
].map((route) => ({ ...route, type: $t('page'), $if: () => $user?.isAdmin }));
|
||||||
|
|
||||||
const commands = $derived([...userCommands, ...adminCommands]);
|
const commands = $derived([...userCommands, ...adminCommands]);
|
||||||
</script>
|
</script>
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue