Compare commits

..

3 Commits

Author SHA1 Message Date
renovate[bot] c631b48661 fix(deps): update mobile 2026-06-30 10:49:43 +00:00
Daniel Dietzler deeb042a9e feat: honor album access permissions in search endpoints (#29352) 2026-06-29 22:27:22 +02:00
Daniel Dietzler b4cc406a3f fix!: search endpoints visibility can be omitted (#29385) 2026-06-29 22:00:02 +02:00
23 changed files with 223 additions and 552 deletions
+2
View File
@@ -9,6 +9,8 @@ enum SortOrder {
enum TextSearchType { context, filename, description, ocr }
enum AssetVisibilityEnum { timeline, hidden, archive, locked }
enum ActionSource { timeline, viewer }
enum ShareAssetType { original, preview }
@@ -52,8 +52,6 @@ class RemoteAsset extends BaseAsset {
bool get isTrashed => deletedAt != null;
bool get isStacked => stackId != null;
@override
String toString() {
return '''Asset {
@@ -10,7 +10,6 @@ import 'package:immich_mobile/infrastructure/entities/remote_asset.entity.dart';
import 'package:immich_mobile/infrastructure/entities/remote_asset.entity.drift.dart';
import 'package:immich_mobile/infrastructure/entities/stack.entity.drift.dart';
import 'package:immich_mobile/infrastructure/repositories/db.repository.dart';
import 'package:immich_mobile/utils/option.dart';
import 'package:maplibre_gl/maplibre_gl.dart';
class RemoteAssetRepository extends DriftDatabaseRepository {
@@ -287,20 +286,4 @@ class RemoteAssetRepository extends DriftDatabaseRepository {
..orderBy([(row) => OrderingTerm.asc(row.sequence)]);
return query.map((row) => row.toDto()!).get();
}
Future<void> update(
List<String> remoteIds, {
Option<bool> isFavorite = const .none(),
Option<AssetVisibility> visibility = const .none(),
}) {
final companion = RemoteAssetEntityCompanion(
visibility: visibility.toDriftValue(),
isFavorite: isFavorite.toDriftValue(),
);
return _db.batch((batch) {
for (final remoteId in remoteIds) {
batch.update(_db.remoteAssetEntity, companion, where: (e) => e.id.equals(remoteId));
}
});
}
}
@@ -3,29 +3,25 @@ import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
import 'package:immich_mobile/generated/translations.g.dart';
import 'package:immich_mobile/presentation/actions/action.dart';
import 'package:immich_mobile/providers/infrastructure/asset.provider.dart';
import 'package:immich_mobile/utils/asset_filter.dart';
import 'package:immich_ui/immich_ui.dart';
class FavoriteAction extends AssetAction<RemoteAsset> {
final bool favorite;
final bool shouldFavorite;
FavoriteAction({required super.assets}) : favorite = assets.any((asset) => !asset.isFavorite);
FavoriteAction({required super.assets}) : shouldFavorite = assets.any((asset) => !asset.isFavorite);
@override
IconData get icon => favorite ? Icons.favorite_border_rounded : Icons.favorite_rounded;
IconData get icon => shouldFavorite ? Icons.favorite_border_rounded : Icons.favorite_rounded;
@override
String label(ActionScope scope) => favorite ? scope.context.t.favorite : scope.context.t.unfavorite;
String label(ActionScope scope) => shouldFavorite ? scope.context.t.favorite : scope.context.t.unfavorite;
@override
Iterable<RemoteAsset> filter(ActionScope scope) {
final owned = AssetFilter(assets).owned(scope.authUser.id);
if (favorite) {
return owned.notFavorites();
} else {
return owned.favorites();
}
}
Iterable<RemoteAsset> filter(ActionScope scope) => assets
.where(
(asset) => asset is RemoteAsset && asset.ownerId == scope.authUser.id && asset.isFavorite == !shouldFavorite,
)
.cast<RemoteAsset>();
@override
bool isVisible(ActionScope scope) => filter(scope).isNotEmpty;
@@ -35,8 +31,8 @@ class FavoriteAction extends AssetAction<RemoteAsset> {
final ActionScope(:ref) = scope;
final assets = filter(scope).map((asset) => asset.id).toList(growable: false);
await ref.read(assetServiceProvider).updateFavorite(assets, favorite);
final message = favorite
await ref.read(assetServiceProvider).updateFavorite(assets, shouldFavorite);
final message = shouldFavorite
? StaticTranslations.instance.favorite_action_prompt(count: assets.length)
: StaticTranslations.instance.unfavorite_action_prompt(count: assets.length);
snackbar.success(message);
@@ -1,4 +0,0 @@
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/repositories/toast.repository.dart';
final toastRepositoryProvider = Provider<ToastRepository>((ref) => const .new());
@@ -1,14 +1,12 @@
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:http/http.dart';
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
import 'package:immich_mobile/constants/enums.dart';
import 'package:immich_mobile/domain/models/asset_edit.model.dart' hide AssetEditAction;
import 'package:immich_mobile/domain/models/stack.model.dart';
import 'package:immich_mobile/providers/api.provider.dart';
import 'package:immich_mobile/repositories/api.repository.dart';
import 'package:immich_mobile/utils/option.dart';
import 'package:maplibre_gl/maplibre_gl.dart';
import 'package:openapi/api.dart' as api show AssetVisibility;
import 'package:openapi/api.dart' hide AssetVisibility;
import 'package:openapi/api.dart';
final assetApiRepositoryProvider = Provider(
(ref) => AssetApiRepository(
@@ -43,7 +41,7 @@ class AssetApiRepository extends ApiRepository {
return response?.count ?? 0;
}
Future<void> updateVisibility(List<String> ids, AssetVisibility visibility) async {
Future<void> updateVisibility(List<String> ids, AssetVisibilityEnum visibility) async {
return _api.updateAssets(AssetBulkUpdateDto(ids: ids, visibility: Optional.present(_mapVisibility(visibility))));
}
@@ -79,11 +77,11 @@ class AssetApiRepository extends ApiRepository {
return _api.downloadAssetWithHttpInfo(id, edited: edited);
}
api.AssetVisibility _mapVisibility(AssetVisibility visibility) => switch (visibility) {
AssetVisibility.timeline => api.AssetVisibility.timeline,
AssetVisibility.hidden => api.AssetVisibility.hidden,
AssetVisibility.locked => api.AssetVisibility.locked,
AssetVisibility.archive => api.AssetVisibility.archive,
_mapVisibility(AssetVisibilityEnum visibility) => switch (visibility) {
AssetVisibilityEnum.timeline => AssetVisibility.timeline,
AssetVisibilityEnum.hidden => AssetVisibility.hidden,
AssetVisibilityEnum.locked => AssetVisibility.locked,
AssetVisibilityEnum.archive => AssetVisibility.archive,
};
Future<String?> getAssetMIMEType(String assetId) async {
@@ -108,20 +106,6 @@ class AssetApiRepository extends ApiRepository {
Future<void> removeEdits(String assetId) async {
return _api.removeAssetEdits(assetId);
}
Future<void> update(
List<String> remoteIds, {
Option<bool> isFavorite = const .none(),
Option<AssetVisibility> visibility = const .none(),
}) {
return _api.updateAssets(
AssetBulkUpdateDto(
ids: remoteIds,
isFavorite: isFavorite.toOptional(),
visibility: visibility.map(_mapVisibility).toOptional(),
),
);
}
}
extension on StackResponseDto {
@@ -1,26 +0,0 @@
import 'dart:async';
import 'package:immich_ui/immich_ui.dart';
class ToastOption {
final Duration? timeout;
final FutureOr<void> Function()? onUndo;
const ToastOption({this.timeout, this.onUndo});
}
class ToastRepository {
const ToastRepository();
FutureOr<void> success(String message, {ToastOption? toast}) {
snackbar.success(message, duration: toast?.timeout);
}
FutureOr<void> info(String message, {ToastOption? toast}) {
snackbar.info(message, duration: toast?.timeout);
}
FutureOr<void> error(String message, {ToastOption? toast}) {
snackbar.error(message, duration: toast?.timeout);
}
}
+4 -4
View File
@@ -79,17 +79,17 @@ class ActionService {
}
Future<void> archive(List<String> remoteIds) async {
await _assetApiRepository.updateVisibility(remoteIds, .archive);
await _assetApiRepository.updateVisibility(remoteIds, AssetVisibilityEnum.archive);
await _remoteAssetRepository.updateVisibility(remoteIds, AssetVisibility.archive);
}
Future<void> unArchive(List<String> remoteIds) async {
await _assetApiRepository.updateVisibility(remoteIds, .timeline);
await _assetApiRepository.updateVisibility(remoteIds, AssetVisibilityEnum.timeline);
await _remoteAssetRepository.updateVisibility(remoteIds, AssetVisibility.timeline);
}
Future<void> moveToLockFolder(List<String> remoteIds, List<String> localIds) async {
await _assetApiRepository.updateVisibility(remoteIds, .locked);
await _assetApiRepository.updateVisibility(remoteIds, AssetVisibilityEnum.locked);
await _remoteAssetRepository.updateVisibility(remoteIds, AssetVisibility.locked);
// Ask user if they want to delete local copies
@@ -99,7 +99,7 @@ class ActionService {
}
Future<void> removeFromLockFolder(List<String> remoteIds) async {
await _assetApiRepository.updateVisibility(remoteIds, .timeline);
await _assetApiRepository.updateVisibility(remoteIds, AssetVisibilityEnum.timeline);
await _remoteAssetRepository.updateVisibility(remoteIds, AssetVisibility.timeline);
}
-28
View File
@@ -1,28 +0,0 @@
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
extension type const AssetFilter<T extends BaseAsset>(Iterable<T> assets) implements Iterable<T> {
AssetFilter<T> where(bool Function(T asset) test) => AssetFilter(assets.where(test));
AssetFilter<T> whereNot(bool Function(T asset) test) => AssetFilter(assets.where((asset) => !test(asset)));
AssetFilter<T> type(AssetType type) => where((asset) => asset.type == type);
AssetFilter<T> favorites() => where(_isFavorite);
AssetFilter<T> notFavorites() => whereNot(_isFavorite);
AssetFilter<RemoteAsset> remote() => AssetFilter(assets.whereType<RemoteAsset>());
AssetFilter<RemoteAsset> owned(String ownerId) => remote().where((asset) => asset.ownerId == ownerId);
AssetFilter<RemoteAsset> visibility(AssetVisibility visibility) => remote().where(_hasVisibility(visibility));
AssetFilter<RemoteAsset> notVisibility(AssetVisibility visibility) => remote().whereNot(_hasVisibility(visibility));
AssetFilter<RemoteAsset> archived() => visibility(.archive);
AssetFilter<RemoteAsset> notArchived() => notVisibility(.archive);
AssetFilter<RemoteAsset> stacked() => remote().where(_isStacked);
AssetFilter<RemoteAsset> notStacked() => remote().whereNot(_isStacked);
AssetFilter<LocalAsset> local() => AssetFilter(assets.whereType<LocalAsset>());
AssetFilter<LocalAsset> backedUp() => local().where(_isBackedUp);
}
bool _isFavorite(BaseAsset asset) => asset.isFavorite;
bool _isStacked(RemoteAsset asset) => asset.isStacked;
bool _isBackedUp(LocalAsset asset) => asset.remoteAssetId != null;
bool Function(RemoteAsset asset) _hasVisibility(AssetVisibility visibility) =>
(asset) => asset.visibility == visibility;
-13
View File
@@ -1,4 +1,3 @@
import 'package:drift/drift.dart';
import 'package:openapi/api.dart' show Optional;
sealed class Option<T> {
@@ -22,11 +21,6 @@ sealed class Option<T> {
None() => null,
};
Option<U> map<U>(U Function(T value) f) => switch (this) {
Some(:final value) => Some(f(value)),
None() => None<U>(),
};
U fold<U>(U Function(T value) onSome, U Function() onNone) => switch (this) {
Some(:final value) => onSome(value),
None() => onNone(),
@@ -71,10 +65,3 @@ extension OptionToOptional<T> on Option<T> {
Some(:final value) => Optional.present(value),
};
}
extension OptionToDriftValue<T> on Option<T> {
Value<T> toDriftValue() => switch (this) {
Some(:final value) => Value(value),
None() => const Value.absent(),
};
}
+50 -140
View File
@@ -1,186 +1,96 @@
# @generated - this file is auto-generated by `mise lock` https://mise.en.dev/dev-tools/mise-lock.html
[[tools."aqua:flutter/flutter"]]
version = "3.44.1"
version = "3.44.4"
backend = "aqua:flutter/flutter"
[tools."aqua:flutter/flutter"."platforms.linux-arm64"]
url = "https://storage.googleapis.com/flutter_infra_release/releases/stable/linux/flutter_linux_3.44.1-stable.tar.xz"
url = "https://storage.googleapis.com/flutter_infra_release/releases/stable/linux/flutter_linux_3.44.4-stable.tar.xz"
[tools."aqua:flutter/flutter"."platforms.linux-arm64-musl"]
url = "https://storage.googleapis.com/flutter_infra_release/releases/stable/linux/flutter_linux_3.44.1-stable.tar.xz"
url = "https://storage.googleapis.com/flutter_infra_release/releases/stable/linux/flutter_linux_3.44.4-stable.tar.xz"
[tools."aqua:flutter/flutter"."platforms.linux-x64"]
url = "https://storage.googleapis.com/flutter_infra_release/releases/stable/linux/flutter_linux_3.44.1-stable.tar.xz"
url = "https://storage.googleapis.com/flutter_infra_release/releases/stable/linux/flutter_linux_3.44.4-stable.tar.xz"
[tools."aqua:flutter/flutter"."platforms.linux-x64-musl"]
url = "https://storage.googleapis.com/flutter_infra_release/releases/stable/linux/flutter_linux_3.44.1-stable.tar.xz"
url = "https://storage.googleapis.com/flutter_infra_release/releases/stable/linux/flutter_linux_3.44.4-stable.tar.xz"
[tools."aqua:flutter/flutter"."platforms.macos-arm64"]
url = "https://storage.googleapis.com/flutter_infra_release/releases/stable/macos/flutter_macos_arm64_3.44.1-stable.zip"
url = "https://storage.googleapis.com/flutter_infra_release/releases/stable/macos/flutter_macos_arm64_3.44.4-stable.zip"
[tools."aqua:flutter/flutter"."platforms.macos-x64"]
url = "https://storage.googleapis.com/flutter_infra_release/releases/stable/macos/flutter_macos_3.44.1-stable.zip"
url = "https://storage.googleapis.com/flutter_infra_release/releases/stable/macos/flutter_macos_3.44.4-stable.zip"
[tools."aqua:flutter/flutter"."platforms.windows-x64"]
url = "https://storage.googleapis.com/flutter_infra_release/releases/stable/windows/flutter_windows_3.44.1-stable.zip"
url = "https://storage.googleapis.com/flutter_infra_release/releases/stable/windows/flutter_windows_3.44.4-stable.zip"
[[tools."github:CQLabs/homebrew-dcm"]]
version = "1.37.0"
backend = "github:CQLabs/homebrew-dcm"
[tools."github:CQLabs/homebrew-dcm"."platforms.linux-arm64"]
checksum = "sha256:253da2512b149913dfe345bf9a62a79acb2d730f66e71162ba4a92dfc4224b82"
url = "https://github.com/CQLabs/homebrew-dcm/releases/download/1.37.0/dcm-linux-arm-release.zip"
url_api = "https://api.github.com/repos/CQLabs/homebrew-dcm/releases/assets/404543838"
[tools."github:CQLabs/homebrew-dcm"."platforms.linux-arm64-musl"]
checksum = "sha256:253da2512b149913dfe345bf9a62a79acb2d730f66e71162ba4a92dfc4224b82"
url = "https://github.com/CQLabs/homebrew-dcm/releases/download/1.37.0/dcm-linux-arm-release.zip"
url_api = "https://api.github.com/repos/CQLabs/homebrew-dcm/releases/assets/404543838"
[tools."github:CQLabs/homebrew-dcm"."platforms.linux-x64"]
checksum = "sha256:477e086d4099c12f21e5ccd83b005d5fb945dd4cac4fd127fd9a08d7649af1cf"
url = "https://github.com/CQLabs/homebrew-dcm/releases/download/1.37.0/dcm-linux-x64-release.zip"
url_api = "https://api.github.com/repos/CQLabs/homebrew-dcm/releases/assets/404543797"
[tools."github:CQLabs/homebrew-dcm"."platforms.linux-x64-musl"]
checksum = "sha256:477e086d4099c12f21e5ccd83b005d5fb945dd4cac4fd127fd9a08d7649af1cf"
url = "https://github.com/CQLabs/homebrew-dcm/releases/download/1.37.0/dcm-linux-x64-release.zip"
url_api = "https://api.github.com/repos/CQLabs/homebrew-dcm/releases/assets/404543797"
[tools."github:CQLabs/homebrew-dcm"."platforms.macos-arm64"]
checksum = "sha256:30bede64367d09067093cc57af6ec9496d7717898138ded5cb98a16ac8dd9d93"
url = "https://github.com/CQLabs/homebrew-dcm/releases/download/1.37.0/dcm-macos-arm-release.zip"
url_api = "https://api.github.com/repos/CQLabs/homebrew-dcm/releases/assets/404543757"
[tools."github:CQLabs/homebrew-dcm"."platforms.macos-x64"]
checksum = "sha256:e56cb99872be7445a4de1d37e5438ca70e3bcd83be7a2b9b385e3538881f8068"
url = "https://github.com/CQLabs/homebrew-dcm/releases/download/1.37.0/dcm-macos-x64-release.zip"
url_api = "https://api.github.com/repos/CQLabs/homebrew-dcm/releases/assets/404543727"
[tools."github:CQLabs/homebrew-dcm"."platforms.windows-x64"]
checksum = "sha256:f133470daa3fb0427f039b424392af7e917d7e7db6b556aa2a968ab0e31587da"
url = "https://github.com/CQLabs/homebrew-dcm/releases/download/1.37.0/dcm-windows-release.zip"
url_api = "https://api.github.com/repos/CQLabs/homebrew-dcm/releases/assets/404543660"
[[tools."github:CQLabs/homebrew-dcm"]]
version = "1.37.0"
version = "1.38.1"
backend = "github:CQLabs/homebrew-dcm"
[tools."github:CQLabs/homebrew-dcm".options]
asset_pattern = "dcm-linux-arm-release.zip"
[tools."github:CQLabs/homebrew-dcm"."platforms.linux-arm64"]
checksum = "sha256:253da2512b149913dfe345bf9a62a79acb2d730f66e71162ba4a92dfc4224b82"
url = "https://github.com/CQLabs/homebrew-dcm/releases/download/1.37.0/dcm-linux-arm-release.zip"
url_api = "https://api.github.com/repos/CQLabs/homebrew-dcm/releases/assets/404543838"
checksum = "sha256:0934337f9838fd2c74615070a8b1c0cb80f1b8261080a6e03aa71c16642d06e6"
url = "https://github.com/CQLabs/homebrew-dcm/releases/download/1.38.1/dcm-linux-arm-release.zip"
url_api = "https://api.github.com/repos/CQLabs/homebrew-dcm/releases/assets/455844707"
[tools."github:CQLabs/homebrew-dcm"."platforms.linux-arm64-musl"]
checksum = "sha256:253da2512b149913dfe345bf9a62a79acb2d730f66e71162ba4a92dfc4224b82"
url = "https://github.com/CQLabs/homebrew-dcm/releases/download/1.37.0/dcm-linux-arm-release.zip"
url_api = "https://api.github.com/repos/CQLabs/homebrew-dcm/releases/assets/404543838"
checksum = "sha256:0934337f9838fd2c74615070a8b1c0cb80f1b8261080a6e03aa71c16642d06e6"
url = "https://github.com/CQLabs/homebrew-dcm/releases/download/1.38.1/dcm-linux-arm-release.zip"
url_api = "https://api.github.com/repos/CQLabs/homebrew-dcm/releases/assets/455844707"
[[tools."github:CQLabs/homebrew-dcm"]]
version = "1.37.0"
backend = "github:CQLabs/homebrew-dcm"
[tools."github:CQLabs/homebrew-dcm".options]
asset_pattern = "dcm-linux-x64-release.zip"
[tools."github:CQLabs/homebrew-dcm"."platforms.linux-x64"]
checksum = "sha256:477e086d4099c12f21e5ccd83b005d5fb945dd4cac4fd127fd9a08d7649af1cf"
url = "https://github.com/CQLabs/homebrew-dcm/releases/download/1.37.0/dcm-linux-x64-release.zip"
url_api = "https://api.github.com/repos/CQLabs/homebrew-dcm/releases/assets/404543797"
[tools."github:CQLabs/homebrew-dcm"."platforms.linux-x64-musl"]
checksum = "sha256:477e086d4099c12f21e5ccd83b005d5fb945dd4cac4fd127fd9a08d7649af1cf"
url = "https://github.com/CQLabs/homebrew-dcm/releases/download/1.37.0/dcm-linux-x64-release.zip"
url_api = "https://api.github.com/repos/CQLabs/homebrew-dcm/releases/assets/404543797"
[[tools."github:CQLabs/homebrew-dcm"]]
version = "1.37.0"
backend = "github:CQLabs/homebrew-dcm"
[tools."github:CQLabs/homebrew-dcm".options]
asset_pattern = "dcm-macos-x64-release.zip"
[tools."github:CQLabs/homebrew-dcm"."platforms.macos-x64"]
checksum = "sha256:e56cb99872be7445a4de1d37e5438ca70e3bcd83be7a2b9b385e3538881f8068"
url = "https://github.com/CQLabs/homebrew-dcm/releases/download/1.37.0/dcm-macos-x64-release.zip"
url_api = "https://api.github.com/repos/CQLabs/homebrew-dcm/releases/assets/404543727"
[[tools."github:CQLabs/homebrew-dcm"]]
version = "1.37.0"
backend = "github:CQLabs/homebrew-dcm"
[tools."github:CQLabs/homebrew-dcm".options]
asset_pattern = "dcm-windows-release.zip"
[tools."github:CQLabs/homebrew-dcm"."platforms.windows-x64"]
checksum = "sha256:f133470daa3fb0427f039b424392af7e917d7e7db6b556aa2a968ab0e31587da"
url = "https://github.com/CQLabs/homebrew-dcm/releases/download/1.37.0/dcm-windows-release.zip"
url_api = "https://api.github.com/repos/CQLabs/homebrew-dcm/releases/assets/404543660"
[[tools."github:CQLabs/homebrew-dcm"]]
version = "1.37.0"
version = "1.38.1"
backend = "github:CQLabs/homebrew-dcm"
[tools."github:CQLabs/homebrew-dcm".options]
asset_pattern = "dcm-macos-arm-release.zip"
[tools."github:CQLabs/homebrew-dcm"."platforms.macos-arm64"]
checksum = "sha256:30bede64367d09067093cc57af6ec9496d7717898138ded5cb98a16ac8dd9d93"
url = "https://github.com/CQLabs/homebrew-dcm/releases/download/1.37.0/dcm-macos-arm-release.zip"
url_api = "https://api.github.com/repos/CQLabs/homebrew-dcm/releases/assets/404543757"
checksum = "sha256:a08e0e5e881ce04885e787b24aa942597188950d5e53c66f45c2293b70758c5b"
url = "https://github.com/CQLabs/homebrew-dcm/releases/download/1.38.1/dcm-macos-arm-release.zip"
url_api = "https://api.github.com/repos/CQLabs/homebrew-dcm/releases/assets/455844382"
[[tools.java]]
version = "21.0.2"
backend = "core:java"
[[tools."github:CQLabs/homebrew-dcm"]]
version = "1.38.1"
backend = "github:CQLabs/homebrew-dcm"
[tools.java."platforms.linux-arm64"]
checksum = "sha256:08db1392a48d4eb5ea5315cf8f18b89dbaf36cda663ba882cf03c704c9257ec2"
url = "https://download.java.net/java/GA/jdk21.0.2/f2283984656d49d69e91c558476027ac/13/GPL/openjdk-21.0.2_linux-aarch64_bin.tar.gz"
[tools."github:CQLabs/homebrew-dcm".options]
asset_pattern = "dcm-linux-x64-release.zip"
[tools.java."platforms.linux-x64"]
checksum = "sha256:a2def047a73941e01a73739f92755f86b895811afb1f91243db214cff5bdac3f"
url = "https://download.java.net/java/GA/jdk21.0.2/f2283984656d49d69e91c558476027ac/13/GPL/openjdk-21.0.2_linux-x64_bin.tar.gz"
[tools."github:CQLabs/homebrew-dcm"."platforms.linux-x64"]
checksum = "sha256:27ea2b517a393b70f1abd9111b02b557854a89abcdfe756a4bded3cea3cff0aa"
url = "https://github.com/CQLabs/homebrew-dcm/releases/download/1.38.1/dcm-linux-x64-release.zip"
url_api = "https://api.github.com/repos/CQLabs/homebrew-dcm/releases/assets/455844566"
[tools.java."platforms.macos-arm64"]
checksum = "sha256:b3d588e16ec1e0ef9805d8a696591bd518a5cea62567da8f53b5ce32d11d22e4"
url = "https://download.java.net/java/GA/jdk21.0.2/f2283984656d49d69e91c558476027ac/13/GPL/openjdk-21.0.2_macos-aarch64_bin.tar.gz"
[tools."github:CQLabs/homebrew-dcm"."platforms.linux-x64-musl"]
checksum = "sha256:27ea2b517a393b70f1abd9111b02b557854a89abcdfe756a4bded3cea3cff0aa"
url = "https://github.com/CQLabs/homebrew-dcm/releases/download/1.38.1/dcm-linux-x64-release.zip"
url_api = "https://api.github.com/repos/CQLabs/homebrew-dcm/releases/assets/455844566"
[tools.java."platforms.macos-x64"]
checksum = "sha256:8fd09e15dc406387a0aba70bf5d99692874e999bf9cd9208b452b5d76ac922d3"
url = "https://download.java.net/java/GA/jdk21.0.2/f2283984656d49d69e91c558476027ac/13/GPL/openjdk-21.0.2_macos-x64_bin.tar.gz"
[[tools."github:CQLabs/homebrew-dcm"]]
version = "1.38.1"
backend = "github:CQLabs/homebrew-dcm"
[tools.java."platforms.windows-x64"]
checksum = "sha256:b6c17e747ae78cdd6de4d7532b3164b277daee97c007d3eaa2b39cca99882664"
url = "https://download.java.net/java/GA/jdk21.0.2/f2283984656d49d69e91c558476027ac/13/GPL/openjdk-21.0.2_windows-x64_bin.zip"
[tools."github:CQLabs/homebrew-dcm".options]
asset_pattern = "dcm-macos-x64-release.zip"
[[tools.java]]
version = "21.0.2"
backend = "core:java"
[tools."github:CQLabs/homebrew-dcm"."platforms.macos-x64"]
checksum = "sha256:27182502dfc6dab9181be9f0d1b3669468afb127cd2bcba09d534693e96cbe9a"
url = "https://github.com/CQLabs/homebrew-dcm/releases/download/1.38.1/dcm-macos-x64-release.zip"
url_api = "https://api.github.com/repos/CQLabs/homebrew-dcm/releases/assets/455844091"
[tools.java.options]
shorthand_vendor = "openjdk"
[[tools."github:CQLabs/homebrew-dcm"]]
version = "1.38.1"
backend = "github:CQLabs/homebrew-dcm"
[tools.java."platforms.linux-arm64"]
checksum = "sha256:08db1392a48d4eb5ea5315cf8f18b89dbaf36cda663ba882cf03c704c9257ec2"
url = "https://download.java.net/java/GA/jdk21.0.2/f2283984656d49d69e91c558476027ac/13/GPL/openjdk-21.0.2_linux-aarch64_bin.tar.gz"
[tools."github:CQLabs/homebrew-dcm".options]
asset_pattern = "dcm-windows-release.zip"
[tools.java."platforms.linux-x64"]
checksum = "sha256:a2def047a73941e01a73739f92755f86b895811afb1f91243db214cff5bdac3f"
url = "https://download.java.net/java/GA/jdk21.0.2/f2283984656d49d69e91c558476027ac/13/GPL/openjdk-21.0.2_linux-x64_bin.tar.gz"
[tools.java."platforms.macos-arm64"]
checksum = "sha256:b3d588e16ec1e0ef9805d8a696591bd518a5cea62567da8f53b5ce32d11d22e4"
url = "https://download.java.net/java/GA/jdk21.0.2/f2283984656d49d69e91c558476027ac/13/GPL/openjdk-21.0.2_macos-aarch64_bin.tar.gz"
[tools.java."platforms.macos-x64"]
checksum = "sha256:8fd09e15dc406387a0aba70bf5d99692874e999bf9cd9208b452b5d76ac922d3"
url = "https://download.java.net/java/GA/jdk21.0.2/f2283984656d49d69e91c558476027ac/13/GPL/openjdk-21.0.2_macos-x64_bin.tar.gz"
[tools.java."platforms.windows-x64"]
checksum = "sha256:b6c17e747ae78cdd6de4d7532b3164b277daee97c007d3eaa2b39cca99882664"
url = "https://download.java.net/java/GA/jdk21.0.2/f2283984656d49d69e91c558476027ac/13/GPL/openjdk-21.0.2_windows-x64_bin.zip"
[tools."github:CQLabs/homebrew-dcm"."platforms.windows-x64"]
checksum = "sha256:ef51b1f3e6312db9c3e4727f8e834972e93a9477358d8bb34c0481c3cc18a79e"
url = "https://github.com/CQLabs/homebrew-dcm/releases/download/1.38.1/dcm-windows-release.zip"
url_api = "https://api.github.com/repos/CQLabs/homebrew-dcm/releases/assets/455843911"
+3 -3
View File
@@ -1,9 +1,9 @@
[tools]
"aqua:flutter/flutter" = "3.44.1"
java = "21.0.2"
"aqua:flutter/flutter" = "3.44.4"
java = "21.0.11+10.0.LTS"
[tools."github:CQLabs/homebrew-dcm"]
version = "1.37.0"
version = "1.38.1"
bin = "dcm"
postinstall = "chmod +x \"$MISE_TOOL_INSTALL_PATH/dcm\" || true"
+7 -15
View File
@@ -6,23 +6,18 @@ final scaffoldMessengerKey = GlobalKey<ScaffoldMessengerState>();
class SnackbarManager {
const SnackbarManager();
ScaffoldFeatureController<SnackBar, SnackBarClosedReason>? show(
String message,
SnackbarType type, {
Duration? duration,
}) {
ScaffoldFeatureController<SnackBar, SnackBarClosedReason>? show(String message, SnackbarType type) {
final messenger = scaffoldMessengerKey.currentState;
final context = scaffoldMessengerKey.currentContext;
if (messenger == null || context == null) {
return null;
}
duration ??= const .new(seconds: 4);
messenger.hideCurrentSnackBar();
return messenger.showSnackBar(_build(context, message, type, duration));
return messenger.showSnackBar(_build(context, message, type));
}
SnackBar _build(BuildContext context, String message, SnackbarType type, Duration duration) {
SnackBar _build(BuildContext context, String message, SnackbarType type) {
final theme = Theme.of(context);
final colors = theme.extension<ImmichColors>() ?? ImmichColors.harmonized(theme.colorScheme);
final (IconData icon, Color background, Color foreground) = switch (type) {
@@ -34,7 +29,7 @@ class SnackbarManager {
return SnackBar(
behavior: .floating,
backgroundColor: background,
duration: duration,
duration: const .new(seconds: 4),
shape: const RoundedRectangleBorder(borderRadius: .all(.circular(ImmichRadius.sm))),
content: Row(
children: [
@@ -53,14 +48,11 @@ class SnackbarManager {
);
}
ScaffoldFeatureController<SnackBar, SnackBarClosedReason>? info(String message, {Duration? duration}) =>
show(message, .info, duration: duration);
ScaffoldFeatureController<SnackBar, SnackBarClosedReason>? info(String message) => show(message, .info);
ScaffoldFeatureController<SnackBar, SnackBarClosedReason>? success(String message, {Duration? duration}) =>
show(message, .success, duration: duration);
ScaffoldFeatureController<SnackBar, SnackBarClosedReason>? success(String message) => show(message, .success);
ScaffoldFeatureController<SnackBar, SnackBarClosedReason>? error(String message, {Duration? duration}) =>
show(message, .error, duration: duration);
ScaffoldFeatureController<SnackBar, SnackBarClosedReason>? error(String message) => show(message, .error);
}
const snackbar = SnackbarManager();
+6 -6
View File
@@ -6,7 +6,7 @@ version: 3.0.0-rc.4+3052
environment:
sdk: '>=3.12.0 <4.0.0'
flutter: 3.44.1
flutter: 3.44.4
dependencies:
async: ^2.13.1
@@ -35,7 +35,7 @@ dependencies:
flutter_web_auth_2: ^5.0.2
fluttertoast: ^8.2.14
geolocator: ^14.0.2
home_widget: ^0.8.1
home_widget: ^0.9.0
hooks_riverpod: ^2.6.1
http: ^1.6.0
image_picker: ^1.2.1
@@ -44,7 +44,7 @@ dependencies:
intl: ^0.20.2
local_auth: ^2.3.0
logging: ^1.3.0
maplibre_gl: ^0.22.0
maplibre_gl: ^0.26.0
native_video_player:
git:
url: https://github.com/immich-app/native_video_player
@@ -68,10 +68,10 @@ dependencies:
sliver_tools: ^0.2.12
stream_transform: ^2.1.1
sqlite3: ^3.3.2
sqlite_async: 0.14.2
sqlite_async: 0.14.3
sqlite3_connection_pool: ^0.2.6
thumbhash: 0.1.0+1
timezone: ^0.9.4
timezone: ^0.11.0
url_launcher: ^6.3.2
uuid: ^4.5.3
wakelock_plus: ^1.3.3
@@ -84,7 +84,7 @@ dependencies:
cupertino_http:
git:
url: https://github.com/mertalev/http
ref: 'a0a933358517c6d01cff37fc2a2752ee2d744a3c' # https://github.com/dart-lang/http/pull/1876
ref: '0.13.4' # https://github.com/dart-lang/http/pull/1876
path: pkgs/cupertino_http/
ok_http:
git:
@@ -5,14 +5,7 @@ import '../../utils.dart';
class RemoteAssetFactory {
const RemoteAssetFactory();
static RemoteAsset create({
String? id,
String? name,
String? ownerId,
bool isFavorite = false,
AssetVisibility visibility = AssetVisibility.timeline,
String? stackId,
}) {
static RemoteAsset create({String? id, String? name, String? ownerId, bool isFavorite = false}) {
id = TestUtils.uuid(id);
return RemoteAsset(
@@ -24,8 +17,6 @@ class RemoteAssetFactory {
createdAt: TestUtils.yesterday(),
updatedAt: TestUtils.now(),
isFavorite: isFavorite,
visibility: visibility,
stackId: stackId,
isEdited: false,
);
}
@@ -1,200 +0,0 @@
import 'package:flutter_test/flutter_test.dart';
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
import 'package:immich_mobile/utils/asset_filter.dart';
import '../factories/local_asset_factory.dart';
import '../factories/remote_asset_factory.dart';
void main() {
group('AssetFilter', () {
group('type promotion', () {
test('a bare filter retains every BaseAsset', () {
final remoteAsset = RemoteAssetFactory.create();
final localAsset = LocalAssetFactory.create();
final AssetFilter<BaseAsset> filter = AssetFilter(<BaseAsset>[remoteAsset, localAsset]);
expect(filter.toList(), [remoteAsset, localAsset]);
});
test('remote keeps only remote assets', () {
final remoteAsset = RemoteAssetFactory.create();
final localAsset = LocalAssetFactory.create();
final AssetFilter<RemoteAsset> remoteOnly = AssetFilter(<BaseAsset>[remoteAsset, localAsset]).remote();
expect(remoteOnly.toList(), [remoteAsset]);
});
test('local keeps only local assets', () {
final remoteAsset = RemoteAssetFactory.create();
final localAsset = LocalAssetFactory.create();
final AssetFilter<LocalAsset> localOnly = AssetFilter(<BaseAsset>[remoteAsset, localAsset]).local();
expect(localOnly.toList(), [localAsset]);
});
test('owned promotes to RemoteAsset and drops local assets', () {
final remoteAsset = RemoteAssetFactory.create();
final localAsset = LocalAssetFactory.create();
final AssetFilter<RemoteAsset> remoteOnly = AssetFilter(<BaseAsset>[
remoteAsset,
localAsset,
]).owned(remoteAsset.ownerId);
expect(remoteOnly.toList(), [remoteAsset]);
});
test('backedUp promotes to LocalAsset and drops remote assets', () {
final syncedPhoto = LocalAssetFactory.create().copyWith(remoteId: 'remote');
final offlinePhoto = LocalAssetFactory.create();
final remotePhoto = RemoteAssetFactory.create();
final AssetFilter<LocalAsset> syncedPhotos = AssetFilter(<BaseAsset>[
syncedPhoto,
offlinePhoto,
remotePhoto,
]).backedUp();
expect(syncedPhotos.toList(), [syncedPhoto]);
});
});
group('named filters', () {
test('owned keeps only assets of the given owner', () {
final asset1 = RemoteAssetFactory.create();
final asset2 = RemoteAssetFactory.create();
final alexPhotos = AssetFilter([asset1, asset2]).owned(asset1.ownerId);
expect(alexPhotos.toList(), [asset1]);
});
test('favorites keeps only favorite assets', () {
final asset1 = RemoteAssetFactory.create(isFavorite: true);
final asset2 = RemoteAssetFactory.create(ownerId: asset1.ownerId);
final favorites = AssetFilter([asset1, asset2]).favorites();
expect(favorites.toList(), [asset1]);
});
test('type keeps only assets of the given type', () {
final image = RemoteAssetFactory.create();
final video = RemoteAssetFactory.create(ownerId: image.ownerId).copyWith(type: .video);
final videos = AssetFilter([image, video]).type(.video);
expect(videos.toList(), [video]);
});
test('visibility keeps only assets with the given visibility', () {
final locked = RemoteAssetFactory.create(visibility: AssetVisibility.locked);
final onTimeline = RemoteAssetFactory.create(ownerId: locked.ownerId);
final lockedPhotos = AssetFilter([locked, onTimeline]).visibility(.locked);
expect(lockedPhotos.toList(), [locked]);
});
test('archived keeps only archived assets', () {
final archived = RemoteAssetFactory.create(visibility: AssetVisibility.archive);
final onTimeline = RemoteAssetFactory.create(ownerId: archived.ownerId);
final archivedPhotos = AssetFilter([archived, onTimeline]).archived();
expect(archivedPhotos.toList(), [archived]);
});
test('stacked keeps only assets belonging to a stack', () {
final stacked = RemoteAssetFactory.create(stackId: 'stack');
final loose = RemoteAssetFactory.create(ownerId: stacked.ownerId);
final stackedPhotos = AssetFilter([stacked, loose]).stacked();
expect(stackedPhotos.toList(), [stacked]);
});
});
group('inversion', () {
test('notArchived keeps every non-archived visibility', () {
final archived = RemoteAssetFactory.create(visibility: AssetVisibility.archive);
final onTimeline = RemoteAssetFactory.create(ownerId: archived.ownerId, visibility: AssetVisibility.timeline);
final hidden = RemoteAssetFactory.create(ownerId: archived.ownerId, visibility: AssetVisibility.hidden);
final locked = RemoteAssetFactory.create(ownerId: archived.ownerId, visibility: AssetVisibility.locked);
final visiblePhotos = AssetFilter([archived, onTimeline, hidden, locked]).notArchived();
expect(visiblePhotos.toSet(), {onTimeline, hidden, locked});
});
test('notVisibility keeps every asset not at the target visibility', () {
final archived = RemoteAssetFactory.create(visibility: AssetVisibility.archive);
final onTimeline = RemoteAssetFactory.create(ownerId: archived.ownerId, visibility: AssetVisibility.timeline);
final locked = RemoteAssetFactory.create(ownerId: archived.ownerId, visibility: AssetVisibility.locked);
final toArchive = AssetFilter([archived, onTimeline, locked]).notVisibility(.archive);
expect(toArchive.toSet(), {onTimeline, locked});
});
test('notStacked keeps only assets without a stack', () {
final stacked = RemoteAssetFactory.create(stackId: 'stack');
final loose = RemoteAssetFactory.create(ownerId: stacked.ownerId);
final loosePhotos = AssetFilter([stacked, loose]).notStacked();
expect(loosePhotos.toList(), [loose]);
});
test('whereNot inverts an arbitrary predicate', () {
final favorite = RemoteAssetFactory.create(isFavorite: true);
final regular = RemoteAssetFactory.create(ownerId: favorite.ownerId);
final nonFavorites = AssetFilter([favorite, regular]).whereNot((asset) => asset.isFavorite);
expect(nonFavorites.toList(), [regular]);
});
test('notFavorites keeps only non-favorite assets', () {
final favorite = RemoteAssetFactory.create(isFavorite: true);
final regular = RemoteAssetFactory.create(ownerId: favorite.ownerId);
final nonFavorites = AssetFilter([favorite, regular]).notFavorites();
expect(nonFavorites.toList(), [regular]);
});
});
group('chaining', () {
test('combines predicates across owner, visibility and stack', () {
final asset = RemoteAssetFactory.create();
final wrongOwner = RemoteAssetFactory.create();
final archived = RemoteAssetFactory.create(ownerId: asset.ownerId, visibility: AssetVisibility.archive);
final stacked = RemoteAssetFactory.create(ownerId: asset.ownerId, stackId: 'stack-1');
final localPhoto = LocalAssetFactory.create();
final result = AssetFilter(<BaseAsset>[
asset,
wrongOwner,
archived,
stacked,
localPhoto,
]).owned(asset.ownerId).notArchived().notStacked();
expect(result.toList(), [asset]);
});
test('a base filter after a promotion retains the promoted type', () {
final favorite = RemoteAssetFactory.create(isFavorite: true);
final regular = RemoteAssetFactory.create(ownerId: favorite.ownerId);
final AssetFilter<RemoteAsset> result = AssetFilter([favorite, regular]).owned(favorite.ownerId).favorites();
expect(result.toList(), [favorite]);
});
});
});
}
+28 -33
View File
@@ -7,18 +7,17 @@ from
"asset"
inner join "asset_exif" on "asset"."id" = "asset_exif"."assetId"
where
"asset"."visibility" = $1
and "asset"."fileCreatedAt" >= $2
and "asset_exif"."lensModel" = $3
and "asset"."ownerId" = any ($4::uuid[])
and "asset"."isFavorite" = $5
"asset"."fileCreatedAt" >= $1
and "asset_exif"."lensModel" = $2
and "asset"."ownerId" = any ($3::uuid[])
and "asset"."isFavorite" = $4
and "asset"."deletedAt" is null
order by
"asset"."fileCreatedAt" desc
limit
$6
$5
offset
$7
$6
-- SearchRepository.searchStatistics
select
@@ -27,11 +26,10 @@ from
"asset"
inner join "asset_exif" on "asset"."id" = "asset_exif"."assetId"
where
"asset"."visibility" = $1
and "asset"."fileCreatedAt" >= $2
and "asset_exif"."lensModel" = $3
and "asset"."ownerId" = any ($4::uuid[])
and "asset"."isFavorite" = $5
"asset"."fileCreatedAt" >= $1
and "asset_exif"."lensModel" = $2
and "asset"."ownerId" = any ($3::uuid[])
and "asset"."isFavorite" = $4
and "asset"."deletedAt" is null
-- SearchRepository.searchRandom
@@ -41,16 +39,15 @@ from
"asset"
inner join "asset_exif" on "asset"."id" = "asset_exif"."assetId"
where
"asset"."visibility" = $1
and "asset"."fileCreatedAt" >= $2
and "asset_exif"."lensModel" = $3
and "asset"."ownerId" = any ($4::uuid[])
and "asset"."isFavorite" = $5
"asset"."fileCreatedAt" >= $1
and "asset_exif"."lensModel" = $2
and "asset"."ownerId" = any ($3::uuid[])
and "asset"."isFavorite" = $4
and "asset"."deletedAt" is null
order by
random()
limit
$6
$5
-- SearchRepository.searchLargeAssets
select
@@ -60,17 +57,16 @@ from
"asset"
inner join "asset_exif" on "asset"."id" = "asset_exif"."assetId"
where
"asset"."visibility" = $1
and "asset"."fileCreatedAt" >= $2
and "asset_exif"."lensModel" = $3
and "asset"."ownerId" = any ($4::uuid[])
and "asset"."isFavorite" = $5
"asset"."fileCreatedAt" >= $1
and "asset_exif"."lensModel" = $2
and "asset"."ownerId" = any ($3::uuid[])
and "asset"."isFavorite" = $4
and "asset"."deletedAt" is null
and "asset_exif"."fileSizeInByte" > $6
and "asset_exif"."fileSizeInByte" > $5
order by
"asset_exif"."fileSizeInByte" desc
limit
$7
$6
-- SearchRepository.searchSmart
begin
@@ -83,18 +79,17 @@ from
inner join "asset_exif" on "asset"."id" = "asset_exif"."assetId"
inner join "smart_search" on "asset"."id" = "smart_search"."assetId"
where
"asset"."visibility" = $1
and "asset"."fileCreatedAt" >= $2
and "asset_exif"."lensModel" = $3
and "asset"."ownerId" = any ($4::uuid[])
and "asset"."isFavorite" = $5
"asset"."fileCreatedAt" >= $1
and "asset_exif"."lensModel" = $2
and "asset"."ownerId" = any ($3::uuid[])
and "asset"."isFavorite" = $4
and "asset"."deletedAt" is null
order by
smart_search.embedding <=> $6
smart_search.embedding <=> $5
limit
$7
$6
offset
$8
$7
commit
-- SearchRepository.getEmbedding
+4 -3
View File
@@ -117,7 +117,8 @@ type BaseAssetSearchOptions = SearchDateOptions &
SearchAlbumOptions &
SearchOcrOptions;
export type AssetSearchOptions = BaseAssetSearchOptions & SearchRelationOptions;
export type AssetSearchOptions = Omit<BaseAssetSearchOptions, 'visibility'> &
SearchRelationOptions & { visibility?: AssetVisibility | 'not-locked' };
export type AssetSearchBuilderOptions = Omit<AssetSearchOptions, 'orderDirection'>;
@@ -125,11 +126,11 @@ export type SmartSearchOptions = SearchDateOptions &
SearchEmbeddingOptions &
SearchExifOptions &
SearchOneToOneRelationOptions &
SearchStatusOptions &
Omit<SearchStatusOptions, 'visibility'> &
SearchUserIdOptions &
SearchPeopleOptions &
SearchTagOptions &
SearchOcrOptions;
SearchOcrOptions & { visibility?: AssetVisibility | 'not-locked' };
export type OcrSearchOptions = SearchDateOptions & SearchOcrOptions;
+1 -1
View File
@@ -250,7 +250,7 @@ describe(SearchService.name, () => {
);
expect(mocks.search.searchSmart).toHaveBeenCalledWith(
{ page: 1, size: 100 },
{ query: 'test', embedding: '[1, 2, 3]', userIds: [authStub.user1.user.id] },
{ query: 'test', embedding: '[1, 2, 3]', userIds: [authStub.user1.user.id], visibility: 'not-locked' },
);
});
+24 -3
View File
@@ -73,14 +73,22 @@ export class SearchService extends BaseService {
checksum = Buffer.from(dto.checksum, encoding);
}
let userIds: string[] | undefined;
if (dto.albumIds && dto.albumIds.length > 0) {
await this.requireAccess({ auth, ids: dto.albumIds, permission: Permission.AlbumRead });
} else {
userIds = await this.getUserIdsToSearch(auth, dto.visibility);
}
const page = dto.page ?? 1;
const size = dto.size || 250;
const userIds = await this.getUserIdsToSearch(auth, dto.visibility);
const { hasNextPage, items } = await this.searchRepository.searchMetadata(
{ page, size },
{
...dto,
checksum,
visibility: dto.visibility ?? (auth.session?.hasElevatedPermission ? undefined : 'not-locked'),
userIds,
orderDirection: dto.order ?? AssetOrder.Desc,
},
@@ -91,9 +99,13 @@ export class SearchService extends BaseService {
async searchStatistics(auth: AuthDto, dto: StatisticsSearchDto): Promise<SearchStatisticsResponseDto> {
const userIds = await this.getUserIdsToSearch(auth);
if (dto.visibility === AssetVisibility.Locked) {
requireElevatedPermission(auth);
}
return await this.searchRepository.searchStatistics({
...dto,
visibility: dto.visibility ?? (auth.session?.hasElevatedPermission ? undefined : 'not-locked'),
userIds,
});
}
@@ -114,7 +126,11 @@ export class SearchService extends BaseService {
}
const userIds = await this.getUserIdsToSearch(auth, dto.visibility);
const items = await this.searchRepository.searchLargeAssets(dto.size || 250, { ...dto, userIds });
const items = await this.searchRepository.searchLargeAssets(dto.size || 250, {
...dto,
visibility: dto.visibility ?? (auth.session?.hasElevatedPermission ? undefined : 'not-locked'),
userIds,
});
return items.map((item) => mapAsset(item, { auth }));
}
@@ -155,7 +171,12 @@ export class SearchService extends BaseService {
const size = dto.size || 100;
const { hasNextPage, items } = await this.searchRepository.searchSmart(
{ page, size },
{ ...dto, userIds: await userIds, embedding },
{
...dto,
userIds: await userIds,
embedding,
visibility: dto.visibility ?? (auth.session?.hasElevatedPermission ? undefined : 'not-locked'),
},
);
return this.mapResponse(items, hasNextPage ? (page + 1).toString() : null, { auth });
+5 -2
View File
@@ -373,12 +373,15 @@ const joinDeduplicationPlugin = new DeduplicateJoinsPlugin();
export function searchAssetBuilder(kysely: Kysely<DB>, options: AssetSearchBuilderOptions) {
options.withDeleted ||= !!(options.trashedAfter || options.trashedBefore || options.isOffline);
const visibility = options.visibility == null ? AssetVisibility.Timeline : options.visibility;
return kysely
.withPlugin(joinDeduplicationPlugin)
.selectFrom('asset')
.where('asset.visibility', '=', visibility)
.$if(!!options.visibility, (qb) =>
options.visibility === 'not-locked'
? qb.where('asset.visibility', '!=', AssetVisibility.Locked)
: qb.where('asset.visibility', '=', options.visibility!),
)
.$if(!!options.albumIds && options.albumIds.length > 0, (qb) => inAlbums(qb, options.albumIds!))
.$if(!!options.tagIds && options.tagIds.length > 0, (qb) => hasTags(qb, options.tagIds!))
.$if(options.tagIds === null, (qb) =>
@@ -1,5 +1,6 @@
import { Kysely } from 'kysely';
import { SearchSuggestionType } from 'src/dtos/search.dto';
import { AlbumUserRole, AssetVisibility } from 'src/enum';
import { AccessRepository } from 'src/repositories/access.repository';
import { AssetRepository } from 'src/repositories/asset.repository';
import { DatabaseRepository } from 'src/repositories/database.repository';
@@ -108,6 +109,71 @@ describe(SearchService.name, () => {
expect(response.assets.items.length).toBe(1);
expect(response.assets.items[0].id).toBe(unstackedAsset.id);
});
describe('visibility', () => {
it('should filter out locked assets in a default session', async () => {
const { sut, ctx } = setup();
const { user } = await ctx.newUser();
await ctx.newAsset({ ownerId: user.id, visibility: AssetVisibility.Locked });
const auth = factory.auth({ user: { id: user.id } });
const response = await sut.searchMetadata(auth, { withStacked: false });
expect(response.assets.items.length).toBe(0);
});
it('should return locked assets in an elevated session', async () => {
const { sut, ctx } = setup();
const { user } = await ctx.newUser();
await ctx.newAsset({ ownerId: user.id, visibility: AssetVisibility.Locked });
const auth = factory.auth({ user: { id: user.id }, session: { hasElevatedPermission: true } });
const response = await sut.searchMetadata(auth, { withStacked: false });
expect(response.assets.items.length).toBe(1);
});
});
});
describe('albumIds option', () => {
it('should return assets from shared album', async () => {
const { sut, ctx } = setup();
const { user } = await ctx.newUser();
const { user: otherUser } = await ctx.newUser();
const { asset } = await ctx.newAsset({ ownerId: otherUser.id });
const { album } = await ctx.newAlbum({ ownerId: otherUser.id });
await ctx.newAlbumAsset({ albumId: album.id, assetId: asset.id });
await ctx.newAlbumUser({ albumId: album.id, userId: user.id, role: AlbumUserRole.Editor });
const auth = factory.auth({ user: { id: user.id } });
const response = await sut.searchMetadata(auth, { albumIds: [album.id] });
expect(response.assets.items.length).toBe(1);
});
it('should not return assets for album, a user is not in, when partner sharing is enabled', async () => {
const { sut, ctx } = setup();
const { user } = await ctx.newUser();
const { user: otherUser } = await ctx.newUser();
await ctx.newPartner({ sharedById: otherUser.id, sharedWithId: user.id });
const { asset } = await ctx.newAsset({ ownerId: otherUser.id });
const { album } = await ctx.newAlbum({ ownerId: otherUser.id });
await ctx.newAlbumAsset({ albumId: album.id, assetId: asset.id });
const auth = factory.auth({ user: { id: user.id } });
await expect(sut.searchMetadata(auth, { albumIds: [album.id] })).rejects.toThrow(
'Not found or no album.read access',
);
});
});
describe('getSearchSuggestions', () => {
+3 -3
View File
@@ -27,7 +27,7 @@ const authFactory = ({
user,
}: {
apiKey?: Partial<AuthApiKey>;
session?: { id: string };
session?: { id?: string; hasElevatedPermission?: boolean };
user?: Omit<
Partial<UserAdmin>,
'createdAt' | 'updatedAt' | 'deletedAt' | 'fileCreatedAt' | 'fileModifiedAt' | 'localDateTime' | 'profileChangedAt'
@@ -46,8 +46,8 @@ const authFactory = ({
if (session) {
auth.session = {
id: session.id,
hasElevatedPermission: false,
id: session.id ?? newUuid(),
hasElevatedPermission: session.hasElevatedPermission ?? false,
};
}