mirror of
https://github.com/immich-app/immich.git
synced 2026-06-26 08:24:29 -07:00
Compare commits
4 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| a8b5817bce | |||
| cb1af3a8ec | |||
| 49a821b0d0 | |||
| 3a7034d25e |
@@ -1,35 +1,38 @@
|
||||
import 'package:immich_mobile/domain/models/album/local_album.model.dart';
|
||||
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
|
||||
import 'package:immich_mobile/domain/models/asset_edit.model.dart';
|
||||
import 'package:immich_mobile/domain/models/exif.model.dart';
|
||||
import 'package:immich_mobile/infrastructure/repositories/local_asset.repository.dart';
|
||||
import 'package:immich_mobile/infrastructure/repositories/remote_asset.repository.dart';
|
||||
import 'package:immich_mobile/repositories/asset_api.repository.dart';
|
||||
|
||||
class AssetService {
|
||||
final RemoteAssetRepository _remoteAssetRepository;
|
||||
final DriftLocalAssetRepository _localAssetRepository;
|
||||
final RemoteAssetRepository _remoteRepository;
|
||||
final DriftLocalAssetRepository _localRepository;
|
||||
final AssetApiRepository _apiRepository;
|
||||
|
||||
const AssetService({required this._remoteAssetRepository, required this._localAssetRepository});
|
||||
const AssetService({required this._remoteRepository, required this._localRepository, required this._apiRepository});
|
||||
|
||||
Future<BaseAsset?> getAsset(BaseAsset asset) {
|
||||
final id = asset is LocalAsset ? asset.id : (asset as RemoteAsset).id;
|
||||
return asset is LocalAsset ? _localAssetRepository.get(id) : _remoteAssetRepository.get(id);
|
||||
return asset is LocalAsset ? _localRepository.get(id) : _remoteRepository.get(id);
|
||||
}
|
||||
|
||||
Stream<BaseAsset?> watchAsset(BaseAsset asset) {
|
||||
final id = asset is LocalAsset ? asset.id : (asset as RemoteAsset).id;
|
||||
return asset is LocalAsset ? _localAssetRepository.watch(id) : _remoteAssetRepository.watch(id);
|
||||
return asset is LocalAsset ? _localRepository.watch(id) : _remoteRepository.watch(id);
|
||||
}
|
||||
|
||||
Future<List<LocalAsset?>> getLocalAssetsByChecksum(String checksum) {
|
||||
return _localAssetRepository.getByChecksum(checksum);
|
||||
return _localRepository.getByChecksum(checksum);
|
||||
}
|
||||
|
||||
Future<RemoteAsset?> getRemoteAssetByChecksum(String checksum) {
|
||||
return _remoteAssetRepository.getByChecksum(checksum);
|
||||
return _remoteRepository.getByChecksum(checksum);
|
||||
}
|
||||
|
||||
Future<RemoteAsset?> getRemoteAsset(String id) {
|
||||
return _remoteAssetRepository.get(id);
|
||||
return _remoteRepository.get(id);
|
||||
}
|
||||
|
||||
Future<List<RemoteAsset>> getStack(RemoteAsset asset) async {
|
||||
@@ -37,7 +40,7 @@ class AssetService {
|
||||
return const [];
|
||||
}
|
||||
|
||||
final stack = await _remoteAssetRepository.getStackChildren(asset);
|
||||
final stack = await _remoteRepository.getStackChildren(asset);
|
||||
// Include the primary asset in the stack as the first item
|
||||
return [asset, ...stack];
|
||||
}
|
||||
@@ -48,22 +51,39 @@ class AssetService {
|
||||
}
|
||||
|
||||
final id = asset is LocalAsset ? asset.remoteId! : (asset as RemoteAsset).id;
|
||||
return _remoteAssetRepository.getExif(id);
|
||||
return _remoteRepository.getExif(id);
|
||||
}
|
||||
|
||||
Future<List<(String, String)>> getPlaces(String userId) {
|
||||
return _remoteAssetRepository.getPlaces(userId);
|
||||
return _remoteRepository.getPlaces(userId);
|
||||
}
|
||||
|
||||
Future<(int local, int remote)> getAssetCounts() async {
|
||||
return (await _localAssetRepository.getCount(), await _remoteAssetRepository.getCount());
|
||||
return (await _localRepository.getCount(), await _remoteRepository.getCount());
|
||||
}
|
||||
|
||||
Future<int> getLocalHashedCount() {
|
||||
return _localAssetRepository.getHashedCount();
|
||||
return _localRepository.getHashedCount();
|
||||
}
|
||||
|
||||
Future<List<LocalAlbum>> getSourceAlbums(String localAssetId, {BackupSelection? backupSelection}) {
|
||||
return _localAssetRepository.getSourceAlbums(localAssetId, backupSelection: backupSelection);
|
||||
return _localRepository.getSourceAlbums(localAssetId, backupSelection: backupSelection);
|
||||
}
|
||||
|
||||
Future<void> updateFavorite(List<String> remoteIds, bool isFavorite) async {
|
||||
if (remoteIds.isEmpty) {
|
||||
return;
|
||||
}
|
||||
|
||||
await _apiRepository.updateFavorite(remoteIds, isFavorite);
|
||||
await _remoteRepository.updateFavorite(remoteIds, isFavorite);
|
||||
}
|
||||
|
||||
Future<void> applyEdits(String remoteId, List<AssetEdit> edits) async {
|
||||
if (edits.isEmpty) {
|
||||
await _apiRepository.removeEdits(remoteId);
|
||||
} else {
|
||||
await _apiRepository.editAsset(remoteId, edits);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
|
||||
import 'package:immich_mobile/domain/models/user.model.dart';
|
||||
|
||||
class ActionScope {
|
||||
@@ -21,3 +22,11 @@ abstract class BaseAction {
|
||||
|
||||
Future<void> onAction(ActionScope scope);
|
||||
}
|
||||
|
||||
abstract class AssetAction<T extends BaseAsset> extends BaseAction {
|
||||
final Iterable<BaseAsset> assets;
|
||||
|
||||
const AssetAction({required this.assets});
|
||||
|
||||
Iterable<T> filter(ActionScope scope) => assets.whereType<T>();
|
||||
}
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:auto_route/auto_route.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
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/setting.provider.dart';
|
||||
import 'package:immich_mobile/routing/router.dart';
|
||||
|
||||
class AssetDebugAction extends AssetAction<BaseAsset> {
|
||||
const AssetDebugAction({required super.assets});
|
||||
|
||||
@override
|
||||
IconData get icon => Icons.help_outline_rounded;
|
||||
|
||||
@override
|
||||
String label(ActionScope scope) => scope.context.t.troubleshoot;
|
||||
|
||||
@override
|
||||
bool isVisible(ActionScope scope) =>
|
||||
assets.length == 1 && scope.ref.watch(settingsProvider.notifier).get(.advancedTroubleshooting);
|
||||
|
||||
@override
|
||||
Future<void> onAction(ActionScope scope) async =>
|
||||
unawaited(scope.context.pushRoute(AssetTroubleshootRoute(asset: assets.first)));
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:auto_route/auto_route.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
|
||||
import 'package:immich_mobile/domain/models/asset_edit.model.dart';
|
||||
import 'package:immich_mobile/generated/translations.g.dart';
|
||||
import 'package:immich_mobile/presentation/actions/action.dart';
|
||||
import 'package:immich_mobile/presentation/pages/edit/editor.provider.dart';
|
||||
import 'package:immich_mobile/presentation/widgets/images/image_provider.dart';
|
||||
import 'package:immich_mobile/providers/infrastructure/asset.provider.dart';
|
||||
import 'package:immich_mobile/providers/server_info.provider.dart';
|
||||
import 'package:immich_mobile/providers/websocket.provider.dart';
|
||||
import 'package:immich_mobile/routing/router.dart';
|
||||
import 'package:immich_mobile/utils/semver.dart';
|
||||
|
||||
class EditAssetAction extends AssetAction<RemoteAsset> {
|
||||
const EditAssetAction({required super.assets});
|
||||
|
||||
@override
|
||||
IconData get icon => Icons.tune;
|
||||
|
||||
@override
|
||||
String label(ActionScope scope) => scope.context.t.edit;
|
||||
|
||||
@override
|
||||
Iterable<RemoteAsset> filter(ActionScope scope) =>
|
||||
assets.where((asset) => asset is RemoteAsset && asset.ownerId == scope.authUser.id && asset.isEditable).cast();
|
||||
|
||||
@override
|
||||
bool isVisible(ActionScope scope) =>
|
||||
filter(scope).length == 1 &&
|
||||
scope.ref.watch(serverInfoProvider).serverVersion >= const SemVer(major: 2, minor: 6, patch: 0);
|
||||
|
||||
@override
|
||||
Future<void> onAction(ActionScope scope) async {
|
||||
final ActionScope(:context, :ref) = scope;
|
||||
|
||||
final asset = filter(scope).first;
|
||||
final remoteId = asset.id;
|
||||
final repository = ref.read(remoteAssetRepositoryProvider);
|
||||
final (edits, exif) = await (repository.getAssetEdits(remoteId), repository.getExif(remoteId)).wait;
|
||||
if (exif == null || !context.mounted) {
|
||||
return;
|
||||
}
|
||||
|
||||
ref.read(editorStateProvider.notifier).init(edits, exif);
|
||||
unawaited(
|
||||
context.pushRoute(
|
||||
DriftEditImageRoute(
|
||||
image: Image(image: getFullImageProvider(asset, edited: false)),
|
||||
applyEdits: (newEdits) => applyEdits(ref, remoteId, newEdits),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@visibleForTesting
|
||||
static Future<void> applyEdits(WidgetRef ref, String remoteId, List<AssetEdit> edits) async {
|
||||
final websocket = ref.read(websocketProvider.notifier);
|
||||
|
||||
bool isCurrentId(dynamic data) => data is Map && (data['asset'] as Map?)?['id'] == remoteId;
|
||||
await ref.read(assetServiceProvider).applyEdits(remoteId, edits);
|
||||
await Future.any([
|
||||
websocket.waitForEvent('AssetEditReadyV1', isCurrentId, const Duration(seconds: 10)),
|
||||
websocket.waitForEvent('AssetEditReadyV2', isCurrentId, const Duration(seconds: 10)),
|
||||
]).catchError((_) {});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
import 'package:flutter/material.dart';
|
||||
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_ui/immich_ui.dart';
|
||||
|
||||
class FavoriteAction extends AssetAction<RemoteAsset> {
|
||||
final bool shouldFavorite;
|
||||
|
||||
FavoriteAction({required super.assets}) : shouldFavorite = assets.any((asset) => !asset.isFavorite);
|
||||
|
||||
@override
|
||||
IconData get icon => shouldFavorite ? Icons.favorite_border_rounded : Icons.favorite_rounded;
|
||||
|
||||
@override
|
||||
String label(ActionScope scope) => shouldFavorite ? scope.context.t.favorite : scope.context.t.unfavorite;
|
||||
|
||||
@override
|
||||
Iterable<RemoteAsset> filter(ActionScope scope) => assets
|
||||
.where(
|
||||
(asset) => asset is RemoteAsset && asset.ownerId == scope.authUser.id && asset.isFavorite == !shouldFavorite,
|
||||
)
|
||||
.cast();
|
||||
|
||||
@override
|
||||
bool isVisible(ActionScope scope) => filter(scope).isNotEmpty;
|
||||
|
||||
@override
|
||||
Future<void> onAction(ActionScope scope) async {
|
||||
final ActionScope(:ref) = scope;
|
||||
final assets = filter(scope).map((asset) => asset.id).toList(growable: false);
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:immich_mobile/presentation/actions/action.dart';
|
||||
import 'package:immich_mobile/providers/timeline/multiselect.provider.dart';
|
||||
|
||||
class TimelineAction extends BaseAction {
|
||||
final BaseAction action;
|
||||
|
||||
const TimelineAction({required this.action});
|
||||
|
||||
@override
|
||||
IconData get icon => action.icon;
|
||||
|
||||
@override
|
||||
String label(ActionScope scope) => action.label(scope);
|
||||
|
||||
@override
|
||||
bool isVisible(ActionScope scope) => action.isVisible(scope);
|
||||
|
||||
@override
|
||||
Future<void> onAction(ActionScope scope) async {
|
||||
await action.onAction(scope);
|
||||
scope.ref.read(multiSelectProvider.notifier).reset();
|
||||
}
|
||||
}
|
||||
-36
@@ -1,36 +0,0 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:immich_mobile/constants/enums.dart';
|
||||
import 'package:immich_mobile/extensions/translate_extensions.dart';
|
||||
import 'package:immich_mobile/presentation/widgets/action_buttons/base_action_button.widget.dart';
|
||||
import 'package:immich_mobile/providers/infrastructure/action.provider.dart';
|
||||
|
||||
class AdvancedInfoActionButton extends ConsumerWidget {
|
||||
final ActionSource source;
|
||||
final bool iconOnly;
|
||||
final bool menuItem;
|
||||
|
||||
const AdvancedInfoActionButton({super.key, required this.source, this.iconOnly = false, this.menuItem = false});
|
||||
|
||||
void _onTap(BuildContext context, WidgetRef ref) async {
|
||||
if (!context.mounted) {
|
||||
return;
|
||||
}
|
||||
|
||||
unawaited(ref.read(actionProvider.notifier).troubleshoot(source, context));
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
return BaseActionButton(
|
||||
maxWidth: 115.0,
|
||||
iconData: Icons.help_outline_rounded,
|
||||
label: "troubleshoot".t(context: context),
|
||||
iconOnly: iconOnly,
|
||||
menuItem: menuItem,
|
||||
onPressed: () => _onTap(context, ref),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,59 +0,0 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:auto_route/auto_route.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:immich_mobile/constants/enums.dart';
|
||||
import 'package:immich_mobile/domain/models/asset_edit.model.dart';
|
||||
import 'package:immich_mobile/extensions/translate_extensions.dart';
|
||||
import 'package:immich_mobile/presentation/pages/edit/editor.provider.dart';
|
||||
import 'package:immich_mobile/presentation/widgets/action_buttons/base_action_button.widget.dart';
|
||||
import 'package:immich_mobile/presentation/widgets/images/image_provider.dart';
|
||||
import 'package:immich_mobile/providers/asset_viewer/asset_viewer.provider.dart';
|
||||
import 'package:immich_mobile/providers/infrastructure/action.provider.dart';
|
||||
import 'package:immich_mobile/providers/infrastructure/asset.provider.dart';
|
||||
import 'package:immich_mobile/routing/router.dart';
|
||||
|
||||
class EditImageActionButton extends ConsumerWidget {
|
||||
const EditImageActionButton({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final currentAsset = ref.watch(assetViewerProvider.select((s) => s.currentAsset));
|
||||
|
||||
Future<void> editImage(List<AssetEdit> edits) async {
|
||||
if (currentAsset == null || currentAsset.remoteId == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
await ref.read(actionProvider.notifier).applyEdits(ActionSource.viewer, edits);
|
||||
}
|
||||
|
||||
Future<void> onPress() async {
|
||||
if (currentAsset == null || currentAsset.remoteId == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
final imageProvider = getFullImageProvider(currentAsset, edited: false);
|
||||
|
||||
final image = Image(image: imageProvider);
|
||||
final (edits, exifInfo) = await (
|
||||
ref.read(remoteAssetRepositoryProvider).getAssetEdits(currentAsset.remoteId!),
|
||||
ref.read(remoteAssetRepositoryProvider).getExif(currentAsset.remoteId!),
|
||||
).wait;
|
||||
|
||||
if (exifInfo == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
ref.read(editorStateProvider.notifier).init(edits, exifInfo);
|
||||
await context.pushRoute(DriftEditImageRoute(image: image, applyEdits: editImage));
|
||||
}
|
||||
|
||||
return BaseActionButton(
|
||||
iconData: Icons.tune,
|
||||
label: "edit".t(context: context),
|
||||
onPressed: onPress,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -4,11 +4,12 @@ import 'package:immich_mobile/constants/enums.dart';
|
||||
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
|
||||
import 'package:immich_mobile/domain/services/timeline.service.dart';
|
||||
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
||||
import 'package:immich_mobile/presentation/actions/action.widget.dart';
|
||||
import 'package:immich_mobile/presentation/actions/edit.action.dart';
|
||||
import 'package:immich_mobile/presentation/widgets/action_buttons/add_action_button.widget.dart';
|
||||
import 'package:immich_mobile/presentation/widgets/action_buttons/delete_action_button.widget.dart';
|
||||
import 'package:immich_mobile/presentation/widgets/action_buttons/delete_local_action_button.widget.dart';
|
||||
import 'package:immich_mobile/presentation/widgets/action_buttons/delete_permanent_action_button.widget.dart';
|
||||
import 'package:immich_mobile/presentation/widgets/action_buttons/edit_image_action_button.widget.dart';
|
||||
import 'package:immich_mobile/presentation/widgets/action_buttons/restore_action_button.widget.dart';
|
||||
import 'package:immich_mobile/presentation/widgets/action_buttons/share_action_button.widget.dart';
|
||||
import 'package:immich_mobile/presentation/widgets/action_buttons/upload_action_button.widget.dart';
|
||||
@@ -17,10 +18,9 @@ import 'package:immich_mobile/providers/asset_viewer/asset_viewer.provider.dart'
|
||||
import 'package:immich_mobile/providers/infrastructure/readonly_mode.provider.dart';
|
||||
import 'package:immich_mobile/providers/infrastructure/timeline.provider.dart';
|
||||
import 'package:immich_mobile/providers/routes.provider.dart';
|
||||
import 'package:immich_mobile/providers/server_info.provider.dart';
|
||||
import 'package:immich_mobile/providers/user.provider.dart';
|
||||
import 'package:immich_mobile/utils/semver.dart';
|
||||
import 'package:immich_mobile/widgets/asset_viewer/video_controls.dart';
|
||||
import 'package:immich_ui/immich_ui.dart';
|
||||
|
||||
class ViewerBottomBar extends ConsumerWidget {
|
||||
const ViewerBottomBar({super.key});
|
||||
@@ -37,10 +37,10 @@ class ViewerBottomBar extends ConsumerWidget {
|
||||
final isOwner = asset is RemoteAsset && asset.ownerId == user?.id;
|
||||
final showingDetails = ref.watch(assetViewerProvider.select((s) => s.showingDetails));
|
||||
final isInLockedView = ref.watch(inLockedViewProvider);
|
||||
final serverInfo = ref.watch(serverInfoProvider);
|
||||
final isInTrash = ref.read(timelineServiceProvider).origin == TimelineOrigin.trash;
|
||||
|
||||
final originalTheme = context.themeData;
|
||||
final actionAsset = [asset];
|
||||
|
||||
final actions = <Widget>[
|
||||
if (isInTrash && isOwner && asset.hasRemote)
|
||||
@@ -51,9 +51,7 @@ class ViewerBottomBar extends ConsumerWidget {
|
||||
if (!isInLockedView) ...[
|
||||
if (!isInTrash) ...[
|
||||
if (asset.isLocalOnly) const UploadActionButton(source: ActionSource.viewer),
|
||||
// edit sync was added in 2.6.0
|
||||
if (asset.isEditable && serverInfo.serverVersion >= const SemVer(major: 2, minor: 6, patch: 0))
|
||||
const EditImageActionButton(),
|
||||
ActionColumnButtonWidget(action: EditAssetAction(assets: actionAsset)),
|
||||
if (asset.hasRemote) AddActionButton(originalTheme: originalTheme),
|
||||
],
|
||||
if (isOwner) ...[
|
||||
@@ -104,7 +102,10 @@ class ViewerBottomBar extends ConsumerWidget {
|
||||
OcrToggleButton(asset: asset),
|
||||
if (asset.isVideo) VideoControls(videoPlayerName: asset.heroTag),
|
||||
if (!isReadonlyModeEnabled)
|
||||
Row(mainAxisAlignment: MainAxisAlignment.spaceEvenly, children: actions),
|
||||
ImmichColorOverride(
|
||||
color: Colors.white,
|
||||
child: Row(mainAxisAlignment: MainAxisAlignment.spaceEvenly, children: actions),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
@@ -12,6 +12,7 @@ import 'package:immich_mobile/providers/routes.provider.dart';
|
||||
import 'package:immich_mobile/providers/server_info.provider.dart';
|
||||
import 'package:immich_mobile/providers/user.provider.dart';
|
||||
import 'package:immich_mobile/utils/action_button.utils.dart';
|
||||
import 'package:immich_ui/immich_ui.dart';
|
||||
|
||||
class ViewerKebabMenu extends ConsumerWidget {
|
||||
const ViewerKebabMenu({super.key, this.originalTheme});
|
||||
@@ -49,9 +50,9 @@ class ViewerKebabMenu extends ConsumerWidget {
|
||||
timelineOrigin: timelineOrigin,
|
||||
);
|
||||
|
||||
final menuChildren = ActionButtonBuilder.buildViewerKebabMenu(actionContext, context, ref);
|
||||
final menuChildren = ActionButtonBuilder.buildViewerKebabMenu(actionContext, context);
|
||||
|
||||
return MenuAnchor(
|
||||
return ImmichMenu(
|
||||
consumeOutsideTap: true,
|
||||
style: MenuStyle(
|
||||
backgroundColor: WidgetStatePropertyAll(context.themeData.scaffoldBackgroundColor),
|
||||
@@ -62,7 +63,7 @@ class ViewerKebabMenu extends ConsumerWidget {
|
||||
),
|
||||
padding: const WidgetStatePropertyAll(EdgeInsets.symmetric(vertical: 6)),
|
||||
),
|
||||
menuChildren: [
|
||||
children: [
|
||||
ConstrainedBox(
|
||||
constraints: const BoxConstraints(minWidth: 150),
|
||||
child: Theme(
|
||||
|
||||
@@ -2,12 +2,11 @@ import 'package:auto_route/auto_route.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:immich_mobile/constants/enums.dart';
|
||||
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
|
||||
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
||||
import 'package:immich_mobile/presentation/widgets/action_buttons/favorite_action_button.widget.dart';
|
||||
import 'package:immich_mobile/presentation/actions/action.widget.dart';
|
||||
import 'package:immich_mobile/presentation/actions/favorite.action.dart';
|
||||
import 'package:immich_mobile/presentation/widgets/action_buttons/motion_photo_action_button.widget.dart';
|
||||
import 'package:immich_mobile/presentation/widgets/action_buttons/unfavorite_action_button.widget.dart';
|
||||
import 'package:immich_mobile/presentation/widgets/asset_viewer/viewer_kebab_menu.widget.dart';
|
||||
import 'package:immich_mobile/providers/activity.provider.dart';
|
||||
import 'package:immich_mobile/providers/asset_viewer/asset_viewer.provider.dart';
|
||||
@@ -15,9 +14,9 @@ import 'package:immich_mobile/providers/infrastructure/asset_viewer/asset.provid
|
||||
import 'package:immich_mobile/providers/infrastructure/current_album.provider.dart';
|
||||
import 'package:immich_mobile/providers/infrastructure/readonly_mode.provider.dart';
|
||||
import 'package:immich_mobile/providers/routes.provider.dart';
|
||||
import 'package:immich_mobile/providers/user.provider.dart';
|
||||
import 'package:immich_mobile/routing/router.dart';
|
||||
import 'package:immich_mobile/utils/timezone.dart';
|
||||
import 'package:immich_ui/immich_ui.dart';
|
||||
|
||||
class ViewerTopAppBar extends ConsumerWidget implements PreferredSizeWidget {
|
||||
const ViewerTopAppBar({super.key});
|
||||
@@ -31,8 +30,6 @@ class ViewerTopAppBar extends ConsumerWidget implements PreferredSizeWidget {
|
||||
|
||||
final album = ref.watch(currentRemoteAlbumProvider);
|
||||
|
||||
final user = ref.watch(currentUserProvider);
|
||||
final isOwner = asset is RemoteAsset && asset.ownerId == user?.id;
|
||||
final isInLockedView = ref.watch(inLockedViewProvider);
|
||||
final isReadonlyModeEnabled = ref.watch(readonlyModeProvider);
|
||||
|
||||
@@ -46,6 +43,7 @@ class ViewerTopAppBar extends ConsumerWidget implements PreferredSizeWidget {
|
||||
double opacity = ref.watch(assetViewerProvider.select((s) => s.backgroundOpacity)) * (showingControls ? 1 : 0);
|
||||
|
||||
final originalTheme = context.themeData;
|
||||
final assetForAction = [asset];
|
||||
|
||||
final actions = <Widget>[
|
||||
if (asset.isMotionPhoto) const MotionPhotoActionButton(iconOnly: true),
|
||||
@@ -63,10 +61,7 @@ class ViewerTopAppBar extends ConsumerWidget implements PreferredSizeWidget {
|
||||
},
|
||||
),
|
||||
|
||||
if (asset.hasRemote && isOwner && !asset.isFavorite)
|
||||
const FavoriteActionButton(source: ActionSource.viewer, iconOnly: true),
|
||||
if (asset.hasRemote && isOwner && asset.isFavorite)
|
||||
const UnFavoriteActionButton(source: ActionSource.viewer, iconOnly: true),
|
||||
ActionIconButtonWidget(action: FavoriteAction(assets: assetForAction)),
|
||||
|
||||
ViewerKebabMenu(originalTheme: originalTheme),
|
||||
];
|
||||
@@ -107,7 +102,13 @@ class ViewerTopAppBar extends ConsumerWidget implements PreferredSizeWidget {
|
||||
leading: const _AppBarBackButton(),
|
||||
middle: showingDetails ? null : _AssetInfoTitle(asset: asset),
|
||||
trailing: !showingDetails && !isReadonlyModeEnabled
|
||||
? Row(mainAxisSize: MainAxisSize.min, children: isInLockedView ? lockedViewActions : actions)
|
||||
? ImmichColorOverride(
|
||||
color: Colors.white,
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: isInLockedView ? lockedViewActions : actions,
|
||||
),
|
||||
)
|
||||
: null,
|
||||
),
|
||||
),
|
||||
|
||||
@@ -3,12 +3,14 @@ import 'package:flutter/material.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:immich_mobile/constants/enums.dart';
|
||||
import 'package:immich_mobile/domain/models/album/album.model.dart';
|
||||
import 'package:immich_mobile/presentation/actions/action.widget.dart';
|
||||
import 'package:immich_mobile/presentation/actions/favorite.action.dart';
|
||||
import 'package:immich_mobile/presentation/actions/timeline.action.dart';
|
||||
import 'package:immich_mobile/presentation/widgets/action_buttons/delete_local_action_button.widget.dart';
|
||||
import 'package:immich_mobile/presentation/widgets/action_buttons/delete_permanent_action_button.widget.dart';
|
||||
import 'package:immich_mobile/presentation/widgets/action_buttons/download_action_button.widget.dart';
|
||||
import 'package:immich_mobile/presentation/widgets/action_buttons/edit_date_time_action_button.widget.dart';
|
||||
import 'package:immich_mobile/presentation/widgets/action_buttons/edit_location_action_button.widget.dart';
|
||||
import 'package:immich_mobile/presentation/widgets/action_buttons/favorite_action_button.widget.dart';
|
||||
import 'package:immich_mobile/presentation/widgets/action_buttons/move_to_lock_folder_action_button.widget.dart';
|
||||
import 'package:immich_mobile/presentation/widgets/action_buttons/share_action_button.widget.dart';
|
||||
import 'package:immich_mobile/presentation/widgets/action_buttons/share_link_action_button.widget.dart';
|
||||
@@ -74,6 +76,9 @@ class _ArchiveBottomSheetState extends ConsumerState<ArchiveBottomSheet> {
|
||||
return sheetController.animateTo(0.85, duration: const Duration(milliseconds: 200), curve: Curves.easeInOut);
|
||||
}
|
||||
|
||||
final assets = multiselect.selectedAssets.toList(growable: false);
|
||||
final actions = [FavoriteAction(assets: assets)];
|
||||
|
||||
return BaseBottomSheet(
|
||||
controller: sheetController,
|
||||
initialChildSize: 0.25,
|
||||
@@ -84,7 +89,7 @@ class _ArchiveBottomSheetState extends ConsumerState<ArchiveBottomSheet> {
|
||||
if (multiselect.hasRemote) ...[
|
||||
const ShareLinkActionButton(source: ActionSource.timeline),
|
||||
const UnArchiveActionButton(source: ActionSource.timeline),
|
||||
const FavoriteActionButton(source: ActionSource.timeline),
|
||||
...actions.map((action) => ActionColumnButtonWidget(action: TimelineAction(action: action))),
|
||||
if (multiselect.onlyRemote) const DownloadActionButton(source: ActionSource.timeline),
|
||||
isTrashEnable
|
||||
? const TrashActionButton(source: ActionSource.timeline)
|
||||
|
||||
@@ -4,6 +4,9 @@ import 'package:immich_mobile/constants/enums.dart';
|
||||
import 'package:immich_mobile/domain/models/album/album.model.dart';
|
||||
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
|
||||
import 'package:immich_mobile/extensions/translate_extensions.dart';
|
||||
import 'package:immich_mobile/presentation/actions/action.widget.dart';
|
||||
import 'package:immich_mobile/presentation/actions/favorite.action.dart';
|
||||
import 'package:immich_mobile/presentation/actions/timeline.action.dart';
|
||||
import 'package:immich_mobile/presentation/widgets/action_buttons/archive_action_button.widget.dart';
|
||||
import 'package:immich_mobile/presentation/widgets/action_buttons/delete_local_action_button.widget.dart';
|
||||
import 'package:immich_mobile/presentation/widgets/action_buttons/delete_permanent_action_button.widget.dart';
|
||||
@@ -15,7 +18,6 @@ import 'package:immich_mobile/presentation/widgets/action_buttons/share_action_b
|
||||
import 'package:immich_mobile/presentation/widgets/action_buttons/share_link_action_button.widget.dart';
|
||||
import 'package:immich_mobile/presentation/widgets/action_buttons/stack_action_button.widget.dart';
|
||||
import 'package:immich_mobile/presentation/widgets/action_buttons/trash_action_button.widget.dart';
|
||||
import 'package:immich_mobile/presentation/widgets/action_buttons/unfavorite_action_button.widget.dart';
|
||||
import 'package:immich_mobile/presentation/widgets/action_buttons/unstack_action_button.widget.dart';
|
||||
import 'package:immich_mobile/presentation/widgets/album/album_selector.widget.dart';
|
||||
import 'package:immich_mobile/presentation/widgets/bottom_sheet/base_bottom_sheet.widget.dart';
|
||||
@@ -65,6 +67,9 @@ class FavoriteBottomSheet extends ConsumerWidget {
|
||||
ref.read(multiSelectProvider.notifier).reset();
|
||||
}
|
||||
|
||||
final assets = multiselect.selectedAssets.toList(growable: false);
|
||||
final actions = [FavoriteAction(assets: assets)];
|
||||
|
||||
return BaseBottomSheet(
|
||||
initialChildSize: 0.4,
|
||||
maxChildSize: 0.7,
|
||||
@@ -73,7 +78,7 @@ class FavoriteBottomSheet extends ConsumerWidget {
|
||||
const ShareActionButton(source: ActionSource.timeline),
|
||||
if (multiselect.hasRemote) ...[
|
||||
const ShareLinkActionButton(source: ActionSource.timeline),
|
||||
const UnFavoriteActionButton(source: ActionSource.timeline),
|
||||
...actions.map((action) => ActionColumnButtonWidget(action: TimelineAction(action: action))),
|
||||
const ArchiveActionButton(source: ActionSource.timeline),
|
||||
if (multiselect.onlyRemote) const DownloadActionButton(source: ActionSource.timeline),
|
||||
isTrashEnable
|
||||
|
||||
@@ -3,8 +3,9 @@ import 'package:flutter/material.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:immich_mobile/constants/enums.dart';
|
||||
import 'package:immich_mobile/domain/models/album/album.model.dart';
|
||||
import 'package:immich_mobile/domain/models/setting.model.dart';
|
||||
import 'package:immich_mobile/presentation/widgets/action_buttons/advanced_info_action_button.widget.dart';
|
||||
import 'package:immich_mobile/presentation/actions/action.widget.dart';
|
||||
import 'package:immich_mobile/presentation/actions/asset_debug.action.dart';
|
||||
import 'package:immich_mobile/presentation/actions/timeline.action.dart';
|
||||
import 'package:immich_mobile/presentation/widgets/action_buttons/archive_action_button.widget.dart';
|
||||
import 'package:immich_mobile/presentation/widgets/action_buttons/bulk_tag_assets_action_button.widget.dart';
|
||||
import 'package:immich_mobile/presentation/widgets/action_buttons/delete_action_button.widget.dart';
|
||||
@@ -24,7 +25,6 @@ import 'package:immich_mobile/presentation/widgets/action_buttons/upload_action_
|
||||
import 'package:immich_mobile/presentation/widgets/album/album_selector.widget.dart';
|
||||
import 'package:immich_mobile/presentation/widgets/bottom_sheet/base_bottom_sheet.widget.dart';
|
||||
import 'package:immich_mobile/providers/infrastructure/action.provider.dart';
|
||||
import 'package:immich_mobile/providers/infrastructure/setting.provider.dart';
|
||||
import 'package:immich_mobile/providers/infrastructure/user_metadata.provider.dart';
|
||||
import 'package:immich_mobile/providers/server_info.provider.dart';
|
||||
import 'package:immich_mobile/providers/timeline/multiselect.provider.dart';
|
||||
@@ -56,7 +56,6 @@ class _GeneralBottomSheetState extends ConsumerState<GeneralBottomSheet> {
|
||||
Widget build(BuildContext context) {
|
||||
final multiselect = ref.watch(multiSelectProvider);
|
||||
final isTrashEnable = ref.watch(serverInfoProvider.select((state) => state.serverFeatures.trash));
|
||||
final advancedTroubleshooting = ref.watch(settingsProvider.notifier).get(Setting.advancedTroubleshooting);
|
||||
final tagsEnabled = ref.watch(
|
||||
userMetadataPreferencesProvider.select((value) => value.valueOrNull?.tagsEnabled ?? false),
|
||||
);
|
||||
@@ -84,6 +83,9 @@ class _GeneralBottomSheetState extends ConsumerState<GeneralBottomSheet> {
|
||||
return sheetController.animateTo(0.85, duration: const Duration(milliseconds: 200), curve: Curves.easeInOut);
|
||||
}
|
||||
|
||||
final assets = multiselect.selectedAssets.toList(growable: false);
|
||||
final actions = [AssetDebugAction(assets: assets)];
|
||||
|
||||
return BaseBottomSheet(
|
||||
controller: sheetController,
|
||||
initialChildSize: widget.minChildSize ?? 0.15,
|
||||
@@ -91,9 +93,7 @@ class _GeneralBottomSheetState extends ConsumerState<GeneralBottomSheet> {
|
||||
maxChildSize: 0.85,
|
||||
shouldCloseOnMinExtent: false,
|
||||
actions: [
|
||||
if (multiselect.selectedAssets.length == 1 && advancedTroubleshooting) ...[
|
||||
const AdvancedInfoActionButton(source: ActionSource.timeline),
|
||||
],
|
||||
...actions.map((action) => ActionColumnButtonWidget(action: TimelineAction(action: action))),
|
||||
const ShareActionButton(source: ActionSource.timeline),
|
||||
if (multiselect.hasRemote) ...[
|
||||
const ShareLinkActionButton(source: ActionSource.timeline),
|
||||
|
||||
@@ -3,13 +3,15 @@ import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:immich_mobile/constants/enums.dart';
|
||||
import 'package:immich_mobile/domain/models/album/album.model.dart';
|
||||
import 'package:immich_mobile/extensions/translate_extensions.dart';
|
||||
import 'package:immich_mobile/presentation/actions/action.widget.dart';
|
||||
import 'package:immich_mobile/presentation/actions/favorite.action.dart';
|
||||
import 'package:immich_mobile/presentation/actions/timeline.action.dart';
|
||||
import 'package:immich_mobile/presentation/widgets/action_buttons/archive_action_button.widget.dart';
|
||||
import 'package:immich_mobile/presentation/widgets/action_buttons/delete_local_action_button.widget.dart';
|
||||
import 'package:immich_mobile/presentation/widgets/action_buttons/delete_permanent_action_button.widget.dart';
|
||||
import 'package:immich_mobile/presentation/widgets/action_buttons/download_action_button.widget.dart';
|
||||
import 'package:immich_mobile/presentation/widgets/action_buttons/edit_date_time_action_button.widget.dart';
|
||||
import 'package:immich_mobile/presentation/widgets/action_buttons/edit_location_action_button.widget.dart';
|
||||
import 'package:immich_mobile/presentation/widgets/action_buttons/favorite_action_button.widget.dart';
|
||||
import 'package:immich_mobile/presentation/widgets/action_buttons/move_to_lock_folder_action_button.widget.dart';
|
||||
import 'package:immich_mobile/presentation/widgets/action_buttons/remove_from_album_action_button.widget.dart';
|
||||
import 'package:immich_mobile/presentation/widgets/action_buttons/set_album_cover.widget.dart';
|
||||
@@ -83,6 +85,9 @@ class _RemoteAlbumBottomSheetState extends ConsumerState<RemoteAlbumBottomSheet>
|
||||
return sheetController.animateTo(0.85, duration: const Duration(milliseconds: 200), curve: Curves.easeInOut);
|
||||
}
|
||||
|
||||
final assets = multiselect.selectedAssets.toList(growable: false);
|
||||
final actions = [FavoriteAction(assets: assets)];
|
||||
|
||||
return BaseBottomSheet(
|
||||
controller: sheetController,
|
||||
initialChildSize: 0.22,
|
||||
@@ -96,7 +101,7 @@ class _RemoteAlbumBottomSheetState extends ConsumerState<RemoteAlbumBottomSheet>
|
||||
|
||||
if (ownsAlbum) ...[
|
||||
const ArchiveActionButton(source: ActionSource.timeline),
|
||||
const FavoriteActionButton(source: ActionSource.timeline),
|
||||
...actions.map((action) => ActionColumnButtonWidget(action: TimelineAction(action: action))),
|
||||
],
|
||||
const DownloadActionButton(source: ActionSource.timeline),
|
||||
if (ownsAlbum) ...[
|
||||
|
||||
@@ -1,13 +1,11 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:auto_route/auto_route.dart';
|
||||
import 'package:background_downloader/background_downloader.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:immich_mobile/constants/enums.dart';
|
||||
import 'package:immich_mobile/domain/models/album/album.model.dart';
|
||||
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
|
||||
import 'package:immich_mobile/domain/models/asset_edit.model.dart';
|
||||
import 'package:immich_mobile/domain/services/asset.service.dart';
|
||||
import 'package:immich_mobile/domain/services/remote_album.service.dart';
|
||||
import 'package:immich_mobile/models/download/livephotos_medatada.model.dart';
|
||||
@@ -17,18 +15,13 @@ import 'package:immich_mobile/providers/infrastructure/album.provider.dart';
|
||||
import 'package:immich_mobile/providers/infrastructure/asset.provider.dart';
|
||||
import 'package:immich_mobile/providers/infrastructure/asset_viewer/asset.provider.dart' show assetExifProvider;
|
||||
import 'package:immich_mobile/providers/infrastructure/tag.provider.dart';
|
||||
import 'package:immich_mobile/providers/server_info.provider.dart';
|
||||
import 'package:immich_mobile/providers/timeline/multiselect.provider.dart';
|
||||
import 'package:immich_mobile/providers/user.provider.dart';
|
||||
import 'package:immich_mobile/providers/websocket.provider.dart';
|
||||
import 'package:immich_mobile/routing/router.dart';
|
||||
import 'package:immich_mobile/services/action.service.dart';
|
||||
import 'package:immich_mobile/services/download.service.dart';
|
||||
import 'package:immich_mobile/services/foreground_upload.service.dart';
|
||||
import 'package:immich_mobile/utils/semver.dart';
|
||||
import 'package:immich_mobile/widgets/asset_grid/delete_dialog.dart';
|
||||
import 'package:logging/logging.dart';
|
||||
import 'package:openapi/api.dart';
|
||||
|
||||
final actionProvider = NotifierProvider<ActionNotifier, void>(ActionNotifier.new, dependencies: [multiSelectProvider]);
|
||||
|
||||
@@ -135,16 +128,6 @@ class ActionNotifier extends Notifier<void> {
|
||||
};
|
||||
}
|
||||
|
||||
Future<ActionResult> troubleshoot(ActionSource source, BuildContext context) async {
|
||||
final assets = _getAssets(source);
|
||||
if (assets.length > 1) {
|
||||
return ActionResult(count: assets.length, success: false, error: 'Cannot troubleshoot multiple assets');
|
||||
}
|
||||
unawaited(context.pushRoute(AssetTroubleshootRoute(asset: assets.first)));
|
||||
|
||||
return ActionResult(count: assets.length, success: true);
|
||||
}
|
||||
|
||||
Future<ActionResult> shareLink(ActionSource source, BuildContext context) async {
|
||||
final ids = _getRemoteIdsForSource(source);
|
||||
try {
|
||||
@@ -631,37 +614,6 @@ class ActionNotifier extends Notifier<void> {
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
Future<ActionResult> applyEdits(ActionSource source, List<AssetEdit> edits) async {
|
||||
final ids = _getOwnedRemoteIdsForSource(source);
|
||||
|
||||
if (ids.length != 1) {
|
||||
_logger.warning('applyEdits called with multiple assets, expected single asset');
|
||||
return ActionResult(count: ids.length, success: false, error: 'Expected single asset for applying edits');
|
||||
}
|
||||
|
||||
Future<void> editReady;
|
||||
if (ref.read(serverInfoProvider).serverVersion >= const SemVer(major: 3, minor: 0, patch: 0)) {
|
||||
editReady = ref.read(websocketProvider.notifier).waitForEvent("AssetEditReadyV2", (dynamic data) {
|
||||
final eventAsset = SyncAssetV2.fromJson(data["asset"]);
|
||||
return eventAsset?.id == ids.first;
|
||||
}, const Duration(seconds: 10));
|
||||
} else {
|
||||
editReady = ref.read(websocketProvider.notifier).waitForEvent("AssetEditReadyV1", (dynamic data) {
|
||||
final eventAsset = SyncAssetV1.fromJson(data["asset"]);
|
||||
return eventAsset?.id == ids.first;
|
||||
}, const Duration(seconds: 10));
|
||||
}
|
||||
|
||||
try {
|
||||
await _service.applyEdits(ids.first, edits);
|
||||
await editReady;
|
||||
return const ActionResult(count: 1, success: true);
|
||||
} catch (error, stack) {
|
||||
_logger.severe('Failed to apply edits to assets', error, stack);
|
||||
return ActionResult(count: ids.length, success: false, error: error.toString());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension on Iterable<RemoteAsset> {
|
||||
|
||||
@@ -5,6 +5,7 @@ import 'package:immich_mobile/infrastructure/repositories/remote_asset.repositor
|
||||
import 'package:immich_mobile/infrastructure/repositories/trashed_local_asset.repository.dart';
|
||||
import 'package:immich_mobile/providers/infrastructure/db.provider.dart';
|
||||
import 'package:immich_mobile/providers/user.provider.dart';
|
||||
import 'package:immich_mobile/repositories/asset_api.repository.dart';
|
||||
|
||||
final localAssetRepository = Provider<DriftLocalAssetRepository>(
|
||||
(ref) => DriftLocalAssetRepository(ref.watch(driftProvider)),
|
||||
@@ -20,8 +21,9 @@ final trashedLocalAssetRepository = Provider<DriftTrashedLocalAssetRepository>(
|
||||
|
||||
final assetServiceProvider = Provider(
|
||||
(ref) => AssetService(
|
||||
remoteAssetRepository: ref.watch(remoteAssetRepositoryProvider),
|
||||
localAssetRepository: ref.watch(localAssetRepository),
|
||||
remoteRepository: ref.watch(remoteAssetRepositoryProvider),
|
||||
localRepository: ref.watch(localAssetRepository),
|
||||
apiRepository: ref.watch(assetApiRepositoryProvider),
|
||||
),
|
||||
);
|
||||
|
||||
|
||||
@@ -5,7 +5,6 @@ import 'package:flutter/material.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:immich_mobile/constants/enums.dart';
|
||||
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
|
||||
import 'package:immich_mobile/domain/models/asset_edit.model.dart';
|
||||
import 'package:immich_mobile/domain/models/store.model.dart';
|
||||
import 'package:immich_mobile/domain/services/tag.service.dart';
|
||||
import 'package:immich_mobile/entities/store.entity.dart';
|
||||
@@ -305,14 +304,6 @@ class ActionService {
|
||||
return true;
|
||||
}
|
||||
|
||||
Future<void> applyEdits(String remoteId, List<AssetEdit> edits) async {
|
||||
if (edits.isEmpty) {
|
||||
await _assetApiRepository.removeEdits(remoteId);
|
||||
} else {
|
||||
await _assetApiRepository.editAsset(remoteId, edits);
|
||||
}
|
||||
}
|
||||
|
||||
Future<int> _deleteLocalAssets(List<String> localIds) async {
|
||||
final deletedIds = await _assetMediaRepository.deleteAll(localIds);
|
||||
if (deletedIds.isEmpty) {
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
import 'package:auto_route/auto_route.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:immich_mobile/constants/enums.dart';
|
||||
import 'package:immich_mobile/domain/models/album/album.model.dart';
|
||||
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
|
||||
import 'package:immich_mobile/domain/models/events.model.dart';
|
||||
import 'package:immich_mobile/domain/services/timeline.service.dart';
|
||||
import 'package:immich_mobile/domain/utils/event_stream.dart';
|
||||
import 'package:immich_mobile/presentation/widgets/action_buttons/advanced_info_action_button.widget.dart';
|
||||
import 'package:immich_mobile/presentation/actions/action.widget.dart';
|
||||
import 'package:immich_mobile/presentation/actions/asset_debug.action.dart';
|
||||
import 'package:immich_mobile/presentation/widgets/action_buttons/archive_action_button.widget.dart';
|
||||
import 'package:immich_mobile/presentation/widgets/action_buttons/base_action_button.widget.dart';
|
||||
import 'package:immich_mobile/presentation/widgets/action_buttons/cast_action_button.widget.dart';
|
||||
@@ -185,18 +185,14 @@ enum ActionButtonType {
|
||||
};
|
||||
}
|
||||
|
||||
ConsumerWidget buildButton(
|
||||
Widget buildButton(
|
||||
ActionButtonContext context, [
|
||||
BuildContext? buildContext,
|
||||
bool iconOnly = false,
|
||||
bool menuItem = false,
|
||||
]) {
|
||||
return switch (this) {
|
||||
ActionButtonType.advancedInfo => AdvancedInfoActionButton(
|
||||
source: context.source,
|
||||
iconOnly: iconOnly,
|
||||
menuItem: menuItem,
|
||||
),
|
||||
ActionButtonType.advancedInfo => ActionMenuItemWidget(action: AssetDebugAction(assets: [context.asset])),
|
||||
ActionButtonType.share => ShareActionButton(source: context.source, iconOnly: iconOnly, menuItem: menuItem),
|
||||
ActionButtonType.shareLink => ShareLinkActionButton(
|
||||
source: context.source,
|
||||
@@ -334,7 +330,7 @@ class ActionButtonBuilder {
|
||||
return _actionTypes.where((type) => type.shouldShow(context)).map((type) => type.buildButton(context)).toList();
|
||||
}
|
||||
|
||||
static List<Widget> buildViewerKebabMenu(ActionButtonContext context, BuildContext buildContext, WidgetRef ref) {
|
||||
static List<Widget> buildViewerKebabMenu(ActionButtonContext context, BuildContext buildContext) {
|
||||
final visibleButtons = defaultViewerKebabMenuOrder
|
||||
.where((type) => !defaultViewerBottomBarButtons.contains(type) && type.shouldShow(context))
|
||||
.toList();
|
||||
@@ -350,7 +346,7 @@ class ActionButtonBuilder {
|
||||
if (lastGroup != null && type.kebabMenuGroup != lastGroup) {
|
||||
result.add(const Divider(height: 1));
|
||||
}
|
||||
result.add(type.buildButton(context, buildContext, false, true).build(buildContext, ref));
|
||||
result.add(type.buildButton(context, buildContext, false, true));
|
||||
lastGroup = type.kebabMenuGroup;
|
||||
}
|
||||
|
||||
|
||||
Generated
+1
-6
@@ -92,12 +92,10 @@ Class | Method | HTTP request | Description
|
||||
*AlbumsApi* | [**getAlbumMapMarkers**](doc//AlbumsApi.md#getalbummapmarkers) | **GET** /albums/{id}/map-markers | Retrieve album map markers
|
||||
*AlbumsApi* | [**getAlbumStatistics**](doc//AlbumsApi.md#getalbumstatistics) | **GET** /albums/statistics | Retrieve album statistics
|
||||
*AlbumsApi* | [**getAllAlbums**](doc//AlbumsApi.md#getallalbums) | **GET** /albums | List all albums
|
||||
*AlbumsApi* | [**getOwnAlbumUser**](doc//AlbumsApi.md#getownalbumuser) | **GET** /albums/{id}/user/self | Get own sharing permissions
|
||||
*AlbumsApi* | [**removeAssetFromAlbum**](doc//AlbumsApi.md#removeassetfromalbum) | **DELETE** /albums/{id}/assets | Remove assets from an album
|
||||
*AlbumsApi* | [**removeUserFromAlbum**](doc//AlbumsApi.md#removeuserfromalbum) | **DELETE** /albums/{id}/user/{userId} | Remove user from album
|
||||
*AlbumsApi* | [**updateAlbumInfo**](doc//AlbumsApi.md#updatealbuminfo) | **PATCH** /albums/{id} | Update an album
|
||||
*AlbumsApi* | [**updateAlbumUser**](doc//AlbumsApi.md#updatealbumuser) | **PUT** /albums/{id}/user/{userId} | Update user role
|
||||
*AlbumsApi* | [**updateOwnAlbumUser**](doc//AlbumsApi.md#updateownalbumuser) | **PUT** /albums/{id}/user/self | Update own sharing permissions
|
||||
*AssetsApi* | [**checkBulkUpload**](doc//AssetsApi.md#checkbulkupload) | **POST** /assets/bulk-upload-check | Check bulk upload
|
||||
*AssetsApi* | [**copyAsset**](doc//AssetsApi.md#copyasset) | **PUT** /assets/copy | Copy asset
|
||||
*AssetsApi* | [**deleteAssetMetadata**](doc//AssetsApi.md#deleteassetmetadata) | **DELETE** /assets/{id}/metadata/{key} | Delete asset metadata by key
|
||||
@@ -487,7 +485,7 @@ Class | Method | HTTP request | Description
|
||||
- [MemoryStatisticsResponseDto](doc//MemoryStatisticsResponseDto.md)
|
||||
- [MemoryType](doc//MemoryType.md)
|
||||
- [MemoryUpdateDto](doc//MemoryUpdateDto.md)
|
||||
- [MergeFaceClusterDto](doc//MergeFaceClusterDto.md)
|
||||
- [MergePersonDto](doc//MergePersonDto.md)
|
||||
- [MetadataSearchDto](doc//MetadataSearchDto.md)
|
||||
- [MirrorAxis](doc//MirrorAxis.md)
|
||||
- [MirrorParameters](doc//MirrorParameters.md)
|
||||
@@ -584,8 +582,6 @@ Class | Method | HTTP request | Description
|
||||
- [SharedLinkType](doc//SharedLinkType.md)
|
||||
- [SharedLinksResponse](doc//SharedLinksResponse.md)
|
||||
- [SharedLinksUpdate](doc//SharedLinksUpdate.md)
|
||||
- [SharingOptionsResponseDto](doc//SharingOptionsResponseDto.md)
|
||||
- [SharingPermission](doc//SharingPermission.md)
|
||||
- [SignUpDto](doc//SignUpDto.md)
|
||||
- [SmartSearchDto](doc//SmartSearchDto.md)
|
||||
- [SourceType](doc//SourceType.md)
|
||||
@@ -691,7 +687,6 @@ Class | Method | HTTP request | Description
|
||||
- [UpdateAlbumUserDto](doc//UpdateAlbumUserDto.md)
|
||||
- [UpdateAssetDto](doc//UpdateAssetDto.md)
|
||||
- [UpdateLibraryDto](doc//UpdateLibraryDto.md)
|
||||
- [UpdateSharingOptionsDto](doc//UpdateSharingOptionsDto.md)
|
||||
- [UsageByUserDto](doc//UsageByUserDto.md)
|
||||
- [UserAdminCreateDto](doc//UserAdminCreateDto.md)
|
||||
- [UserAdminDeleteDto](doc//UserAdminDeleteDto.md)
|
||||
|
||||
Generated
+1
-4
@@ -206,7 +206,7 @@ part 'model/memory_search_order.dart';
|
||||
part 'model/memory_statistics_response_dto.dart';
|
||||
part 'model/memory_type.dart';
|
||||
part 'model/memory_update_dto.dart';
|
||||
part 'model/merge_face_cluster_dto.dart';
|
||||
part 'model/merge_person_dto.dart';
|
||||
part 'model/metadata_search_dto.dart';
|
||||
part 'model/mirror_axis.dart';
|
||||
part 'model/mirror_parameters.dart';
|
||||
@@ -303,8 +303,6 @@ part 'model/shared_link_response_dto.dart';
|
||||
part 'model/shared_link_type.dart';
|
||||
part 'model/shared_links_response.dart';
|
||||
part 'model/shared_links_update.dart';
|
||||
part 'model/sharing_options_response_dto.dart';
|
||||
part 'model/sharing_permission.dart';
|
||||
part 'model/sign_up_dto.dart';
|
||||
part 'model/smart_search_dto.dart';
|
||||
part 'model/source_type.dart';
|
||||
@@ -410,7 +408,6 @@ part 'model/update_album_dto.dart';
|
||||
part 'model/update_album_user_dto.dart';
|
||||
part 'model/update_asset_dto.dart';
|
||||
part 'model/update_library_dto.dart';
|
||||
part 'model/update_sharing_options_dto.dart';
|
||||
part 'model/usage_by_user_dto.dart';
|
||||
part 'model/user_admin_create_dto.dart';
|
||||
part 'model/user_admin_delete_dto.dart';
|
||||
|
||||
Generated
-112
@@ -607,64 +607,6 @@ class AlbumsApi {
|
||||
return null;
|
||||
}
|
||||
|
||||
/// Get own sharing permissions
|
||||
///
|
||||
/// Get the own sharing permissions in a specific album.
|
||||
///
|
||||
/// Note: This method returns the HTTP [Response].
|
||||
///
|
||||
/// Parameters:
|
||||
///
|
||||
/// * [String] id (required):
|
||||
Future<Response> getOwnAlbumUserWithHttpInfo(String id, { Future<void>? abortTrigger, }) async {
|
||||
// ignore: prefer_const_declarations
|
||||
final apiPath = r'/albums/{id}/user/self'
|
||||
.replaceAll('{id}', id);
|
||||
|
||||
// ignore: prefer_final_locals
|
||||
Object? postBody;
|
||||
|
||||
final queryParams = <QueryParam>[];
|
||||
final headerParams = <String, String>{};
|
||||
final formParams = <String, String>{};
|
||||
|
||||
const contentTypes = <String>[];
|
||||
|
||||
|
||||
return apiClient.invokeAPI(
|
||||
apiPath,
|
||||
'GET',
|
||||
queryParams,
|
||||
postBody,
|
||||
headerParams,
|
||||
formParams,
|
||||
contentTypes.isEmpty ? null : contentTypes.first,
|
||||
abortTrigger: abortTrigger,
|
||||
);
|
||||
}
|
||||
|
||||
/// Get own sharing permissions
|
||||
///
|
||||
/// Get the own sharing permissions in a specific album.
|
||||
///
|
||||
/// Parameters:
|
||||
///
|
||||
/// * [String] id (required):
|
||||
Future<SharingOptionsResponseDto?> getOwnAlbumUser(String id, { Future<void>? abortTrigger, }) async {
|
||||
final response = await getOwnAlbumUserWithHttpInfo(id, abortTrigger: abortTrigger,);
|
||||
if (response.statusCode >= HttpStatus.badRequest) {
|
||||
throw ApiException(response.statusCode, await _decodeBodyBytes(response));
|
||||
}
|
||||
// When a remote server returns no body with a status of 204, we shall not decode it.
|
||||
// At the time of writing this, `dart:convert` will throw an "Unexpected end of input"
|
||||
// FormatException when trying to decode an empty string.
|
||||
if (response.body.isNotEmpty && response.statusCode != HttpStatus.noContent) {
|
||||
return await apiClient.deserializeAsync(await _decodeBodyBytes(response), 'SharingOptionsResponseDto',) as SharingOptionsResponseDto;
|
||||
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/// Remove assets from an album
|
||||
///
|
||||
/// Remove multiple assets from a specific album by its ID.
|
||||
@@ -905,58 +847,4 @@ class AlbumsApi {
|
||||
throw ApiException(response.statusCode, await _decodeBodyBytes(response));
|
||||
}
|
||||
}
|
||||
|
||||
/// Update own sharing permissions
|
||||
///
|
||||
/// Change the own sharing permissions in a specific album.
|
||||
///
|
||||
/// Note: This method returns the HTTP [Response].
|
||||
///
|
||||
/// Parameters:
|
||||
///
|
||||
/// * [String] id (required):
|
||||
///
|
||||
/// * [UpdateSharingOptionsDto] updateSharingOptionsDto (required):
|
||||
Future<Response> updateOwnAlbumUserWithHttpInfo(String id, UpdateSharingOptionsDto updateSharingOptionsDto, { Future<void>? abortTrigger, }) async {
|
||||
// ignore: prefer_const_declarations
|
||||
final apiPath = r'/albums/{id}/user/self'
|
||||
.replaceAll('{id}', id);
|
||||
|
||||
// ignore: prefer_final_locals
|
||||
Object? postBody = updateSharingOptionsDto;
|
||||
|
||||
final queryParams = <QueryParam>[];
|
||||
final headerParams = <String, String>{};
|
||||
final formParams = <String, String>{};
|
||||
|
||||
const contentTypes = <String>['application/json'];
|
||||
|
||||
|
||||
return apiClient.invokeAPI(
|
||||
apiPath,
|
||||
'PUT',
|
||||
queryParams,
|
||||
postBody,
|
||||
headerParams,
|
||||
formParams,
|
||||
contentTypes.isEmpty ? null : contentTypes.first,
|
||||
abortTrigger: abortTrigger,
|
||||
);
|
||||
}
|
||||
|
||||
/// Update own sharing permissions
|
||||
///
|
||||
/// Change the own sharing permissions in a specific album.
|
||||
///
|
||||
/// Parameters:
|
||||
///
|
||||
/// * [String] id (required):
|
||||
///
|
||||
/// * [UpdateSharingOptionsDto] updateSharingOptionsDto (required):
|
||||
Future<void> updateOwnAlbumUser(String id, UpdateSharingOptionsDto updateSharingOptionsDto, { Future<void>? abortTrigger, }) async {
|
||||
final response = await updateOwnAlbumUserWithHttpInfo(id, updateSharingOptionsDto, abortTrigger: abortTrigger,);
|
||||
if (response.statusCode >= HttpStatus.badRequest) {
|
||||
throw ApiException(response.statusCode, await _decodeBodyBytes(response));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Generated
+6
-6
@@ -455,14 +455,14 @@ class PeopleApi {
|
||||
///
|
||||
/// * [String] id (required):
|
||||
///
|
||||
/// * [MergeFaceClusterDto] mergeFaceClusterDto (required):
|
||||
Future<Response> mergePersonWithHttpInfo(String id, MergeFaceClusterDto mergeFaceClusterDto, { Future<void>? abortTrigger, }) async {
|
||||
/// * [MergePersonDto] mergePersonDto (required):
|
||||
Future<Response> mergePersonWithHttpInfo(String id, MergePersonDto mergePersonDto, { Future<void>? abortTrigger, }) async {
|
||||
// ignore: prefer_const_declarations
|
||||
final apiPath = r'/people/{id}/merge'
|
||||
.replaceAll('{id}', id);
|
||||
|
||||
// ignore: prefer_final_locals
|
||||
Object? postBody = mergeFaceClusterDto;
|
||||
Object? postBody = mergePersonDto;
|
||||
|
||||
final queryParams = <QueryParam>[];
|
||||
final headerParams = <String, String>{};
|
||||
@@ -491,9 +491,9 @@ class PeopleApi {
|
||||
///
|
||||
/// * [String] id (required):
|
||||
///
|
||||
/// * [MergeFaceClusterDto] mergeFaceClusterDto (required):
|
||||
Future<List<BulkIdResponseDto>?> mergePerson(String id, MergeFaceClusterDto mergeFaceClusterDto, { Future<void>? abortTrigger, }) async {
|
||||
final response = await mergePersonWithHttpInfo(id, mergeFaceClusterDto, abortTrigger: abortTrigger,);
|
||||
/// * [MergePersonDto] mergePersonDto (required):
|
||||
Future<List<BulkIdResponseDto>?> mergePerson(String id, MergePersonDto mergePersonDto, { Future<void>? abortTrigger, }) async {
|
||||
final response = await mergePersonWithHttpInfo(id, mergePersonDto, abortTrigger: abortTrigger,);
|
||||
if (response.statusCode >= HttpStatus.badRequest) {
|
||||
throw ApiException(response.statusCode, await _decodeBodyBytes(response));
|
||||
}
|
||||
|
||||
Generated
+2
-8
@@ -457,8 +457,8 @@ class ApiClient {
|
||||
return MemoryTypeTypeTransformer().decode(value);
|
||||
case 'MemoryUpdateDto':
|
||||
return MemoryUpdateDto.fromJson(value);
|
||||
case 'MergeFaceClusterDto':
|
||||
return MergeFaceClusterDto.fromJson(value);
|
||||
case 'MergePersonDto':
|
||||
return MergePersonDto.fromJson(value);
|
||||
case 'MetadataSearchDto':
|
||||
return MetadataSearchDto.fromJson(value);
|
||||
case 'MirrorAxis':
|
||||
@@ -651,10 +651,6 @@ class ApiClient {
|
||||
return SharedLinksResponse.fromJson(value);
|
||||
case 'SharedLinksUpdate':
|
||||
return SharedLinksUpdate.fromJson(value);
|
||||
case 'SharingOptionsResponseDto':
|
||||
return SharingOptionsResponseDto.fromJson(value);
|
||||
case 'SharingPermission':
|
||||
return SharingPermissionTypeTransformer().decode(value);
|
||||
case 'SignUpDto':
|
||||
return SignUpDto.fromJson(value);
|
||||
case 'SmartSearchDto':
|
||||
@@ -865,8 +861,6 @@ class ApiClient {
|
||||
return UpdateAssetDto.fromJson(value);
|
||||
case 'UpdateLibraryDto':
|
||||
return UpdateLibraryDto.fromJson(value);
|
||||
case 'UpdateSharingOptionsDto':
|
||||
return UpdateSharingOptionsDto.fromJson(value);
|
||||
case 'UsageByUserDto':
|
||||
return UsageByUserDto.fromJson(value);
|
||||
case 'UserAdminCreateDto':
|
||||
|
||||
Generated
-3
@@ -175,9 +175,6 @@ String parameterToString(dynamic value) {
|
||||
if (value is SharedLinkType) {
|
||||
return SharedLinkTypeTypeTransformer().encode(value).toString();
|
||||
}
|
||||
if (value is SharingPermission) {
|
||||
return SharingPermissionTypeTransformer().encode(value).toString();
|
||||
}
|
||||
if (value is SourceType) {
|
||||
return SourceTypeTypeTransformer().encode(value).toString();
|
||||
}
|
||||
|
||||
+1
-9
@@ -37,7 +37,6 @@ class AssetResponseDto {
|
||||
this.owner = const Optional.absent(),
|
||||
required this.ownerId,
|
||||
this.people = const Optional.present(const []),
|
||||
this.permissions = const [],
|
||||
this.resized = const Optional.absent(),
|
||||
this.stack = const Optional.absent(),
|
||||
this.tags = const Optional.present(const []),
|
||||
@@ -141,8 +140,6 @@ class AssetResponseDto {
|
||||
|
||||
Optional<List<PersonResponseDto>?> people;
|
||||
|
||||
List<SharingPermission> permissions;
|
||||
|
||||
/// Is resized
|
||||
///
|
||||
/// Please note: This property should have been non-nullable! Since the specification file
|
||||
@@ -198,7 +195,6 @@ class AssetResponseDto {
|
||||
other.owner == owner &&
|
||||
other.ownerId == ownerId &&
|
||||
_deepEquality.equals(other.people, people) &&
|
||||
_deepEquality.equals(other.permissions, permissions) &&
|
||||
other.resized == resized &&
|
||||
other.stack == stack &&
|
||||
_deepEquality.equals(other.tags, tags) &&
|
||||
@@ -235,7 +231,6 @@ class AssetResponseDto {
|
||||
(owner == null ? 0 : owner!.hashCode) +
|
||||
(ownerId.hashCode) +
|
||||
(people.hashCode) +
|
||||
(permissions.hashCode) +
|
||||
(resized == null ? 0 : resized!.hashCode) +
|
||||
(stack == null ? 0 : stack!.hashCode) +
|
||||
(tags.hashCode) +
|
||||
@@ -246,7 +241,7 @@ class AssetResponseDto {
|
||||
(width == null ? 0 : width!.hashCode);
|
||||
|
||||
@override
|
||||
String toString() => 'AssetResponseDto[checksum=$checksum, createdAt=$createdAt, duplicateId=$duplicateId, duration=$duration, exifInfo=$exifInfo, fileCreatedAt=$fileCreatedAt, fileModifiedAt=$fileModifiedAt, hasMetadata=$hasMetadata, height=$height, id=$id, isArchived=$isArchived, isEdited=$isEdited, isFavorite=$isFavorite, isOffline=$isOffline, isTrashed=$isTrashed, libraryId=$libraryId, livePhotoVideoId=$livePhotoVideoId, localDateTime=$localDateTime, originalFileName=$originalFileName, originalMimeType=$originalMimeType, originalPath=$originalPath, owner=$owner, ownerId=$ownerId, people=$people, permissions=$permissions, resized=$resized, stack=$stack, tags=$tags, thumbhash=$thumbhash, type=$type, updatedAt=$updatedAt, visibility=$visibility, width=$width]';
|
||||
String toString() => 'AssetResponseDto[checksum=$checksum, createdAt=$createdAt, duplicateId=$duplicateId, duration=$duration, exifInfo=$exifInfo, fileCreatedAt=$fileCreatedAt, fileModifiedAt=$fileModifiedAt, hasMetadata=$hasMetadata, height=$height, id=$id, isArchived=$isArchived, isEdited=$isEdited, isFavorite=$isFavorite, isOffline=$isOffline, isTrashed=$isTrashed, libraryId=$libraryId, livePhotoVideoId=$livePhotoVideoId, localDateTime=$localDateTime, originalFileName=$originalFileName, originalMimeType=$originalMimeType, originalPath=$originalPath, owner=$owner, ownerId=$ownerId, people=$people, resized=$resized, stack=$stack, tags=$tags, thumbhash=$thumbhash, type=$type, updatedAt=$updatedAt, visibility=$visibility, width=$width]';
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
final json = <String, dynamic>{};
|
||||
@@ -303,7 +298,6 @@ class AssetResponseDto {
|
||||
final value = this.people.value;
|
||||
json[r'people'] = value;
|
||||
}
|
||||
json[r'permissions'] = this.permissions;
|
||||
if (this.resized.isPresent) {
|
||||
final value = this.resized.value;
|
||||
json[r'resized'] = value;
|
||||
@@ -365,7 +359,6 @@ class AssetResponseDto {
|
||||
owner: json.containsKey(r'owner') ? Optional.present(UserResponseDto.fromJson(json[r'owner'])) : const Optional.absent(),
|
||||
ownerId: mapValueOfType<String>(json, r'ownerId')!,
|
||||
people: json.containsKey(r'people') ? Optional.present(PersonResponseDto.listFromJson(json[r'people'])) : const Optional.absent(),
|
||||
permissions: SharingPermission.listFromJson(json[r'permissions']),
|
||||
resized: json.containsKey(r'resized') ? Optional.present(mapValueOfType<bool>(json, r'resized')) : const Optional.absent(),
|
||||
stack: json.containsKey(r'stack') ? Optional.present(AssetStackResponseDto.fromJson(json[r'stack'])) : const Optional.absent(),
|
||||
tags: json.containsKey(r'tags') ? Optional.present(TagResponseDto.listFromJson(json[r'tags'])) : const Optional.absent(),
|
||||
@@ -438,7 +431,6 @@ class AssetResponseDto {
|
||||
'originalFileName',
|
||||
'originalPath',
|
||||
'ownerId',
|
||||
'permissions',
|
||||
'thumbhash',
|
||||
'type',
|
||||
'updatedAt',
|
||||
|
||||
Generated
-3
@@ -42,7 +42,6 @@ class JobName {
|
||||
static const databaseBackup = JobName._(r'DatabaseBackup');
|
||||
static const facialRecognitionQueueAll = JobName._(r'FacialRecognitionQueueAll');
|
||||
static const facialRecognition = JobName._(r'FacialRecognition');
|
||||
static const facialRecognitionMerge = JobName._(r'FacialRecognitionMerge');
|
||||
static const fileDelete = JobName._(r'FileDelete');
|
||||
static const fileMigrationQueueAll = JobName._(r'FileMigrationQueueAll');
|
||||
static const libraryDeleteCheck = JobName._(r'LibraryDeleteCheck');
|
||||
@@ -112,7 +111,6 @@ class JobName {
|
||||
databaseBackup,
|
||||
facialRecognitionQueueAll,
|
||||
facialRecognition,
|
||||
facialRecognitionMerge,
|
||||
fileDelete,
|
||||
fileMigrationQueueAll,
|
||||
libraryDeleteCheck,
|
||||
@@ -217,7 +215,6 @@ class JobNameTypeTransformer {
|
||||
case r'DatabaseBackup': return JobName.databaseBackup;
|
||||
case r'FacialRecognitionQueueAll': return JobName.facialRecognitionQueueAll;
|
||||
case r'FacialRecognition': return JobName.facialRecognition;
|
||||
case r'FacialRecognitionMerge': return JobName.facialRecognitionMerge;
|
||||
case r'FileDelete': return JobName.fileDelete;
|
||||
case r'FileMigrationQueueAll': return JobName.fileMigrationQueueAll;
|
||||
case r'LibraryDeleteCheck': return JobName.libraryDeleteCheck;
|
||||
|
||||
-3
@@ -38,7 +38,6 @@ class ManualJobName {
|
||||
static const integrityMissingFilesDeleteAll = ManualJobName._(r'integrity-missing-files-delete-all');
|
||||
static const integrityUntrackedFilesDeleteAll = ManualJobName._(r'integrity-untracked-files-delete-all');
|
||||
static const integrityChecksumMismatchDeleteAll = ManualJobName._(r'integrity-checksum-mismatch-delete-all');
|
||||
static const personGroupMerge = ManualJobName._(r'person-group-merge');
|
||||
|
||||
/// List of all possible values in this [enum][ManualJobName].
|
||||
static const values = <ManualJobName>[
|
||||
@@ -57,7 +56,6 @@ class ManualJobName {
|
||||
integrityMissingFilesDeleteAll,
|
||||
integrityUntrackedFilesDeleteAll,
|
||||
integrityChecksumMismatchDeleteAll,
|
||||
personGroupMerge,
|
||||
];
|
||||
|
||||
static ManualJobName? fromJson(dynamic value) => ManualJobNameTypeTransformer().decode(value);
|
||||
@@ -111,7 +109,6 @@ class ManualJobNameTypeTransformer {
|
||||
case r'integrity-missing-files-delete-all': return ManualJobName.integrityMissingFilesDeleteAll;
|
||||
case r'integrity-untracked-files-delete-all': return ManualJobName.integrityUntrackedFilesDeleteAll;
|
||||
case r'integrity-checksum-mismatch-delete-all': return ManualJobName.integrityChecksumMismatchDeleteAll;
|
||||
case r'person-group-merge': return ManualJobName.personGroupMerge;
|
||||
default:
|
||||
if (!allowNull) {
|
||||
throw ArgumentError('Unknown enum value to decode: $data');
|
||||
|
||||
+20
-20
@@ -10,17 +10,17 @@
|
||||
|
||||
part of openapi.api;
|
||||
|
||||
class MergeFaceClusterDto {
|
||||
/// Returns a new [MergeFaceClusterDto] instance.
|
||||
MergeFaceClusterDto({
|
||||
class MergePersonDto {
|
||||
/// Returns a new [MergePersonDto] instance.
|
||||
MergePersonDto({
|
||||
this.ids = const [],
|
||||
});
|
||||
|
||||
/// Face cluster IDs to merge
|
||||
/// Person IDs to merge
|
||||
List<String> ids;
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) => identical(this, other) || other is MergeFaceClusterDto &&
|
||||
bool operator ==(Object other) => identical(this, other) || other is MergePersonDto &&
|
||||
_deepEquality.equals(other.ids, ids);
|
||||
|
||||
@override
|
||||
@@ -29,7 +29,7 @@ class MergeFaceClusterDto {
|
||||
(ids.hashCode);
|
||||
|
||||
@override
|
||||
String toString() => 'MergeFaceClusterDto[ids=$ids]';
|
||||
String toString() => 'MergePersonDto[ids=$ids]';
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
final json = <String, dynamic>{};
|
||||
@@ -37,15 +37,15 @@ class MergeFaceClusterDto {
|
||||
return json;
|
||||
}
|
||||
|
||||
/// Returns a new [MergeFaceClusterDto] instance and imports its values from
|
||||
/// Returns a new [MergePersonDto] instance and imports its values from
|
||||
/// [value] if it's a [Map], null otherwise.
|
||||
// ignore: prefer_constructors_over_static_methods
|
||||
static MergeFaceClusterDto? fromJson(dynamic value) {
|
||||
upgradeDto(value, "MergeFaceClusterDto");
|
||||
static MergePersonDto? fromJson(dynamic value) {
|
||||
upgradeDto(value, "MergePersonDto");
|
||||
if (value is Map) {
|
||||
final json = value.cast<String, dynamic>();
|
||||
|
||||
return MergeFaceClusterDto(
|
||||
return MergePersonDto(
|
||||
ids: json[r'ids'] is Iterable
|
||||
? (json[r'ids'] as Iterable).cast<String>().toList(growable: false)
|
||||
: const [],
|
||||
@@ -54,11 +54,11 @@ class MergeFaceClusterDto {
|
||||
return null;
|
||||
}
|
||||
|
||||
static List<MergeFaceClusterDto> listFromJson(dynamic json, {bool growable = false,}) {
|
||||
final result = <MergeFaceClusterDto>[];
|
||||
static List<MergePersonDto> listFromJson(dynamic json, {bool growable = false,}) {
|
||||
final result = <MergePersonDto>[];
|
||||
if (json is List && json.isNotEmpty) {
|
||||
for (final row in json) {
|
||||
final value = MergeFaceClusterDto.fromJson(row);
|
||||
final value = MergePersonDto.fromJson(row);
|
||||
if (value != null) {
|
||||
result.add(value);
|
||||
}
|
||||
@@ -67,12 +67,12 @@ class MergeFaceClusterDto {
|
||||
return result.toList(growable: growable);
|
||||
}
|
||||
|
||||
static Map<String, MergeFaceClusterDto> mapFromJson(dynamic json) {
|
||||
final map = <String, MergeFaceClusterDto>{};
|
||||
static Map<String, MergePersonDto> mapFromJson(dynamic json) {
|
||||
final map = <String, MergePersonDto>{};
|
||||
if (json is Map && json.isNotEmpty) {
|
||||
json = json.cast<String, dynamic>(); // ignore: parameter_assignments
|
||||
for (final entry in json.entries) {
|
||||
final value = MergeFaceClusterDto.fromJson(entry.value);
|
||||
final value = MergePersonDto.fromJson(entry.value);
|
||||
if (value != null) {
|
||||
map[entry.key] = value;
|
||||
}
|
||||
@@ -81,14 +81,14 @@ class MergeFaceClusterDto {
|
||||
return map;
|
||||
}
|
||||
|
||||
// maps a json object with a list of MergeFaceClusterDto-objects as value to a dart map
|
||||
static Map<String, List<MergeFaceClusterDto>> mapListFromJson(dynamic json, {bool growable = false,}) {
|
||||
final map = <String, List<MergeFaceClusterDto>>{};
|
||||
// maps a json object with a list of MergePersonDto-objects as value to a dart map
|
||||
static Map<String, List<MergePersonDto>> mapListFromJson(dynamic json, {bool growable = false,}) {
|
||||
final map = <String, List<MergePersonDto>>{};
|
||||
if (json is Map && json.isNotEmpty) {
|
||||
// ignore: parameter_assignments
|
||||
json = json.cast<String, dynamic>();
|
||||
for (final entry in json.entries) {
|
||||
map[entry.key] = MergeFaceClusterDto.listFromJson(entry.value, growable: growable,);
|
||||
map[entry.key] = MergePersonDto.listFromJson(entry.value, growable: growable,);
|
||||
}
|
||||
}
|
||||
return map;
|
||||
+1
-14
@@ -15,7 +15,6 @@ class PersonResponseDto {
|
||||
PersonResponseDto({
|
||||
required this.birthDate,
|
||||
this.color = const Optional.absent(),
|
||||
required this.faceClusterId,
|
||||
required this.id,
|
||||
this.isFavorite = const Optional.absent(),
|
||||
required this.isHidden,
|
||||
@@ -36,9 +35,6 @@ class PersonResponseDto {
|
||||
///
|
||||
Optional<String?> color;
|
||||
|
||||
/// Face cluster ID
|
||||
String? faceClusterId;
|
||||
|
||||
/// Person ID
|
||||
String id;
|
||||
|
||||
@@ -73,7 +69,6 @@ class PersonResponseDto {
|
||||
bool operator ==(Object other) => identical(this, other) || other is PersonResponseDto &&
|
||||
other.birthDate == birthDate &&
|
||||
other.color == color &&
|
||||
other.faceClusterId == faceClusterId &&
|
||||
other.id == id &&
|
||||
other.isFavorite == isFavorite &&
|
||||
other.isHidden == isHidden &&
|
||||
@@ -86,7 +81,6 @@ class PersonResponseDto {
|
||||
// ignore: unnecessary_parenthesis
|
||||
(birthDate == null ? 0 : birthDate!.hashCode) +
|
||||
(color == null ? 0 : color!.hashCode) +
|
||||
(faceClusterId == null ? 0 : faceClusterId!.hashCode) +
|
||||
(id.hashCode) +
|
||||
(isFavorite == null ? 0 : isFavorite!.hashCode) +
|
||||
(isHidden.hashCode) +
|
||||
@@ -95,7 +89,7 @@ class PersonResponseDto {
|
||||
(updatedAt == null ? 0 : updatedAt!.hashCode);
|
||||
|
||||
@override
|
||||
String toString() => 'PersonResponseDto[birthDate=$birthDate, color=$color, faceClusterId=$faceClusterId, id=$id, isFavorite=$isFavorite, isHidden=$isHidden, name=$name, thumbnailPath=$thumbnailPath, updatedAt=$updatedAt]';
|
||||
String toString() => 'PersonResponseDto[birthDate=$birthDate, color=$color, id=$id, isFavorite=$isFavorite, isHidden=$isHidden, name=$name, thumbnailPath=$thumbnailPath, updatedAt=$updatedAt]';
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
final json = <String, dynamic>{};
|
||||
@@ -107,11 +101,6 @@ class PersonResponseDto {
|
||||
if (this.color.isPresent) {
|
||||
final value = this.color.value;
|
||||
json[r'color'] = value;
|
||||
}
|
||||
if (this.faceClusterId != null) {
|
||||
json[r'faceClusterId'] = this.faceClusterId;
|
||||
} else {
|
||||
json[r'faceClusterId'] = null;
|
||||
}
|
||||
json[r'id'] = this.id;
|
||||
if (this.isFavorite.isPresent) {
|
||||
@@ -139,7 +128,6 @@ class PersonResponseDto {
|
||||
return PersonResponseDto(
|
||||
birthDate: mapDateTime(json, r'birthDate', r''),
|
||||
color: json.containsKey(r'color') ? Optional.present(mapValueOfType<String>(json, r'color')) : const Optional.absent(),
|
||||
faceClusterId: mapValueOfType<String>(json, r'faceClusterId'),
|
||||
id: mapValueOfType<String>(json, r'id')!,
|
||||
isFavorite: json.containsKey(r'isFavorite') ? Optional.present(mapValueOfType<bool>(json, r'isFavorite')) : const Optional.absent(),
|
||||
isHidden: mapValueOfType<bool>(json, r'isHidden')!,
|
||||
@@ -194,7 +182,6 @@ class PersonResponseDto {
|
||||
/// The list of required keys that must be present in a JSON.
|
||||
static const requiredKeys = <String>{
|
||||
'birthDate',
|
||||
'faceClusterId',
|
||||
'id',
|
||||
'isHidden',
|
||||
'name',
|
||||
|
||||
@@ -1,107 +0,0 @@
|
||||
//
|
||||
// AUTO-GENERATED FILE, DO NOT MODIFY!
|
||||
//
|
||||
// @dart=2.18
|
||||
|
||||
// ignore_for_file: unused_element, unused_import
|
||||
// ignore_for_file: always_put_required_named_parameters_first
|
||||
// ignore_for_file: constant_identifier_names
|
||||
// ignore_for_file: lines_longer_than_80_chars
|
||||
|
||||
part of openapi.api;
|
||||
|
||||
class SharingOptionsResponseDto {
|
||||
/// Returns a new [SharingOptionsResponseDto] instance.
|
||||
SharingOptionsResponseDto({
|
||||
required this.inTimeline,
|
||||
this.permissions = const [],
|
||||
});
|
||||
|
||||
bool inTimeline;
|
||||
|
||||
List<SharingPermission> permissions;
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) => identical(this, other) || other is SharingOptionsResponseDto &&
|
||||
other.inTimeline == inTimeline &&
|
||||
_deepEquality.equals(other.permissions, permissions);
|
||||
|
||||
@override
|
||||
int get hashCode =>
|
||||
// ignore: unnecessary_parenthesis
|
||||
(inTimeline.hashCode) +
|
||||
(permissions.hashCode);
|
||||
|
||||
@override
|
||||
String toString() => 'SharingOptionsResponseDto[inTimeline=$inTimeline, permissions=$permissions]';
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
final json = <String, dynamic>{};
|
||||
json[r'inTimeline'] = this.inTimeline;
|
||||
json[r'permissions'] = this.permissions;
|
||||
return json;
|
||||
}
|
||||
|
||||
/// Returns a new [SharingOptionsResponseDto] instance and imports its values from
|
||||
/// [value] if it's a [Map], null otherwise.
|
||||
// ignore: prefer_constructors_over_static_methods
|
||||
static SharingOptionsResponseDto? fromJson(dynamic value) {
|
||||
upgradeDto(value, "SharingOptionsResponseDto");
|
||||
if (value is Map) {
|
||||
final json = value.cast<String, dynamic>();
|
||||
|
||||
return SharingOptionsResponseDto(
|
||||
inTimeline: mapValueOfType<bool>(json, r'inTimeline')!,
|
||||
permissions: SharingPermission.listFromJson(json[r'permissions']),
|
||||
);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
static List<SharingOptionsResponseDto> listFromJson(dynamic json, {bool growable = false,}) {
|
||||
final result = <SharingOptionsResponseDto>[];
|
||||
if (json is List && json.isNotEmpty) {
|
||||
for (final row in json) {
|
||||
final value = SharingOptionsResponseDto.fromJson(row);
|
||||
if (value != null) {
|
||||
result.add(value);
|
||||
}
|
||||
}
|
||||
}
|
||||
return result.toList(growable: growable);
|
||||
}
|
||||
|
||||
static Map<String, SharingOptionsResponseDto> mapFromJson(dynamic json) {
|
||||
final map = <String, SharingOptionsResponseDto>{};
|
||||
if (json is Map && json.isNotEmpty) {
|
||||
json = json.cast<String, dynamic>(); // ignore: parameter_assignments
|
||||
for (final entry in json.entries) {
|
||||
final value = SharingOptionsResponseDto.fromJson(entry.value);
|
||||
if (value != null) {
|
||||
map[entry.key] = value;
|
||||
}
|
||||
}
|
||||
}
|
||||
return map;
|
||||
}
|
||||
|
||||
// maps a json object with a list of SharingOptionsResponseDto-objects as value to a dart map
|
||||
static Map<String, List<SharingOptionsResponseDto>> mapListFromJson(dynamic json, {bool growable = false,}) {
|
||||
final map = <String, List<SharingOptionsResponseDto>>{};
|
||||
if (json is Map && json.isNotEmpty) {
|
||||
// ignore: parameter_assignments
|
||||
json = json.cast<String, dynamic>();
|
||||
for (final entry in json.entries) {
|
||||
map[entry.key] = SharingOptionsResponseDto.listFromJson(entry.value, growable: growable,);
|
||||
}
|
||||
}
|
||||
return map;
|
||||
}
|
||||
|
||||
/// The list of required keys that must be present in a JSON.
|
||||
static const requiredKeys = <String>{
|
||||
'inTimeline',
|
||||
'permissions',
|
||||
};
|
||||
}
|
||||
|
||||
-112
@@ -1,112 +0,0 @@
|
||||
//
|
||||
// AUTO-GENERATED FILE, DO NOT MODIFY!
|
||||
//
|
||||
// @dart=2.18
|
||||
|
||||
// ignore_for_file: unused_element, unused_import
|
||||
// ignore_for_file: always_put_required_named_parameters_first
|
||||
// ignore_for_file: constant_identifier_names
|
||||
// ignore_for_file: lines_longer_than_80_chars
|
||||
|
||||
part of openapi.api;
|
||||
|
||||
/// Sharing permission schema
|
||||
class SharingPermission {
|
||||
/// Instantiate a new enum with the provided [value].
|
||||
const SharingPermission._(this.value);
|
||||
|
||||
/// The underlying value of this enum member.
|
||||
final String value;
|
||||
|
||||
@override
|
||||
String toString() => value;
|
||||
|
||||
String toJson() => value;
|
||||
|
||||
static const all = SharingPermission._(r'all');
|
||||
static const assetPeriodRead = SharingPermission._(r'asset.read');
|
||||
static const assetPeriodUpdate = SharingPermission._(r'asset.update');
|
||||
static const assetPeriodEdit = SharingPermission._(r'asset.edit');
|
||||
static const assetPeriodDelete = SharingPermission._(r'asset.delete');
|
||||
static const assetPeriodShare = SharingPermission._(r'asset.share');
|
||||
static const exifPeriodRead = SharingPermission._(r'exif.read');
|
||||
static const personPeriodRead = SharingPermission._(r'person.read');
|
||||
static const personPeriodUpdate = SharingPermission._(r'person.update');
|
||||
static const personPeriodMerge = SharingPermission._(r'person.merge');
|
||||
static const personPeriodDelete = SharingPermission._(r'person.delete');
|
||||
|
||||
/// List of all possible values in this [enum][SharingPermission].
|
||||
static const values = <SharingPermission>[
|
||||
all,
|
||||
assetPeriodRead,
|
||||
assetPeriodUpdate,
|
||||
assetPeriodEdit,
|
||||
assetPeriodDelete,
|
||||
assetPeriodShare,
|
||||
exifPeriodRead,
|
||||
personPeriodRead,
|
||||
personPeriodUpdate,
|
||||
personPeriodMerge,
|
||||
personPeriodDelete,
|
||||
];
|
||||
|
||||
static SharingPermission? fromJson(dynamic value) => SharingPermissionTypeTransformer().decode(value);
|
||||
|
||||
static List<SharingPermission> listFromJson(dynamic json, {bool growable = false,}) {
|
||||
final result = <SharingPermission>[];
|
||||
if (json is List && json.isNotEmpty) {
|
||||
for (final row in json) {
|
||||
final value = SharingPermission.fromJson(row);
|
||||
if (value != null) {
|
||||
result.add(value);
|
||||
}
|
||||
}
|
||||
}
|
||||
return result.toList(growable: growable);
|
||||
}
|
||||
}
|
||||
|
||||
/// Transformation class that can [encode] an instance of [SharingPermission] to String,
|
||||
/// and [decode] dynamic data back to [SharingPermission].
|
||||
class SharingPermissionTypeTransformer {
|
||||
factory SharingPermissionTypeTransformer() => _instance ??= const SharingPermissionTypeTransformer._();
|
||||
|
||||
const SharingPermissionTypeTransformer._();
|
||||
|
||||
String encode(SharingPermission data) => data.value;
|
||||
|
||||
/// Decodes a [dynamic value][data] to a SharingPermission.
|
||||
///
|
||||
/// If [allowNull] is true and the [dynamic value][data] cannot be decoded successfully,
|
||||
/// then null is returned. However, if [allowNull] is false and the [dynamic value][data]
|
||||
/// cannot be decoded successfully, then an [UnimplementedError] is thrown.
|
||||
///
|
||||
/// The [allowNull] is very handy when an API changes and a new enum value is added or removed,
|
||||
/// and users are still using an old app with the old code.
|
||||
SharingPermission? decode(dynamic data, {bool allowNull = true}) {
|
||||
if (data != null) {
|
||||
switch (data) {
|
||||
case r'all': return SharingPermission.all;
|
||||
case r'asset.read': return SharingPermission.assetPeriodRead;
|
||||
case r'asset.update': return SharingPermission.assetPeriodUpdate;
|
||||
case r'asset.edit': return SharingPermission.assetPeriodEdit;
|
||||
case r'asset.delete': return SharingPermission.assetPeriodDelete;
|
||||
case r'asset.share': return SharingPermission.assetPeriodShare;
|
||||
case r'exif.read': return SharingPermission.exifPeriodRead;
|
||||
case r'person.read': return SharingPermission.personPeriodRead;
|
||||
case r'person.update': return SharingPermission.personPeriodUpdate;
|
||||
case r'person.merge': return SharingPermission.personPeriodMerge;
|
||||
case r'person.delete': return SharingPermission.personPeriodDelete;
|
||||
default:
|
||||
if (!allowNull) {
|
||||
throw ArgumentError('Unknown enum value to decode: $data');
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/// Singleton [SharingPermissionTypeTransformer] instance.
|
||||
static SharingPermissionTypeTransformer? _instance;
|
||||
}
|
||||
|
||||
+14
-14
@@ -19,11 +19,11 @@ class SyncAssetFaceV2 {
|
||||
required this.boundingBoxY1,
|
||||
required this.boundingBoxY2,
|
||||
required this.deletedAt,
|
||||
required this.faceClusterId,
|
||||
required this.id,
|
||||
required this.imageHeight,
|
||||
required this.imageWidth,
|
||||
required this.isVisible,
|
||||
required this.personId,
|
||||
required this.sourceType,
|
||||
});
|
||||
|
||||
@@ -57,9 +57,6 @@ class SyncAssetFaceV2 {
|
||||
/// Face deleted at
|
||||
DateTime? deletedAt;
|
||||
|
||||
/// Person ID
|
||||
String? faceClusterId;
|
||||
|
||||
/// Asset face ID
|
||||
String id;
|
||||
|
||||
@@ -78,6 +75,9 @@ class SyncAssetFaceV2 {
|
||||
/// Is the face visible in the asset
|
||||
bool isVisible;
|
||||
|
||||
/// Person ID
|
||||
String? personId;
|
||||
|
||||
/// Source type
|
||||
String sourceType;
|
||||
|
||||
@@ -89,11 +89,11 @@ class SyncAssetFaceV2 {
|
||||
other.boundingBoxY1 == boundingBoxY1 &&
|
||||
other.boundingBoxY2 == boundingBoxY2 &&
|
||||
other.deletedAt == deletedAt &&
|
||||
other.faceClusterId == faceClusterId &&
|
||||
other.id == id &&
|
||||
other.imageHeight == imageHeight &&
|
||||
other.imageWidth == imageWidth &&
|
||||
other.isVisible == isVisible &&
|
||||
other.personId == personId &&
|
||||
other.sourceType == sourceType;
|
||||
|
||||
@override
|
||||
@@ -105,15 +105,15 @@ class SyncAssetFaceV2 {
|
||||
(boundingBoxY1.hashCode) +
|
||||
(boundingBoxY2.hashCode) +
|
||||
(deletedAt == null ? 0 : deletedAt!.hashCode) +
|
||||
(faceClusterId == null ? 0 : faceClusterId!.hashCode) +
|
||||
(id.hashCode) +
|
||||
(imageHeight.hashCode) +
|
||||
(imageWidth.hashCode) +
|
||||
(isVisible.hashCode) +
|
||||
(personId == null ? 0 : personId!.hashCode) +
|
||||
(sourceType.hashCode);
|
||||
|
||||
@override
|
||||
String toString() => 'SyncAssetFaceV2[assetId=$assetId, boundingBoxX1=$boundingBoxX1, boundingBoxX2=$boundingBoxX2, boundingBoxY1=$boundingBoxY1, boundingBoxY2=$boundingBoxY2, deletedAt=$deletedAt, faceClusterId=$faceClusterId, id=$id, imageHeight=$imageHeight, imageWidth=$imageWidth, isVisible=$isVisible, sourceType=$sourceType]';
|
||||
String toString() => 'SyncAssetFaceV2[assetId=$assetId, boundingBoxX1=$boundingBoxX1, boundingBoxX2=$boundingBoxX2, boundingBoxY1=$boundingBoxY1, boundingBoxY2=$boundingBoxY2, deletedAt=$deletedAt, id=$id, imageHeight=$imageHeight, imageWidth=$imageWidth, isVisible=$isVisible, personId=$personId, sourceType=$sourceType]';
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
final json = <String, dynamic>{};
|
||||
@@ -128,16 +128,16 @@ class SyncAssetFaceV2 {
|
||||
: this.deletedAt!.toUtc().toIso8601String();
|
||||
} else {
|
||||
json[r'deletedAt'] = null;
|
||||
}
|
||||
if (this.faceClusterId != null) {
|
||||
json[r'faceClusterId'] = this.faceClusterId;
|
||||
} else {
|
||||
json[r'faceClusterId'] = null;
|
||||
}
|
||||
json[r'id'] = this.id;
|
||||
json[r'imageHeight'] = this.imageHeight;
|
||||
json[r'imageWidth'] = this.imageWidth;
|
||||
json[r'isVisible'] = this.isVisible;
|
||||
if (this.personId != null) {
|
||||
json[r'personId'] = this.personId;
|
||||
} else {
|
||||
json[r'personId'] = null;
|
||||
}
|
||||
json[r'sourceType'] = this.sourceType;
|
||||
return json;
|
||||
}
|
||||
@@ -157,11 +157,11 @@ class SyncAssetFaceV2 {
|
||||
boundingBoxY1: mapValueOfType<int>(json, r'boundingBoxY1')!,
|
||||
boundingBoxY2: mapValueOfType<int>(json, r'boundingBoxY2')!,
|
||||
deletedAt: mapDateTime(json, r'deletedAt', r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z|([+-](?:[01]\\d|2[0-3]):[0-5]\\d)))$/'),
|
||||
faceClusterId: mapValueOfType<String>(json, r'faceClusterId'),
|
||||
id: mapValueOfType<String>(json, r'id')!,
|
||||
imageHeight: mapValueOfType<int>(json, r'imageHeight')!,
|
||||
imageWidth: mapValueOfType<int>(json, r'imageWidth')!,
|
||||
isVisible: mapValueOfType<bool>(json, r'isVisible')!,
|
||||
personId: mapValueOfType<String>(json, r'personId'),
|
||||
sourceType: mapValueOfType<String>(json, r'sourceType')!,
|
||||
);
|
||||
}
|
||||
@@ -216,11 +216,11 @@ class SyncAssetFaceV2 {
|
||||
'boundingBoxY1',
|
||||
'boundingBoxY2',
|
||||
'deletedAt',
|
||||
'faceClusterId',
|
||||
'id',
|
||||
'imageHeight',
|
||||
'imageWidth',
|
||||
'isVisible',
|
||||
'personId',
|
||||
'sourceType',
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,107 +0,0 @@
|
||||
//
|
||||
// AUTO-GENERATED FILE, DO NOT MODIFY!
|
||||
//
|
||||
// @dart=2.18
|
||||
|
||||
// ignore_for_file: unused_element, unused_import
|
||||
// ignore_for_file: always_put_required_named_parameters_first
|
||||
// ignore_for_file: constant_identifier_names
|
||||
// ignore_for_file: lines_longer_than_80_chars
|
||||
|
||||
part of openapi.api;
|
||||
|
||||
class UpdateSharingOptionsDto {
|
||||
/// Returns a new [UpdateSharingOptionsDto] instance.
|
||||
UpdateSharingOptionsDto({
|
||||
required this.inTimeline,
|
||||
this.permissions = const [],
|
||||
});
|
||||
|
||||
bool inTimeline;
|
||||
|
||||
List<SharingPermission> permissions;
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) => identical(this, other) || other is UpdateSharingOptionsDto &&
|
||||
other.inTimeline == inTimeline &&
|
||||
_deepEquality.equals(other.permissions, permissions);
|
||||
|
||||
@override
|
||||
int get hashCode =>
|
||||
// ignore: unnecessary_parenthesis
|
||||
(inTimeline.hashCode) +
|
||||
(permissions.hashCode);
|
||||
|
||||
@override
|
||||
String toString() => 'UpdateSharingOptionsDto[inTimeline=$inTimeline, permissions=$permissions]';
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
final json = <String, dynamic>{};
|
||||
json[r'inTimeline'] = this.inTimeline;
|
||||
json[r'permissions'] = this.permissions;
|
||||
return json;
|
||||
}
|
||||
|
||||
/// Returns a new [UpdateSharingOptionsDto] instance and imports its values from
|
||||
/// [value] if it's a [Map], null otherwise.
|
||||
// ignore: prefer_constructors_over_static_methods
|
||||
static UpdateSharingOptionsDto? fromJson(dynamic value) {
|
||||
upgradeDto(value, "UpdateSharingOptionsDto");
|
||||
if (value is Map) {
|
||||
final json = value.cast<String, dynamic>();
|
||||
|
||||
return UpdateSharingOptionsDto(
|
||||
inTimeline: mapValueOfType<bool>(json, r'inTimeline')!,
|
||||
permissions: SharingPermission.listFromJson(json[r'permissions']),
|
||||
);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
static List<UpdateSharingOptionsDto> listFromJson(dynamic json, {bool growable = false,}) {
|
||||
final result = <UpdateSharingOptionsDto>[];
|
||||
if (json is List && json.isNotEmpty) {
|
||||
for (final row in json) {
|
||||
final value = UpdateSharingOptionsDto.fromJson(row);
|
||||
if (value != null) {
|
||||
result.add(value);
|
||||
}
|
||||
}
|
||||
}
|
||||
return result.toList(growable: growable);
|
||||
}
|
||||
|
||||
static Map<String, UpdateSharingOptionsDto> mapFromJson(dynamic json) {
|
||||
final map = <String, UpdateSharingOptionsDto>{};
|
||||
if (json is Map && json.isNotEmpty) {
|
||||
json = json.cast<String, dynamic>(); // ignore: parameter_assignments
|
||||
for (final entry in json.entries) {
|
||||
final value = UpdateSharingOptionsDto.fromJson(entry.value);
|
||||
if (value != null) {
|
||||
map[entry.key] = value;
|
||||
}
|
||||
}
|
||||
}
|
||||
return map;
|
||||
}
|
||||
|
||||
// maps a json object with a list of UpdateSharingOptionsDto-objects as value to a dart map
|
||||
static Map<String, List<UpdateSharingOptionsDto>> mapListFromJson(dynamic json, {bool growable = false,}) {
|
||||
final map = <String, List<UpdateSharingOptionsDto>>{};
|
||||
if (json is Map && json.isNotEmpty) {
|
||||
// ignore: parameter_assignments
|
||||
json = json.cast<String, dynamic>();
|
||||
for (final entry in json.entries) {
|
||||
map[entry.key] = UpdateSharingOptionsDto.listFromJson(entry.value, growable: growable,);
|
||||
}
|
||||
}
|
||||
return map;
|
||||
}
|
||||
|
||||
/// The list of required keys that must be present in a JSON.
|
||||
static const requiredKeys = <String>{
|
||||
'inTimeline',
|
||||
'permissions',
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
export 'src/color_override.dart';
|
||||
export 'src/components/close_button.dart';
|
||||
export 'src/components/column_button.dart';
|
||||
export 'src/components/form.dart';
|
||||
|
||||
@@ -5,6 +5,7 @@ import 'package:immich_mobile/domain/services/user.service.dart';
|
||||
import 'package:immich_mobile/domain/utils/background_sync.dart';
|
||||
import 'package:immich_mobile/platform/native_sync_api.g.dart';
|
||||
import 'package:immich_mobile/services/app_settings.service.dart';
|
||||
import 'package:immich_mobile/services/server_info.service.dart';
|
||||
import 'package:mocktail/mocktail.dart';
|
||||
|
||||
class MockStoreService extends Mock implements StoreService {}
|
||||
@@ -20,3 +21,5 @@ class MockPartnerService extends Mock implements PartnerService {}
|
||||
class MockAssetService extends Mock implements AssetService {}
|
||||
|
||||
class MockUserService extends Mock implements UserService {}
|
||||
|
||||
class MockServerInfoService extends Mock implements ServerInfoService {}
|
||||
|
||||
@@ -217,8 +217,8 @@ class MediumRepositoryContext {
|
||||
}
|
||||
|
||||
Future<AssetFaceEntityData> newFace({String? assetId, String? personId, int? imageWidth, int? imageHeight}) {
|
||||
imageWidth ??= TestUtils.randInt(999) + 1;
|
||||
imageHeight ??= TestUtils.randInt(999) + 1;
|
||||
imageWidth ??= TestUtils.randInt(999) + 2;
|
||||
imageHeight ??= TestUtils.randInt(999) + 2;
|
||||
|
||||
final x1 = TestUtils.randInt(imageWidth - 1);
|
||||
final y1 = TestUtils.randInt(imageHeight - 1);
|
||||
|
||||
@@ -5,7 +5,13 @@ import '../../utils.dart';
|
||||
class RemoteAssetFactory {
|
||||
const RemoteAssetFactory();
|
||||
|
||||
static RemoteAsset create({String? id, String? name, String? ownerId, bool isFavorite = false}) {
|
||||
static RemoteAsset create({
|
||||
String? id,
|
||||
String? name,
|
||||
String? ownerId,
|
||||
bool isFavorite = false,
|
||||
AssetType type = .image,
|
||||
}) {
|
||||
id = TestUtils.uuid(id);
|
||||
|
||||
return RemoteAsset(
|
||||
@@ -13,7 +19,7 @@ class RemoteAssetFactory {
|
||||
name: name ?? 'remote_$id.jpg',
|
||||
ownerId: TestUtils.uuid(ownerId),
|
||||
checksum: 'checksum-$id',
|
||||
type: .image,
|
||||
type: type,
|
||||
createdAt: TestUtils.yesterday(),
|
||||
updatedAt: TestUtils.now(),
|
||||
isFavorite: isFavorite,
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import 'dart:typed_data';
|
||||
|
||||
import 'package:immich_mobile/constants/enums.dart';
|
||||
import 'package:immich_mobile/domain/models/asset_edit.model.dart';
|
||||
import 'package:immich_mobile/domain/models/user.model.dart';
|
||||
import 'package:mocktail/mocktail.dart' as mock;
|
||||
import 'package:mocktail/mocktail.dart';
|
||||
@@ -15,6 +16,7 @@ class RepositoryMocks {
|
||||
final localAlbum = MockLocalAlbumRepository();
|
||||
final localAsset = MockDriftLocalAssetRepository();
|
||||
final trashedAsset = MockTrashedLocalAssetRepository();
|
||||
final remoteAsset = MockRemoteAssetRepository();
|
||||
|
||||
final nativeApi = MockNativeSyncApi();
|
||||
|
||||
@@ -28,12 +30,14 @@ class RepositoryMocks {
|
||||
reset(localAsset);
|
||||
reset(trashedAsset);
|
||||
reset(nativeApi);
|
||||
reset(remoteAsset);
|
||||
}
|
||||
}
|
||||
|
||||
class ServiceMocks {
|
||||
final PartnerStub partner = PartnerStub(MockPartnerService());
|
||||
final UserStub user = UserStub(MockUserService());
|
||||
final partner = PartnerStub(MockPartnerService());
|
||||
final user = UserStub(MockUserService());
|
||||
final asset = AssetStub(MockAssetService());
|
||||
|
||||
ServiceMocks() {
|
||||
resetAll();
|
||||
@@ -43,8 +47,10 @@ class ServiceMocks {
|
||||
_registerFallbacks();
|
||||
partner.reset();
|
||||
user.reset();
|
||||
asset.reset();
|
||||
_stubUserService();
|
||||
_stubPartnerService();
|
||||
_stubAssetService();
|
||||
}
|
||||
|
||||
void _stubUserService() {
|
||||
@@ -63,16 +69,22 @@ class ServiceMocks {
|
||||
when(partner.create).thenAnswer((_) async {});
|
||||
when(partner.delete).thenAnswer((_) async {});
|
||||
}
|
||||
|
||||
void _stubAssetService() {
|
||||
when(asset.updateFavorite).thenAnswer((_) async {});
|
||||
when(asset.applyEdits).thenAnswer((_) async {});
|
||||
}
|
||||
}
|
||||
|
||||
void _registerFallbacks() {
|
||||
registerFallbackValue(LocalAlbumFactory.create());
|
||||
registerFallbackValue(LocalAssetFactory.create());
|
||||
registerFallbackValue(Uint8List(0));
|
||||
registerFallbackValue(<AssetEdit>[]);
|
||||
}
|
||||
|
||||
extension type const Stub<T extends Mock>(T mockedService) {
|
||||
void reset() => mock.reset(mockedService);
|
||||
extension type const Stub<T extends Mock>(T mockedClass) {
|
||||
void reset() => mock.reset(mockedClass);
|
||||
}
|
||||
|
||||
extension type const PartnerStub(MockPartnerService service) implements Stub<MockPartnerService> {
|
||||
@@ -119,3 +131,11 @@ extension type const UserStub(MockUserService service) implements Stub<MockUserS
|
||||
Future<String?> Function() get createProfileImage =>
|
||||
() => service.createProfileImage(any(), any());
|
||||
}
|
||||
|
||||
extension type const AssetStub(MockAssetService service) implements Stub<MockAssetService> {
|
||||
Future<void> Function() get updateFavorite =>
|
||||
() => service.updateFavorite(any(), any());
|
||||
|
||||
Future<void> Function() get applyEdits =>
|
||||
() => service.applyEdits(any(), any());
|
||||
}
|
||||
|
||||
@@ -0,0 +1,54 @@
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:immich_mobile/domain/models/store.model.dart';
|
||||
import 'package:immich_mobile/domain/services/store.service.dart';
|
||||
import 'package:immich_mobile/presentation/actions/action.widget.dart';
|
||||
import 'package:immich_mobile/presentation/actions/asset_debug.action.dart';
|
||||
import 'package:immich_ui/immich_ui.dart';
|
||||
|
||||
import '../../factories/remote_asset_factory.dart';
|
||||
import '../../presentation_context.dart';
|
||||
|
||||
void main() {
|
||||
late PresentationContext context;
|
||||
|
||||
setUp(() async {
|
||||
context = await PresentationContext.create();
|
||||
await StoreService.I.put(StoreKey.advancedTroubleshooting, true);
|
||||
});
|
||||
|
||||
tearDown(() {
|
||||
context.dispose();
|
||||
});
|
||||
|
||||
group('AssetDebugAction', () {
|
||||
testWidgets('visible for a single asset when advanced troubleshooting is on', (tester) async {
|
||||
await tester.pumpTestWidget(
|
||||
ActionIconButtonWidget(action: AssetDebugAction(assets: [RemoteAssetFactory.create()])),
|
||||
overrides: context.overrides,
|
||||
);
|
||||
|
||||
expect(find.byType(ImmichIconButton), findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets('hidden for multiple assets', (tester) async {
|
||||
await tester.pumpTestWidget(
|
||||
ActionIconButtonWidget(
|
||||
action: AssetDebugAction(assets: [RemoteAssetFactory.create(), RemoteAssetFactory.create()]),
|
||||
),
|
||||
overrides: context.overrides,
|
||||
);
|
||||
|
||||
expect(find.byType(ImmichIconButton), findsNothing);
|
||||
});
|
||||
|
||||
testWidgets('hidden when advanced troubleshooting is off', (tester) async {
|
||||
await StoreService.I.put(StoreKey.advancedTroubleshooting, false);
|
||||
await tester.pumpTestWidget(
|
||||
ActionIconButtonWidget(action: AssetDebugAction(assets: [RemoteAssetFactory.create()])),
|
||||
overrides: context.overrides,
|
||||
);
|
||||
|
||||
expect(find.byType(ImmichIconButton), findsNothing);
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,119 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
|
||||
import 'package:immich_mobile/domain/models/asset_edit.model.dart';
|
||||
import 'package:immich_mobile/models/server_info/server_version.model.dart';
|
||||
import 'package:immich_mobile/presentation/actions/action.widget.dart';
|
||||
import 'package:immich_mobile/presentation/actions/edit.action.dart';
|
||||
import 'package:immich_mobile/providers/infrastructure/asset.provider.dart';
|
||||
import 'package:immich_mobile/providers/server_info.provider.dart';
|
||||
import 'package:immich_mobile/providers/websocket.provider.dart';
|
||||
import 'package:immich_ui/immich_ui.dart';
|
||||
import 'package:mocktail/mocktail.dart';
|
||||
|
||||
import '../../../infrastructure/repository.mock.dart';
|
||||
import '../../factories/remote_asset_factory.dart';
|
||||
import '../../presentation_context.dart';
|
||||
import '../../riverpod_mocks.dart';
|
||||
|
||||
void main() {
|
||||
late PresentationContext context;
|
||||
|
||||
const supportedVersion = ServerVersion(major: 2, minor: 6, patch: 0);
|
||||
const unsupportedVersion = ServerVersion(major: 2, minor: 5, patch: 9);
|
||||
|
||||
setUp(() async {
|
||||
context = await PresentationContext.create();
|
||||
});
|
||||
|
||||
tearDown(() {
|
||||
context.dispose();
|
||||
});
|
||||
|
||||
List<Override> overrides(ServerVersion version) => [
|
||||
...context.overrides,
|
||||
serverInfoProvider.overrideWith((ref) => FakeServerInfoNotifier(version)),
|
||||
];
|
||||
|
||||
RemoteAsset owned({AssetType type = AssetType.image}) =>
|
||||
RemoteAssetFactory.create(ownerId: context.currentUser.id, type: type);
|
||||
|
||||
Future<void> pumpAction(WidgetTester tester, EditAssetAction action, {ServerVersion version = supportedVersion}) =>
|
||||
tester.pumpTestWidget(ActionIconButtonWidget(action: action), overrides: overrides(version));
|
||||
|
||||
group('EditAssetAction', () {
|
||||
testWidgets('visible for a single owned editable asset on a supported server', (tester) async {
|
||||
await pumpAction(tester, EditAssetAction(assets: [owned()]));
|
||||
|
||||
expect(find.byType(ImmichIconButton), findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets('hidden when the server is older than 2.6.0', (tester) async {
|
||||
await pumpAction(tester, EditAssetAction(assets: [owned()]), version: unsupportedVersion);
|
||||
|
||||
expect(find.byType(ImmichIconButton), findsNothing);
|
||||
});
|
||||
|
||||
testWidgets('hidden for more than one asset', (tester) async {
|
||||
await pumpAction(tester, EditAssetAction(assets: [owned(), owned()]));
|
||||
|
||||
expect(find.byType(ImmichIconButton), findsNothing);
|
||||
});
|
||||
|
||||
testWidgets('hidden for an asset owned by someone else', (tester) async {
|
||||
await pumpAction(tester, EditAssetAction(assets: [RemoteAssetFactory.create()]));
|
||||
|
||||
expect(find.byType(ImmichIconButton), findsNothing);
|
||||
});
|
||||
|
||||
testWidgets('hidden for a non-editable asset', (tester) async {
|
||||
await pumpAction(tester, EditAssetAction(assets: [owned(type: AssetType.video)]));
|
||||
|
||||
expect(find.byType(ImmichIconButton), findsNothing);
|
||||
});
|
||||
});
|
||||
|
||||
group('EditAssetAction onAction', () {
|
||||
testWidgets('reads the edits and exif for the asset from the repository', (tester) async {
|
||||
final asset = owned();
|
||||
final repository = MockRemoteAssetRepository();
|
||||
when(() => repository.getAssetEdits(any())).thenAnswer((_) async => const <AssetEdit>[]);
|
||||
when(() => repository.getExif(any())).thenAnswer((_) async => null);
|
||||
|
||||
await tester.pumpTestAction(
|
||||
EditAssetAction(assets: [asset]),
|
||||
overrides: [...overrides(supportedVersion), remoteAssetRepositoryProvider.overrideWithValue(repository)],
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
verify(() => repository.getAssetEdits(asset.id)).called(1);
|
||||
verify(() => repository.getExif(asset.id)).called(1);
|
||||
});
|
||||
|
||||
testWidgets('applyEdits forwards the edits to the service and waits for both ready events', (tester) async {
|
||||
late FakeWebsocketNotifier websocket;
|
||||
const edits = <AssetEdit>[];
|
||||
|
||||
late WidgetRef capturedRef;
|
||||
await tester.pumpTestWidget(
|
||||
Consumer(
|
||||
builder: (_, ref, _) {
|
||||
capturedRef = ref;
|
||||
return const SizedBox.shrink();
|
||||
},
|
||||
),
|
||||
overrides: [
|
||||
...context.overrides,
|
||||
assetServiceProvider.overrideWithValue(context.mocks.asset.service),
|
||||
websocketProvider.overrideWith((ref) => websocket = FakeWebsocketNotifier(ref)),
|
||||
],
|
||||
);
|
||||
|
||||
await EditAssetAction.applyEdits(capturedRef, 'asset-1', edits);
|
||||
|
||||
verify(() => context.mocks.asset.service.applyEdits('asset-1', edits)).called(1);
|
||||
expect(websocket.waitedEvents, containsAll(['AssetEditReadyV1', 'AssetEditReadyV2']));
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
|
||||
import 'package:immich_mobile/presentation/actions/favorite.action.dart';
|
||||
import 'package:immich_mobile/providers/infrastructure/asset.provider.dart';
|
||||
import 'package:mocktail/mocktail.dart';
|
||||
|
||||
import '../../factories/remote_asset_factory.dart';
|
||||
import '../../presentation_context.dart';
|
||||
|
||||
void main() {
|
||||
late PresentationContext context;
|
||||
|
||||
setUp(() async {
|
||||
context = await PresentationContext.create();
|
||||
});
|
||||
|
||||
tearDown(() {
|
||||
context.dispose();
|
||||
});
|
||||
|
||||
List<Override> overrides() => [
|
||||
...context.overrides,
|
||||
assetServiceProvider.overrideWithValue(context.mocks.asset.service),
|
||||
];
|
||||
|
||||
RemoteAsset owned({bool isFavorite = false}) =>
|
||||
RemoteAssetFactory.create(ownerId: context.currentUser.id, isFavorite: isFavorite);
|
||||
|
||||
group('FavoriteAction', () {
|
||||
testWidgets('favorites the eligible owned assets', (tester) async {
|
||||
final asset = owned();
|
||||
|
||||
await tester.pumpTestAction(FavoriteAction(assets: [asset]), overrides: overrides());
|
||||
|
||||
verify(() => context.mocks.asset.service.updateFavorite([asset.id], true)).called(1);
|
||||
});
|
||||
|
||||
testWidgets('unfavorite the eligible owned assets', (tester) async {
|
||||
final asset = owned(isFavorite: true);
|
||||
|
||||
await tester.pumpTestAction(FavoriteAction(assets: [asset]), overrides: overrides());
|
||||
|
||||
verify(() => context.mocks.asset.service.updateFavorite([asset.id], false)).called(1);
|
||||
});
|
||||
|
||||
testWidgets('ignores assets owned by someone else', (tester) async {
|
||||
final mine = owned();
|
||||
final theirs = RemoteAssetFactory.create();
|
||||
|
||||
await tester.pumpTestAction(FavoriteAction(assets: [mine, theirs]), overrides: overrides());
|
||||
|
||||
verify(() => context.mocks.asset.service.updateFavorite([mine.id], true)).called(1);
|
||||
});
|
||||
|
||||
testWidgets('batches every eligible owned asset into a single call', (tester) async {
|
||||
final first = owned();
|
||||
final second = owned();
|
||||
|
||||
await tester.pumpTestAction(FavoriteAction(assets: [first, second]), overrides: overrides());
|
||||
|
||||
verify(() => context.mocks.asset.service.updateFavorite([first.id, second.id], true)).called(1);
|
||||
});
|
||||
|
||||
testWidgets('skips owned assets already in the target state', (tester) async {
|
||||
final stale = owned();
|
||||
final alreadyFavorite = owned(isFavorite: true);
|
||||
|
||||
await tester.pumpTestAction(FavoriteAction(assets: [stale, alreadyFavorite]), overrides: overrides());
|
||||
|
||||
verify(() => context.mocks.asset.service.updateFavorite([stale.id], true)).called(1);
|
||||
});
|
||||
|
||||
testWidgets('shows a confirmation snackbar on success', (tester) async {
|
||||
await tester.pumpTestAction(FavoriteAction(assets: [owned()]), overrides: overrides());
|
||||
await tester.pumpUntilFound(find.byType(SnackBar));
|
||||
|
||||
expect(find.byType(SnackBar), findsOneWidget);
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -5,32 +5,25 @@ import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:immich_mobile/domain/models/user.model.dart';
|
||||
import 'package:immich_mobile/presentation/actions/partner.action.dart';
|
||||
import 'package:immich_mobile/providers/infrastructure/user.provider.dart';
|
||||
import 'package:immich_mobile/providers/user.provider.dart';
|
||||
import 'package:mocktail/mocktail.dart';
|
||||
|
||||
import '../../factories/user_factory.dart';
|
||||
import '../../mocks.dart';
|
||||
import '../../presentation_context.dart';
|
||||
|
||||
void main() {
|
||||
late PresentationContext context;
|
||||
late UserDto currentUser;
|
||||
final mocks = ServiceMocks();
|
||||
|
||||
setUp(() async {
|
||||
currentUser = UserFactory.createDto();
|
||||
context = await PresentationContext.create();
|
||||
when(mocks.user.tryGetMyUser).thenReturn(currentUser);
|
||||
});
|
||||
|
||||
tearDown(() async {
|
||||
mocks.resetAll();
|
||||
await context.dispose();
|
||||
tearDown(() {
|
||||
context.dispose();
|
||||
});
|
||||
|
||||
List<Override> overrides({List<User> candidates = const []}) => [
|
||||
currentUserProvider.overrideWith((ref) => CurrentUserProvider(mocks.user.service)),
|
||||
partnerServiceProvider.overrideWithValue(mocks.partner.service),
|
||||
...context.overrides,
|
||||
partnerServiceProvider.overrideWithValue(context.mocks.partner.service),
|
||||
candidatesStateProvider.overrideWith((ref) => Stream<Iterable<User>>.value(candidates)),
|
||||
];
|
||||
|
||||
@@ -43,7 +36,9 @@ void main() {
|
||||
await tester.tap(find.text(candidate.name));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
verify(() => mocks.partner.service.create(sharedById: currentUser.id, sharedWithId: candidate.id)).called(1);
|
||||
verify(
|
||||
() => context.mocks.partner.service.create(sharedById: context.currentUser.id, sharedWithId: candidate.id),
|
||||
).called(1);
|
||||
});
|
||||
|
||||
testWidgets('creates nothing when the selection dialog is dismissed', (tester) async {
|
||||
@@ -51,7 +46,7 @@ void main() {
|
||||
await tester.sendKeyEvent(LogicalKeyboardKey.escape); // dismiss without selecting
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
verifyNever(mocks.partner.create);
|
||||
verifyNever(context.mocks.partner.create);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -65,7 +60,9 @@ void main() {
|
||||
await tester.tap(find.byType(TextButton).last); // confirm
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
verify(() => mocks.partner.service.delete(sharedById: currentUser.id, sharedWithId: partner.id)).called(1);
|
||||
verify(
|
||||
() => context.mocks.partner.service.delete(sharedById: context.currentUser.id, sharedWithId: partner.id),
|
||||
).called(1);
|
||||
});
|
||||
|
||||
testWidgets('deletes nothing when the confirmation is cancelled', (tester) async {
|
||||
@@ -77,7 +74,7 @@ void main() {
|
||||
await tester.tap(find.byType(TextButton).first); // cancel
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
verifyNever(mocks.partner.delete);
|
||||
verifyNever(context.mocks.partner.delete);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@@ -0,0 +1,108 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:immich_mobile/presentation/actions/action.dart';
|
||||
import 'package:immich_mobile/presentation/actions/action.widget.dart';
|
||||
import 'package:immich_mobile/presentation/actions/timeline.action.dart';
|
||||
import 'package:immich_mobile/providers/timeline/multiselect.provider.dart';
|
||||
|
||||
import '../../factories/remote_asset_factory.dart';
|
||||
import '../../presentation_context.dart';
|
||||
|
||||
class _FakeAction extends BaseAction {
|
||||
_FakeAction({this.visible = true, this.error});
|
||||
|
||||
final bool visible;
|
||||
final Object? error;
|
||||
|
||||
bool ran = false;
|
||||
bool? selectionDuringOnAction;
|
||||
|
||||
@override
|
||||
IconData get icon => Icons.bolt;
|
||||
|
||||
@override
|
||||
String label(ActionScope scope) => 'fake';
|
||||
|
||||
@override
|
||||
bool isVisible(ActionScope scope) => visible;
|
||||
|
||||
@override
|
||||
Future<void> onAction(ActionScope scope) async {
|
||||
ran = true;
|
||||
selectionDuringOnAction = scope.ref.read(multiSelectProvider).isEnabled;
|
||||
if (error != null) {
|
||||
throw error!;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void main() {
|
||||
late PresentationContext context;
|
||||
|
||||
setUp(() async {
|
||||
context = await PresentationContext.create();
|
||||
});
|
||||
|
||||
tearDown(() {
|
||||
context.dispose();
|
||||
});
|
||||
|
||||
List<Override> seededOverrides() => [
|
||||
...context.overrides,
|
||||
multiSelectProvider.overrideWith(
|
||||
() => MultiSelectNotifier(
|
||||
MultiSelectState(selectedAssets: {RemoteAssetFactory.create()}, lockedSelectionAssets: const {}),
|
||||
),
|
||||
),
|
||||
];
|
||||
|
||||
Future<(ActionScope, ProviderContainer)> pumpScope(WidgetTester tester) async {
|
||||
late ActionScope scope;
|
||||
late ProviderContainer container;
|
||||
await tester.pumpTestWidget(
|
||||
Consumer(
|
||||
builder: (innerContext, ref, _) {
|
||||
scope = ActionScope(context: innerContext, ref: ref, authUser: context.currentUser);
|
||||
container = ProviderScope.containerOf(innerContext, listen: false);
|
||||
return const SizedBox.shrink();
|
||||
},
|
||||
),
|
||||
overrides: seededOverrides(),
|
||||
);
|
||||
return (scope, container);
|
||||
}
|
||||
|
||||
group('TimelineAction', () {
|
||||
testWidgets('runs the wrapped action and then clears the selection', (tester) async {
|
||||
final inner = _FakeAction();
|
||||
final (scope, container) = await pumpScope(tester);
|
||||
await TimelineAction(action: inner).onAction(scope);
|
||||
|
||||
expect(inner.ran, isTrue);
|
||||
expect(inner.selectionDuringOnAction, isTrue, reason: 'reset must run after the inner action, not before');
|
||||
expect(container.read(multiSelectProvider).isEnabled, isFalse);
|
||||
});
|
||||
|
||||
testWidgets('rethrows and keeps the selection when the wrapped action throws', (tester) async {
|
||||
final error = Exception('boom');
|
||||
final inner = _FakeAction(error: error);
|
||||
final (scope, container) = await pumpScope(tester);
|
||||
|
||||
await expectLater(TimelineAction(action: inner).onAction(scope), throwsA(same(error)));
|
||||
|
||||
expect(inner.ran, isTrue);
|
||||
expect(container.read(multiSelectProvider).isEnabled, isTrue);
|
||||
});
|
||||
|
||||
testWidgets('delegates visibility to the wrapped action', (tester) async {
|
||||
await tester.pumpTestWidget(
|
||||
ActionIconButtonWidget(action: TimelineAction(action: _FakeAction(visible: false))),
|
||||
overrides: context.overrides,
|
||||
);
|
||||
|
||||
expect(find.byType(ActionIconButtonWidget), findsOneWidget);
|
||||
expect(find.byIcon(Icons.bolt), findsNothing);
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -13,7 +13,7 @@ void main() {
|
||||
late PresentationContext context;
|
||||
|
||||
setUp(() async => context = await PresentationContext.create());
|
||||
tearDown(() async => await context.dispose());
|
||||
tearDown(() => context.dispose());
|
||||
|
||||
group('PartnerSharedByList', () {
|
||||
testWidgets('shows the empty-state add button when there are no partners', (tester) async {
|
||||
|
||||
@@ -23,7 +23,7 @@ import 'mocks.dart';
|
||||
|
||||
class PresentationContext {
|
||||
PresentationContext._({required UserDto user}) : currentUser = user, mocks = ServiceMocks() {
|
||||
when(mocks.user.tryGetMyUser).thenReturn(currentUser);
|
||||
setup();
|
||||
}
|
||||
|
||||
static const String serverEndpoint = 'http://localhost:3000';
|
||||
@@ -46,10 +46,14 @@ class PresentationContext {
|
||||
return PresentationContext._(user: UserFactory.createDto());
|
||||
}
|
||||
|
||||
Future<void> dispose() async {
|
||||
// TODO: Dispose the store and database after each test.
|
||||
// This is currently not possible because the store is a singleton and is used across tests.
|
||||
// Refactor the store to be created per test to allow proper disposal.
|
||||
void setup() {
|
||||
when(mocks.user.tryGetMyUser).thenReturn(currentUser);
|
||||
}
|
||||
|
||||
void dispose() {
|
||||
addTearDown(() {
|
||||
mocks.resetAll();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -73,7 +77,7 @@ extension PumpPresentationWidget on WidgetTester {
|
||||
localizationsDelegates: context.localizationDelegates,
|
||||
supportedLocales: context.supportedLocales,
|
||||
locale: context.locale,
|
||||
home: Material(child: widget),
|
||||
home: Scaffold(body: widget),
|
||||
),
|
||||
),
|
||||
),
|
||||
@@ -83,10 +87,7 @@ extension PumpPresentationWidget on WidgetTester {
|
||||
}
|
||||
|
||||
Future<void> pumpTestAction(BaseAction action, {List<Override> overrides = const []}) async {
|
||||
await pumpTestWidget(
|
||||
Scaffold(body: ActionIconButtonWidget(action: action)),
|
||||
overrides: overrides,
|
||||
);
|
||||
await pumpTestWidget(ActionIconButtonWidget(action: action), overrides: overrides);
|
||||
await tap(find.byType(ImmichIconButton));
|
||||
await pump();
|
||||
}
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
import 'package:immich_mobile/models/server_info/server_version.model.dart';
|
||||
import 'package:immich_mobile/providers/server_info.provider.dart';
|
||||
import 'package:immich_mobile/providers/websocket.provider.dart';
|
||||
|
||||
import '../domain/service.mock.dart';
|
||||
|
||||
class FakeServerInfoNotifier extends ServerInfoNotifier {
|
||||
FakeServerInfoNotifier([ServerVersion version = const ServerVersion(major: 2, minor: 6, patch: 0)])
|
||||
: super(MockServerInfoService()) {
|
||||
state = state.copyWith(serverVersion: version);
|
||||
}
|
||||
}
|
||||
|
||||
class FakeWebsocketNotifier extends WebsocketNotifier {
|
||||
FakeWebsocketNotifier(super.ref);
|
||||
|
||||
final List<String> waitedEvents = [];
|
||||
|
||||
@override
|
||||
Future<void> waitForEvent(String event, bool Function(dynamic)? predicate, Duration timeout) {
|
||||
waitedEvents.add(event);
|
||||
return Future.value();
|
||||
}
|
||||
}
|
||||
@@ -2693,121 +2693,6 @@
|
||||
"x-immich-permission": "album.read"
|
||||
}
|
||||
},
|
||||
"/albums/{id}/user/self": {
|
||||
"get": {
|
||||
"description": "Get the own sharing permissions in a specific album.",
|
||||
"operationId": "getOwnAlbumUser",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "id",
|
||||
"required": true,
|
||||
"in": "path",
|
||||
"schema": {
|
||||
"format": "uuid",
|
||||
"pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$",
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/SharingOptionsResponseDto"
|
||||
}
|
||||
}
|
||||
},
|
||||
"description": ""
|
||||
}
|
||||
},
|
||||
"security": [
|
||||
{
|
||||
"bearer": []
|
||||
},
|
||||
{
|
||||
"cookie": []
|
||||
},
|
||||
{
|
||||
"api_key": []
|
||||
}
|
||||
],
|
||||
"summary": "Get own sharing permissions",
|
||||
"tags": [
|
||||
"Albums"
|
||||
],
|
||||
"x-immich-history": [
|
||||
{
|
||||
"version": "v3",
|
||||
"state": "Added"
|
||||
},
|
||||
{
|
||||
"version": "v3",
|
||||
"state": "Stable"
|
||||
}
|
||||
],
|
||||
"x-immich-permission": "albumAsset.create",
|
||||
"x-immich-state": "Stable"
|
||||
},
|
||||
"put": {
|
||||
"description": "Change the own sharing permissions in a specific album.",
|
||||
"operationId": "updateOwnAlbumUser",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "id",
|
||||
"required": true,
|
||||
"in": "path",
|
||||
"schema": {
|
||||
"format": "uuid",
|
||||
"pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$",
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
],
|
||||
"requestBody": {
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/UpdateSharingOptionsDto"
|
||||
}
|
||||
}
|
||||
},
|
||||
"required": true
|
||||
},
|
||||
"responses": {
|
||||
"204": {
|
||||
"description": ""
|
||||
}
|
||||
},
|
||||
"security": [
|
||||
{
|
||||
"bearer": []
|
||||
},
|
||||
{
|
||||
"cookie": []
|
||||
},
|
||||
{
|
||||
"api_key": []
|
||||
}
|
||||
],
|
||||
"summary": "Update own sharing permissions",
|
||||
"tags": [
|
||||
"Albums"
|
||||
],
|
||||
"x-immich-history": [
|
||||
{
|
||||
"version": "v3",
|
||||
"state": "Added"
|
||||
},
|
||||
{
|
||||
"version": "v3",
|
||||
"state": "Stable"
|
||||
}
|
||||
],
|
||||
"x-immich-permission": "albumAsset.create",
|
||||
"x-immich-state": "Stable"
|
||||
}
|
||||
},
|
||||
"/albums/{id}/user/{userId}": {
|
||||
"delete": {
|
||||
"description": "Remove a user from an album. Use an ID of \"me\" to leave a shared album.",
|
||||
@@ -9282,7 +9167,7 @@
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/MergeFaceClusterDto"
|
||||
"$ref": "#/components/schemas/MergePersonDto"
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -18075,12 +17960,6 @@
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
"permissions": {
|
||||
"items": {
|
||||
"$ref": "#/components/schemas/SharingPermission"
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
"resized": {
|
||||
"description": "Is resized",
|
||||
"type": "boolean",
|
||||
@@ -18152,7 +18031,6 @@
|
||||
"originalFileName",
|
||||
"originalPath",
|
||||
"ownerId",
|
||||
"permissions",
|
||||
"thumbhash",
|
||||
"type",
|
||||
"updatedAt",
|
||||
@@ -19354,7 +19232,6 @@
|
||||
"DatabaseBackup",
|
||||
"FacialRecognitionQueueAll",
|
||||
"FacialRecognition",
|
||||
"FacialRecognitionMerge",
|
||||
"FileDelete",
|
||||
"FileMigrationQueueAll",
|
||||
"LibraryDeleteCheck",
|
||||
@@ -19790,8 +19667,7 @@
|
||||
"integrity-checksum-mismatch-refresh",
|
||||
"integrity-missing-files-delete-all",
|
||||
"integrity-untracked-files-delete-all",
|
||||
"integrity-checksum-mismatch-delete-all",
|
||||
"person-group-merge"
|
||||
"integrity-checksum-mismatch-delete-all"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
@@ -20123,10 +19999,10 @@
|
||||
},
|
||||
"type": "object"
|
||||
},
|
||||
"MergeFaceClusterDto": {
|
||||
"MergePersonDto": {
|
||||
"properties": {
|
||||
"ids": {
|
||||
"description": "Face cluster IDs to merge",
|
||||
"description": "Person IDs to merge",
|
||||
"items": {
|
||||
"format": "uuid",
|
||||
"pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$",
|
||||
@@ -21172,11 +21048,6 @@
|
||||
],
|
||||
"x-immich-state": "Stable"
|
||||
},
|
||||
"faceClusterId": {
|
||||
"description": "Face cluster ID",
|
||||
"nullable": true,
|
||||
"type": "string"
|
||||
},
|
||||
"id": {
|
||||
"description": "Person ID",
|
||||
"format": "uuid",
|
||||
@@ -21229,7 +21100,6 @@
|
||||
},
|
||||
"required": [
|
||||
"birthDate",
|
||||
"faceClusterId",
|
||||
"id",
|
||||
"isHidden",
|
||||
"name",
|
||||
@@ -23310,41 +23180,6 @@
|
||||
},
|
||||
"type": "object"
|
||||
},
|
||||
"SharingOptionsResponseDto": {
|
||||
"properties": {
|
||||
"inTimeline": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"permissions": {
|
||||
"items": {
|
||||
"$ref": "#/components/schemas/SharingPermission"
|
||||
},
|
||||
"type": "array"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"inTimeline",
|
||||
"permissions"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"SharingPermission": {
|
||||
"description": "Sharing permission schema",
|
||||
"enum": [
|
||||
"all",
|
||||
"asset.read",
|
||||
"asset.update",
|
||||
"asset.edit",
|
||||
"asset.delete",
|
||||
"asset.share",
|
||||
"exif.read",
|
||||
"person.read",
|
||||
"person.update",
|
||||
"person.merge",
|
||||
"person.delete"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"SignUpDto": {
|
||||
"properties": {
|
||||
"email": {
|
||||
@@ -24497,11 +24332,6 @@
|
||||
"pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z|([+-](?:[01]\\d|2[0-3]):[0-5]\\d)))$",
|
||||
"type": "string"
|
||||
},
|
||||
"faceClusterId": {
|
||||
"description": "Person ID",
|
||||
"nullable": true,
|
||||
"type": "string"
|
||||
},
|
||||
"id": {
|
||||
"description": "Asset face ID",
|
||||
"format": "uuid",
|
||||
@@ -24524,6 +24354,11 @@
|
||||
"description": "Is the face visible in the asset",
|
||||
"type": "boolean"
|
||||
},
|
||||
"personId": {
|
||||
"description": "Person ID",
|
||||
"nullable": true,
|
||||
"type": "string"
|
||||
},
|
||||
"sourceType": {
|
||||
"description": "Source type",
|
||||
"type": "string"
|
||||
@@ -24536,11 +24371,11 @@
|
||||
"boundingBoxY1",
|
||||
"boundingBoxY2",
|
||||
"deletedAt",
|
||||
"faceClusterId",
|
||||
"id",
|
||||
"imageHeight",
|
||||
"imageWidth",
|
||||
"isVisible",
|
||||
"personId",
|
||||
"sourceType"
|
||||
],
|
||||
"type": "object"
|
||||
@@ -27306,24 +27141,6 @@
|
||||
},
|
||||
"type": "object"
|
||||
},
|
||||
"UpdateSharingOptionsDto": {
|
||||
"properties": {
|
||||
"inTimeline": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"permissions": {
|
||||
"items": {
|
||||
"$ref": "#/components/schemas/SharingPermission"
|
||||
},
|
||||
"type": "array"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"inTimeline",
|
||||
"permissions"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"UsageByUserDto": {
|
||||
"properties": {
|
||||
"photos": {
|
||||
|
||||
@@ -588,14 +588,6 @@ export type MapMarkerResponseDto = {
|
||||
/** State/Province name */
|
||||
state: string | null;
|
||||
};
|
||||
export type SharingOptionsResponseDto = {
|
||||
inTimeline: boolean;
|
||||
permissions: SharingPermission[];
|
||||
};
|
||||
export type UpdateSharingOptionsDto = {
|
||||
inTimeline: boolean;
|
||||
permissions: SharingPermission[];
|
||||
};
|
||||
export type UpdateAlbumUserDto = {
|
||||
role: AlbumUserRole;
|
||||
};
|
||||
@@ -833,8 +825,6 @@ export type PersonResponseDto = {
|
||||
birthDate: string | null;
|
||||
/** Person color (hex) */
|
||||
color?: string;
|
||||
/** Face cluster ID */
|
||||
faceClusterId: string | null;
|
||||
/** Person ID */
|
||||
id: string;
|
||||
/** Is favorite */
|
||||
@@ -918,7 +908,6 @@ export type AssetResponseDto = {
|
||||
/** Owner user ID */
|
||||
ownerId: string;
|
||||
people?: PersonResponseDto[];
|
||||
permissions: SharingPermission[];
|
||||
/** Is resized */
|
||||
resized?: boolean;
|
||||
stack?: (AssetStackResponseDto) | null;
|
||||
@@ -1505,8 +1494,8 @@ export type PersonUpdateDto = {
|
||||
/** Person name */
|
||||
name?: string;
|
||||
};
|
||||
export type MergeFaceClusterDto = {
|
||||
/** Face cluster IDs to merge */
|
||||
export type MergePersonDto = {
|
||||
/** Person IDs to merge */
|
||||
ids: string[];
|
||||
};
|
||||
export type AssetFaceUpdateItem = {
|
||||
@@ -3034,8 +3023,6 @@ export type SyncAssetFaceV2 = {
|
||||
boundingBoxY2: number;
|
||||
/** Face deleted at */
|
||||
deletedAt: string | null;
|
||||
/** Person ID */
|
||||
faceClusterId: string | null;
|
||||
/** Asset face ID */
|
||||
id: string;
|
||||
/** Image height */
|
||||
@@ -3044,6 +3031,8 @@ export type SyncAssetFaceV2 = {
|
||||
imageWidth: number;
|
||||
/** Is the face visible in the asset */
|
||||
isVisible: boolean;
|
||||
/** Person ID */
|
||||
personId: string | null;
|
||||
/** Source type */
|
||||
sourceType: string;
|
||||
};
|
||||
@@ -3968,32 +3957,6 @@ export function getAlbumMapMarkers({ id, key, slug }: {
|
||||
...opts
|
||||
}));
|
||||
}
|
||||
/**
|
||||
* Get own sharing permissions
|
||||
*/
|
||||
export function getOwnAlbumUser({ id }: {
|
||||
id: string;
|
||||
}, opts?: Oazapfts.RequestOpts) {
|
||||
return oazapfts.ok(oazapfts.fetchJson<{
|
||||
status: 200;
|
||||
data: SharingOptionsResponseDto;
|
||||
}>(`/albums/${encodeURIComponent(id)}/user/self`, {
|
||||
...opts
|
||||
}));
|
||||
}
|
||||
/**
|
||||
* Update own sharing permissions
|
||||
*/
|
||||
export function updateOwnAlbumUser({ id, updateSharingOptionsDto }: {
|
||||
id: string;
|
||||
updateSharingOptionsDto: UpdateSharingOptionsDto;
|
||||
}, opts?: Oazapfts.RequestOpts) {
|
||||
return oazapfts.ok(oazapfts.fetchText(`/albums/${encodeURIComponent(id)}/user/self`, oazapfts.json({
|
||||
...opts,
|
||||
method: "PUT",
|
||||
body: updateSharingOptionsDto
|
||||
})));
|
||||
}
|
||||
/**
|
||||
* Remove user from album
|
||||
*/
|
||||
@@ -5482,9 +5445,9 @@ export function updatePerson({ id, personUpdateDto }: {
|
||||
/**
|
||||
* Merge people
|
||||
*/
|
||||
export function mergePerson({ id, mergeFaceClusterDto }: {
|
||||
export function mergePerson({ id, mergePersonDto }: {
|
||||
id: string;
|
||||
mergeFaceClusterDto: MergeFaceClusterDto;
|
||||
mergePersonDto: MergePersonDto;
|
||||
}, opts?: Oazapfts.RequestOpts) {
|
||||
return oazapfts.ok(oazapfts.fetchJson<{
|
||||
status: 200;
|
||||
@@ -5492,7 +5455,7 @@ export function mergePerson({ id, mergeFaceClusterDto }: {
|
||||
}>(`/people/${encodeURIComponent(id)}/merge`, oazapfts.json({
|
||||
...opts,
|
||||
method: "POST",
|
||||
body: mergeFaceClusterDto
|
||||
body: mergePersonDto
|
||||
})));
|
||||
}
|
||||
/**
|
||||
@@ -7178,19 +7141,6 @@ export enum BulkIdErrorReason {
|
||||
Unknown = "unknown",
|
||||
Validation = "validation"
|
||||
}
|
||||
export enum SharingPermission {
|
||||
All = "all",
|
||||
AssetRead = "asset.read",
|
||||
AssetUpdate = "asset.update",
|
||||
AssetEdit = "asset.edit",
|
||||
AssetDelete = "asset.delete",
|
||||
AssetShare = "asset.share",
|
||||
ExifRead = "exif.read",
|
||||
PersonRead = "person.read",
|
||||
PersonUpdate = "person.update",
|
||||
PersonMerge = "person.merge",
|
||||
PersonDelete = "person.delete"
|
||||
}
|
||||
export enum Permission {
|
||||
All = "all",
|
||||
ActivityCreate = "activity.create",
|
||||
@@ -7407,8 +7357,7 @@ export enum ManualJobName {
|
||||
IntegrityChecksumMismatchRefresh = "integrity-checksum-mismatch-refresh",
|
||||
IntegrityMissingFilesDeleteAll = "integrity-missing-files-delete-all",
|
||||
IntegrityUntrackedFilesDeleteAll = "integrity-untracked-files-delete-all",
|
||||
IntegrityChecksumMismatchDeleteAll = "integrity-checksum-mismatch-delete-all",
|
||||
PersonGroupMerge = "person-group-merge"
|
||||
IntegrityChecksumMismatchDeleteAll = "integrity-checksum-mismatch-delete-all"
|
||||
}
|
||||
export enum QueueName {
|
||||
ThumbnailGeneration = "thumbnailGeneration",
|
||||
@@ -7485,7 +7434,6 @@ export enum JobName {
|
||||
DatabaseBackup = "DatabaseBackup",
|
||||
FacialRecognitionQueueAll = "FacialRecognitionQueueAll",
|
||||
FacialRecognition = "FacialRecognition",
|
||||
FacialRecognitionMerge = "FacialRecognitionMerge",
|
||||
FileDelete = "FileDelete",
|
||||
FileMigrationQueueAll = "FileMigrationQueueAll",
|
||||
LibraryDeleteCheck = "LibraryDeleteCheck",
|
||||
|
||||
@@ -11,7 +11,6 @@ import {
|
||||
GetAlbumsDto,
|
||||
UpdateAlbumDto,
|
||||
UpdateAlbumUserDto,
|
||||
UpdateSharingPermissionsDto as UpdateSharingOptionsDto,
|
||||
} from 'src/dtos/album.dto';
|
||||
import { BulkIdResponseDto, BulkIdsDto } from 'src/dtos/asset-ids.response.dto';
|
||||
import { AuthDto } from 'src/dtos/auth.dto';
|
||||
@@ -166,33 +165,6 @@ export class AlbumController {
|
||||
return this.service.addUsers(auth, id, dto);
|
||||
}
|
||||
|
||||
@Get(':id/user/self')
|
||||
@Authenticated({ permission: Permission.AlbumAssetCreate })
|
||||
@Endpoint({
|
||||
summary: 'Get own sharing permissions',
|
||||
description: 'Get the own sharing permissions in a specific album.',
|
||||
history: new HistoryBuilder().added('v3').stable('v3'),
|
||||
})
|
||||
getOwnAlbumUser(@Auth() auth: AuthDto, @Param() { id }: UUIDParamDto) {
|
||||
return this.service.getSelf(auth, id);
|
||||
}
|
||||
|
||||
@Put(':id/user/self')
|
||||
@Authenticated({ permission: Permission.AlbumAssetCreate })
|
||||
@HttpCode(HttpStatus.NO_CONTENT)
|
||||
@Endpoint({
|
||||
summary: 'Update own sharing permissions',
|
||||
description: 'Change the own sharing permissions in a specific album.',
|
||||
history: new HistoryBuilder().added('v3').stable('v3'),
|
||||
})
|
||||
updateOwnAlbumUser(
|
||||
@Auth() auth: AuthDto,
|
||||
@Param() { id }: UUIDParamDto,
|
||||
@Body() dto: UpdateSharingOptionsDto,
|
||||
): Promise<void> {
|
||||
return this.service.updateSelf(auth, id, dto);
|
||||
}
|
||||
|
||||
@Put(':id/user/:userId')
|
||||
@Authenticated({ permission: Permission.AlbumUserUpdate })
|
||||
@HttpCode(HttpStatus.NO_CONTENT)
|
||||
|
||||
@@ -20,7 +20,7 @@ import { BulkIdResponseDto, BulkIdsDto } from 'src/dtos/asset-ids.response.dto';
|
||||
import { AuthDto } from 'src/dtos/auth.dto';
|
||||
import {
|
||||
AssetFaceUpdateDto,
|
||||
MergeFaceClusterDto,
|
||||
MergePersonDto,
|
||||
PeopleResponseDto,
|
||||
PeopleUpdateDto,
|
||||
PersonCreateDto,
|
||||
@@ -198,7 +198,7 @@ export class PersonController {
|
||||
mergePerson(
|
||||
@Auth() auth: AuthDto,
|
||||
@Param() { id }: UUIDParamDto,
|
||||
@Body() dto: MergeFaceClusterDto,
|
||||
@Body() dto: MergePersonDto,
|
||||
): Promise<BulkIdResponseDto[]> {
|
||||
return this.service.mergePerson(auth, id, dto);
|
||||
}
|
||||
|
||||
@@ -9,7 +9,6 @@ import {
|
||||
MemoryType,
|
||||
Permission,
|
||||
SharedLinkType,
|
||||
SharingPermission,
|
||||
SourceType,
|
||||
UserAvatarColor,
|
||||
UserStatus,
|
||||
@@ -210,7 +209,6 @@ export type Partner = {
|
||||
updatedAt: Date;
|
||||
updateId: string;
|
||||
inTimeline: boolean;
|
||||
permissions: SharingPermission[];
|
||||
};
|
||||
|
||||
export type Place = {
|
||||
@@ -254,7 +252,6 @@ export type Person = {
|
||||
faceAssetId: string | null;
|
||||
isHidden: boolean;
|
||||
thumbnailPath: string;
|
||||
faceClusterId: string | null;
|
||||
};
|
||||
|
||||
export type AssetFace = {
|
||||
@@ -267,7 +264,7 @@ export type AssetFace = {
|
||||
boundingBoxY2: number;
|
||||
imageHeight: number;
|
||||
imageWidth: number;
|
||||
faceClusterId: string | null;
|
||||
personId: string | null;
|
||||
sourceType: SourceType;
|
||||
person?: ShallowDehydrateObject<Person> | null;
|
||||
updatedAt: Date;
|
||||
|
||||
@@ -3,8 +3,8 @@ import { createZodDto } from 'nestjs-zod';
|
||||
import { AlbumUser, AuthSharedLink } from 'src/database';
|
||||
import { BulkIdErrorReasonSchema } from 'src/dtos/asset-ids.response.dto';
|
||||
import { MapAsset } from 'src/dtos/asset-response.dto';
|
||||
import { mapUser, UserResponseSchema } from 'src/dtos/user.dto';
|
||||
import { AlbumUserRole, AlbumUserRoleSchema, AssetOrder, AssetOrderSchema, SharingPermissionSchema } from 'src/enum';
|
||||
import { UserResponseSchema, mapUser } from 'src/dtos/user.dto';
|
||||
import { AlbumUserRole, AlbumUserRoleSchema, AssetOrder, AssetOrderSchema } from 'src/enum';
|
||||
import { MaybeDehydrated } from 'src/types';
|
||||
import { asDateTimeString } from 'src/utils/date';
|
||||
import { stringToBool } from 'src/validation';
|
||||
@@ -63,14 +63,6 @@ const UpdateAlbumSchema = z
|
||||
})
|
||||
.meta({ id: 'UpdateAlbumDto' });
|
||||
|
||||
const UpdateSharingOptionsSchema = z
|
||||
.object({ inTimeline: z.boolean(), permissions: z.array(SharingPermissionSchema) })
|
||||
.meta({ id: 'UpdateSharingOptionsDto' });
|
||||
|
||||
const SharingOptionsResponseSchema = z
|
||||
.object({ inTimeline: z.boolean(), permissions: z.array(SharingPermissionSchema) })
|
||||
.meta({ id: 'SharingOptionsResponseDto' });
|
||||
|
||||
const GetAlbumsSchema = z
|
||||
.object({
|
||||
id: z.uuidv4().optional().describe('Album ID'),
|
||||
@@ -157,8 +149,6 @@ export class UpdateAlbumDto extends createZodDto(UpdateAlbumSchema) {}
|
||||
export class GetAlbumsDto extends createZodDto(GetAlbumsSchema) {}
|
||||
export class AlbumStatisticsResponseDto extends createZodDto(AlbumStatisticsResponseSchema) {}
|
||||
export class UpdateAlbumUserDto extends createZodDto(UpdateAlbumUserSchema) {}
|
||||
export class UpdateSharingPermissionsDto extends createZodDto(UpdateSharingOptionsSchema) {}
|
||||
export class SharingPermissionsResponseDto extends createZodDto(SharingOptionsResponseSchema) {}
|
||||
export class AlbumResponseDto extends createZodDto(AlbumResponseSchema) {}
|
||||
class AlbumUserResponseDto extends createZodDto(AlbumUserResponseSchema) {}
|
||||
|
||||
|
||||
@@ -15,8 +15,6 @@ import {
|
||||
AssetVisibility,
|
||||
AssetVisibilitySchema,
|
||||
ChecksumAlgorithm,
|
||||
SharingPermission,
|
||||
SharingPermissionSchema,
|
||||
} from 'src/enum';
|
||||
import { MaybeDehydrated } from 'src/types';
|
||||
import { hexOrBufferToBase64 } from 'src/utils/bytes';
|
||||
@@ -47,7 +45,6 @@ const SanitizedAssetResponseSchema = z
|
||||
hasMetadata: z.boolean().describe('Whether asset has metadata'),
|
||||
width: z.int().min(0).nullable().describe('Asset width'),
|
||||
height: z.int().min(0).nullable().describe('Asset height'),
|
||||
permissions: z.array(SharingPermissionSchema),
|
||||
})
|
||||
.meta({ id: 'SanitizedAssetResponseDto' });
|
||||
|
||||
@@ -116,7 +113,6 @@ export const AssetResponseSchema = SanitizedAssetResponseSchema.extend(
|
||||
.boolean()
|
||||
.describe('Is edited')
|
||||
.meta(new HistoryBuilder().added('v2.5.0').beta('v2.5.0').getExtensions()),
|
||||
permissions: z.array(SharingPermissionSchema),
|
||||
}).shape,
|
||||
).meta({ id: 'AssetResponseDto' });
|
||||
|
||||
@@ -158,7 +154,6 @@ export type MapAsset = {
|
||||
width: number | null;
|
||||
height: number | null;
|
||||
isEdited: boolean;
|
||||
permissions?: { permission: SharingPermission }[];
|
||||
};
|
||||
|
||||
export type AssetMapOptions = {
|
||||
@@ -197,16 +192,8 @@ const mapStack = (entity: { stack?: Stack | null }) => {
|
||||
|
||||
export function mapAsset(entity: MaybeDehydrated<MapAsset>, options: AssetMapOptions = {}): AssetResponseDto {
|
||||
const { stripMetadata = false, withStack = false } = options;
|
||||
const permissions =
|
||||
options.auth?.user.id === entity.ownerId
|
||||
? [SharingPermission.All]
|
||||
: (entity.permissions?.map(({ permission }) => permission) ?? []);
|
||||
|
||||
if (
|
||||
stripMetadata ||
|
||||
(entity.permissions &&
|
||||
!(permissions.includes(SharingPermission.All) || permissions.includes(SharingPermission.ExifRead)))
|
||||
) {
|
||||
if (stripMetadata) {
|
||||
const sanitizedAssetResponse: SanitizedAssetResponseDto = {
|
||||
id: entity.id,
|
||||
type: entity.type,
|
||||
@@ -218,7 +205,6 @@ export function mapAsset(entity: MaybeDehydrated<MapAsset>, options: AssetMapOpt
|
||||
hasMetadata: false,
|
||||
width: entity.width,
|
||||
height: entity.height,
|
||||
permissions,
|
||||
};
|
||||
return sanitizedAssetResponse as AssetResponseDto;
|
||||
}
|
||||
@@ -256,6 +242,5 @@ export function mapAsset(entity: MaybeDehydrated<MapAsset>, options: AssetMapOpt
|
||||
width: entity.width,
|
||||
height: entity.height,
|
||||
isEdited: entity.isEdited,
|
||||
permissions,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ import { Selectable } from 'kysely';
|
||||
import { createZodDto } from 'nestjs-zod';
|
||||
import { AssetFace, Person } from 'src/database';
|
||||
import { HistoryBuilder } from 'src/decorators';
|
||||
import { AuthDto } from 'src/dtos/auth.dto';
|
||||
import { AssetEditActionItem } from 'src/dtos/editing.dto';
|
||||
import { SourceTypeSchema } from 'src/enum';
|
||||
import { AssetFaceTable } from 'src/schema/tables/asset-face.table';
|
||||
@@ -41,11 +42,11 @@ const PeopleUpdateSchema = z
|
||||
})
|
||||
.meta({ id: 'PeopleUpdateDto' });
|
||||
|
||||
const MergeFaceClusterSchema = z
|
||||
const MergePersonSchema = z
|
||||
.object({
|
||||
ids: z.array(z.uuidv4()).describe('Face cluster IDs to merge'),
|
||||
ids: z.array(z.uuidv4()).describe('Person IDs to merge'),
|
||||
})
|
||||
.meta({ id: 'MergeFaceClusterDto' });
|
||||
.meta({ id: 'MergePersonDto' });
|
||||
|
||||
const PersonSearchSchema = z
|
||||
.object({
|
||||
@@ -82,14 +83,13 @@ export const PersonResponseSchema = z
|
||||
.optional()
|
||||
.describe('Person color (hex)')
|
||||
.meta(new HistoryBuilder().added('v1.126.0').stable('v2').getExtensions()),
|
||||
faceClusterId: z.string().nullable().describe('Face cluster ID'),
|
||||
})
|
||||
.meta({ id: 'PersonResponseDto' });
|
||||
|
||||
export class PersonCreateDto extends createZodDto(PersonCreateSchema) {}
|
||||
export class PersonUpdateDto extends createZodDto(PersonUpdateSchema) {}
|
||||
export class PeopleUpdateDto extends createZodDto(PeopleUpdateSchema) {}
|
||||
export class MergeFaceClusterDto extends createZodDto(MergeFaceClusterSchema) {}
|
||||
export class MergePersonDto extends createZodDto(MergePersonSchema) {}
|
||||
export class PersonSearchDto extends createZodDto(PersonSearchSchema) {}
|
||||
export class PersonResponseDto extends createZodDto(PersonResponseSchema) {}
|
||||
|
||||
@@ -181,7 +181,6 @@ export function mapPerson(person: MaybeDehydrated<Person>): PersonResponseDto {
|
||||
isFavorite: person.isFavorite,
|
||||
color: person.color ?? undefined,
|
||||
updatedAt: asDateTimeString(person.updatedAt),
|
||||
faceClusterId: person.faceClusterId,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -210,11 +209,12 @@ function mapFacesWithoutPerson(
|
||||
|
||||
export function mapFaces(
|
||||
face: AssetFace,
|
||||
auth: AuthDto,
|
||||
edits?: AssetEditActionItem[],
|
||||
assetDimensions?: ImageDimensions,
|
||||
): AssetFaceResponseDto {
|
||||
return {
|
||||
...mapFacesWithoutPerson(face, edits, assetDimensions),
|
||||
person: face.person ? mapPerson(face.person) : null,
|
||||
person: face.person?.ownerId === auth.user.id ? mapPerson(face.person) : null,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -365,13 +365,10 @@ const SyncAssetFaceV1Schema = z
|
||||
})
|
||||
.meta({ id: 'SyncAssetFaceV1' });
|
||||
|
||||
const SyncAssetFaceV2Schema = SyncAssetFaceV1Schema.omit({ personId: true })
|
||||
.extend({
|
||||
deletedAt: isoDatetimeToDate.nullable().describe('Face deleted at'),
|
||||
isVisible: z.boolean().describe('Is the face visible in the asset'),
|
||||
faceClusterId: z.string().nullable().describe('Person ID'),
|
||||
})
|
||||
.meta({ id: 'SyncAssetFaceV2' });
|
||||
const SyncAssetFaceV2Schema = SyncAssetFaceV1Schema.extend({
|
||||
deletedAt: isoDatetimeToDate.nullable().describe('Face deleted at'),
|
||||
isVisible: z.boolean().describe('Is the face visible in the asset'),
|
||||
}).meta({ id: 'SyncAssetFaceV2' });
|
||||
|
||||
const SyncAssetFaceDeleteV1Schema = z
|
||||
.object({ assetFaceId: z.uuidv4().describe('Asset face ID') })
|
||||
|
||||
@@ -309,28 +309,6 @@ export enum Permission {
|
||||
AdminAuthUnlinkAll = 'adminAuth.unlinkAll',
|
||||
}
|
||||
|
||||
export enum SharingPermission {
|
||||
All = 'all',
|
||||
|
||||
AssetRead = 'asset.read',
|
||||
AssetUpdate = 'asset.update',
|
||||
AssetEdit = 'asset.edit',
|
||||
AssetDelete = 'asset.delete',
|
||||
AssetShare = 'asset.share',
|
||||
|
||||
ExifRead = 'exif.read',
|
||||
|
||||
PersonRead = 'person.read',
|
||||
PersonUpdate = 'person.update',
|
||||
PersonMerge = 'person.merge',
|
||||
PersonDelete = 'person.delete',
|
||||
}
|
||||
|
||||
export const SharingPermissionSchema = z
|
||||
.enum(SharingPermission)
|
||||
.describe('Sharing permission schema')
|
||||
.meta({ id: 'SharingPermission' });
|
||||
|
||||
export enum SharedLinkType {
|
||||
Album = 'ALBUM',
|
||||
|
||||
@@ -450,7 +428,6 @@ export enum ManualJobName {
|
||||
IntegrityMissingFilesDeleteAll = `integrity-missing-files-delete-all`,
|
||||
IntegrityUntrackedFilesDeleteAll = `integrity-untracked-files-delete-all`,
|
||||
IntegrityChecksumFilesDeleteAll = `integrity-checksum-mismatch-delete-all`,
|
||||
PersonGroupMerge = 'person-group-merge',
|
||||
}
|
||||
|
||||
export const ManualJobNameSchema = z.enum(ManualJobName).describe('Manual job name').meta({ id: 'ManualJobName' });
|
||||
@@ -857,7 +834,6 @@ export enum JobName {
|
||||
|
||||
FacialRecognitionQueueAll = 'FacialRecognitionQueueAll',
|
||||
FacialRecognition = 'FacialRecognition',
|
||||
FacialRecognitionMerge = 'FacialRecognitionMerge',
|
||||
|
||||
FileDelete = 'FileDelete',
|
||||
FileMigrationQueueAll = 'FileMigrationQueueAll',
|
||||
|
||||
@@ -149,40 +149,6 @@ where
|
||||
"albumAssets"."livePhotoVideoId"
|
||||
] && array[$2]::uuid[]
|
||||
|
||||
-- AccessRepository.asset.checkSharedAccess
|
||||
select
|
||||
"album_asset"."assetId"
|
||||
from
|
||||
"album_asset"
|
||||
inner join "album_user" on "album_asset"."albumId" = "album_user"."albumId"
|
||||
and "album_user"."userId" = $1
|
||||
where
|
||||
"album_asset"."assetId" in ($2)
|
||||
and "album_asset"."albumId" in (
|
||||
select
|
||||
"album_user"."albumId"
|
||||
from
|
||||
"album_user"
|
||||
where
|
||||
(
|
||||
"album_user"."permissions" @> $3::sharing_permission_enum[]
|
||||
or $4 = any ("album_user"."permissions")
|
||||
)
|
||||
)
|
||||
union
|
||||
select
|
||||
"asset"."id" as "assetId"
|
||||
from
|
||||
"partner"
|
||||
inner join "asset" on "asset"."ownerId" = "partner"."sharedById"
|
||||
and "asset"."id" in ($5)
|
||||
where
|
||||
"partner"."sharedWithId" = $6
|
||||
and (
|
||||
"partner"."permissions" @> $7::sharing_permission_enum[]
|
||||
or $8 = any ("partner"."permissions")
|
||||
)
|
||||
|
||||
-- AccessRepository.authDevice.checkOwnerAccess
|
||||
select
|
||||
"session"."id"
|
||||
|
||||
@@ -182,25 +182,18 @@ select
|
||||
from
|
||||
(
|
||||
select
|
||||
(
|
||||
select
|
||||
to_json(obj)
|
||||
from
|
||||
(
|
||||
select
|
||||
"person".*
|
||||
from
|
||||
"face_cluster"
|
||||
inner join "person" on "person"."faceClusterId" = "face_cluster"."id"
|
||||
where
|
||||
"face_cluster"."id" = "asset_face"."faceClusterId"
|
||||
limit
|
||||
$1
|
||||
) as obj
|
||||
) as "person",
|
||||
"asset_face".*
|
||||
"asset_face".*,
|
||||
"person" as "person"
|
||||
from
|
||||
"asset_face"
|
||||
left join lateral (
|
||||
select
|
||||
"person".*
|
||||
from
|
||||
"person"
|
||||
where
|
||||
"asset_face"."personId" = "person"."id"
|
||||
) as "person" on true
|
||||
where
|
||||
"asset_face"."assetId" = "asset"."id"
|
||||
and "asset_face"."deletedAt" is null
|
||||
@@ -231,7 +224,7 @@ from
|
||||
"asset"
|
||||
left join "asset_exif" on "asset"."id" = "asset_exif"."assetId"
|
||||
where
|
||||
"asset"."id" = any ($2::uuid[])
|
||||
"asset"."id" = any ($1::uuid[])
|
||||
|
||||
-- AssetRepository.deleteAll
|
||||
delete from "asset"
|
||||
@@ -297,44 +290,13 @@ limit
|
||||
|
||||
-- AssetRepository.getById
|
||||
select
|
||||
"asset".*,
|
||||
(
|
||||
select
|
||||
coalesce(json_agg(agg), '[]')
|
||||
from
|
||||
(
|
||||
select distinct
|
||||
unnest("album_user"."permissions") as "permission"
|
||||
from
|
||||
"album_user"
|
||||
inner join "album_asset" on "album_user"."albumId" = "album_asset"."albumId"
|
||||
where
|
||||
"album_asset"."assetId" = "asset"."id"
|
||||
and "album_user"."userId" = "asset"."ownerId"
|
||||
and "album_user"."albumId" in (
|
||||
select
|
||||
"album_user"."albumId"
|
||||
from
|
||||
"album_user"
|
||||
where
|
||||
"album_user"."userId" = $1
|
||||
)
|
||||
union
|
||||
select distinct
|
||||
unnest("partner"."permissions") as "permission"
|
||||
from
|
||||
"partner"
|
||||
where
|
||||
"partner"."sharedById" = "asset"."ownerId"
|
||||
and "partner"."sharedWithId" = $2
|
||||
) as agg
|
||||
) as "permissions"
|
||||
"asset".*
|
||||
from
|
||||
"asset"
|
||||
where
|
||||
"asset"."id" = $3::uuid
|
||||
"asset"."id" = $1::uuid
|
||||
limit
|
||||
$4
|
||||
$2
|
||||
|
||||
-- AssetRepository.updateAll
|
||||
update "asset"
|
||||
|
||||
@@ -47,7 +47,7 @@ select
|
||||
$1 as "one"
|
||||
from
|
||||
"asset_face"
|
||||
inner join "person" on "person"."faceClusterId" = "asset_face"."faceClusterId"
|
||||
inner join "person" on "person"."id" = "asset_face"."personId"
|
||||
where
|
||||
"asset_face"."assetId" = "asset"."id"
|
||||
and "person"."isHidden" = $2
|
||||
@@ -86,7 +86,7 @@ select
|
||||
$1 as "one"
|
||||
from
|
||||
"asset_face"
|
||||
inner join "person" on "person"."faceClusterId" = "asset_face"."faceClusterId"
|
||||
inner join "person" on "person"."id" = "asset_face"."personId"
|
||||
where
|
||||
"asset_face"."assetId" = "asset"."id"
|
||||
and "person"."isHidden" = $2
|
||||
|
||||
@@ -3,6 +3,9 @@
|
||||
-- PersonRepository.reassignFaces
|
||||
update "asset_face"
|
||||
set
|
||||
"personId" = $1
|
||||
where
|
||||
"asset_face"."personId" = $2
|
||||
|
||||
-- PersonRepository.delete
|
||||
delete from "person"
|
||||
@@ -21,59 +24,24 @@ limit
|
||||
3
|
||||
|
||||
-- PersonRepository.getAllForUser
|
||||
select distinct
|
||||
on ("person"."faceClusterId") "person".*
|
||||
select
|
||||
"person".*
|
||||
from
|
||||
"person"
|
||||
inner join "asset_face" on "asset_face"."faceClusterId" = "person"."faceClusterId"
|
||||
inner join "asset_face" on "asset_face"."personId" = "person"."id"
|
||||
inner join "asset" on "asset_face"."assetId" = "asset"."id"
|
||||
and "asset"."visibility" = 'timeline'
|
||||
and "asset"."deletedAt" is null
|
||||
where
|
||||
(
|
||||
"person"."ownerId" = $1
|
||||
or (
|
||||
exists (
|
||||
select
|
||||
from
|
||||
"partner"
|
||||
where
|
||||
"partner"."sharedById" = "person"."ownerId"
|
||||
and "partner"."sharedWithId" = $2
|
||||
and (
|
||||
$3 = any ("partner"."permissions")
|
||||
or "partner"."permissions" @> $4
|
||||
)
|
||||
)
|
||||
or exists (
|
||||
select
|
||||
from
|
||||
"album_user"
|
||||
where
|
||||
"album_user"."albumId" in (
|
||||
select
|
||||
"album_user"."albumId"
|
||||
from
|
||||
"album_user"
|
||||
where
|
||||
"album_user"."userId" = $5
|
||||
)
|
||||
and "album_user"."userId" = "person"."ownerId"
|
||||
and (
|
||||
$6 = any ("album_user"."permissions")
|
||||
or "album_user"."permissions" @> $7
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
"person"."ownerId" = $1
|
||||
and "asset_face"."deletedAt" is null
|
||||
and "asset_face"."isVisible" is true
|
||||
and "person"."isHidden" = $8
|
||||
and "person"."isHidden" = $2
|
||||
group by
|
||||
"person"."id"
|
||||
having
|
||||
(
|
||||
"person"."name" != $9
|
||||
"person"."name" != $3
|
||||
or count("asset_face"."assetId") >= COALESCE(
|
||||
(
|
||||
SELECT
|
||||
@@ -81,15 +49,13 @@ having
|
||||
FROM
|
||||
user_metadata
|
||||
WHERE
|
||||
"userId" = $10
|
||||
"userId" = $4
|
||||
AND key = 'preferences'
|
||||
),
|
||||
'3'
|
||||
)::int
|
||||
)
|
||||
order by
|
||||
"person"."faceClusterId",
|
||||
"person"."ownerId" = $11 desc,
|
||||
"person"."isHidden" asc,
|
||||
"person"."isFavorite" desc,
|
||||
NULLIF(person.name, '') is null asc,
|
||||
@@ -97,16 +63,16 @@ order by
|
||||
NULLIF(person.name, '') asc nulls last,
|
||||
"person"."createdAt"
|
||||
limit
|
||||
$12
|
||||
$5
|
||||
offset
|
||||
$13
|
||||
$6
|
||||
|
||||
-- PersonRepository.getAllWithoutFaces
|
||||
select
|
||||
"person".*
|
||||
from
|
||||
"person"
|
||||
left join "asset_face" on "asset_face"."faceClusterId" = "person"."faceClusterId"
|
||||
left join "asset_face" on "asset_face"."personId" = "person"."id"
|
||||
where
|
||||
"asset_face"."deletedAt" is null
|
||||
and "asset_face"."isVisible" is true
|
||||
@@ -128,26 +94,15 @@ select
|
||||
from
|
||||
"person"
|
||||
where
|
||||
"person"."faceClusterId" = "asset_face"."faceClusterId"
|
||||
order by
|
||||
"person"."ownerId" = (
|
||||
select
|
||||
"asset"."ownerId"
|
||||
from
|
||||
"asset"
|
||||
where
|
||||
"asset"."id" = "asset_face"."assetId"
|
||||
) desc
|
||||
limit
|
||||
$1
|
||||
"person"."id" = "asset_face"."personId"
|
||||
) as obj
|
||||
) as "person"
|
||||
from
|
||||
"asset_face"
|
||||
where
|
||||
"asset_face"."assetId" = $2
|
||||
"asset_face"."assetId" = $1
|
||||
and "asset_face"."deletedAt" is null
|
||||
and "asset_face"."isVisible" = $3
|
||||
and "asset_face"."isVisible" = $2
|
||||
order by
|
||||
"asset_face"."boundingBoxX1" asc
|
||||
|
||||
@@ -164,30 +119,19 @@ select
|
||||
from
|
||||
"person"
|
||||
where
|
||||
"person"."faceClusterId" = "asset_face"."faceClusterId"
|
||||
order by
|
||||
"person"."ownerId" = (
|
||||
select
|
||||
"asset"."ownerId"
|
||||
from
|
||||
"asset"
|
||||
where
|
||||
"asset"."id" = "asset_face"."assetId"
|
||||
) desc
|
||||
limit
|
||||
$1
|
||||
"person"."id" = "asset_face"."personId"
|
||||
) as obj
|
||||
) as "person"
|
||||
from
|
||||
"asset_face"
|
||||
where
|
||||
"asset_face"."id" = $2
|
||||
"asset_face"."id" = $1
|
||||
and "asset_face"."deletedAt" is null
|
||||
|
||||
-- PersonRepository.getFaceForFacialRecognitionJob
|
||||
select
|
||||
"asset_face"."id",
|
||||
"asset_face"."faceClusterId",
|
||||
"asset_face"."personId",
|
||||
"asset_face"."sourceType",
|
||||
(
|
||||
select
|
||||
@@ -257,7 +201,7 @@ where
|
||||
-- PersonRepository.reassignFace
|
||||
update "asset_face"
|
||||
set
|
||||
"faceClusterId" = $1
|
||||
"personId" = $1
|
||||
where
|
||||
"asset_face"."id" = $2
|
||||
|
||||
@@ -276,10 +220,9 @@ where
|
||||
"person"."ownerId" = $1
|
||||
and f_unaccent ("person"."name") %> f_unaccent ($2)
|
||||
order by
|
||||
f_unaccent ("person"."name") <->>> f_unaccent ($3),
|
||||
"person"."ownerId" = $4 desc
|
||||
f_unaccent ("person"."name") <->>> f_unaccent ($3)
|
||||
limit
|
||||
$5
|
||||
$4
|
||||
|
||||
-- PersonRepository.getDistinctNames
|
||||
select distinct
|
||||
@@ -302,52 +245,9 @@ from
|
||||
and "asset"."visibility" = 'timeline'
|
||||
and "asset"."deletedAt" is null
|
||||
where
|
||||
(
|
||||
"asset"."ownerId" = $1
|
||||
or exists (
|
||||
select
|
||||
from
|
||||
"partner"
|
||||
where
|
||||
"partner"."sharedById" = "asset"."ownerId"
|
||||
and "partner"."sharedWithId" = $2
|
||||
and (
|
||||
$3 = any ("partner"."permissions")
|
||||
or "partner"."permissions" @> $4
|
||||
)
|
||||
)
|
||||
or exists (
|
||||
select
|
||||
from
|
||||
"album_asset"
|
||||
inner join "album_user" on "album_user"."albumId" = "album_asset"."albumId"
|
||||
and "album_user"."userId" = $5
|
||||
where
|
||||
"album_asset"."assetId" = "asset"."id"
|
||||
and "album_user"."albumId" in (
|
||||
select
|
||||
"album_user"."albumId"
|
||||
from
|
||||
"album_user"
|
||||
where
|
||||
"album_user"."userId" = "asset"."ownerId"
|
||||
and (
|
||||
$6 = any ("album_user"."permissions")
|
||||
or "album_user"."permissions" @> $7
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
and "asset_face"."deletedAt" is null
|
||||
"asset_face"."deletedAt" is null
|
||||
and "asset_face"."isVisible" is true
|
||||
and "asset_face"."faceClusterId" = (
|
||||
select
|
||||
"person"."faceClusterId"
|
||||
from
|
||||
"person"
|
||||
where
|
||||
"person"."id" = $8
|
||||
)
|
||||
and "asset_face"."personId" = $1
|
||||
|
||||
-- PersonRepository.getNumberOfPeople
|
||||
select
|
||||
@@ -367,7 +267,7 @@ where
|
||||
from
|
||||
"asset_face"
|
||||
where
|
||||
"asset_face"."faceClusterId" = "person"."faceClusterId"
|
||||
"asset_face"."personId" = "person"."id"
|
||||
and "asset_face"."deletedAt" is null
|
||||
and "asset_face"."isVisible" = $2
|
||||
and exists (
|
||||
@@ -380,42 +280,7 @@ where
|
||||
and "asset"."deletedAt" is null
|
||||
)
|
||||
)
|
||||
and (
|
||||
"person"."ownerId" = $3
|
||||
or (
|
||||
exists (
|
||||
select
|
||||
from
|
||||
"partner"
|
||||
where
|
||||
"partner"."sharedById" = "person"."ownerId"
|
||||
and "partner"."sharedWithId" = $4
|
||||
and (
|
||||
$5 = any ("partner"."permissions")
|
||||
or "partner"."permissions" @> $6
|
||||
)
|
||||
)
|
||||
or exists (
|
||||
select
|
||||
from
|
||||
"album_user"
|
||||
where
|
||||
"album_user"."albumId" in (
|
||||
select
|
||||
"album_user"."albumId"
|
||||
from
|
||||
"album_user"
|
||||
where
|
||||
"album_user"."userId" = $7
|
||||
)
|
||||
and "album_user"."userId" = "person"."ownerId"
|
||||
and (
|
||||
$8 = any ("album_user"."permissions")
|
||||
or "album_user"."permissions" @> $9
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
and "person"."ownerId" = $3
|
||||
|
||||
-- PersonRepository.refreshFaces
|
||||
with
|
||||
@@ -445,26 +310,14 @@ select
|
||||
from
|
||||
"person"
|
||||
where
|
||||
"person"."faceClusterId" = "asset_face"."faceClusterId"
|
||||
order by
|
||||
"person"."ownerId" = (
|
||||
select
|
||||
"asset"."ownerId"
|
||||
from
|
||||
"asset"
|
||||
where
|
||||
"asset"."id" = "asset_face"."assetId"
|
||||
) desc
|
||||
limit
|
||||
$1
|
||||
"person"."id" = "asset_face"."personId"
|
||||
) as obj
|
||||
) as "person"
|
||||
from
|
||||
"asset_face"
|
||||
inner join "person" on "person"."faceClusterId" = "asset_face"."faceClusterId"
|
||||
where
|
||||
"person"."id" in ($2)
|
||||
and "asset_face"."assetId" in ($3)
|
||||
"asset_face"."assetId" in ($1)
|
||||
and "asset_face"."personId" in ($2)
|
||||
and "asset_face"."deletedAt" is null
|
||||
|
||||
-- PersonRepository.getRandomFace
|
||||
@@ -472,52 +325,8 @@ select
|
||||
"asset_face".*
|
||||
from
|
||||
"asset_face"
|
||||
inner join "person" on "asset_face"."faceClusterId" = "person"."faceClusterId"
|
||||
and "person"."id" = $1
|
||||
where
|
||||
"asset_face"."assetId" in (
|
||||
select
|
||||
"asset"."id"
|
||||
from
|
||||
"asset"
|
||||
where
|
||||
(
|
||||
"asset"."ownerId" = "person"."ownerId"
|
||||
or exists (
|
||||
select
|
||||
from
|
||||
"partner"
|
||||
where
|
||||
"partner"."sharedById" = "asset"."ownerId"
|
||||
and "partner"."sharedWithId" = "person"."ownerId"
|
||||
and (
|
||||
$2 = any ("partner"."permissions")
|
||||
or "partner"."permissions" @> $3
|
||||
)
|
||||
)
|
||||
or exists (
|
||||
select
|
||||
from
|
||||
"album_asset"
|
||||
inner join "album_user" on "album_user"."albumId" = "album_asset"."albumId"
|
||||
and "album_user"."userId" = "person"."ownerId"
|
||||
where
|
||||
"album_asset"."assetId" = "asset"."id"
|
||||
and "album_user"."albumId" in (
|
||||
select
|
||||
"album_user"."albumId"
|
||||
from
|
||||
"album_user"
|
||||
where
|
||||
"album_user"."userId" = "asset"."ownerId"
|
||||
and (
|
||||
$4 = any ("album_user"."permissions")
|
||||
or "album_user"."permissions" @> $5
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
"asset_face"."personId" = $1
|
||||
and "asset_face"."deletedAt" is null
|
||||
and "asset_face"."isVisible" is true
|
||||
|
||||
@@ -553,9 +362,8 @@ select
|
||||
"asset_face"."id"
|
||||
from
|
||||
"asset_face"
|
||||
inner join "person" on "person"."faceClusterId" = "asset_face"."faceClusterId"
|
||||
and "person"."id" = $1
|
||||
inner join "asset" on "asset"."id" = "asset_face"."assetId"
|
||||
and "asset"."isOffline" = $2
|
||||
and "asset"."isOffline" = $1
|
||||
where
|
||||
"asset_face"."assetId" = $3
|
||||
"asset_face"."assetId" = $2
|
||||
and "asset_face"."personId" = $3
|
||||
|
||||
@@ -10,52 +10,15 @@ where
|
||||
"asset"."visibility" = $1
|
||||
and "asset"."fileCreatedAt" >= $2
|
||||
and "asset_exif"."lensModel" = $3
|
||||
and (
|
||||
"asset"."ownerId" = $4
|
||||
or exists (
|
||||
select
|
||||
from
|
||||
"partner"
|
||||
where
|
||||
"partner"."sharedById" = "asset"."ownerId"
|
||||
and "partner"."sharedWithId" = $5
|
||||
and (
|
||||
$6 = any ("partner"."permissions")
|
||||
or "partner"."permissions" @> $7
|
||||
)
|
||||
and "partner"."inTimeline" = $8
|
||||
)
|
||||
or exists (
|
||||
select
|
||||
from
|
||||
"album_asset"
|
||||
inner join "album_user" on "album_user"."albumId" = "album_asset"."albumId"
|
||||
and "album_user"."userId" = $9
|
||||
where
|
||||
"album_asset"."assetId" = "asset"."id"
|
||||
and "album_user"."inTimeline" = $10
|
||||
and "album_user"."albumId" in (
|
||||
select
|
||||
"album_user"."albumId"
|
||||
from
|
||||
"album_user"
|
||||
where
|
||||
"album_user"."userId" = "asset"."ownerId"
|
||||
and (
|
||||
$11 = any ("album_user"."permissions")
|
||||
or "album_user"."permissions" @> $12
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
and "asset"."isFavorite" = $13
|
||||
and "asset"."ownerId" = any ($4::uuid[])
|
||||
and "asset"."isFavorite" = $5
|
||||
and "asset"."deletedAt" is null
|
||||
order by
|
||||
"asset"."fileCreatedAt" desc
|
||||
limit
|
||||
$14
|
||||
$6
|
||||
offset
|
||||
$15
|
||||
$7
|
||||
|
||||
-- SearchRepository.searchStatistics
|
||||
select
|
||||
@@ -67,45 +30,8 @@ where
|
||||
"asset"."visibility" = $1
|
||||
and "asset"."fileCreatedAt" >= $2
|
||||
and "asset_exif"."lensModel" = $3
|
||||
and (
|
||||
"asset"."ownerId" = $4
|
||||
or exists (
|
||||
select
|
||||
from
|
||||
"partner"
|
||||
where
|
||||
"partner"."sharedById" = "asset"."ownerId"
|
||||
and "partner"."sharedWithId" = $5
|
||||
and (
|
||||
$6 = any ("partner"."permissions")
|
||||
or "partner"."permissions" @> $7
|
||||
)
|
||||
and "partner"."inTimeline" = $8
|
||||
)
|
||||
or exists (
|
||||
select
|
||||
from
|
||||
"album_asset"
|
||||
inner join "album_user" on "album_user"."albumId" = "album_asset"."albumId"
|
||||
and "album_user"."userId" = $9
|
||||
where
|
||||
"album_asset"."assetId" = "asset"."id"
|
||||
and "album_user"."inTimeline" = $10
|
||||
and "album_user"."albumId" in (
|
||||
select
|
||||
"album_user"."albumId"
|
||||
from
|
||||
"album_user"
|
||||
where
|
||||
"album_user"."userId" = "asset"."ownerId"
|
||||
and (
|
||||
$11 = any ("album_user"."permissions")
|
||||
or "album_user"."permissions" @> $12
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
and "asset"."isFavorite" = $13
|
||||
and "asset"."ownerId" = any ($4::uuid[])
|
||||
and "asset"."isFavorite" = $5
|
||||
and "asset"."deletedAt" is null
|
||||
|
||||
-- SearchRepository.searchRandom
|
||||
@@ -118,50 +44,13 @@ where
|
||||
"asset"."visibility" = $1
|
||||
and "asset"."fileCreatedAt" >= $2
|
||||
and "asset_exif"."lensModel" = $3
|
||||
and (
|
||||
"asset"."ownerId" = $4
|
||||
or exists (
|
||||
select
|
||||
from
|
||||
"partner"
|
||||
where
|
||||
"partner"."sharedById" = "asset"."ownerId"
|
||||
and "partner"."sharedWithId" = $5
|
||||
and (
|
||||
$6 = any ("partner"."permissions")
|
||||
or "partner"."permissions" @> $7
|
||||
)
|
||||
and "partner"."inTimeline" = $8
|
||||
)
|
||||
or exists (
|
||||
select
|
||||
from
|
||||
"album_asset"
|
||||
inner join "album_user" on "album_user"."albumId" = "album_asset"."albumId"
|
||||
and "album_user"."userId" = $9
|
||||
where
|
||||
"album_asset"."assetId" = "asset"."id"
|
||||
and "album_user"."inTimeline" = $10
|
||||
and "album_user"."albumId" in (
|
||||
select
|
||||
"album_user"."albumId"
|
||||
from
|
||||
"album_user"
|
||||
where
|
||||
"album_user"."userId" = "asset"."ownerId"
|
||||
and (
|
||||
$11 = any ("album_user"."permissions")
|
||||
or "album_user"."permissions" @> $12
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
and "asset"."isFavorite" = $13
|
||||
and "asset"."ownerId" = any ($4::uuid[])
|
||||
and "asset"."isFavorite" = $5
|
||||
and "asset"."deletedAt" is null
|
||||
order by
|
||||
random()
|
||||
limit
|
||||
$14
|
||||
$6
|
||||
|
||||
-- SearchRepository.searchLargeAssets
|
||||
select
|
||||
@@ -174,51 +63,14 @@ where
|
||||
"asset"."visibility" = $1
|
||||
and "asset"."fileCreatedAt" >= $2
|
||||
and "asset_exif"."lensModel" = $3
|
||||
and (
|
||||
"asset"."ownerId" = $4
|
||||
or exists (
|
||||
select
|
||||
from
|
||||
"partner"
|
||||
where
|
||||
"partner"."sharedById" = "asset"."ownerId"
|
||||
and "partner"."sharedWithId" = $5
|
||||
and (
|
||||
$6 = any ("partner"."permissions")
|
||||
or "partner"."permissions" @> $7
|
||||
)
|
||||
and "partner"."inTimeline" = $8
|
||||
)
|
||||
or exists (
|
||||
select
|
||||
from
|
||||
"album_asset"
|
||||
inner join "album_user" on "album_user"."albumId" = "album_asset"."albumId"
|
||||
and "album_user"."userId" = $9
|
||||
where
|
||||
"album_asset"."assetId" = "asset"."id"
|
||||
and "album_user"."inTimeline" = $10
|
||||
and "album_user"."albumId" in (
|
||||
select
|
||||
"album_user"."albumId"
|
||||
from
|
||||
"album_user"
|
||||
where
|
||||
"album_user"."userId" = "asset"."ownerId"
|
||||
and (
|
||||
$11 = any ("album_user"."permissions")
|
||||
or "album_user"."permissions" @> $12
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
and "asset"."isFavorite" = $13
|
||||
and "asset"."ownerId" = any ($4::uuid[])
|
||||
and "asset"."isFavorite" = $5
|
||||
and "asset"."deletedAt" is null
|
||||
and "asset_exif"."fileSizeInByte" > $14
|
||||
and "asset_exif"."fileSizeInByte" > $6
|
||||
order by
|
||||
"asset_exif"."fileSizeInByte" desc
|
||||
limit
|
||||
$15
|
||||
$7
|
||||
|
||||
-- SearchRepository.searchSmart
|
||||
begin
|
||||
@@ -234,52 +86,15 @@ where
|
||||
"asset"."visibility" = $1
|
||||
and "asset"."fileCreatedAt" >= $2
|
||||
and "asset_exif"."lensModel" = $3
|
||||
and (
|
||||
"asset"."ownerId" = $4
|
||||
or exists (
|
||||
select
|
||||
from
|
||||
"partner"
|
||||
where
|
||||
"partner"."sharedById" = "asset"."ownerId"
|
||||
and "partner"."sharedWithId" = $5
|
||||
and (
|
||||
$6 = any ("partner"."permissions")
|
||||
or "partner"."permissions" @> $7
|
||||
)
|
||||
and "partner"."inTimeline" = $8
|
||||
)
|
||||
or exists (
|
||||
select
|
||||
from
|
||||
"album_asset"
|
||||
inner join "album_user" on "album_user"."albumId" = "album_asset"."albumId"
|
||||
and "album_user"."userId" = $9
|
||||
where
|
||||
"album_asset"."assetId" = "asset"."id"
|
||||
and "album_user"."inTimeline" = $10
|
||||
and "album_user"."albumId" in (
|
||||
select
|
||||
"album_user"."albumId"
|
||||
from
|
||||
"album_user"
|
||||
where
|
||||
"album_user"."userId" = "asset"."ownerId"
|
||||
and (
|
||||
$11 = any ("album_user"."permissions")
|
||||
or "album_user"."permissions" @> $12
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
and "asset"."isFavorite" = $13
|
||||
and "asset"."ownerId" = any ($4::uuid[])
|
||||
and "asset"."isFavorite" = $5
|
||||
and "asset"."deletedAt" is null
|
||||
order by
|
||||
smart_search.embedding <=> $14
|
||||
smart_search.embedding <=> $6
|
||||
limit
|
||||
$15
|
||||
$7
|
||||
offset
|
||||
$16
|
||||
$8
|
||||
commit
|
||||
|
||||
-- SearchRepository.getEmbedding
|
||||
@@ -298,30 +113,15 @@ with
|
||||
"cte" as (
|
||||
select
|
||||
"asset_face"."id",
|
||||
"asset_face"."faceClusterId",
|
||||
face_search.embedding <=> $1 as "distance",
|
||||
"asset"."ownerId"
|
||||
"asset_face"."personId",
|
||||
face_search.embedding <=> $1 as "distance"
|
||||
from
|
||||
"asset_face"
|
||||
inner join "asset" on "asset"."id" = "asset_face"."assetId"
|
||||
inner join "face_search" on "face_search"."faceId" = "asset_face"."id"
|
||||
left join "person" on "person"."faceClusterId" = "asset_face"."faceClusterId"
|
||||
left join "person" on "person"."id" = "asset_face"."personId"
|
||||
where
|
||||
"asset"."ownerId" in (
|
||||
select
|
||||
"user"."id"
|
||||
from
|
||||
"user"
|
||||
where
|
||||
"user"."trustedGroupId" in (
|
||||
select
|
||||
"user"."trustedGroupId"
|
||||
from
|
||||
"user"
|
||||
where
|
||||
"user"."id" = any ($2::uuid[])
|
||||
)
|
||||
)
|
||||
"asset"."ownerId" = any ($2::uuid[])
|
||||
and "asset"."deletedAt" is null
|
||||
order by
|
||||
"distance"
|
||||
|
||||
@@ -536,7 +536,7 @@ order by
|
||||
select
|
||||
"asset_face"."id",
|
||||
"assetId",
|
||||
"faceClusterId",
|
||||
"personId",
|
||||
"imageWidth",
|
||||
"imageHeight",
|
||||
"boundingBoxX1",
|
||||
|
||||
@@ -397,73 +397,3 @@ set
|
||||
where
|
||||
"user"."deletedAt" is null
|
||||
and "user"."id" = $2::uuid
|
||||
|
||||
-- UserRepository.getInSameTrustedGroup
|
||||
select
|
||||
"user"."id"
|
||||
from
|
||||
"user"
|
||||
where
|
||||
"user"."trustedGroupId" = (
|
||||
select
|
||||
"user"."trustedGroupId"
|
||||
from
|
||||
"user"
|
||||
where
|
||||
"user"."id" = $1
|
||||
)
|
||||
|
||||
-- UserRepository.mergeTrustedGroups
|
||||
update "user"
|
||||
set
|
||||
"trustedGroupId" = "u"."trustedGroupId"
|
||||
from
|
||||
"user" as "u"
|
||||
where
|
||||
"u"."id" = $1
|
||||
and "user"."trustedGroupId" = (
|
||||
select
|
||||
"user"."trustedGroupId"
|
||||
from
|
||||
"user"
|
||||
where
|
||||
"user"."id" = $2
|
||||
and "user"."trustedGroupId" != "u"."trustedGroupId"
|
||||
)
|
||||
|
||||
-- UserRepository.updateTrustedGroups
|
||||
update "user"
|
||||
set
|
||||
"trustedGroupId" = uuid_generate_v4 ()
|
||||
where
|
||||
"user"."trustedGroupId" = (
|
||||
select
|
||||
"user"."trustedGroupId"
|
||||
from
|
||||
"user"
|
||||
where
|
||||
"user"."id" = $1
|
||||
)
|
||||
and "user"."id" != $2
|
||||
and "user"."id" not in (
|
||||
select
|
||||
"partner"."sharedById" as "userId"
|
||||
from
|
||||
"partner"
|
||||
where
|
||||
"sharedWithId" = $3
|
||||
union
|
||||
select
|
||||
"album_user"."userId"
|
||||
from
|
||||
"album_user"
|
||||
where
|
||||
"album_user"."albumId" in (
|
||||
select
|
||||
"album_user"."albumId"
|
||||
from
|
||||
"album_user"
|
||||
where
|
||||
"album_user"."userId" = $4
|
||||
)
|
||||
)
|
||||
|
||||
@@ -2,9 +2,7 @@ import { Injectable } from '@nestjs/common';
|
||||
import { Kysely, NotNull, sql } from 'kysely';
|
||||
import { InjectKysely } from 'nestjs-kysely';
|
||||
import { ChunkedSet, DummyValue, GenerateSql } from 'src/decorators';
|
||||
import { AlbumUserRole, AssetVisibility, SharingPermission } from 'src/enum';
|
||||
import { hasAssetPermissions } from 'src/repositories/asset.repository';
|
||||
import { hasPermissions } from 'src/repositories/person.repository';
|
||||
import { AlbumUserRole, AssetVisibility } from 'src/enum';
|
||||
import { DB } from 'src/schema';
|
||||
import { asUuid } from 'src/utils/database';
|
||||
|
||||
@@ -275,46 +273,6 @@ class AssetAccess {
|
||||
return allowedIds;
|
||||
});
|
||||
}
|
||||
|
||||
@GenerateSql({ params: [DummyValue.UUID, DummyValue.UUID_SET, [SharingPermission.All]] })
|
||||
async checkSharedAccess(userId: string, assetIds: Set<string>, permissions: SharingPermission[]) {
|
||||
const ids = await this.db
|
||||
.selectFrom('album_asset')
|
||||
.select('album_asset.assetId')
|
||||
.where('album_asset.assetId', 'in', [...assetIds])
|
||||
.where('album_asset.albumId', 'in', (eb) =>
|
||||
eb
|
||||
.selectFrom('album_user')
|
||||
.select('album_user.albumId')
|
||||
.where((eb) =>
|
||||
eb.or([
|
||||
eb('album_user.permissions', '@>', sql<SharingPermission[]>`${permissions}::sharing_permission_enum[]`),
|
||||
eb(eb.val(SharingPermission.All), '=', eb.fn.any('album_user.permissions')),
|
||||
]),
|
||||
),
|
||||
)
|
||||
.innerJoin('album_user', (join) =>
|
||||
join.onRef('album_asset.albumId', '=', 'album_user.albumId').on('album_user.userId', '=', userId),
|
||||
)
|
||||
.union((eb) =>
|
||||
eb
|
||||
.selectFrom('partner')
|
||||
.where('partner.sharedWithId', '=', userId)
|
||||
.where((eb) =>
|
||||
eb.or([
|
||||
eb('partner.permissions', '@>', sql<SharingPermission[]>`${permissions}::sharing_permission_enum[]`),
|
||||
eb(eb.val(SharingPermission.All), '=', eb.fn.any('partner.permissions')),
|
||||
]),
|
||||
)
|
||||
.innerJoin('asset', (join) =>
|
||||
join.onRef('asset.ownerId', '=', 'partner.sharedById').on('asset.id', 'in', [...assetIds]),
|
||||
)
|
||||
.select('asset.id as assetId'),
|
||||
)
|
||||
.execute();
|
||||
|
||||
return new Set(ids.map(({ assetId }) => assetId));
|
||||
}
|
||||
}
|
||||
|
||||
class AuthDeviceAccess {
|
||||
@@ -494,37 +452,6 @@ class PersonAccess {
|
||||
.execute()
|
||||
.then((faces) => new Set(faces.map((face) => face.id)));
|
||||
}
|
||||
|
||||
async checkSharedAccess(userId: string, personIds: Set<string>, permissions: SharingPermission[]) {
|
||||
if (personIds.size === 0) {
|
||||
return new Set<string>();
|
||||
}
|
||||
|
||||
const ids = await this.db
|
||||
.selectFrom('person')
|
||||
.select('person.id')
|
||||
.where('person.id', 'in', [...personIds])
|
||||
.where(hasPermissions(userId, permissions))
|
||||
.execute();
|
||||
|
||||
return new Set(ids.map(({ id }) => id));
|
||||
}
|
||||
|
||||
async checkSharedFaceAccess(userId: string, faceIds: Set<string>, permissions: SharingPermission[]) {
|
||||
if (faceIds.size === 0) {
|
||||
return new Set<string>();
|
||||
}
|
||||
|
||||
const ids = await this.db
|
||||
.selectFrom('asset_face')
|
||||
.select('asset_face.id')
|
||||
.leftJoin('asset', (join) => join.onRef('asset.id', '=', 'asset_face.assetId'))
|
||||
.where('asset_face.id', 'in', [...faceIds])
|
||||
.where(hasAssetPermissions(userId, permissions))
|
||||
.execute();
|
||||
|
||||
return new Set(ids.map(({ id }) => id));
|
||||
}
|
||||
}
|
||||
|
||||
class PartnerAccess {
|
||||
|
||||
@@ -38,13 +38,4 @@ export class AlbumUserRepository {
|
||||
async delete({ userId, albumId }: AlbumPermissionId): Promise<void> {
|
||||
await this.db.deleteFrom('album_user').where('userId', '=', userId).where('albumId', '=', albumId).execute();
|
||||
}
|
||||
|
||||
get({ userId, albumId }: AlbumPermissionId) {
|
||||
return this.db
|
||||
.selectFrom('album_user')
|
||||
.select(['permissions', 'inTimeline'])
|
||||
.where('userId', '=', userId)
|
||||
.where('albumId', '=', albumId)
|
||||
.executeTakeFirstOrThrow();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,7 +8,6 @@ import {
|
||||
SelectQueryBuilder,
|
||||
ShallowDehydrateObject,
|
||||
sql,
|
||||
StringReference,
|
||||
Updateable,
|
||||
UpdateResult,
|
||||
} from 'kysely';
|
||||
@@ -26,7 +25,6 @@ import {
|
||||
AssetType,
|
||||
AssetVisibility,
|
||||
CalendarHeatmapType,
|
||||
SharingPermission,
|
||||
} from 'src/enum';
|
||||
import { DB } from 'src/schema';
|
||||
import { AssetAudioTable, AssetKeyframeTable, AssetVideoTable } from 'src/schema/tables/asset-av.table';
|
||||
@@ -51,7 +49,6 @@ import {
|
||||
withFiles,
|
||||
withLibrary,
|
||||
withOwner,
|
||||
withPermissions,
|
||||
withSmartSearch,
|
||||
withTagId,
|
||||
withTags,
|
||||
@@ -176,93 +173,6 @@ const withBoundingBox = <T>(qb: SelectQueryBuilder<DB, 'asset' | 'asset_exif', T
|
||||
);
|
||||
};
|
||||
|
||||
export const hasAssetPermissions =
|
||||
(userId: string, permissions: SharingPermission[], ignoreTimelineVisibility: boolean = false) =>
|
||||
(eb: ExpressionBuilder<DB, 'asset'>) =>
|
||||
eb.or([
|
||||
eb('asset.ownerId', '=', userId),
|
||||
eb.exists(
|
||||
eb
|
||||
.selectFrom('partner')
|
||||
.whereRef('partner.sharedById', '=', 'asset.ownerId')
|
||||
.where('partner.sharedWithId', '=', userId)
|
||||
.where((eb) =>
|
||||
eb.or([
|
||||
eb(eb.val(SharingPermission.All), '=', eb.fn.any('partner.permissions')),
|
||||
eb('partner.permissions', '@>', eb.val(permissions)),
|
||||
]),
|
||||
)
|
||||
.$if(!ignoreTimelineVisibility, (qb) => qb.where('partner.inTimeline', '=', true)),
|
||||
),
|
||||
eb.exists(
|
||||
eb
|
||||
.selectFrom('album_asset')
|
||||
.whereRef('album_asset.assetId', '=', 'asset.id')
|
||||
.innerJoin('album_user', (join) =>
|
||||
join.onRef('album_user.albumId', '=', 'album_asset.albumId').on('album_user.userId', '=', userId),
|
||||
)
|
||||
.$if(!ignoreTimelineVisibility, (qb) => qb.where('album_user.inTimeline', '=', true))
|
||||
.where('album_user.albumId', 'in', (eb) =>
|
||||
eb
|
||||
.selectFrom('album_user')
|
||||
.select('album_user.albumId')
|
||||
.whereRef('album_user.userId', '=', 'asset.ownerId')
|
||||
.where((eb) =>
|
||||
eb.or([
|
||||
eb(eb.val(SharingPermission.All), '=', eb.fn.any('album_user.permissions')),
|
||||
eb('album_user.permissions', '@>', eb.val(permissions)),
|
||||
]),
|
||||
),
|
||||
),
|
||||
),
|
||||
]);
|
||||
|
||||
export const hasAssetPermissionsRef = <T extends keyof DB>(
|
||||
eb: ExpressionBuilder<DB, 'asset'>,
|
||||
userIdRef: StringReference<DB, 'asset' | T>,
|
||||
permissions: SharingPermission[],
|
||||
ignoreTimelineVisibility: boolean = false,
|
||||
) =>
|
||||
eb.or([
|
||||
eb('asset.ownerId', '=', eb.ref(userIdRef as never)),
|
||||
eb.exists(
|
||||
eb
|
||||
.selectFrom('partner')
|
||||
.whereRef('partner.sharedById', '=', 'asset.ownerId')
|
||||
.whereRef('partner.sharedWithId', '=', userIdRef as never)
|
||||
.where((eb) =>
|
||||
eb.or([
|
||||
eb(eb.val(SharingPermission.All), '=', eb.fn.any('partner.permissions')),
|
||||
eb('partner.permissions', '@>', eb.val(permissions)),
|
||||
]),
|
||||
)
|
||||
.$if(!ignoreTimelineVisibility, (qb) => qb.where('partner.inTimeline', '=', true)),
|
||||
),
|
||||
eb.exists(
|
||||
eb
|
||||
.selectFrom('album_asset')
|
||||
.whereRef('album_asset.assetId', '=', 'asset.id')
|
||||
.innerJoin('album_user', (join) =>
|
||||
join
|
||||
.onRef('album_user.albumId', '=', 'album_asset.albumId')
|
||||
.onRef('album_user.userId', '=', userIdRef as never),
|
||||
)
|
||||
.$if(!ignoreTimelineVisibility, (qb) => qb.where('album_user.inTimeline', '=', true))
|
||||
.where('album_user.albumId', 'in', (eb) =>
|
||||
eb
|
||||
.selectFrom('album_user')
|
||||
.select('album_user.albumId')
|
||||
.whereRef('album_user.userId', '=', 'asset.ownerId')
|
||||
.where((eb) =>
|
||||
eb.or([
|
||||
eb(eb.val(SharingPermission.All), '=', eb.fn.any('album_user.permissions')),
|
||||
eb('album_user.permissions', '@>', eb.val(permissions)),
|
||||
]),
|
||||
),
|
||||
),
|
||||
),
|
||||
]);
|
||||
|
||||
@Injectable()
|
||||
export class AssetRepository {
|
||||
constructor(@InjectKysely() private db: Kysely<DB>) {}
|
||||
@@ -654,22 +564,17 @@ export class AssetRepository {
|
||||
.executeTakeFirst();
|
||||
}
|
||||
|
||||
@GenerateSql({ params: [DummyValue.UUID, {}, DummyValue.UUID] })
|
||||
@GenerateSql({ params: [DummyValue.UUID] })
|
||||
getById(
|
||||
id: string,
|
||||
{ exifInfo, faces, files, library, owner, smartSearch, stack, tags, edits }: GetByIdsRelations = {},
|
||||
userId?: string,
|
||||
) {
|
||||
return this.db
|
||||
.selectFrom('asset')
|
||||
.selectAll('asset')
|
||||
.where('asset.id', '=', asUuid(id))
|
||||
.$if(!!exifInfo, withExif)
|
||||
.$if(!!faces, (qb) =>
|
||||
qb
|
||||
.select(faces?.person ? (eb) => withFacesAndPeople(eb, { userId }) : withFaces)
|
||||
.$narrowType<{ faces: NotNull }>(),
|
||||
)
|
||||
.$if(!!faces, (qb) => qb.select(faces?.person ? withFacesAndPeople : withFaces).$narrowType<{ faces: NotNull }>())
|
||||
.$if(!!library, (qb) => qb.select(withLibrary))
|
||||
.$if(!!owner, (qb) => qb.select(withOwner))
|
||||
.$if(!!smartSearch, withSmartSearch)
|
||||
@@ -705,7 +610,6 @@ export class AssetRepository {
|
||||
.$if(!!files, (qb) => qb.select(withFiles))
|
||||
.$if(!!tags, (qb) => qb.select(withTags))
|
||||
.$if(!!edits, (qb) => qb.select(withEdits))
|
||||
.$if(!!userId, (qb) => qb.select(withPermissions(userId!)))
|
||||
.limit(1)
|
||||
.executeTakeFirst();
|
||||
}
|
||||
@@ -874,9 +778,7 @@ export class AssetRepository {
|
||||
)
|
||||
.where((eb) => eb.or([eb('asset.stackId', 'is', null), eb(eb.table('stack'), 'is not', null)])),
|
||||
)
|
||||
.$if(!!options.userIds, (qb) =>
|
||||
qb.where(hasAssetPermissions(options.userIds![0], [SharingPermission.AssetRead], !!options.personId)),
|
||||
)
|
||||
.$if(!!options.userIds, (qb) => qb.where('asset.ownerId', '=', anyUuid(options.userIds!)))
|
||||
.$if(options.isFavorite !== undefined, (qb) => qb.where('asset.isFavorite', '=', options.isFavorite!))
|
||||
.$if(!!options.assetType, (qb) => qb.where('asset.type', '=', options.assetType!))
|
||||
.$if(options.isDuplicate !== undefined, (qb) =>
|
||||
@@ -962,9 +864,7 @@ export class AssetRepository {
|
||||
),
|
||||
)
|
||||
.$if(!!options.personId, (qb) => hasPeople(qb, [options.personId!]))
|
||||
.$if(!!options.userIds, (qb) =>
|
||||
qb.where(hasAssetPermissions(options.userIds![0], [SharingPermission.AssetRead], !!options.personId)),
|
||||
)
|
||||
.$if(!!options.userIds, (qb) => qb.where('asset.ownerId', '=', anyUuid(options.userIds!)))
|
||||
.$if(options.isFavorite !== undefined, (qb) => qb.where('asset.isFavorite', '=', options.isFavorite!))
|
||||
.$if(!!options.withStacked, (qb) =>
|
||||
qb
|
||||
|
||||
@@ -15,7 +15,7 @@ import { getKeyByValue, getMethodNames, ImmichStartupError } from 'src/utils/mis
|
||||
type JobMapItem = {
|
||||
jobName: JobName;
|
||||
queueName: QueueName;
|
||||
handler: (job?: JobOf<any>) => Promise<JobStatus>;
|
||||
handler: (job: JobOf<any>) => Promise<JobStatus>;
|
||||
label: string;
|
||||
};
|
||||
|
||||
@@ -132,17 +132,14 @@ export class JobRepository {
|
||||
this.microservicesPresent = present;
|
||||
}
|
||||
|
||||
async run(job: JobItem) {
|
||||
const item = this.handlers[job.name];
|
||||
async run({ name, data }: JobItem) {
|
||||
const item = this.handlers[name as JobName];
|
||||
if (!item) {
|
||||
this.logger.warn(`Skipping unknown job: "${job.name}"`);
|
||||
this.logger.warn(`Skipping unknown job: "${name}"`);
|
||||
return JobStatus.Skipped;
|
||||
}
|
||||
|
||||
if ('data' in job) {
|
||||
return item.handler(job.data);
|
||||
}
|
||||
return item.handler();
|
||||
return item.handler(data);
|
||||
}
|
||||
|
||||
setConcurrency(queueName: QueueName, concurrency: number) {
|
||||
@@ -207,7 +204,7 @@ export class JobRepository {
|
||||
const queueName = this.getQueueName(item.name);
|
||||
const job = {
|
||||
name: item.name,
|
||||
data: ('data' in item ? item.data : undefined) || {},
|
||||
data: item.data || {},
|
||||
options: this.getJobOptions(item) || undefined,
|
||||
} as JobItem & { data: any; options: JobsOptions | undefined };
|
||||
|
||||
|
||||
@@ -73,7 +73,7 @@ export class MemoryRepository implements IBulkAsset {
|
||||
eb.exists(
|
||||
eb
|
||||
.selectFrom('asset_face')
|
||||
.innerJoin('person', 'person.faceClusterId', 'asset_face.faceClusterId')
|
||||
.innerJoin('person', 'person.id', 'asset_face.personId')
|
||||
.select((eb) => eb.val(1).as('one'))
|
||||
.whereRef('asset_face.assetId', '=', 'asset.id')
|
||||
.where('person.isHidden', '=', true),
|
||||
|
||||
@@ -4,8 +4,7 @@ import { jsonObjectFrom } from 'kysely/helpers/postgres';
|
||||
import { InjectKysely } from 'nestjs-kysely';
|
||||
import { AssetFace } from 'src/database';
|
||||
import { Chunked, ChunkedArray, DummyValue, GenerateSql } from 'src/decorators';
|
||||
import { AssetFileType, AssetVisibility, SharingPermission, SourceType, UserMetadataKey } from 'src/enum';
|
||||
import { hasAssetPermissions, hasAssetPermissionsRef } from 'src/repositories/asset.repository';
|
||||
import { AssetFileType, AssetVisibility, SourceType, UserMetadataKey } from 'src/enum';
|
||||
import { DB } from 'src/schema';
|
||||
import { AssetFaceTable } from 'src/schema/tables/asset-face.table';
|
||||
import { FaceSearchTable } from 'src/schema/tables/face-search.table';
|
||||
@@ -33,9 +32,9 @@ export interface AssetFaceId {
|
||||
}
|
||||
|
||||
export interface UpdateFacesData {
|
||||
oldFaceClusterId?: string;
|
||||
oldPersonId?: string;
|
||||
faceIds?: string[];
|
||||
newFaceClusterId: string;
|
||||
newPersonId: string;
|
||||
}
|
||||
|
||||
export interface PersonStatistics {
|
||||
@@ -54,7 +53,7 @@ export interface GetAllPeopleOptions {
|
||||
}
|
||||
|
||||
export interface GetAllFacesOptions {
|
||||
faceClusterId?: string | null;
|
||||
personId?: string | null;
|
||||
assetId?: string;
|
||||
sourceType?: SourceType;
|
||||
}
|
||||
@@ -63,27 +62,9 @@ export type UnassignFacesOptions = DeleteFacesOptions;
|
||||
|
||||
export type SelectFaceOptions = (keyof Selectable<AssetFaceTable>)[];
|
||||
|
||||
const withPerson = (eb: ExpressionBuilder<DB, 'asset_face'>, userId?: string) => {
|
||||
const withPerson = (eb: ExpressionBuilder<DB, 'asset_face'>) => {
|
||||
return jsonObjectFrom(
|
||||
eb
|
||||
.selectFrom('person')
|
||||
.selectAll('person')
|
||||
.whereRef('person.faceClusterId', '=', 'asset_face.faceClusterId')
|
||||
.$if(!!userId, (qb) =>
|
||||
qb.where((eb) =>
|
||||
eb.or([eb('person.ownerId', '=', userId!), hasPermissions(userId!, [SharingPermission.PersonRead])(eb)]),
|
||||
),
|
||||
)
|
||||
.orderBy(
|
||||
(eb) =>
|
||||
eb(
|
||||
'person.ownerId',
|
||||
'=',
|
||||
eb.selectFrom('asset').select('asset.ownerId').whereRef('asset.id', '=', 'asset_face.assetId'),
|
||||
),
|
||||
'desc',
|
||||
)
|
||||
.limit(1),
|
||||
eb.selectFrom('person').selectAll('person').whereRef('person.id', '=', 'asset_face.personId'),
|
||||
).as('person');
|
||||
};
|
||||
|
||||
@@ -93,47 +74,16 @@ const withFaceSearch = (eb: ExpressionBuilder<DB, 'asset_face'>) => {
|
||||
).as('faceSearch');
|
||||
};
|
||||
|
||||
export const hasPermissions =
|
||||
(userId: string, permissions: SharingPermission[]) => (eb: ExpressionBuilder<DB, 'person'>) =>
|
||||
eb.or([
|
||||
eb.exists((eb) =>
|
||||
eb
|
||||
.selectFrom('partner')
|
||||
.whereRef('partner.sharedById', '=', 'person.ownerId')
|
||||
.where('partner.sharedWithId', '=', userId)
|
||||
.where((eb) =>
|
||||
eb.or([
|
||||
eb(eb.val(SharingPermission.All), '=', eb.fn.any('partner.permissions')),
|
||||
eb('partner.permissions', '@>', eb.val(permissions)),
|
||||
]),
|
||||
),
|
||||
),
|
||||
eb.exists((eb) =>
|
||||
eb
|
||||
.selectFrom('album_user')
|
||||
.where('album_user.albumId', 'in', (eb) =>
|
||||
eb.selectFrom('album_user').select('album_user.albumId').where('album_user.userId', '=', userId),
|
||||
)
|
||||
.whereRef('album_user.userId', '=', 'person.ownerId')
|
||||
.where((eb) =>
|
||||
eb.or([
|
||||
eb(eb.val(SharingPermission.All), '=', eb.fn.any('album_user.permissions')),
|
||||
eb('album_user.permissions', '@>', eb.val(permissions)),
|
||||
]),
|
||||
),
|
||||
),
|
||||
]);
|
||||
|
||||
@Injectable()
|
||||
export class PersonRepository {
|
||||
constructor(@InjectKysely() private db: Kysely<DB>) {}
|
||||
|
||||
@GenerateSql({ params: [{ oldPersonId: DummyValue.UUID, newPersonId: DummyValue.UUID }] })
|
||||
async reassignFaces({ oldFaceClusterId, faceIds, newFaceClusterId }: UpdateFacesData): Promise<number> {
|
||||
async reassignFaces({ oldPersonId, faceIds, newPersonId }: UpdateFacesData): Promise<number> {
|
||||
const result = await this.db
|
||||
.updateTable('asset_face')
|
||||
.set({ faceClusterId: newFaceClusterId })
|
||||
.$if(!!oldFaceClusterId, (qb) => qb.where('asset_face.faceClusterId', '=', oldFaceClusterId!))
|
||||
.set({ personId: newPersonId })
|
||||
.$if(!!oldPersonId, (qb) => qb.where('asset_face.personId', '=', oldPersonId!))
|
||||
.$if(!!faceIds, (qb) => qb.where('asset_face.id', 'in', faceIds!))
|
||||
.executeTakeFirst();
|
||||
|
||||
@@ -143,7 +93,7 @@ export class PersonRepository {
|
||||
async unassignFaces({ sourceType }: UnassignFacesOptions): Promise<void> {
|
||||
await this.db
|
||||
.updateTable('asset_face')
|
||||
.set({ faceClusterId: null })
|
||||
.set({ personId: null })
|
||||
.where('asset_face.sourceType', '=', sourceType)
|
||||
.execute();
|
||||
}
|
||||
@@ -166,8 +116,8 @@ export class PersonRepository {
|
||||
return this.db
|
||||
.selectFrom('asset_face')
|
||||
.selectAll('asset_face')
|
||||
.$if(options.faceClusterId === null, (qb) => qb.where('asset_face.faceClusterId', 'is', null))
|
||||
.$if(!!options.faceClusterId, (qb) => qb.where('asset_face.faceClusterId', '=', options.faceClusterId!))
|
||||
.$if(options.personId === null, (qb) => qb.where('asset_face.personId', 'is', null))
|
||||
.$if(!!options.personId, (qb) => qb.where('asset_face.personId', '=', options.personId!))
|
||||
.$if(!!options.sourceType, (qb) => qb.where('asset_face.sourceType', '=', options.sourceType!))
|
||||
.$if(!!options.assetId, (qb) => qb.where('asset_face.assetId', '=', options.assetId!))
|
||||
.where('asset_face.deletedAt', 'is', null)
|
||||
@@ -202,20 +152,16 @@ export class PersonRepository {
|
||||
const items = await this.db
|
||||
.selectFrom('person')
|
||||
.selectAll('person')
|
||||
.innerJoin('asset_face', 'asset_face.faceClusterId', 'person.faceClusterId')
|
||||
.innerJoin('asset_face', 'asset_face.personId', 'person.id')
|
||||
.innerJoin('asset', (join) =>
|
||||
join
|
||||
.onRef('asset_face.assetId', '=', 'asset.id')
|
||||
.on('asset.visibility', '=', sql.lit(AssetVisibility.Timeline))
|
||||
.on('asset.deletedAt', 'is', null),
|
||||
)
|
||||
.where((eb) =>
|
||||
eb.or([eb('person.ownerId', '=', userId), hasPermissions(userId, [SharingPermission.PersonRead])(eb)]),
|
||||
)
|
||||
.where('person.ownerId', '=', userId)
|
||||
.where('asset_face.deletedAt', 'is', null)
|
||||
.where('asset_face.isVisible', 'is', true)
|
||||
.orderBy('person.faceClusterId')
|
||||
.orderBy((eb) => eb('person.ownerId', '=', userId), 'desc')
|
||||
.orderBy('person.isHidden', 'asc')
|
||||
.orderBy('person.isFavorite', 'desc')
|
||||
.having((eb) =>
|
||||
@@ -234,7 +180,6 @@ export class PersonRepository {
|
||||
),
|
||||
]),
|
||||
)
|
||||
.distinctOn('person.faceClusterId')
|
||||
.groupBy('person.id')
|
||||
.$if(!!options?.closestFaceAssetId, (qb) =>
|
||||
qb.orderBy((eb) =>
|
||||
@@ -273,7 +218,7 @@ export class PersonRepository {
|
||||
return this.db
|
||||
.selectFrom('person')
|
||||
.selectAll('person')
|
||||
.leftJoin('asset_face', 'asset_face.faceClusterId', 'person.faceClusterId')
|
||||
.leftJoin('asset_face', 'asset_face.personId', 'person.id')
|
||||
.where('asset_face.deletedAt', 'is', null)
|
||||
.where('asset_face.isVisible', 'is', true)
|
||||
.having((eb) => eb.fn.count('asset_face.assetId'), '=', 0)
|
||||
@@ -282,13 +227,13 @@ export class PersonRepository {
|
||||
}
|
||||
|
||||
@GenerateSql({ params: [DummyValue.UUID] })
|
||||
getFaces(assetId: string, options: { isVisible?: boolean; userId?: string } = {}) {
|
||||
const { isVisible = true, userId } = options;
|
||||
getFaces(assetId: string, options?: { isVisible?: boolean }) {
|
||||
const isVisible = options === undefined ? true : options.isVisible;
|
||||
|
||||
return this.db
|
||||
.selectFrom('asset_face')
|
||||
.selectAll('asset_face')
|
||||
.select((eb) => withPerson(eb, userId))
|
||||
.select(withPerson)
|
||||
.where('asset_face.assetId', '=', assetId)
|
||||
.where('asset_face.deletedAt', 'is', null)
|
||||
.$if(isVisible !== undefined, (qb) => qb.where('asset_face.isVisible', '=', isVisible!))
|
||||
@@ -312,7 +257,7 @@ export class PersonRepository {
|
||||
getFaceForFacialRecognitionJob(id: string) {
|
||||
return this.db
|
||||
.selectFrom('asset_face')
|
||||
.select(['asset_face.id', 'asset_face.faceClusterId', 'asset_face.sourceType'])
|
||||
.select(['asset_face.id', 'asset_face.personId', 'asset_face.sourceType'])
|
||||
.select((eb) =>
|
||||
jsonObjectFrom(
|
||||
eb
|
||||
@@ -353,10 +298,10 @@ export class PersonRepository {
|
||||
}
|
||||
|
||||
@GenerateSql({ params: [DummyValue.UUID, DummyValue.UUID] })
|
||||
async reassignFace(assetFaceId: string, newFaceClusterId: string): Promise<number> {
|
||||
async reassignFace(assetFaceId: string, newPersonId: string): Promise<number> {
|
||||
const result = await this.db
|
||||
.updateTable('asset_face')
|
||||
.set({ faceClusterId: newFaceClusterId })
|
||||
.set({ personId: newPersonId })
|
||||
.where('asset_face.id', '=', assetFaceId)
|
||||
.executeTakeFirst();
|
||||
|
||||
@@ -382,7 +327,6 @@ export class PersonRepository {
|
||||
.where('person.ownerId', '=', userId)
|
||||
.where(() => sql`f_unaccent("person"."name") %> f_unaccent(${personName})`)
|
||||
.orderBy(sql`f_unaccent("person"."name") <->>> f_unaccent(${personName})`)
|
||||
.orderBy((eb) => eb('person.ownerId', '=', userId), 'desc')
|
||||
.limit(100)
|
||||
.$if(!withHidden, (qb) => qb.where('person.isHidden', '=', false))
|
||||
.execute();
|
||||
@@ -400,7 +344,7 @@ export class PersonRepository {
|
||||
}
|
||||
|
||||
@GenerateSql({ params: [DummyValue.UUID] })
|
||||
async getStatistics(userId: string, personId: string): Promise<PersonStatistics> {
|
||||
async getStatistics(personId: string): Promise<PersonStatistics> {
|
||||
const result = await this.db
|
||||
.selectFrom('asset_face')
|
||||
.leftJoin('asset', (join) =>
|
||||
@@ -409,13 +353,10 @@ export class PersonRepository {
|
||||
.on('asset.visibility', '=', sql.lit(AssetVisibility.Timeline))
|
||||
.on('asset.deletedAt', 'is', null),
|
||||
)
|
||||
.where(hasAssetPermissions(userId, [SharingPermission.AssetRead], true))
|
||||
.select((eb) => eb.fn.count(eb.fn('distinct', ['asset.id'])).as('count'))
|
||||
.where('asset_face.deletedAt', 'is', null)
|
||||
.where('asset_face.isVisible', 'is', true)
|
||||
.where('asset_face.faceClusterId', '=', (eb) =>
|
||||
eb.selectFrom('person').select('person.faceClusterId').where('person.id', '=', personId),
|
||||
)
|
||||
.where('asset_face.personId', '=', personId)
|
||||
.executeTakeFirst();
|
||||
|
||||
return {
|
||||
@@ -432,7 +373,7 @@ export class PersonRepository {
|
||||
eb.exists((eb) =>
|
||||
eb
|
||||
.selectFrom('asset_face')
|
||||
.whereRef('asset_face.faceClusterId', '=', 'person.faceClusterId')
|
||||
.whereRef('asset_face.personId', '=', 'person.id')
|
||||
.where('asset_face.deletedAt', 'is', null)
|
||||
.where('asset_face.isVisible', '=', true)
|
||||
.where((eb) =>
|
||||
@@ -446,20 +387,13 @@ export class PersonRepository {
|
||||
),
|
||||
),
|
||||
)
|
||||
.where((eb) =>
|
||||
eb.or([eb('person.ownerId', '=', userId), hasPermissions(userId, [SharingPermission.PersonRead])(eb)]),
|
||||
)
|
||||
.where('person.ownerId', '=', userId)
|
||||
.select((eb) => eb.fn.coalesce(eb.fn.countAll<number>(), zero).as('total'))
|
||||
.select((eb) => eb.fn.coalesce(eb.fn.countAll<number>().filterWhere('isHidden', '=', true), zero).as('hidden'))
|
||||
.executeTakeFirstOrThrow();
|
||||
}
|
||||
|
||||
async create(person: Insertable<PersonTable>) {
|
||||
if (!person.faceClusterId) {
|
||||
const { id } = await this.db.insertInto('face_cluster').defaultValues().returning('id').executeTakeFirstOrThrow();
|
||||
person.faceClusterId = id;
|
||||
}
|
||||
|
||||
create(person: Insertable<PersonTable>) {
|
||||
return this.db.insertInto('person').values(person).returningAll().executeTakeFirstOrThrow();
|
||||
}
|
||||
|
||||
@@ -550,9 +484,8 @@ export class PersonRepository {
|
||||
.selectFrom('asset_face')
|
||||
.selectAll('asset_face')
|
||||
.select(withPerson)
|
||||
.innerJoin('person', (join) => join.onRef('person.faceClusterId', '=', 'asset_face.faceClusterId'))
|
||||
.where('person.id', 'in', personIds)
|
||||
.where('asset_face.assetId', 'in', assetIds)
|
||||
.where('asset_face.personId', 'in', personIds)
|
||||
.where('asset_face.deletedAt', 'is', null)
|
||||
.execute();
|
||||
}
|
||||
@@ -562,15 +495,7 @@ export class PersonRepository {
|
||||
return this.db
|
||||
.selectFrom('asset_face')
|
||||
.selectAll('asset_face')
|
||||
.innerJoin('person', (join) =>
|
||||
join.onRef('asset_face.faceClusterId', '=', 'person.faceClusterId').on('person.id', '=', personId),
|
||||
)
|
||||
.where('asset_face.assetId', 'in', (eb) =>
|
||||
eb
|
||||
.selectFrom('asset')
|
||||
.select('asset.id')
|
||||
.where((eb) => hasAssetPermissionsRef(eb, 'person.ownerId', [SharingPermission.AssetRead], true)),
|
||||
)
|
||||
.where('asset_face.personId', '=', personId)
|
||||
.where('asset_face.deletedAt', 'is', null)
|
||||
.where('asset_face.isVisible', 'is', true)
|
||||
.executeTakeFirst();
|
||||
@@ -657,14 +582,8 @@ export class PersonRepository {
|
||||
.selectFrom('asset_face')
|
||||
.select('asset_face.id')
|
||||
.where('asset_face.assetId', '=', assetId)
|
||||
.innerJoin('person', (join) =>
|
||||
join.onRef('person.faceClusterId', '=', 'asset_face.faceClusterId').on('person.id', '=', personId),
|
||||
)
|
||||
.where('asset_face.personId', '=', personId)
|
||||
.innerJoin('asset', (join) => join.onRef('asset.id', '=', 'asset_face.assetId').on('asset.isOffline', '=', false))
|
||||
.executeTakeFirst();
|
||||
}
|
||||
|
||||
getByFaceClusterId(faceClusterId: string) {
|
||||
return this.db.selectFrom('person').selectAll().where('person.faceClusterId', '=', faceClusterId).execute();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -325,23 +325,15 @@ export class SearchRepository {
|
||||
.selectFrom('asset_face')
|
||||
.select([
|
||||
'asset_face.id',
|
||||
'asset_face.faceClusterId',
|
||||
'asset_face.personId',
|
||||
sql<number>`face_search.embedding <=> ${embedding}`.as('distance'),
|
||||
])
|
||||
.innerJoin('asset', 'asset.id', 'asset_face.assetId')
|
||||
.select('asset.ownerId')
|
||||
.innerJoin('face_search', 'face_search.faceId', 'asset_face.id')
|
||||
.leftJoin('person', 'person.faceClusterId', 'asset_face.faceClusterId')
|
||||
.where('asset.ownerId', 'in', (eb) =>
|
||||
eb
|
||||
.selectFrom('user')
|
||||
.select('user.id')
|
||||
.where('user.trustedGroupId', 'in', (eb) =>
|
||||
eb.selectFrom('user').select('user.trustedGroupId').where('user.id', '=', anyUuid(userIds)),
|
||||
),
|
||||
)
|
||||
.leftJoin('person', 'person.id', 'asset_face.personId')
|
||||
.where('asset.ownerId', '=', anyUuid(userIds))
|
||||
.where('asset.deletedAt', 'is', null)
|
||||
.$if(!!hasPerson, (qb) => qb.where('asset_face.faceClusterId', 'is not', null))
|
||||
.$if(!!hasPerson, (qb) => qb.where('asset_face.personId', 'is not', null))
|
||||
.$if(!!minBirthDate, (qb) =>
|
||||
qb.where((eb) =>
|
||||
eb.or([eb('person.birthDate', 'is', null), eb('person.birthDate', '<=', minBirthDate!)]),
|
||||
|
||||
@@ -472,7 +472,7 @@ class AssetFaceSync extends BaseSync {
|
||||
.select([
|
||||
'asset_face.id',
|
||||
'assetId',
|
||||
'faceClusterId',
|
||||
'personId',
|
||||
'imageWidth',
|
||||
'imageHeight',
|
||||
'boundingBoxX1',
|
||||
|
||||
@@ -325,61 +325,4 @@ export class UserRepository {
|
||||
|
||||
await query.execute();
|
||||
}
|
||||
|
||||
@GenerateSql({ params: [DummyValue.UUID] })
|
||||
async getInSameTrustedGroup(userId: string) {
|
||||
return this.db
|
||||
.selectFrom('user')
|
||||
.select('user.id')
|
||||
.where('user.trustedGroupId', '=', (eb) =>
|
||||
eb.selectFrom('user').select('user.trustedGroupId').where('user.id', '=', userId),
|
||||
)
|
||||
.execute()
|
||||
.then((result) => result.map(({ id }) => id));
|
||||
}
|
||||
|
||||
@GenerateSql({ params: [{ userId: DummyValue.UUID, userIdToMerge: DummyValue.UUID }] })
|
||||
async mergeTrustedGroups({ userId, userIdToMerge }: { userId: string; userIdToMerge: string }) {
|
||||
return this.db
|
||||
.updateTable('user')
|
||||
.from('user as u')
|
||||
.where('u.id', '=', userId)
|
||||
.where('user.trustedGroupId', '=', (eb) =>
|
||||
eb
|
||||
.selectFrom('user')
|
||||
.select('user.trustedGroupId')
|
||||
.where('user.id', '=', userIdToMerge)
|
||||
.whereRef('user.trustedGroupId', '!=', 'u.trustedGroupId'),
|
||||
)
|
||||
.set((eb) => ({
|
||||
trustedGroupId: eb.ref('u.trustedGroupId'),
|
||||
}))
|
||||
.executeTakeFirst();
|
||||
}
|
||||
|
||||
@GenerateSql({ params: [DummyValue.UUID] })
|
||||
async updateTrustedGroups(userId: string) {
|
||||
return this.db
|
||||
.updateTable('user')
|
||||
.set((eb) => ({ trustedGroupId: eb.fn('uuid_generate_v4') }))
|
||||
.where('user.trustedGroupId', '=', (eb) =>
|
||||
eb.selectFrom('user').select('user.trustedGroupId').where('user.id', '=', userId),
|
||||
)
|
||||
.where('user.id', '!=', userId)
|
||||
.where('user.id', 'not in', (eb) =>
|
||||
eb
|
||||
.selectFrom('partner')
|
||||
.select('partner.sharedById as userId')
|
||||
.where('sharedWithId', '=', userId)
|
||||
.union((eb) =>
|
||||
eb
|
||||
.selectFrom('album_user')
|
||||
.select('album_user.userId')
|
||||
.where('album_user.albumId', 'in', (eb) =>
|
||||
eb.selectFrom('album_user').select('album_user.albumId').where('album_user.userId', '=', userId),
|
||||
),
|
||||
),
|
||||
)
|
||||
.executeTakeFirst();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,13 +1,5 @@
|
||||
import { registerEnum } from '@immich/sql-tools';
|
||||
import {
|
||||
AlbumUserRole,
|
||||
AssetStatus,
|
||||
AssetVisibility,
|
||||
ChecksumAlgorithm,
|
||||
SharingPermission,
|
||||
SourceType,
|
||||
VideoCodec,
|
||||
} from 'src/enum';
|
||||
import { AlbumUserRole, AssetStatus, AssetVisibility, ChecksumAlgorithm, SourceType, VideoCodec } from 'src/enum';
|
||||
|
||||
export const album_user_role_enum = registerEnum({
|
||||
name: 'album_user_role_enum',
|
||||
@@ -38,8 +30,3 @@ export const video_stream_variant_codec_enum = registerEnum({
|
||||
name: 'video_stream_variant_codec_enum',
|
||||
values: [VideoCodec.Av1, VideoCodec.Hevc, VideoCodec.H264],
|
||||
});
|
||||
|
||||
export const sharing_permission_enum = registerEnum({
|
||||
name: 'sharing_permission_enum',
|
||||
values: Object.values(SharingPermission),
|
||||
});
|
||||
|
||||
@@ -4,7 +4,6 @@ import {
|
||||
asset_face_source_type,
|
||||
asset_visibility_enum,
|
||||
assets_status_enum,
|
||||
sharing_permission_enum,
|
||||
} from 'src/schema/enums';
|
||||
import {
|
||||
album_user_after_insert,
|
||||
@@ -48,7 +47,6 @@ import { AssetMetadataTable } from 'src/schema/tables/asset-metadata.table';
|
||||
import { AssetOcrAuditTable } from 'src/schema/tables/asset-ocr-audit.table';
|
||||
import { AssetOcrTable } from 'src/schema/tables/asset-ocr.table';
|
||||
import { AssetTable } from 'src/schema/tables/asset.table';
|
||||
import { FaceClusterTable } from 'src/schema/tables/face-cluster.table';
|
||||
import { FaceSearchTable } from 'src/schema/tables/face-search.table';
|
||||
import { GeodataPlacesTable } from 'src/schema/tables/geodata-places.table';
|
||||
import { IntegrityReportTable } from 'src/schema/tables/integrity-report.table';
|
||||
@@ -116,7 +114,6 @@ export class ImmichDatabase {
|
||||
AssetTable,
|
||||
AssetFileTable,
|
||||
AssetExifTable,
|
||||
FaceClusterTable,
|
||||
FaceSearchTable,
|
||||
GeodataPlacesTable,
|
||||
IntegrityReportTable,
|
||||
@@ -179,13 +176,7 @@ export class ImmichDatabase {
|
||||
asset_ocr_delete_audit,
|
||||
];
|
||||
|
||||
enum = [
|
||||
album_user_role_enum,
|
||||
assets_status_enum,
|
||||
asset_face_source_type,
|
||||
asset_visibility_enum,
|
||||
sharing_permission_enum,
|
||||
];
|
||||
enum = [album_user_role_enum, assets_status_enum, asset_face_source_type, asset_visibility_enum];
|
||||
}
|
||||
|
||||
export interface Migrations {
|
||||
@@ -227,7 +218,6 @@ export interface DB {
|
||||
ocr_search: OcrSearchTable;
|
||||
|
||||
face_search: FaceSearchTable;
|
||||
face_cluster: FaceClusterTable;
|
||||
|
||||
geodata_places: GeodataPlacesTable;
|
||||
|
||||
|
||||
@@ -1,17 +0,0 @@
|
||||
import { Kysely, sql } from 'kysely';
|
||||
|
||||
export async function up(db: Kysely<any>): Promise<void> {
|
||||
await sql`CREATE TYPE "sharing_permission_enum" AS ENUM ('all','asset.read','asset.update','asset.edit','asset.delete','asset.share','exif.read','person.read','person.update','person.merge','person.delete');`.execute(db);
|
||||
await sql`ALTER TABLE "user" ADD "trustedGroupId" uuid NOT NULL DEFAULT uuid_generate_v4();`.execute(db);
|
||||
await sql`ALTER TABLE "album_user" ADD "permissions" sharing_permission_enum[] NOT NULL DEFAULT '{asset.read,exif.read}';`.execute(db);
|
||||
await sql`ALTER TABLE "album_user" ADD "inTimeline" boolean NOT NULL DEFAULT false;`.execute(db);
|
||||
await sql`ALTER TABLE "partner" ADD "permissions" sharing_permission_enum[] NOT NULL DEFAULT '{all}';`.execute(db);
|
||||
}
|
||||
|
||||
export async function down(db: Kysely<any>): Promise<void> {
|
||||
await sql`DROP TYPE "sharing_permission_enum";`.execute(db);
|
||||
await sql`ALTER TABLE "partner" DROP COLUMN "permissions";`.execute(db);
|
||||
await sql`ALTER TABLE "user" DROP COLUMN "trustedGroupId";`.execute(db);
|
||||
await sql`ALTER TABLE "album_user" DROP COLUMN "permissions";`.execute(db);
|
||||
await sql`ALTER TABLE "album_user" DROP COLUMN "inTimeline";`.execute(db);
|
||||
}
|
||||
@@ -1,51 +0,0 @@
|
||||
import { Kysely, sql } from 'kysely';
|
||||
|
||||
export async function up(db: Kysely<any>): Promise<void> {
|
||||
await sql`ALTER TABLE "asset_face" RENAME COLUMN "personId" TO "faceClusterId";`.execute(db);
|
||||
await sql`CREATE INDEX "asset_face_faceClusterId_assetId_idx" ON "asset_face" ("faceClusterId", "assetId");`.execute(db);
|
||||
await sql`CREATE INDEX "asset_face_faceClusterId_assetId_notDeleted_isVisible_idx" ON "asset_face" ("faceClusterId", "assetId") WHERE ("deletedAt" IS NULL AND "isVisible" IS TRUE);`.execute(db);
|
||||
await sql`CREATE INDEX "asset_face_assetId_faceClusterId_idx" ON "asset_face" ("assetId", "faceClusterId");`.execute(db);
|
||||
await sql`DROP INDEX "asset_face_personId_assetId_notDeleted_isVisible_idx";`.execute(db);
|
||||
await sql`DROP INDEX "asset_face_assetId_personId_idx";`.execute(db);
|
||||
await sql`DROP INDEX "asset_face_personId_assetId_idx";`.execute(db);
|
||||
await sql`CREATE TABLE "face_cluster" (
|
||||
"id" uuid NOT NULL DEFAULT uuid_generate_v4(),
|
||||
"createdAt" timestamp with time zone NOT NULL DEFAULT now(),
|
||||
"updatedAt" timestamp with time zone NOT NULL DEFAULT now(),
|
||||
"updateId" uuid NOT NULL DEFAULT immich_uuid_v7(),
|
||||
CONSTRAINT "face_cluster_pkey" PRIMARY KEY ("id")
|
||||
);`.execute(db);
|
||||
await sql`ALTER TABLE "asset_face" ADD CONSTRAINT "asset_face_faceClusterId_fkey" FOREIGN KEY ("faceClusterId") REFERENCES "face_cluster" ("id") ON UPDATE CASCADE ON DELETE SET NULL;`.execute(db);
|
||||
await sql`ALTER TABLE "asset_face" DROP CONSTRAINT "asset_face_personId_fkey";`.execute(db);
|
||||
await sql`ALTER TABLE "person" ADD "faceClusterId" uuid;`.execute(db);
|
||||
await sql`CREATE INDEX "person_faceClusterId_idx" ON "person" ("faceClusterId");`.execute(db);
|
||||
await sql`ALTER TABLE "person" ADD CONSTRAINT "person_faceClusterId_fkey" FOREIGN KEY ("faceClusterId") REFERENCES "face_cluster" ("id") ON UPDATE CASCADE ON DELETE CASCADE;`.execute(db);
|
||||
await sql`CREATE INDEX "face_cluster_updateId_idx" ON "face_cluster" ("updateId");`.execute(db);
|
||||
await sql`CREATE OR REPLACE TRIGGER "face_cluster_updatedAt"
|
||||
BEFORE UPDATE ON "face_cluster"
|
||||
FOR EACH ROW
|
||||
EXECUTE FUNCTION updated_at();`.execute(db);
|
||||
await sql`INSERT INTO "migration_overrides" ("name", "value") VALUES ('trigger_face_cluster_updatedAt', '{"type":"trigger","name":"face_cluster_updatedAt","sql":"CREATE OR REPLACE TRIGGER \\"face_cluster_updatedAt\\"\\n BEFORE UPDATE ON \\"face_cluster\\"\\n FOR EACH ROW\\n EXECUTE FUNCTION updated_at();"}'::jsonb);`.execute(db);
|
||||
await sql`INSERT INTO "migration_overrides" ("name", "value") VALUES ('index_asset_face_faceClusterId_assetId_notDeleted_isVisible_idx', '{"type":"index","name":"asset_face_faceClusterId_assetId_notDeleted_isVisible_idx","sql":"CREATE INDEX \\"asset_face_faceClusterId_assetId_notDeleted_isVisible_idx\\" ON \\"asset_face\\" (\\"faceClusterId\\", \\"assetId\\") WHERE (\\"deletedAt\\" IS NULL AND \\"isVisible\\" IS TRUE);"}'::jsonb);`.execute(db);
|
||||
await sql`DELETE FROM "migration_overrides" WHERE "name" = 'index_asset_face_personId_assetId_notDeleted_isVisible_idx';`.execute(db);
|
||||
}
|
||||
|
||||
export async function down(db: Kysely<any>): Promise<void> {
|
||||
await sql`ALTER TABLE "person" DROP COLUMN "faceClusterId";`.execute(db);
|
||||
await sql`DROP INDEX "person_faceClusterId_idx";`.execute(db);
|
||||
await sql`ALTER TABLE "person" DROP CONSTRAINT "person_faceClusterId_fkey";`.execute(db);
|
||||
await sql`ALTER TABLE "asset_face" RENAME COLUMN "faceClusterId" TO "personId";`.execute(db);
|
||||
await sql`CREATE INDEX "asset_face_personId_assetId_notDeleted_isVisible_idx" ON "asset_face" ("personId", "assetId") WHERE ((("deletedAt" IS NULL) AND ("isVisible" IS TRUE)));`.execute(db);
|
||||
await sql`CREATE INDEX "asset_face_assetId_personId_idx" ON "asset_face" ("assetId", "personId");`.execute(db);
|
||||
await sql`CREATE INDEX "asset_face_personId_assetId_idx" ON "asset_face" ("personId", "assetId");`.execute(db);
|
||||
await sql`DROP INDEX "asset_face_faceClusterId_assetId_idx";`.execute(db);
|
||||
await sql`DROP INDEX "asset_face_faceClusterId_assetId_notDeleted_isVisible_idx";`.execute(db);
|
||||
await sql`DROP INDEX "asset_face_assetId_faceClusterId_idx";`.execute(db);
|
||||
await sql`ALTER TABLE "asset_face" ADD CONSTRAINT "asset_face_personId_fkey" FOREIGN KEY ("personId") REFERENCES "person" ("id") ON UPDATE CASCADE ON DELETE SET NULL;`.execute(db);
|
||||
await sql`ALTER TABLE "asset_face" DROP CONSTRAINT "asset_face_faceClusterId_fkey";`.execute(db);
|
||||
await sql`DROP TABLE "face_cluster";`.execute(db);
|
||||
await sql`DROP TRIGGER "face_cluster_updatedAt" ON "face_cluster";`.execute(db);
|
||||
await sql`INSERT INTO "migration_overrides" ("name", "value") VALUES ('index_asset_face_personId_assetId_notDeleted_isVisible_idx', '{"sql":"CREATE INDEX \\"asset_face_personId_assetId_notDeleted_isVisible_idx\\" ON \\"asset_face\\" (\\"personId\\", \\"assetId\\") WHERE (\\"deletedAt\\" IS NULL AND \\"isVisible\\" IS TRUE);","name":"asset_face_personId_assetId_notDeleted_isVisible_idx","type":"index"}'::jsonb);`.execute(db);
|
||||
await sql`DELETE FROM "migration_overrides" WHERE "name" = 'trigger_face_cluster_updatedAt';`.execute(db);
|
||||
await sql`DELETE FROM "migration_overrides" WHERE "name" = 'index_asset_face_faceClusterId_assetId_notDeleted_isVisible_idx';`.execute(db);
|
||||
}
|
||||
@@ -11,8 +11,8 @@ import {
|
||||
UpdateDateColumn,
|
||||
} from '@immich/sql-tools';
|
||||
import { CreateIdColumn, UpdatedAtTrigger, UpdateIdColumn } from 'src/decorators';
|
||||
import { AlbumUserRole, SharingPermission } from 'src/enum';
|
||||
import { album_user_role_enum, sharing_permission_enum } from 'src/schema/enums';
|
||||
import { AlbumUserRole } from 'src/enum';
|
||||
import { album_user_role_enum } from 'src/schema/enums';
|
||||
import { album_user_after_insert, album_user_delete_audit } from 'src/schema/functions';
|
||||
import { AlbumTable } from 'src/schema/tables/album.table';
|
||||
import { UserTable } from 'src/schema/tables/user.table';
|
||||
@@ -69,14 +69,4 @@ export class AlbumUserTable {
|
||||
|
||||
@UpdateDateColumn()
|
||||
updatedAt!: Generated<Timestamp>;
|
||||
|
||||
@Column({
|
||||
array: true,
|
||||
enum: sharing_permission_enum,
|
||||
default: [SharingPermission.AssetRead, SharingPermission.ExifRead],
|
||||
})
|
||||
permissions!: Generated<SharingPermission[]>;
|
||||
|
||||
@Column({ type: 'boolean', default: false })
|
||||
inTimeline!: Generated<boolean>;
|
||||
}
|
||||
|
||||
@@ -15,7 +15,7 @@ import { SourceType } from 'src/enum';
|
||||
import { asset_face_source_type } from 'src/schema/enums';
|
||||
import { asset_face_audit } from 'src/schema/functions';
|
||||
import { AssetTable } from 'src/schema/tables/asset.table';
|
||||
import { FaceClusterTable } from 'src/schema/tables/face-cluster.table';
|
||||
import { PersonTable } from 'src/schema/tables/person.table';
|
||||
|
||||
@Table({ name: 'asset_face' })
|
||||
@UpdatedAtTrigger('asset_face_updatedAt')
|
||||
@@ -26,13 +26,13 @@ import { FaceClusterTable } from 'src/schema/tables/face-cluster.table';
|
||||
when: 'pg_trigger_depth() = 0',
|
||||
})
|
||||
// schemaFromDatabase does not preserve column order
|
||||
@Index({ name: 'asset_face_assetId_faceClusterId_idx', columns: ['assetId', 'faceClusterId'] })
|
||||
@Index({ name: 'asset_face_assetId_personId_idx', columns: ['assetId', 'personId'] })
|
||||
@Index({
|
||||
name: 'asset_face_faceClusterId_assetId_notDeleted_isVisible_idx',
|
||||
columns: ['faceClusterId', 'assetId'],
|
||||
name: 'asset_face_personId_assetId_notDeleted_isVisible_idx',
|
||||
columns: ['personId', 'assetId'],
|
||||
where: '"deletedAt" IS NULL AND "isVisible" IS TRUE',
|
||||
})
|
||||
@Index({ columns: ['faceClusterId', 'assetId'] })
|
||||
@Index({ columns: ['personId', 'assetId'] })
|
||||
export class AssetFaceTable {
|
||||
@PrimaryGeneratedColumn()
|
||||
id!: Generated<string>;
|
||||
@@ -45,14 +45,14 @@ export class AssetFaceTable {
|
||||
})
|
||||
assetId!: string;
|
||||
|
||||
@ForeignKeyColumn(() => FaceClusterTable, {
|
||||
@ForeignKeyColumn(() => PersonTable, {
|
||||
onDelete: 'SET NULL',
|
||||
onUpdate: 'CASCADE',
|
||||
nullable: true,
|
||||
// [faceClusterId, assetId] makes this redundant
|
||||
// [personId, assetId] makes this redundant
|
||||
index: false,
|
||||
})
|
||||
faceClusterId!: string | null;
|
||||
personId!: string | null;
|
||||
|
||||
@Column({ default: 0, type: 'integer' })
|
||||
imageWidth!: Generated<number>;
|
||||
|
||||
@@ -1,25 +0,0 @@
|
||||
import {
|
||||
CreateDateColumn,
|
||||
Generated,
|
||||
PrimaryGeneratedColumn,
|
||||
Table,
|
||||
Timestamp,
|
||||
UpdateDateColumn,
|
||||
} from '@immich/sql-tools';
|
||||
import { UpdatedAtTrigger, UpdateIdColumn } from 'src/decorators';
|
||||
|
||||
@Table('face_cluster')
|
||||
@UpdatedAtTrigger('face_cluster_updatedAt')
|
||||
export class FaceClusterTable {
|
||||
@PrimaryGeneratedColumn('uuid')
|
||||
id!: Generated<string>;
|
||||
|
||||
@CreateDateColumn()
|
||||
createdAt!: Generated<Timestamp>;
|
||||
|
||||
@UpdateDateColumn()
|
||||
updatedAt!: Generated<Timestamp>;
|
||||
|
||||
@UpdateIdColumn({ index: true })
|
||||
updateId!: Generated<string>;
|
||||
}
|
||||
@@ -9,8 +9,6 @@ import {
|
||||
UpdateDateColumn,
|
||||
} from '@immich/sql-tools';
|
||||
import { CreateIdColumn, UpdatedAtTrigger, UpdateIdColumn } from 'src/decorators';
|
||||
import { SharingPermission } from 'src/enum';
|
||||
import { sharing_permission_enum } from 'src/schema/enums';
|
||||
import { partner_delete_audit } from 'src/schema/functions';
|
||||
import { UserTable } from 'src/schema/tables/user.table';
|
||||
|
||||
@@ -48,7 +46,4 @@ export class PartnerTable {
|
||||
|
||||
@UpdateIdColumn({ index: true })
|
||||
updateId!: Generated<string>;
|
||||
|
||||
@Column({ array: true, enum: sharing_permission_enum, default: [SharingPermission.All] })
|
||||
permissions!: Generated<SharingPermission[]>;
|
||||
}
|
||||
|
||||
@@ -14,7 +14,6 @@ import {
|
||||
import { UpdatedAtTrigger, UpdateIdColumn } from 'src/decorators';
|
||||
import { person_delete_audit } from 'src/schema/functions';
|
||||
import { AssetFaceTable } from 'src/schema/tables/asset-face.table';
|
||||
import { FaceClusterTable } from 'src/schema/tables/face-cluster.table';
|
||||
import { UserTable } from 'src/schema/tables/user.table';
|
||||
|
||||
@Table('person')
|
||||
@@ -44,6 +43,9 @@ export class PersonTable {
|
||||
@ForeignKeyColumn(() => UserTable, { onDelete: 'CASCADE', onUpdate: 'CASCADE', nullable: false })
|
||||
ownerId!: string;
|
||||
|
||||
@Column({ default: '' })
|
||||
name!: Generated<string>;
|
||||
|
||||
@Column({ default: '' })
|
||||
thumbnailPath!: Generated<string>;
|
||||
|
||||
@@ -53,9 +55,6 @@ export class PersonTable {
|
||||
@Column({ type: 'date', nullable: true })
|
||||
birthDate!: Timestamp | null;
|
||||
|
||||
@Column({ default: '' })
|
||||
name!: Generated<string>;
|
||||
|
||||
@ForeignKeyColumn(() => AssetFaceTable, { onDelete: 'SET NULL', nullable: true })
|
||||
faceAssetId!: string | null;
|
||||
|
||||
@@ -67,7 +66,4 @@ export class PersonTable {
|
||||
|
||||
@UpdateIdColumn({ index: true })
|
||||
updateId!: Generated<string>;
|
||||
|
||||
@ForeignKeyColumn(() => FaceClusterTable, { onDelete: 'CASCADE', onUpdate: 'CASCADE', nullable: true, index: true })
|
||||
faceClusterId!: string | null;
|
||||
}
|
||||
|
||||
@@ -4,7 +4,6 @@ import {
|
||||
CreateDateColumn,
|
||||
DeleteDateColumn,
|
||||
Generated,
|
||||
GeneratedColumn,
|
||||
Index,
|
||||
PrimaryGeneratedColumn,
|
||||
Table,
|
||||
@@ -83,7 +82,4 @@ export class UserTable {
|
||||
|
||||
@UpdateIdColumn({ index: true })
|
||||
updateId!: Generated<string>;
|
||||
|
||||
@GeneratedColumn('uuid')
|
||||
trustedGroupId!: Generated<string>;
|
||||
}
|
||||
|
||||
@@ -8,15 +8,13 @@ import {
|
||||
CreateAlbumDto,
|
||||
GetAlbumsDto,
|
||||
mapAlbum,
|
||||
SharingPermissionsResponseDto,
|
||||
UpdateAlbumDto,
|
||||
UpdateAlbumUserDto,
|
||||
UpdateSharingPermissionsDto,
|
||||
} from 'src/dtos/album.dto';
|
||||
import { BulkIdErrorReason, BulkIdResponseDto, BulkIdsDto } from 'src/dtos/asset-ids.response.dto';
|
||||
import { AuthDto } from 'src/dtos/auth.dto';
|
||||
import { MapMarkerResponseDto } from 'src/dtos/map.dto';
|
||||
import { AlbumUserRole, Permission, SharingPermission } from 'src/enum';
|
||||
import { AlbumUserRole, Permission } from 'src/enum';
|
||||
import { AlbumAssetCount, AlbumInfoOptions } from 'src/repositories/album.repository';
|
||||
import { BaseService } from 'src/services/base.service';
|
||||
import { addAssets, removeAssets } from 'src/utils/asset.util';
|
||||
@@ -132,11 +130,6 @@ export class AlbumService extends BaseService {
|
||||
);
|
||||
|
||||
for (const { userId } of albumUsers) {
|
||||
await this.userRepository.mergeTrustedGroups({
|
||||
userId: auth.user.id,
|
||||
userIdToMerge: userId,
|
||||
});
|
||||
|
||||
await this.eventRepository.emit('AlbumInvite', { id: album.id, userId, senderName: auth.user.name });
|
||||
}
|
||||
|
||||
@@ -306,17 +299,7 @@ export class AlbumService extends BaseService {
|
||||
throw new BadRequestException('Invalid user');
|
||||
}
|
||||
|
||||
await this.userRepository.mergeTrustedGroups({
|
||||
userId: auth.user.id,
|
||||
userIdToMerge: userId,
|
||||
});
|
||||
await this.albumUserRepository.create({
|
||||
userId,
|
||||
albumId: id,
|
||||
role,
|
||||
permissions: [SharingPermission.AssetRead, SharingPermission.ExifRead],
|
||||
});
|
||||
|
||||
await this.albumUserRepository.create({ userId, albumId: id, role });
|
||||
await this.eventRepository.emit('AlbumInvite', { id, userId, senderName: auth.user.name });
|
||||
}
|
||||
|
||||
@@ -355,19 +338,6 @@ export class AlbumService extends BaseService {
|
||||
await this.albumUserRepository.update({ albumId: id, userId }, { role: dto.role });
|
||||
}
|
||||
|
||||
async updateSelf(auth: AuthDto, albumId: string, dto: UpdateSharingPermissionsDto): Promise<void> {
|
||||
await this.requireAccess({ auth, permission: Permission.AlbumAssetCreate, ids: [albumId] });
|
||||
await this.albumUserRepository.update(
|
||||
{ albumId, userId: auth.user.id },
|
||||
{ permissions: dto.permissions, inTimeline: dto.inTimeline },
|
||||
);
|
||||
}
|
||||
|
||||
async getSelf(auth: AuthDto, albumId: string): Promise<SharingPermissionsResponseDto> {
|
||||
await this.requireAccess({ auth, permission: Permission.AlbumAssetCreate, ids: [albumId] });
|
||||
return this.albumUserRepository.get({ userId: auth.user.id, albumId });
|
||||
}
|
||||
|
||||
private async findOrFail(id: string, authUserId: string, options: AlbumInfoOptions) {
|
||||
const album = await this.albumRepository.getById(id, options, authUserId);
|
||||
if (!album) {
|
||||
|
||||
@@ -32,11 +32,10 @@ import {
|
||||
JobStatus,
|
||||
Permission,
|
||||
QueueName,
|
||||
SharingPermission,
|
||||
} from 'src/enum';
|
||||
import { BaseService } from 'src/services/base.service';
|
||||
import { JobItem, JobOf } from 'src/types';
|
||||
import { hasPermissions, requireElevatedPermission } from 'src/utils/access';
|
||||
import { requireElevatedPermission } from 'src/utils/access';
|
||||
import {
|
||||
getAssetFiles,
|
||||
getDimensions,
|
||||
@@ -63,18 +62,14 @@ export class AssetService extends BaseService {
|
||||
async get(auth: AuthDto, id: string): Promise<AssetResponseDto | SanitizedAssetResponseDto> {
|
||||
await this.requireAccess({ auth, permission: Permission.AssetRead, ids: [id] });
|
||||
|
||||
const asset = await this.assetRepository.getById(
|
||||
id,
|
||||
{
|
||||
exifInfo: true,
|
||||
owner: true,
|
||||
faces: { person: true },
|
||||
stack: { assets: true },
|
||||
edits: true,
|
||||
tags: true,
|
||||
},
|
||||
auth.user.id,
|
||||
);
|
||||
const asset = await this.assetRepository.getById(id, {
|
||||
exifInfo: true,
|
||||
owner: true,
|
||||
faces: { person: true },
|
||||
stack: { assets: true },
|
||||
edits: true,
|
||||
tags: true,
|
||||
});
|
||||
|
||||
if (!asset) {
|
||||
throw new BadRequestException('Asset not found');
|
||||
@@ -90,7 +85,7 @@ export class AssetService extends BaseService {
|
||||
delete data.owner;
|
||||
}
|
||||
|
||||
if (!hasPermissions(data, SharingPermission.PersonRead)) {
|
||||
if (data.ownerId !== auth.user.id || auth.sharedLink) {
|
||||
data.people = [];
|
||||
}
|
||||
|
||||
|
||||
@@ -85,11 +85,7 @@ export class NotificationService extends BaseService {
|
||||
return;
|
||||
}
|
||||
|
||||
this.logger.error(
|
||||
`Unable to run job handler (${job.name}): ${error}`,
|
||||
error?.stack,
|
||||
'data' in job ? JSON.stringify(job.data) : {},
|
||||
);
|
||||
this.logger.error(`Unable to run job handler (${job.name}): ${error}`, error?.stack, JSON.stringify(job.data));
|
||||
|
||||
switch (job.name) {
|
||||
case JobName.DatabaseBackup: {
|
||||
|
||||
@@ -3,7 +3,7 @@ import { Partner } from 'src/database';
|
||||
import { AuthDto } from 'src/dtos/auth.dto';
|
||||
import { PartnerCreateDto, PartnerResponseDto, PartnerSearchDto, PartnerUpdateDto } from 'src/dtos/partner.dto';
|
||||
import { mapUser } from 'src/dtos/user.dto';
|
||||
import { JobName, Permission, SharingPermission } from 'src/enum';
|
||||
import { Permission } from 'src/enum';
|
||||
import { PartnerDirection, PartnerIds } from 'src/repositories/partner.repository';
|
||||
import { BaseService } from 'src/services/base.service';
|
||||
|
||||
@@ -16,15 +16,7 @@ export class PartnerService extends BaseService {
|
||||
throw new BadRequestException(`Partner already exists`);
|
||||
}
|
||||
|
||||
const { numUpdatedRows } = await this.userRepository.mergeTrustedGroups({
|
||||
userId: auth.user.id,
|
||||
userIdToMerge: sharedWithId,
|
||||
});
|
||||
const partner = await this.partnerRepository.create({ ...partnerId, permissions: [SharingPermission.All] });
|
||||
if (numUpdatedRows > 0) {
|
||||
await this.jobRepository.queue({ name: JobName.FacialRecognitionMerge, data: { id: sharedWithId } });
|
||||
}
|
||||
|
||||
const partner = await this.partnerRepository.create(partnerId);
|
||||
return this.mapPartner(partner, PartnerDirection.SharedBy);
|
||||
}
|
||||
|
||||
@@ -36,10 +28,6 @@ export class PartnerService extends BaseService {
|
||||
}
|
||||
|
||||
await this.partnerRepository.remove(partnerId);
|
||||
const { numUpdatedRows } = await this.userRepository.updateTrustedGroups(auth.user.id);
|
||||
if (numUpdatedRows > 0) {
|
||||
await this.jobRepository.queue({ name: JobName.FacialRecognitionQueueAll, data: { force: true } });
|
||||
}
|
||||
}
|
||||
|
||||
async search(auth: AuthDto, { direction }: PartnerSearchDto): Promise<PartnerResponseDto[]> {
|
||||
|
||||
@@ -13,7 +13,7 @@ import {
|
||||
FaceDto,
|
||||
mapFaces,
|
||||
mapPerson,
|
||||
MergeFaceClusterDto,
|
||||
MergePersonDto,
|
||||
PeopleResponseDto,
|
||||
PeopleUpdateDto,
|
||||
PersonCreateDto,
|
||||
@@ -125,11 +125,11 @@ export class PersonService extends BaseService {
|
||||
|
||||
async getFacesById(auth: AuthDto, dto: FaceDto): Promise<AssetFaceResponseDto[]> {
|
||||
await this.requireAccess({ auth, permission: Permission.AssetRead, ids: [dto.id] });
|
||||
const faces = await this.personRepository.getFaces(dto.id, { userId: auth.user.id });
|
||||
const faces = await this.personRepository.getFaces(dto.id);
|
||||
const asset = await this.assetRepository.getForFaces(dto.id);
|
||||
const assetDimensions = getDimensions(asset);
|
||||
|
||||
return faces.map((face) => mapFaces(face, asset.edits, assetDimensions));
|
||||
return faces.map((face) => mapFaces(face, auth, asset.edits, assetDimensions));
|
||||
}
|
||||
|
||||
async createNewFeaturePhoto(changeFeaturePhoto: string[]) {
|
||||
@@ -157,7 +157,7 @@ export class PersonService extends BaseService {
|
||||
|
||||
async getStatistics(auth: AuthDto, id: string): Promise<PersonStatisticsResponseDto> {
|
||||
await this.requireAccess({ auth, permission: Permission.PersonRead, ids: [id] });
|
||||
return this.personRepository.getStatistics(auth.user.id, id);
|
||||
return this.personRepository.getStatistics(id);
|
||||
}
|
||||
|
||||
async getThumbnail(auth: AuthDto, id: string): Promise<ImmichFileResponse> {
|
||||
@@ -436,7 +436,7 @@ export class PersonService extends BaseService {
|
||||
|
||||
const lastRun = new Date().toISOString();
|
||||
const facePagination = this.personRepository.getAllFaces(
|
||||
force ? undefined : { faceClusterId: null, sourceType: SourceType.MachineLearning },
|
||||
force ? undefined : { personId: null, sourceType: SourceType.MachineLearning },
|
||||
);
|
||||
|
||||
let jobs: { name: JobName.FacialRecognition; data: { id: string; deferred: false } }[] = [];
|
||||
@@ -479,8 +479,8 @@ export class PersonService extends BaseService {
|
||||
return JobStatus.Failed;
|
||||
}
|
||||
|
||||
if (face.faceClusterId) {
|
||||
this.logger.debug(`Face ${id} already belongs to a face cluster`);
|
||||
if (face.personId) {
|
||||
this.logger.debug(`Face ${id} already has a person assigned`);
|
||||
return JobStatus.Skipped;
|
||||
}
|
||||
|
||||
@@ -509,8 +509,8 @@ export class PersonService extends BaseService {
|
||||
return JobStatus.Skipped;
|
||||
}
|
||||
|
||||
let faceClusterId = matches.find((match) => match.faceClusterId)?.faceClusterId;
|
||||
if (!faceClusterId) {
|
||||
let personId = matches.find((match) => match.personId)?.personId;
|
||||
if (!personId) {
|
||||
const matchWithPerson = await this.searchRepository.searchFaces({
|
||||
userIds: [face.asset.ownerId],
|
||||
embedding: face.faceSearch.embedding,
|
||||
@@ -521,109 +521,20 @@ export class PersonService extends BaseService {
|
||||
});
|
||||
|
||||
if (matchWithPerson.length > 0) {
|
||||
faceClusterId = matchWithPerson[0].faceClusterId;
|
||||
personId = matchWithPerson[0].personId;
|
||||
}
|
||||
}
|
||||
|
||||
if (isCore && !faceClusterId) {
|
||||
if (isCore && !personId) {
|
||||
this.logger.log(`Creating new person for face ${id}`);
|
||||
const newPerson = await this.personRepository.create({ ownerId: face.asset.ownerId, faceAssetId: face.id });
|
||||
await this.jobRepository.queue({ name: JobName.PersonGenerateThumbnail, data: { id: newPerson.id } });
|
||||
faceClusterId = newPerson.faceClusterId;
|
||||
personId = newPerson.id;
|
||||
}
|
||||
|
||||
if (faceClusterId) {
|
||||
this.logger.debug(`Assigning face ${id} to face cluster ${faceClusterId}`);
|
||||
await this.personRepository.reassignFaces({ faceIds: [id], newFaceClusterId: faceClusterId });
|
||||
}
|
||||
|
||||
return JobStatus.Success;
|
||||
}
|
||||
|
||||
@OnJob({ name: JobName.FacialRecognitionMerge, queue: QueueName.FacialRecognition })
|
||||
async mergeClusters({ id: userId }: JobOf<JobName.FacialRecognitionMerge>): Promise<JobStatus> {
|
||||
const { machineLearning } = await this.getConfig({ withCache: true });
|
||||
if (!isFacialRecognitionEnabled(machineLearning)) {
|
||||
return JobStatus.Skipped;
|
||||
}
|
||||
|
||||
const faces = this.personRepository.getAllFaces({ sourceType: SourceType.MachineLearning });
|
||||
for await (const { id } of faces) {
|
||||
const face = await this.personRepository.getFaceForFacialRecognitionJob(id);
|
||||
if (!face?.faceSearch || !face.asset) {
|
||||
this.logger.warn(`Face ${id} does not have an embedding`);
|
||||
continue;
|
||||
}
|
||||
|
||||
let faceClusterId: string | null = null;
|
||||
let personId: string | null = null;
|
||||
const matchWithPerson = await this.searchRepository.searchFaces({
|
||||
userIds: [face.asset.ownerId],
|
||||
embedding: face.faceSearch.embedding,
|
||||
maxDistance: machineLearning.facialRecognition.maxDistance,
|
||||
numResults: 100,
|
||||
hasPerson: true,
|
||||
minBirthDate: new Date(face.asset.fileCreatedAt),
|
||||
});
|
||||
|
||||
if (matchWithPerson.length > 0) {
|
||||
// favor a person that's not owned by us to merge people with a newly shared with user
|
||||
// probably do smarter stuff here like pick the person with a name, if both have a name set aliases or whatever
|
||||
const match = matchWithPerson.find((match) => match.ownerId !== userId) ?? matchWithPerson[0];
|
||||
if (match.faceClusterId && face.asset.ownerId !== match.ownerId) {
|
||||
// TODO should probably be a DB constraint?
|
||||
const people = await this.personRepository.getByFaceClusterId(match.faceClusterId);
|
||||
if (!people.some((person) => person.ownerId === face.asset?.ownerId)) {
|
||||
const { id } = await this.personRepository.create({
|
||||
ownerId: face.asset.ownerId,
|
||||
faceClusterId: match.faceClusterId,
|
||||
});
|
||||
personId = id;
|
||||
}
|
||||
}
|
||||
|
||||
faceClusterId = match.faceClusterId;
|
||||
}
|
||||
|
||||
if (!faceClusterId) {
|
||||
const matches = await this.searchRepository.searchFaces({
|
||||
userIds: [userId],
|
||||
embedding: face.faceSearch.embedding,
|
||||
maxDistance: machineLearning.facialRecognition.maxDistance,
|
||||
numResults: machineLearning.facialRecognition.minFaces,
|
||||
minBirthDate: new Date(face.asset.fileCreatedAt),
|
||||
});
|
||||
|
||||
const match = matches.find((match) => match.faceClusterId);
|
||||
if (
|
||||
match &&
|
||||
match.faceClusterId &&
|
||||
face.asset.ownerId !== match.ownerId &&
|
||||
matches.length >= machineLearning.facialRecognition.minFaces
|
||||
) {
|
||||
// TODO should probably be a DB constraint?
|
||||
const people = await this.personRepository.getByFaceClusterId(match.faceClusterId);
|
||||
|
||||
if (!people.some((person) => person.ownerId === face.asset?.ownerId)) {
|
||||
const { id } = await this.personRepository.create({
|
||||
ownerId: face.asset.ownerId,
|
||||
faceClusterId: match.faceClusterId,
|
||||
});
|
||||
personId = id;
|
||||
}
|
||||
}
|
||||
|
||||
faceClusterId = match?.faceClusterId ?? null;
|
||||
}
|
||||
|
||||
if (faceClusterId) {
|
||||
this.logger.log(`Assigning face ${id} to face cluster ${faceClusterId}`);
|
||||
await this.personRepository.reassignFaces({ faceIds: [id], newFaceClusterId: faceClusterId });
|
||||
}
|
||||
|
||||
if (personId) {
|
||||
await this.createNewFeaturePhoto([personId]);
|
||||
}
|
||||
if (personId) {
|
||||
this.logger.debug(`Assigning face ${id} to person ${personId}`);
|
||||
await this.personRepository.reassignFaces({ faceIds: [id], newPersonId: personId });
|
||||
}
|
||||
|
||||
return JobStatus.Success;
|
||||
@@ -641,7 +552,7 @@ export class PersonService extends BaseService {
|
||||
return JobStatus.Success;
|
||||
}
|
||||
|
||||
async mergePerson(auth: AuthDto, id: string, dto: MergeFaceClusterDto): Promise<BulkIdResponseDto[]> {
|
||||
async mergePerson(auth: AuthDto, id: string, dto: MergePersonDto): Promise<BulkIdResponseDto[]> {
|
||||
const mergeIds = dto.ids;
|
||||
if (mergeIds.includes(id)) {
|
||||
throw new BadRequestException('Cannot merge a person into themselves');
|
||||
@@ -687,7 +598,7 @@ export class PersonService extends BaseService {
|
||||
}
|
||||
|
||||
const mergeName = mergePerson.name || mergePerson.id;
|
||||
const mergeData: UpdateFacesData = { oldFaceClusterId: mergeId, newFaceClusterId: id };
|
||||
const mergeData: UpdateFacesData = { oldPersonId: mergeId, newPersonId: id };
|
||||
this.logger.log(`Merging ${mergeName} into ${primaryName}`);
|
||||
|
||||
await this.personRepository.reassignFaces(mergeData);
|
||||
@@ -700,7 +611,6 @@ export class PersonService extends BaseService {
|
||||
results.push({ id: mergeId, success: false, error: BulkIdErrorReason.UNKNOWN });
|
||||
}
|
||||
}
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
@@ -770,12 +680,8 @@ export class PersonService extends BaseService {
|
||||
dto.imageHeight = originalDimensions.height;
|
||||
}
|
||||
|
||||
if (!person?.faceClusterId) {
|
||||
throw new Error('Person must already have some recognized faces and belong to a face cluster');
|
||||
}
|
||||
|
||||
await this.personRepository.createAssetFace({
|
||||
faceClusterId: person.faceClusterId,
|
||||
personId: dto.personId,
|
||||
assetId: dto.assetId,
|
||||
imageHeight: dto.imageHeight,
|
||||
imageWidth: dto.imageWidth,
|
||||
|
||||
@@ -212,7 +212,6 @@ export class SearchService extends BaseService {
|
||||
repository: this.partnerRepository,
|
||||
timelineEnabled: true,
|
||||
});
|
||||
console.log(auth.user.id, partnerIds);
|
||||
return [auth.user.id, ...partnerIds];
|
||||
}
|
||||
|
||||
|
||||
+1
-4
@@ -220,9 +220,7 @@ export type ConcurrentQueueName = Exclude<
|
||||
| QueueName.BackupDatabase
|
||||
>;
|
||||
|
||||
export type Jobs = {
|
||||
[K in JobItem['name']]: 'data' extends keyof (JobItem & { name: K }) ? (JobItem & { name: K })['data'] : never;
|
||||
};
|
||||
export type Jobs = { [K in JobItem['name']]: (JobItem & { name: K })['data'] };
|
||||
export type JobOf<T extends JobName> = Jobs[T];
|
||||
|
||||
export interface IBaseJob {
|
||||
@@ -406,7 +404,6 @@ export type JobItem =
|
||||
| { name: JobName.AssetDetectFaces; data: IEntityJob }
|
||||
| { name: JobName.FacialRecognitionQueueAll; data: INightlyJob }
|
||||
| { name: JobName.FacialRecognition; data: IDeferrableJob }
|
||||
| { name: JobName.FacialRecognitionMerge; data: IEntityJob }
|
||||
| { name: JobName.PersonGenerateThumbnail; data: IEntityJob }
|
||||
|
||||
// Smart Search
|
||||
|
||||
+22
-82
@@ -1,7 +1,7 @@
|
||||
import { BadRequestException, UnauthorizedException } from '@nestjs/common';
|
||||
import { AuthSharedLink } from 'src/database';
|
||||
import { AuthDto } from 'src/dtos/auth.dto';
|
||||
import { AlbumUserRole, Permission, SharingPermission } from 'src/enum';
|
||||
import { AlbumUserRole, Permission } from 'src/enum';
|
||||
import { AccessRepository } from 'src/repositories/access.repository';
|
||||
import { setDifference, setIsEqual, setIsSuperset, setUnion } from 'src/utils/set';
|
||||
|
||||
@@ -115,41 +115,37 @@ const checkOtherAccess = async (access: AccessRepository, request: OtherAccessRe
|
||||
|
||||
case Permission.AssetRead: {
|
||||
const isOwner = await access.asset.checkOwnerAccess(auth.user.id, ids, auth.session?.hasElevatedPermission);
|
||||
const isShared = await access.asset.checkSharedAccess(auth.user.id, ids, [SharingPermission.AssetRead]);
|
||||
return setUnion(isOwner, isShared);
|
||||
const isAlbum = await access.asset.checkAlbumAccess(auth.user.id, setDifference(ids, isOwner));
|
||||
const isPartner = await access.asset.checkPartnerAccess(auth.user.id, setDifference(ids, isOwner, isAlbum));
|
||||
return setUnion(isOwner, isAlbum, isPartner);
|
||||
}
|
||||
|
||||
case Permission.AssetShare: {
|
||||
const isOwner = await access.asset.checkOwnerAccess(auth.user.id, ids, false);
|
||||
const isShared = await access.asset.checkSharedAccess(auth.user.id, ids, [SharingPermission.AssetShare]);
|
||||
return setUnion(isOwner, isShared);
|
||||
const isPartner = await access.asset.checkPartnerAccess(auth.user.id, setDifference(ids, isOwner));
|
||||
return setUnion(isOwner, isPartner);
|
||||
}
|
||||
|
||||
case Permission.AssetView: {
|
||||
const isOwner = await access.asset.checkOwnerAccess(auth.user.id, ids, auth.session?.hasElevatedPermission);
|
||||
const isShared = await access.asset.checkSharedAccess(auth.user.id, ids, [SharingPermission.AssetRead]);
|
||||
return setUnion(isOwner, isShared);
|
||||
const isAlbum = await access.asset.checkAlbumAccess(auth.user.id, setDifference(ids, isOwner));
|
||||
const isPartner = await access.asset.checkPartnerAccess(auth.user.id, setDifference(ids, isOwner, isAlbum));
|
||||
return setUnion(isOwner, isAlbum, isPartner);
|
||||
}
|
||||
|
||||
case Permission.AssetDownload: {
|
||||
const isOwner = await access.asset.checkOwnerAccess(auth.user.id, ids, auth.session?.hasElevatedPermission);
|
||||
const isShared = await access.asset.checkSharedAccess(auth.user.id, ids, [
|
||||
SharingPermission.AssetRead,
|
||||
SharingPermission.ExifRead,
|
||||
]);
|
||||
return setUnion(isOwner, isShared);
|
||||
const isAlbum = await access.asset.checkAlbumAccess(auth.user.id, setDifference(ids, isOwner));
|
||||
const isPartner = await access.asset.checkPartnerAccess(auth.user.id, setDifference(ids, isOwner, isAlbum));
|
||||
return setUnion(isOwner, isAlbum, isPartner);
|
||||
}
|
||||
|
||||
case Permission.AssetUpdate: {
|
||||
const isOwner = await access.asset.checkOwnerAccess(auth.user.id, ids, auth.session?.hasElevatedPermission);
|
||||
const isShared = await access.asset.checkSharedAccess(auth.user.id, ids, [SharingPermission.AssetUpdate]);
|
||||
return setUnion(isOwner, isShared);
|
||||
return await access.asset.checkOwnerAccess(auth.user.id, ids, auth.session?.hasElevatedPermission);
|
||||
}
|
||||
|
||||
case Permission.AssetDelete: {
|
||||
const isOwner = await access.asset.checkOwnerAccess(auth.user.id, ids, auth.session?.hasElevatedPermission);
|
||||
const isShared = await access.asset.checkSharedAccess(auth.user.id, ids, [SharingPermission.AssetDelete]);
|
||||
return setUnion(isOwner, isShared);
|
||||
return await access.asset.checkOwnerAccess(auth.user.id, ids, auth.session?.hasElevatedPermission);
|
||||
}
|
||||
|
||||
case Permission.AssetCopy: {
|
||||
@@ -157,21 +153,15 @@ const checkOtherAccess = async (access: AccessRepository, request: OtherAccessRe
|
||||
}
|
||||
|
||||
case Permission.AssetEditGet: {
|
||||
const isOwner = await access.asset.checkOwnerAccess(auth.user.id, ids, auth.session?.hasElevatedPermission);
|
||||
const isShared = await access.asset.checkSharedAccess(auth.user.id, ids, [SharingPermission.AssetEdit]);
|
||||
return setUnion(isOwner, isShared);
|
||||
return await access.asset.checkOwnerAccess(auth.user.id, ids, auth.session?.hasElevatedPermission);
|
||||
}
|
||||
|
||||
case Permission.AssetEditCreate: {
|
||||
const isOwner = await access.asset.checkOwnerAccess(auth.user.id, ids, auth.session?.hasElevatedPermission);
|
||||
const isShared = await access.asset.checkSharedAccess(auth.user.id, ids, [SharingPermission.AssetEdit]);
|
||||
return setUnion(isOwner, isShared);
|
||||
return await access.asset.checkOwnerAccess(auth.user.id, ids, auth.session?.hasElevatedPermission);
|
||||
}
|
||||
|
||||
case Permission.AssetEditDelete: {
|
||||
const isOwner = await access.asset.checkOwnerAccess(auth.user.id, ids, auth.session?.hasElevatedPermission);
|
||||
const isShared = await access.asset.checkSharedAccess(auth.user.id, ids, [SharingPermission.AssetEdit]);
|
||||
return setUnion(isOwner, isShared);
|
||||
return await access.asset.checkOwnerAccess(auth.user.id, ids, auth.session?.hasElevatedPermission);
|
||||
}
|
||||
|
||||
case Permission.AlbumRead: {
|
||||
@@ -256,11 +246,7 @@ const checkOtherAccess = async (access: AccessRepository, request: OtherAccessRe
|
||||
}
|
||||
|
||||
case Permission.FaceDelete: {
|
||||
const isOwner = await access.person.checkFaceOwnerAccess(auth.user.id, ids);
|
||||
const isShared = await access.person.checkSharedFaceAccess(auth.user.id, setDifference(ids, isOwner), [
|
||||
SharingPermission.AssetUpdate,
|
||||
]);
|
||||
return setUnion(isOwner, isShared);
|
||||
return access.person.checkFaceOwnerAccess(auth.user.id, ids);
|
||||
}
|
||||
|
||||
case Permission.NotificationRead:
|
||||
@@ -302,40 +288,11 @@ const checkOtherAccess = async (access: AccessRepository, request: OtherAccessRe
|
||||
return access.person.checkFaceOwnerAccess(auth.user.id, ids);
|
||||
}
|
||||
|
||||
case Permission.PersonRead: {
|
||||
const isOwner = await access.person.checkOwnerAccess(auth.user.id, ids);
|
||||
const isShared = await access.person.checkSharedAccess(auth.user.id, setDifference(ids, isOwner), [
|
||||
SharingPermission.PersonRead,
|
||||
]);
|
||||
|
||||
return setUnion(isOwner, isShared);
|
||||
}
|
||||
|
||||
case Permission.PersonRead:
|
||||
case Permission.PersonUpdate:
|
||||
case Permission.PersonDelete:
|
||||
case Permission.PersonMerge: {
|
||||
const isOwner = await access.person.checkOwnerAccess(auth.user.id, ids);
|
||||
const isShared = await access.person.checkSharedAccess(auth.user.id, setDifference(ids, isOwner), [
|
||||
SharingPermission.PersonMerge,
|
||||
]);
|
||||
|
||||
return setUnion(isOwner, isShared);
|
||||
}
|
||||
|
||||
case Permission.PersonUpdate: {
|
||||
const isOwner = await access.person.checkOwnerAccess(auth.user.id, ids);
|
||||
const isShared = await access.person.checkSharedAccess(auth.user.id, setDifference(ids, isOwner), [
|
||||
SharingPermission.PersonUpdate,
|
||||
]);
|
||||
|
||||
return setUnion(isOwner, isShared);
|
||||
}
|
||||
|
||||
case Permission.PersonDelete: {
|
||||
const isOwner = await access.person.checkOwnerAccess(auth.user.id, ids);
|
||||
const isShared = await access.person.checkSharedAccess(auth.user.id, setDifference(ids, isOwner), [
|
||||
SharingPermission.PersonDelete,
|
||||
]);
|
||||
|
||||
return setUnion(isOwner, isShared);
|
||||
return await access.person.checkOwnerAccess(auth.user.id, ids);
|
||||
}
|
||||
|
||||
case Permission.PersonReassign: {
|
||||
@@ -382,20 +339,3 @@ export const requireElevatedPermission = (auth: AuthDto) => {
|
||||
throw new UnauthorizedException('Elevated permission is required');
|
||||
}
|
||||
};
|
||||
|
||||
export const hasPermissions = (
|
||||
assetLike: { permissions: SharingPermission[] },
|
||||
...permissions: SharingPermission[]
|
||||
) => {
|
||||
if (assetLike.permissions.includes(SharingPermission.All)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
for (const permission of permissions) {
|
||||
if (!assetLike.permissions.includes(permission)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
};
|
||||
|
||||
@@ -4,7 +4,7 @@ import { AssetFile } from 'src/database';
|
||||
import { BulkIdErrorReason, BulkIdResponseDto } from 'src/dtos/asset-ids.response.dto';
|
||||
import { UploadFieldName } from 'src/dtos/asset-media.dto';
|
||||
import { AuthDto } from 'src/dtos/auth.dto';
|
||||
import { AssetFileType, AssetType, AssetVisibility, Permission, SharingPermission } from 'src/enum';
|
||||
import { AssetFileType, AssetType, AssetVisibility, Permission } from 'src/enum';
|
||||
import { AuthRequest } from 'src/middleware/auth.guard';
|
||||
import { AccessRepository } from 'src/repositories/access.repository';
|
||||
import { AssetRepository } from 'src/repositories/asset.repository';
|
||||
@@ -134,11 +134,6 @@ export const getMyPartnerIds = async ({ userId, repository, timelineEnabled }: P
|
||||
continue;
|
||||
}
|
||||
|
||||
const permissions = [SharingPermission.All, SharingPermission.AssetRead];
|
||||
if (!permissions.some((permission) => partner.permissions.includes(permission))) {
|
||||
continue;
|
||||
}
|
||||
|
||||
partnerIds.add(partner.sharedById);
|
||||
}
|
||||
|
||||
|
||||
@@ -15,17 +15,9 @@ import {
|
||||
import { PostgresJSDialect } from 'kysely-postgres-js';
|
||||
import { jsonArrayFrom, jsonObjectFrom } from 'kysely/helpers/postgres';
|
||||
import { Notice, PostgresError } from 'postgres';
|
||||
import { columns, lockableProperties, LockableProperty } from 'src/database';
|
||||
import { columns, lockableProperties, LockableProperty, Person } from 'src/database';
|
||||
import { AssetEditActionItem } from 'src/dtos/editing.dto';
|
||||
import {
|
||||
AssetFileType,
|
||||
AssetOrderBy,
|
||||
AssetVisibility,
|
||||
DatabaseExtension,
|
||||
ExifOrientation,
|
||||
SharingPermission,
|
||||
} from 'src/enum';
|
||||
import { hasAssetPermissions } from 'src/repositories/asset.repository';
|
||||
import { AssetFileType, AssetOrderBy, AssetVisibility, DatabaseExtension, ExifOrientation } from 'src/enum';
|
||||
import { AssetSearchBuilderOptions } from 'src/repositories/search.repository';
|
||||
import { DB } from 'src/schema';
|
||||
import { AssetExifTable } from 'src/schema/tables/asset-exif.table';
|
||||
@@ -223,22 +215,19 @@ export function withFilePath(eb: ExpressionBuilder<DB, 'asset'>, type: AssetFile
|
||||
|
||||
export function withFacesAndPeople(
|
||||
eb: ExpressionBuilder<DB, 'asset'>,
|
||||
{ withHidden, withDeletedFace, userId: _ }: { withHidden?: boolean; withDeletedFace?: boolean; userId?: string } = {},
|
||||
withHidden?: boolean,
|
||||
withDeletedFace?: boolean,
|
||||
) {
|
||||
return jsonArrayFrom(
|
||||
eb
|
||||
.selectFrom('asset_face')
|
||||
.select((eb) =>
|
||||
jsonObjectFrom(
|
||||
eb
|
||||
.selectFrom('face_cluster')
|
||||
.whereRef('face_cluster.id', '=', 'asset_face.faceClusterId')
|
||||
.innerJoin('person', 'person.faceClusterId', 'face_cluster.id')
|
||||
.selectAll('person')
|
||||
.limit(1),
|
||||
).as('person'),
|
||||
.leftJoinLateral(
|
||||
(eb) =>
|
||||
eb.selectFrom('person').selectAll('person').whereRef('asset_face.personId', '=', 'person.id').as('person'),
|
||||
(join) => join.onTrue(),
|
||||
)
|
||||
.selectAll('asset_face')
|
||||
.select((eb) => eb.table('person').$castTo<ShallowDehydrateObject<Person>>().as('person'))
|
||||
.whereRef('asset_face.assetId', '=', 'asset.id')
|
||||
.$if(!withDeletedFace, (qb) => qb.where('asset_face.deletedAt', 'is', null))
|
||||
.$if(!withHidden, (qb) => qb.where('asset_face.isVisible', 'is', true)),
|
||||
@@ -251,12 +240,11 @@ export function hasPeople<O>(qb: SelectQueryBuilder<DB, 'asset', O>, personIds:
|
||||
eb
|
||||
.selectFrom('asset_face')
|
||||
.select('assetId')
|
||||
.innerJoin('person', 'person.faceClusterId', 'asset_face.faceClusterId')
|
||||
.where('person.id', '=', anyUuid(personIds!))
|
||||
.where('personId', '=', anyUuid(personIds!))
|
||||
.where('deletedAt', 'is', null)
|
||||
.where('isVisible', 'is', true)
|
||||
.groupBy('assetId')
|
||||
.having((eb) => eb.fn.count('person.id').distinct(), '=', personIds.length)
|
||||
.having((eb) => eb.fn.count('personId').distinct(), '=', personIds.length)
|
||||
.as('has_people'),
|
||||
(join) => join.onRef('has_people.assetId', '=', 'asset.id'),
|
||||
);
|
||||
@@ -317,30 +305,6 @@ export function truncatedDate<O>(order: AssetOrderBy = AssetOrderBy.TakenAt, siz
|
||||
return sql<O>`date_trunc(${sql.lit(size ?? 'MONTH')}, ${sql.ref(order === AssetOrderBy.CreatedAt ? 'asset.createdAt' : 'localDateTime')} AT TIME ZONE 'UTC') AT TIME ZONE 'UTC'`;
|
||||
}
|
||||
|
||||
export function withPermissions(userId: string) {
|
||||
return (eb: ExpressionBuilder<DB, 'asset'>) =>
|
||||
jsonArrayFrom(
|
||||
eb
|
||||
.selectFrom('album_user')
|
||||
.select((eb) => eb.fn<SharingPermission>('unnest', ['album_user.permissions']).as('permission'))
|
||||
.distinct()
|
||||
.innerJoin('album_asset', 'album_user.albumId', 'album_asset.albumId')
|
||||
.whereRef('album_asset.assetId', '=', 'asset.id')
|
||||
.whereRef('album_user.userId', '=', 'asset.ownerId')
|
||||
.where('album_user.albumId', 'in', (eb) =>
|
||||
eb.selectFrom('album_user').select('album_user.albumId').where('album_user.userId', '=', userId),
|
||||
)
|
||||
.union(
|
||||
eb
|
||||
.selectFrom('partner')
|
||||
.select((eb) => eb.fn<SharingPermission>('unnest', ['partner.permissions']).as('permission'))
|
||||
.distinct()
|
||||
.whereRef('partner.sharedById', '=', 'asset.ownerId')
|
||||
.where('partner.sharedWithId', '=', userId),
|
||||
),
|
||||
).as('permissions');
|
||||
}
|
||||
|
||||
export function withTagId<O>(qb: SelectQueryBuilder<DB, 'asset', O>, tagId: string) {
|
||||
return qb.where((eb) =>
|
||||
eb.exists(
|
||||
@@ -467,7 +431,7 @@ export function searchAssetBuilder(kysely: Kysely<DB>, options: AssetSearchBuild
|
||||
.$if(!!options.checksum, (qb) => qb.where('asset.checksum', '=', options.checksum!))
|
||||
.$if(!!options.id, (qb) => qb.where('asset.id', '=', asUuid(options.id!)))
|
||||
.$if(!!options.libraryId, (qb) => qb.where('asset.libraryId', '=', asUuid(options.libraryId!)))
|
||||
.$if(!!options.userIds, (qb) => qb.where(hasAssetPermissions(options.userIds![0], [SharingPermission.AssetRead])))
|
||||
.$if(!!options.userIds, (qb) => qb.where('asset.ownerId', '=', anyUuid(options.userIds!)))
|
||||
.$if(!!options.encodedVideoPath, (qb) =>
|
||||
qb
|
||||
.innerJoin('asset_file', (join) =>
|
||||
|
||||
@@ -38,7 +38,6 @@ const createAsset = (
|
||||
fileSizeInByte !== null || Object.keys(exifFields).length > 0
|
||||
? ExifResponseSchema.parse({ fileSizeInByte, ...exifFields })
|
||||
: undefined,
|
||||
permissions: [],
|
||||
});
|
||||
|
||||
describe('duplicate utils', () => {
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { AssetFace } from 'src/database';
|
||||
import { AssetOcrResponseDto } from 'src/dtos/ocr.dto';
|
||||
import { ImageDimensions } from 'src/types';
|
||||
|
||||
@@ -30,21 +31,11 @@ const scale = (box: BoundingBox, target: ImageDimensions, source?: ImageDimensio
|
||||
};
|
||||
};
|
||||
|
||||
export const checkFaceVisibility = <
|
||||
T extends {
|
||||
isVisible: boolean;
|
||||
boundingBoxX1: number;
|
||||
boundingBoxX2: number;
|
||||
boundingBoxY1: number;
|
||||
boundingBoxY2: number;
|
||||
imageHeight: number;
|
||||
imageWidth: number;
|
||||
},
|
||||
>(
|
||||
faces: T[],
|
||||
export const checkFaceVisibility = (
|
||||
faces: AssetFace[],
|
||||
originalAssetDimensions: ImageDimensions,
|
||||
crop?: BoundingBox,
|
||||
): { visible: T[]; hidden: T[] } => {
|
||||
): { visible: AssetFace[]; hidden: AssetFace[] } => {
|
||||
if (!crop) {
|
||||
return {
|
||||
visible: faces.filter((face) => !face.isVisible),
|
||||
|
||||
@@ -28,8 +28,6 @@ export class AlbumUserFactory {
|
||||
createdAt: newDate(),
|
||||
updateId: newUuidV7(),
|
||||
updatedAt: newDate(),
|
||||
permissions: [],
|
||||
inTimeline: false,
|
||||
...dto,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -26,7 +26,6 @@ export class PartnerFactory {
|
||||
sharedWithId,
|
||||
updatedAt: newDate(),
|
||||
updateId: newUuidV7(),
|
||||
permissions: [],
|
||||
...dto,
|
||||
})
|
||||
.sharedBy({ id: sharedById })
|
||||
|
||||
@@ -35,7 +35,6 @@ export class UserFactory {
|
||||
status: UserStatus.Active,
|
||||
profileChangedAt: newDate(),
|
||||
updateId: newUuidV7(),
|
||||
trustedGroupId: newUuid(),
|
||||
...dto,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -21,7 +21,6 @@ export const newAccessRepositoryMock = (): IAccessRepositoryMock => {
|
||||
checkAlbumAccess: vitest.fn().mockResolvedValue(new Set()),
|
||||
checkPartnerAccess: vitest.fn().mockResolvedValue(new Set()),
|
||||
checkSharedLinkAccess: vitest.fn().mockResolvedValue(new Set()),
|
||||
checkSharedAccess: vitest.fn().mockResolvedValue(new Set()),
|
||||
},
|
||||
|
||||
album: {
|
||||
@@ -49,8 +48,6 @@ export const newAccessRepositoryMock = (): IAccessRepositoryMock => {
|
||||
person: {
|
||||
checkFaceOwnerAccess: vitest.fn().mockResolvedValue(new Set()),
|
||||
checkOwnerAccess: vitest.fn().mockResolvedValue(new Set()),
|
||||
checkSharedAccess: vitest.fn().mockResolvedValue(new Set()),
|
||||
checkSharedFaceAccess: vitest.fn().mockResolvedValue(new Set()),
|
||||
},
|
||||
|
||||
partner: {
|
||||
|
||||
@@ -21,13 +21,12 @@
|
||||
import { getAlbumAssetActions } from '$lib/services/album.service';
|
||||
import { getGlobalActions } from '$lib/services/app.service';
|
||||
import { getAssetActions } from '$lib/services/asset.service';
|
||||
import { getSharedLink, hasPermissions, withoutIcons } from '$lib/utils';
|
||||
import { getSharedLink, withoutIcons } from '$lib/utils';
|
||||
import type { OnUndoDelete } from '$lib/utils/actions';
|
||||
import { toTimelineAsset } from '$lib/utils/timeline-util';
|
||||
import {
|
||||
AssetTypeEnum,
|
||||
AssetVisibility,
|
||||
SharingPermission,
|
||||
type AlbumResponseDto,
|
||||
type AssetResponseDto,
|
||||
type PersonResponseDto,
|
||||
@@ -131,7 +130,7 @@
|
||||
|
||||
<ActionButton action={Actions.Edit} />
|
||||
|
||||
{#if hasPermissions(asset, SharingPermission.AssetDelete)}
|
||||
{#if isOwner}
|
||||
<DeleteAction {asset} {onAction} {preAction} {onUndoDelete} />
|
||||
{/if}
|
||||
|
||||
@@ -147,7 +146,7 @@
|
||||
{/if}
|
||||
|
||||
<ActionMenuItem action={Actions.AddToAlbum} />
|
||||
{#if album && (hasPermissions(asset, SharingPermission.AssetShare) || isAlbumOwner)}
|
||||
{#if album && (isOwner || isAlbumOwner)}
|
||||
<RemoveFromAlbumAction {album} onRemove={onRemoveFromAlbum} assetIds={[asset.id]} menuItem />
|
||||
{/if}
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user