mirror of
https://github.com/immich-app/immich.git
synced 2026-02-03 18:48:01 -08:00
Compare commits
10 Commits
docs/contr
...
v2.5.3
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
38c1f0b1fd | ||
|
|
5212bca3d0 | ||
|
|
2990bde0bb | ||
|
|
af1ecaf5cc | ||
|
|
3870ebc3c6 | ||
|
|
0a9d969b47 | ||
|
|
94965f6d66 | ||
|
|
8872d2c7ae | ||
|
|
23445fdcc1 | ||
|
|
25f2273e24 |
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@immich/cli",
|
||||
"version": "2.5.2",
|
||||
"version": "2.5.3",
|
||||
"description": "Command Line Interface (CLI) for Immich",
|
||||
"type": "module",
|
||||
"exports": "./dist/index.js",
|
||||
|
||||
4
docs/static/archived-versions.json
vendored
4
docs/static/archived-versions.json
vendored
@@ -1,7 +1,7 @@
|
||||
[
|
||||
{
|
||||
"label": "v2.5.2",
|
||||
"url": "https://docs.v2.5.2.archive.immich.app"
|
||||
"label": "v2.5.3",
|
||||
"url": "https://docs.v2.5.3.archive.immich.app"
|
||||
},
|
||||
{
|
||||
"label": "v2.4.1",
|
||||
|
||||
@@ -70,7 +70,7 @@ services:
|
||||
restart: unless-stopped
|
||||
|
||||
redis:
|
||||
image: redis:6.2-alpine@sha256:37e002448575b32a599109664107e374c8709546905c372a34d64919043b9ceb
|
||||
image: redis:6.2-alpine@sha256:46884be93652d02a96a176ccf173d1040bef365c5706aa7b6a1931caec8bfeef
|
||||
|
||||
database:
|
||||
image: ghcr.io/immich-app/postgres:14-vectorchord0.3.0@sha256:6f3e9d2c2177af16c2988ff71425d79d89ca630ec2f9c8db03209ab716542338
|
||||
|
||||
@@ -42,7 +42,7 @@ services:
|
||||
- 2285:2285
|
||||
|
||||
redis:
|
||||
image: redis:6.2-alpine@sha256:37e002448575b32a599109664107e374c8709546905c372a34d64919043b9ceb
|
||||
image: redis:6.2-alpine@sha256:46884be93652d02a96a176ccf173d1040bef365c5706aa7b6a1931caec8bfeef
|
||||
|
||||
database:
|
||||
image: ghcr.io/immich-app/postgres:14-vectorchord0.3.0@sha256:6f3e9d2c2177af16c2988ff71425d79d89ca630ec2f9c8db03209ab716542338
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "immich-e2e",
|
||||
"version": "2.5.2",
|
||||
"version": "2.5.3",
|
||||
"description": "",
|
||||
"main": "index.js",
|
||||
"type": "module",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "immich-i18n",
|
||||
"version": "2.5.2",
|
||||
"version": "2.5.3",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"format": "prettier --check .",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[project]
|
||||
name = "immich-ml"
|
||||
version = "2.5.2"
|
||||
version = "2.5.3"
|
||||
description = ""
|
||||
authors = [{ name = "Hau Tran", email = "alex.tran1502@gmail.com" }]
|
||||
requires-python = ">=3.11,<4.0"
|
||||
|
||||
2
machine-learning/uv.lock
generated
2
machine-learning/uv.lock
generated
@@ -882,7 +882,7 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "immich-ml"
|
||||
version = "2.5.2"
|
||||
version = "2.5.3"
|
||||
source = { editable = "." }
|
||||
dependencies = [
|
||||
{ name = "aiocache" },
|
||||
|
||||
@@ -35,8 +35,8 @@ platform :android do
|
||||
task: 'bundle',
|
||||
build_type: 'Release',
|
||||
properties: {
|
||||
"android.injected.version.code" => 3033,
|
||||
"android.injected.version.name" => "2.5.2",
|
||||
"android.injected.version.code" => 3034,
|
||||
"android.injected.version.name" => "2.5.3",
|
||||
}
|
||||
)
|
||||
upload_to_play_store(skip_upload_apk: true, skip_upload_images: true, skip_upload_screenshots: true, aab: '../build/app/outputs/bundle/release/app-release.aab')
|
||||
|
||||
@@ -133,7 +133,6 @@ class LocalImageApiImpl: LocalImageApi {
|
||||
"height": Int64(buffer.height),
|
||||
"rowBytes": Int64(buffer.rowBytes)
|
||||
]))
|
||||
print("Successful response for \(requestId)")
|
||||
Self.remove(requestId: requestId)
|
||||
} catch {
|
||||
Self.remove(requestId: requestId)
|
||||
|
||||
@@ -80,7 +80,7 @@
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>APPL</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>2.5.2</string>
|
||||
<string>2.5.3</string>
|
||||
<key>CFBundleSignature</key>
|
||||
<string>????</string>
|
||||
<key>CFBundleURLTypes</key>
|
||||
|
||||
@@ -33,7 +33,7 @@ class _DriftAlbumsPageState extends ConsumerState<DriftAlbumsPage> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final albumCount = ref.watch(remoteAlbumProvider.select((state) => state.albums.length));
|
||||
final showScrollbar = albumCount > 10;
|
||||
final showScrollbar = albumCount > 20;
|
||||
|
||||
final scrollView = CustomScrollView(
|
||||
controller: _scrollController,
|
||||
|
||||
@@ -87,7 +87,7 @@ class _AlbumSelectorState extends ConsumerState<AlbumSelector> {
|
||||
}
|
||||
|
||||
void onSearch(String searchTerm, QuickFilterMode filterMode) {
|
||||
final userId = ref.watch(currentUserProvider)?.id;
|
||||
final userId = ref.read(currentUserProvider)?.id;
|
||||
filter = filter.copyWith(query: searchTerm, userId: userId, mode: filterMode);
|
||||
|
||||
filterAlbums();
|
||||
@@ -186,7 +186,7 @@ class _AlbumSelectorState extends ConsumerState<AlbumSelector> {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final userId = ref.watch(currentUserProvider)?.id;
|
||||
final userId = ref.watch(currentUserProvider.select((user) => user?.id));
|
||||
|
||||
// refilter and sort when albums change
|
||||
ref.listen(remoteAlbumProvider.select((state) => state.albums), (_, _) async {
|
||||
|
||||
@@ -259,6 +259,11 @@ class DriftBackupNotifier extends StateNotifier<DriftBackupState> {
|
||||
}
|
||||
|
||||
Future<void> startForegroundBackup(String userId) async {
|
||||
// Cancel any existing backup before starting a new one
|
||||
if (state.cancelToken != null) {
|
||||
await stopForegroundBackup();
|
||||
}
|
||||
|
||||
state = state.copyWith(error: BackupError.none);
|
||||
|
||||
final cancelToken = CancellationToken();
|
||||
@@ -375,21 +380,21 @@ class DriftBackupNotifier extends StateNotifier<DriftBackupState> {
|
||||
_logger.warning("Skip handleBackupResume (pre-call): notifier disposed");
|
||||
return;
|
||||
}
|
||||
_logger.info("Resuming backup tasks...");
|
||||
_logger.info("Start background backup sequence");
|
||||
state = state.copyWith(error: BackupError.none);
|
||||
final tasks = await _backgroundUploadService.getActiveTasks(kBackupGroup);
|
||||
if (!mounted) {
|
||||
_logger.warning("Skip handleBackupResume (post-call): notifier disposed");
|
||||
return;
|
||||
}
|
||||
_logger.info("Found ${tasks.length} tasks");
|
||||
_logger.info("Found ${tasks.length} pending tasks");
|
||||
|
||||
if (tasks.isEmpty) {
|
||||
_logger.info("Start backup with URLSession");
|
||||
_logger.info("No pending tasks, starting new upload");
|
||||
return _backgroundUploadService.uploadBackupCandidates(userId);
|
||||
}
|
||||
|
||||
_logger.info("Tasks to resume: ${tasks.length}");
|
||||
_logger.info("Resuming upload ${tasks.length} assets");
|
||||
return _backgroundUploadService.resume();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -164,9 +164,12 @@ class BackgroundUploadService {
|
||||
|
||||
final candidates = await _backupRepository.getCandidates(userId);
|
||||
if (candidates.isEmpty) {
|
||||
_logger.info("No new backup candidates found, finishing background upload");
|
||||
return;
|
||||
}
|
||||
|
||||
_logger.info("Found ${candidates.length} backup candidates for background tasks");
|
||||
|
||||
const batchSize = 100;
|
||||
final batch = candidates.take(batchSize).toList();
|
||||
List<UploadTask> tasks = [];
|
||||
@@ -179,6 +182,7 @@ class BackgroundUploadService {
|
||||
}
|
||||
|
||||
if (tasks.isNotEmpty && !shouldAbortQueuingTasks) {
|
||||
_logger.info("Enqueuing ${tasks.length} background upload tasks");
|
||||
await enqueueTasks(tasks);
|
||||
}
|
||||
}
|
||||
|
||||
2
mobile/openapi/README.md
generated
2
mobile/openapi/README.md
generated
@@ -3,7 +3,7 @@ Immich API
|
||||
|
||||
This Dart package is automatically generated by the [OpenAPI Generator](https://openapi-generator.tech) project:
|
||||
|
||||
- API version: 2.5.2
|
||||
- API version: 2.5.3
|
||||
- Generator version: 7.8.0
|
||||
- Build package: org.openapitools.codegen.languages.DartClientCodegen
|
||||
|
||||
|
||||
@@ -1249,10 +1249,10 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: meta
|
||||
sha256: e3641ec5d63ebf0d9b41bd43201a66e3fc79a65db5f61fc181f04cd27aab950c
|
||||
sha256: "23f08335362185a5ea2ad3a4e597f1375e78bce8a040df5c600c8d3552ef2394"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.16.0"
|
||||
version: "1.17.0"
|
||||
mime:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -1942,10 +1942,10 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: test_api
|
||||
sha256: "522f00f556e73044315fa4585ec3270f1808a4b186c936e612cab0b565ff1e00"
|
||||
sha256: ab2726c1a94d3176a45960b6234466ec367179b87dd74f1611adb1f3b5fb9d55
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.7.6"
|
||||
version: "0.7.7"
|
||||
thumbhash:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
|
||||
@@ -2,7 +2,7 @@ name: immich_mobile
|
||||
description: Immich - selfhosted backup media file on mobile phone
|
||||
|
||||
publish_to: 'none'
|
||||
version: 2.5.2+3033
|
||||
version: 2.5.3+3034
|
||||
|
||||
environment:
|
||||
sdk: '>=3.8.0 <4.0.0'
|
||||
|
||||
@@ -15057,7 +15057,7 @@
|
||||
"info": {
|
||||
"title": "Immich",
|
||||
"description": "Immich API",
|
||||
"version": "2.5.2",
|
||||
"version": "2.5.3",
|
||||
"contact": {}
|
||||
},
|
||||
"tags": [
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@immich/sdk",
|
||||
"version": "2.5.2",
|
||||
"version": "2.5.3",
|
||||
"description": "Auto-generated TypeScript SDK for the Immich API",
|
||||
"type": "module",
|
||||
"main": "./build/index.js",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/**
|
||||
* Immich
|
||||
* 2.5.2
|
||||
* 2.5.3
|
||||
* DO NOT MODIFY - This file has been generated using oazapfts.
|
||||
* See https://www.npmjs.com/package/oazapfts
|
||||
*/
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "immich-monorepo",
|
||||
"version": "2.5.2",
|
||||
"version": "2.5.3",
|
||||
"description": "Monorepo for Immich",
|
||||
"private": true,
|
||||
"packageManager": "pnpm@10.28.0+sha512.05df71d1421f21399e053fde567cea34d446fa02c76571441bfc1c7956e98e363088982d940465fd34480d4d90a0668bc12362f8aa88000a64e83d0b0e47be48",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "immich",
|
||||
"version": "2.5.2",
|
||||
"version": "2.5.3",
|
||||
"description": "",
|
||||
"author": "",
|
||||
"private": true,
|
||||
|
||||
@@ -307,7 +307,6 @@ export class MetadataService extends BaseService {
|
||||
const assetHeight = isSidewards ? validate(width) : validate(height);
|
||||
|
||||
const promises: Promise<unknown>[] = [
|
||||
this.assetRepository.upsertExif(exifData, { lockedPropertiesBehavior: 'skip' }),
|
||||
this.assetRepository.update({
|
||||
id: asset.id,
|
||||
duration: this.getDuration(exifTags),
|
||||
@@ -322,6 +321,7 @@ export class MetadataService extends BaseService {
|
||||
}),
|
||||
];
|
||||
|
||||
await this.assetRepository.upsertExif(exifData, { lockedPropertiesBehavior: 'skip' });
|
||||
await this.applyTagList(asset);
|
||||
|
||||
if (this.isMotionPhoto(asset, exifTags)) {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "immich-web",
|
||||
"version": "2.5.2",
|
||||
"version": "2.5.3",
|
||||
"license": "GNU Affero General Public License version 3",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
|
||||
@@ -14,6 +14,7 @@
|
||||
import { eventManager } from '$lib/managers/event-manager.svelte';
|
||||
import { imageManager } from '$lib/managers/ImageManager.svelte';
|
||||
import { Route } from '$lib/route';
|
||||
import { getAssetActions } from '$lib/services/asset.service';
|
||||
import { assetViewingStore } from '$lib/stores/asset-viewing.store';
|
||||
import { ocrManager } from '$lib/stores/ocr.svelte';
|
||||
import { alwaysLoadOriginalVideo } from '$lib/stores/preferences.store';
|
||||
@@ -36,6 +37,7 @@
|
||||
type PersonResponseDto,
|
||||
type StackResponseDto,
|
||||
} from '@immich/sdk';
|
||||
import { CommandPaletteDefaultProvider } from '@immich/ui';
|
||||
import { onDestroy, onMount, untrack } from 'svelte';
|
||||
import { t } from 'svelte-i18n';
|
||||
import { fly } from 'svelte/transition';
|
||||
@@ -426,8 +428,11 @@
|
||||
!assetViewerManager.isShowEditor &&
|
||||
ocrManager.hasOcrData,
|
||||
);
|
||||
|
||||
const { Tag } = $derived(getAssetActions($t, asset));
|
||||
</script>
|
||||
|
||||
<CommandPaletteDefaultProvider name={$t('assets')} actions={[Tag]} />
|
||||
<OnEvents {onAssetReplace} {onAssetUpdate} />
|
||||
|
||||
<svelte:document bind:fullscreenElement />
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
<script lang="ts">
|
||||
import { shortcut } from '$lib/actions/shortcut';
|
||||
import HeaderActionButton from '$lib/components/HeaderActionButton.svelte';
|
||||
import OnEvents from '$lib/components/OnEvents.svelte';
|
||||
import { authManager } from '$lib/managers/auth-manager.svelte';
|
||||
import AssetTagModal from '$lib/modals/AssetTagModal.svelte';
|
||||
import { Route } from '$lib/route';
|
||||
import { getAssetActions } from '$lib/services/asset.service';
|
||||
import { removeTag } from '$lib/utils/asset-utils';
|
||||
import { getAssetInfo, type AssetResponseDto } from '@immich/sdk';
|
||||
import { Icon, modalManager, Text } from '@immich/ui';
|
||||
import { mdiClose, mdiPlus } from '@mdi/js';
|
||||
import { Badge, IconButton, Link, Text } from '@immich/ui';
|
||||
import { mdiClose } from '@mdi/js';
|
||||
import { t } from 'svelte-i18n';
|
||||
|
||||
interface Props {
|
||||
@@ -18,22 +19,23 @@
|
||||
|
||||
let tags = $derived(asset.tags || []);
|
||||
|
||||
const handleAddTag = async () => {
|
||||
const success = await modalManager.show(AssetTagModal, { assetIds: [asset.id] });
|
||||
if (success) {
|
||||
asset = await getAssetInfo({ id: asset.id });
|
||||
}
|
||||
};
|
||||
|
||||
const handleRemove = async (tagId: string) => {
|
||||
const ids = await removeTag({ tagIds: [tagId], assetIds: [asset.id], showNotification: false });
|
||||
if (ids) {
|
||||
asset = await getAssetInfo({ id: asset.id });
|
||||
}
|
||||
};
|
||||
|
||||
const onAssetsTag = async (ids: string[]) => {
|
||||
if (ids.includes(asset.id)) {
|
||||
asset = await getAssetInfo({ id: asset.id });
|
||||
}
|
||||
};
|
||||
|
||||
const { Tag } = $derived(getAssetActions($t, asset));
|
||||
</script>
|
||||
|
||||
<svelte:document use:shortcut={{ shortcut: { key: 't' }, onShortcut: handleAddTag }} />
|
||||
<OnEvents {onAssetsTag} />
|
||||
|
||||
{#if isOwner && !authManager.isSharedLink}
|
||||
<section class="px-4 mt-4">
|
||||
@@ -42,36 +44,24 @@
|
||||
</div>
|
||||
<section class="flex flex-wrap pt-2 gap-1" data-testid="detail-panel-tags">
|
||||
{#each tags as tag (tag.id)}
|
||||
<div class="flex group transition-all">
|
||||
<a
|
||||
class="inline-block h-min whitespace-nowrap ps-3 pe-1 group-hover:ps-3 py-1 text-center align-baseline leading-none text-gray-100 dark:text-immich-dark-gray bg-primary rounded-s-full hover:bg-immich-primary/80 dark:hover:bg-immich-dark-primary/80 transition-all"
|
||||
<Badge size="small" class="items-center px-0" shape="round">
|
||||
<Link
|
||||
href={Route.tags({ path: tag.value })}
|
||||
class="text-light no-underline rounded-full hover:bg-primary-400 px-2"
|
||||
>
|
||||
<p class="text-sm">
|
||||
{tag.value}
|
||||
</p>
|
||||
</a>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
class="text-gray-100 dark:text-immich-dark-gray bg-immich-primary/95 dark:bg-immich-dark-primary/95 rounded-e-full place-items-center place-content-center pe-2 ps-1 py-1 hover:bg-immich-primary/80 dark:hover:bg-immich-dark-primary/80 transition-all"
|
||||
title="Remove tag"
|
||||
{tag.value}
|
||||
</Link>
|
||||
<IconButton
|
||||
aria-label={$t('remove_tag')}
|
||||
icon={mdiClose}
|
||||
onclick={() => handleRemove(tag.id)}
|
||||
>
|
||||
<Icon icon={mdiClose} />
|
||||
</button>
|
||||
</div>
|
||||
size="tiny"
|
||||
class="hover:bg-primary-400"
|
||||
shape="round"
|
||||
/>
|
||||
</Badge>
|
||||
{/each}
|
||||
<button
|
||||
type="button"
|
||||
class="rounded-full bg-gray-100 dark:bg-gray-800 text-gray-600 dark:text-gray-300 hover:bg-gray-200 dark:hover:bg-gray-700 hover:text-gray-700 dark:hover:text-gray-200 flex place-items-center place-content-center gap-1 px-2 py-1"
|
||||
title={$t('add_tag')}
|
||||
onclick={handleAddTag}
|
||||
>
|
||||
<span class="text-sm px-1 flex place-items-center place-content-center gap-1"
|
||||
><Icon icon={mdiPlus} />{$t('add')}</span
|
||||
>
|
||||
</button>
|
||||
<HeaderActionButton action={Tag} />
|
||||
</section>
|
||||
</section>
|
||||
{/if}
|
||||
|
||||
@@ -55,13 +55,10 @@
|
||||
|
||||
let loader = $state<HTMLImageElement>();
|
||||
|
||||
assetViewerManager.zoomState = {
|
||||
currentRotation: 0,
|
||||
currentZoom: 1,
|
||||
enable: true,
|
||||
currentPositionX: 0,
|
||||
currentPositionY: 0,
|
||||
};
|
||||
$effect.pre(() => {
|
||||
void asset.id;
|
||||
untrack(() => assetViewerManager.resetZoomState());
|
||||
});
|
||||
|
||||
onDestroy(() => {
|
||||
$boundingBoxesArray = [];
|
||||
|
||||
@@ -20,11 +20,8 @@
|
||||
|
||||
const handleTagAssets = async () => {
|
||||
const assets = [...getOwnedAssets()];
|
||||
const success = await modalManager.show(AssetTagModal, { assetIds: assets.map(({ id }) => id) });
|
||||
|
||||
if (success) {
|
||||
clearSelect();
|
||||
}
|
||||
await modalManager.show(AssetTagModal, { assetIds: assets.map(({ id }) => id) });
|
||||
clearSelect();
|
||||
};
|
||||
</script>
|
||||
|
||||
|
||||
@@ -5,6 +5,14 @@ import type { ZoomImageWheelState } from '@zoom-image/core';
|
||||
|
||||
const isShowDetailPanel = new PersistedLocalStorage<boolean>('asset-viewer-state', false);
|
||||
|
||||
const createDefaultZoomState = (): ZoomImageWheelState => ({
|
||||
currentRotation: 0,
|
||||
currentZoom: 1,
|
||||
enable: true,
|
||||
currentPositionX: 0,
|
||||
currentPositionY: 0,
|
||||
});
|
||||
|
||||
export type Events = {
|
||||
Zoom: [];
|
||||
ZoomChange: [ZoomImageWheelState];
|
||||
@@ -12,13 +20,7 @@ export type Events = {
|
||||
};
|
||||
|
||||
export class AssetViewerManager extends BaseEventManager<Events> {
|
||||
#zoomState = $state<ZoomImageWheelState>({
|
||||
currentRotation: 0,
|
||||
currentZoom: 1,
|
||||
enable: true,
|
||||
currentPositionX: 0,
|
||||
currentPositionY: 0,
|
||||
});
|
||||
#zoomState = $state(createDefaultZoomState());
|
||||
|
||||
imgRef = $state<HTMLImageElement | undefined>();
|
||||
isShowActivityPanel = $state(false);
|
||||
@@ -67,6 +69,10 @@ export class AssetViewerManager extends BaseEventManager<Events> {
|
||||
this.#zoomState = state;
|
||||
}
|
||||
|
||||
resetZoomState() {
|
||||
this.zoomState = createDefaultZoomState();
|
||||
}
|
||||
|
||||
toggleActivityPanel() {
|
||||
this.closeDetailPanel();
|
||||
this.isShowActivityPanel = !this.isShowActivityPanel;
|
||||
|
||||
@@ -37,6 +37,7 @@ export type Events = {
|
||||
AssetsArchive: [string[]];
|
||||
AssetsDelete: [string[]];
|
||||
AssetEditsApplied: [string];
|
||||
AssetsTag: [string[]];
|
||||
|
||||
AlbumAddAssets: [];
|
||||
AlbumUpdate: [AlbumResponseDto];
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
<script lang="ts">
|
||||
import { eventManager } from '$lib/managers/event-manager.svelte';
|
||||
import { tagAssets } from '$lib/utils/asset-utils';
|
||||
import { getAllTags, upsertTags, type TagResponseDto } from '@immich/sdk';
|
||||
import { FormModal, Icon } from '@immich/ui';
|
||||
@@ -9,7 +10,7 @@
|
||||
import Combobox, { type ComboBoxOption } from '../components/shared-components/combobox.svelte';
|
||||
|
||||
interface Props {
|
||||
onClose: (success?: true) => void;
|
||||
onClose: () => void;
|
||||
assetIds: string[];
|
||||
}
|
||||
|
||||
@@ -30,8 +31,8 @@
|
||||
return;
|
||||
}
|
||||
|
||||
await tagAssets({ tagIds: [...selectedIds], assetIds, showNotification: false });
|
||||
onClose(true);
|
||||
const updatedIds = await tagAssets({ tagIds: [...selectedIds], assetIds, showNotification: false });
|
||||
eventManager.emit('AssetsTag', updatedIds);
|
||||
};
|
||||
|
||||
const handleSelect = async (option?: ComboBoxOption) => {
|
||||
|
||||
@@ -2,6 +2,7 @@ import { ProjectionType } from '$lib/constants';
|
||||
import { assetViewerManager } from '$lib/managers/asset-viewer-manager.svelte';
|
||||
import { authManager } from '$lib/managers/auth-manager.svelte';
|
||||
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';
|
||||
@@ -41,6 +42,7 @@ import {
|
||||
mdiMotionPauseOutline,
|
||||
mdiMotionPlayOutline,
|
||||
mdiShareVariantOutline,
|
||||
mdiTagPlusOutline,
|
||||
mdiTune,
|
||||
} from '@mdi/js';
|
||||
import type { MessageFormatter } from 'svelte-i18n';
|
||||
@@ -49,6 +51,7 @@ import { get } from 'svelte/store';
|
||||
export const getAssetActions = ($t: MessageFormatter, asset: AssetResponseDto) => {
|
||||
const sharedLink = getSharedLink();
|
||||
const currentAuthUser = get(authUser);
|
||||
const userPreferences = get(preferences);
|
||||
const isOwner = !!(currentAuthUser && currentAuthUser.id === asset.ownerId);
|
||||
|
||||
const Share: ActionItem = {
|
||||
@@ -155,7 +158,16 @@ export const getAssetActions = ($t: MessageFormatter, asset: AssetResponseDto) =
|
||||
type: $t('assets'),
|
||||
$if: () => asset.hasMetadata,
|
||||
onAction: () => assetViewerManager.toggleDetailPanel(),
|
||||
shortcuts: [{ key: 'i' }],
|
||||
shortcuts: { key: 'i' },
|
||||
};
|
||||
|
||||
const Tag: ActionItem = {
|
||||
title: $t('add_tag'),
|
||||
icon: mdiTagPlusOutline,
|
||||
type: $t('assets'),
|
||||
$if: () => userPreferences.tags.enabled,
|
||||
onAction: () => modalManager.show(AssetTagModal, { assetIds: [asset.id] }),
|
||||
shortcuts: { key: 't' },
|
||||
};
|
||||
|
||||
const Edit: ActionItem = {
|
||||
@@ -212,6 +224,7 @@ export const getAssetActions = ($t: MessageFormatter, asset: AssetResponseDto) =
|
||||
ZoomIn,
|
||||
ZoomOut,
|
||||
Copy,
|
||||
Tag,
|
||||
Edit,
|
||||
RefreshFacesJob,
|
||||
RefreshMetadataJob,
|
||||
|
||||
@@ -67,6 +67,7 @@ export const getLibraryActions = ($t: MessageFormatter, library: LibraryResponse
|
||||
color: 'danger',
|
||||
onAction: () => handleDeleteLibrary(library),
|
||||
shortcuts: { key: 'Backspace' },
|
||||
shortcutOptions: { ignoreInputFields: true },
|
||||
};
|
||||
|
||||
const AddFolder: ActionItem = {
|
||||
|
||||
@@ -67,6 +67,7 @@ export const getUserAdminActions = ($t: MessageFormatter, user: UserAdminRespons
|
||||
$if: () => get(authUser).id !== user.id && !user.deletedAt,
|
||||
onAction: () => modalManager.show(UserDeleteConfirmModal, { user }),
|
||||
shortcuts: { key: 'Backspace' },
|
||||
shortcutOptions: { ignoreInputFields: true },
|
||||
};
|
||||
|
||||
const getDeleteDate = (deletedAt: string): Date =>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<script lang="ts">
|
||||
import { goto } from '$app/navigation';
|
||||
import { goto, invalidateAll } from '$app/navigation';
|
||||
import AdminCard from '$lib/components/AdminCard.svelte';
|
||||
import AdminPageLayout from '$lib/components/layouts/AdminPageLayout.svelte';
|
||||
import OnEvents from '$lib/components/OnEvents.svelte';
|
||||
@@ -48,10 +48,7 @@
|
||||
|
||||
const { children, data }: Props = $props();
|
||||
|
||||
let user = $state(data.user);
|
||||
const userPreferences = $state(data.userPreferences);
|
||||
const userStatistics = $state(data.userStatistics);
|
||||
const userSessions = $state(data.userSessions);
|
||||
const { user, userPreferences, userStatistics, userSessions } = $derived(data);
|
||||
const TiB = 1024 ** 4;
|
||||
const usage = $derived(user.quotaUsageInBytes ?? 0);
|
||||
let [statsUsage, statsUsageUnit] = $derived(getBytesWithUnit(usage, usage > TiB ? 2 : 0));
|
||||
@@ -79,9 +76,10 @@
|
||||
|
||||
const { ResetPassword, ResetPinCode, Update, Delete, Restore } = $derived(getUserAdminActions($t, user));
|
||||
|
||||
const onUpdate = (update: UserAdminResponseDto) => {
|
||||
const onUpdate = async (update: UserAdminResponseDto) => {
|
||||
if (update.id === user.id) {
|
||||
user = update;
|
||||
data.user = update;
|
||||
await invalidateAll();
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -16,12 +16,10 @@
|
||||
|
||||
let { data }: Props = $props();
|
||||
|
||||
const user = $state(data.user);
|
||||
let isAdmin = $state(user.isAdmin);
|
||||
let name = $state(user.name);
|
||||
let email = $state(user.email);
|
||||
let storageLabel = $state(user.storageLabel || '');
|
||||
const previousQuota = $state(user.quotaSizeInBytes);
|
||||
const user = $derived(data.user);
|
||||
let { isAdmin, name, email } = $derived(user);
|
||||
let storageLabel = $derived(user.storageLabel || '');
|
||||
const previousQuota = $derived(user.quotaSizeInBytes);
|
||||
|
||||
let quotaSize = $derived(
|
||||
typeof user.quotaSizeInBytes === 'number' ? convertFromBytes(user.quotaSizeInBytes, ByteUnit.GiB) : undefined,
|
||||
|
||||
Reference in New Issue
Block a user