mirror of
https://github.com/immich-app/immich.git
synced 2026-02-04 02:57:59 -08:00
Compare commits
1 Commits
release/ne
...
docs/contr
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f554c0915e |
@@ -26,81 +26,7 @@
|
||||
"vitest.explorer",
|
||||
"ms-playwright.playwright",
|
||||
"ms-azuretools.vscode-docker"
|
||||
],
|
||||
"settings": {
|
||||
"tasks": {
|
||||
"version": "2.0.0",
|
||||
"tasks": [
|
||||
{
|
||||
"label": "Fix Permissions, Install Dependencies",
|
||||
"type": "shell",
|
||||
"command": "[ -f /immich-devcontainer/container-start.sh ] && /immich-devcontainer/container-start.sh || exit 0",
|
||||
"isBackground": true,
|
||||
"presentation": {
|
||||
"echo": true,
|
||||
"reveal": "always",
|
||||
"focus": false,
|
||||
"panel": "dedicated",
|
||||
"showReuseMessage": true,
|
||||
"clear": false,
|
||||
"group": "Devcontainer tasks",
|
||||
"close": true
|
||||
},
|
||||
"runOptions": {
|
||||
"runOn": "default"
|
||||
},
|
||||
"problemMatcher": []
|
||||
},
|
||||
{
|
||||
"label": "Immich API Server (Nest)",
|
||||
"dependsOn": ["Fix Permissions, Install Dependencies"],
|
||||
"type": "shell",
|
||||
"command": "[ -f /immich-devcontainer/container-start-backend.sh ] && /immich-devcontainer/container-start-backend.sh || exit 0",
|
||||
"isBackground": true,
|
||||
"presentation": {
|
||||
"echo": true,
|
||||
"reveal": "always",
|
||||
"focus": false,
|
||||
"panel": "dedicated",
|
||||
"showReuseMessage": true,
|
||||
"clear": false,
|
||||
"group": "Devcontainer tasks",
|
||||
"close": true
|
||||
},
|
||||
"runOptions": {
|
||||
"runOn": "folderOpen"
|
||||
},
|
||||
"problemMatcher": []
|
||||
},
|
||||
{
|
||||
"label": "Immich Web Server (Vite)",
|
||||
"dependsOn": ["Fix Permissions, Install Dependencies"],
|
||||
"type": "shell",
|
||||
"command": "[ -f /immich-devcontainer/container-start-frontend.sh ] && /immich-devcontainer/container-start-frontend.sh || exit 0",
|
||||
"isBackground": true,
|
||||
"presentation": {
|
||||
"echo": true,
|
||||
"reveal": "always",
|
||||
"focus": false,
|
||||
"panel": "dedicated",
|
||||
"showReuseMessage": true,
|
||||
"clear": false,
|
||||
"group": "Devcontainer tasks",
|
||||
"close": true
|
||||
},
|
||||
"runOptions": {
|
||||
"runOn": "folderOpen"
|
||||
},
|
||||
"problemMatcher": []
|
||||
},
|
||||
{
|
||||
"label": "Build Immich CLI",
|
||||
"type": "shell",
|
||||
"command": "pnpm --filter cli build:dev"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"features": {
|
||||
|
||||
80
.vscode/tasks.json
vendored
Normal file
80
.vscode/tasks.json
vendored
Normal file
@@ -0,0 +1,80 @@
|
||||
{
|
||||
"version": "2.0.0",
|
||||
"tasks": [
|
||||
{
|
||||
"label": "Fix Permissions, Install Dependencies",
|
||||
"type": "shell",
|
||||
"command": "[ -f /immich-devcontainer/container-start.sh ] && /immich-devcontainer/container-start.sh || exit 0",
|
||||
"isBackground": true,
|
||||
"presentation": {
|
||||
"echo": true,
|
||||
"reveal": "always",
|
||||
"focus": false,
|
||||
"panel": "dedicated",
|
||||
"showReuseMessage": true,
|
||||
"clear": false,
|
||||
"group": "Devcontainer tasks",
|
||||
"close": true
|
||||
},
|
||||
"runOptions": {
|
||||
"runOn": "default"
|
||||
},
|
||||
"problemMatcher": []
|
||||
},
|
||||
{
|
||||
"label": "Immich API Server (Nest)",
|
||||
"dependsOn": ["Fix Permissions, Install Dependencies"],
|
||||
"type": "shell",
|
||||
"command": "[ -f /immich-devcontainer/container-start-backend.sh ] && /immich-devcontainer/container-start-backend.sh || exit 0",
|
||||
"isBackground": true,
|
||||
"presentation": {
|
||||
"echo": true,
|
||||
"reveal": "always",
|
||||
"focus": false,
|
||||
"panel": "dedicated",
|
||||
"showReuseMessage": true,
|
||||
"clear": false,
|
||||
"group": "Devcontainer tasks",
|
||||
"close": true
|
||||
},
|
||||
"runOptions": {
|
||||
"runOn": "default"
|
||||
},
|
||||
"problemMatcher": []
|
||||
},
|
||||
{
|
||||
"label": "Immich Web Server (Vite)",
|
||||
"dependsOn": ["Fix Permissions, Install Dependencies"],
|
||||
"type": "shell",
|
||||
"command": "[ -f /immich-devcontainer/container-start-frontend.sh ] && /immich-devcontainer/container-start-frontend.sh || exit 0",
|
||||
"isBackground": true,
|
||||
"presentation": {
|
||||
"echo": true,
|
||||
"reveal": "always",
|
||||
"focus": false,
|
||||
"panel": "dedicated",
|
||||
"showReuseMessage": true,
|
||||
"clear": false,
|
||||
"group": "Devcontainer tasks",
|
||||
"close": true
|
||||
},
|
||||
"runOptions": {
|
||||
"runOn": "default"
|
||||
},
|
||||
"problemMatcher": []
|
||||
},
|
||||
{
|
||||
"label": "Immich Server and Web",
|
||||
"dependsOn": ["Immich Web Server (Vite)", "Immich API Server (Nest)"],
|
||||
"runOptions": {
|
||||
"runOn": "folderOpen"
|
||||
},
|
||||
"problemMatcher": []
|
||||
},
|
||||
{
|
||||
"label": "Build Immich CLI",
|
||||
"type": "shell",
|
||||
"command": "pnpm --filter cli build:dev"
|
||||
}
|
||||
]
|
||||
}
|
||||
27
CHANGELOG.md
27
CHANGELOG.md
@@ -1,27 +0,0 @@
|
||||
|
||||
# v2.5.4
|
||||
|
||||
## Highlights
|
||||
|
||||
{{RELEASE HIGHLIGHTS}}
|
||||
|
||||
As always, please consider supporting the project.
|
||||
|
||||
🎉 Cheers! 🎉
|
||||
|
||||
|
||||
----
|
||||
|
||||
And as always, bugs are fixed, and many other improvements also come with this release.
|
||||
|
||||
<!-- Release notes generated using configuration in .github/release.yml at main -->
|
||||
|
||||
## What's Changed
|
||||
### 🐛 Bug fixes
|
||||
* fix: album dto docs by @jrasm91 in https://github.com/immich-app/immich/pull/25873
|
||||
|
||||
|
||||
**Full Changelog**: https://github.com/immich-app/immich/compare/v2.5.3...v2.5.4
|
||||
|
||||
---
|
||||
|
||||
@@ -23,9 +23,21 @@ We generally discourage PRs entirely generated by an LLM. For any part generated
|
||||
|
||||
From time to time, we put a feature freeze on parts of the codebase. For us, this means we won't accept most PRs that make changes in that area. Exempted from this are simple bug fixes that require only minor changes. We will close feature PRs that target a feature-frozen area, even if that feature is highly requested and you put a lot of work into it. Please keep that in mind, and if you're ever uncertain if a PR would be accepted, reach out to us first (e.g., in the aforementioned `#contributing` channel). We hate to throw away work. Currently, we have feature freezes on:
|
||||
|
||||
* Sharing/Asset ownership
|
||||
* (External) libraries
|
||||
- Sharing/Asset ownership
|
||||
- (External) libraries
|
||||
|
||||
## Non-code contributions
|
||||
|
||||
If you want to contribute to Immich but you don't feel comfortable programming in our tech stack, there are other ways you can help the team. All our translations are done through [Weblate](https://hosted.weblate.org/projects/immich). These rely entirely on the community; if you speak a language that isn't fully translated yet, submitting translations there is greatly appreciated! If you like helping others, answering Q&A discussions here on GitHub and replying to people on our Discord is also always appreciated.
|
||||
If you want to contribute to Immich but you don't feel comfortable programming in our tech stack, there are other ways you can help the team.
|
||||
|
||||
### Translations
|
||||
|
||||
All our translations are done through [Weblate](https://hosted.weblate.org/projects/immich). These rely entirely on the community; if you speak a language that isn't fully translated yet, submitting translations there is greatly appreciated!
|
||||
|
||||
### Datasets
|
||||
|
||||
Help us improve our [Immich Datasets](https://datasets.immich.app) by submitting photos and videos taken from a variety of devices, including smartphones, DSLRs, and action cameras, as well as photos with unique features, such as panoramas, burst photos, and photo spheres. These datasets will be publically available for anyone to use, do not submit private/sensitive photos.
|
||||
|
||||
### Community support
|
||||
|
||||
If you like helping others, answering Q&A discussions here on GitHub and replying to people on our Discord is also always appreciated.
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@immich/cli",
|
||||
"version": "2.5.4",
|
||||
"version": "2.5.2",
|
||||
"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.4",
|
||||
"url": "https://docs.v2.5.4.archive.immich.app"
|
||||
"label": "v2.5.2",
|
||||
"url": "https://docs.v2.5.2.archive.immich.app"
|
||||
},
|
||||
{
|
||||
"label": "v2.4.1",
|
||||
|
||||
@@ -70,7 +70,7 @@ services:
|
||||
restart: unless-stopped
|
||||
|
||||
redis:
|
||||
image: redis:6.2-alpine@sha256:46884be93652d02a96a176ccf173d1040bef365c5706aa7b6a1931caec8bfeef
|
||||
image: redis:6.2-alpine@sha256:37e002448575b32a599109664107e374c8709546905c372a34d64919043b9ceb
|
||||
|
||||
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:46884be93652d02a96a176ccf173d1040bef365c5706aa7b6a1931caec8bfeef
|
||||
image: redis:6.2-alpine@sha256:37e002448575b32a599109664107e374c8709546905c372a34d64919043b9ceb
|
||||
|
||||
database:
|
||||
image: ghcr.io/immich-app/postgres:14-vectorchord0.3.0@sha256:6f3e9d2c2177af16c2988ff71425d79d89ca630ec2f9c8db03209ab716542338
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "immich-e2e",
|
||||
"version": "2.5.4",
|
||||
"version": "2.5.2",
|
||||
"description": "",
|
||||
"main": "index.js",
|
||||
"type": "module",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "immich-i18n",
|
||||
"version": "2.5.4",
|
||||
"version": "2.5.2",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"format": "prettier --check .",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[project]
|
||||
name = "immich-ml"
|
||||
version = "2.5.4"
|
||||
version = "2.5.2"
|
||||
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.4"
|
||||
version = "2.5.2"
|
||||
source = { editable = "." }
|
||||
dependencies = [
|
||||
{ name = "aiocache" },
|
||||
|
||||
@@ -35,8 +35,8 @@ platform :android do
|
||||
task: 'bundle',
|
||||
build_type: 'Release',
|
||||
properties: {
|
||||
"android.injected.version.code" => 3035,
|
||||
"android.injected.version.name" => "2.5.4",
|
||||
"android.injected.version.code" => 3033,
|
||||
"android.injected.version.name" => "2.5.2",
|
||||
}
|
||||
)
|
||||
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,6 +133,7 @@ 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.4</string>
|
||||
<string>2.5.2</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 > 20;
|
||||
final showScrollbar = albumCount > 10;
|
||||
|
||||
final scrollView = CustomScrollView(
|
||||
controller: _scrollController,
|
||||
|
||||
@@ -87,7 +87,7 @@ class _AlbumSelectorState extends ConsumerState<AlbumSelector> {
|
||||
}
|
||||
|
||||
void onSearch(String searchTerm, QuickFilterMode filterMode) {
|
||||
final userId = ref.read(currentUserProvider)?.id;
|
||||
final userId = ref.watch(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.select((user) => user?.id));
|
||||
final userId = ref.watch(currentUserProvider)?.id;
|
||||
|
||||
// refilter and sort when albums change
|
||||
ref.listen(remoteAlbumProvider.select((state) => state.albums), (_, _) async {
|
||||
|
||||
@@ -259,11 +259,6 @@ 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();
|
||||
@@ -380,21 +375,21 @@ class DriftBackupNotifier extends StateNotifier<DriftBackupState> {
|
||||
_logger.warning("Skip handleBackupResume (pre-call): notifier disposed");
|
||||
return;
|
||||
}
|
||||
_logger.info("Start background backup sequence");
|
||||
_logger.info("Resuming backup tasks...");
|
||||
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} pending tasks");
|
||||
_logger.info("Found ${tasks.length} tasks");
|
||||
|
||||
if (tasks.isEmpty) {
|
||||
_logger.info("No pending tasks, starting new upload");
|
||||
_logger.info("Start backup with URLSession");
|
||||
return _backgroundUploadService.uploadBackupCandidates(userId);
|
||||
}
|
||||
|
||||
_logger.info("Resuming upload ${tasks.length} assets");
|
||||
_logger.info("Tasks to resume: ${tasks.length}");
|
||||
return _backgroundUploadService.resume();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -164,12 +164,9 @@ 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 = [];
|
||||
@@ -182,7 +179,6 @@ 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.4
|
||||
- API version: 2.5.2
|
||||
- Generator version: 7.8.0
|
||||
- Build package: org.openapitools.codegen.languages.DartClientCodegen
|
||||
|
||||
|
||||
4
mobile/openapi/lib/api/albums_api.dart
generated
4
mobile/openapi/lib/api/albums_api.dart
generated
@@ -473,7 +473,7 @@ class AlbumsApi {
|
||||
/// Filter albums containing this asset ID (ignores shared parameter)
|
||||
///
|
||||
/// * [bool] shared:
|
||||
/// Filter by shared status: true = only shared, false = not shared, undefined = all owned albums
|
||||
/// Filter by shared status: true = only shared, false = only own, undefined = all
|
||||
Future<Response> getAllAlbumsWithHttpInfo({ String? assetId, bool? shared, }) async {
|
||||
// ignore: prefer_const_declarations
|
||||
final apiPath = r'/albums';
|
||||
@@ -516,7 +516,7 @@ class AlbumsApi {
|
||||
/// Filter albums containing this asset ID (ignores shared parameter)
|
||||
///
|
||||
/// * [bool] shared:
|
||||
/// Filter by shared status: true = only shared, false = not shared, undefined = all owned albums
|
||||
/// Filter by shared status: true = only shared, false = only own, undefined = all
|
||||
Future<List<AlbumResponseDto>?> getAllAlbums({ String? assetId, bool? shared, }) async {
|
||||
final response = await getAllAlbumsWithHttpInfo( assetId: assetId, shared: shared, );
|
||||
if (response.statusCode >= HttpStatus.badRequest) {
|
||||
|
||||
@@ -1249,10 +1249,10 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: meta
|
||||
sha256: "23f08335362185a5ea2ad3a4e597f1375e78bce8a040df5c600c8d3552ef2394"
|
||||
sha256: e3641ec5d63ebf0d9b41bd43201a66e3fc79a65db5f61fc181f04cd27aab950c
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.17.0"
|
||||
version: "1.16.0"
|
||||
mime:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -1942,10 +1942,10 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: test_api
|
||||
sha256: ab2726c1a94d3176a45960b6234466ec367179b87dd74f1611adb1f3b5fb9d55
|
||||
sha256: "522f00f556e73044315fa4585ec3270f1808a4b186c936e612cab0b565ff1e00"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.7.7"
|
||||
version: "0.7.6"
|
||||
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.4+3035
|
||||
version: 2.5.2+3033
|
||||
|
||||
environment:
|
||||
sdk: '>=3.8.0 <4.0.0'
|
||||
|
||||
@@ -1618,7 +1618,7 @@
|
||||
"name": "shared",
|
||||
"required": false,
|
||||
"in": "query",
|
||||
"description": "Filter by shared status: true = only shared, false = not shared, undefined = all owned albums",
|
||||
"description": "Filter by shared status: true = only shared, false = only own, undefined = all",
|
||||
"schema": {
|
||||
"type": "boolean"
|
||||
}
|
||||
@@ -15057,7 +15057,7 @@
|
||||
"info": {
|
||||
"title": "Immich",
|
||||
"description": "Immich API",
|
||||
"version": "2.5.4",
|
||||
"version": "2.5.2",
|
||||
"contact": {}
|
||||
},
|
||||
"tags": [
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@immich/sdk",
|
||||
"version": "2.5.4",
|
||||
"version": "2.5.2",
|
||||
"description": "Auto-generated TypeScript SDK for the Immich API",
|
||||
"type": "module",
|
||||
"main": "./build/index.js",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/**
|
||||
* Immich
|
||||
* 2.5.4
|
||||
* 2.5.2
|
||||
* 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.4",
|
||||
"version": "2.5.2",
|
||||
"description": "Monorepo for Immich",
|
||||
"private": true,
|
||||
"packageManager": "pnpm@10.28.0+sha512.05df71d1421f21399e053fde567cea34d446fa02c76571441bfc1c7956e98e363088982d940465fd34480d4d90a0668bc12362f8aa88000a64e83d0b0e47be48",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "immich",
|
||||
"version": "2.5.4",
|
||||
"version": "2.5.2",
|
||||
"description": "",
|
||||
"author": "",
|
||||
"private": true,
|
||||
|
||||
@@ -102,7 +102,7 @@ export class UpdateAlbumDto {
|
||||
export class GetAlbumsDto {
|
||||
@ValidateBoolean({
|
||||
optional: true,
|
||||
description: 'Filter by shared status: true = only shared, false = not shared, undefined = all owned albums',
|
||||
description: 'Filter by shared status: true = only shared, false = only own, undefined = all',
|
||||
})
|
||||
shared?: boolean;
|
||||
|
||||
|
||||
@@ -307,6 +307,7 @@ 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),
|
||||
@@ -321,7 +322,6 @@ 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.4",
|
||||
"version": "2.5.2",
|
||||
"license": "GNU Affero General Public License version 3",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
|
||||
@@ -14,7 +14,6 @@
|
||||
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';
|
||||
@@ -37,7 +36,6 @@
|
||||
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';
|
||||
@@ -428,11 +426,8 @@
|
||||
!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,13 +1,12 @@
|
||||
<script lang="ts">
|
||||
import HeaderActionButton from '$lib/components/HeaderActionButton.svelte';
|
||||
import OnEvents from '$lib/components/OnEvents.svelte';
|
||||
import { shortcut } from '$lib/actions/shortcut';
|
||||
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 { Badge, IconButton, Link, Text } from '@immich/ui';
|
||||
import { mdiClose } from '@mdi/js';
|
||||
import { Icon, modalManager, Text } from '@immich/ui';
|
||||
import { mdiClose, mdiPlus } from '@mdi/js';
|
||||
import { t } from 'svelte-i18n';
|
||||
|
||||
interface Props {
|
||||
@@ -19,23 +18,22 @@
|
||||
|
||||
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>
|
||||
|
||||
<OnEvents {onAssetsTag} />
|
||||
<svelte:document use:shortcut={{ shortcut: { key: 't' }, onShortcut: handleAddTag }} />
|
||||
|
||||
{#if isOwner && !authManager.isSharedLink}
|
||||
<section class="px-4 mt-4">
|
||||
@@ -44,24 +42,36 @@
|
||||
</div>
|
||||
<section class="flex flex-wrap pt-2 gap-1" data-testid="detail-panel-tags">
|
||||
{#each tags as tag (tag.id)}
|
||||
<Badge size="small" class="items-center px-0" shape="round">
|
||||
<Link
|
||||
<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"
|
||||
href={Route.tags({ path: tag.value })}
|
||||
class="text-light no-underline rounded-full hover:bg-primary-400 px-2"
|
||||
>
|
||||
{tag.value}
|
||||
</Link>
|
||||
<IconButton
|
||||
aria-label={$t('remove_tag')}
|
||||
icon={mdiClose}
|
||||
<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"
|
||||
onclick={() => handleRemove(tag.id)}
|
||||
size="tiny"
|
||||
class="hover:bg-primary-400"
|
||||
shape="round"
|
||||
/>
|
||||
</Badge>
|
||||
>
|
||||
<Icon icon={mdiClose} />
|
||||
</button>
|
||||
</div>
|
||||
{/each}
|
||||
<HeaderActionButton action={Tag} />
|
||||
<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>
|
||||
</section>
|
||||
</section>
|
||||
{/if}
|
||||
|
||||
@@ -55,10 +55,13 @@
|
||||
|
||||
let loader = $state<HTMLImageElement>();
|
||||
|
||||
$effect.pre(() => {
|
||||
void asset.id;
|
||||
untrack(() => assetViewerManager.resetZoomState());
|
||||
});
|
||||
assetViewerManager.zoomState = {
|
||||
currentRotation: 0,
|
||||
currentZoom: 1,
|
||||
enable: true,
|
||||
currentPositionX: 0,
|
||||
currentPositionY: 0,
|
||||
};
|
||||
|
||||
onDestroy(() => {
|
||||
$boundingBoxesArray = [];
|
||||
|
||||
@@ -20,8 +20,11 @@
|
||||
|
||||
const handleTagAssets = async () => {
|
||||
const assets = [...getOwnedAssets()];
|
||||
await modalManager.show(AssetTagModal, { assetIds: assets.map(({ id }) => id) });
|
||||
clearSelect();
|
||||
const success = await modalManager.show(AssetTagModal, { assetIds: assets.map(({ id }) => id) });
|
||||
|
||||
if (success) {
|
||||
clearSelect();
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
|
||||
@@ -5,14 +5,6 @@ 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];
|
||||
@@ -20,7 +12,13 @@ export type Events = {
|
||||
};
|
||||
|
||||
export class AssetViewerManager extends BaseEventManager<Events> {
|
||||
#zoomState = $state(createDefaultZoomState());
|
||||
#zoomState = $state<ZoomImageWheelState>({
|
||||
currentRotation: 0,
|
||||
currentZoom: 1,
|
||||
enable: true,
|
||||
currentPositionX: 0,
|
||||
currentPositionY: 0,
|
||||
});
|
||||
|
||||
imgRef = $state<HTMLImageElement | undefined>();
|
||||
isShowActivityPanel = $state(false);
|
||||
@@ -69,10 +67,6 @@ export class AssetViewerManager extends BaseEventManager<Events> {
|
||||
this.#zoomState = state;
|
||||
}
|
||||
|
||||
resetZoomState() {
|
||||
this.zoomState = createDefaultZoomState();
|
||||
}
|
||||
|
||||
toggleActivityPanel() {
|
||||
this.closeDetailPanel();
|
||||
this.isShowActivityPanel = !this.isShowActivityPanel;
|
||||
|
||||
@@ -37,7 +37,6 @@ export type Events = {
|
||||
AssetsArchive: [string[]];
|
||||
AssetsDelete: [string[]];
|
||||
AssetEditsApplied: [string];
|
||||
AssetsTag: [string[]];
|
||||
|
||||
AlbumAddAssets: [];
|
||||
AlbumUpdate: [AlbumResponseDto];
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
<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';
|
||||
@@ -10,7 +9,7 @@
|
||||
import Combobox, { type ComboBoxOption } from '../components/shared-components/combobox.svelte';
|
||||
|
||||
interface Props {
|
||||
onClose: () => void;
|
||||
onClose: (success?: true) => void;
|
||||
assetIds: string[];
|
||||
}
|
||||
|
||||
@@ -31,8 +30,8 @@
|
||||
return;
|
||||
}
|
||||
|
||||
const updatedIds = await tagAssets({ tagIds: [...selectedIds], assetIds, showNotification: false });
|
||||
eventManager.emit('AssetsTag', updatedIds);
|
||||
await tagAssets({ tagIds: [...selectedIds], assetIds, showNotification: false });
|
||||
onClose(true);
|
||||
};
|
||||
|
||||
const handleSelect = async (option?: ComboBoxOption) => {
|
||||
|
||||
@@ -2,7 +2,6 @@ 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';
|
||||
@@ -42,7 +41,6 @@ import {
|
||||
mdiMotionPauseOutline,
|
||||
mdiMotionPlayOutline,
|
||||
mdiShareVariantOutline,
|
||||
mdiTagPlusOutline,
|
||||
mdiTune,
|
||||
} from '@mdi/js';
|
||||
import type { MessageFormatter } from 'svelte-i18n';
|
||||
@@ -51,7 +49,6 @@ 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 = {
|
||||
@@ -158,16 +155,7 @@ export const getAssetActions = ($t: MessageFormatter, asset: AssetResponseDto) =
|
||||
type: $t('assets'),
|
||||
$if: () => asset.hasMetadata,
|
||||
onAction: () => assetViewerManager.toggleDetailPanel(),
|
||||
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' },
|
||||
shortcuts: [{ key: 'i' }],
|
||||
};
|
||||
|
||||
const Edit: ActionItem = {
|
||||
@@ -224,7 +212,6 @@ export const getAssetActions = ($t: MessageFormatter, asset: AssetResponseDto) =
|
||||
ZoomIn,
|
||||
ZoomOut,
|
||||
Copy,
|
||||
Tag,
|
||||
Edit,
|
||||
RefreshFacesJob,
|
||||
RefreshMetadataJob,
|
||||
|
||||
@@ -67,7 +67,6 @@ export const getLibraryActions = ($t: MessageFormatter, library: LibraryResponse
|
||||
color: 'danger',
|
||||
onAction: () => handleDeleteLibrary(library),
|
||||
shortcuts: { key: 'Backspace' },
|
||||
shortcutOptions: { ignoreInputFields: true },
|
||||
};
|
||||
|
||||
const AddFolder: ActionItem = {
|
||||
|
||||
@@ -67,7 +67,6 @@ 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, invalidateAll } from '$app/navigation';
|
||||
import { goto } 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,7 +48,10 @@
|
||||
|
||||
const { children, data }: Props = $props();
|
||||
|
||||
const { user, userPreferences, userStatistics, userSessions } = $derived(data);
|
||||
let user = $state(data.user);
|
||||
const userPreferences = $state(data.userPreferences);
|
||||
const userStatistics = $state(data.userStatistics);
|
||||
const userSessions = $state(data.userSessions);
|
||||
const TiB = 1024 ** 4;
|
||||
const usage = $derived(user.quotaUsageInBytes ?? 0);
|
||||
let [statsUsage, statsUsageUnit] = $derived(getBytesWithUnit(usage, usage > TiB ? 2 : 0));
|
||||
@@ -76,10 +79,9 @@
|
||||
|
||||
const { ResetPassword, ResetPinCode, Update, Delete, Restore } = $derived(getUserAdminActions($t, user));
|
||||
|
||||
const onUpdate = async (update: UserAdminResponseDto) => {
|
||||
const onUpdate = (update: UserAdminResponseDto) => {
|
||||
if (update.id === user.id) {
|
||||
data.user = update;
|
||||
await invalidateAll();
|
||||
user = update;
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -16,10 +16,12 @@
|
||||
|
||||
let { data }: Props = $props();
|
||||
|
||||
const user = $derived(data.user);
|
||||
let { isAdmin, name, email } = $derived(user);
|
||||
let storageLabel = $derived(user.storageLabel || '');
|
||||
const previousQuota = $derived(user.quotaSizeInBytes);
|
||||
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);
|
||||
|
||||
let quotaSize = $derived(
|
||||
typeof user.quotaSizeInBytes === 'number' ? convertFromBytes(user.quotaSizeInBytes, ByteUnit.GiB) : undefined,
|
||||
|
||||
Reference in New Issue
Block a user