mirror of
https://github.com/immich-app/immich.git
synced 2026-05-18 03:10:24 +03:00
Merge remote-tracking branch 'origin/main' into feat/integrity-checks-izzy
This commit is contained in:
+2
-2
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "immich-web",
|
||||
"version": "2.5.5",
|
||||
"version": "2.5.6",
|
||||
"license": "GNU Affero General Public License version 3",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
@@ -27,7 +27,7 @@
|
||||
"@formatjs/icu-messageformat-parser": "^3.0.0",
|
||||
"@immich/justified-layout-wasm": "^0.4.3",
|
||||
"@immich/sdk": "file:../open-api/typescript-sdk",
|
||||
"@immich/ui": "^0.61.3",
|
||||
"@immich/ui": "^0.62.0",
|
||||
"@mapbox/mapbox-gl-rtl-text": "0.2.3",
|
||||
"@mdi/js": "^7.4.47",
|
||||
"@photo-sphere-viewer/core": "^5.14.0",
|
||||
|
||||
@@ -75,6 +75,7 @@
|
||||
show: false,
|
||||
},
|
||||
width: 2,
|
||||
pxAlign: 0,
|
||||
};
|
||||
|
||||
const options: uPlot.Options = {
|
||||
@@ -91,7 +92,7 @@
|
||||
width: 200,
|
||||
height: 200,
|
||||
ms: 1,
|
||||
pxAlign: true,
|
||||
pxAlign: 0,
|
||||
scales: {
|
||||
y: {
|
||||
distr: 1,
|
||||
|
||||
@@ -105,72 +105,74 @@
|
||||
|
||||
<svelte:document use:shortcut={{ shortcut: { key: 'Escape' }, onShortcut: onClose }} />
|
||||
|
||||
<div
|
||||
class="fixed top-0 z-1 flex h-16 w-full items-center justify-between border-b bg-white p-1 dark:border-immich-dark-gray dark:bg-black dark:text-immich-dark-fg md:p-8"
|
||||
>
|
||||
<div class="flex items-center">
|
||||
<IconButton
|
||||
shape="round"
|
||||
color="secondary"
|
||||
variant="ghost"
|
||||
aria-label={$t('close')}
|
||||
icon={mdiClose}
|
||||
onclick={onClose}
|
||||
/>
|
||||
<div class="flex gap-2 items-center">
|
||||
<p id={titleId} class="ms-2">{$t('show_and_hide_people')}</p>
|
||||
<p class="text-sm text-gray-400 dark:text-gray-600">({totalPeopleCount.toLocaleString($locale)})</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex items-center justify-end">
|
||||
<div class="flex items-center md:me-4">
|
||||
<div class="h-full overflow-y-auto">
|
||||
<div
|
||||
class="sticky top-0 z-1 flex h-16 w-full items-center justify-between border-b bg-white p-1 dark:border-immich-dark-gray dark:bg-black dark:text-immich-dark-fg md:p-8"
|
||||
>
|
||||
<div class="flex items-center">
|
||||
<IconButton
|
||||
shape="round"
|
||||
color="secondary"
|
||||
variant="ghost"
|
||||
aria-label={$t('reset_people_visibility')}
|
||||
icon={mdiRestart}
|
||||
onclick={() => overrides.clear()}
|
||||
/>
|
||||
<IconButton
|
||||
shape="round"
|
||||
color="secondary"
|
||||
variant="ghost"
|
||||
aria-label={toggleButton.label}
|
||||
icon={toggleButton.icon}
|
||||
onclick={handleToggleVisibility}
|
||||
aria-label={$t('close')}
|
||||
icon={mdiClose}
|
||||
onclick={onClose}
|
||||
/>
|
||||
<div class="flex gap-2 items-center">
|
||||
<p id={titleId} class="ms-2">{$t('show_and_hide_people')}</p>
|
||||
<p class="text-sm text-gray-400 dark:text-gray-600">({totalPeopleCount.toLocaleString($locale)})</p>
|
||||
</div>
|
||||
</div>
|
||||
<Button loading={showLoadingSpinner} onclick={handleSaveVisibility} size="small">{$t('done')}</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-wrap gap-1 p-2 pb-8 md:px-8 mt-16">
|
||||
<PeopleInfiniteScroll {people} hasNextPage={true} {loadNextPage}>
|
||||
{#snippet children({ person })}
|
||||
{@const hidden = overrides.get(person.id) ?? person.isHidden}
|
||||
<button
|
||||
type="button"
|
||||
class="group relative w-full h-full"
|
||||
onclick={() => setHiddenOverride(person, !hidden)}
|
||||
aria-pressed={hidden}
|
||||
aria-label={person.name ? $t('hide_named_person', { values: { name: person.name } }) : $t('hide_person')}
|
||||
>
|
||||
<ImageThumbnail
|
||||
{hidden}
|
||||
shadow
|
||||
url={getPeopleThumbnailUrl(person)}
|
||||
altText={person.name}
|
||||
widthStyle="100%"
|
||||
hiddenIconClass="text-white group-hover:text-black transition-colors"
|
||||
preload={false}
|
||||
<div class="flex items-center justify-end">
|
||||
<div class="flex items-center md:me-4">
|
||||
<IconButton
|
||||
shape="round"
|
||||
color="secondary"
|
||||
variant="ghost"
|
||||
aria-label={$t('reset_people_visibility')}
|
||||
icon={mdiRestart}
|
||||
onclick={() => overrides.clear()}
|
||||
/>
|
||||
{#if person.name}
|
||||
<span class="absolute bottom-2 start-0 w-full select-text px-1 text-center font-medium text-white">
|
||||
{person.name}
|
||||
</span>
|
||||
{/if}
|
||||
</button>
|
||||
{/snippet}
|
||||
</PeopleInfiniteScroll>
|
||||
<IconButton
|
||||
shape="round"
|
||||
color="secondary"
|
||||
variant="ghost"
|
||||
aria-label={toggleButton.label}
|
||||
icon={toggleButton.icon}
|
||||
onclick={handleToggleVisibility}
|
||||
/>
|
||||
</div>
|
||||
<Button loading={showLoadingSpinner} onclick={handleSaveVisibility} size="small">{$t('done')}</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-wrap gap-1 p-2 pb-8 md:px-8">
|
||||
<PeopleInfiniteScroll {people} hasNextPage={true} {loadNextPage}>
|
||||
{#snippet children({ person })}
|
||||
{@const hidden = overrides.get(person.id) ?? person.isHidden}
|
||||
<button
|
||||
type="button"
|
||||
class="group relative w-full h-full"
|
||||
onclick={() => setHiddenOverride(person, !hidden)}
|
||||
aria-pressed={hidden}
|
||||
aria-label={person.name ? $t('hide_named_person', { values: { name: person.name } }) : $t('hide_person')}
|
||||
>
|
||||
<ImageThumbnail
|
||||
{hidden}
|
||||
shadow
|
||||
url={getPeopleThumbnailUrl(person)}
|
||||
altText={person.name}
|
||||
widthStyle="100%"
|
||||
hiddenIconClass="text-white group-hover:text-black transition-colors"
|
||||
preload={false}
|
||||
/>
|
||||
{#if person.name}
|
||||
<span class="absolute bottom-2 start-0 w-full select-text px-1 text-center font-medium text-white">
|
||||
{person.name}
|
||||
</span>
|
||||
{/if}
|
||||
</button>
|
||||
{/snippet}
|
||||
</PeopleInfiniteScroll>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
<script lang="ts">
|
||||
import OnEvents from '$lib/components/OnEvents.svelte';
|
||||
import { timeBeforeShowLoadingSpinner } from '$lib/constants';
|
||||
import { eventManager } from '$lib/managers/event-manager.svelte';
|
||||
import { assetViewingStore } from '$lib/stores/asset-viewing.store';
|
||||
import { boundingBoxesArray } from '$lib/stores/people.store';
|
||||
import { getPeopleThumbnailUrl, handlePromiseError } from '$lib/utils';
|
||||
@@ -174,6 +175,8 @@
|
||||
|
||||
await deleteFace({ id: face.id, assetFaceDeleteDto: { force: false } });
|
||||
|
||||
eventManager.emit('PersonAssetDelete', { id: face.person.id, assetId });
|
||||
|
||||
peopleWithFaces = peopleWithFaces.filter((f) => f.id !== face.id);
|
||||
|
||||
await assetViewingStore.setAssetId(assetId);
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<script lang="ts">
|
||||
import { ByteUnit } from '$lib/utils/byte-units';
|
||||
import { Code, Icon, Text } from '@immich/ui';
|
||||
import { Icon, Text } from '@immich/ui';
|
||||
import type { Snippet } from 'svelte';
|
||||
|
||||
interface Props {
|
||||
@@ -30,10 +30,10 @@
|
||||
<Text size="giant" fontWeight="medium">{title}</Text>
|
||||
</div>
|
||||
|
||||
<div class="relative mx-auto font-mono text-2xl font-medium">
|
||||
<div class="mx-auto font-mono text-2xl font-medium">
|
||||
<span class="text-gray-300 dark:text-gray-600">{zeros()}</span><span>{value}</span>
|
||||
{#if unit}
|
||||
<Code color="muted" class="font-mono absolute -top-5 end-1 font-light p-0">{unit}</Code>
|
||||
<code class="font-mono text-base font-normal">{unit}</code>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
|
||||
@@ -47,30 +47,23 @@
|
||||
|
||||
const left = Math.max(8, Math.min(window.innerWidth - rect.width, x - directionWidth));
|
||||
const top = Math.max(8, Math.min(window.innerHeight - menuHeight, y));
|
||||
const maxHeight = window.innerHeight - top - 8;
|
||||
|
||||
return { left, top };
|
||||
return { left, top, maxHeight };
|
||||
});
|
||||
|
||||
// We need to bind clientHeight since the bounding box may return a height
|
||||
// of zero when starting the 'slide' animation.
|
||||
let height: number = $state(0);
|
||||
|
||||
let isTransitioned = $state(false);
|
||||
</script>
|
||||
|
||||
<div
|
||||
bind:clientHeight={height}
|
||||
class="fixed min-w-50 w-max max-w-75 overflow-hidden rounded-lg shadow-lg z-1"
|
||||
class="fixed min-w-50 w-max max-w-75 overflow-hidden rounded-lg shadow-lg z-1 immich-scrollbar"
|
||||
style:left="{position.left}px"
|
||||
style:top="{position.top}px"
|
||||
transition:slide={{ duration: 250, easing: quintOut }}
|
||||
use:clickOutside={{ onOutclick: onClose }}
|
||||
onintroend={() => {
|
||||
isTransitioned = true;
|
||||
}}
|
||||
onoutrostart={() => {
|
||||
isTransitioned = false;
|
||||
}}
|
||||
>
|
||||
<ul
|
||||
{id}
|
||||
@@ -78,11 +71,8 @@
|
||||
aria-label={ariaLabel}
|
||||
aria-labelledby={ariaLabelledBy}
|
||||
bind:this={menuElement}
|
||||
class="{isVisible
|
||||
? 'max-h-dvh'
|
||||
: 'max-h-0'} flex flex-col transition-all duration-250 ease-in-out outline-none {isTransitioned
|
||||
? 'overflow-auto'
|
||||
: ''}"
|
||||
class="flex flex-col transition-all duration-250 ease-in-out outline-none overflow-auto immich-scrollbar"
|
||||
style:max-height={isVisible ? `${position.maxHeight}px` : '0px'}
|
||||
role="menu"
|
||||
tabindex="-1"
|
||||
>
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
<script lang="ts">
|
||||
import { browser } from '$app/environment';
|
||||
|
||||
import { isSelectingAllAssets } from '$lib/stores/assets-store.svelte';
|
||||
import { IconButton } from '@immich/ui';
|
||||
import { mdiClose } from '@mdi/js';
|
||||
import { onDestroy, onMount, type Snippet } from 'svelte';
|
||||
@@ -46,11 +44,6 @@
|
||||
}
|
||||
};
|
||||
|
||||
const handleClose = () => {
|
||||
$isSelectingAllAssets = false;
|
||||
onClose();
|
||||
};
|
||||
|
||||
onMount(() => {
|
||||
if (browser) {
|
||||
document.addEventListener('scroll', onScroll, { passive: true });
|
||||
@@ -82,7 +75,7 @@
|
||||
{#if showBackButton}
|
||||
<IconButton
|
||||
aria-label={$t('close')}
|
||||
onclick={handleClose}
|
||||
onclick={onClose}
|
||||
color="secondary"
|
||||
shape="round"
|
||||
variant="ghost"
|
||||
|
||||
@@ -29,7 +29,7 @@
|
||||
<div class="flex place-items-center justify-between">
|
||||
<div>
|
||||
<div class="flex h-6.5 place-items-center gap-1">
|
||||
<label class="font-medium text-primary text-sm" for={title}>
|
||||
<label class="font-medium text-sm" for={title}>
|
||||
{title}
|
||||
</label>
|
||||
{#if isEdited}
|
||||
@@ -48,6 +48,7 @@
|
||||
<div class="w-fit">
|
||||
<Dropdown
|
||||
{options}
|
||||
position="bottom-right"
|
||||
hideTextOnSmallScreen={false}
|
||||
bind:selectedOption
|
||||
render={(option) => {
|
||||
|
||||
@@ -1,16 +1,6 @@
|
||||
<script lang="ts" module>
|
||||
import { createContext } from '$lib/utils/context';
|
||||
import { setAssetControlContext } from '$lib/utils/context';
|
||||
import { t } from 'svelte-i18n';
|
||||
|
||||
export interface AssetControlContext {
|
||||
// Wrap assets in a function, because context isn't reactive.
|
||||
getAssets: () => TimelineAsset[]; // All assets includes partners' assets
|
||||
getOwnedAssets: () => TimelineAsset[]; // Only assets owned by the user
|
||||
clearSelect: () => void;
|
||||
}
|
||||
|
||||
const { get: getAssetControlContext, set: setContext } = createContext<AssetControlContext>();
|
||||
export { getAssetControlContext };
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
@@ -19,20 +9,20 @@
|
||||
import type { Snippet } from 'svelte';
|
||||
import ControlAppBar from '../shared-components/control-app-bar.svelte';
|
||||
|
||||
interface Props {
|
||||
type Props = {
|
||||
assets: TimelineAsset[];
|
||||
clearSelect: () => void;
|
||||
ownerId?: string | undefined;
|
||||
children?: Snippet;
|
||||
forceDark?: boolean;
|
||||
}
|
||||
};
|
||||
|
||||
let { assets, clearSelect, ownerId = undefined, children, forceDark }: Props = $props();
|
||||
|
||||
setContext({
|
||||
setAssetControlContext({
|
||||
getAssets: () => assets,
|
||||
getOwnedAssets: () => (ownerId === undefined ? assets : assets.filter((asset) => asset.ownerId === ownerId)),
|
||||
clearSelect,
|
||||
clearSelect: () => clearSelect(),
|
||||
});
|
||||
</script>
|
||||
|
||||
|
||||
@@ -20,7 +20,6 @@
|
||||
import { assetsSnapshot } from '$lib/managers/timeline-manager/utils.svelte';
|
||||
import type { AssetInteraction } from '$lib/stores/asset-interaction.svelte';
|
||||
import { assetViewingStore } from '$lib/stores/asset-viewing.store';
|
||||
import { isSelectingAllAssets } from '$lib/stores/assets-store.svelte';
|
||||
import { mediaQueryManager } from '$lib/stores/media-query-manager.svelte';
|
||||
import { isAssetViewerRoute, navigate } from '$lib/utils/navigation';
|
||||
import { getTimes, type ScrubberListener } from '$lib/utils/timeline-util';
|
||||
@@ -411,11 +410,7 @@
|
||||
}
|
||||
}
|
||||
|
||||
if (timelineManager.assetCount == assetInteraction.selectedAssets.length) {
|
||||
isSelectingAllAssets.set(true);
|
||||
} else {
|
||||
isSelectingAllAssets.set(false);
|
||||
}
|
||||
assetInteraction.selectAll = timelineManager.assetCount === assetInteraction.selectedAssets.length;
|
||||
};
|
||||
|
||||
const onSelectAssets = async (asset: TimelineAsset) => {
|
||||
@@ -559,7 +554,7 @@
|
||||
assetInteraction.removeGroupFromMultiselectGroup(groupTitle);
|
||||
}
|
||||
|
||||
isSelectingAllAssets.set(timelineManager.assetCount === assetInteraction.selectedAssets.length);
|
||||
assetInteraction.selectAll = timelineManager.assetCount === assetInteraction.selectedAssets.length;
|
||||
};
|
||||
|
||||
const _onClick = (
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
<script lang="ts">
|
||||
import MenuOption from '$lib/components/shared-components/context-menu/menu-option.svelte';
|
||||
import { getAssetControlContext } from '$lib/components/timeline/AssetSelectControlBar.svelte';
|
||||
import AlbumPickerModal from '$lib/modals/AlbumPickerModal.svelte';
|
||||
import type { OnAddToAlbum } from '$lib/utils/actions';
|
||||
import { addAssetsToAlbum, addAssetsToAlbums } from '$lib/utils/asset-utils';
|
||||
import { getAssetControlContext } from '$lib/utils/context';
|
||||
import { modalManager } from '@immich/ui';
|
||||
import { mdiImageAlbum, mdiShareVariantOutline } from '@mdi/js';
|
||||
import { t } from 'svelte-i18n';
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
<script lang="ts">
|
||||
import { getAssetControlContext } from '$lib/components/timeline/AssetSelectControlBar.svelte';
|
||||
import MenuOption from '$lib/components/shared-components/context-menu/menu-option.svelte';
|
||||
import type { OnArchive } from '$lib/utils/actions';
|
||||
import { archiveAssets } from '$lib/utils/asset-utils';
|
||||
import { getAssetControlContext } from '$lib/utils/context';
|
||||
import { AssetVisibility } from '@immich/sdk';
|
||||
import { IconButton } from '@immich/ui';
|
||||
import { mdiArchiveArrowDownOutline, mdiArchiveArrowUpOutline, mdiTimerSand } from '@mdi/js';
|
||||
import { t } from 'svelte-i18n';
|
||||
import MenuOption from '../../shared-components/context-menu/menu-option.svelte';
|
||||
|
||||
interface Props {
|
||||
onArchive?: OnArchive;
|
||||
|
||||
@@ -1,37 +0,0 @@
|
||||
<script lang="ts">
|
||||
import MenuOption from '$lib/components/shared-components/context-menu/menu-option.svelte';
|
||||
import { getAssetControlContext } from '$lib/components/timeline/AssetSelectControlBar.svelte';
|
||||
import { getAssetJobIcon, getAssetJobName } from '$lib/utils';
|
||||
import { handleError } from '$lib/utils/handle-error';
|
||||
import { AssetJobName, runAssetJobs } from '@immich/sdk';
|
||||
import { toastManager } from '@immich/ui';
|
||||
import { t } from 'svelte-i18n';
|
||||
|
||||
interface Props {
|
||||
jobs?: AssetJobName[];
|
||||
}
|
||||
|
||||
let { jobs = [AssetJobName.RegenerateThumbnail, AssetJobName.RefreshMetadata, AssetJobName.TranscodeVideo] }: Props =
|
||||
$props();
|
||||
|
||||
const { clearSelect, getOwnedAssets } = getAssetControlContext();
|
||||
|
||||
const isAllVideos = $derived([...getOwnedAssets()].every((asset) => asset.isVideo));
|
||||
|
||||
const handleRunJob = async (name: AssetJobName) => {
|
||||
try {
|
||||
const ids = [...getOwnedAssets()].map(({ id }) => id);
|
||||
await runAssetJobs({ assetJobsDto: { assetIds: ids, name } });
|
||||
toastManager.success(getAssetJobName($t, name));
|
||||
clearSelect();
|
||||
} catch (error) {
|
||||
handleError(error, $t('errors.unable_to_submit_job'));
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
{#each jobs as job (job)}
|
||||
{#if isAllVideos || job !== AssetJobName.TranscodeVideo}
|
||||
<MenuOption text={getAssetJobName($t, job)} icon={getAssetJobIcon(job)} onClick={() => handleRunJob(job)} />
|
||||
{/if}
|
||||
{/each}
|
||||
@@ -1,7 +1,7 @@
|
||||
<script lang="ts">
|
||||
import MenuOption from '$lib/components/shared-components/context-menu/menu-option.svelte';
|
||||
import { getAssetControlContext } from '$lib/components/timeline/AssetSelectControlBar.svelte';
|
||||
import AssetSelectionChangeDateModal from '$lib/modals/AssetSelectionChangeDateModal.svelte';
|
||||
import { getAssetControlContext } from '$lib/utils/context';
|
||||
import { fromTimelinePlainDateTime } from '$lib/utils/timeline-util';
|
||||
import { modalManager } from '@immich/ui';
|
||||
import { mdiCalendarEditOutline } from '@mdi/js';
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
<script lang="ts">
|
||||
import { getAssetControlContext } from '$lib/components/timeline/AssetSelectControlBar.svelte';
|
||||
import AssetUpdateDescriptionConfirmModal from '$lib/modals/AssetUpdateDescriptionConfirmModal.svelte';
|
||||
import { user } from '$lib/stores/user.store';
|
||||
import { getOwnedAssetsWithWarning } from '$lib/utils/asset-utils';
|
||||
import { getAssetControlContext } from '$lib/utils/context';
|
||||
import { handleError } from '$lib/utils/handle-error';
|
||||
import { updateAssets } from '@immich/sdk';
|
||||
import { modalManager } from '@immich/ui';
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
<script lang="ts">
|
||||
import ChangeLocation from '$lib/components/shared-components/change-location.svelte';
|
||||
import { getAssetControlContext } from '$lib/components/timeline/AssetSelectControlBar.svelte';
|
||||
import { user } from '$lib/stores/user.store';
|
||||
import { getOwnedAssetsWithWarning } from '$lib/utils/asset-utils';
|
||||
import { getAssetControlContext } from '$lib/utils/context';
|
||||
import { handleError } from '$lib/utils/handle-error';
|
||||
import { updateAssets } from '@immich/sdk';
|
||||
import { mdiMapMarkerMultipleOutline } from '@mdi/js';
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<script lang="ts">
|
||||
import { getAssetControlContext } from '$lib/components/timeline/AssetSelectControlBar.svelte';
|
||||
import SharedLinkCreateModal from '$lib/modals/SharedLinkCreateModal.svelte';
|
||||
import { getAssetControlContext } from '$lib/utils/context';
|
||||
import { IconButton, modalManager } from '@immich/ui';
|
||||
import { mdiShareVariantOutline } from '@mdi/js';
|
||||
import { t } from 'svelte-i18n';
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
<script lang="ts">
|
||||
import MenuOption from '$lib/components/shared-components/context-menu/menu-option.svelte';
|
||||
import { getAssetControlContext } from '$lib/components/timeline/AssetSelectControlBar.svelte';
|
||||
import { featureFlagsManager } from '$lib/managers/feature-flags-manager.svelte';
|
||||
import AssetDeleteConfirmModal from '$lib/modals/AssetDeleteConfirmModal.svelte';
|
||||
import { showDeleteModal } from '$lib/stores/preferences.store';
|
||||
import { type OnDelete, type OnUndoDelete, deleteAssets } from '$lib/utils/actions';
|
||||
import { getAssetControlContext } from '$lib/utils/context';
|
||||
import { IconButton, modalManager } from '@immich/ui';
|
||||
import { mdiDeleteForeverOutline, mdiDeleteOutline, mdiTimerSand } from '@mdi/js';
|
||||
import { t } from 'svelte-i18n';
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
<script lang="ts">
|
||||
import { shortcut } from '$lib/actions/shortcut';
|
||||
|
||||
import { getAssetControlContext } from '$lib/components/timeline/AssetSelectControlBar.svelte';
|
||||
import { authManager } from '$lib/managers/auth-manager.svelte';
|
||||
import { handleDownloadAsset } from '$lib/services/asset.service';
|
||||
import { downloadArchive } from '$lib/utils/asset-utils';
|
||||
import { getAssetControlContext } from '$lib/utils/context';
|
||||
import { getAssetInfo } from '@immich/sdk';
|
||||
import { IconButton } from '@immich/ui';
|
||||
import { mdiDownload } from '@mdi/js';
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<script lang="ts">
|
||||
import MenuOption from '$lib/components/shared-components/context-menu/menu-option.svelte';
|
||||
import { getAssetControlContext } from '$lib/components/timeline/AssetSelectControlBar.svelte';
|
||||
import type { OnFavorite } from '$lib/utils/actions';
|
||||
import { getAssetControlContext } from '$lib/utils/context';
|
||||
import { handleError } from '$lib/utils/handle-error';
|
||||
import { updateAssets } from '@immich/sdk';
|
||||
import { IconButton, toastManager } from '@immich/ui';
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
<script lang="ts">
|
||||
import { getAssetControlContext } from '$lib/components/timeline/AssetSelectControlBar.svelte';
|
||||
import { authManager } from '$lib/managers/auth-manager.svelte';
|
||||
import type { TimelineAsset } from '$lib/managers/timeline-manager/types';
|
||||
import type { OnLink, OnUnlink } from '$lib/utils/actions';
|
||||
import { getAssetControlContext } from '$lib/utils/context';
|
||||
import { handleError } from '$lib/utils/handle-error';
|
||||
import { toTimelineAsset } from '$lib/utils/timeline-util';
|
||||
import { getAssetInfo, updateAsset } from '@immich/sdk';
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<script lang="ts">
|
||||
import { getAssetControlContext } from '$lib/components/timeline/AssetSelectControlBar.svelte';
|
||||
import { getAssetControlContext } from '$lib/utils/context';
|
||||
import { handleError } from '$lib/utils/handle-error';
|
||||
import { getAlbumInfo, removeAssetFromAlbum, type AlbumResponseDto } from '@immich/sdk';
|
||||
import { IconButton, modalManager, toastManager } from '@immich/ui';
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<script lang="ts">
|
||||
import { getAssetControlContext } from '$lib/components/timeline/AssetSelectControlBar.svelte';
|
||||
import { handleRemoveSharedLinkAssets } from '$lib/services/shared-link.service';
|
||||
import { getAssetControlContext } from '$lib/utils/context';
|
||||
import { type SharedLinkResponseDto } from '@immich/sdk';
|
||||
import { IconButton } from '@immich/ui';
|
||||
import { mdiDeleteOutline } from '@mdi/js';
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<script lang="ts">
|
||||
import { getAssetControlContext } from '$lib/components/timeline/AssetSelectControlBar.svelte';
|
||||
import type { OnRestore } from '$lib/utils/actions';
|
||||
import { getAssetControlContext } from '$lib/utils/context';
|
||||
import { handleError } from '$lib/utils/handle-error';
|
||||
import { restoreAssets } from '@immich/sdk';
|
||||
import { Button, toastManager } from '@immich/ui';
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
<script lang="ts">
|
||||
import { TimelineManager } from '$lib/managers/timeline-manager/timeline-manager.svelte';
|
||||
import type { AssetInteraction } from '$lib/stores/asset-interaction.svelte';
|
||||
import { isSelectingAllAssets } from '$lib/stores/assets-store.svelte';
|
||||
import { cancelMultiselect, selectAllAssets } from '$lib/utils/asset-utils';
|
||||
import { Button, IconButton } from '@immich/ui';
|
||||
import { mdiSelectAll, mdiSelectRemove } from '@mdi/js';
|
||||
@@ -14,6 +13,7 @@
|
||||
}
|
||||
|
||||
let { timelineManager, assetInteraction, withText = false }: Props = $props();
|
||||
const allAssetsSelected = $derived(assetInteraction.selectAll);
|
||||
|
||||
const handleSelectAll = async () => {
|
||||
await selectAllAssets(timelineManager, assetInteraction);
|
||||
@@ -26,21 +26,21 @@
|
||||
|
||||
{#if withText}
|
||||
<Button
|
||||
leadingIcon={$isSelectingAllAssets ? mdiSelectRemove : mdiSelectAll}
|
||||
leadingIcon={allAssetsSelected ? mdiSelectRemove : mdiSelectAll}
|
||||
size="medium"
|
||||
color="secondary"
|
||||
variant="ghost"
|
||||
onclick={$isSelectingAllAssets ? handleCancel : handleSelectAll}
|
||||
onclick={allAssetsSelected ? handleCancel : handleSelectAll}
|
||||
>
|
||||
{$isSelectingAllAssets ? $t('unselect_all') : $t('select_all')}
|
||||
{allAssetsSelected ? $t('unselect_all') : $t('select_all')}
|
||||
</Button>
|
||||
{:else}
|
||||
<IconButton
|
||||
shape="round"
|
||||
color="secondary"
|
||||
variant="ghost"
|
||||
aria-label={$isSelectingAllAssets ? $t('unselect_all') : $t('select_all')}
|
||||
icon={$isSelectingAllAssets ? mdiSelectRemove : mdiSelectAll}
|
||||
onclick={$isSelectingAllAssets ? handleCancel : handleSelectAll}
|
||||
aria-label={allAssetsSelected ? $t('unselect_all') : $t('select_all')}
|
||||
icon={allAssetsSelected ? mdiSelectRemove : mdiSelectAll}
|
||||
onclick={allAssetsSelected ? handleCancel : handleSelectAll}
|
||||
/>
|
||||
{/if}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<script lang="ts">
|
||||
import { getAssetControlContext } from '$lib/components/timeline/AssetSelectControlBar.svelte';
|
||||
import MenuOption from '$lib/components/shared-components/context-menu/menu-option.svelte';
|
||||
import type { OnSetVisibility } from '$lib/utils/actions';
|
||||
import { getAssetControlContext } from '$lib/utils/context';
|
||||
import { handleError } from '$lib/utils/handle-error';
|
||||
import { AssetVisibility, updateAssets } from '@immich/sdk';
|
||||
import { Button, modalManager } from '@immich/ui';
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
<script lang="ts">
|
||||
import { getAssetControlContext } from '$lib/components/timeline/AssetSelectControlBar.svelte';
|
||||
import MenuOption from '$lib/components/shared-components/context-menu/menu-option.svelte';
|
||||
import type { OnStack, OnUnstack } from '$lib/utils/actions';
|
||||
import { deleteStack, stackAssets } from '$lib/utils/asset-utils';
|
||||
import { getAssetControlContext } from '$lib/utils/context';
|
||||
import { toTimelineAsset } from '$lib/utils/timeline-util';
|
||||
import { mdiImageMultipleOutline, mdiImageOffOutline } from '@mdi/js';
|
||||
import { t } from 'svelte-i18n';
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<script lang="ts">
|
||||
import { shortcut } from '$lib/actions/shortcut';
|
||||
import { getAssetControlContext } from '$lib/components/timeline/AssetSelectControlBar.svelte';
|
||||
import AssetTagModal from '$lib/modals/AssetTagModal.svelte';
|
||||
import { getAssetControlContext } from '$lib/utils/context';
|
||||
import { IconButton, modalManager } from '@immich/ui';
|
||||
import { mdiTagMultipleOutline } from '@mdi/js';
|
||||
import { t } from 'svelte-i18n';
|
||||
|
||||
@@ -50,6 +50,7 @@ export type Events = {
|
||||
|
||||
PersonUpdate: [PersonResponseDto];
|
||||
PersonThumbnailReady: [{ id: string }];
|
||||
PersonAssetDelete: [{ id: string; assetId: string }];
|
||||
|
||||
BackupDeleteStatus: [{ filename: string; isDeleting: boolean }];
|
||||
BackupDeleted: [{ filename: string }];
|
||||
|
||||
@@ -5,7 +5,8 @@ import { eventManager } from '$lib/managers/event-manager.svelte';
|
||||
import AssetTagModal from '$lib/modals/AssetTagModal.svelte';
|
||||
import SharedLinkCreateModal from '$lib/modals/SharedLinkCreateModal.svelte';
|
||||
import { user as authUser, preferences } from '$lib/stores/user.store';
|
||||
import { getAssetJobName, getSharedLink, sleep } from '$lib/utils';
|
||||
import type { AssetControlContext } from '$lib/types';
|
||||
import { getSharedLink, sleep } from '$lib/utils';
|
||||
import { downloadUrl } from '$lib/utils/asset-utils';
|
||||
import { openFileUploadDialog } from '$lib/utils/file-uploader';
|
||||
import { handleError } from '$lib/utils/handle-error';
|
||||
@@ -48,6 +49,44 @@ import {
|
||||
import type { MessageFormatter } from 'svelte-i18n';
|
||||
import { get } from 'svelte/store';
|
||||
|
||||
export const getAssetBulkActions = ($t: MessageFormatter, ctx: AssetControlContext) => {
|
||||
const ownedAssets = ctx.getOwnedAssets();
|
||||
const assetIds = ownedAssets.map((asset) => asset.id);
|
||||
const isAllVideos = ownedAssets.every((asset) => asset.isVideo);
|
||||
|
||||
const onAction = async (name: AssetJobName) => {
|
||||
await handleRunAssetJob({ name, assetIds });
|
||||
ctx.clearSelect();
|
||||
};
|
||||
|
||||
const RefreshFacesJob: ActionItem = {
|
||||
title: $t('refresh_faces'),
|
||||
icon: mdiHeadSyncOutline,
|
||||
onAction: () => onAction(AssetJobName.RefreshFaces),
|
||||
};
|
||||
|
||||
const RefreshMetadataJob: ActionItem = {
|
||||
title: $t('refresh_metadata'),
|
||||
icon: mdiDatabaseRefreshOutline,
|
||||
onAction: () => onAction(AssetJobName.RefreshMetadata),
|
||||
};
|
||||
|
||||
const RegenerateThumbnailJob: ActionItem = {
|
||||
title: $t('refresh_thumbnails'),
|
||||
icon: mdiImageRefreshOutline,
|
||||
onAction: () => onAction(AssetJobName.RegenerateThumbnail),
|
||||
};
|
||||
|
||||
const TranscodeVideoJob: ActionItem = {
|
||||
title: $t('refresh_encoded_videos'),
|
||||
icon: mdiCogRefreshOutline,
|
||||
onAction: () => onAction(AssetJobName.TranscodeVideo),
|
||||
$if: () => isAllVideos,
|
||||
};
|
||||
|
||||
return { RefreshFacesJob, RefreshMetadataJob, RegenerateThumbnailJob, TranscodeVideoJob };
|
||||
};
|
||||
|
||||
export const getAssetActions = ($t: MessageFormatter, asset: AssetResponseDto) => {
|
||||
const sharedLink = getSharedLink();
|
||||
const currentAuthUser = get(authUser);
|
||||
@@ -186,25 +225,25 @@ export const getAssetActions = ($t: MessageFormatter, asset: AssetResponseDto) =
|
||||
};
|
||||
|
||||
const RefreshFacesJob: ActionItem = {
|
||||
title: getAssetJobName($t, AssetJobName.RefreshFaces),
|
||||
title: $t('refresh_faces'),
|
||||
icon: mdiHeadSyncOutline,
|
||||
onAction: () => handleRunAssetJob({ name: AssetJobName.RefreshFaces, assetIds: [asset.id] }),
|
||||
};
|
||||
|
||||
const RefreshMetadataJob: ActionItem = {
|
||||
title: getAssetJobName($t, AssetJobName.RefreshMetadata),
|
||||
title: $t('refresh_metadata'),
|
||||
icon: mdiDatabaseRefreshOutline,
|
||||
onAction: () => handleRunAssetJob({ name: AssetJobName.RefreshMetadata, assetIds: [asset.id] }),
|
||||
};
|
||||
|
||||
const RegenerateThumbnailJob: ActionItem = {
|
||||
title: getAssetJobName($t, AssetJobName.RegenerateThumbnail),
|
||||
title: $t('refresh_thumbnails'),
|
||||
icon: mdiImageRefreshOutline,
|
||||
onAction: () => handleRunAssetJob({ name: AssetJobName.RegenerateThumbnail, assetIds: [asset.id] }),
|
||||
};
|
||||
|
||||
const TranscodeVideoJob: ActionItem = {
|
||||
title: getAssetJobName($t, AssetJobName.TranscodeVideo),
|
||||
title: $t('refresh_encoded_videos'),
|
||||
icon: mdiCogRefreshOutline,
|
||||
onAction: () => handleRunAssetJob({ name: AssetJobName.TranscodeVideo, assetIds: [asset.id] }),
|
||||
$if: () => asset.type === AssetTypeEnum.Video,
|
||||
@@ -313,12 +352,23 @@ export const handleReplaceAsset = async (oldAssetId: string) => {
|
||||
eventManager.emit('AssetReplace', { oldAssetId, newAssetId });
|
||||
};
|
||||
|
||||
const getAssetJobMessage = ($t: MessageFormatter, job: AssetJobName) => {
|
||||
const messages: Record<AssetJobName, string> = {
|
||||
[AssetJobName.RefreshFaces]: $t('refreshing_faces'),
|
||||
[AssetJobName.RefreshMetadata]: $t('refreshing_metadata'),
|
||||
[AssetJobName.RegenerateThumbnail]: $t('regenerating_thumbnails'),
|
||||
[AssetJobName.TranscodeVideo]: $t('refreshing_encoded_video'),
|
||||
};
|
||||
|
||||
return messages[job];
|
||||
};
|
||||
|
||||
const handleRunAssetJob = async (dto: AssetJobsDto) => {
|
||||
const $t = await getFormatter();
|
||||
|
||||
try {
|
||||
await runAssetJobs({ assetJobsDto: dto });
|
||||
toastManager.success(getAssetJobName($t, dto.name));
|
||||
toastManager.success(getAssetJobMessage($t, dto.name));
|
||||
} catch (error) {
|
||||
handleError(error, $t('errors.unable_to_submit_job'));
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ import { fromStore } from 'svelte/store';
|
||||
|
||||
export class AssetInteraction {
|
||||
selectedAssets = $state<TimelineAsset[]>([]);
|
||||
selectAll = $state(false);
|
||||
hasSelectedAsset(assetId: string) {
|
||||
return this.selectedAssets.some((asset) => asset.id === assetId);
|
||||
}
|
||||
@@ -65,6 +66,8 @@ export class AssetInteraction {
|
||||
}
|
||||
|
||||
clearMultiselect() {
|
||||
this.selectAll = false;
|
||||
|
||||
// Multi-selection
|
||||
this.selectedAssets = [];
|
||||
this.selectedGroup.clear();
|
||||
|
||||
@@ -1,3 +0,0 @@
|
||||
import { writable } from 'svelte/store';
|
||||
|
||||
export const isSelectingAllAssets = writable(false);
|
||||
@@ -9,7 +9,7 @@ export interface ThemeSetting {
|
||||
}
|
||||
|
||||
// Locale to use for formatting dates, numbers, etc.
|
||||
export const locale = persisted<string | undefined>('locale', 'default', {
|
||||
export const locale = persisted('locale', 'default', {
|
||||
serializer: {
|
||||
parse: (text) => text || 'default',
|
||||
stringify: (object) => object ?? '',
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import type { TimelineAsset } from '$lib/managers/timeline-manager/types';
|
||||
import type { QueueResponseDto, ServerVersionResponseDto } from '@immich/sdk';
|
||||
import type { ActionItem } from '@immich/ui';
|
||||
|
||||
@@ -40,3 +41,10 @@ export enum OnboardingRole {
|
||||
SERVER = 'server',
|
||||
USER = 'user',
|
||||
}
|
||||
|
||||
export type AssetControlContext = {
|
||||
// Wrap assets in a function, because context isn't reactive.
|
||||
getAssets: () => TimelineAsset[]; // All assets includes partners' assets
|
||||
getOwnedAssets: () => TimelineAsset[]; // Only assets owned by the user
|
||||
clearSelect: () => void;
|
||||
};
|
||||
|
||||
+1
-25
@@ -4,7 +4,6 @@ import { alwaysLoadOriginalFile, lang } from '$lib/stores/preferences.store';
|
||||
import { isWebCompatibleImage } from '$lib/utils/asset-utils';
|
||||
import { handleError } from '$lib/utils/handle-error';
|
||||
import {
|
||||
AssetJobName,
|
||||
AssetMediaSize,
|
||||
AssetTypeEnum,
|
||||
MemoryType,
|
||||
@@ -27,8 +26,7 @@ import {
|
||||
type UserResponseDto,
|
||||
} from '@immich/sdk';
|
||||
import { toastManager, type ActionItem, type IfLike } from '@immich/ui';
|
||||
import { mdiCogRefreshOutline, mdiDatabaseRefreshOutline, mdiHeadSyncOutline, mdiImageRefreshOutline } from '@mdi/js';
|
||||
import { init, register, t, type MessageFormatter } from 'svelte-i18n';
|
||||
import { init, register, t } from 'svelte-i18n';
|
||||
import { derived, get } from 'svelte/store';
|
||||
|
||||
interface DownloadRequestOptions<T = unknown> {
|
||||
@@ -246,28 +244,6 @@ export const getProfileImageUrl = (user: UserResponseDto) =>
|
||||
export const getPeopleThumbnailUrl = (person: PersonResponseDto, updatedAt?: string) =>
|
||||
createUrl(getPeopleThumbnailPath(person.id), { updatedAt: updatedAt ?? person.updatedAt });
|
||||
|
||||
export const getAssetJobName = ($t: MessageFormatter, job: AssetJobName) => {
|
||||
const messages: Record<AssetJobName, string> = {
|
||||
[AssetJobName.RefreshFaces]: $t('refreshing_faces'),
|
||||
[AssetJobName.RefreshMetadata]: $t('refreshing_metadata'),
|
||||
[AssetJobName.RegenerateThumbnail]: $t('regenerating_thumbnails'),
|
||||
[AssetJobName.TranscodeVideo]: $t('refreshing_encoded_video'),
|
||||
};
|
||||
|
||||
return messages[job];
|
||||
};
|
||||
|
||||
export const getAssetJobIcon = (job: AssetJobName) => {
|
||||
const names: Record<AssetJobName, string> = {
|
||||
[AssetJobName.RefreshFaces]: mdiHeadSyncOutline,
|
||||
[AssetJobName.RefreshMetadata]: mdiDatabaseRefreshOutline,
|
||||
[AssetJobName.RegenerateThumbnail]: mdiImageRefreshOutline,
|
||||
[AssetJobName.TranscodeVideo]: mdiCogRefreshOutline,
|
||||
};
|
||||
|
||||
return names[job];
|
||||
};
|
||||
|
||||
export const copyToClipboard = async (secret: string) => {
|
||||
const $t = get(t);
|
||||
|
||||
|
||||
@@ -7,7 +7,6 @@ import type { TimelineAsset } from '$lib/managers/timeline-manager/types';
|
||||
import { assetsSnapshot } from '$lib/managers/timeline-manager/utils.svelte';
|
||||
import { Route } from '$lib/route';
|
||||
import type { AssetInteraction } from '$lib/stores/asset-interaction.svelte';
|
||||
import { isSelectingAllAssets } from '$lib/stores/assets-store.svelte';
|
||||
import { preferences } from '$lib/stores/user.store';
|
||||
import { downloadRequest, withError } from '$lib/utils';
|
||||
import { getByteUnitString } from '$lib/utils/byte-units';
|
||||
@@ -295,6 +294,7 @@ export function getAssetRatio(asset: AssetResponseDto) {
|
||||
const supportedImageMimeTypes = new Set([
|
||||
'image/apng',
|
||||
'image/avif',
|
||||
'image/bmp',
|
||||
'image/gif',
|
||||
'image/jpeg',
|
||||
'image/png',
|
||||
@@ -303,9 +303,18 @@ const supportedImageMimeTypes = new Set([
|
||||
|
||||
const isSafari = /^((?!chrome|android).)*safari/i.test(navigator.userAgent); // https://stackoverflow.com/a/23522755
|
||||
if (isSafari) {
|
||||
supportedImageMimeTypes.add('image/heic').add('image/heif').add('image/jxl');
|
||||
supportedImageMimeTypes.add('image/heic').add('image/heif');
|
||||
}
|
||||
|
||||
function checkJxlSupport(): void {
|
||||
const img = new Image();
|
||||
img.addEventListener('load', () => {
|
||||
supportedImageMimeTypes.add('image/jxl');
|
||||
});
|
||||
img.src = 'data:image/jxl;base64,/woIAAAMABKIAgC4AF3lEgA='; // Small valid JPEG XL image
|
||||
}
|
||||
checkJxlSupport();
|
||||
|
||||
/**
|
||||
* Returns true if the asset is an image supported by web browsers, false otherwise
|
||||
*/
|
||||
@@ -426,17 +435,17 @@ export const keepThisDeleteOthers = async (keepAsset: AssetResponseDto, stack: S
|
||||
};
|
||||
|
||||
export const selectAllAssets = async (timelineManager: TimelineManager, assetInteraction: AssetInteraction) => {
|
||||
if (get(isSelectingAllAssets)) {
|
||||
if (assetInteraction.selectAll) {
|
||||
// Selection is already ongoing
|
||||
return;
|
||||
}
|
||||
isSelectingAllAssets.set(true);
|
||||
assetInteraction.selectAll = true;
|
||||
|
||||
try {
|
||||
for (const monthGroup of timelineManager.months) {
|
||||
await timelineManager.loadMonthGroup(monthGroup.yearMonth);
|
||||
|
||||
if (!get(isSelectingAllAssets)) {
|
||||
if (!assetInteraction.selectAll) {
|
||||
assetInteraction.clearMultiselect();
|
||||
break; // Cancelled
|
||||
}
|
||||
@@ -449,12 +458,12 @@ export const selectAllAssets = async (timelineManager: TimelineManager, assetInt
|
||||
} catch (error) {
|
||||
const $t = get(t);
|
||||
handleError(error, $t('errors.error_selecting_all_assets'));
|
||||
isSelectingAllAssets.set(false);
|
||||
assetInteraction.selectAll = false;
|
||||
}
|
||||
};
|
||||
|
||||
export const cancelMultiselect = (assetInteraction: AssetInteraction) => {
|
||||
isSelectingAllAssets.set(false);
|
||||
assetInteraction.selectAll = false;
|
||||
assetInteraction.clearMultiselect();
|
||||
};
|
||||
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import type { AssetControlContext } from '$lib/types';
|
||||
import { getContext, setContext } from 'svelte';
|
||||
|
||||
export function createContext<T>(key: string | symbol = Symbol()) {
|
||||
@@ -6,3 +7,5 @@ export function createContext<T>(key: string | symbol = Symbol()) {
|
||||
set: (context: T) => setContext<T>(key, context),
|
||||
};
|
||||
}
|
||||
|
||||
export const { get: getAssetControlContext, set: setAssetControlContext } = createContext<AssetControlContext>();
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
<script lang="ts">
|
||||
import { afterNavigate, goto, invalidateAll } from '$app/navigation';
|
||||
import ActionMenuItem from '$lib/components/ActionMenuItem.svelte';
|
||||
import UserPageLayout, { headerId } from '$lib/components/layouts/user-page-layout.svelte';
|
||||
import ButtonContextMenu from '$lib/components/shared-components/context-menu/button-context-menu.svelte';
|
||||
import GalleryViewer from '$lib/components/shared-components/gallery-viewer/gallery-viewer.svelte';
|
||||
@@ -9,7 +10,6 @@
|
||||
import Sidebar from '$lib/components/sidebar/sidebar.svelte';
|
||||
import AddToAlbum from '$lib/components/timeline/actions/AddToAlbumAction.svelte';
|
||||
import ArchiveAction from '$lib/components/timeline/actions/ArchiveAction.svelte';
|
||||
import AssetJobActions from '$lib/components/timeline/actions/AssetJobActions.svelte';
|
||||
import ChangeDate from '$lib/components/timeline/actions/ChangeDateAction.svelte';
|
||||
import ChangeDescription from '$lib/components/timeline/actions/ChangeDescriptionAction.svelte';
|
||||
import ChangeLocation from '$lib/components/timeline/actions/ChangeLocationAction.svelte';
|
||||
@@ -22,10 +22,12 @@
|
||||
import SkipLink from '$lib/elements/SkipLink.svelte';
|
||||
import type { Viewport } from '$lib/managers/timeline-manager/types';
|
||||
import { Route } from '$lib/route';
|
||||
import { getAssetBulkActions } from '$lib/services/asset.service';
|
||||
import { AssetInteraction } from '$lib/stores/asset-interaction.svelte';
|
||||
import { foldersStore } from '$lib/stores/folders.svelte';
|
||||
import { preferences } from '$lib/stores/user.store';
|
||||
import { cancelMultiselect } from '$lib/utils/asset-utils';
|
||||
import { getAssetControlContext } from '$lib/utils/context';
|
||||
import { toTimelineAsset } from '$lib/utils/timeline-util';
|
||||
import { joinPaths } from '$lib/utils/tree-utils';
|
||||
import { IconButton, Text } from '@immich/ui';
|
||||
@@ -117,6 +119,8 @@
|
||||
assets={assetInteraction.selectedAssets}
|
||||
clearSelect={() => cancelMultiselect(assetInteraction)}
|
||||
>
|
||||
{@const Actions = getAssetBulkActions($t, getAssetControlContext())}
|
||||
|
||||
<CreateSharedLink />
|
||||
<IconButton
|
||||
shape="round"
|
||||
@@ -155,7 +159,10 @@
|
||||
{/if}
|
||||
<DeleteAssets menuItem onAssetDelete={triggerAssetUpdate} onUndoDelete={triggerAssetUpdate} />
|
||||
<hr />
|
||||
<AssetJobActions />
|
||||
|
||||
<ActionMenuItem action={Actions.RegenerateThumbnailJob} />
|
||||
<ActionMenuItem action={Actions.RefreshMetadataJob} />
|
||||
<ActionMenuItem action={Actions.TranscodeVideoJob} />
|
||||
</ButtonContextMenu>
|
||||
</AssetSelectControlBar>
|
||||
</div>
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
<script lang="ts">
|
||||
import { goto } from '$app/navigation';
|
||||
import { page } from '$app/stores';
|
||||
import { focusTrap } from '$lib/actions/focus-trap';
|
||||
import { scrollMemory } from '$lib/actions/scroll-memory';
|
||||
import { shortcut } from '$lib/actions/shortcut';
|
||||
import ManagePeopleVisibility from '$lib/components/faces-page/manage-people-visibility.svelte';
|
||||
@@ -381,12 +380,10 @@
|
||||
|
||||
{#if selectHidden}
|
||||
<dialog
|
||||
open
|
||||
transition:fly={{ y: innerHeight, duration: 150, easing: quintOut, opacity: 0 }}
|
||||
class="absolute start-0 top-0 h-full w-full bg-light"
|
||||
aria-modal="true"
|
||||
class="fixed inset-0 h-full w-full max-w-none max-h-none bg-light"
|
||||
aria-labelledby="manage-visibility-title"
|
||||
use:focusTrap
|
||||
{@attach (dialog) => dialog.showModal()}
|
||||
>
|
||||
<ManagePeopleVisibility
|
||||
{people}
|
||||
|
||||
+14
-1
@@ -297,6 +297,14 @@
|
||||
person = response;
|
||||
};
|
||||
|
||||
const handlePersonAssetDelete = async ({ id, assetId }: { id: string; assetId: string }) => {
|
||||
if (id !== person.id) {
|
||||
return;
|
||||
}
|
||||
timelineManager.removeAssets([assetId]);
|
||||
await updateAssetCount();
|
||||
};
|
||||
|
||||
const { SetDateOfBirth, Favorite, Unfavorite, HidePerson, ShowPerson } = $derived(getPersonActions($t, person));
|
||||
const SelectFeaturePhoto: ActionItem = {
|
||||
title: $t('select_featured_photo'),
|
||||
@@ -315,7 +323,12 @@
|
||||
};
|
||||
</script>
|
||||
|
||||
<OnEvents {onPersonUpdate} onAssetsDelete={updateAssetCount} onAssetsArchive={updateAssetCount} />
|
||||
<OnEvents
|
||||
{onPersonUpdate}
|
||||
onPersonAssetDelete={handlePersonAssetDelete}
|
||||
onAssetsDelete={updateAssetCount}
|
||||
onAssetsArchive={updateAssetCount}
|
||||
/>
|
||||
|
||||
<main
|
||||
class="relative z-0 h-dvh overflow-hidden px-2 md:px-6 md:pt-(--navbar-height-md) pt-(--navbar-height)"
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
<script lang="ts">
|
||||
import { beforeNavigate } from '$app/navigation';
|
||||
import ActionMenuItem from '$lib/components/ActionMenuItem.svelte';
|
||||
import UserPageLayout from '$lib/components/layouts/user-page-layout.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 AddToAlbum from '$lib/components/timeline/actions/AddToAlbumAction.svelte';
|
||||
import ArchiveAction from '$lib/components/timeline/actions/ArchiveAction.svelte';
|
||||
import AssetJobActions from '$lib/components/timeline/actions/AssetJobActions.svelte';
|
||||
import ChangeDate from '$lib/components/timeline/actions/ChangeDateAction.svelte';
|
||||
import ChangeDescription from '$lib/components/timeline/actions/ChangeDescriptionAction.svelte';
|
||||
import ChangeLocation from '$lib/components/timeline/actions/ChangeLocationAction.svelte';
|
||||
@@ -23,6 +23,7 @@
|
||||
import { AssetAction } from '$lib/constants';
|
||||
import { TimelineManager } from '$lib/managers/timeline-manager/timeline-manager.svelte';
|
||||
import { Route } from '$lib/route';
|
||||
import { getAssetBulkActions } from '$lib/services/asset.service';
|
||||
import { AssetInteraction } from '$lib/stores/asset-interaction.svelte';
|
||||
import { assetViewingStore } from '$lib/stores/asset-viewing.store';
|
||||
import { isFaceEditMode } from '$lib/stores/face-edit.svelte';
|
||||
@@ -35,6 +36,7 @@
|
||||
type OnLink,
|
||||
type OnUnlink,
|
||||
} from '$lib/utils/actions';
|
||||
import { getAssetControlContext } from '$lib/utils/context';
|
||||
import { openFileUploadDialog } from '$lib/utils/file-uploader';
|
||||
import { getAltText } from '$lib/utils/thumbnail-util';
|
||||
import { toTimelineAsset } from '$lib/utils/timeline-util';
|
||||
@@ -128,6 +130,8 @@
|
||||
assets={assetInteraction.selectedAssets}
|
||||
clearSelect={() => assetInteraction.clearMultiselect()}
|
||||
>
|
||||
{@const Actions = getAssetBulkActions($t, getAssetControlContext())}
|
||||
|
||||
<CreateSharedLink />
|
||||
<SelectAllAssets {timelineManager} {assetInteraction} />
|
||||
<ButtonContextMenu icon={mdiPlus} title={$t('add_to')}>
|
||||
@@ -175,7 +179,9 @@
|
||||
/>
|
||||
<SetVisibilityAction menuItem onVisibilitySet={handleSetVisibility} />
|
||||
<hr />
|
||||
<AssetJobActions />
|
||||
<ActionMenuItem action={Actions.RegenerateThumbnailJob} />
|
||||
<ActionMenuItem action={Actions.RefreshMetadataJob} />
|
||||
<ActionMenuItem action={Actions.TranscodeVideoJob} />
|
||||
</ButtonContextMenu>
|
||||
{:else}
|
||||
<DownloadAction />
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
<script lang="ts">
|
||||
import { afterNavigate, goto } from '$app/navigation';
|
||||
import { page } from '$app/state';
|
||||
import ActionMenuItem from '$lib/components/ActionMenuItem.svelte';
|
||||
import ButtonContextMenu from '$lib/components/shared-components/context-menu/button-context-menu.svelte';
|
||||
import ControlAppBar from '$lib/components/shared-components/control-app-bar.svelte';
|
||||
import GalleryViewer from '$lib/components/shared-components/gallery-viewer/gallery-viewer.svelte';
|
||||
import SearchBar from '$lib/components/shared-components/search-bar/search-bar.svelte';
|
||||
import AddToAlbum from '$lib/components/timeline/actions/AddToAlbumAction.svelte';
|
||||
import ArchiveAction from '$lib/components/timeline/actions/ArchiveAction.svelte';
|
||||
import AssetJobActions from '$lib/components/timeline/actions/AssetJobActions.svelte';
|
||||
import ChangeDate from '$lib/components/timeline/actions/ChangeDateAction.svelte';
|
||||
import ChangeDescription from '$lib/components/timeline/actions/ChangeDescriptionAction.svelte';
|
||||
import ChangeLocation from '$lib/components/timeline/actions/ChangeLocationAction.svelte';
|
||||
@@ -22,11 +22,13 @@
|
||||
import { featureFlagsManager } from '$lib/managers/feature-flags-manager.svelte';
|
||||
import type { Viewport } from '$lib/managers/timeline-manager/types';
|
||||
import { Route } from '$lib/route';
|
||||
import { getAssetBulkActions } from '$lib/services/asset.service';
|
||||
import { AssetInteraction } from '$lib/stores/asset-interaction.svelte';
|
||||
import { lang, locale } from '$lib/stores/preferences.store';
|
||||
import { preferences, user } from '$lib/stores/user.store';
|
||||
import { handlePromiseError } from '$lib/utils';
|
||||
import { cancelMultiselect } from '$lib/utils/asset-utils';
|
||||
import { getAssetControlContext } from '$lib/utils/context';
|
||||
import { parseUtcDate } from '$lib/utils/date-time';
|
||||
import { handleError } from '$lib/utils/handle-error';
|
||||
import { isAlbumsRoute, isPeopleRoute } from '$lib/utils/navigation';
|
||||
@@ -326,6 +328,8 @@
|
||||
assets={assetInteraction.selectedAssets}
|
||||
clearSelect={() => cancelMultiselect(assetInteraction)}
|
||||
>
|
||||
{@const Actions = getAssetBulkActions($t, getAssetControlContext())}
|
||||
|
||||
<CreateSharedLink />
|
||||
<IconButton
|
||||
shape="round"
|
||||
@@ -366,7 +370,9 @@
|
||||
{/if}
|
||||
<DeleteAssets menuItem {onAssetDelete} onUndoDelete={onSearchQueryUpdate} />
|
||||
<hr />
|
||||
<AssetJobActions />
|
||||
<ActionMenuItem action={Actions.RegenerateThumbnailJob} />
|
||||
<ActionMenuItem action={Actions.RefreshMetadataJob} />
|
||||
<ActionMenuItem action={Actions.TranscodeVideoJob} />
|
||||
</ButtonContextMenu>
|
||||
{:else}
|
||||
<DownloadAction />
|
||||
|
||||
@@ -13,6 +13,7 @@
|
||||
import { themeManager } from '$lib/managers/theme-manager.svelte';
|
||||
import ServerRestartingModal from '$lib/modals/ServerRestartingModal.svelte';
|
||||
import { Route } from '$lib/route';
|
||||
import { locale } from '$lib/stores/preferences.store';
|
||||
import { sidebarStore } from '$lib/stores/sidebar.svelte';
|
||||
import { user } from '$lib/stores/user.store';
|
||||
import { closeWebsocketConnection, openWebsocketConnection, websocketStore } from '$lib/stores/websocket';
|
||||
@@ -23,6 +24,7 @@
|
||||
CommandPaletteDefaultProvider,
|
||||
TooltipProvider,
|
||||
modalManager,
|
||||
setLocale,
|
||||
setTranslations,
|
||||
toastManager,
|
||||
type ActionItem,
|
||||
@@ -52,6 +54,8 @@
|
||||
});
|
||||
});
|
||||
|
||||
$effect(() => setLocale($locale));
|
||||
|
||||
let { children }: Props = $props();
|
||||
|
||||
let showNavigationLoadingBar = $state(false);
|
||||
@@ -60,7 +64,7 @@
|
||||
return new URL(page.url.pathname + page.url.search, 'https://my.immich.app');
|
||||
};
|
||||
|
||||
toastManager.setOptions({ class: 'top-16' });
|
||||
toastManager.setOptions({ class: 'top-16 fixed' });
|
||||
|
||||
onMount(() => {
|
||||
const element = document.querySelector('#stencil');
|
||||
|
||||
@@ -53,7 +53,7 @@
|
||||
|
||||
<div class="flex gap-1 mb-4">
|
||||
<Badge>{$t('active_count', { values: { count: queue.statistics.active } })}</Badge>
|
||||
<Badge>{$t('waiting_count', { values: { count: queue.statistics.waiting } })}</Badge>
|
||||
<Badge>{$t('waiting_count', { values: { count: queue.statistics.waiting + queue.statistics.paused } })}</Badge>
|
||||
{#if queue.statistics.failed > 0}
|
||||
<Badge color="danger">{$t('failed_count', { values: { count: queue.statistics.failed } })}</Badge>
|
||||
{/if}
|
||||
|
||||
Reference in New Issue
Block a user