From 6dd60532224b322b85df8fbb3792f2abc85873fa Mon Sep 17 00:00:00 2001 From: Brandon Wees Date: Wed, 15 Apr 2026 09:26:40 -0500 Subject: [PATCH 01/16] feat: mobile editing (#25397) * feat: mobile editing fix: openapi patch this sucks :pepehands: chore: migrate some changes from the filtering PR chore: color tweak fix: hide edit button on server versions chore: translation * chore: code review changes chore: code review * sealed class * const constant * enum * concurrent queries * chore: major cleanup to use riverpod provider * fix: aspect ratio selection * chore: typesafe websocket event parsing * fix: wrong disable state for save button * chore: remove isCancelled shim * chore: cleanup postframe callback usage * chore: clean import --------- Co-authored-by: mertalev <101130780+mertalev@users.noreply.github.com> --- mobile/lib/constants/aspect_ratios.dart | 19 + .../domain/models/asset/base_asset.model.dart | 2 + .../models/asset/remote_asset.model.dart | 3 + .../lib/domain/models/asset_edit.model.dart | 32 +- mobile/lib/domain/models/exif.model.dart | 14 + mobile/lib/extensions/object_extensions.dart | 3 + .../entities/asset_edit.entity.dart | 11 +- .../infrastructure/entities/exif.entity.dart | 2 + .../repositories/remote_asset.repository.dart | 9 + .../pages/edit/drift_edit.page.dart | 399 ++++++++++++++++++ .../pages/edit/editor.provider.dart | 210 +++++++++ .../pages/editing/drift_crop.page.dart | 179 -------- .../pages/editing/drift_edit.page.dart | 153 ------- .../pages/editing/drift_filter.page.dart | 159 ------- .../edit_image_action_button.widget.dart | 35 +- .../asset_viewer/bottom_bar.widget.dart | 9 +- .../widgets/images/image_provider.dart | 7 +- .../widgets/images/remote_image_provider.dart | 46 +- .../infrastructure/action.provider.dart | 30 +- mobile/lib/providers/websocket.provider.dart | 21 + .../repositories/asset_api.repository.dart | 28 ++ mobile/lib/routing/router.dart | 9 +- mobile/lib/routing/router.gr.dart | 114 +---- mobile/lib/services/action.service.dart | 9 + mobile/lib/utils/editor.utils.dart | 65 +++ mobile/lib/utils/matrix.utils.dart | 50 +++ .../lib/model/asset_edit_action_item_dto.dart | 4 +- mobile/test/utils/editor_test.dart | 322 ++++++++++++++ open-api/bin/generate-open-api.sh | 1 + .../asset_edit_action_item_dto.dart.patch | 18 + 30 files changed, 1317 insertions(+), 646 deletions(-) create mode 100644 mobile/lib/constants/aspect_ratios.dart create mode 100644 mobile/lib/extensions/object_extensions.dart create mode 100644 mobile/lib/presentation/pages/edit/drift_edit.page.dart create mode 100644 mobile/lib/presentation/pages/edit/editor.provider.dart delete mode 100644 mobile/lib/presentation/pages/editing/drift_crop.page.dart delete mode 100644 mobile/lib/presentation/pages/editing/drift_edit.page.dart delete mode 100644 mobile/lib/presentation/pages/editing/drift_filter.page.dart create mode 100644 mobile/lib/utils/editor.utils.dart create mode 100644 mobile/lib/utils/matrix.utils.dart create mode 100644 mobile/test/utils/editor_test.dart create mode 100644 open-api/patch/asset_edit_action_item_dto.dart.patch diff --git a/mobile/lib/constants/aspect_ratios.dart b/mobile/lib/constants/aspect_ratios.dart new file mode 100644 index 0000000000..9159db4ef1 --- /dev/null +++ b/mobile/lib/constants/aspect_ratios.dart @@ -0,0 +1,19 @@ +import 'package:flutter/material.dart'; + +enum AspectRatioPreset { + free(ratio: null, label: 'Free', icon: Icons.crop_free_rounded), + square(ratio: 1.0, label: '1:1', icon: Icons.crop_square_rounded), + ratio16x9(ratio: 16 / 9, label: '16:9', icon: Icons.crop_16_9_rounded), + ratio3x2(ratio: 3 / 2, label: '3:2', icon: Icons.crop_3_2_rounded), + ratio7x5(ratio: 7 / 5, label: '7:5', icon: Icons.crop_7_5_rounded), + ratio9x16(ratio: 9 / 16, label: '9:16', icon: Icons.crop_16_9_rounded, iconRotated: true), + ratio2x3(ratio: 2 / 3, label: '2:3', icon: Icons.crop_3_2_rounded, iconRotated: true), + ratio5x7(ratio: 5 / 7, label: '5:7', icon: Icons.crop_7_5_rounded, iconRotated: true); + + final double? ratio; + final String label; + final IconData icon; + final bool iconRotated; + + const AspectRatioPreset({required this.ratio, required this.label, required this.icon, this.iconRotated = false}); +} diff --git a/mobile/lib/domain/models/asset/base_asset.model.dart b/mobile/lib/domain/models/asset/base_asset.model.dart index 9ba8cd06f8..85c42fd24f 100644 --- a/mobile/lib/domain/models/asset/base_asset.model.dart +++ b/mobile/lib/domain/models/asset/base_asset.model.dart @@ -71,6 +71,8 @@ sealed class BaseAsset { bool get isLocalOnly => storage == AssetState.local; bool get isRemoteOnly => storage == AssetState.remote; + bool get isEditable => false; + // Overridden in subclasses AssetState get storage; String? get localId; diff --git a/mobile/lib/domain/models/asset/remote_asset.model.dart b/mobile/lib/domain/models/asset/remote_asset.model.dart index b9a0e64d6a..745e8f46ff 100644 --- a/mobile/lib/domain/models/asset/remote_asset.model.dart +++ b/mobile/lib/domain/models/asset/remote_asset.model.dart @@ -43,6 +43,9 @@ class RemoteAsset extends BaseAsset { @override String get heroTag => '${localId ?? checksum}_$id'; + @override + bool get isEditable => isImage && !isMotionPhoto && !isAnimatedImage; + @override String toString() { return '''Asset { diff --git a/mobile/lib/domain/models/asset_edit.model.dart b/mobile/lib/domain/models/asset_edit.model.dart index b3266dba46..9809b9c606 100644 --- a/mobile/lib/domain/models/asset_edit.model.dart +++ b/mobile/lib/domain/models/asset_edit.model.dart @@ -1,21 +1,25 @@ -import "package:openapi/api.dart" as api show AssetEditAction; +import "package:openapi/api.dart" show CropParameters, RotateParameters, MirrorParameters; enum AssetEditAction { rotate, crop, mirror, other } -extension AssetEditActionExtension on AssetEditAction { - api.AssetEditAction? toDto() { - return switch (this) { - AssetEditAction.rotate => api.AssetEditAction.rotate, - AssetEditAction.crop => api.AssetEditAction.crop, - AssetEditAction.mirror => api.AssetEditAction.mirror, - AssetEditAction.other => null, - }; - } +sealed class AssetEdit { + const AssetEdit(); } -class AssetEdit { - final AssetEditAction action; - final Map parameters; +class CropEdit extends AssetEdit { + final CropParameters parameters; - const AssetEdit({required this.action, required this.parameters}); + const CropEdit(this.parameters); +} + +class RotateEdit extends AssetEdit { + final RotateParameters parameters; + + const RotateEdit(this.parameters); +} + +class MirrorEdit extends AssetEdit { + final MirrorParameters parameters; + + const MirrorEdit(this.parameters); } diff --git a/mobile/lib/domain/models/exif.model.dart b/mobile/lib/domain/models/exif.model.dart index d0f78b59de..45b787d586 100644 --- a/mobile/lib/domain/models/exif.model.dart +++ b/mobile/lib/domain/models/exif.model.dart @@ -7,6 +7,8 @@ class ExifInfo { final String? timeZone; final DateTime? dateTimeOriginal; final int? rating; + final int? width; + final int? height; // GPS final double? latitude; @@ -48,6 +50,8 @@ class ExifInfo { this.timeZone, this.dateTimeOriginal, this.rating, + this.width, + this.height, this.isFlipped = false, this.latitude, this.longitude, @@ -74,6 +78,8 @@ class ExifInfo { other.timeZone == timeZone && other.dateTimeOriginal == dateTimeOriginal && other.rating == rating && + other.width == width && + other.height == height && other.latitude == latitude && other.longitude == longitude && other.city == city && @@ -98,6 +104,8 @@ class ExifInfo { timeZone.hashCode ^ dateTimeOriginal.hashCode ^ rating.hashCode ^ + width.hashCode ^ + height.hashCode ^ latitude.hashCode ^ longitude.hashCode ^ city.hashCode ^ @@ -123,6 +131,8 @@ isFlipped: $isFlipped, timeZone: ${timeZone ?? 'NA'}, dateTimeOriginal: ${dateTimeOriginal ?? 'NA'}, rating: ${rating ?? 'NA'}, +width: ${width ?? 'NA'}, +height: ${height ?? 'NA'}, latitude: ${latitude ?? 'NA'}, longitude: ${longitude ?? 'NA'}, city: ${city ?? 'NA'}, @@ -146,6 +156,8 @@ exposureSeconds: ${exposureSeconds ?? 'NA'}, String? timeZone, DateTime? dateTimeOriginal, int? rating, + int? width, + int? height, double? latitude, double? longitude, String? city, @@ -168,6 +180,8 @@ exposureSeconds: ${exposureSeconds ?? 'NA'}, timeZone: timeZone ?? this.timeZone, dateTimeOriginal: dateTimeOriginal ?? this.dateTimeOriginal, rating: rating ?? this.rating, + width: width ?? this.width, + height: height ?? this.height, isFlipped: isFlipped ?? this.isFlipped, latitude: latitude ?? this.latitude, longitude: longitude ?? this.longitude, diff --git a/mobile/lib/extensions/object_extensions.dart b/mobile/lib/extensions/object_extensions.dart new file mode 100644 index 0000000000..4e76532137 --- /dev/null +++ b/mobile/lib/extensions/object_extensions.dart @@ -0,0 +1,3 @@ +extension Let on T { + R let(R Function(T) transform) => transform(this); +} diff --git a/mobile/lib/infrastructure/entities/asset_edit.entity.dart b/mobile/lib/infrastructure/entities/asset_edit.entity.dart index 22d059bdb4..87a05ab8fe 100644 --- a/mobile/lib/infrastructure/entities/asset_edit.entity.dart +++ b/mobile/lib/infrastructure/entities/asset_edit.entity.dart @@ -1,8 +1,10 @@ import 'package:drift/drift.dart'; import 'package:immich_mobile/domain/models/asset_edit.model.dart'; +import 'package:immich_mobile/extensions/object_extensions.dart'; import 'package:immich_mobile/infrastructure/entities/asset_edit.entity.drift.dart'; import 'package:immich_mobile/infrastructure/entities/remote_asset.entity.dart'; import 'package:immich_mobile/infrastructure/utils/drift_default.mixin.dart'; +import 'package:openapi/api.dart' hide AssetEditAction; @TableIndex.sql('CREATE INDEX IF NOT EXISTS idx_asset_edit_asset_id ON asset_edit_entity (asset_id)') class AssetEditEntity extends Table with DriftDefaultsMixin { @@ -27,7 +29,12 @@ final JsonTypeConverter2, Uint8List, Object?> editParameter ); extension AssetEditEntityDataDomainEx on AssetEditEntityData { - AssetEdit toDto() { - return AssetEdit(action: action, parameters: parameters); + AssetEdit? toDto() { + return switch (action) { + AssetEditAction.crop => CropParameters.fromJson(parameters)?.let(CropEdit.new), + AssetEditAction.rotate => RotateParameters.fromJson(parameters)?.let(RotateEdit.new), + AssetEditAction.mirror => MirrorParameters.fromJson(parameters)?.let(MirrorEdit.new), + AssetEditAction.other => null, + }; } } diff --git a/mobile/lib/infrastructure/entities/exif.entity.dart b/mobile/lib/infrastructure/entities/exif.entity.dart index 77cae5dbbe..06262f4afc 100644 --- a/mobile/lib/infrastructure/entities/exif.entity.dart +++ b/mobile/lib/infrastructure/entities/exif.entity.dart @@ -152,6 +152,8 @@ extension RemoteExifEntityDataDomainEx on RemoteExifEntityData { fileSize: fileSize, dateTimeOriginal: dateTimeOriginal, rating: rating, + width: width, + height: height, timeZone: timeZone, make: make, model: model, diff --git a/mobile/lib/infrastructure/repositories/remote_asset.repository.dart b/mobile/lib/infrastructure/repositories/remote_asset.repository.dart index df4172df99..00c0b81850 100644 --- a/mobile/lib/infrastructure/repositories/remote_asset.repository.dart +++ b/mobile/lib/infrastructure/repositories/remote_asset.repository.dart @@ -1,7 +1,9 @@ import 'package:drift/drift.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/domain/models/stack.model.dart'; +import 'package:immich_mobile/infrastructure/entities/asset_edit.entity.dart'; import 'package:immich_mobile/infrastructure/entities/exif.entity.dart' hide ExifInfo; import 'package:immich_mobile/infrastructure/entities/exif.entity.drift.dart'; import 'package:immich_mobile/infrastructure/entities/remote_asset.entity.dart'; @@ -264,4 +266,11 @@ class RemoteAssetRepository extends DriftDatabaseRepository { Future getCount() { return _db.managers.remoteAssetEntity.count(); } + + Future> getAssetEdits(String assetId) { + final query = _db.assetEditEntity.select() + ..where((row) => row.assetId.equals(assetId) & row.action.equals(AssetEditAction.other.index).not()) + ..orderBy([(row) => OrderingTerm.asc(row.sequence)]); + return query.map((row) => row.toDto()!).get(); + } } diff --git a/mobile/lib/presentation/pages/edit/drift_edit.page.dart b/mobile/lib/presentation/pages/edit/drift_edit.page.dart new file mode 100644 index 0000000000..8f7d874983 --- /dev/null +++ b/mobile/lib/presentation/pages/edit/drift_edit.page.dart @@ -0,0 +1,399 @@ +import 'dart:async'; +import 'dart:math'; + +import 'package:auto_route/auto_route.dart'; +import 'package:crop_image/crop_image.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/aspect_ratios.dart'; +import 'package:immich_mobile/domain/models/asset_edit.model.dart'; +import 'package:immich_mobile/extensions/build_context_extensions.dart'; +import 'package:immich_mobile/presentation/pages/edit/editor.provider.dart'; +import 'package:immich_mobile/providers/theme.provider.dart'; +import 'package:immich_mobile/theme/theme_data.dart'; +import 'package:immich_mobile/utils/editor.utils.dart'; +import 'package:immich_mobile/widgets/common/immich_toast.dart'; +import 'package:immich_ui/immich_ui.dart'; +import 'package:openapi/api.dart' show RotateParameters, MirrorParameters, MirrorAxis; + +@RoutePage() +class DriftEditImagePage extends ConsumerStatefulWidget { + final Image image; + final Future Function(List edits) applyEdits; + + const DriftEditImagePage({super.key, required this.image, required this.applyEdits}); + + @override + ConsumerState createState() => _DriftEditImagePageState(); +} + +class _DriftEditImagePageState extends ConsumerState with TickerProviderStateMixin { + Future _saveEditedImage() async { + ref.read(editorStateProvider.notifier).setIsEditing(true); + + final editorState = ref.read(editorStateProvider); + final cropParameters = convertRectToCropParameters( + editorState.crop, + editorState.originalWidth, + editorState.originalHeight, + ); + final edits = []; + + if (cropParameters.width != editorState.originalWidth || cropParameters.height != editorState.originalHeight) { + edits.add(CropEdit(cropParameters)); + } + + if (editorState.flipHorizontal) { + edits.add(MirrorEdit(MirrorParameters(axis: MirrorAxis.horizontal))); + } + + if (editorState.flipVertical) { + edits.add(MirrorEdit(MirrorParameters(axis: MirrorAxis.vertical))); + } + + final normalizedRotation = (editorState.rotationAngle % 360 + 360) % 360; + if (normalizedRotation != 0) { + edits.add(RotateEdit(RotateParameters(angle: normalizedRotation))); + } + + try { + await widget.applyEdits(edits); + ImmichToast.show(context: context, msg: 'success'.tr(), toastType: ToastType.success); + Navigator.of(context).pop(); + } catch (e) { + ImmichToast.show(context: context, msg: 'error_title'.tr(), toastType: ToastType.error); + } finally { + ref.read(editorStateProvider.notifier).setIsEditing(false); + } + } + + Future _showDiscardChangesDialog() { + return showDialog( + context: context, + builder: (context) => AlertDialog( + title: Text('editor_discard_edits_title'.tr()), + content: Text('editor_discard_edits_prompt'.tr()), + actions: [ + TextButton( + onPressed: () => Navigator.of(context).pop(false), + style: ButtonStyle( + foregroundColor: WidgetStateProperty.all(context.themeData.colorScheme.onSurfaceVariant), + ), + child: Text('cancel'.tr()), + ), + TextButton(onPressed: () => Navigator.of(context).pop(true), child: Text('confirm'.tr())), + ], + ), + ); + } + + @override + Widget build(BuildContext context) { + final hasUnsavedEdits = ref.watch(editorStateProvider.select((state) => state.hasUnsavedEdits)); + + return PopScope( + canPop: !hasUnsavedEdits, + onPopInvokedWithResult: (didPop, result) async { + if (didPop) return; + final shouldDiscard = await _showDiscardChangesDialog() ?? false; + if (shouldDiscard && mounted) { + Navigator.of(context).pop(); + } + }, + child: Theme( + data: getThemeData(colorScheme: ref.watch(immichThemeProvider).dark, locale: context.locale), + child: Scaffold( + appBar: AppBar( + backgroundColor: Colors.black, + title: Text("edit".tr()), + leading: ImmichCloseButton(onPressed: () => Navigator.of(context).maybePop()), + actions: [_SaveEditsButton(onSave: _saveEditedImage)], + ), + backgroundColor: Colors.black, + body: SafeArea( + bottom: false, + child: Column( + children: [ + Expanded(child: _EditorPreview(image: widget.image)), + AnimatedSize( + duration: const Duration(milliseconds: 250), + curve: Curves.easeInOut, + alignment: Alignment.bottomCenter, + clipBehavior: Clip.none, + child: Container( + width: double.infinity, + decoration: BoxDecoration( + color: ref.watch(immichThemeProvider).dark.surface, + borderRadius: const BorderRadius.only( + topLeft: Radius.circular(20), + topRight: Radius.circular(20), + ), + ), + child: const Column( + mainAxisSize: MainAxisSize.min, + children: [ + _TransformControls(), + Padding( + padding: EdgeInsets.only(bottom: 36, left: 24, right: 24), + child: Row(children: [Spacer(), _ResetEditsButton()]), + ), + ], + ), + ), + ), + ], + ), + ), + ), + ), + ); + } +} + +class _AspectRatioButton extends StatelessWidget { + final AspectRatioPreset ratio; + final bool isSelected; + final VoidCallback onPressed; + + const _AspectRatioButton({required this.ratio, required this.isSelected, required this.onPressed}); + + @override + Widget build(BuildContext context) { + return Column( + mainAxisSize: MainAxisSize.max, + children: [ + IconButton( + iconSize: 36, + icon: Transform.rotate( + angle: ratio.iconRotated ? pi / 2 : 0, + child: Icon(ratio.icon, color: isSelected ? context.primaryColor : context.themeData.iconTheme.color), + ), + onPressed: onPressed, + ), + Text(ratio.label, style: context.textTheme.displayMedium), + ], + ); + } +} + +class _AspectRatioSelector extends ConsumerWidget { + const _AspectRatioSelector(); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final editorState = ref.watch(editorStateProvider); + final editorNotifier = ref.read(editorStateProvider.notifier); + + // the whole crop view is rotated, so we need to swap the aspect ratio when the rotation is 90 or 270 degrees + double? selectedAspectRatio = editorState.aspectRatio; + if (editorState.rotationAngle % 180 != 0 && selectedAspectRatio != null) { + selectedAspectRatio = 1 / selectedAspectRatio; + } + + return SingleChildScrollView( + scrollDirection: Axis.horizontal, + child: Row( + children: AspectRatioPreset.values.map((entry) { + return Padding( + padding: const EdgeInsets.symmetric(horizontal: 8.0), + child: _AspectRatioButton( + ratio: entry, + isSelected: selectedAspectRatio == entry.ratio, + onPressed: () => editorNotifier.setAspectRatio(entry.ratio), + ), + ); + }).toList(), + ), + ); + } +} + +class _TransformControls extends ConsumerWidget { + const _TransformControls(); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final editorNotifier = ref.read(editorStateProvider.notifier); + + return Column( + mainAxisSize: MainAxisSize.min, + children: [ + Padding( + padding: const EdgeInsets.only(left: 20, right: 20, top: 20, bottom: 10), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Row( + children: [ + ImmichIconButton( + icon: Icons.rotate_left, + variant: ImmichVariant.ghost, + color: ImmichColor.secondary, + onPressed: editorNotifier.rotateCCW, + ), + const SizedBox(width: 8), + ImmichIconButton( + icon: Icons.rotate_right, + variant: ImmichVariant.ghost, + color: ImmichColor.secondary, + onPressed: editorNotifier.rotateCW, + ), + ], + ), + Row( + children: [ + ImmichIconButton( + icon: Icons.flip, + variant: ImmichVariant.ghost, + color: ImmichColor.secondary, + onPressed: editorNotifier.flipHorizontally, + ), + const SizedBox(width: 8), + Transform.rotate( + angle: pi / 2, + child: ImmichIconButton( + icon: Icons.flip, + variant: ImmichVariant.ghost, + color: ImmichColor.secondary, + onPressed: editorNotifier.flipVertically, + ), + ), + ], + ), + ], + ), + ), + const _AspectRatioSelector(), + const SizedBox(height: 32), + ], + ); + } +} + +class _SaveEditsButton extends ConsumerWidget { + final VoidCallback onSave; + + const _SaveEditsButton({required this.onSave}); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final isApplyingEdits = ref.watch(editorStateProvider.select((state) => state.isApplyingEdits)); + final hasUnsavedEdits = ref.watch(editorStateProvider.select((state) => state.hasUnsavedEdits)); + + return isApplyingEdits + ? const Padding( + padding: EdgeInsets.all(8.0), + child: SizedBox(width: 28, height: 28, child: CircularProgressIndicator(strokeWidth: 2.5)), + ) + : ImmichIconButton( + icon: Icons.done_rounded, + color: ImmichColor.primary, + variant: ImmichVariant.ghost, + disabled: !hasUnsavedEdits, + onPressed: onSave, + ); + } +} + +class _ResetEditsButton extends ConsumerWidget { + const _ResetEditsButton(); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final editorState = ref.watch(editorStateProvider); + final editorNotifier = ref.read(editorStateProvider.notifier); + + return ImmichTextButton( + labelText: 'reset'.tr(), + onPressed: editorNotifier.resetEdits, + variant: ImmichVariant.ghost, + expanded: false, + disabled: !editorState.hasEdits || editorState.isApplyingEdits, + ); + } +} + +class _EditorPreview extends ConsumerStatefulWidget { + final Image image; + + const _EditorPreview({required this.image}); + + @override + ConsumerState<_EditorPreview> createState() => _EditorPreviewState(); +} + +class _EditorPreviewState extends ConsumerState<_EditorPreview> with TickerProviderStateMixin { + late final CropController cropController; + + @override + void initState() { + super.initState(); + + cropController = CropController(); + cropController.crop = ref.read(editorStateProvider.select((state) => state.crop)); + cropController.addListener(onCrop); + } + + void onCrop() { + if (!mounted || cropController.crop == ref.read(editorStateProvider).crop) { + return; + } + + ref.read(editorStateProvider.notifier).setCrop(cropController.crop); + } + + @override + void dispose() { + cropController.removeListener(onCrop); + cropController.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + final editorState = ref.watch(editorStateProvider); + final editorNotifier = ref.read(editorStateProvider.notifier); + + ref.listen(editorStateProvider, (_, current) { + cropController.aspectRatio = current.aspectRatio; + + if (cropController.crop != current.crop) { + cropController.crop = current.crop; + } + }); + + return LayoutBuilder( + builder: (BuildContext context, BoxConstraints constraints) { + // Calculate the bounding box size needed for the rotated container + final baseWidth = constraints.maxWidth * 0.9; + final baseHeight = constraints.maxHeight * 0.95; + + return Center( + child: AnimatedRotation( + turns: editorState.rotationAngle / 360, + duration: editorState.animationDuration, + curve: Curves.easeInOut, + onEnd: editorNotifier.normalizeRotation, + child: Transform( + alignment: Alignment.center, + transform: Matrix4.identity() + ..scaleByDouble( + editorState.flipHorizontal ? -1.0 : 1.0, + editorState.flipVertical ? -1.0 : 1.0, + 1.0, + 1.0, + ), + child: Container( + padding: const EdgeInsets.all(10), + width: (editorState.rotationAngle % 180 == 0) ? baseWidth : baseHeight, + height: (editorState.rotationAngle % 180 == 0) ? baseHeight : baseWidth, + child: CropImage(controller: cropController, image: widget.image, gridColor: Colors.white), + ), + ), + ), + ); + }, + ); + } +} diff --git a/mobile/lib/presentation/pages/edit/editor.provider.dart b/mobile/lib/presentation/pages/edit/editor.provider.dart new file mode 100644 index 0000000000..21b5268912 --- /dev/null +++ b/mobile/lib/presentation/pages/edit/editor.provider.dart @@ -0,0 +1,210 @@ +import 'package:flutter/services.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:immich_mobile/domain/models/asset_edit.model.dart'; +import 'package:immich_mobile/domain/models/exif.model.dart'; +import 'package:immich_mobile/utils/editor.utils.dart'; + +final editorStateProvider = NotifierProvider(EditorProvider.new); + +class EditorProvider extends Notifier { + @override + EditorState build() { + return const EditorState(); + } + + void clear() { + state = const EditorState(); + } + + void init(List edits, ExifInfo exifInfo) { + clear(); + + final existingCrop = edits.whereType().firstOrNull; + + final originalWidth = exifInfo.isFlipped ? exifInfo.height : exifInfo.width; + final originalHeight = exifInfo.isFlipped ? exifInfo.width : exifInfo.height; + + Rect crop = existingCrop != null && originalWidth != null && originalHeight != null + ? convertCropParametersToRect(existingCrop.parameters, originalWidth, originalHeight) + : const Rect.fromLTRB(0, 0, 1, 1); + + final transform = normalizeTransformEdits(edits); + + state = state.copyWith( + originalWidth: originalWidth, + originalHeight: originalHeight, + crop: crop, + flipHorizontal: transform.mirrorHorizontal, + flipVertical: transform.mirrorVertical, + ); + + _animateRotation(transform.rotation.toInt(), duration: Duration.zero); + } + + void _animateRotation(int angle, {Duration duration = const Duration(milliseconds: 300)}) { + state = state.copyWith(rotationAngle: angle, animationDuration: duration); + } + + void normalizeRotation() { + final normalizedAngle = ((state.rotationAngle % 360) + 360) % 360; + if (normalizedAngle != state.rotationAngle) { + state = state.copyWith(rotationAngle: normalizedAngle, animationDuration: Duration.zero); + } + } + + void setIsEditing(bool isApplyingEdits) { + state = state.copyWith(isApplyingEdits: isApplyingEdits); + } + + void setCrop(Rect crop) { + state = state.copyWith(crop: crop, hasUnsavedEdits: true); + } + + void setAspectRatio(double? aspectRatio) { + if (aspectRatio != null && state.rotationAngle % 180 != 0) { + // When rotated 90 or 270 degrees, swap width and height for aspect ratio calculations + aspectRatio = 1 / aspectRatio; + } + + state = state.copyWith(aspectRatio: aspectRatio); + } + + void resetEdits() { + _animateRotation(0); + + state = state.copyWith( + flipHorizontal: false, + flipVertical: false, + crop: const Rect.fromLTRB(0, 0, 1, 1), + aspectRatio: null, + hasUnsavedEdits: true, + ); + } + + void rotateCCW() { + _animateRotation(state.rotationAngle - 90); + state = state.copyWith(hasUnsavedEdits: true); + } + + void rotateCW() { + _animateRotation(state.rotationAngle + 90); + state = state.copyWith(hasUnsavedEdits: true); + } + + void flipHorizontally() { + if (state.rotationAngle % 180 != 0) { + // When rotated 90 or 270 degrees, flipping horizontally is equivalent to flipping vertically + state = state.copyWith(flipVertical: !state.flipVertical, hasUnsavedEdits: true); + } else { + state = state.copyWith(flipHorizontal: !state.flipHorizontal, hasUnsavedEdits: true); + } + } + + void flipVertically() { + if (state.rotationAngle % 180 != 0) { + // When rotated 90 or 270 degrees, flipping vertically is equivalent to flipping horizontally + state = state.copyWith(flipHorizontal: !state.flipHorizontal, hasUnsavedEdits: true); + } else { + state = state.copyWith(flipVertical: !state.flipVertical, hasUnsavedEdits: true); + } + } +} + +class EditorState { + final bool isApplyingEdits; + + final int rotationAngle; + final bool flipHorizontal; + final bool flipVertical; + final Rect crop; + final double? aspectRatio; + + final int originalWidth; + final int originalHeight; + + final Duration animationDuration; + + final bool hasUnsavedEdits; + + const EditorState({ + bool? isApplyingEdits, + int? rotationAngle, + bool? flipHorizontal, + bool? flipVertical, + Rect? crop, + this.aspectRatio, + int? originalWidth, + int? originalHeight, + Duration? animationDuration, + bool? hasUnsavedEdits, + }) : isApplyingEdits = isApplyingEdits ?? false, + rotationAngle = rotationAngle ?? 0, + flipHorizontal = flipHorizontal ?? false, + flipVertical = flipVertical ?? false, + animationDuration = animationDuration ?? Duration.zero, + originalWidth = originalWidth ?? 0, + originalHeight = originalHeight ?? 0, + crop = crop ?? const Rect.fromLTRB(0, 0, 1, 1), + hasUnsavedEdits = hasUnsavedEdits ?? false; + + EditorState copyWith({ + bool? isApplyingEdits, + int? rotationAngle, + bool? flipHorizontal, + bool? flipVertical, + double? aspectRatio = double.infinity, + int? originalWidth, + int? originalHeight, + Duration? animationDuration, + Rect? crop, + bool? hasUnsavedEdits, + }) { + return EditorState( + isApplyingEdits: isApplyingEdits ?? this.isApplyingEdits, + rotationAngle: rotationAngle ?? this.rotationAngle, + flipHorizontal: flipHorizontal ?? this.flipHorizontal, + flipVertical: flipVertical ?? this.flipVertical, + aspectRatio: aspectRatio == double.infinity ? this.aspectRatio : aspectRatio, + animationDuration: animationDuration ?? this.animationDuration, + originalWidth: originalWidth ?? this.originalWidth, + originalHeight: originalHeight ?? this.originalHeight, + crop: crop ?? this.crop, + hasUnsavedEdits: hasUnsavedEdits ?? this.hasUnsavedEdits, + ); + } + + bool get hasEdits { + return rotationAngle != 0 || flipHorizontal || flipVertical || crop != const Rect.fromLTRB(0, 0, 1, 1); + } + + @override + bool operator ==(Object other) { + if (identical(this, other)) return true; + + return other is EditorState && + other.isApplyingEdits == isApplyingEdits && + other.rotationAngle == rotationAngle && + other.flipHorizontal == flipHorizontal && + other.flipVertical == flipVertical && + other.crop == crop && + other.aspectRatio == aspectRatio && + other.originalWidth == originalWidth && + other.originalHeight == originalHeight && + other.animationDuration == animationDuration && + other.hasUnsavedEdits == hasUnsavedEdits; + } + + @override + int get hashCode { + return isApplyingEdits.hashCode ^ + rotationAngle.hashCode ^ + flipHorizontal.hashCode ^ + flipVertical.hashCode ^ + crop.hashCode ^ + aspectRatio.hashCode ^ + originalWidth.hashCode ^ + originalHeight.hashCode ^ + animationDuration.hashCode ^ + hasUnsavedEdits.hashCode; + } +} diff --git a/mobile/lib/presentation/pages/editing/drift_crop.page.dart b/mobile/lib/presentation/pages/editing/drift_crop.page.dart deleted file mode 100644 index a213e4c640..0000000000 --- a/mobile/lib/presentation/pages/editing/drift_crop.page.dart +++ /dev/null @@ -1,179 +0,0 @@ -import 'dart:async'; - -import 'package:auto_route/auto_route.dart'; -import 'package:crop_image/crop_image.dart'; -import 'package:easy_localization/easy_localization.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_hooks/flutter_hooks.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/routing/router.dart'; -import 'package:immich_mobile/utils/hooks/crop_controller_hook.dart'; -import 'package:immich_ui/immich_ui.dart'; - -/// A widget for cropping an image. -/// This widget uses [HookWidget] to manage its lifecycle and state. It allows -/// users to crop an image and then navigate to the [EditImagePage] with the -/// cropped image. - -@RoutePage() -class DriftCropImagePage extends HookWidget { - final Image image; - final BaseAsset asset; - const DriftCropImagePage({super.key, required this.image, required this.asset}); - - @override - Widget build(BuildContext context) { - final cropController = useCropController(); - final aspectRatio = useState(null); - - return Scaffold( - appBar: AppBar( - backgroundColor: context.scaffoldBackgroundColor, - title: Text("crop".tr()), - leading: const ImmichCloseButton(), - actions: [ - ImmichIconButton( - icon: Icons.done_rounded, - color: ImmichColor.primary, - variant: ImmichVariant.ghost, - onPressed: () async { - final croppedImage = await cropController.croppedImage(); - unawaited(context.pushRoute(DriftEditImageRoute(asset: asset, image: croppedImage, isEdited: true))); - }, - ), - ], - ), - backgroundColor: context.scaffoldBackgroundColor, - body: SafeArea( - child: LayoutBuilder( - builder: (BuildContext context, BoxConstraints constraints) { - return Column( - children: [ - Container( - padding: const EdgeInsets.only(top: 20), - width: constraints.maxWidth * 0.9, - height: constraints.maxHeight * 0.6, - child: CropImage(controller: cropController, image: image, gridColor: Colors.white), - ), - Expanded( - child: Container( - width: double.infinity, - decoration: BoxDecoration( - color: context.scaffoldBackgroundColor, - borderRadius: const BorderRadius.only( - topLeft: Radius.circular(20), - topRight: Radius.circular(20), - ), - ), - child: Center( - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Padding( - padding: const EdgeInsets.only(left: 20, right: 20, bottom: 10), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - ImmichIconButton( - icon: Icons.rotate_left, - variant: ImmichVariant.ghost, - color: ImmichColor.secondary, - onPressed: () => cropController.rotateLeft(), - ), - ImmichIconButton( - icon: Icons.rotate_right, - variant: ImmichVariant.ghost, - color: ImmichColor.secondary, - onPressed: () => cropController.rotateRight(), - ), - ], - ), - ), - Row( - mainAxisAlignment: MainAxisAlignment.spaceEvenly, - children: [ - _AspectRatioButton( - cropController: cropController, - aspectRatio: aspectRatio, - ratio: null, - label: 'Free', - ), - _AspectRatioButton( - cropController: cropController, - aspectRatio: aspectRatio, - ratio: 1.0, - label: '1:1', - ), - _AspectRatioButton( - cropController: cropController, - aspectRatio: aspectRatio, - ratio: 16.0 / 9.0, - label: '16:9', - ), - _AspectRatioButton( - cropController: cropController, - aspectRatio: aspectRatio, - ratio: 3.0 / 2.0, - label: '3:2', - ), - _AspectRatioButton( - cropController: cropController, - aspectRatio: aspectRatio, - ratio: 7.0 / 5.0, - label: '7:5', - ), - ], - ), - ], - ), - ), - ), - ), - ], - ); - }, - ), - ), - ); - } -} - -class _AspectRatioButton extends StatelessWidget { - final CropController cropController; - final ValueNotifier aspectRatio; - final double? ratio; - final String label; - - const _AspectRatioButton({ - required this.cropController, - required this.aspectRatio, - required this.ratio, - required this.label, - }); - - @override - Widget build(BuildContext context) { - return Column( - mainAxisSize: MainAxisSize.min, - children: [ - IconButton( - icon: Icon(switch (label) { - 'Free' => Icons.crop_free_rounded, - '1:1' => Icons.crop_square_rounded, - '16:9' => Icons.crop_16_9_rounded, - '3:2' => Icons.crop_3_2_rounded, - '7:5' => Icons.crop_7_5_rounded, - _ => Icons.crop_free_rounded, - }, color: aspectRatio.value == ratio ? context.primaryColor : context.themeData.iconTheme.color), - onPressed: () { - cropController.crop = const Rect.fromLTRB(0.1, 0.1, 0.9, 0.9); - aspectRatio.value = ratio; - cropController.aspectRatio = ratio; - }, - ), - Text(label, style: context.textTheme.displayMedium), - ], - ); - } -} diff --git a/mobile/lib/presentation/pages/editing/drift_edit.page.dart b/mobile/lib/presentation/pages/editing/drift_edit.page.dart deleted file mode 100644 index 6d4ea4d3a6..0000000000 --- a/mobile/lib/presentation/pages/editing/drift_edit.page.dart +++ /dev/null @@ -1,153 +0,0 @@ -import 'dart:async'; - -import 'package:auto_route/auto_route.dart'; -import 'package:easy_localization/easy_localization.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter/services.dart'; -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/domain/models/asset/base_asset.model.dart'; -import 'package:immich_mobile/entities/asset.entity.dart'; -import 'package:immich_mobile/extensions/build_context_extensions.dart'; -import 'package:immich_mobile/providers/background_sync.provider.dart'; -import 'package:immich_mobile/repositories/file_media.repository.dart'; -import 'package:immich_mobile/routing/router.dart'; -import 'package:immich_mobile/services/foreground_upload.service.dart'; -import 'package:immich_mobile/utils/image_converter.dart'; -import 'package:immich_mobile/widgets/common/immich_toast.dart'; -import 'package:logging/logging.dart'; -import 'package:path/path.dart' as p; - -/// A stateless widget that provides functionality for editing an image. -/// -/// This widget allows users to edit an image provided either as an [Asset] or -/// directly as an [Image]. It ensures that exactly one of these is provided. -/// -/// It also includes a conversion method to convert an [Image] to a [Uint8List] to save the image on the user's phone -/// They automatically navigate to the [HomePage] with the edited image saved and they eventually get backed up to the server. -@immutable -@RoutePage() -class DriftEditImagePage extends ConsumerWidget { - final BaseAsset asset; - final Image image; - final bool isEdited; - - const DriftEditImagePage({super.key, required this.asset, required this.image, required this.isEdited}); - - void _exitEditing(BuildContext context) { - // this assumes that the only way to get to this page is from the AssetViewerRoute - context.navigator.popUntil((route) => route.data?.name == AssetViewerRoute.name); - } - - Future _saveEditedImage(BuildContext context, BaseAsset asset, Image image, WidgetRef ref) async { - try { - final Uint8List imageData = await imageToUint8List(image); - LocalAsset? localAsset; - - try { - localAsset = await ref - .read(fileMediaRepositoryProvider) - .saveLocalAsset(imageData, title: "${p.withoutExtension(asset.name)}_edited.jpg"); - } on PlatformException catch (e) { - // OS might not return the saved image back, so we handle that gracefully - // This can happen if app does not have full library access - Logger("SaveEditedImage").warning("Failed to retrieve the saved image back from OS", e); - } - - unawaited(ref.read(backgroundSyncProvider).syncLocal(full: true)); - _exitEditing(context); - ImmichToast.show(durationInSecond: 3, context: context, msg: 'Image Saved!'); - - if (localAsset == null) { - return; - } - - await ref.read(foregroundUploadServiceProvider).uploadManual([localAsset]); - } catch (e) { - ImmichToast.show( - durationInSecond: 6, - context: context, - msg: "error_saving_image".tr(namedArgs: {'error': e.toString()}), - ); - } - } - - @override - Widget build(BuildContext context, WidgetRef ref) { - return Scaffold( - appBar: AppBar( - title: Text("edit".tr()), - backgroundColor: context.scaffoldBackgroundColor, - leading: IconButton( - icon: Icon(Icons.close_rounded, color: context.primaryColor, size: 24), - onPressed: () => _exitEditing(context), - ), - actions: [ - TextButton( - onPressed: isEdited ? () => _saveEditedImage(context, asset, image, ref) : null, - child: Text("save_to_gallery".tr(), style: TextStyle(color: isEdited ? context.primaryColor : Colors.grey)), - ), - ], - ), - backgroundColor: context.scaffoldBackgroundColor, - body: Center( - child: ConstrainedBox( - constraints: BoxConstraints(maxHeight: context.height * 0.7, maxWidth: context.width * 0.9), - child: Container( - decoration: BoxDecoration( - borderRadius: const BorderRadius.all(Radius.circular(7)), - boxShadow: [ - BoxShadow( - color: Colors.black.withValues(alpha: 0.2), - spreadRadius: 2, - blurRadius: 10, - offset: const Offset(0, 3), - ), - ], - ), - child: ClipRRect( - borderRadius: const BorderRadius.all(Radius.circular(7)), - child: Image(image: image.image, fit: BoxFit.contain), - ), - ), - ), - ), - bottomNavigationBar: Container( - height: 70, - margin: const EdgeInsets.only(bottom: 60, right: 10, left: 10, top: 10), - decoration: BoxDecoration( - color: context.scaffoldBackgroundColor, - borderRadius: const BorderRadius.all(Radius.circular(30)), - ), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceEvenly, - children: [ - Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - IconButton( - icon: Icon(Icons.crop_rotate_rounded, color: context.themeData.iconTheme.color, size: 25), - onPressed: () { - context.pushRoute(DriftCropImageRoute(asset: asset, image: image)); - }, - ), - Text("crop".tr(), style: context.textTheme.displayMedium), - ], - ), - Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - IconButton( - icon: Icon(Icons.filter, color: context.themeData.iconTheme.color, size: 25), - onPressed: () { - context.pushRoute(DriftFilterImageRoute(asset: asset, image: image)); - }, - ), - Text("filter".tr(), style: context.textTheme.displayMedium), - ], - ), - ], - ), - ), - ); - } -} diff --git a/mobile/lib/presentation/pages/editing/drift_filter.page.dart b/mobile/lib/presentation/pages/editing/drift_filter.page.dart deleted file mode 100644 index 8198a41bbe..0000000000 --- a/mobile/lib/presentation/pages/editing/drift_filter.page.dart +++ /dev/null @@ -1,159 +0,0 @@ -import 'dart:async'; -import 'dart:ui' as ui; - -import 'package:auto_route/auto_route.dart'; -import 'package:easy_localization/easy_localization.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_hooks/flutter_hooks.dart'; -import 'package:immich_mobile/constants/filters.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/routing/router.dart'; - -/// A widget for filtering an image. -/// This widget uses [HookWidget] to manage its lifecycle and state. It allows -/// users to add filters to an image and then navigate to the [EditImagePage] with the -/// final composition.' -@RoutePage() -class DriftFilterImagePage extends HookWidget { - final Image image; - final BaseAsset asset; - - const DriftFilterImagePage({super.key, required this.image, required this.asset}); - - @override - Widget build(BuildContext context) { - final colorFilter = useState(filters[0]); - final selectedFilterIndex = useState(0); - - Future createFilteredImage(ui.Image inputImage, ColorFilter filter) { - final completer = Completer(); - final size = Size(inputImage.width.toDouble(), inputImage.height.toDouble()); - final recorder = ui.PictureRecorder(); - final canvas = Canvas(recorder); - - final paint = Paint()..colorFilter = filter; - canvas.drawImage(inputImage, Offset.zero, paint); - - recorder.endRecording().toImage(size.width.round(), size.height.round()).then((image) { - completer.complete(image); - }); - - return completer.future; - } - - void applyFilter(ColorFilter filter, int index) { - colorFilter.value = filter; - selectedFilterIndex.value = index; - } - - Future applyFilterAndConvert(ColorFilter filter) async { - final completer = Completer(); - image.image - .resolve(ImageConfiguration.empty) - .addListener( - ImageStreamListener((ImageInfo info, bool _) { - completer.complete(info.image); - }), - ); - final uiImage = await completer.future; - - final filteredUiImage = await createFilteredImage(uiImage, filter); - final byteData = await filteredUiImage.toByteData(format: ui.ImageByteFormat.png); - final pngBytes = byteData!.buffer.asUint8List(); - - return Image.memory(pngBytes, fit: BoxFit.contain); - } - - return Scaffold( - appBar: AppBar( - backgroundColor: context.scaffoldBackgroundColor, - title: Text("filter".tr()), - leading: CloseButton(color: context.primaryColor), - actions: [ - IconButton( - icon: Icon(Icons.done_rounded, color: context.primaryColor, size: 24), - onPressed: () async { - final filteredImage = await applyFilterAndConvert(colorFilter.value); - unawaited(context.pushRoute(DriftEditImageRoute(asset: asset, image: filteredImage, isEdited: true))); - }, - ), - ], - ), - backgroundColor: context.scaffoldBackgroundColor, - body: Column( - children: [ - SizedBox( - height: context.height * 0.7, - child: Center( - child: ColorFiltered(colorFilter: colorFilter.value, child: image), - ), - ), - SizedBox( - height: 120, - child: ListView.builder( - scrollDirection: Axis.horizontal, - itemCount: filters.length, - itemBuilder: (context, index) { - return Padding( - padding: const EdgeInsets.symmetric(horizontal: 8.0), - child: _FilterButton( - image: image, - label: filterNames[index], - filter: filters[index], - isSelected: selectedFilterIndex.value == index, - onTap: () => applyFilter(filters[index], index), - ), - ); - }, - ), - ), - ], - ), - ); - } -} - -class _FilterButton extends StatelessWidget { - final Image image; - final String label; - final ColorFilter filter; - final bool isSelected; - final VoidCallback onTap; - - const _FilterButton({ - required this.image, - required this.label, - required this.filter, - required this.isSelected, - required this.onTap, - }); - - @override - Widget build(BuildContext context) { - return Column( - children: [ - GestureDetector( - onTap: onTap, - child: Container( - width: 80, - height: 80, - decoration: BoxDecoration( - borderRadius: const BorderRadius.all(Radius.circular(10)), - border: isSelected ? Border.all(color: context.primaryColor, width: 3) : null, - ), - child: ClipRRect( - borderRadius: const BorderRadius.all(Radius.circular(10)), - child: ColorFiltered( - colorFilter: filter, - child: FittedBox(fit: BoxFit.cover, child: image), - ), - ), - ), - ), - const SizedBox(height: 10), - Text(label, style: context.themeData.textTheme.bodyMedium), - ], - ); - } -} diff --git a/mobile/lib/presentation/widgets/action_buttons/edit_image_action_button.widget.dart b/mobile/lib/presentation/widgets/action_buttons/edit_image_action_button.widget.dart index cad74ce658..564b02d884 100644 --- a/mobile/lib/presentation/widgets/action_buttons/edit_image_action_button.widget.dart +++ b/mobile/lib/presentation/widgets/action_buttons/edit_image_action_button.widget.dart @@ -1,10 +1,17 @@ +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 { @@ -14,13 +21,33 @@ class EditImageActionButton extends ConsumerWidget { Widget build(BuildContext context, WidgetRef ref) { final currentAsset = ref.watch(assetViewerProvider.select((s) => s.currentAsset)); - onPress() { - if (currentAsset == null) { + Future editImage(List edits) async { + if (currentAsset == null || currentAsset.remoteId == null) { return; } - final image = Image(image: getFullImageProvider(currentAsset)); - context.pushRoute(DriftEditImageRoute(asset: currentAsset, image: image, isEdited: false)); + await ref.read(actionProvider.notifier).applyEdits(ActionSource.viewer, edits); + } + + Future 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( diff --git a/mobile/lib/presentation/widgets/asset_viewer/bottom_bar.widget.dart b/mobile/lib/presentation/widgets/asset_viewer/bottom_bar.widget.dart index b51960bb05..cf7ffbd234 100644 --- a/mobile/lib/presentation/widgets/asset_viewer/bottom_bar.widget.dart +++ b/mobile/lib/presentation/widgets/asset_viewer/bottom_bar.widget.dart @@ -3,16 +3,18 @@ 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/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/edit_image_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'; -import 'package:immich_mobile/presentation/widgets/action_buttons/add_action_button.widget.dart'; 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/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'; class ViewerBottomBar extends ConsumerWidget { @@ -30,6 +32,7 @@ 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 originalTheme = context.themeData; @@ -38,7 +41,9 @@ class ViewerBottomBar extends ConsumerWidget { if (!isInLockedView) ...[ if (asset.isLocalOnly) const UploadActionButton(source: ActionSource.viewer), - if (asset.type == AssetType.image) const EditImageActionButton(), + // edit sync was added in 2.6.0 + if (asset.isEditable && serverInfo.serverVersion >= const SemVer(major: 2, minor: 6, patch: 0)) + const EditImageActionButton(), if (asset.hasRemote) AddActionButton(originalTheme: originalTheme), if (isOwner) ...[ diff --git a/mobile/lib/presentation/widgets/images/image_provider.dart b/mobile/lib/presentation/widgets/images/image_provider.dart index 47ebd37014..ea416d9d71 100644 --- a/mobile/lib/presentation/widgets/images/image_provider.dart +++ b/mobile/lib/presentation/widgets/images/image_provider.dart @@ -147,7 +147,7 @@ mixin CancellableImageProviderMixin on CancellableImageProvide } } -ImageProvider getFullImageProvider(BaseAsset asset, {Size size = const Size(1080, 1920)}) { +ImageProvider getFullImageProvider(BaseAsset asset, {Size size = const Size(1080, 1920), bool edited = true}) { // Create new provider and cache it final ImageProvider provider; if (_shouldUseLocalAsset(asset)) { @@ -170,13 +170,14 @@ ImageProvider getFullImageProvider(BaseAsset asset, {Size size = const Size(1080 thumbhash: thumbhash, assetType: asset.type, isAnimated: asset.isAnimatedImage, + edited: edited, ); } return provider; } -ImageProvider? getThumbnailImageProvider(BaseAsset asset, {Size size = kThumbnailResolution}) { +ImageProvider? getThumbnailImageProvider(BaseAsset asset, {Size size = kThumbnailResolution, bool edited = true}) { if (_shouldUseLocalAsset(asset)) { final id = asset is LocalAsset ? asset.id : (asset as RemoteAsset).localId!; return LocalThumbProvider(id: id, size: size, assetType: asset.type); @@ -184,7 +185,7 @@ ImageProvider? getThumbnailImageProvider(BaseAsset asset, {Size size = kThumbnai final assetId = asset is RemoteAsset ? asset.id : (asset as LocalAsset).remoteId; final thumbhash = asset is RemoteAsset ? asset.thumbHash ?? "" : ""; - return assetId != null ? RemoteImageProvider.thumbnail(assetId: assetId, thumbhash: thumbhash) : null; + return assetId != null ? RemoteImageProvider.thumbnail(assetId: assetId, thumbhash: thumbhash, edited: edited) : null; } bool _shouldUseLocalAsset(BaseAsset asset) => diff --git a/mobile/lib/presentation/widgets/images/remote_image_provider.dart b/mobile/lib/presentation/widgets/images/remote_image_provider.dart index d9cc053ccf..f7fc5868c3 100644 --- a/mobile/lib/presentation/widgets/images/remote_image_provider.dart +++ b/mobile/lib/presentation/widgets/images/remote_image_provider.dart @@ -13,11 +13,12 @@ import 'package:openapi/api.dart'; class RemoteImageProvider extends CancellableImageProvider with CancellableImageProviderMixin { final String url; + final bool edited; - RemoteImageProvider({required this.url}); + RemoteImageProvider({required this.url, this.edited = true}); - RemoteImageProvider.thumbnail({required String assetId, required String thumbhash}) - : url = getThumbnailUrlForRemoteId(assetId, thumbhash: thumbhash); + RemoteImageProvider.thumbnail({required String assetId, required String thumbhash, this.edited = true}) + : url = getThumbnailUrlForRemoteId(assetId, thumbhash: thumbhash, edited: edited); @override Future obtainKey(ImageConfiguration configuration) { @@ -45,13 +46,13 @@ class RemoteImageProvider extends CancellableImageProvider bool operator ==(Object other) { if (identical(this, other)) return true; if (other is RemoteImageProvider) { - return url == other.url; + return url == other.url && edited == other.edited; } return false; } @override - int get hashCode => url.hashCode; + int get hashCode => url.hashCode ^ edited.hashCode; } class RemoteFullImageProvider extends CancellableImageProvider @@ -60,12 +61,14 @@ class RemoteFullImageProvider extends CancellableImageProvider [ DiagnosticsProperty('Image provider', this), DiagnosticsProperty('Asset Id', key.assetId), @@ -109,7 +114,12 @@ class RemoteFullImageProvider extends CancellableImageProvider assetId.hashCode ^ thumbhash.hashCode ^ isAnimated.hashCode; + int get hashCode => assetId.hashCode ^ thumbhash.hashCode ^ isAnimated.hashCode ^ edited.hashCode; } diff --git a/mobile/lib/providers/infrastructure/action.provider.dart b/mobile/lib/providers/infrastructure/action.provider.dart index bad0d986d0..d24e2cc6cd 100644 --- a/mobile/lib/providers/infrastructure/action.provider.dart +++ b/mobile/lib/providers/infrastructure/action.provider.dart @@ -5,21 +5,24 @@ import 'package:background_downloader/background_downloader.dart'; import 'package:flutter/material.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/services/asset.service.dart'; import 'package:immich_mobile/models/download/livephotos_medatada.model.dart'; import 'package:immich_mobile/providers/asset_viewer/asset_viewer.provider.dart'; +import 'package:immich_mobile/providers/backup/asset_upload_progress.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/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/providers/backup/asset_upload_progress.provider.dart'; import 'package:immich_mobile/services/action.service.dart'; import 'package:immich_mobile/services/download.service.dart'; -import 'package:immich_mobile/services/timeline.service.dart'; import 'package:immich_mobile/services/foreground_upload.service.dart'; +import 'package:immich_mobile/services/timeline.service.dart'; import 'package:immich_mobile/widgets/asset_grid/delete_dialog.dart'; import 'package:logging/logging.dart'; +import 'package:openapi/api.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart'; final actionProvider = NotifierProvider( @@ -490,6 +493,29 @@ class ActionNotifier extends Notifier { }); } } + + Future applyEdits(ActionSource source, List 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'); + } + + final completer = 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 completer; + 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 { diff --git a/mobile/lib/providers/websocket.provider.dart b/mobile/lib/providers/websocket.provider.dart index 6643404786..8059e54605 100644 --- a/mobile/lib/providers/websocket.provider.dart +++ b/mobile/lib/providers/websocket.provider.dart @@ -197,6 +197,27 @@ class WebsocketNotifier extends StateNotifier { state.socket?.on('on_upload_success', _handleOnUploadSuccess); } + Future waitForEvent(String event, bool Function(dynamic)? predicate, Duration timeout) { + final completer = Completer(); + + void handler(dynamic data) { + if (predicate == null || predicate(data)) { + completer.complete(); + state.socket?.off(event, handler); + } + } + + state.socket?.on(event, handler); + + return completer.future.timeout( + timeout, + onTimeout: () { + state.socket?.off(event, handler); + completer.completeError(TimeoutException("Timeout waiting for event: $event")); + }, + ); + } + void addPendingChange(PendingAction action, dynamic value) { final now = DateTime.now(); state = state.copyWith( diff --git a/mobile/lib/repositories/asset_api.repository.dart b/mobile/lib/repositories/asset_api.repository.dart index 011b1edc94..d66b39ecde 100644 --- a/mobile/lib/repositories/asset_api.repository.dart +++ b/mobile/lib/repositories/asset_api.repository.dart @@ -1,6 +1,7 @@ import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:http/http.dart'; import 'package:immich_mobile/constants/enums.dart'; +import 'package:immich_mobile/domain/models/asset_edit.model.dart' hide AssetEditAction; import 'package:immich_mobile/domain/models/stack.model.dart'; import 'package:immich_mobile/entities/asset.entity.dart'; import 'package:immich_mobile/providers/api.provider.dart'; @@ -105,6 +106,14 @@ class AssetApiRepository extends ApiRepository { Future updateRating(String assetId, int rating) { return _api.updateAsset(assetId, UpdateAssetDto(rating: rating)); } + + Future editAsset(String assetId, List edits) { + return _api.editAsset(assetId, AssetEditsCreateDto(edits: edits.map((e) => e.toApi()).toList())); + } + + Future removeEdits(String assetId) async { + return _api.removeAssetEdits(assetId); + } } extension on StackResponseDto { @@ -112,3 +121,22 @@ extension on StackResponseDto { return StackResponse(id: id, primaryAssetId: primaryAssetId, assetIds: assets.map((asset) => asset.id).toList()); } } + +extension on AssetEdit { + AssetEditActionItemDto toApi() { + return switch (this) { + CropEdit(:final parameters) => AssetEditActionItemDto( + action: AssetEditAction.crop, + parameters: parameters.toJson(), + ), + RotateEdit(:final parameters) => AssetEditActionItemDto( + action: AssetEditAction.rotate, + parameters: parameters.toJson(), + ), + MirrorEdit(:final parameters) => AssetEditActionItemDto( + action: AssetEditAction.mirror, + parameters: parameters.toJson(), + ), + }; + } +} diff --git a/mobile/lib/routing/router.dart b/mobile/lib/routing/router.dart index b385bcbf71..90a17b1617 100644 --- a/mobile/lib/routing/router.dart +++ b/mobile/lib/routing/router.dart @@ -4,6 +4,7 @@ import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/domain/models/album/album.model.dart'; 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/log.model.dart'; import 'package:immich_mobile/domain/models/memory.model.dart'; import 'package:immich_mobile/domain/models/person.model.dart'; @@ -105,11 +106,9 @@ import 'package:immich_mobile/presentation/pages/drift_remote_album.page.dart'; import 'package:immich_mobile/presentation/pages/drift_trash.page.dart'; import 'package:immich_mobile/presentation/pages/drift_user_selection.page.dart'; import 'package:immich_mobile/presentation/pages/drift_video.page.dart'; -import 'package:immich_mobile/presentation/pages/editing/drift_crop.page.dart'; -import 'package:immich_mobile/presentation/pages/profile/profile_picture_crop.page.dart'; -import 'package:immich_mobile/presentation/pages/editing/drift_edit.page.dart'; -import 'package:immich_mobile/presentation/pages/editing/drift_filter.page.dart'; +import 'package:immich_mobile/presentation/pages/edit/drift_edit.page.dart'; import 'package:immich_mobile/presentation/pages/local_timeline.page.dart'; +import 'package:immich_mobile/presentation/pages/profile/profile_picture_crop.page.dart'; import 'package:immich_mobile/presentation/pages/search/drift_search.page.dart'; import 'package:immich_mobile/presentation/widgets/asset_viewer/asset_viewer.page.dart'; import 'package:immich_mobile/providers/api.provider.dart'; @@ -333,8 +332,6 @@ class AppRouter extends RootStackRouter { AutoRoute(page: DriftAlbumOptionsRoute.page, guards: [_authGuard, _duplicateGuard]), AutoRoute(page: DriftMapRoute.page, guards: [_authGuard, _duplicateGuard]), AutoRoute(page: DriftEditImageRoute.page), - AutoRoute(page: DriftCropImageRoute.page), - AutoRoute(page: DriftFilterImageRoute.page), AutoRoute(page: DriftActivitiesRoute.page, guards: [_authGuard, _duplicateGuard]), AutoRoute(page: DriftBackupAssetDetailRoute.page, guards: [_authGuard, _duplicateGuard]), AutoRoute(page: AssetTroubleshootRoute.page, guards: [_authGuard, _duplicateGuard]), diff --git a/mobile/lib/routing/router.gr.dart b/mobile/lib/routing/router.gr.dart index 2d57c16573..18b16c23ef 100644 --- a/mobile/lib/routing/router.gr.dart +++ b/mobile/lib/routing/router.gr.dart @@ -1003,70 +1003,20 @@ class DriftCreateAlbumRoute extends PageRouteInfo { ); } -/// generated route for -/// [DriftCropImagePage] -class DriftCropImageRoute extends PageRouteInfo { - DriftCropImageRoute({ - Key? key, - required Image image, - required BaseAsset asset, - List? children, - }) : super( - DriftCropImageRoute.name, - args: DriftCropImageRouteArgs(key: key, image: image, asset: asset), - initialChildren: children, - ); - - static const String name = 'DriftCropImageRoute'; - - static PageInfo page = PageInfo( - name, - builder: (data) { - final args = data.argsAs(); - return DriftCropImagePage( - key: args.key, - image: args.image, - asset: args.asset, - ); - }, - ); -} - -class DriftCropImageRouteArgs { - const DriftCropImageRouteArgs({ - this.key, - required this.image, - required this.asset, - }); - - final Key? key; - - final Image image; - - final BaseAsset asset; - - @override - String toString() { - return 'DriftCropImageRouteArgs{key: $key, image: $image, asset: $asset}'; - } -} - /// generated route for /// [DriftEditImagePage] class DriftEditImageRoute extends PageRouteInfo { DriftEditImageRoute({ Key? key, - required BaseAsset asset, required Image image, - required bool isEdited, + required Future Function(List) applyEdits, List? children, }) : super( DriftEditImageRoute.name, args: DriftEditImageRouteArgs( key: key, - asset: asset, image: image, - isEdited: isEdited, + applyEdits: applyEdits, ), initialChildren: children, ); @@ -1079,9 +1029,8 @@ class DriftEditImageRoute extends PageRouteInfo { final args = data.argsAs(); return DriftEditImagePage( key: args.key, - asset: args.asset, image: args.image, - isEdited: args.isEdited, + applyEdits: args.applyEdits, ); }, ); @@ -1090,22 +1039,19 @@ class DriftEditImageRoute extends PageRouteInfo { class DriftEditImageRouteArgs { const DriftEditImageRouteArgs({ this.key, - required this.asset, required this.image, - required this.isEdited, + required this.applyEdits, }); final Key? key; - final BaseAsset asset; - final Image image; - final bool isEdited; + final Future Function(List) applyEdits; @override String toString() { - return 'DriftEditImageRouteArgs{key: $key, asset: $asset, image: $image, isEdited: $isEdited}'; + return 'DriftEditImageRouteArgs{key: $key, image: $image, applyEdits: $applyEdits}'; } } @@ -1125,54 +1071,6 @@ class DriftFavoriteRoute extends PageRouteInfo { ); } -/// generated route for -/// [DriftFilterImagePage] -class DriftFilterImageRoute extends PageRouteInfo { - DriftFilterImageRoute({ - Key? key, - required Image image, - required BaseAsset asset, - List? children, - }) : super( - DriftFilterImageRoute.name, - args: DriftFilterImageRouteArgs(key: key, image: image, asset: asset), - initialChildren: children, - ); - - static const String name = 'DriftFilterImageRoute'; - - static PageInfo page = PageInfo( - name, - builder: (data) { - final args = data.argsAs(); - return DriftFilterImagePage( - key: args.key, - image: args.image, - asset: args.asset, - ); - }, - ); -} - -class DriftFilterImageRouteArgs { - const DriftFilterImageRouteArgs({ - this.key, - required this.image, - required this.asset, - }); - - final Key? key; - - final Image image; - - final BaseAsset asset; - - @override - String toString() { - return 'DriftFilterImageRouteArgs{key: $key, image: $image, asset: $asset}'; - } -} - /// generated route for /// [DriftLibraryPage] class DriftLibraryRoute extends PageRouteInfo { diff --git a/mobile/lib/services/action.service.dart b/mobile/lib/services/action.service.dart index c435bf9d79..1a6333215a 100644 --- a/mobile/lib/services/action.service.dart +++ b/mobile/lib/services/action.service.dart @@ -5,6 +5,7 @@ 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/entities/store.entity.dart'; import 'package:immich_mobile/extensions/platform_extensions.dart'; @@ -246,6 +247,14 @@ class ActionService { return true; } + Future applyEdits(String remoteId, List edits) async { + if (edits.isEmpty) { + await _assetApiRepository.removeEdits(remoteId); + } else { + await _assetApiRepository.editAsset(remoteId, edits); + } + } + Future _deleteLocalAssets(List localIds) async { final deletedIds = await _assetMediaRepository.deleteAll(localIds); if (deletedIds.isEmpty) { diff --git a/mobile/lib/utils/editor.utils.dart b/mobile/lib/utils/editor.utils.dart new file mode 100644 index 0000000000..fa2dedf383 --- /dev/null +++ b/mobile/lib/utils/editor.utils.dart @@ -0,0 +1,65 @@ +import 'dart:math'; + +import 'package:flutter/material.dart'; +import 'package:immich_mobile/domain/models/asset_edit.model.dart'; +import 'package:immich_mobile/utils/matrix.utils.dart'; +import 'package:openapi/api.dart' hide AssetEditAction; + +Rect convertCropParametersToRect(CropParameters parameters, int originalWidth, int originalHeight) { + return Rect.fromLTWH( + parameters.x.toDouble() / originalWidth, + parameters.y.toDouble() / originalHeight, + parameters.width.toDouble() / originalWidth, + parameters.height.toDouble() / originalHeight, + ); +} + +CropParameters convertRectToCropParameters(Rect rect, int originalWidth, int originalHeight) { + final x = (rect.left * originalWidth).truncate(); + final y = (rect.top * originalHeight).truncate(); + final width = (rect.width * originalWidth).truncate(); + final height = (rect.height * originalHeight).truncate(); + + return CropParameters( + x: max(x, 0).clamp(0, originalWidth), + y: max(y, 0).clamp(0, originalHeight), + width: max(width, 0).clamp(0, originalWidth - x), + height: max(height, 0).clamp(0, originalHeight - y), + ); +} + +AffineMatrix buildAffineFromEdits(List edits) { + return AffineMatrix.compose( + edits.map((edit) { + return switch (edit) { + RotateEdit(:final parameters) => AffineMatrix.rotate(parameters.angle * pi / 180), + MirrorEdit(:final parameters) => + parameters.axis == MirrorAxis.horizontal ? AffineMatrix.flipY() : AffineMatrix.flipX(), + CropEdit() => AffineMatrix.identity(), + }; + }).toList(), + ); +} + +bool isCloseToZero(double value, [double epsilon = 1e-15]) { + return value.abs() < epsilon; +} + +typedef NormalizedTransform = ({double rotation, bool mirrorHorizontal, bool mirrorVertical}); + +NormalizedTransform normalizeTransformEdits(List edits) { + final matrix = buildAffineFromEdits(edits); + + double a = matrix.a; + double b = matrix.b; + double c = matrix.c; + double d = matrix.d; + + final rotation = ((isCloseToZero(a) ? asin(c) : acos(a)) * 180) / pi; + + return ( + rotation: rotation < 0 ? 360 + rotation : rotation, + mirrorHorizontal: false, + mirrorVertical: isCloseToZero(a) ? b == c : a == -d, + ); +} diff --git a/mobile/lib/utils/matrix.utils.dart b/mobile/lib/utils/matrix.utils.dart new file mode 100644 index 0000000000..8363a8b93d --- /dev/null +++ b/mobile/lib/utils/matrix.utils.dart @@ -0,0 +1,50 @@ +import 'dart:math'; + +class AffineMatrix { + final double a; + final double b; + final double c; + final double d; + final double e; + final double f; + + const AffineMatrix(this.a, this.b, this.c, this.d, this.e, this.f); + + @override + String toString() { + return 'AffineMatrix(a: $a, b: $b, c: $c, d: $d, e: $e, f: $f)'; + } + + factory AffineMatrix.identity() { + return const AffineMatrix(1, 0, 0, 1, 0, 0); + } + + AffineMatrix multiply(AffineMatrix other) { + return AffineMatrix( + a * other.a + c * other.b, + b * other.a + d * other.b, + a * other.c + c * other.d, + b * other.c + d * other.d, + a * other.e + c * other.f + e, + b * other.e + d * other.f + f, + ); + } + + factory AffineMatrix.compose([List transformations = const []]) { + return transformations.fold(AffineMatrix.identity(), (acc, matrix) => acc.multiply(matrix)); + } + + factory AffineMatrix.rotate(double angle) { + final cosAngle = cos(angle); + final sinAngle = sin(angle); + return AffineMatrix(cosAngle, -sinAngle, sinAngle, cosAngle, 0, 0); + } + + factory AffineMatrix.flipY() { + return const AffineMatrix(-1, 0, 0, 1, 0, 0); + } + + factory AffineMatrix.flipX() { + return const AffineMatrix(1, 0, 0, -1, 0, 0); + } +} diff --git a/mobile/openapi/lib/model/asset_edit_action_item_dto.dart b/mobile/openapi/lib/model/asset_edit_action_item_dto.dart index 2c7bb82c24..1b19612bf3 100644 --- a/mobile/openapi/lib/model/asset_edit_action_item_dto.dart +++ b/mobile/openapi/lib/model/asset_edit_action_item_dto.dart @@ -19,7 +19,7 @@ class AssetEditActionItemDto { AssetEditAction action; - AssetEditActionItemDtoParameters parameters; + Map parameters; @override bool operator ==(Object other) => identical(this, other) || other is AssetEditActionItemDto && @@ -52,7 +52,7 @@ class AssetEditActionItemDto { return AssetEditActionItemDto( action: AssetEditAction.fromJson(json[r'action'])!, - parameters: AssetEditActionItemDtoParameters.fromJson(json[r'parameters'])!, + parameters: json[r'parameters'], ); } return null; diff --git a/mobile/test/utils/editor_test.dart b/mobile/test/utils/editor_test.dart new file mode 100644 index 0000000000..16f1c08d05 --- /dev/null +++ b/mobile/test/utils/editor_test.dart @@ -0,0 +1,322 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:immich_mobile/domain/models/asset_edit.model.dart'; +import 'package:immich_mobile/utils/editor.utils.dart'; +import 'package:openapi/api.dart' show MirrorAxis, MirrorParameters, RotateParameters; + +List normalizedToEdits(NormalizedTransform transform) { + List edits = []; + + if (transform.mirrorHorizontal) { + edits.add(MirrorEdit(MirrorParameters(axis: MirrorAxis.horizontal))); + } + + if (transform.mirrorVertical) { + edits.add(MirrorEdit(MirrorParameters(axis: MirrorAxis.vertical))); + } + + if (transform.rotation != 0) { + edits.add(RotateEdit(RotateParameters(angle: transform.rotation))); + } + + return edits; +} + +bool compareEditAffines(List editsA, List editsB) { + final normA = buildAffineFromEdits(editsA); + final normB = buildAffineFromEdits(editsB); + + return ((normA.a - normB.a).abs() < 0.0001 && + (normA.b - normB.b).abs() < 0.0001 && + (normA.c - normB.c).abs() < 0.0001 && + (normA.d - normB.d).abs() < 0.0001); +} + +void main() { + group('normalizeEdits', () { + test('should handle no edits', () { + final edits = []; + + final result = normalizeTransformEdits(edits); + final normalizedEdits = normalizedToEdits(result); + + expect(compareEditAffines(normalizedEdits, edits), true); + }); + + test('should handle a single 90° rotation', () { + final edits = [ + RotateEdit(RotateParameters(angle: 90)), + ]; + + final result = normalizeTransformEdits(edits); + final normalizedEdits = normalizedToEdits(result); + + expect(compareEditAffines(normalizedEdits, edits), true); + }); + + test('should handle a single 180° rotation', () { + final edits = [ + RotateEdit(RotateParameters(angle: 180)), + ]; + + final result = normalizeTransformEdits(edits); + final normalizedEdits = normalizedToEdits(result); + + expect(compareEditAffines(normalizedEdits, edits), true); + }); + + test('should handle a single 270° rotation', () { + final edits = [ + RotateEdit(RotateParameters(angle: 270)), + ]; + + final result = normalizeTransformEdits(edits); + final normalizedEdits = normalizedToEdits(result); + + expect(compareEditAffines(normalizedEdits, edits), true); + }); + + test('should handle a single horizontal mirror', () { + final edits = [ + MirrorEdit(MirrorParameters(axis: MirrorAxis.horizontal)), + ]; + + final result = normalizeTransformEdits(edits); + final normalizedEdits = normalizedToEdits(result); + + expect(compareEditAffines(normalizedEdits, edits), true); + }); + + test('should handle a single vertical mirror', () { + final edits = [ + MirrorEdit(MirrorParameters(axis: MirrorAxis.vertical)), + ]; + + final result = normalizeTransformEdits(edits); + final normalizedEdits = normalizedToEdits(result); + + expect(compareEditAffines(normalizedEdits, edits), true); + }); + + test('should handle 90° rotation + horizontal mirror', () { + final edits = [ + RotateEdit(RotateParameters(angle: 90)), + MirrorEdit(MirrorParameters(axis: MirrorAxis.horizontal)), + ]; + + final result = normalizeTransformEdits(edits); + final normalizedEdits = normalizedToEdits(result); + + expect(compareEditAffines(normalizedEdits, edits), true); + }); + + test('should handle 90° rotation + vertical mirror', () { + final edits = [ + RotateEdit(RotateParameters(angle: 90)), + MirrorEdit(MirrorParameters(axis: MirrorAxis.vertical)), + ]; + + final result = normalizeTransformEdits(edits); + final normalizedEdits = normalizedToEdits(result); + + expect(compareEditAffines(normalizedEdits, edits), true); + }); + + test('should handle 90° rotation + both mirrors', () { + final edits = [ + RotateEdit(RotateParameters(angle: 90)), + MirrorEdit(MirrorParameters(axis: MirrorAxis.horizontal)), + MirrorEdit(MirrorParameters(axis: MirrorAxis.vertical)), + ]; + + final result = normalizeTransformEdits(edits); + final normalizedEdits = normalizedToEdits(result); + + expect(compareEditAffines(normalizedEdits, edits), true); + }); + + test('should handle 180° rotation + horizontal mirror', () { + final edits = [ + RotateEdit(RotateParameters(angle: 180)), + MirrorEdit(MirrorParameters(axis: MirrorAxis.horizontal)), + ]; + + final result = normalizeTransformEdits(edits); + final normalizedEdits = normalizedToEdits(result); + + expect(compareEditAffines(normalizedEdits, edits), true); + }); + + test('should handle 180° rotation + vertical mirror', () { + final edits = [ + RotateEdit(RotateParameters(angle: 180)), + MirrorEdit(MirrorParameters(axis: MirrorAxis.vertical)), + ]; + + final result = normalizeTransformEdits(edits); + final normalizedEdits = normalizedToEdits(result); + + expect(compareEditAffines(normalizedEdits, edits), true); + }); + + test('should handle 180° rotation + both mirrors', () { + final edits = [ + RotateEdit(RotateParameters(angle: 180)), + MirrorEdit(MirrorParameters(axis: MirrorAxis.horizontal)), + MirrorEdit(MirrorParameters(axis: MirrorAxis.vertical)), + ]; + + final result = normalizeTransformEdits(edits); + final normalizedEdits = normalizedToEdits(result); + + expect(compareEditAffines(normalizedEdits, edits), true); + }); + + test('should handle 270° rotation + horizontal mirror', () { + final edits = [ + RotateEdit(RotateParameters(angle: 270)), + MirrorEdit(MirrorParameters(axis: MirrorAxis.horizontal)), + ]; + + final result = normalizeTransformEdits(edits); + final normalizedEdits = normalizedToEdits(result); + + expect(compareEditAffines(normalizedEdits, edits), true); + }); + + test('should handle 270° rotation + vertical mirror', () { + final edits = [ + RotateEdit(RotateParameters(angle: 270)), + MirrorEdit(MirrorParameters(axis: MirrorAxis.vertical)), + ]; + + final result = normalizeTransformEdits(edits); + final normalizedEdits = normalizedToEdits(result); + + expect(compareEditAffines(normalizedEdits, edits), true); + }); + + test('should handle 270° rotation + both mirrors', () { + final edits = [ + RotateEdit(RotateParameters(angle: 270)), + MirrorEdit(MirrorParameters(axis: MirrorAxis.horizontal)), + MirrorEdit(MirrorParameters(axis: MirrorAxis.vertical)), + ]; + + final result = normalizeTransformEdits(edits); + final normalizedEdits = normalizedToEdits(result); + + expect(compareEditAffines(normalizedEdits, edits), true); + }); + + test('should handle horizontal mirror + 90° rotation', () { + final edits = [ + MirrorEdit(MirrorParameters(axis: MirrorAxis.horizontal)), + RotateEdit(RotateParameters(angle: 90)), + ]; + + final result = normalizeTransformEdits(edits); + final normalizedEdits = normalizedToEdits(result); + + expect(compareEditAffines(normalizedEdits, edits), true); + }); + + test('should handle horizontal mirror + 180° rotation', () { + final edits = [ + MirrorEdit(MirrorParameters(axis: MirrorAxis.horizontal)), + RotateEdit(RotateParameters(angle: 180)), + ]; + + final result = normalizeTransformEdits(edits); + final normalizedEdits = normalizedToEdits(result); + + expect(compareEditAffines(normalizedEdits, edits), true); + }); + + test('should handle horizontal mirror + 270° rotation', () { + final edits = [ + MirrorEdit(MirrorParameters(axis: MirrorAxis.horizontal)), + RotateEdit(RotateParameters(angle: 270)), + ]; + + final result = normalizeTransformEdits(edits); + final normalizedEdits = normalizedToEdits(result); + + expect(compareEditAffines(normalizedEdits, edits), true); + }); + + test('should handle vertical mirror + 90° rotation', () { + final edits = [ + MirrorEdit(MirrorParameters(axis: MirrorAxis.vertical)), + RotateEdit(RotateParameters(angle: 90)), + ]; + + final result = normalizeTransformEdits(edits); + final normalizedEdits = normalizedToEdits(result); + + expect(compareEditAffines(normalizedEdits, edits), true); + }); + + test('should handle vertical mirror + 180° rotation', () { + final edits = [ + MirrorEdit(MirrorParameters(axis: MirrorAxis.vertical)), + RotateEdit(RotateParameters(angle: 180)), + ]; + + final result = normalizeTransformEdits(edits); + final normalizedEdits = normalizedToEdits(result); + + expect(compareEditAffines(normalizedEdits, edits), true); + }); + + test('should handle vertical mirror + 270° rotation', () { + final edits = [ + MirrorEdit(MirrorParameters(axis: MirrorAxis.vertical)), + RotateEdit(RotateParameters(angle: 270)), + ]; + + final result = normalizeTransformEdits(edits); + final normalizedEdits = normalizedToEdits(result); + + expect(compareEditAffines(normalizedEdits, edits), true); + }); + + test('should handle both mirrors + 90° rotation', () { + final edits = [ + MirrorEdit(MirrorParameters(axis: MirrorAxis.horizontal)), + MirrorEdit(MirrorParameters(axis: MirrorAxis.vertical)), + RotateEdit(RotateParameters(angle: 90)), + ]; + + final result = normalizeTransformEdits(edits); + final normalizedEdits = normalizedToEdits(result); + + expect(compareEditAffines(normalizedEdits, edits), true); + }); + + test('should handle both mirrors + 180° rotation', () { + final edits = [ + MirrorEdit(MirrorParameters(axis: MirrorAxis.horizontal)), + MirrorEdit(MirrorParameters(axis: MirrorAxis.vertical)), + RotateEdit(RotateParameters(angle: 180)), + ]; + + final result = normalizeTransformEdits(edits); + final normalizedEdits = normalizedToEdits(result); + + expect(compareEditAffines(normalizedEdits, edits), true); + }); + + test('should handle both mirrors + 270° rotation', () { + final edits = [ + MirrorEdit(MirrorParameters(axis: MirrorAxis.horizontal)), + MirrorEdit(MirrorParameters(axis: MirrorAxis.vertical)), + RotateEdit(RotateParameters(angle: 270)), + ]; + + final result = normalizeTransformEdits(edits); + final normalizedEdits = normalizedToEdits(result); + + expect(compareEditAffines(normalizedEdits, edits), true); + }); + }); +} diff --git a/open-api/bin/generate-open-api.sh b/open-api/bin/generate-open-api.sh index 83a6df31a8..99adf9f4a8 100755 --- a/open-api/bin/generate-open-api.sh +++ b/open-api/bin/generate-open-api.sh @@ -23,6 +23,7 @@ function dart { patch --no-backup-if-mismatch -u ../mobile/openapi/lib/api_client.dart <./patch/api_client.dart.patch patch --no-backup-if-mismatch -u ../mobile/openapi/lib/api.dart <./patch/api.dart.patch patch --no-backup-if-mismatch -u ../mobile/openapi/pubspec.yaml <./patch/pubspec_immich_mobile.yaml.patch + patch --no-backup-if-mismatch -u ../mobile/openapi/lib/model/asset_edit_action_item_dto.dart <./patch/asset_edit_action_item_dto.dart.patch # Don't include analysis_options.yaml for the generated openapi files # so that language servers can properly exclude the mobile/openapi directory rm ../mobile/openapi/analysis_options.yaml diff --git a/open-api/patch/asset_edit_action_item_dto.dart.patch b/open-api/patch/asset_edit_action_item_dto.dart.patch new file mode 100644 index 0000000000..b825795bf4 --- /dev/null +++ b/open-api/patch/asset_edit_action_item_dto.dart.patch @@ -0,0 +1,18 @@ +@@ -20,7 +20,7 @@ class AssetEditActionItemDto { + /// Type of edit action to perform + AssetEditAction action; + +- AssetEditActionItemDtoParameters parameters; ++ Map parameters; + + @override + bool operator ==(Object other) => identical(this, other) || other is AssetEditActionItemDto && +@@ -53,7 +53,7 @@ class AssetEditActionItemDto { + + return AssetEditActionItemDto( + action: AssetEditAction.fromJson(json[r'action'])!, +- parameters: AssetEditActionItemDtoParameters.fromJson(json[r'parameters'])!, ++ parameters: json[r'parameters'], + ); + } + return null; \ No newline at end of file From 79fccdbee00a831f869bc99cf114fdfd3f03874e Mon Sep 17 00:00:00 2001 From: shenlong <139912620+shenlong-tanwen@users.noreply.github.com> Date: Wed, 15 Apr 2026 23:00:27 +0530 Subject: [PATCH 02/16] refactor: yeet old timeline (#27666) * refactor: yank old timeline # Conflicts: # mobile/lib/presentation/pages/editing/drift_edit.page.dart # mobile/lib/providers/websocket.provider.dart # mobile/lib/routing/router.dart * more cleanup * remove native code * chore: bump sqlite-data version * remove old background tasks from BGTaskSchedulerPermittedIdentifiers * rebase --------- Co-authored-by: shenlong-tanwen <139912620+shalong-tanwen@users.noreply.github.com> --- .../immich/BackgroundServicePlugin.kt | 389 -- .../app/alextran/immich/BackupWorker.kt | 394 -- .../alextran/immich/ContentObserverWorker.kt | 144 - .../kotlin/app/alextran/immich/ImmichApp.kt | 2 - .../app/alextran/immich/MainActivity.kt | 1 - .../test_utils/general_helper.dart | 14 +- mobile/ios/Podfile.lock | 22 +- mobile/ios/Runner.xcodeproj/project.pbxproj | 34 +- .../xcshareddata/swiftpm/Package.resolved | 12 +- mobile/ios/Runner/AppDelegate.swift | 25 - .../BackgroundServicePlugin.swift | 408 -- .../BackgroundSync/BackgroundSyncWorker.swift | 271 -- mobile/ios/Runner/Info.plist | 2 - mobile/lib/constants/constants.dart | 7 - mobile/lib/constants/enums.dart | 2 - .../lib/domain/interfaces/db.interface.dart | 3 - .../lib/domain/models/device_asset.model.dart | 34 - mobile/lib/domain/services/asset.service.dart | 46 - .../services/background_worker.service.dart | 28 +- mobile/lib/domain/services/log.service.dart | 6 +- mobile/lib/domain/services/store.service.dart | 10 +- mobile/lib/domain/services/user.service.dart | 22 +- mobile/lib/entities/README.md | 1 - mobile/lib/entities/album.entity.dart | 192 - mobile/lib/entities/album.entity.g.dart | 2240 ---------- .../entities/android_device_asset.entity.dart | 10 - .../android_device_asset.entity.g.dart | 463 -- mobile/lib/entities/asset.entity.dart | 575 --- mobile/lib/entities/asset.entity.g.dart | 3711 ----------------- mobile/lib/entities/backup_album.entity.dart | 22 - .../lib/entities/backup_album.entity.g.dart | 679 --- mobile/lib/entities/device_asset.entity.dart | 8 - .../lib/entities/duplicated_asset.entity.dart | 11 - .../entities/duplicated_asset.entity.g.dart | 444 -- mobile/lib/entities/etag.entity.dart | 14 - mobile/lib/entities/etag.entity.g.dart | 796 ---- .../lib/entities/ios_device_asset.entity.dart | 14 - .../entities/ios_device_asset.entity.g.dart | 766 ---- mobile/lib/entities/store.entity.dart | 34 - mobile/lib/extensions/asset_extensions.dart | 17 - .../lib/extensions/collection_extensions.dart | 28 - .../lib/extensions/translate_extensions.dart | 2 +- .../entities/device_asset.entity.dart | 25 - .../entities/device_asset.entity.g.dart | 874 ---- .../infrastructure/entities/exif.entity.dart | 90 - .../entities/exif.entity.g.dart | 3200 -------------- .../infrastructure/entities/store.entity.dart | 13 - .../entities/store.entity.g.dart | 596 --- .../infrastructure/entities/user.entity.dart | 73 - .../entities/user.entity.g.dart | 1854 -------- .../repositories/db.repository.dart | 22 +- .../repositories/device_asset.repository.dart | 31 - .../repositories/exif.repository.dart | 40 - .../repositories/logger_db.repository.dart | 3 +- .../repositories/remote_asset.repository.dart | 2 +- .../repositories/store.repository.dart | 111 +- .../repositories/user.repository.dart | 64 +- mobile/lib/main.dart | 43 +- .../album_add_asset_response.model.dart | 38 - .../albums/album_viewer_page_state.model.dart | 60 - .../asset_selection_page_result.model.dart | 18 - mobile/lib/models/asset_selection_state.dart | 47 - .../models/backup/available_album.model.dart | 35 - .../models/backup/backup_candidate.model.dart | 19 - .../lib/models/backup/backup_state.model.dart | 173 - .../backup/current_upload_asset.model.dart | 95 - .../backup/error_upload_asset.model.dart | 65 - .../backup/manual_upload_state.model.dart | 102 - .../backup/success_upload_asset.model.dart | 31 - mobile/lib/models/memories/memory.model.dart | 29 - .../models/search/search_filter.model.dart | 2 +- .../models/search/search_result.model.dart | 28 - .../search_result_page_state.model.dart | 57 - ...additional_shared_user_selection.page.dart | 115 - .../album/album_asset_selection.page.dart | 74 - .../lib/pages/album/album_control_button.dart | 37 - mobile/lib/pages/album/album_date_range.dart | 53 - mobile/lib/pages/album/album_description.dart | 42 - .../lib/pages/album/album_options.page.dart | 192 - .../pages/album/album_shared_user_icons.dart | 52 - .../album_shared_user_selection.page.dart | 140 - mobile/lib/pages/album/album_title.dart | 38 - mobile/lib/pages/album/album_viewer.dart | 165 - mobile/lib/pages/album/album_viewer.page.dart | 29 - mobile/lib/pages/albums/albums.page.dart | 359 -- .../lib/pages/backup/album_preview.page.dart | 64 - .../backup/backup_album_selection.page.dart | 225 - .../pages/backup/backup_controller.page.dart | 286 -- .../lib/pages/backup/backup_options.page.dart | 24 - .../backup/failed_backup_status.page.dart | 116 - mobile/lib/pages/common/activities.page.dart | 97 - .../pages/common/change_experience.page.dart | 168 - .../lib/pages/common/create_album.page.dart | 238 -- .../common/gallery_stacked_children.dart | 85 - .../lib/pages/common/gallery_viewer.page.dart | 438 -- .../common/native_video_viewer.page.dart | 282 -- mobile/lib/pages/common/settings.page.dart | 19 +- .../lib/pages/common/splash_screen.page.dart | 97 +- .../lib/pages/common/tab_controller.page.dart | 142 - mobile/lib/pages/editing/crop.page.dart | 177 - mobile/lib/pages/editing/edit.page.dart | 131 - mobile/lib/pages/editing/filter.page.dart | 159 - mobile/lib/pages/library/archive.page.dart | 37 - mobile/lib/pages/library/favorite.page.dart | 34 - mobile/lib/pages/library/library.page.dart | 383 -- .../lib/pages/library/local_albums.page.dart | 49 - .../lib/pages/library/locked/locked.page.dart | 82 - .../pages/library/locked/pin_auth.page.dart | 14 +- .../pages/library/partner/partner.page.dart | 139 - .../library/partner/partner_detail.page.dart | 99 - .../people/people_collection.page.dart | 127 - .../places/places_collection.page.dart | 136 - mobile/lib/pages/library/trash.page.dart | 225 - .../permission_onboarding.page.dart | 141 - mobile/lib/pages/photos/memory.page.dart | 324 -- mobile/lib/pages/photos/photos.page.dart | 130 - .../pages/search/all_motion_videos.page.dart | 25 - mobile/lib/pages/search/all_people.page.dart | 31 - mobile/lib/pages/search/all_places.page.dart | 26 - mobile/lib/pages/search/all_videos.page.dart | 22 - mobile/lib/pages/search/map/map.page.dart | 384 -- .../lib/pages/search/person_result.page.dart | 101 - .../lib/pages/search/recently_taken.page.dart | 25 - mobile/lib/pages/search/search.page.dart | 760 ---- .../pages/share_intent/share_intent.page.dart | 3 +- .../pages/search/drift_search.page.dart | 2 +- .../similar_photos_action_button.widget.dart | 2 +- .../widgets/album/album_selector.widget.dart | 2 +- .../presentation/widgets/map/map.widget.dart | 2 +- .../lib/providers/album/album.provider.dart | 151 - .../album/album_sort_by_options.provider.dart | 114 +- .../album_sort_by_options.provider.g.dart | 43 - .../album/album_viewer.provider.dart | 74 - .../album/current_album.provider.dart | 15 - .../suggested_shared_users.provider.dart | 14 - .../providers/app_life_cycle.provider.dart | 78 +- mobile/lib/providers/asset.provider.dart | 182 - .../asset_viewer/asset_people.provider.dart | 49 - .../asset_viewer/asset_people.provider.g.dart | 192 - .../asset_viewer/asset_stack.provider.dart | 42 - .../asset_viewer/asset_stack.provider.g.dart | 27 - .../asset_viewer/current_asset.provider.dart | 15 - .../current_asset.provider.g.dart | 26 - .../asset_viewer/download.provider.dart | 52 +- .../render_list_status_provider.dart | 19 - .../lib/providers/backup/backup.provider.dart | 661 +-- .../backup/backup_verification.provider.dart | 101 - .../backup_verification.provider.g.dart | 27 - .../backup/error_backup_list.provider.dart | 22 - .../ios_background_settings.provider.dart | 54 - .../backup/manual_upload.provider.dart | 391 -- mobile/lib/providers/cast.provider.dart | 21 - mobile/lib/providers/db.provider.dart | 5 - .../exceptions/image_loading_exception.dart | 5 - .../infrastructure/action.provider.dart | 6 +- .../providers/infrastructure/db.provider.dart | 7 - .../infrastructure/db.provider.g.dart | 27 - .../infrastructure/device_asset.provider.dart | 7 - .../infrastructure/exif.provider.dart | 9 - .../infrastructure/exif.provider.g.dart | 27 - .../infrastructure/store.provider.dart | 5 - .../infrastructure/store.provider.g.dart | 17 - .../infrastructure/user.provider.dart | 11 +- .../infrastructure/user.provider.g.dart | 19 +- mobile/lib/providers/memory.provider.dart | 9 - mobile/lib/providers/partner.provider.dart | 89 - .../search/all_motion_photos.provider.dart | 7 - .../search/paginated_search.provider.dart | 46 - .../search/paginated_search.provider.g.dart | 29 - .../lib/providers/search/people.provider.dart | 13 - .../providers/search/people.provider.g.dart | 122 +- .../search/recently_taken_asset.provider.dart | 9 - mobile/lib/providers/timeline.provider.dart | 68 - mobile/lib/providers/trash.provider.dart | 45 - mobile/lib/providers/user.provider.dart | 27 - mobile/lib/providers/websocket.provider.dart | 182 +- mobile/lib/repositories/album.repository.dart | 139 - .../repositories/album_api.repository.dart | 171 - .../repositories/album_media.repository.dart | 99 - mobile/lib/repositories/asset.repository.dart | 220 - .../repositories/asset_api.repository.dart | 26 +- .../repositories/asset_media.repository.dart | 39 +- mobile/lib/repositories/auth.repository.dart | 25 +- .../lib/repositories/backup.repository.dart | 32 - .../lib/repositories/database.repository.dart | 25 - mobile/lib/repositories/etag.repository.dart | 27 - .../repositories/file_media.repository.dart | 24 +- .../lib/repositories/partner.repository.dart | 34 - .../lib/repositories/timeline.repository.dart | 146 - .../lib/routing/app_navigation_observer.dart | 16 - .../lib/routing/backup_permission_guard.dart | 21 - mobile/lib/routing/gallery_guard.dart | 34 - mobile/lib/routing/router.dart | 154 - mobile/lib/routing/router.gr.dart | 1201 ------ mobile/lib/services/activity.service.dart | 23 +- mobile/lib/services/album.service.dart | 425 -- mobile/lib/services/asset.service.dart | 465 --- mobile/lib/services/background.service.dart | 595 --- mobile/lib/services/backup.service.dart | 473 --- mobile/lib/services/backup_album.service.dart | 33 - .../services/backup_verification.service.dart | 192 - mobile/lib/services/deep_link.service.dart | 145 +- mobile/lib/services/device.service.dart | 25 - mobile/lib/services/download.service.dart | 65 +- mobile/lib/services/entity.service.dart | 44 - mobile/lib/services/etag.service.dart | 14 - mobile/lib/services/exif.service.dart | 15 - mobile/lib/services/hash.service.dart | 191 - .../services/local_notification.service.dart | 118 - mobile/lib/services/memory.service.dart | 71 - mobile/lib/services/partner.service.dart | 73 - mobile/lib/services/person.service.dart | 24 +- mobile/lib/services/person.service.g.dart | 2 +- mobile/lib/services/search.service.dart | 33 +- mobile/lib/services/share.service.dart | 74 - mobile/lib/services/stack.service.dart | 64 - mobile/lib/services/sync.service.dart | 945 ----- mobile/lib/services/timeline.service.dart | 98 - mobile/lib/services/trash.service.dart | 75 - mobile/lib/utils/backup_progress.dart | 83 - mobile/lib/utils/bootstrap.dart | 63 +- mobile/lib/utils/color_filter_generator.dart | 99 - mobile/lib/utils/datetime_comparison.dart | 2 - mobile/lib/utils/hooks/blurhash_hook.dart | 12 +- mobile/lib/utils/image_url_builder.dart | 40 - mobile/lib/utils/immich_loading_overlay.dart | 64 - mobile/lib/utils/isolate.dart | 16 +- mobile/lib/utils/migration.dart | 466 +-- mobile/lib/utils/provider_utils.dart | 4 - mobile/lib/utils/selection_handlers.dart | 143 - mobile/lib/utils/string_helper.dart | 7 - mobile/lib/utils/throttle.dart | 51 - mobile/lib/utils/thumbnail_utils.dart | 49 - .../activities/activity_text_field.dart | 85 - .../lib/widgets/activities/activity_tile.dart | 113 - .../activities/dismissible_activity.dart | 2 - .../album/add_to_album_bottom_sheet.dart | 98 - .../album/add_to_album_sliverlist.dart | 65 - .../widgets/album/album_thumbnail_card.dart | 111 - .../album/album_thumbnail_listtile.dart | 93 - .../widgets/album/album_title_text_field.dart | 74 - .../widgets/album/album_viewer_appbar.dart | 307 -- .../album_viewer_editable_description.dart | 82 - .../album/album_viewer_editable_title.dart | 81 - .../album/shared_album_thumbnail_image.dart | 20 - .../widgets/asset_grid/asset_drag_region.dart | 207 - .../asset_grid/asset_grid_data_structure.dart | 307 -- .../asset_grid/control_bottom_app_bar.dart | 388 -- .../lib/widgets/asset_grid/delete_dialog.dart | 12 - .../disable_multi_select_button.dart | 31 - .../asset_grid/draggable_scrollbar.dart | 559 --- .../draggable_scrollbar_custom.dart | 490 --- .../asset_grid/group_divider_title.dart | 84 - .../widgets/asset_grid/immich_asset_grid.dart | 135 - .../asset_grid/immich_asset_grid_view.dart | 828 ---- .../widgets/asset_grid/multiselect_grid.dart | 458 -- .../multiselect_grid_status_indicator.dart | 26 - .../widgets/asset_grid/thumbnail_image.dart | 259 -- .../lib/widgets/asset_grid/upload_dialog.dart | 14 - .../asset_viewer/advanced_bottom_sheet.dart | 77 - .../asset_viewer/bottom_gallery_bar.dart | 362 -- .../asset_viewer/center_play_button.dart | 44 - .../custom_video_player_controls.dart | 114 - .../asset_viewer/description_input.dart | 106 - .../detail_panel/asset_date_time.dart | 44 - .../detail_panel/asset_details.dart | 40 - .../detail_panel/asset_location.dart | 88 - .../detail_panel/camera_info.dart | 26 - .../detail_panel/detail_panel.dart | 37 - .../asset_viewer/detail_panel/file_info.dart | 47 - .../detail_panel/people_info.dart | 91 - .../widgets/asset_viewer/gallery_app_bar.dart | 119 - .../asset_viewer/motion_photo_button.dart | 22 - .../asset_viewer/top_control_app_bar.dart | 182 - .../lib/widgets/backup/album_info_card.dart | 185 - .../widgets/backup/album_info_list_tile.dart | 98 - .../lib/widgets/backup/asset_info_table.dart | 105 - .../backup/current_backup_asset_info_box.dart | 37 - mobile/lib/widgets/backup/error_chip.dart | 28 - .../lib/widgets/backup/error_chip_text.dart | 22 - .../backup/icloud_download_progress_bar.dart | 43 - .../widgets/backup/ios_debug_info_tile.dart | 49 - .../widgets/backup/upload_progress_bar.dart | 45 - mobile/lib/widgets/backup/upload_stats.dart | 51 - .../common/app_bar_dialog/app_bar_dialog.dart | 20 +- .../app_bar_dialog/app_bar_profile_info.dart | 5 - mobile/lib/widgets/common/drag_sheet.dart | 54 - mobile/lib/widgets/common/immich_app_bar.dart | 170 - mobile/lib/widgets/common/immich_image.dart | 100 - .../lib/widgets/common/immich_thumbnail.dart | 88 - mobile/lib/widgets/common/share_dialog.dart | 19 - .../widgets/common/thumbhash_placeholder.dart | 9 - .../widgets/forms/change_password_form.dart | 9 +- .../lib/widgets/forms/login/email_input.dart | 41 - .../lib/widgets/forms/login/loading_icon.dart | 13 - .../lib/widgets/forms/login/login_button.dart | 19 - .../lib/widgets/forms/login/login_form.dart | 40 +- mobile/lib/widgets/map/map_app_bar.dart | 128 - mobile/lib/widgets/map/map_asset_grid.dart | 289 -- mobile/lib/widgets/map/map_bottom_sheet.dart | 98 - .../lib/widgets/map/map_settings_sheet.dart | 61 - .../map/positioned_asset_marker_icon.dart | 43 - .../widgets/memories/memory_bottom_info.dart | 50 - mobile/lib/widgets/memories/memory_card.dart | 117 - mobile/lib/widgets/memories/memory_lane.dart | 91 - .../widgets/search/curated_people_row.dart | 89 - .../widgets/search/curated_places_row.dart | 60 - mobile/lib/widgets/search/explore_grid.dart | 69 - .../widgets/search/person_name_edit_form.dart | 65 - .../search_filter/media_type_picker.dart | 2 +- .../widgets/search/search_map_thumbnail.dart | 27 - .../widgets/search/search_row_section.dart | 34 - .../widgets/settings/advanced_settings.dart | 47 +- .../asset_list_group_settings.dart | 2 +- .../asset_list_layout_settings.dart | 9 - .../backup_settings/background_settings.dart | 204 - .../backup_settings/backup_settings.dart | 82 - .../backup_settings/foreground_settings.dart | 35 - .../settings/beta_timeline_list_tile.dart | 71 - .../settings/local_storage_settings.dart | 51 - mobile/pubspec.lock | 58 - mobile/pubspec.yaml | 12 - mobile/test/api.mocks.dart | 2 - mobile/test/domain/service.mock.dart | 7 - .../domain/services/album.service_test.dart | 115 - .../domain/services/asset.service_test.dart | 185 - .../domain/services/log_service_test.dart | 4 +- .../domain/services/store_service_test.dart | 32 +- .../domain/services/user_service_test.dart | 15 +- mobile/test/dto.mocks.dart | 6 - mobile/test/fixtures/album.stub.dart | 104 - mobile/test/fixtures/asset.stub.dart | 55 - mobile/test/fixtures/exif.stub.dart | 18 - mobile/test/fixtures/user.stub.dart | 20 - .../repositories/exif_repository_test.dart | 49 - .../repositories/store_repository_test.dart | 71 +- .../sync_api_repository_test.dart | 13 +- .../test/infrastructure/repository.mock.dart | 8 - .../activity/activities_page_test.dart | 175 - .../test/modules/activity/activity_mocks.dart | 19 - .../activity/activity_provider_test.dart | 331 -- .../activity_statistics_provider_test.dart | 71 - .../activity/activity_text_field_test.dart | 149 - .../modules/activity/activity_tile_test.dart | 165 - .../activity/dismissible_activity_test.dart | 99 - mobile/test/modules/album/album_mocks.dart | 13 - .../album_sort_by_options_provider_test.dart | 270 -- .../asset_viewer/asset_viewer_mocks.dart | 13 - .../extensions/asset_extensions_test.dart | 113 - .../home/asset_grid_data_structure_test.dart | 113 - .../modules/map/map_theme_override_test.dart | 10 +- .../test/modules/settings/settings_mocks.dart | 4 - mobile/test/modules/shared/shared_mocks.dart | 11 - .../modules/shared/sync_service_test.dart | 285 -- mobile/test/modules/utils/migration_test.dart | 131 - mobile/test/modules/utils/throttler_test.dart | 46 - .../modules/utils/thumbnail_utils_test.dart | 63 - .../test/pages/search/search.page_test.dart | 98 - mobile/test/repository.mocks.dart | 38 +- mobile/test/service.mocks.dart | 21 - mobile/test/services/album.service_test.dart | 177 - mobile/test/services/asset.service_test.dart | 103 - mobile/test/services/auth.service_test.dart | 24 +- mobile/test/services/entity.service_test.dart | 76 - mobile/test/services/hash_service_test.dart | 349 -- mobile/test/test_utils.dart | 69 - mobile/test/test_utils/medium_factory.dart | 23 - 367 files changed, 332 insertions(+), 50870 deletions(-) delete mode 100644 mobile/android/app/src/main/kotlin/app/alextran/immich/BackgroundServicePlugin.kt delete mode 100644 mobile/android/app/src/main/kotlin/app/alextran/immich/BackupWorker.kt delete mode 100644 mobile/android/app/src/main/kotlin/app/alextran/immich/ContentObserverWorker.kt delete mode 100644 mobile/ios/Runner/BackgroundSync/BackgroundServicePlugin.swift delete mode 100644 mobile/ios/Runner/BackgroundSync/BackgroundSyncWorker.swift delete mode 100644 mobile/lib/domain/interfaces/db.interface.dart delete mode 100644 mobile/lib/domain/models/device_asset.model.dart delete mode 100644 mobile/lib/entities/README.md delete mode 100644 mobile/lib/entities/album.entity.dart delete mode 100644 mobile/lib/entities/album.entity.g.dart delete mode 100644 mobile/lib/entities/android_device_asset.entity.dart delete mode 100644 mobile/lib/entities/android_device_asset.entity.g.dart delete mode 100644 mobile/lib/entities/asset.entity.dart delete mode 100644 mobile/lib/entities/asset.entity.g.dart delete mode 100644 mobile/lib/entities/backup_album.entity.dart delete mode 100644 mobile/lib/entities/backup_album.entity.g.dart delete mode 100644 mobile/lib/entities/device_asset.entity.dart delete mode 100644 mobile/lib/entities/duplicated_asset.entity.dart delete mode 100644 mobile/lib/entities/duplicated_asset.entity.g.dart delete mode 100644 mobile/lib/entities/etag.entity.dart delete mode 100644 mobile/lib/entities/etag.entity.g.dart delete mode 100644 mobile/lib/entities/ios_device_asset.entity.dart delete mode 100644 mobile/lib/entities/ios_device_asset.entity.g.dart delete mode 100644 mobile/lib/infrastructure/entities/device_asset.entity.dart delete mode 100644 mobile/lib/infrastructure/entities/device_asset.entity.g.dart delete mode 100644 mobile/lib/infrastructure/entities/exif.entity.g.dart delete mode 100644 mobile/lib/infrastructure/entities/store.entity.g.dart delete mode 100644 mobile/lib/infrastructure/entities/user.entity.g.dart delete mode 100644 mobile/lib/infrastructure/repositories/device_asset.repository.dart delete mode 100644 mobile/lib/infrastructure/repositories/exif.repository.dart delete mode 100644 mobile/lib/models/albums/album_add_asset_response.model.dart delete mode 100644 mobile/lib/models/albums/album_viewer_page_state.model.dart delete mode 100644 mobile/lib/models/albums/asset_selection_page_result.model.dart delete mode 100644 mobile/lib/models/asset_selection_state.dart delete mode 100644 mobile/lib/models/backup/available_album.model.dart delete mode 100644 mobile/lib/models/backup/backup_candidate.model.dart delete mode 100644 mobile/lib/models/backup/backup_state.model.dart delete mode 100644 mobile/lib/models/backup/current_upload_asset.model.dart delete mode 100644 mobile/lib/models/backup/error_upload_asset.model.dart delete mode 100644 mobile/lib/models/backup/manual_upload_state.model.dart delete mode 100644 mobile/lib/models/backup/success_upload_asset.model.dart delete mode 100644 mobile/lib/models/memories/memory.model.dart delete mode 100644 mobile/lib/models/search/search_result.model.dart delete mode 100644 mobile/lib/models/search/search_result_page_state.model.dart delete mode 100644 mobile/lib/pages/album/album_additional_shared_user_selection.page.dart delete mode 100644 mobile/lib/pages/album/album_asset_selection.page.dart delete mode 100644 mobile/lib/pages/album/album_control_button.dart delete mode 100644 mobile/lib/pages/album/album_date_range.dart delete mode 100644 mobile/lib/pages/album/album_description.dart delete mode 100644 mobile/lib/pages/album/album_options.page.dart delete mode 100644 mobile/lib/pages/album/album_shared_user_icons.dart delete mode 100644 mobile/lib/pages/album/album_shared_user_selection.page.dart delete mode 100644 mobile/lib/pages/album/album_title.dart delete mode 100644 mobile/lib/pages/album/album_viewer.dart delete mode 100644 mobile/lib/pages/album/album_viewer.page.dart delete mode 100644 mobile/lib/pages/albums/albums.page.dart delete mode 100644 mobile/lib/pages/backup/album_preview.page.dart delete mode 100644 mobile/lib/pages/backup/backup_album_selection.page.dart delete mode 100644 mobile/lib/pages/backup/backup_controller.page.dart delete mode 100644 mobile/lib/pages/backup/backup_options.page.dart delete mode 100644 mobile/lib/pages/backup/failed_backup_status.page.dart delete mode 100644 mobile/lib/pages/common/activities.page.dart delete mode 100644 mobile/lib/pages/common/change_experience.page.dart delete mode 100644 mobile/lib/pages/common/create_album.page.dart delete mode 100644 mobile/lib/pages/common/gallery_stacked_children.dart delete mode 100644 mobile/lib/pages/common/gallery_viewer.page.dart delete mode 100644 mobile/lib/pages/common/native_video_viewer.page.dart delete mode 100644 mobile/lib/pages/common/tab_controller.page.dart delete mode 100644 mobile/lib/pages/editing/crop.page.dart delete mode 100644 mobile/lib/pages/editing/edit.page.dart delete mode 100644 mobile/lib/pages/editing/filter.page.dart delete mode 100644 mobile/lib/pages/library/archive.page.dart delete mode 100644 mobile/lib/pages/library/favorite.page.dart delete mode 100644 mobile/lib/pages/library/library.page.dart delete mode 100644 mobile/lib/pages/library/local_albums.page.dart delete mode 100644 mobile/lib/pages/library/locked/locked.page.dart delete mode 100644 mobile/lib/pages/library/partner/partner.page.dart delete mode 100644 mobile/lib/pages/library/partner/partner_detail.page.dart delete mode 100644 mobile/lib/pages/library/people/people_collection.page.dart delete mode 100644 mobile/lib/pages/library/places/places_collection.page.dart delete mode 100644 mobile/lib/pages/library/trash.page.dart delete mode 100644 mobile/lib/pages/onboarding/permission_onboarding.page.dart delete mode 100644 mobile/lib/pages/photos/memory.page.dart delete mode 100644 mobile/lib/pages/photos/photos.page.dart delete mode 100644 mobile/lib/pages/search/all_motion_videos.page.dart delete mode 100644 mobile/lib/pages/search/all_people.page.dart delete mode 100644 mobile/lib/pages/search/all_places.page.dart delete mode 100644 mobile/lib/pages/search/all_videos.page.dart delete mode 100644 mobile/lib/pages/search/map/map.page.dart delete mode 100644 mobile/lib/pages/search/person_result.page.dart delete mode 100644 mobile/lib/pages/search/recently_taken.page.dart delete mode 100644 mobile/lib/pages/search/search.page.dart delete mode 100644 mobile/lib/providers/album/album.provider.dart delete mode 100644 mobile/lib/providers/album/album_sort_by_options.provider.g.dart delete mode 100644 mobile/lib/providers/album/album_viewer.provider.dart delete mode 100644 mobile/lib/providers/album/current_album.provider.dart delete mode 100644 mobile/lib/providers/album/suggested_shared_users.provider.dart delete mode 100644 mobile/lib/providers/asset.provider.dart delete mode 100644 mobile/lib/providers/asset_viewer/asset_people.provider.dart delete mode 100644 mobile/lib/providers/asset_viewer/asset_people.provider.g.dart delete mode 100644 mobile/lib/providers/asset_viewer/asset_stack.provider.dart delete mode 100644 mobile/lib/providers/asset_viewer/asset_stack.provider.g.dart delete mode 100644 mobile/lib/providers/asset_viewer/current_asset.provider.dart delete mode 100644 mobile/lib/providers/asset_viewer/current_asset.provider.g.dart delete mode 100644 mobile/lib/providers/asset_viewer/render_list_status_provider.dart delete mode 100644 mobile/lib/providers/backup/backup_verification.provider.dart delete mode 100644 mobile/lib/providers/backup/backup_verification.provider.g.dart delete mode 100644 mobile/lib/providers/backup/error_backup_list.provider.dart delete mode 100644 mobile/lib/providers/backup/ios_background_settings.provider.dart delete mode 100644 mobile/lib/providers/backup/manual_upload.provider.dart delete mode 100644 mobile/lib/providers/db.provider.dart delete mode 100644 mobile/lib/providers/image/exceptions/image_loading_exception.dart delete mode 100644 mobile/lib/providers/infrastructure/db.provider.g.dart delete mode 100644 mobile/lib/providers/infrastructure/device_asset.provider.dart delete mode 100644 mobile/lib/providers/infrastructure/exif.provider.dart delete mode 100644 mobile/lib/providers/infrastructure/exif.provider.g.dart delete mode 100644 mobile/lib/providers/memory.provider.dart delete mode 100644 mobile/lib/providers/partner.provider.dart delete mode 100644 mobile/lib/providers/search/all_motion_photos.provider.dart delete mode 100644 mobile/lib/providers/search/paginated_search.provider.dart delete mode 100644 mobile/lib/providers/search/paginated_search.provider.g.dart delete mode 100644 mobile/lib/providers/search/recently_taken_asset.provider.dart delete mode 100644 mobile/lib/providers/timeline.provider.dart delete mode 100644 mobile/lib/providers/trash.provider.dart delete mode 100644 mobile/lib/repositories/album.repository.dart delete mode 100644 mobile/lib/repositories/album_api.repository.dart delete mode 100644 mobile/lib/repositories/album_media.repository.dart delete mode 100644 mobile/lib/repositories/asset.repository.dart delete mode 100644 mobile/lib/repositories/backup.repository.dart delete mode 100644 mobile/lib/repositories/database.repository.dart delete mode 100644 mobile/lib/repositories/etag.repository.dart delete mode 100644 mobile/lib/repositories/partner.repository.dart delete mode 100644 mobile/lib/repositories/timeline.repository.dart delete mode 100644 mobile/lib/routing/backup_permission_guard.dart delete mode 100644 mobile/lib/routing/gallery_guard.dart delete mode 100644 mobile/lib/services/album.service.dart delete mode 100644 mobile/lib/services/asset.service.dart delete mode 100644 mobile/lib/services/background.service.dart delete mode 100644 mobile/lib/services/backup.service.dart delete mode 100644 mobile/lib/services/backup_album.service.dart delete mode 100644 mobile/lib/services/backup_verification.service.dart delete mode 100644 mobile/lib/services/device.service.dart delete mode 100644 mobile/lib/services/entity.service.dart delete mode 100644 mobile/lib/services/etag.service.dart delete mode 100644 mobile/lib/services/exif.service.dart delete mode 100644 mobile/lib/services/hash.service.dart delete mode 100644 mobile/lib/services/local_notification.service.dart delete mode 100644 mobile/lib/services/memory.service.dart delete mode 100644 mobile/lib/services/partner.service.dart delete mode 100644 mobile/lib/services/share.service.dart delete mode 100644 mobile/lib/services/stack.service.dart delete mode 100644 mobile/lib/services/sync.service.dart delete mode 100644 mobile/lib/services/timeline.service.dart delete mode 100644 mobile/lib/services/trash.service.dart delete mode 100644 mobile/lib/utils/backup_progress.dart delete mode 100644 mobile/lib/utils/color_filter_generator.dart delete mode 100644 mobile/lib/utils/datetime_comparison.dart delete mode 100644 mobile/lib/utils/immich_loading_overlay.dart delete mode 100644 mobile/lib/utils/selection_handlers.dart delete mode 100644 mobile/lib/utils/string_helper.dart delete mode 100644 mobile/lib/utils/throttle.dart delete mode 100644 mobile/lib/utils/thumbnail_utils.dart delete mode 100644 mobile/lib/widgets/activities/activity_text_field.dart delete mode 100644 mobile/lib/widgets/activities/activity_tile.dart delete mode 100644 mobile/lib/widgets/album/add_to_album_bottom_sheet.dart delete mode 100644 mobile/lib/widgets/album/add_to_album_sliverlist.dart delete mode 100644 mobile/lib/widgets/album/album_thumbnail_card.dart delete mode 100644 mobile/lib/widgets/album/album_thumbnail_listtile.dart delete mode 100644 mobile/lib/widgets/album/album_title_text_field.dart delete mode 100644 mobile/lib/widgets/album/album_viewer_appbar.dart delete mode 100644 mobile/lib/widgets/album/album_viewer_editable_description.dart delete mode 100644 mobile/lib/widgets/album/album_viewer_editable_title.dart delete mode 100644 mobile/lib/widgets/album/shared_album_thumbnail_image.dart delete mode 100644 mobile/lib/widgets/asset_grid/asset_drag_region.dart delete mode 100644 mobile/lib/widgets/asset_grid/asset_grid_data_structure.dart delete mode 100644 mobile/lib/widgets/asset_grid/control_bottom_app_bar.dart delete mode 100644 mobile/lib/widgets/asset_grid/disable_multi_select_button.dart delete mode 100644 mobile/lib/widgets/asset_grid/draggable_scrollbar.dart delete mode 100644 mobile/lib/widgets/asset_grid/draggable_scrollbar_custom.dart delete mode 100644 mobile/lib/widgets/asset_grid/group_divider_title.dart delete mode 100644 mobile/lib/widgets/asset_grid/immich_asset_grid.dart delete mode 100644 mobile/lib/widgets/asset_grid/immich_asset_grid_view.dart delete mode 100644 mobile/lib/widgets/asset_grid/multiselect_grid.dart delete mode 100644 mobile/lib/widgets/asset_grid/multiselect_grid_status_indicator.dart delete mode 100644 mobile/lib/widgets/asset_grid/thumbnail_image.dart delete mode 100644 mobile/lib/widgets/asset_grid/upload_dialog.dart delete mode 100644 mobile/lib/widgets/asset_viewer/advanced_bottom_sheet.dart delete mode 100644 mobile/lib/widgets/asset_viewer/bottom_gallery_bar.dart delete mode 100644 mobile/lib/widgets/asset_viewer/center_play_button.dart delete mode 100644 mobile/lib/widgets/asset_viewer/custom_video_player_controls.dart delete mode 100644 mobile/lib/widgets/asset_viewer/description_input.dart delete mode 100644 mobile/lib/widgets/asset_viewer/detail_panel/asset_date_time.dart delete mode 100644 mobile/lib/widgets/asset_viewer/detail_panel/asset_details.dart delete mode 100644 mobile/lib/widgets/asset_viewer/detail_panel/asset_location.dart delete mode 100644 mobile/lib/widgets/asset_viewer/detail_panel/camera_info.dart delete mode 100644 mobile/lib/widgets/asset_viewer/detail_panel/detail_panel.dart delete mode 100644 mobile/lib/widgets/asset_viewer/detail_panel/file_info.dart delete mode 100644 mobile/lib/widgets/asset_viewer/detail_panel/people_info.dart delete mode 100644 mobile/lib/widgets/asset_viewer/gallery_app_bar.dart delete mode 100644 mobile/lib/widgets/asset_viewer/motion_photo_button.dart delete mode 100644 mobile/lib/widgets/asset_viewer/top_control_app_bar.dart delete mode 100644 mobile/lib/widgets/backup/album_info_card.dart delete mode 100644 mobile/lib/widgets/backup/album_info_list_tile.dart delete mode 100644 mobile/lib/widgets/backup/asset_info_table.dart delete mode 100644 mobile/lib/widgets/backup/current_backup_asset_info_box.dart delete mode 100644 mobile/lib/widgets/backup/error_chip.dart delete mode 100644 mobile/lib/widgets/backup/error_chip_text.dart delete mode 100644 mobile/lib/widgets/backup/icloud_download_progress_bar.dart delete mode 100644 mobile/lib/widgets/backup/ios_debug_info_tile.dart delete mode 100644 mobile/lib/widgets/backup/upload_progress_bar.dart delete mode 100644 mobile/lib/widgets/backup/upload_stats.dart delete mode 100644 mobile/lib/widgets/common/drag_sheet.dart delete mode 100644 mobile/lib/widgets/common/immich_app_bar.dart delete mode 100644 mobile/lib/widgets/common/immich_image.dart delete mode 100644 mobile/lib/widgets/common/immich_thumbnail.dart delete mode 100644 mobile/lib/widgets/common/share_dialog.dart delete mode 100644 mobile/lib/widgets/forms/login/email_input.dart delete mode 100644 mobile/lib/widgets/forms/login/loading_icon.dart delete mode 100644 mobile/lib/widgets/forms/login/login_button.dart delete mode 100644 mobile/lib/widgets/map/map_app_bar.dart delete mode 100644 mobile/lib/widgets/map/map_asset_grid.dart delete mode 100644 mobile/lib/widgets/map/map_bottom_sheet.dart delete mode 100644 mobile/lib/widgets/map/map_settings_sheet.dart delete mode 100644 mobile/lib/widgets/map/positioned_asset_marker_icon.dart delete mode 100644 mobile/lib/widgets/memories/memory_bottom_info.dart delete mode 100644 mobile/lib/widgets/memories/memory_card.dart delete mode 100644 mobile/lib/widgets/memories/memory_lane.dart delete mode 100644 mobile/lib/widgets/search/curated_people_row.dart delete mode 100644 mobile/lib/widgets/search/curated_places_row.dart delete mode 100644 mobile/lib/widgets/search/explore_grid.dart delete mode 100644 mobile/lib/widgets/search/person_name_edit_form.dart delete mode 100644 mobile/lib/widgets/search/search_map_thumbnail.dart delete mode 100644 mobile/lib/widgets/search/search_row_section.dart delete mode 100644 mobile/lib/widgets/settings/backup_settings/background_settings.dart delete mode 100644 mobile/lib/widgets/settings/backup_settings/backup_settings.dart delete mode 100644 mobile/lib/widgets/settings/backup_settings/foreground_settings.dart delete mode 100644 mobile/lib/widgets/settings/beta_timeline_list_tile.dart delete mode 100644 mobile/lib/widgets/settings/local_storage_settings.dart delete mode 100644 mobile/test/domain/services/album.service_test.dart delete mode 100644 mobile/test/domain/services/asset.service_test.dart delete mode 100644 mobile/test/dto.mocks.dart delete mode 100644 mobile/test/fixtures/exif.stub.dart delete mode 100644 mobile/test/infrastructure/repositories/exif_repository_test.dart delete mode 100644 mobile/test/modules/activity/activities_page_test.dart delete mode 100644 mobile/test/modules/activity/activity_mocks.dart delete mode 100644 mobile/test/modules/activity/activity_provider_test.dart delete mode 100644 mobile/test/modules/activity/activity_statistics_provider_test.dart delete mode 100644 mobile/test/modules/activity/activity_text_field_test.dart delete mode 100644 mobile/test/modules/activity/activity_tile_test.dart delete mode 100644 mobile/test/modules/activity/dismissible_activity_test.dart delete mode 100644 mobile/test/modules/album/album_mocks.dart delete mode 100644 mobile/test/modules/album/album_sort_by_options_provider_test.dart delete mode 100644 mobile/test/modules/asset_viewer/asset_viewer_mocks.dart delete mode 100644 mobile/test/modules/extensions/asset_extensions_test.dart delete mode 100644 mobile/test/modules/home/asset_grid_data_structure_test.dart delete mode 100644 mobile/test/modules/settings/settings_mocks.dart delete mode 100644 mobile/test/modules/shared/shared_mocks.dart delete mode 100644 mobile/test/modules/shared/sync_service_test.dart delete mode 100644 mobile/test/modules/utils/migration_test.dart delete mode 100644 mobile/test/modules/utils/throttler_test.dart delete mode 100644 mobile/test/modules/utils/thumbnail_utils_test.dart delete mode 100644 mobile/test/pages/search/search.page_test.dart delete mode 100644 mobile/test/services/album.service_test.dart delete mode 100644 mobile/test/services/asset.service_test.dart delete mode 100644 mobile/test/services/entity.service_test.dart delete mode 100644 mobile/test/services/hash_service_test.dart diff --git a/mobile/android/app/src/main/kotlin/app/alextran/immich/BackgroundServicePlugin.kt b/mobile/android/app/src/main/kotlin/app/alextran/immich/BackgroundServicePlugin.kt deleted file mode 100644 index f62f25558d..0000000000 --- a/mobile/android/app/src/main/kotlin/app/alextran/immich/BackgroundServicePlugin.kt +++ /dev/null @@ -1,389 +0,0 @@ -package app.alextran.immich - -import android.app.Activity -import android.content.ContentResolver -import android.content.ContentUris -import android.content.Context -import android.content.Intent -import android.net.Uri -import android.os.Build -import android.os.Bundle -import android.provider.MediaStore -import android.provider.Settings -import android.util.Log -import androidx.annotation.RequiresApi -import io.flutter.embedding.engine.plugins.FlutterPlugin -import io.flutter.embedding.engine.plugins.activity.ActivityAware -import io.flutter.embedding.engine.plugins.activity.ActivityPluginBinding -import io.flutter.plugin.common.BinaryMessenger -import io.flutter.plugin.common.MethodCall -import io.flutter.plugin.common.MethodChannel -import io.flutter.plugin.common.MethodChannel.Result -import io.flutter.plugin.common.PluginRegistry -import java.security.MessageDigest -import java.io.FileInputStream -import kotlinx.coroutines.* -import androidx.core.net.toUri - -/** - * Android plugin for Dart `BackgroundService` and file trash operations - */ -class BackgroundServicePlugin : FlutterPlugin, MethodChannel.MethodCallHandler, ActivityAware, PluginRegistry.ActivityResultListener { - - private var methodChannel: MethodChannel? = null - private var fileTrashChannel: MethodChannel? = null - private var context: Context? = null - private var pendingResult: Result? = null - private val permissionRequestCode = 1001 - private val trashRequestCode = 1002 - private var activityBinding: ActivityPluginBinding? = null - - override fun onAttachedToEngine(binding: FlutterPlugin.FlutterPluginBinding) { - onAttachedToEngine(binding.applicationContext, binding.binaryMessenger) - } - - private fun onAttachedToEngine(ctx: Context, messenger: BinaryMessenger) { - context = ctx - methodChannel = MethodChannel(messenger, "immich/foregroundChannel") - methodChannel?.setMethodCallHandler(this) - - // Add file trash channel - fileTrashChannel = MethodChannel(messenger, "file_trash") - fileTrashChannel?.setMethodCallHandler(this) - } - - override fun onDetachedFromEngine(binding: FlutterPlugin.FlutterPluginBinding) { - onDetachedFromEngine() - } - - private fun onDetachedFromEngine() { - methodChannel?.setMethodCallHandler(null) - methodChannel = null - fileTrashChannel?.setMethodCallHandler(null) - fileTrashChannel = null - } - - override fun onMethodCall(call: MethodCall, result: Result) { - val ctx = context!! - when (call.method) { - // Existing BackgroundService methods - "enable" -> { - val args = call.arguments>()!! - ctx.getSharedPreferences(BackupWorker.SHARED_PREF_NAME, Context.MODE_PRIVATE) - .edit() - .putBoolean(ContentObserverWorker.SHARED_PREF_SERVICE_ENABLED, true) - .putLong(BackupWorker.SHARED_PREF_CALLBACK_KEY, args[0] as Long) - .putString(BackupWorker.SHARED_PREF_NOTIFICATION_TITLE, args[1] as String) - .apply() - ContentObserverWorker.enable(ctx, immediate = args[2] as Boolean) - result.success(true) - } - - "configure" -> { - val args = call.arguments>()!! - val requireUnmeteredNetwork = args[0] as Boolean - val requireCharging = args[1] as Boolean - val triggerUpdateDelay = (args[2] as Number).toLong() - val triggerMaxDelay = (args[3] as Number).toLong() - ContentObserverWorker.configureWork( - ctx, - requireUnmeteredNetwork, - requireCharging, - triggerUpdateDelay, - triggerMaxDelay - ) - result.success(true) - } - - "disable" -> { - ContentObserverWorker.disable(ctx) - BackupWorker.stopWork(ctx) - result.success(true) - } - - "isEnabled" -> { - result.success(ContentObserverWorker.isEnabled(ctx)) - } - - "isIgnoringBatteryOptimizations" -> { - result.success(BackupWorker.isIgnoringBatteryOptimizations(ctx)) - } - - "digestFiles" -> { - val args = call.arguments>()!! - GlobalScope.launch(Dispatchers.IO) { - val buf = ByteArray(BUFFER_SIZE) - val digest: MessageDigest = MessageDigest.getInstance("SHA-1") - val hashes = arrayOfNulls(args.size) - for (i in args.indices) { - val path = args[i] - var len = 0 - try { - val file = FileInputStream(path) - file.use { assetFile -> - while (true) { - len = assetFile.read(buf) - if (len != BUFFER_SIZE) break - digest.update(buf) - } - } - digest.update(buf, 0, len) - hashes[i] = digest.digest() - } catch (e: Exception) { - // skip this file - Log.w(TAG, "Failed to hash file ${args[i]}: $e") - } - } - result.success(hashes.asList()) - } - } - - // File Trash methods moved from MainActivity - "moveToTrash" -> { - val mediaUrls = call.argument>("mediaUrls") - if (mediaUrls != null) { - if ((Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) && hasManageMediaPermission()) { - moveToTrash(mediaUrls, result) - } else { - result.error("PERMISSION_DENIED", "Media permission required", null) - } - } else { - result.error("INVALID_NAME", "The mediaUrls is not specified.", null) - } - } - - "restoreFromTrash" -> { - val fileName = call.argument("fileName") - val type = call.argument("type") - val mediaId = call.argument("mediaId") - if (fileName != null && type != null) { - if ((Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) && hasManageMediaPermission()) { - restoreFromTrash(fileName, type, result) - } else { - result.error("PERMISSION_DENIED", "Media permission required", null) - } - } else - if (mediaId != null && type != null) { - if ((Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) && hasManageMediaPermission()) { - restoreFromTrashById(mediaId, type, result) - } else { - result.error("PERMISSION_DENIED", "Media permission required", null) - } - } else { - result.error("INVALID_PARAMS", "Required params are not specified.", null) - } - } - - "requestManageMediaPermission" -> { - if (!hasManageMediaPermission()) { - requestManageMediaPermission(result) - } else { - Log.e("Manage storage permission", "Permission already granted") - result.success(true) - } - } - - "hasManageMediaPermission" -> { - if (hasManageMediaPermission()) { - Log.i("Manage storage permission", "Permission already granted") - result.success(true) - } else { - result.success(false) - } - } - - "manageMediaPermission" -> requestManageMediaPermission(result) - - else -> result.notImplemented() - } - } - - private fun hasManageMediaPermission(): Boolean { - return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { - MediaStore.canManageMedia(context!!); - } else { - false - } - } - - private fun requestManageMediaPermission(result: Result) { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { - pendingResult = result // Store the result callback - val activity = activityBinding?.activity ?: return - - val intent = Intent(Settings.ACTION_REQUEST_MANAGE_MEDIA) - intent.data = "package:${activity.packageName}".toUri() - activity.startActivityForResult(intent, permissionRequestCode) - } else { - result.success(false) - } - } - - @RequiresApi(Build.VERSION_CODES.R) - private fun moveToTrash(mediaUrls: List, result: Result) { - val urisToTrash = mediaUrls.map { it.toUri() } - if (urisToTrash.isEmpty()) { - result.error("INVALID_ARGS", "No valid URIs provided", null) - return - } - - toggleTrash(urisToTrash, true, result); - } - - @RequiresApi(Build.VERSION_CODES.R) - private fun restoreFromTrash(name: String, type: Int, result: Result) { - val uri = getTrashedFileUri(name, type) - if (uri == null) { - Log.e("TrashError", "Asset Uri cannot be found obtained") - result.error("TrashError", "Asset Uri cannot be found obtained", null) - return - } - Log.e("FILE_URI", uri.toString()) - uri.let { toggleTrash(listOf(it), false, result) } - } - - @RequiresApi(Build.VERSION_CODES.R) - private fun restoreFromTrashById(mediaId: String, type: Int, result: Result) { - val id = mediaId.toLongOrNull() - if (id == null) { - result.error("INVALID_ID", "The file id is not a valid number: $mediaId", null) - return - } - if (!isInTrash(id)) { - result.error("TrashNotFound", "Item with id=$id not found in trash", null) - return - } - - val uri = ContentUris.withAppendedId(contentUriForType(type), id) - - try { - Log.i(TAG, "restoreFromTrashById: uri=$uri (type=$type,id=$id)") - restoreUris(listOf(uri), result) - } catch (e: Exception) { - Log.w(TAG, "restoreFromTrashById failed", e) - } - } - - @RequiresApi(Build.VERSION_CODES.R) - private fun toggleTrash(contentUris: List, isTrashed: Boolean, result: Result) { - val activity = activityBinding?.activity - val contentResolver = context?.contentResolver - if (activity == null || contentResolver == null) { - result.error("TrashError", "Activity or ContentResolver not available", null) - return - } - - try { - val pendingIntent = MediaStore.createTrashRequest(contentResolver, contentUris, isTrashed) - pendingResult = result // Store for onActivityResult - activity.startIntentSenderForResult( - pendingIntent.intentSender, - trashRequestCode, - null, 0, 0, 0 - ) - } catch (e: Exception) { - Log.e("TrashError", "Error creating or starting trash request", e) - result.error("TrashError", "Error creating or starting trash request", null) - } - } - - @RequiresApi(Build.VERSION_CODES.R) - private fun getTrashedFileUri(fileName: String, type: Int): Uri? { - val contentResolver = context?.contentResolver ?: return null - val queryUri = MediaStore.Files.getContentUri(MediaStore.VOLUME_EXTERNAL) - val projection = arrayOf(MediaStore.Files.FileColumns._ID) - - val queryArgs = Bundle().apply { - putString( - ContentResolver.QUERY_ARG_SQL_SELECTION, - "${MediaStore.Files.FileColumns.DISPLAY_NAME} = ?" - ) - putStringArray(ContentResolver.QUERY_ARG_SQL_SELECTION_ARGS, arrayOf(fileName)) - putInt(MediaStore.QUERY_ARG_MATCH_TRASHED, MediaStore.MATCH_ONLY) - } - - contentResolver.query(queryUri, projection, queryArgs, null)?.use { cursor -> - if (cursor.moveToFirst()) { - val id = cursor.getLong(cursor.getColumnIndexOrThrow(MediaStore.Files.FileColumns._ID)) - return ContentUris.withAppendedId(contentUriForType(type), id) - } - } - return null - } - - // ActivityAware implementation - override fun onAttachedToActivity(binding: ActivityPluginBinding) { - activityBinding = binding - binding.addActivityResultListener(this) - } - - override fun onDetachedFromActivityForConfigChanges() { - activityBinding?.removeActivityResultListener(this) - activityBinding = null - } - - override fun onReattachedToActivityForConfigChanges(binding: ActivityPluginBinding) { - activityBinding = binding - binding.addActivityResultListener(this) - } - - override fun onDetachedFromActivity() { - activityBinding?.removeActivityResultListener(this) - activityBinding = null - } - - // ActivityResultListener implementation - override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?): Boolean { - if (requestCode == permissionRequestCode) { - val granted = hasManageMediaPermission() - pendingResult?.success(granted) - pendingResult = null - return true - } - - if (requestCode == trashRequestCode) { - val approved = resultCode == Activity.RESULT_OK - pendingResult?.success(approved) - pendingResult = null - return true - } - return false - } - - @RequiresApi(Build.VERSION_CODES.R) - private fun isInTrash(id: Long): Boolean { - val contentResolver = context?.contentResolver ?: return false - val filesUri = MediaStore.Files.getContentUri(MediaStore.VOLUME_EXTERNAL) - val args = Bundle().apply { - putString(ContentResolver.QUERY_ARG_SQL_SELECTION, "${MediaStore.Files.FileColumns._ID}=?") - putStringArray(ContentResolver.QUERY_ARG_SQL_SELECTION_ARGS, arrayOf(id.toString())) - putInt(MediaStore.QUERY_ARG_MATCH_TRASHED, MediaStore.MATCH_ONLY) - putInt(ContentResolver.QUERY_ARG_LIMIT, 1) - } - return contentResolver.query(filesUri, arrayOf(MediaStore.Files.FileColumns._ID), args, null) - ?.use { it.moveToFirst() } == true - } - - @RequiresApi(Build.VERSION_CODES.R) - private fun restoreUris(uris: List, result: Result) { - if (uris.isEmpty()) { - result.error("TrashError", "No URIs to restore", null) - return - } - Log.i(TAG, "restoreUris: count=${uris.size}, first=${uris.first()}") - toggleTrash(uris, false, result) - } - - @RequiresApi(Build.VERSION_CODES.Q) - private fun contentUriForType(type: Int): Uri = - when (type) { - // same order as AssetType from dart - 1 -> MediaStore.Images.Media.EXTERNAL_CONTENT_URI - 2 -> MediaStore.Video.Media.EXTERNAL_CONTENT_URI - 3 -> MediaStore.Audio.Media.EXTERNAL_CONTENT_URI - else -> MediaStore.Files.getContentUri(MediaStore.VOLUME_EXTERNAL) - } -} - -private const val TAG = "BackgroundServicePlugin" -private const val BUFFER_SIZE = 2 * 1024 * 1024 diff --git a/mobile/android/app/src/main/kotlin/app/alextran/immich/BackupWorker.kt b/mobile/android/app/src/main/kotlin/app/alextran/immich/BackupWorker.kt deleted file mode 100644 index 9c90528dc9..0000000000 --- a/mobile/android/app/src/main/kotlin/app/alextran/immich/BackupWorker.kt +++ /dev/null @@ -1,394 +0,0 @@ -package app.alextran.immich - -import android.app.Notification -import android.app.NotificationChannel -import android.app.NotificationManager -import android.content.Context -import android.content.pm.ServiceInfo.FOREGROUND_SERVICE_TYPE_SHORT_SERVICE -import android.os.Build -import android.os.Handler -import android.os.Looper -import android.os.PowerManager -import android.os.SystemClock -import android.util.Log -import androidx.annotation.RequiresApi -import androidx.core.app.NotificationCompat -import androidx.concurrent.futures.ResolvableFuture -import androidx.work.BackoffPolicy -import androidx.work.Constraints -import androidx.work.ForegroundInfo -import androidx.work.ListenableWorker -import androidx.work.NetworkType -import androidx.work.WorkerParameters -import androidx.work.ExistingWorkPolicy -import androidx.work.OneTimeWorkRequest -import androidx.work.WorkManager -import androidx.work.WorkInfo -import com.google.common.util.concurrent.ListenableFuture -import io.flutter.embedding.engine.FlutterEngine -import io.flutter.embedding.engine.dart.DartExecutor -import io.flutter.embedding.engine.loader.FlutterLoader -import io.flutter.plugin.common.MethodCall -import io.flutter.plugin.common.MethodChannel -import io.flutter.view.FlutterCallbackInformation -import java.util.concurrent.TimeUnit - -/** - * Worker executed by Android WorkManager to perform backup in background - * - * Starts the Dart runtime/engine and calls `_nativeEntry` function in - * `background.service.dart` to run the actual backup logic. - * Called by Android WorkManager when all constraints for the work are met, - * i.e. battery is not low and optionally Wifi and charging are active. - */ -class BackupWorker(ctx: Context, params: WorkerParameters) : ListenableWorker(ctx, params), - MethodChannel.MethodCallHandler { - - private val resolvableFuture = ResolvableFuture.create() - private var engine: FlutterEngine? = null - private lateinit var backgroundChannel: MethodChannel - private val notificationManager = - ctx.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager - private val isIgnoringBatteryOptimizations = isIgnoringBatteryOptimizations(applicationContext) - private var timeBackupStarted: Long = 0L - private var notificationBuilder: NotificationCompat.Builder? = null - private var notificationDetailBuilder: NotificationCompat.Builder? = null - private var fgFuture: ListenableFuture? = null - - override fun startWork(): ListenableFuture { - - Log.d(TAG, "startWork") - - val ctx = applicationContext - - if (!flutterLoader.initialized()) { - flutterLoader.startInitialization(ctx) - } - - // Create a Notification channel - createChannel() - - Log.d(TAG, "isIgnoringBatteryOptimizations $isIgnoringBatteryOptimizations") - if (isIgnoringBatteryOptimizations) { - // normal background services can only up to 10 minutes - // foreground services are allowed to run indefinitely - // requires battery optimizations to be disabled (either manually by the user - // or by the system learning that immich is important to the user) - val title = ctx.getSharedPreferences(SHARED_PREF_NAME, Context.MODE_PRIVATE) - .getString(SHARED_PREF_NOTIFICATION_TITLE, NOTIFICATION_DEFAULT_TITLE)!! - showInfo(getInfoBuilder(title, indeterminate = true).build()) - } - - engine = FlutterEngine(ctx) - - flutterLoader.ensureInitializationCompleteAsync(ctx, null, Handler(Looper.getMainLooper())) { - runDart() - - } - - return resolvableFuture - } - - /** - * Starts the Dart runtime/engine and calls `_nativeEntry` function in - * `background.service.dart` to run the actual backup logic. - */ - private fun runDart() { - val callbackDispatcherHandle = applicationContext.getSharedPreferences( - SHARED_PREF_NAME, Context.MODE_PRIVATE - ).getLong(SHARED_PREF_CALLBACK_KEY, 0L) - val callbackInformation = - FlutterCallbackInformation.lookupCallbackInformation(callbackDispatcherHandle) - val appBundlePath = flutterLoader.findAppBundlePath() - - engine?.let { engine -> - backgroundChannel = MethodChannel(engine.dartExecutor, "immich/backgroundChannel") - backgroundChannel.setMethodCallHandler(this@BackupWorker) - engine.dartExecutor.executeDartCallback( - DartExecutor.DartCallback( - applicationContext.assets, - appBundlePath, - callbackInformation - ) - ) - } - } - - override fun onStopped() { - Log.d(TAG, "onStopped") - // called when the system has to stop this worker because constraints are - // no longer met or the system needs resources for more important tasks - Handler(Looper.getMainLooper()).postAtFrontOfQueue { - if (::backgroundChannel.isInitialized) { - backgroundChannel.invokeMethod("systemStop", null) - } - } - waitOnSetForegroundAsync() - // cannot await/get(block) on resolvableFuture as its already cancelled (would throw CancellationException) - // instead, wait for 5 seconds until forcefully stopping backup work - Handler(Looper.getMainLooper()).postDelayed({ - stopEngine(null) - }, 5000) - } - - private fun waitOnSetForegroundAsync() { - val fgFuture = this.fgFuture - if (fgFuture != null && !fgFuture.isCancelled && !fgFuture.isDone) { - try { - fgFuture.get(500, TimeUnit.MILLISECONDS) - } catch (e: Exception) { - // ignored, there is nothing to be done - } - } - } - - private fun stopEngine(result: Result?) { - clearBackgroundNotification() - engine?.destroy() - engine = null - if (result != null) { - Log.d(TAG, "stopEngine result=${result}") - resolvableFuture.set(result) - } - waitOnSetForegroundAsync() - } - - @RequiresApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE) - override fun onMethodCall(call: MethodCall, r: MethodChannel.Result) { - when (call.method) { - "initialized" -> { - timeBackupStarted = SystemClock.uptimeMillis() - backgroundChannel.invokeMethod( - "onAssetsChanged", - null, - object : MethodChannel.Result { - override fun notImplemented() { - stopEngine(Result.failure()) - } - - override fun error(errorCode: String, errorMessage: String?, errorDetails: Any?) { - stopEngine(Result.failure()) - } - - override fun success(receivedResult: Any?) { - val success = receivedResult as Boolean - stopEngine(if (success) Result.success() else Result.retry()) - } - } - ) - } - - "updateNotification" -> { - val args = call.arguments>()!! - val title = args[0] as String? - val content = args[1] as String? - val progress = args[2] as Int - val max = args[3] as Int - val indeterminate = args[4] as Boolean - val isDetail = args[5] as Boolean - val onlyIfFG = args[6] as Boolean - if (!onlyIfFG || isIgnoringBatteryOptimizations) { - showInfo( - getInfoBuilder(title, content, isDetail, progress, max, indeterminate).build(), - isDetail - ) - } - } - - "showError" -> { - val args = call.arguments>()!! - val title = args[0] as String - val content = args[1] as String? - val individualTag = args[2] as String? - showError(title, content, individualTag) - } - - "clearErrorNotifications" -> clearErrorNotifications() - "hasContentChanged" -> { - val lastChange = applicationContext - .getSharedPreferences(SHARED_PREF_NAME, Context.MODE_PRIVATE) - .getLong(SHARED_PREF_LAST_CHANGE, timeBackupStarted) - val hasContentChanged = lastChange > timeBackupStarted; - timeBackupStarted = SystemClock.uptimeMillis() - r.success(hasContentChanged) - } - - else -> r.notImplemented() - } - } - - private fun showError(title: String, content: String?, individualTag: String?) { - val notification = NotificationCompat.Builder(applicationContext, NOTIFICATION_CHANNEL_ERROR_ID) - .setContentTitle(title) - .setTicker(title) - .setContentText(content) - .setSmallIcon(R.drawable.notification_icon) - .build() - notificationManager.notify(individualTag, NOTIFICATION_ERROR_ID, notification) - } - - private fun clearErrorNotifications() { - notificationManager.cancel(NOTIFICATION_ERROR_ID) - } - - private fun clearBackgroundNotification() { - notificationManager.cancel(NOTIFICATION_ID) - notificationManager.cancel(NOTIFICATION_DETAIL_ID) - } - - private fun showInfo(notification: Notification, isDetail: Boolean = false) { - val id = if (isDetail) NOTIFICATION_DETAIL_ID else NOTIFICATION_ID - - if (isIgnoringBatteryOptimizations && !isDetail) { - fgFuture = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) { - setForegroundAsync(ForegroundInfo(id, notification, FOREGROUND_SERVICE_TYPE_SHORT_SERVICE)) - } else { - setForegroundAsync(ForegroundInfo(id, notification)) - } - } else { - notificationManager.notify(id, notification) - } - } - - private fun getInfoBuilder( - title: String? = null, - content: String? = null, - isDetail: Boolean = false, - progress: Int = 0, - max: Int = 0, - indeterminate: Boolean = false, - ): NotificationCompat.Builder { - var builder = if (isDetail) notificationDetailBuilder else notificationBuilder - if (builder == null) { - builder = NotificationCompat.Builder(applicationContext, NOTIFICATION_CHANNEL_ID) - .setSmallIcon(R.drawable.notification_icon) - .setOnlyAlertOnce(true) - .setOngoing(true) - if (isDetail) { - notificationDetailBuilder = builder - } else { - notificationBuilder = builder - } - } - if (title != null) { - builder.setTicker(title).setContentTitle(title) - } - if (content != null) { - builder.setContentText(content) - } - return builder.setProgress(max, progress, indeterminate) - } - - private fun createChannel() { - val foreground = NotificationChannel( - NOTIFICATION_CHANNEL_ID, - NOTIFICATION_CHANNEL_ID, - NotificationManager.IMPORTANCE_LOW - ) - notificationManager.createNotificationChannel(foreground) - val error = NotificationChannel( - NOTIFICATION_CHANNEL_ERROR_ID, - NOTIFICATION_CHANNEL_ERROR_ID, - NotificationManager.IMPORTANCE_HIGH - ) - notificationManager.createNotificationChannel(error) - } - - companion object { - const val SHARED_PREF_NAME = "immichBackgroundService" - const val SHARED_PREF_CALLBACK_KEY = "callbackDispatcherHandle" - const val SHARED_PREF_NOTIFICATION_TITLE = "notificationTitle" - const val SHARED_PREF_LAST_CHANGE = "lastChange" - - private const val TASK_NAME_BACKUP = "immich/BackupWorker" - private const val NOTIFICATION_CHANNEL_ID = "immich/backgroundService" - private const val NOTIFICATION_CHANNEL_ERROR_ID = "immich/backgroundServiceError" - private const val NOTIFICATION_DEFAULT_TITLE = "Immich" - private const val NOTIFICATION_ID = 1 - private const val NOTIFICATION_ERROR_ID = 2 - private const val NOTIFICATION_DETAIL_ID = 3 - private const val ONE_MINUTE = 60000L - - /** - * Enqueues the BackupWorker to run once the constraints are met - */ - fun enqueueBackupWorker( - context: Context, - requireWifi: Boolean = false, - requireCharging: Boolean = false, - delayMilliseconds: Long = 0L - ) { - val workRequest = buildWorkRequest(requireWifi, requireCharging, delayMilliseconds) - WorkManager.getInstance(context) - .enqueueUniqueWork(TASK_NAME_BACKUP, ExistingWorkPolicy.KEEP, workRequest) - Log.d(TAG, "enqueueBackupWorker: BackupWorker enqueued") - } - - /** - * Updates the constraints of an already enqueued BackupWorker - */ - fun updateBackupWorker( - context: Context, - requireWifi: Boolean = false, - requireCharging: Boolean = false - ) { - try { - val wm = WorkManager.getInstance(context) - val workInfoFuture = wm.getWorkInfosForUniqueWork(TASK_NAME_BACKUP) - val workInfoList = workInfoFuture.get(1000, TimeUnit.MILLISECONDS) - if (workInfoList != null) { - for (workInfo in workInfoList) { - if (workInfo.state == WorkInfo.State.ENQUEUED) { - val workRequest = buildWorkRequest(requireWifi, requireCharging) - wm.enqueueUniqueWork(TASK_NAME_BACKUP, ExistingWorkPolicy.REPLACE, workRequest) - Log.d(TAG, "updateBackupWorker updated BackupWorker constraints") - return - } - } - } - Log.d(TAG, "updateBackupWorker: BackupWorker not enqueued") - } catch (e: Exception) { - Log.d(TAG, "updateBackupWorker failed: $e") - } - } - - /** - * Stops the currently running worker (if any) and removes it from the work queue - */ - fun stopWork(context: Context) { - WorkManager.getInstance(context).cancelUniqueWork(TASK_NAME_BACKUP) - Log.d(TAG, "stopWork: BackupWorker cancelled") - } - - /** - * Returns `true` if the app is ignoring battery optimizations - */ - fun isIgnoringBatteryOptimizations(ctx: Context): Boolean { - val powerManager = ctx.getSystemService(Context.POWER_SERVICE) as PowerManager - return powerManager.isIgnoringBatteryOptimizations(ctx.packageName) - } - - private fun buildWorkRequest( - requireWifi: Boolean = false, - requireCharging: Boolean = false, - delayMilliseconds: Long = 0L - ): OneTimeWorkRequest { - val constraints = Constraints.Builder() - .setRequiredNetworkType(if (requireWifi) NetworkType.UNMETERED else NetworkType.CONNECTED) - .setRequiresBatteryNotLow(true) - .setRequiresCharging(requireCharging) - .build(); - - val work = OneTimeWorkRequest.Builder(BackupWorker::class.java) - .setConstraints(constraints) - .setBackoffCriteria(BackoffPolicy.EXPONENTIAL, ONE_MINUTE, TimeUnit.MILLISECONDS) - .setInitialDelay(delayMilliseconds, TimeUnit.MILLISECONDS) - .build() - return work - } - - private val flutterLoader = FlutterLoader() - } -} - -private const val TAG = "BackupWorker" diff --git a/mobile/android/app/src/main/kotlin/app/alextran/immich/ContentObserverWorker.kt b/mobile/android/app/src/main/kotlin/app/alextran/immich/ContentObserverWorker.kt deleted file mode 100644 index 9cb2ec7779..0000000000 --- a/mobile/android/app/src/main/kotlin/app/alextran/immich/ContentObserverWorker.kt +++ /dev/null @@ -1,144 +0,0 @@ -package app.alextran.immich - -import android.content.Context -import android.os.SystemClock -import android.provider.MediaStore -import android.util.Log -import androidx.work.Constraints -import androidx.work.Worker -import androidx.work.WorkerParameters -import androidx.work.ExistingWorkPolicy -import androidx.work.OneTimeWorkRequest -import androidx.work.WorkManager -import androidx.work.Operation -import java.util.concurrent.TimeUnit - -/** - * Worker executed by Android WorkManager observing content changes (new photos/videos) - * - * Immediately enqueues the BackupWorker when running. - * As this work is not triggered periodically, but on content change, the - * worker enqueues itself again after each run. - */ -class ContentObserverWorker(ctx: Context, params: WorkerParameters) : Worker(ctx, params) { - - override fun doWork(): Result { - if (!isEnabled(applicationContext)) { - return Result.failure() - } - if (triggeredContentUris.size > 0) { - startBackupWorker(applicationContext, delayMilliseconds = 0) - } - enqueueObserverWorker(applicationContext, ExistingWorkPolicy.REPLACE) - return Result.success() - } - - companion object { - const val SHARED_PREF_SERVICE_ENABLED = "serviceEnabled" - private const val SHARED_PREF_REQUIRE_WIFI = "requireWifi" - private const val SHARED_PREF_REQUIRE_CHARGING = "requireCharging" - private const val SHARED_PREF_TRIGGER_UPDATE_DELAY = "triggerUpdateDelay" - private const val SHARED_PREF_TRIGGER_MAX_DELAY = "triggerMaxDelay" - - private const val TASK_NAME_OBSERVER = "immich/ContentObserver" - - /** - * Enqueues the `ContentObserverWorker`. - * - * @param context Android Context - */ - fun enable(context: Context, immediate: Boolean = false) { - enqueueObserverWorker(context, ExistingWorkPolicy.KEEP) - Log.d(TAG, "enabled ContentObserverWorker") - if (immediate) { - startBackupWorker(context, delayMilliseconds = 5000) - } - } - - /** - * Configures the `BackupWorker` to run when all constraints are met. - * - * @param context Android Context - * @param requireWifi if true, task only runs if connected to wifi - * @param requireCharging if true, task only runs if device is charging - */ - fun configureWork(context: Context, - requireWifi: Boolean = false, - requireCharging: Boolean = false, - triggerUpdateDelay: Long = 5000, - triggerMaxDelay: Long = 50000) { - context.getSharedPreferences(BackupWorker.SHARED_PREF_NAME, Context.MODE_PRIVATE) - .edit() - .putBoolean(SHARED_PREF_SERVICE_ENABLED, true) - .putBoolean(SHARED_PREF_REQUIRE_WIFI, requireWifi) - .putBoolean(SHARED_PREF_REQUIRE_CHARGING, requireCharging) - .putLong(SHARED_PREF_TRIGGER_UPDATE_DELAY, triggerUpdateDelay) - .putLong(SHARED_PREF_TRIGGER_MAX_DELAY, triggerMaxDelay) - .apply() - BackupWorker.updateBackupWorker(context, requireWifi, requireCharging) - } - - /** - * Stops the currently running worker (if any) and removes it from the work queue - */ - fun disable(context: Context) { - context.getSharedPreferences(BackupWorker.SHARED_PREF_NAME, Context.MODE_PRIVATE) - .edit().putBoolean(SHARED_PREF_SERVICE_ENABLED, false).apply() - WorkManager.getInstance(context).cancelUniqueWork(TASK_NAME_OBSERVER) - Log.d(TAG, "disabled ContentObserverWorker") - } - - /** - * Return true if the user has enabled the background backup service - */ - fun isEnabled(ctx: Context): Boolean { - return ctx.getSharedPreferences(BackupWorker.SHARED_PREF_NAME, Context.MODE_PRIVATE) - .getBoolean(SHARED_PREF_SERVICE_ENABLED, false) - } - - /** - * Enqueue and replace the worker without the content trigger but with a short delay - */ - fun workManagerAppClearedWorkaround(context: Context) { - val work = OneTimeWorkRequest.Builder(ContentObserverWorker::class.java) - .setInitialDelay(500, TimeUnit.MILLISECONDS) - .build() - WorkManager - .getInstance(context) - .enqueueUniqueWork(TASK_NAME_OBSERVER, ExistingWorkPolicy.REPLACE, work) - .result - .get() - Log.d(TAG, "workManagerAppClearedWorkaround") - } - - private fun enqueueObserverWorker(context: Context, policy: ExistingWorkPolicy) { - val sp = context.getSharedPreferences(BackupWorker.SHARED_PREF_NAME, Context.MODE_PRIVATE) - val constraints = Constraints.Builder() - .addContentUriTrigger(MediaStore.Images.Media.INTERNAL_CONTENT_URI, true) - .addContentUriTrigger(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, true) - .addContentUriTrigger(MediaStore.Video.Media.INTERNAL_CONTENT_URI, true) - .addContentUriTrigger(MediaStore.Video.Media.EXTERNAL_CONTENT_URI, true) - .setTriggerContentUpdateDelay(sp.getLong(SHARED_PREF_TRIGGER_UPDATE_DELAY, 5000), TimeUnit.MILLISECONDS) - .setTriggerContentMaxDelay(sp.getLong(SHARED_PREF_TRIGGER_MAX_DELAY, 50000), TimeUnit.MILLISECONDS) - .build() - - val work = OneTimeWorkRequest.Builder(ContentObserverWorker::class.java) - .setConstraints(constraints) - .build() - WorkManager.getInstance(context).enqueueUniqueWork(TASK_NAME_OBSERVER, policy, work) - } - - fun startBackupWorker(context: Context, delayMilliseconds: Long) { - val sp = context.getSharedPreferences(BackupWorker.SHARED_PREF_NAME, Context.MODE_PRIVATE) - if (!sp.getBoolean(SHARED_PREF_SERVICE_ENABLED, false)) - return - val requireWifi = sp.getBoolean(SHARED_PREF_REQUIRE_WIFI, true) - val requireCharging = sp.getBoolean(SHARED_PREF_REQUIRE_CHARGING, false) - BackupWorker.enqueueBackupWorker(context, requireWifi, requireCharging, delayMilliseconds) - sp.edit().putLong(BackupWorker.SHARED_PREF_LAST_CHANGE, SystemClock.uptimeMillis()).apply() - } - - } -} - -private const val TAG = "ContentObserverWorker" diff --git a/mobile/android/app/src/main/kotlin/app/alextran/immich/ImmichApp.kt b/mobile/android/app/src/main/kotlin/app/alextran/immich/ImmichApp.kt index 4474c63e09..37a325e896 100644 --- a/mobile/android/app/src/main/kotlin/app/alextran/immich/ImmichApp.kt +++ b/mobile/android/app/src/main/kotlin/app/alextran/immich/ImmichApp.kt @@ -18,8 +18,6 @@ class ImmichApp : Application() { // Thus, the BackupWorker is not started. If the system kills the process after each initialization // (because of low memory etc.), the backup is never performed. // As a workaround, we also run a backup check when initializing the application - - ContentObserverWorker.startBackupWorker(context = this, delayMilliseconds = 0) Handler(Looper.getMainLooper()).postDelayed({ // We can only check the engine count and not the status of the lock here, // as the previous start might have been killed without unlocking. diff --git a/mobile/android/app/src/main/kotlin/app/alextran/immich/MainActivity.kt b/mobile/android/app/src/main/kotlin/app/alextran/immich/MainActivity.kt index 06649de8f0..2c80b8d2bd 100644 --- a/mobile/android/app/src/main/kotlin/app/alextran/immich/MainActivity.kt +++ b/mobile/android/app/src/main/kotlin/app/alextran/immich/MainActivity.kt @@ -51,7 +51,6 @@ class MainActivity : FlutterFragmentActivity() { BackgroundWorkerFgHostApi.setUp(messenger, BackgroundWorkerApiImpl(ctx)) ConnectivityApi.setUp(messenger, ConnectivityApiImpl(ctx)) - flutterEngine.plugins.add(BackgroundServicePlugin()) flutterEngine.plugins.add(backgroundEngineLockImpl) flutterEngine.plugins.add(nativeSyncApiImpl) } diff --git a/mobile/integration_test/test_utils/general_helper.dart b/mobile/integration_test/test_utils/general_helper.dart index d6065170ef..66955364f3 100644 --- a/mobile/integration_test/test_utils/general_helper.dart +++ b/mobile/integration_test/test_utils/general_helper.dart @@ -5,7 +5,6 @@ import 'package:flutter_test/flutter_test.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/entities/store.entity.dart'; import 'package:immich_mobile/main.dart' as app; -import 'package:immich_mobile/providers/db.provider.dart'; import 'package:immich_mobile/providers/infrastructure/db.provider.dart'; import 'package:immich_mobile/utils/bootstrap.dart'; import 'package:integration_test/integration_test.dart'; @@ -39,20 +38,11 @@ class ImmichTestHelper { static Future loadApp(WidgetTester tester) async { await EasyLocalization.ensureInitialized(); // Clear all data from Isar (reuse existing instance if available) - final (isar, drift, logDb) = await Bootstrap.initDB(); - await Bootstrap.initDomain(isar, drift, logDb); + final (drift, _) = await Bootstrap.initDomain(); await Store.clear(); - await isar.writeTxn(() => isar.clear()); // Load main Widget await tester.pumpWidget( - ProviderScope( - overrides: [ - dbProvider.overrideWithValue(isar), - isarProvider.overrideWithValue(isar), - driftProvider.overrideWith(driftOverride(drift)), - ], - child: const app.MainWidget(), - ), + ProviderScope(overrides: [driftProvider.overrideWith(driftOverride(drift))], child: const app.MainWidget()), ); // Post run tasks await EasyLocalization.ensureInitialized(); diff --git a/mobile/ios/Podfile.lock b/mobile/ios/Podfile.lock index e1ec4aff07..c0d7e2c35a 100644 --- a/mobile/ios/Podfile.lock +++ b/mobile/ios/Podfile.lock @@ -33,8 +33,6 @@ PODS: - Flutter - integration_test (0.0.1): - Flutter - - isar_community_flutter_libs (1.0.0): - - Flutter - local_auth_darwin (0.0.1): - Flutter - FlutterMacOS @@ -75,16 +73,16 @@ PODS: - sqflite_darwin (0.0.4): - Flutter - FlutterMacOS - - sqlite3 (3.49.1): - - sqlite3/common (= 3.49.1) - - sqlite3/common (3.49.1) - - sqlite3/dbstatvtab (3.49.1): + - sqlite3 (3.49.2): + - sqlite3/common (= 3.49.2) + - sqlite3/common (3.49.2) + - sqlite3/dbstatvtab (3.49.2): - sqlite3/common - - sqlite3/fts5 (3.49.1): + - sqlite3/fts5 (3.49.2): - sqlite3/common - - sqlite3/perf-threadsafe (3.49.1): + - sqlite3/perf-threadsafe (3.49.2): - sqlite3/common - - sqlite3/rtree (3.49.1): + - sqlite3/rtree (3.49.2): - sqlite3/common - sqlite3_flutter_libs (0.0.1): - Flutter @@ -116,7 +114,6 @@ DEPENDENCIES: - home_widget (from `.symlinks/plugins/home_widget/ios`) - image_picker_ios (from `.symlinks/plugins/image_picker_ios/ios`) - integration_test (from `.symlinks/plugins/integration_test/ios`) - - isar_community_flutter_libs (from `.symlinks/plugins/isar_community_flutter_libs/ios`) - local_auth_darwin (from `.symlinks/plugins/local_auth_darwin/darwin`) - maplibre_gl (from `.symlinks/plugins/maplibre_gl/ios`) - native_video_player (from `.symlinks/plugins/native_video_player/ios`) @@ -174,8 +171,6 @@ EXTERNAL SOURCES: :path: ".symlinks/plugins/image_picker_ios/ios" integration_test: :path: ".symlinks/plugins/integration_test/ios" - isar_community_flutter_libs: - :path: ".symlinks/plugins/isar_community_flutter_libs/ios" local_auth_darwin: :path: ".symlinks/plugins/local_auth_darwin/darwin" maplibre_gl: @@ -228,7 +223,6 @@ SPEC CHECKSUMS: home_widget: f169fc41fd807b4d46ab6615dc44d62adbf9f64f image_picker_ios: e0ece4aa2a75771a7de3fa735d26d90817041326 integration_test: 4a889634ef21a45d28d50d622cf412dc6d9f586e - isar_community_flutter_libs: bede843185a61a05ff364a05c9b23209523f7e0d local_auth_darwin: 553ce4f9b16d3fdfeafce9cf042e7c9f77c1c391 MapLibre: 69e572367f4ef6287e18246cfafc39c80cdcabcd maplibre_gl: 3c924e44725147b03dda33430ad216005b40555f @@ -245,7 +239,7 @@ SPEC CHECKSUMS: share_plus: 50da8cb520a8f0f65671c6c6a99b3617ed10a58a shared_preferences_foundation: 9e1978ff2562383bd5676f64ec4e9aa8fa06a6f7 sqflite_darwin: 20b2a3a3b70e43edae938624ce550a3cbf66a3d0 - sqlite3: fc1400008a9b3525f5914ed715a5d1af0b8f4983 + sqlite3: 3c950dc86011117c307eb0b28c4a7bb449dce9f1 sqlite3_flutter_libs: f8fc13346870e73fe35ebf6dbb997fbcd156b241 url_launcher_ios: 694010445543906933d732453a59da0a173ae33d wakelock_plus: e29112ab3ef0b318e58cfa5c32326458be66b556 diff --git a/mobile/ios/Runner.xcodeproj/project.pbxproj b/mobile/ios/Runner.xcodeproj/project.pbxproj index 178454f381..f88d624b89 100644 --- a/mobile/ios/Runner.xcodeproj/project.pbxproj +++ b/mobile/ios/Runner.xcodeproj/project.pbxproj @@ -10,8 +10,6 @@ 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; }; 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; }; 3B6A31FED0FC846D6BD69BBC /* Pods_ShareExtension.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 357FC57E54FD0F51795CF28A /* Pods_ShareExtension.framework */; }; - 65F32F31299BD2F800CE9261 /* BackgroundServicePlugin.swift in Sources */ = {isa = PBXBuildFile; fileRef = 65F32F30299BD2F800CE9261 /* BackgroundServicePlugin.swift */; }; - 65F32F33299D349D00CE9261 /* BackgroundSyncWorker.swift in Sources */ = {isa = PBXBuildFile; fileRef = 65F32F32299D349D00CE9261 /* BackgroundSyncWorker.swift */; }; 74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74858FAE1ED2DC5600515810 /* AppDelegate.swift */; }; 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; }; 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; }; @@ -90,8 +88,6 @@ 357FC57E54FD0F51795CF28A /* Pods_ShareExtension.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_ShareExtension.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = ""; }; 571EAA93D77181C7C98C2EA6 /* Pods-ShareExtension.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-ShareExtension.release.xcconfig"; path = "Target Support Files/Pods-ShareExtension/Pods-ShareExtension.release.xcconfig"; sourceTree = ""; }; - 65F32F30299BD2F800CE9261 /* BackgroundServicePlugin.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BackgroundServicePlugin.swift; sourceTree = ""; }; - 65F32F32299D349D00CE9261 /* BackgroundSyncWorker.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BackgroundSyncWorker.swift; sourceTree = ""; }; 74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Runner-Bridging-Header.h"; sourceTree = ""; }; 74858FAE1ED2DC5600515810 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = ""; }; @@ -151,11 +147,15 @@ /* Begin PBXFileSystemSynchronizedRootGroup section */ B231F52D2E93A44A00BC45D1 /* Core */ = { isa = PBXFileSystemSynchronizedRootGroup; + exceptions = ( + ); path = Core; sourceTree = ""; }; B2CF7F8C2DDE4EBB00744BF6 /* Sync */ = { isa = PBXFileSystemSynchronizedRootGroup; + exceptions = ( + ); path = Sync; sourceTree = ""; }; @@ -177,6 +177,8 @@ }; FEE084F22EC172080045228E /* Schemas */ = { isa = PBXFileSystemSynchronizedRootGroup; + exceptions = ( + ); path = Schemas; sourceTree = ""; }; @@ -238,15 +240,6 @@ name = Frameworks; sourceTree = ""; }; - 65DD438629917FAD0047FFA8 /* BackgroundSync */ = { - isa = PBXGroup; - children = ( - 65F32F32299D349D00CE9261 /* BackgroundSyncWorker.swift */, - 65F32F30299BD2F800CE9261 /* BackgroundServicePlugin.swift */, - ); - path = BackgroundSync; - sourceTree = ""; - }; 9740EEB11CF90186004384FC /* Flutter */ = { isa = PBXGroup; children = ( @@ -291,7 +284,6 @@ B21E34A62E5AF9760031FDB9 /* Background */, B2CF7F8C2DDE4EBB00744BF6 /* Sync */, FA9973382CF6DF4B000EF859 /* Runner.entitlements */, - 65DD438629917FAD0047FFA8 /* BackgroundSync */, FAC7416727DB9F5500C668D8 /* RunnerProfile.entitlements */, 97C146FA1CF9000F007C117D /* Main.storyboard */, 97C146FD1CF9000F007C117D /* Assets.xcassets */, @@ -571,14 +563,10 @@ inputFileListPaths = ( "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources-${CONFIGURATION}-input-files.xcfilelist", ); - inputPaths = ( - ); name = "[CP] Copy Pods Resources"; outputFileListPaths = ( "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources-${CONFIGURATION}-output-files.xcfilelist", ); - outputPaths = ( - ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources.sh\"\n"; @@ -607,14 +595,10 @@ inputFileListPaths = ( "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-input-files.xcfilelist", ); - inputPaths = ( - ); name = "[CP] Embed Pods Frameworks"; outputFileListPaths = ( "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-output-files.xcfilelist", ); - outputPaths = ( - ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n"; @@ -627,7 +611,6 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( - 65F32F31299BD2F800CE9261 /* BackgroundServicePlugin.swift in Sources */, 74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */, A01DD69B2F7F43B40049AB63 /* ImageRequest.swift in Sources */, B21E34AC2E5B09190031FDB9 /* BackgroundWorker.swift in Sources */, @@ -642,7 +625,6 @@ B21E34AA2E5AFD2B0031FDB9 /* BackgroundWorkerApiImpl.swift in Sources */, 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */, B2BE315F2E5E5229006EEF88 /* BackgroundWorker.g.swift in Sources */, - 65F32F33299D349D00CE9261 /* BackgroundSyncWorker.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -1261,7 +1243,7 @@ repositoryURL = "https://github.com/pointfreeco/sqlite-data"; requirement = { kind = upToNextMajorVersion; - minimumVersion = 1.3.0; + minimumVersion = 1.6.1; }; }; FEE084F92EC1725A0045228E /* XCRemoteSwiftPackageReference "swift-http-structured-headers" */ = { @@ -1269,7 +1251,7 @@ repositoryURL = "https://github.com/apple/swift-http-structured-headers.git"; requirement = { kind = upToNextMajorVersion; - minimumVersion = 1.5.0; + minimumVersion = 1.6.0; }; }; /* End XCRemoteSwiftPackageReference section */ diff --git a/mobile/ios/Runner.xcworkspace/xcshareddata/swiftpm/Package.resolved b/mobile/ios/Runner.xcworkspace/xcshareddata/swiftpm/Package.resolved index 4962230c22..800ff8ac52 100644 --- a/mobile/ios/Runner.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/mobile/ios/Runner.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -24,8 +24,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/pointfreeco/sqlite-data", "state" : { - "revision" : "05704b563ecb7f0bd7e49b6f360a6383a3e53e7d", - "version" : "1.5.1" + "revision" : "da3a94ed49c7a30d82853de551c07a93196e8cab", + "version" : "1.6.1" } }, { @@ -78,8 +78,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/apple/swift-http-structured-headers.git", "state" : { - "revision" : "a9f3c352f4d46afd155e00b3c6e85decae6bcbeb", - "version" : "1.5.0" + "revision" : "76d7627bd88b47bf5a0f8497dd244885960dde0b", + "version" : "1.6.0" } }, { @@ -123,8 +123,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/pointfreeco/swift-structured-queries", "state" : { - "revision" : "d8163b3a98f3c8434c4361e85126db449d84bc66", - "version" : "0.30.0" + "revision" : "8da8818fccd9959bd683934ddc62cf45bb65b3c8", + "version" : "0.31.1" } }, { diff --git a/mobile/ios/Runner/AppDelegate.swift b/mobile/ios/Runner/AppDelegate.swift index 81af41ab08..2d41fd541e 100644 --- a/mobile/ios/Runner/AppDelegate.swift +++ b/mobile/ios/Runner/AppDelegate.swift @@ -24,33 +24,8 @@ import UIKit GeneratedPluginRegistrant.register(with: self) let controller: FlutterViewController = window?.rootViewController as! FlutterViewController AppDelegate.registerPlugins(with: controller.engine, controller: controller) - BackgroundServicePlugin.register(with: self.registrar(forPlugin: "BackgroundServicePlugin")!) - - BackgroundServicePlugin.registerBackgroundProcessing() BackgroundWorkerApiImpl.registerBackgroundWorkers() - BackgroundServicePlugin.setPluginRegistrantCallback { registry in - if !registry.hasPlugin("org.cocoapods.path-provider-foundation") { - PathProviderPlugin.register(with: registry.registrar(forPlugin: "org.cocoapods.path-provider-foundation")!) - } - - if !registry.hasPlugin("org.cocoapods.photo-manager") { - PhotoManagerPlugin.register(with: registry.registrar(forPlugin: "org.cocoapods.photo-manager")!) - } - - if !registry.hasPlugin("org.cocoapods.shared-preferences-foundation") { - SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "org.cocoapods.shared-preferences-foundation")!) - } - - if !registry.hasPlugin("org.cocoapods.permission-handler-apple") { - PermissionHandlerPlugin.register(with: registry.registrar(forPlugin: "org.cocoapods.permission-handler-apple")!) - } - - if !registry.hasPlugin("org.cocoapods.network-info-plus") { - FPPNetworkInfoPlusPlugin.register(with: registry.registrar(forPlugin: "org.cocoapods.network-info-plus")!) - } - } - return super.application(application, didFinishLaunchingWithOptions: launchOptions) } diff --git a/mobile/ios/Runner/BackgroundSync/BackgroundServicePlugin.swift b/mobile/ios/Runner/BackgroundSync/BackgroundServicePlugin.swift deleted file mode 100644 index cac9faab01..0000000000 --- a/mobile/ios/Runner/BackgroundSync/BackgroundServicePlugin.swift +++ /dev/null @@ -1,408 +0,0 @@ -// -// BackgroundServicePlugin.swift -// Runner -// -// Created by Marty Fuhry on 2/14/23. -// - -import Flutter -import BackgroundTasks -import path_provider_foundation -import CryptoKit -import Network - -class BackgroundServicePlugin: NSObject, FlutterPlugin { - - public static var flutterPluginRegistrantCallback: FlutterPluginRegistrantCallback? - - public static func setPluginRegistrantCallback(_ callback: FlutterPluginRegistrantCallback) { - flutterPluginRegistrantCallback = callback - } - - // Pause the application in XCode, then enter - // e -l objc -- (void)[[BGTaskScheduler sharedScheduler] _simulateLaunchForTaskWithIdentifier:@"app.alextran.immich.backgroundFetch"] - // or - // e -l objc -- (void)[[BGTaskScheduler sharedScheduler] _simulateLaunchForTaskWithIdentifier:@"app.alextran.immich.backgroundProcessing"] - // Then resume the application see the background code run - // Tested on a physical device, not a simulator - // This will submit either the Fetch or Processing command to the BGTaskScheduler for immediate processing. - // In my tests, I can only get app.alextran.immich.backgroundProcessing simulated by running the above command - - // This is the task ID in Info.plist to register as our background task ID - public static let backgroundFetchTaskID = "app.alextran.immich.backgroundFetch" - public static let backgroundProcessingTaskID = "app.alextran.immich.backgroundProcessing" - - // Establish communication with the main isolate and set up the channel call - // to this BackgroundServicePlugion() - public static func register(with registrar: FlutterPluginRegistrar) { - let channel = FlutterMethodChannel( - name: "immich/foregroundChannel", - binaryMessenger: registrar.messenger() - ) - - let instance = BackgroundServicePlugin() - registrar.addMethodCallDelegate(instance, channel: channel) - registrar.addApplicationDelegate(instance) - } - - // Registers the Flutter engine with the plugins, used by the other Background Flutter engine - public static func register(engine: FlutterEngine) { - GeneratedPluginRegistrant.register(with: engine) - } - - // Registers the task IDs from the system so that we can process them here in this class - public static func registerBackgroundProcessing() { - - let processingRegisterd = BGTaskScheduler.shared.register( - forTaskWithIdentifier: backgroundProcessingTaskID, - using: nil) { task in - if task is BGProcessingTask { - handleBackgroundProcessing(task: task as! BGProcessingTask) - } - } - - let fetchRegisterd = BGTaskScheduler.shared.register( - forTaskWithIdentifier: backgroundFetchTaskID, - using: nil) { task in - if task is BGAppRefreshTask { - handleBackgroundFetch(task: task as! BGAppRefreshTask) - } - } - } - - // Handles the channel methods from Flutter - public func handle(_ call: FlutterMethodCall, result: @escaping FlutterResult) { - switch call.method { - case "enable": - handleBackgroundEnable(call: call, result: result) - break - case "configure": - handleConfigure(call: call, result: result) - break - case "disable": - handleDisable(call: call, result: result) - break - case "isEnabled": - handleIsEnabled(call: call, result: result) - break - case "isIgnoringBatteryOptimizations": - result(FlutterMethodNotImplemented) - break - case "lastBackgroundFetchTime": - let defaults = UserDefaults.standard - let lastRunTime = defaults.value(forKey: "last_background_fetch_run_time") - result(lastRunTime) - break - case "lastBackgroundProcessingTime": - let defaults = UserDefaults.standard - let lastRunTime = defaults.value(forKey: "last_background_processing_run_time") - result(lastRunTime) - break - case "numberOfBackgroundProcesses": - handleNumberOfProcesses(call: call, result: result) - break - case "backgroundAppRefreshEnabled": - handleBackgroundRefreshStatus(call: call, result: result) - break - case "digestFiles": - handleDigestFiles(call: call, result: result) - break - default: - result(FlutterMethodNotImplemented) - break - } - } - - // Calculates the SHA-1 hash of each file from the list of paths provided - func handleDigestFiles(call: FlutterMethodCall, result: @escaping FlutterResult) { - - let bufsize = 2 * 1024 * 1024 - // Private error to throw if file cannot be read - enum DigestError: String, LocalizedError { - case NoFileHandle = "Cannot Open File Handle" - - public var errorDescription: String? { self.rawValue } - } - - // Parse the arguments or else fail - guard let args = call.arguments as? Array else { - print("Cannot parse args as array: \(String(describing: call.arguments))") - result(FlutterError(code: "Malformed", - message: "Received args is not an Array", - details: nil)) - return - } - - // Compute hash in background thread - DispatchQueue.global(qos: .background).async { - var hashes: [FlutterStandardTypedData?] = Array(repeating: nil, count: args.count) - for i in (0 ..< args.count) { - do { - guard let file = FileHandle(forReadingAtPath: args[i]) else { throw DigestError.NoFileHandle } - var hasher = Insecure.SHA1.init(); - while autoreleasepool(invoking: { - let chunk = file.readData(ofLength: bufsize) - guard !chunk.isEmpty else { return false } // EOF - hasher.update(data: chunk) - return true // continue - }) { } - let digest = hasher.finalize() - hashes[i] = FlutterStandardTypedData(bytes: Data(Array(digest.makeIterator()))) - } catch { - print("Cannot calculate the digest of the file \(args[i]) due to \(error.localizedDescription)") - } - } - - // Return result in main thread - DispatchQueue.main.async { - result(Array(hashes)) - } - } - } - - // Called by the flutter code when enabled so that we can turn on the background services - // and save the callback information to communicate on this method channel - public func handleBackgroundEnable(call: FlutterMethodCall, result: FlutterResult) { - - // Needs to parse the arguments from the method call - guard let args = call.arguments as? Array else { - print("Cannot parse args as array: \(call.arguments)") - result(FlutterMethodNotImplemented) - return - } - - // Requires 3 arguments in the array - guard args.count == 3 else { - print("Requires 3 arguments and received \(args.count)") - result(FlutterMethodNotImplemented) - return - } - - // Parses the arguments - let callbackHandle = args[0] as? Int64 - let notificationTitle = args[1] as? String - let instant = args[2] as? Bool - - // Write enabled to settings - let defaults = UserDefaults.standard - - // We are now enabled, so store this - defaults.set(true, forKey: "background_service_enabled") - - // The callback handle is an int64 address to communicate with the main isolate's - // entry function - defaults.set(callbackHandle, forKey: "callback_handle") - - // This is not used yet and will need to be implemented - defaults.set(notificationTitle, forKey: "notification_title") - - // Schedule the background services - BackgroundServicePlugin.scheduleBackgroundSync() - BackgroundServicePlugin.scheduleBackgroundFetch() - - result(true) - } - - // Called by the flutter code at launch to see if the background service is enabled or not - func handleIsEnabled(call: FlutterMethodCall, result: FlutterResult) { - let defaults = UserDefaults.standard - let enabled = defaults.value(forKey: "background_service_enabled") as? Bool - - // False by default - result(enabled ?? false) - } - - // Called by the Flutter code whenever a change in configuration is set - func handleConfigure(call: FlutterMethodCall, result: FlutterResult) { - - // Needs to be able to parse the arguments or else fail - guard let args = call.arguments as? Array else { - print("Cannot parse args as array: \(call.arguments)") - result(FlutterError()) - return - } - - // Needs to have 4 arguments in the call or else fail - guard args.count == 4 else { - print("Not enough arguments, 4 required: \(args.count) given") - result(FlutterError()) - return - } - - // Parse the arguments from the method call - let requireUnmeteredNetwork = args[0] as? Bool - let requireCharging = args[1] as? Bool - let triggerUpdateDelay = args[2] as? Int - let triggerMaxDelay = args[3] as? Int - - // Store the values from the call in the defaults - let defaults = UserDefaults.standard - defaults.set(requireUnmeteredNetwork, forKey: "require_unmetered_network") - defaults.set(requireCharging, forKey: "require_charging") - defaults.set(triggerUpdateDelay, forKey: "trigger_update_delay") - defaults.set(triggerMaxDelay, forKey: "trigger_max_delay") - - // Cancel the background services and reschedule them - BGTaskScheduler.shared.cancelAllTaskRequests() - BackgroundServicePlugin.scheduleBackgroundSync() - BackgroundServicePlugin.scheduleBackgroundFetch() - result(true) - } - - // Returns the number of currently scheduled background processes to Flutter, strictly - // for debugging - func handleNumberOfProcesses(call: FlutterMethodCall, result: @escaping FlutterResult) { - BGTaskScheduler.shared.getPendingTaskRequests { requests in - result(requests.count) - } - } - - // Disables the service, cancels all the task requests - func handleDisable(call: FlutterMethodCall, result: FlutterResult) { - let defaults = UserDefaults.standard - defaults.set(false, forKey: "background_service_enabled") - - BGTaskScheduler.shared.cancelAllTaskRequests() - result(true) - } - - // Checks the status of the Background App Refresh from the system - // Returns true if the service is enabled for Immich, and false otherwise - func handleBackgroundRefreshStatus(call: FlutterMethodCall, result: FlutterResult) { - switch UIApplication.shared.backgroundRefreshStatus { - case .available: - result(true) - break - case .denied: - result(false) - break - case .restricted: - result(false) - break - default: - result(false) - break - } - } - - - // Schedules a short-running background sync to sync only a few photos - static func scheduleBackgroundFetch() { - // We will schedule this task to run no matter the charging or wifi requirents from the end user - // 1. They can set Background App Refresh to Off / Wi-Fi / Wi-Fi & Cellular Data from Settings - // 2. We will check the battery connectivity when we begin running the background activity - let backgroundFetch = BGAppRefreshTaskRequest(identifier: BackgroundServicePlugin.backgroundFetchTaskID) - - // Use 5 minutes from now as earliest begin date - backgroundFetch.earliestBeginDate = Date(timeIntervalSinceNow: 5 * 60) - - do { - try BGTaskScheduler.shared.submit(backgroundFetch) - } catch { - print("Could not schedule the background task \(error.localizedDescription)") - } - } - - // Schedules a long-running background sync for syncing all of the photos - static func scheduleBackgroundSync() { - let backgroundProcessing = BGProcessingTaskRequest(identifier: BackgroundServicePlugin.backgroundProcessingTaskID) - - // We need the values for requiring charging - let defaults = UserDefaults.standard - let requireCharging = defaults.value(forKey: "require_charging") as? Bool - - // Always require network connectivity, and set the require charging from the above - backgroundProcessing.requiresNetworkConnectivity = true - backgroundProcessing.requiresExternalPower = requireCharging ?? true - - // Use 15 minutes from now as earliest begin date - backgroundProcessing.earliestBeginDate = Date(timeIntervalSinceNow: 15 * 60) - - do { - // Submit the task to the scheduler - try BGTaskScheduler.shared.submit(backgroundProcessing) - } catch { - print("Could not schedule the background task \(error.localizedDescription)") - } - } - - // This function runs when the system kicks off the BGAppRefreshTask from the Background Task Scheduler - static func handleBackgroundFetch(task: BGAppRefreshTask) { - // Schedule the next sync task so we can run this again later - scheduleBackgroundFetch() - - // Log the time of last background processing to now - let defaults = UserDefaults.standard - defaults.set(Date().timeIntervalSince1970, forKey: "last_background_fetch_run_time") - - // If we have required charging, we should check the charging status - let requireCharging = defaults.value(forKey: "require_charging") as? Bool ?? false - if (requireCharging) { - UIDevice.current.isBatteryMonitoringEnabled = true - if (UIDevice.current.batteryState == .unplugged) { - // The device is unplugged and we have required charging - // Therefore, we will simply complete the task without - // running it. - task.setTaskCompleted(success: true) - return - } - } - - // If we have required Wi-Fi, we can check the isExpensive property - let requireWifi = defaults.value(forKey: "require_wifi") as? Bool ?? false - if (requireWifi) { - let wifiMonitor = NWPathMonitor(requiredInterfaceType: .wifi) - let isExpensive = wifiMonitor.currentPath.isExpensive - if (isExpensive) { - // The network is expensive and we have required Wi-Fi - // Therefore, we will simply complete the task without - // running it - task.setTaskCompleted(success: true) - return - } - } - - // Schedule the next sync task so we can run this again later - scheduleBackgroundFetch() - - // The background sync task should only run for 20 seconds at most - BackgroundServicePlugin.runBackgroundSync(task, maxSeconds: 20) - } - - // This function runs when the system kicks off the BGProcessingTask from the Background Task Scheduler - static func handleBackgroundProcessing(task: BGProcessingTask) { - // Schedule the next sync task so we run this again later - scheduleBackgroundSync() - - // Log the time of last background processing to now - let defaults = UserDefaults.standard - defaults.set(Date().timeIntervalSince1970, forKey: "last_background_processing_run_time") - - // We won't specify a max time for the background sync service, so this can run for longer - BackgroundServicePlugin.runBackgroundSync(task, maxSeconds: nil) - } - - // This is a synchronous function which uses a semaphore to run the background sync worker's run - // function, which will create a background Isolate and communicate with the Flutter code to back - // up the assets. When it completes, we signal the semaphore and complete the execution allowing the - // control to pass back to the caller synchronously - static func runBackgroundSync(_ task: BGTask, maxSeconds: Int?) { - - let semaphore = DispatchSemaphore(value: 0) - DispatchQueue.main.async { - let backgroundWorker = BackgroundSyncWorker { _ in - semaphore.signal() - } - task.expirationHandler = { - backgroundWorker.cancel() - task.setTaskCompleted(success: true) - } - - backgroundWorker.run(maxSeconds: maxSeconds) - task.setTaskCompleted(success: true) - } - semaphore.wait() - } - - -} diff --git a/mobile/ios/Runner/BackgroundSync/BackgroundSyncWorker.swift b/mobile/ios/Runner/BackgroundSync/BackgroundSyncWorker.swift deleted file mode 100644 index 88d9368308..0000000000 --- a/mobile/ios/Runner/BackgroundSync/BackgroundSyncWorker.swift +++ /dev/null @@ -1,271 +0,0 @@ -// -// BackgroundSyncProcessing.swift -// Runner -// -// Created by Marty Fuhry on 2/6/23. -// -// Credit to https://github.com/fluttercommunity/flutter_workmanager/blob/main/ios/Classes/BackgroundWorker.swift - -import Foundation -import Flutter -import BackgroundTasks - -// The background worker which creates a new Flutter VM, communicates with it -// to run the backup job, and then finishes execution and calls back to its callback -// handler -class BackgroundSyncWorker { - - // The Flutter engine we create for background execution. - // This is not the main Flutter engine which shows the UI, - // this is a brand new isolate created and managed in this code - // here. It does not share memory with the main - // Flutter engine which shows the UI. - // It needs to be started up, registered, and torn down here - let engine: FlutterEngine? = FlutterEngine( - name: "BackgroundImmich" - ) - - let notificationId = "com.alextran.immich/backgroundNotifications" - // The background message passing channel - var channel: FlutterMethodChannel? - - var completionHandler: (UIBackgroundFetchResult) -> Void - let taskSessionStart = Date() - - // We need the completion handler to tell the system when we are done running - init(_ completionHandler: @escaping (UIBackgroundFetchResult) -> Void) { - - // This is the background message passing channel to be used with the background engine - // created here in this platform code - self.channel = FlutterMethodChannel( - name: "immich/backgroundChannel", - binaryMessenger: engine!.binaryMessenger - ) - self.completionHandler = completionHandler - } - - // Handles all of the messages from the Flutter VM called into this platform code - public func handle(_ call: FlutterMethodCall, result: @escaping FlutterResult) { - switch call.method { - case "initialized": - // Initialize tells us that we can now call into the Flutter VM to tell it to begin the update - self.channel?.invokeMethod( - "backgroundProcessing", - arguments: nil, - result: { flutterResult in - - // This is the result we send back to the BGTaskScheduler to let it know whether we'll need more time later or - // if this execution failed - let result: UIBackgroundFetchResult = (flutterResult as? Bool ?? false) ? .newData : .failed - - // Show the task duration - let taskSessionCompleter = Date() - let taskDuration = taskSessionCompleter.timeIntervalSince(self.taskSessionStart) - print("[\(String(describing: self))] \(#function) -> performBackgroundRequest.\(result) (finished in \(taskDuration) seconds)") - - // Complete the execution - self.complete(result) - }) - break - case "updateNotification": - let handled = self.handleNotification(call) - result(handled) - break - case "showError": - let handled = self.handleError(call) - result(handled) - break - case "clearErrorNotifications": - self.handleClearErrorNotifications() - result(true) - break - case "hasContentChanged": - // This is only called for Android, but we provide an implementation here - // telling Flutter that we don't have any information about whether the gallery - // contents have changed or not, so we can just say "no, they've not changed" - result(false) - break - default: - result(FlutterError()) - self.complete(UIBackgroundFetchResult.failed) - } - } - - // Runs the background sync by starting up a new isolate and handling the calls - // until it completes - public func run(maxSeconds: Int?) { - // We need the callback handle to start up the Flutter VM from the entry point - let defaults = UserDefaults.standard - guard let callbackHandle = defaults.value(forKey: "callback_handle") as? Int64 else { - // Can't find the callback handle, this is fatal - complete(UIBackgroundFetchResult.failed) - return - - } - - // Use the provided callbackHandle to get the callback function - guard let callback = FlutterCallbackCache.lookupCallbackInformation(callbackHandle) else { - // We need this callback or else this is fatal - complete(UIBackgroundFetchResult.failed) - return - } - - // Sanity check for the engine existing - if engine == nil { - complete(UIBackgroundFetchResult.failed) - return - } - - // Run the engine - let isRunning = engine!.run( - withEntrypoint: callback.callbackName, - libraryURI: callback.callbackLibraryPath - ) - - // If this engine isn't running, this is fatal - if !isRunning { - complete(UIBackgroundFetchResult.failed) - return - } - - // If we have a timer, we need to start the timer to cancel ourselves - // so that we don't run longer than the provided maxSeconds - // After maxSeconds has elapsed, we will invoke "systemStop" - if maxSeconds != nil { - // Schedule a non-repeating timer to run after maxSeconds - let timer = Timer.scheduledTimer(withTimeInterval: TimeInterval(maxSeconds!), - repeats: false) { timer in - // The callback invalidates the timer and stops execution - timer.invalidate() - - // If the channel is already deallocated, we don't need to do anything - if self.channel == nil { - return - } - - // Tell the Flutter VM to stop backing up now - self.channel?.invokeMethod( - "systemStop", - arguments: nil, - result: nil) - - // Complete the execution - self.complete(UIBackgroundFetchResult.newData) - } - } - - // Set the handle function to the channel message handler - self.channel?.setMethodCallHandler(handle) - - // Register this to get access to the plugins on the platform channel - BackgroundServicePlugin.flutterPluginRegistrantCallback?(engine!) - } - - // Cancels execution of this task, used by the system's task expiration handler - // which is called shortly before execution is about to expire - public func cancel() { - // If the channel is already deallocated, we don't need to do anything - if self.channel == nil { - return - } - - // Tell the Flutter VM to stop backing up now - self.channel?.invokeMethod( - "systemStop", - arguments: nil, - result: nil) - - // Complete the execution - self.complete(UIBackgroundFetchResult.newData) - } - - // Completes the execution, destroys the engine, and sends a completion to our callback completionHandler - private func complete(_ fetchResult: UIBackgroundFetchResult) { - engine?.destroyContext() - channel = nil - completionHandler(fetchResult) - } - - private func handleNotification(_ call: FlutterMethodCall) -> Bool { - - // Parse the arguments as an array list - guard let args = call.arguments as? Array else { - print("Failed to parse \(call.arguments) as array") - return false; - } - - // Requires 7 arguments passed or else fail - guard args.count == 7 else { - print("Needs 7 arguments, but was only passed \(args.count)") - return false - } - - // Parse the arguments to send the notification update - let title = args[0] as? String - let content = args[1] as? String - let progress = args[2] as? Int - let maximum = args[3] as? Int - let indeterminate = args[4] as? Bool - let isDetail = args[5] as? Bool - let onlyIfForeground = args[6] as? Bool - - // Build the notification - let notificationContent = UNMutableNotificationContent() - notificationContent.body = content ?? "Uploading..." - notificationContent.title = title ?? "Immich" - - // Add it to the Notification center - let notification = UNNotificationRequest( - identifier: notificationId, - content: notificationContent, - trigger: nil - ) - let center = UNUserNotificationCenter.current() - center.add(notification) { (error: Error?) in - if let theError = error { - print("Error showing notifications: \(theError)") - } - } - - return true - } - - private func handleError(_ call: FlutterMethodCall) -> Bool { - // Parse the arguments as an array list - guard let args = call.arguments as? Array else { - return false; - } - - // Requires 7 arguments passed or else fail - guard args.count == 3 else { - return false - } - - let title = args[0] as? String - let content = args[1] as? String - let individualTag = args[2] as? String - - // Build the notification - let notificationContent = UNMutableNotificationContent() - notificationContent.body = content ?? "Error running the backup job." - notificationContent.title = title ?? "Immich" - - // Add it to the Notification center - let notification = UNNotificationRequest( - identifier: notificationId, - content: notificationContent, - trigger: nil - ) - let center = UNUserNotificationCenter.current() - center.add(notification) - - return true - } - - private func handleClearErrorNotifications() { - let center = UNUserNotificationCenter.current() - center.removeDeliveredNotifications(withIdentifiers: [notificationId]) - center.removePendingNotificationRequests(withIdentifiers: [notificationId]) - } -} - diff --git a/mobile/ios/Runner/Info.plist b/mobile/ios/Runner/Info.plist index b5d7d780a6..9d194ad665 100644 --- a/mobile/ios/Runner/Info.plist +++ b/mobile/ios/Runner/Info.plist @@ -8,8 +8,6 @@ app.alextran.immich.background.refreshUpload app.alextran.immich.background.processingUpload - app.alextran.immich.backgroundFetch - app.alextran.immich.backgroundProcessing CADisableMinimumFrameDurationOnPhone diff --git a/mobile/lib/constants/constants.dart b/mobile/lib/constants/constants.dart index 9d28941b8f..1748a2a57d 100644 --- a/mobile/lib/constants/constants.dart +++ b/mobile/lib/constants/constants.dart @@ -1,9 +1,5 @@ import 'dart:io'; -const int noDbId = -9223372036854775808; // from Isar -const double downloadCompleted = -1; -const double downloadFailed = -2; - const String kMobileMetadataKey = "mobile-app"; // Number of log entries to retain on app start @@ -47,9 +43,6 @@ const List<(String, String)> kWidgetNames = [ ('com.immich.widget.memory', 'app.alextran.immich.widget.MemoryReceiver'), ]; -const double kUploadStatusFailed = -1.0; -const double kUploadStatusCanceled = -2.0; - const int kMinMonthsToEnableScrubberSnap = 12; const String kImmichAppStoreLink = "https://apps.apple.com/app/immich/id1613945652"; diff --git a/mobile/lib/constants/enums.dart b/mobile/lib/constants/enums.dart index 32ef9bbbed..877145c322 100644 --- a/mobile/lib/constants/enums.dart +++ b/mobile/lib/constants/enums.dart @@ -11,8 +11,6 @@ enum TextSearchType { context, filename, description, ocr } enum AssetVisibilityEnum { timeline, hidden, archive, locked } -enum SortUserBy { id } - enum ActionSource { timeline, viewer } enum CleanupStep { selectDate, scan, delete } diff --git a/mobile/lib/domain/interfaces/db.interface.dart b/mobile/lib/domain/interfaces/db.interface.dart deleted file mode 100644 index 5645d15c47..0000000000 --- a/mobile/lib/domain/interfaces/db.interface.dart +++ /dev/null @@ -1,3 +0,0 @@ -abstract interface class IDatabaseRepository { - Future transaction(Future Function() callback); -} diff --git a/mobile/lib/domain/models/device_asset.model.dart b/mobile/lib/domain/models/device_asset.model.dart deleted file mode 100644 index a404f5a9e2..0000000000 --- a/mobile/lib/domain/models/device_asset.model.dart +++ /dev/null @@ -1,34 +0,0 @@ -import 'dart:typed_data'; - -class DeviceAsset { - final String assetId; - final Uint8List hash; - final DateTime modifiedTime; - - const DeviceAsset({required this.assetId, required this.hash, required this.modifiedTime}); - - @override - bool operator ==(covariant DeviceAsset other) { - if (identical(this, other)) return true; - - return other.assetId == assetId && other.hash == hash && other.modifiedTime == modifiedTime; - } - - @override - int get hashCode { - return assetId.hashCode ^ hash.hashCode ^ modifiedTime.hashCode; - } - - @override - String toString() { - return 'DeviceAsset(assetId: $assetId, hash: $hash, modifiedTime: $modifiedTime)'; - } - - DeviceAsset copyWith({String? assetId, Uint8List? hash, DateTime? modifiedTime}) { - return DeviceAsset( - assetId: assetId ?? this.assetId, - hash: hash ?? this.hash, - modifiedTime: modifiedTime ?? this.modifiedTime, - ); - } -} diff --git a/mobile/lib/domain/services/asset.service.dart b/mobile/lib/domain/services/asset.service.dart index 198733b3c8..7fa8c13fd8 100644 --- a/mobile/lib/domain/services/asset.service.dart +++ b/mobile/lib/domain/services/asset.service.dart @@ -1,12 +1,9 @@ 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/exif.model.dart'; -import 'package:immich_mobile/extensions/platform_extensions.dart'; import 'package:immich_mobile/infrastructure/repositories/local_asset.repository.dart'; import 'package:immich_mobile/infrastructure/repositories/remote_asset.repository.dart'; -typedef _AssetVideoDimension = ({double? width, double? height, bool isFlipped}); - class AssetService { final RemoteAssetRepository _remoteAssetRepository; final DriftLocalAssetRepository _localAssetRepository; @@ -58,49 +55,6 @@ class AssetService { return _remoteAssetRepository.getExif(id); } - Future getAspectRatio(BaseAsset asset) async { - final dimension = asset is LocalAsset - ? await _getLocalAssetDimensions(asset) - : await _getRemoteAssetDimensions(asset as RemoteAsset); - - if (dimension.width == null || dimension.height == null || dimension.height == 0) { - return 1.0; - } - - return dimension.isFlipped ? dimension.height! / dimension.width! : dimension.width! / dimension.height!; - } - - Future<_AssetVideoDimension> _getLocalAssetDimensions(LocalAsset asset) async { - double? width = asset.width?.toDouble(); - double? height = asset.height?.toDouble(); - int orientation = asset.orientation; - - if (width == null || height == null) { - final fetched = await _localAssetRepository.get(asset.id); - width = fetched?.width?.toDouble(); - height = fetched?.height?.toDouble(); - orientation = fetched?.orientation ?? 0; - } - - // On Android, local assets need orientation correction for 90°/270° rotations - // On iOS, the Photos framework pre-corrects dimensions - final isFlipped = CurrentPlatform.isAndroid && (orientation == 90 || orientation == 270); - return (width: width, height: height, isFlipped: isFlipped); - } - - Future<_AssetVideoDimension> _getRemoteAssetDimensions(RemoteAsset asset) async { - double? width = asset.width?.toDouble(); - double? height = asset.height?.toDouble(); - - if (width == null || height == null) { - final fetched = await _remoteAssetRepository.get(asset.id); - width = fetched?.width?.toDouble(); - height = fetched?.height?.toDouble(); - } - - return (width: width, height: height, isFlipped: false); - } - Future> getPlaces(String userId) { return _remoteAssetRepository.getPlaces(userId); } diff --git a/mobile/lib/domain/services/background_worker.service.dart b/mobile/lib/domain/services/background_worker.service.dart index 93a2a14127..d4da3e31a4 100644 --- a/mobile/lib/domain/services/background_worker.service.dart +++ b/mobile/lib/domain/services/background_worker.service.dart @@ -16,19 +16,16 @@ import 'package:immich_mobile/platform/background_worker_lock_api.g.dart'; import 'package:immich_mobile/providers/app_settings.provider.dart'; import 'package:immich_mobile/providers/background_sync.provider.dart'; import 'package:immich_mobile/providers/backup/drift_backup.provider.dart'; -import 'package:immich_mobile/providers/db.provider.dart'; import 'package:immich_mobile/providers/infrastructure/db.provider.dart'; import 'package:immich_mobile/providers/infrastructure/platform.provider.dart' show nativeSyncApiProvider; import 'package:immich_mobile/providers/user.provider.dart'; -import 'package:immich_mobile/repositories/file_media.repository.dart'; import 'package:immich_mobile/services/app_settings.service.dart'; import 'package:immich_mobile/services/auth.service.dart'; -import 'package:immich_mobile/services/localization.service.dart'; import 'package:immich_mobile/services/foreground_upload.service.dart'; +import 'package:immich_mobile/services/localization.service.dart'; import 'package:immich_mobile/utils/bootstrap.dart'; import 'package:immich_mobile/utils/debug_print.dart'; import 'package:immich_mobile/wm_executor.dart'; -import 'package:isar/isar.dart'; import 'package:logging/logging.dart'; class BackgroundWorkerFgService { @@ -58,7 +55,6 @@ class BackgroundWorkerFgService { class BackgroundWorkerBgService extends BackgroundWorkerFlutterApi { ProviderContainer? _ref; - final Isar _isar; final Drift _drift; final DriftLogger _driftLogger; final BackgroundWorkerBgHostApi _backgroundHostApi; @@ -67,18 +63,11 @@ class BackgroundWorkerBgService extends BackgroundWorkerFlutterApi { bool _isCleanedUp = false; - BackgroundWorkerBgService({required Isar isar, required Drift drift, required DriftLogger driftLogger}) - : _isar = isar, - _drift = drift, + BackgroundWorkerBgService({required Drift drift, required DriftLogger driftLogger}) + : _drift = drift, _driftLogger = driftLogger, _backgroundHostApi = BackgroundWorkerBgHostApi() { - _ref = ProviderContainer( - overrides: [ - dbProvider.overrideWithValue(isar), - isarProvider.overrideWithValue(isar), - driftProvider.overrideWith(driftOverride(drift)), - ], - ); + _ref = ProviderContainer(overrides: [driftProvider.overrideWith(driftOverride(drift))]); BackgroundWorkerFlutterApi.setUp(this); } @@ -102,7 +91,6 @@ class BackgroundWorkerBgService extends BackgroundWorkerFlutterApi { ), FileDownloader().trackTasksInGroup(kDownloadGroupLivePhoto, markDownloadedComplete: false), FileDownloader().trackTasks(), - _ref?.read(fileMediaRepositoryProvider).enableBackgroundAccess(), ].nonNulls, ); @@ -209,9 +197,6 @@ class BackgroundWorkerBgService extends BackgroundWorkerFlutterApi { backgroundSyncManager?.cancel(), ]; - if (_isar.isOpen) { - cleanupFutures.add(_isar.close()); - } await Future.wait(cleanupFutures.nonNulls); _logger.info("Background worker resources cleaned up"); } catch (error, stack) { @@ -301,7 +286,6 @@ Future backgroundSyncNativeEntrypoint() async { WidgetsFlutterBinding.ensureInitialized(); DartPluginRegistrant.ensureInitialized(); - final (isar, drift, logDB) = await Bootstrap.initDB(); - await Bootstrap.initDomain(isar, drift, logDB, shouldBufferLogs: false, listenStoreUpdates: false); - await BackgroundWorkerBgService(isar: isar, drift: drift, driftLogger: logDB).init(); + final (drift, logDB) = await Bootstrap.initDomain(shouldBufferLogs: false, listenStoreUpdates: false); + await BackgroundWorkerBgService(drift: drift, driftLogger: logDB).init(); } diff --git a/mobile/lib/domain/services/log.service.dart b/mobile/lib/domain/services/log.service.dart index 64010b9220..b58ee89535 100644 --- a/mobile/lib/domain/services/log.service.dart +++ b/mobile/lib/domain/services/log.service.dart @@ -15,7 +15,7 @@ import 'package:logging/logging.dart'; /// via [IStoreRepository] class LogService { final LogRepository _logRepository; - final IStoreRepository _storeRepository; + final DriftStoreRepository _storeRepository; final List _msgBuffer = []; @@ -38,7 +38,7 @@ class LogService { static Future init({ required LogRepository logRepository, - required IStoreRepository storeRepository, + required DriftStoreRepository storeRepository, bool shouldBuffer = true, }) async { _instance ??= await create( @@ -51,7 +51,7 @@ class LogService { static Future create({ required LogRepository logRepository, - required IStoreRepository storeRepository, + required DriftStoreRepository storeRepository, bool shouldBuffer = true, }) async { final instance = LogService._(logRepository, storeRepository, shouldBuffer); diff --git a/mobile/lib/domain/services/store.service.dart b/mobile/lib/domain/services/store.service.dart index 0098c3d262..b325ffd631 100644 --- a/mobile/lib/domain/services/store.service.dart +++ b/mobile/lib/domain/services/store.service.dart @@ -6,13 +6,13 @@ import 'package:immich_mobile/infrastructure/repositories/store.repository.dart' /// Provides access to a persistent key-value store with an in-memory cache. /// Listens for repository changes to keep the cache updated. class StoreService { - final IStoreRepository _storeRepository; + final DriftStoreRepository _storeRepository; /// In-memory cache. Keys are [StoreKey.id] final Map _cache = {}; StreamSubscription>? _storeUpdateSubscription; - StoreService._({required IStoreRepository isarStoreRepository}) : _storeRepository = isarStoreRepository; + StoreService._({required DriftStoreRepository isarStoreRepository}) : _storeRepository = isarStoreRepository; // TODO: Temporary typedef to make minimal changes. Remove this and make the presentation layer access store through a provider static StoreService? _instance; @@ -24,12 +24,12 @@ class StoreService { } // TODO: Replace the implementation with the one from create after removing the typedef - static Future init({required IStoreRepository storeRepository, bool listenUpdates = true}) async { + static Future init({required DriftStoreRepository storeRepository, bool listenUpdates = true}) async { _instance ??= await create(storeRepository: storeRepository, listenUpdates: listenUpdates); return _instance!; } - static Future create({required IStoreRepository storeRepository, bool listenUpdates = true}) async { + static Future create({required DriftStoreRepository storeRepository, bool listenUpdates = true}) async { final instance = StoreService._(isarStoreRepository: storeRepository); await instance.populateCache(); if (listenUpdates) { @@ -91,8 +91,6 @@ class StoreService { await _storeRepository.deleteAll(); _cache.clear(); } - - bool get isBetaTimelineEnabled => tryGet(StoreKey.betaTimeline) ?? true; } class StoreKeyNotFoundException implements Exception { diff --git a/mobile/lib/domain/services/user.service.dart b/mobile/lib/domain/services/user.service.dart index d347d8aa4f..1f9c015ad7 100644 --- a/mobile/lib/domain/services/user.service.dart +++ b/mobile/lib/domain/services/user.service.dart @@ -4,23 +4,17 @@ import 'dart:typed_data'; import 'package:immich_mobile/domain/models/store.model.dart'; import 'package:immich_mobile/domain/models/user.model.dart'; import 'package:immich_mobile/domain/services/store.service.dart'; -import 'package:immich_mobile/infrastructure/repositories/user.repository.dart'; import 'package:immich_mobile/infrastructure/repositories/user_api.repository.dart'; import 'package:logging/logging.dart'; class UserService { final Logger _log = Logger("UserService"); - final IsarUserRepository _isarUserRepository; final UserApiRepository _userApiRepository; final StoreService _storeService; - UserService({ - required IsarUserRepository isarUserRepository, - required UserApiRepository userApiRepository, - required StoreService storeService, - }) : _isarUserRepository = isarUserRepository, - _userApiRepository = userApiRepository, - _storeService = storeService; + UserService({required UserApiRepository userApiRepository, required StoreService storeService}) + : _userApiRepository = userApiRepository, + _storeService = storeService; UserDto getMyUser() { return _storeService.get(StoreKey.currentUser); @@ -38,7 +32,6 @@ class UserService { final user = await _userApiRepository.getMyUser(); if (user == null) return null; await _storeService.put(StoreKey.currentUser, user); - await _isarUserRepository.update(user); return user; } @@ -47,19 +40,10 @@ class UserService { final path = await _userApiRepository.createProfileImage(name: name, data: image); final updatedUser = getMyUser(); await _storeService.put(StoreKey.currentUser, updatedUser); - await _isarUserRepository.update(updatedUser); return path; } catch (e) { _log.warning("Failed to upload profile image", e); return null; } } - - Future> getAll() async { - return await _isarUserRepository.getAll(); - } - - Future deleteAll() { - return _isarUserRepository.deleteAll(); - } } diff --git a/mobile/lib/entities/README.md b/mobile/lib/entities/README.md deleted file mode 100644 index c2ad4876e3..0000000000 --- a/mobile/lib/entities/README.md +++ /dev/null @@ -1 +0,0 @@ -This directory contains entity that is stored in the local storage. \ No newline at end of file diff --git a/mobile/lib/entities/album.entity.dart b/mobile/lib/entities/album.entity.dart deleted file mode 100644 index 2ca0d50dcc..0000000000 --- a/mobile/lib/entities/album.entity.dart +++ /dev/null @@ -1,192 +0,0 @@ -import 'package:flutter/foundation.dart'; -import 'package:immich_mobile/constants/enums.dart'; -import 'package:immich_mobile/entities/asset.entity.dart'; -import 'package:immich_mobile/infrastructure/entities/user.entity.dart'; -import 'package:immich_mobile/utils/datetime_comparison.dart'; -import 'package:isar/isar.dart'; -// ignore: implementation_imports -import 'package:isar/src/common/isar_links_common.dart'; -import 'package:openapi/api.dart'; - -part 'album.entity.g.dart'; - -@Collection(inheritance: false) -class Album { - @protected - Album({ - this.remoteId, - this.localId, - required this.name, - required this.createdAt, - required this.modifiedAt, - this.description, - this.startDate, - this.endDate, - this.lastModifiedAssetTimestamp, - required this.shared, - required this.activityEnabled, - this.sortOrder = SortOrder.desc, - }); - - // fields stored in DB - Id id = Isar.autoIncrement; - @Index(unique: false, replace: false, type: IndexType.hash) - String? remoteId; - @Index(unique: false, replace: false, type: IndexType.hash) - String? localId; - String name; - String? description; - DateTime createdAt; - DateTime modifiedAt; - DateTime? startDate; - DateTime? endDate; - DateTime? lastModifiedAssetTimestamp; - bool shared; - bool activityEnabled; - @enumerated - SortOrder sortOrder; - final IsarLink owner = IsarLink(); - final IsarLink thumbnail = IsarLink(); - final IsarLinks sharedUsers = IsarLinks(); - final IsarLinks assets = IsarLinks(); - - // transient fields - @ignore - bool isAll = false; - - @ignore - String? remoteThumbnailAssetId; - - @ignore - int remoteAssetCount = 0; - - // getters - @ignore - bool get isRemote => remoteId != null; - - @ignore - bool get isLocal => localId != null; - - @ignore - int get assetCount => assets.length; - - @ignore - String? get ownerId => owner.value?.id; - - @ignore - String? get ownerName { - // Guard null owner - if (owner.value == null) { - return null; - } - - final name = []; - if (owner.value?.name != null) { - name.add(owner.value!.name); - } - - return name.join(' '); - } - - @ignore - String get eTagKeyAssetCount => "device-album-$localId-asset-count"; - - // the following getter are needed because Isar links do not make data - // accessible in an object freshly created (not loaded from DB) - - @ignore - Iterable get remoteUsers => - sharedUsers.isEmpty ? (sharedUsers as IsarLinksCommon).addedObjects : sharedUsers; - - @ignore - Iterable get remoteAssets => assets.isEmpty ? (assets as IsarLinksCommon).addedObjects : assets; - - @override - bool operator ==(other) { - if (other is! Album) return false; - return id == other.id && - remoteId == other.remoteId && - localId == other.localId && - name == other.name && - description == other.description && - createdAt.isAtSameMomentAs(other.createdAt) && - modifiedAt.isAtSameMomentAs(other.modifiedAt) && - isAtSameMomentAs(startDate, other.startDate) && - isAtSameMomentAs(endDate, other.endDate) && - isAtSameMomentAs(lastModifiedAssetTimestamp, other.lastModifiedAssetTimestamp) && - shared == other.shared && - activityEnabled == other.activityEnabled && - owner.value == other.owner.value && - thumbnail.value == other.thumbnail.value && - sharedUsers.length == other.sharedUsers.length && - assets.length == other.assets.length; - } - - @override - @ignore - int get hashCode => - id.hashCode ^ - remoteId.hashCode ^ - localId.hashCode ^ - name.hashCode ^ - createdAt.hashCode ^ - modifiedAt.hashCode ^ - startDate.hashCode ^ - endDate.hashCode ^ - description.hashCode ^ - lastModifiedAssetTimestamp.hashCode ^ - shared.hashCode ^ - activityEnabled.hashCode ^ - owner.value.hashCode ^ - thumbnail.value.hashCode ^ - sharedUsers.length.hashCode ^ - assets.length.hashCode; - - static Future remote(AlbumResponseDto dto) async { - final Isar db = Isar.getInstance()!; - final Album a = Album( - remoteId: dto.id, - name: dto.albumName, - createdAt: dto.createdAt, - modifiedAt: dto.updatedAt, - description: dto.description, - lastModifiedAssetTimestamp: dto.lastModifiedAssetTimestamp, - shared: dto.shared, - startDate: dto.startDate, - endDate: dto.endDate, - activityEnabled: dto.isActivityEnabled, - ); - a.remoteAssetCount = dto.assetCount; - a.owner.value = await db.users.getById(dto.ownerId); - if (dto.order != null) { - a.sortOrder = dto.order == AssetOrder.asc ? SortOrder.asc : SortOrder.desc; - } - - if (dto.albumThumbnailAssetId != null) { - a.thumbnail.value = await db.assets.where().remoteIdEqualTo(dto.albumThumbnailAssetId).findFirst(); - } - if (dto.albumUsers.isNotEmpty) { - final users = await db.users.getAllById(dto.albumUsers.map((e) => e.user.id).toList(growable: false)); - a.sharedUsers.addAll(users.cast()); - } - if (dto.assets.isNotEmpty) { - final assets = await db.assets.getAllByRemoteId(dto.assets.map((e) => e.id)); - a.assets.addAll(assets); - } - return a; - } - - @override - String toString() => 'remoteId: $remoteId name: $name description: $description'; -} - -extension AssetsHelper on IsarCollection { - Future store(Album a) async { - await put(a); - await a.owner.save(); - await a.thumbnail.save(); - await a.sharedUsers.save(); - await a.assets.save(); - return a; - } -} diff --git a/mobile/lib/entities/album.entity.g.dart b/mobile/lib/entities/album.entity.g.dart deleted file mode 100644 index ecbbab48c2..0000000000 --- a/mobile/lib/entities/album.entity.g.dart +++ /dev/null @@ -1,2240 +0,0 @@ -// GENERATED CODE - DO NOT MODIFY BY HAND - -part of 'album.entity.dart'; - -// ************************************************************************** -// IsarCollectionGenerator -// ************************************************************************** - -// coverage:ignore-file -// ignore_for_file: duplicate_ignore, non_constant_identifier_names, constant_identifier_names, invalid_use_of_protected_member, unnecessary_cast, prefer_const_constructors, lines_longer_than_80_chars, require_trailing_commas, inference_failure_on_function_invocation, unnecessary_parenthesis, unnecessary_raw_strings, unnecessary_null_checks, join_return_with_assignment, prefer_final_locals, avoid_js_rounded_ints, avoid_positional_boolean_parameters, always_specify_types - -extension GetAlbumCollection on Isar { - IsarCollection get albums => this.collection(); -} - -const AlbumSchema = CollectionSchema( - name: r'Album', - id: -1355968412107120937, - properties: { - r'activityEnabled': PropertySchema( - id: 0, - name: r'activityEnabled', - type: IsarType.bool, - ), - r'createdAt': PropertySchema( - id: 1, - name: r'createdAt', - type: IsarType.dateTime, - ), - r'description': PropertySchema( - id: 2, - name: r'description', - type: IsarType.string, - ), - r'endDate': PropertySchema( - id: 3, - name: r'endDate', - type: IsarType.dateTime, - ), - r'lastModifiedAssetTimestamp': PropertySchema( - id: 4, - name: r'lastModifiedAssetTimestamp', - type: IsarType.dateTime, - ), - r'localId': PropertySchema(id: 5, name: r'localId', type: IsarType.string), - r'modifiedAt': PropertySchema( - id: 6, - name: r'modifiedAt', - type: IsarType.dateTime, - ), - r'name': PropertySchema(id: 7, name: r'name', type: IsarType.string), - r'remoteId': PropertySchema( - id: 8, - name: r'remoteId', - type: IsarType.string, - ), - r'shared': PropertySchema(id: 9, name: r'shared', type: IsarType.bool), - r'sortOrder': PropertySchema( - id: 10, - name: r'sortOrder', - type: IsarType.byte, - enumMap: _AlbumsortOrderEnumValueMap, - ), - r'startDate': PropertySchema( - id: 11, - name: r'startDate', - type: IsarType.dateTime, - ), - }, - - estimateSize: _albumEstimateSize, - serialize: _albumSerialize, - deserialize: _albumDeserialize, - deserializeProp: _albumDeserializeProp, - idName: r'id', - indexes: { - r'remoteId': IndexSchema( - id: 6301175856541681032, - name: r'remoteId', - unique: false, - replace: false, - properties: [ - IndexPropertySchema( - name: r'remoteId', - type: IndexType.hash, - caseSensitive: true, - ), - ], - ), - r'localId': IndexSchema( - id: 1199848425898359622, - name: r'localId', - unique: false, - replace: false, - properties: [ - IndexPropertySchema( - name: r'localId', - type: IndexType.hash, - caseSensitive: true, - ), - ], - ), - }, - links: { - r'owner': LinkSchema( - id: 8272576585804958029, - name: r'owner', - target: r'User', - single: true, - ), - r'thumbnail': LinkSchema( - id: 4055421409629988258, - name: r'thumbnail', - target: r'Asset', - single: true, - ), - r'sharedUsers': LinkSchema( - id: 8972835302564625434, - name: r'sharedUsers', - target: r'User', - single: false, - ), - r'assets': LinkSchema( - id: 1059358332698388152, - name: r'assets', - target: r'Asset', - single: false, - ), - }, - embeddedSchemas: {}, - - getId: _albumGetId, - getLinks: _albumGetLinks, - attach: _albumAttach, - version: '3.3.0-dev.3', -); - -int _albumEstimateSize( - Album object, - List offsets, - Map> allOffsets, -) { - var bytesCount = offsets.last; - { - final value = object.description; - if (value != null) { - bytesCount += 3 + value.length * 3; - } - } - { - final value = object.localId; - if (value != null) { - bytesCount += 3 + value.length * 3; - } - } - bytesCount += 3 + object.name.length * 3; - { - final value = object.remoteId; - if (value != null) { - bytesCount += 3 + value.length * 3; - } - } - return bytesCount; -} - -void _albumSerialize( - Album object, - IsarWriter writer, - List offsets, - Map> allOffsets, -) { - writer.writeBool(offsets[0], object.activityEnabled); - writer.writeDateTime(offsets[1], object.createdAt); - writer.writeString(offsets[2], object.description); - writer.writeDateTime(offsets[3], object.endDate); - writer.writeDateTime(offsets[4], object.lastModifiedAssetTimestamp); - writer.writeString(offsets[5], object.localId); - writer.writeDateTime(offsets[6], object.modifiedAt); - writer.writeString(offsets[7], object.name); - writer.writeString(offsets[8], object.remoteId); - writer.writeBool(offsets[9], object.shared); - writer.writeByte(offsets[10], object.sortOrder.index); - writer.writeDateTime(offsets[11], object.startDate); -} - -Album _albumDeserialize( - Id id, - IsarReader reader, - List offsets, - Map> allOffsets, -) { - final object = Album( - activityEnabled: reader.readBool(offsets[0]), - createdAt: reader.readDateTime(offsets[1]), - description: reader.readStringOrNull(offsets[2]), - endDate: reader.readDateTimeOrNull(offsets[3]), - lastModifiedAssetTimestamp: reader.readDateTimeOrNull(offsets[4]), - localId: reader.readStringOrNull(offsets[5]), - modifiedAt: reader.readDateTime(offsets[6]), - name: reader.readString(offsets[7]), - remoteId: reader.readStringOrNull(offsets[8]), - shared: reader.readBool(offsets[9]), - sortOrder: - _AlbumsortOrderValueEnumMap[reader.readByteOrNull(offsets[10])] ?? - SortOrder.desc, - startDate: reader.readDateTimeOrNull(offsets[11]), - ); - object.id = id; - return object; -} - -P _albumDeserializeProp

( - IsarReader reader, - int propertyId, - int offset, - Map> allOffsets, -) { - switch (propertyId) { - case 0: - return (reader.readBool(offset)) as P; - case 1: - return (reader.readDateTime(offset)) as P; - case 2: - return (reader.readStringOrNull(offset)) as P; - case 3: - return (reader.readDateTimeOrNull(offset)) as P; - case 4: - return (reader.readDateTimeOrNull(offset)) as P; - case 5: - return (reader.readStringOrNull(offset)) as P; - case 6: - return (reader.readDateTime(offset)) as P; - case 7: - return (reader.readString(offset)) as P; - case 8: - return (reader.readStringOrNull(offset)) as P; - case 9: - return (reader.readBool(offset)) as P; - case 10: - return (_AlbumsortOrderValueEnumMap[reader.readByteOrNull(offset)] ?? - SortOrder.desc) - as P; - case 11: - return (reader.readDateTimeOrNull(offset)) as P; - default: - throw IsarError('Unknown property with id $propertyId'); - } -} - -const _AlbumsortOrderEnumValueMap = {'asc': 0, 'desc': 1}; -const _AlbumsortOrderValueEnumMap = {0: SortOrder.asc, 1: SortOrder.desc}; - -Id _albumGetId(Album object) { - return object.id; -} - -List> _albumGetLinks(Album object) { - return [object.owner, object.thumbnail, object.sharedUsers, object.assets]; -} - -void _albumAttach(IsarCollection col, Id id, Album object) { - object.id = id; - object.owner.attach(col, col.isar.collection(), r'owner', id); - object.thumbnail.attach(col, col.isar.collection(), r'thumbnail', id); - object.sharedUsers.attach( - col, - col.isar.collection(), - r'sharedUsers', - id, - ); - object.assets.attach(col, col.isar.collection(), r'assets', id); -} - -extension AlbumQueryWhereSort on QueryBuilder { - QueryBuilder anyId() { - return QueryBuilder.apply(this, (query) { - return query.addWhereClause(const IdWhereClause.any()); - }); - } -} - -extension AlbumQueryWhere on QueryBuilder { - QueryBuilder idEqualTo(Id id) { - return QueryBuilder.apply(this, (query) { - return query.addWhereClause(IdWhereClause.between(lower: id, upper: id)); - }); - } - - QueryBuilder idNotEqualTo(Id id) { - return QueryBuilder.apply(this, (query) { - if (query.whereSort == Sort.asc) { - return query - .addWhereClause( - IdWhereClause.lessThan(upper: id, includeUpper: false), - ) - .addWhereClause( - IdWhereClause.greaterThan(lower: id, includeLower: false), - ); - } else { - return query - .addWhereClause( - IdWhereClause.greaterThan(lower: id, includeLower: false), - ) - .addWhereClause( - IdWhereClause.lessThan(upper: id, includeUpper: false), - ); - } - }); - } - - QueryBuilder idGreaterThan( - Id id, { - bool include = false, - }) { - return QueryBuilder.apply(this, (query) { - return query.addWhereClause( - IdWhereClause.greaterThan(lower: id, includeLower: include), - ); - }); - } - - QueryBuilder idLessThan( - Id id, { - bool include = false, - }) { - return QueryBuilder.apply(this, (query) { - return query.addWhereClause( - IdWhereClause.lessThan(upper: id, includeUpper: include), - ); - }); - } - - QueryBuilder idBetween( - Id lowerId, - Id upperId, { - bool includeLower = true, - bool includeUpper = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addWhereClause( - IdWhereClause.between( - lower: lowerId, - includeLower: includeLower, - upper: upperId, - includeUpper: includeUpper, - ), - ); - }); - } - - QueryBuilder remoteIdIsNull() { - return QueryBuilder.apply(this, (query) { - return query.addWhereClause( - IndexWhereClause.equalTo(indexName: r'remoteId', value: [null]), - ); - }); - } - - QueryBuilder remoteIdIsNotNull() { - return QueryBuilder.apply(this, (query) { - return query.addWhereClause( - IndexWhereClause.between( - indexName: r'remoteId', - lower: [null], - includeLower: false, - upper: [], - ), - ); - }); - } - - QueryBuilder remoteIdEqualTo( - String? remoteId, - ) { - return QueryBuilder.apply(this, (query) { - return query.addWhereClause( - IndexWhereClause.equalTo(indexName: r'remoteId', value: [remoteId]), - ); - }); - } - - QueryBuilder remoteIdNotEqualTo( - String? remoteId, - ) { - return QueryBuilder.apply(this, (query) { - if (query.whereSort == Sort.asc) { - return query - .addWhereClause( - IndexWhereClause.between( - indexName: r'remoteId', - lower: [], - upper: [remoteId], - includeUpper: false, - ), - ) - .addWhereClause( - IndexWhereClause.between( - indexName: r'remoteId', - lower: [remoteId], - includeLower: false, - upper: [], - ), - ); - } else { - return query - .addWhereClause( - IndexWhereClause.between( - indexName: r'remoteId', - lower: [remoteId], - includeLower: false, - upper: [], - ), - ) - .addWhereClause( - IndexWhereClause.between( - indexName: r'remoteId', - lower: [], - upper: [remoteId], - includeUpper: false, - ), - ); - } - }); - } - - QueryBuilder localIdIsNull() { - return QueryBuilder.apply(this, (query) { - return query.addWhereClause( - IndexWhereClause.equalTo(indexName: r'localId', value: [null]), - ); - }); - } - - QueryBuilder localIdIsNotNull() { - return QueryBuilder.apply(this, (query) { - return query.addWhereClause( - IndexWhereClause.between( - indexName: r'localId', - lower: [null], - includeLower: false, - upper: [], - ), - ); - }); - } - - QueryBuilder localIdEqualTo( - String? localId, - ) { - return QueryBuilder.apply(this, (query) { - return query.addWhereClause( - IndexWhereClause.equalTo(indexName: r'localId', value: [localId]), - ); - }); - } - - QueryBuilder localIdNotEqualTo( - String? localId, - ) { - return QueryBuilder.apply(this, (query) { - if (query.whereSort == Sort.asc) { - return query - .addWhereClause( - IndexWhereClause.between( - indexName: r'localId', - lower: [], - upper: [localId], - includeUpper: false, - ), - ) - .addWhereClause( - IndexWhereClause.between( - indexName: r'localId', - lower: [localId], - includeLower: false, - upper: [], - ), - ); - } else { - return query - .addWhereClause( - IndexWhereClause.between( - indexName: r'localId', - lower: [localId], - includeLower: false, - upper: [], - ), - ) - .addWhereClause( - IndexWhereClause.between( - indexName: r'localId', - lower: [], - upper: [localId], - includeUpper: false, - ), - ); - } - }); - } -} - -extension AlbumQueryFilter on QueryBuilder { - QueryBuilder activityEnabledEqualTo( - bool value, - ) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.equalTo(property: r'activityEnabled', value: value), - ); - }); - } - - QueryBuilder createdAtEqualTo( - DateTime value, - ) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.equalTo(property: r'createdAt', value: value), - ); - }); - } - - QueryBuilder createdAtGreaterThan( - DateTime value, { - bool include = false, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.greaterThan( - include: include, - property: r'createdAt', - value: value, - ), - ); - }); - } - - QueryBuilder createdAtLessThan( - DateTime value, { - bool include = false, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.lessThan( - include: include, - property: r'createdAt', - value: value, - ), - ); - }); - } - - QueryBuilder createdAtBetween( - DateTime lower, - DateTime upper, { - bool includeLower = true, - bool includeUpper = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.between( - property: r'createdAt', - lower: lower, - includeLower: includeLower, - upper: upper, - includeUpper: includeUpper, - ), - ); - }); - } - - QueryBuilder descriptionIsNull() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - const FilterCondition.isNull(property: r'description'), - ); - }); - } - - QueryBuilder descriptionIsNotNull() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - const FilterCondition.isNotNull(property: r'description'), - ); - }); - } - - QueryBuilder descriptionEqualTo( - String? value, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.equalTo( - property: r'description', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder descriptionGreaterThan( - String? value, { - bool include = false, - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.greaterThan( - include: include, - property: r'description', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder descriptionLessThan( - String? value, { - bool include = false, - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.lessThan( - include: include, - property: r'description', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder descriptionBetween( - String? lower, - String? upper, { - bool includeLower = true, - bool includeUpper = true, - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.between( - property: r'description', - lower: lower, - includeLower: includeLower, - upper: upper, - includeUpper: includeUpper, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder descriptionStartsWith( - String value, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.startsWith( - property: r'description', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder descriptionEndsWith( - String value, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.endsWith( - property: r'description', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder descriptionContains( - String value, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.contains( - property: r'description', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder descriptionMatches( - String pattern, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.matches( - property: r'description', - wildcard: pattern, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder descriptionIsEmpty() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.equalTo(property: r'description', value: ''), - ); - }); - } - - QueryBuilder descriptionIsNotEmpty() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.greaterThan(property: r'description', value: ''), - ); - }); - } - - QueryBuilder endDateIsNull() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - const FilterCondition.isNull(property: r'endDate'), - ); - }); - } - - QueryBuilder endDateIsNotNull() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - const FilterCondition.isNotNull(property: r'endDate'), - ); - }); - } - - QueryBuilder endDateEqualTo( - DateTime? value, - ) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.equalTo(property: r'endDate', value: value), - ); - }); - } - - QueryBuilder endDateGreaterThan( - DateTime? value, { - bool include = false, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.greaterThan( - include: include, - property: r'endDate', - value: value, - ), - ); - }); - } - - QueryBuilder endDateLessThan( - DateTime? value, { - bool include = false, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.lessThan( - include: include, - property: r'endDate', - value: value, - ), - ); - }); - } - - QueryBuilder endDateBetween( - DateTime? lower, - DateTime? upper, { - bool includeLower = true, - bool includeUpper = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.between( - property: r'endDate', - lower: lower, - includeLower: includeLower, - upper: upper, - includeUpper: includeUpper, - ), - ); - }); - } - - QueryBuilder idEqualTo(Id value) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.equalTo(property: r'id', value: value), - ); - }); - } - - QueryBuilder idGreaterThan( - Id value, { - bool include = false, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.greaterThan( - include: include, - property: r'id', - value: value, - ), - ); - }); - } - - QueryBuilder idLessThan( - Id value, { - bool include = false, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.lessThan( - include: include, - property: r'id', - value: value, - ), - ); - }); - } - - QueryBuilder idBetween( - Id lower, - Id upper, { - bool includeLower = true, - bool includeUpper = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.between( - property: r'id', - lower: lower, - includeLower: includeLower, - upper: upper, - includeUpper: includeUpper, - ), - ); - }); - } - - QueryBuilder - lastModifiedAssetTimestampIsNull() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - const FilterCondition.isNull(property: r'lastModifiedAssetTimestamp'), - ); - }); - } - - QueryBuilder - lastModifiedAssetTimestampIsNotNull() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - const FilterCondition.isNotNull( - property: r'lastModifiedAssetTimestamp', - ), - ); - }); - } - - QueryBuilder - lastModifiedAssetTimestampEqualTo(DateTime? value) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.equalTo( - property: r'lastModifiedAssetTimestamp', - value: value, - ), - ); - }); - } - - QueryBuilder - lastModifiedAssetTimestampGreaterThan( - DateTime? value, { - bool include = false, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.greaterThan( - include: include, - property: r'lastModifiedAssetTimestamp', - value: value, - ), - ); - }); - } - - QueryBuilder - lastModifiedAssetTimestampLessThan(DateTime? value, {bool include = false}) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.lessThan( - include: include, - property: r'lastModifiedAssetTimestamp', - value: value, - ), - ); - }); - } - - QueryBuilder - lastModifiedAssetTimestampBetween( - DateTime? lower, - DateTime? upper, { - bool includeLower = true, - bool includeUpper = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.between( - property: r'lastModifiedAssetTimestamp', - lower: lower, - includeLower: includeLower, - upper: upper, - includeUpper: includeUpper, - ), - ); - }); - } - - QueryBuilder localIdIsNull() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - const FilterCondition.isNull(property: r'localId'), - ); - }); - } - - QueryBuilder localIdIsNotNull() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - const FilterCondition.isNotNull(property: r'localId'), - ); - }); - } - - QueryBuilder localIdEqualTo( - String? value, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.equalTo( - property: r'localId', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder localIdGreaterThan( - String? value, { - bool include = false, - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.greaterThan( - include: include, - property: r'localId', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder localIdLessThan( - String? value, { - bool include = false, - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.lessThan( - include: include, - property: r'localId', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder localIdBetween( - String? lower, - String? upper, { - bool includeLower = true, - bool includeUpper = true, - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.between( - property: r'localId', - lower: lower, - includeLower: includeLower, - upper: upper, - includeUpper: includeUpper, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder localIdStartsWith( - String value, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.startsWith( - property: r'localId', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder localIdEndsWith( - String value, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.endsWith( - property: r'localId', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder localIdContains( - String value, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.contains( - property: r'localId', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder localIdMatches( - String pattern, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.matches( - property: r'localId', - wildcard: pattern, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder localIdIsEmpty() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.equalTo(property: r'localId', value: ''), - ); - }); - } - - QueryBuilder localIdIsNotEmpty() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.greaterThan(property: r'localId', value: ''), - ); - }); - } - - QueryBuilder modifiedAtEqualTo( - DateTime value, - ) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.equalTo(property: r'modifiedAt', value: value), - ); - }); - } - - QueryBuilder modifiedAtGreaterThan( - DateTime value, { - bool include = false, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.greaterThan( - include: include, - property: r'modifiedAt', - value: value, - ), - ); - }); - } - - QueryBuilder modifiedAtLessThan( - DateTime value, { - bool include = false, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.lessThan( - include: include, - property: r'modifiedAt', - value: value, - ), - ); - }); - } - - QueryBuilder modifiedAtBetween( - DateTime lower, - DateTime upper, { - bool includeLower = true, - bool includeUpper = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.between( - property: r'modifiedAt', - lower: lower, - includeLower: includeLower, - upper: upper, - includeUpper: includeUpper, - ), - ); - }); - } - - QueryBuilder nameEqualTo( - String value, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.equalTo( - property: r'name', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder nameGreaterThan( - String value, { - bool include = false, - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.greaterThan( - include: include, - property: r'name', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder nameLessThan( - String value, { - bool include = false, - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.lessThan( - include: include, - property: r'name', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder nameBetween( - String lower, - String upper, { - bool includeLower = true, - bool includeUpper = true, - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.between( - property: r'name', - lower: lower, - includeLower: includeLower, - upper: upper, - includeUpper: includeUpper, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder nameStartsWith( - String value, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.startsWith( - property: r'name', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder nameEndsWith( - String value, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.endsWith( - property: r'name', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder nameContains( - String value, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.contains( - property: r'name', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder nameMatches( - String pattern, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.matches( - property: r'name', - wildcard: pattern, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder nameIsEmpty() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.equalTo(property: r'name', value: ''), - ); - }); - } - - QueryBuilder nameIsNotEmpty() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.greaterThan(property: r'name', value: ''), - ); - }); - } - - QueryBuilder remoteIdIsNull() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - const FilterCondition.isNull(property: r'remoteId'), - ); - }); - } - - QueryBuilder remoteIdIsNotNull() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - const FilterCondition.isNotNull(property: r'remoteId'), - ); - }); - } - - QueryBuilder remoteIdEqualTo( - String? value, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.equalTo( - property: r'remoteId', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder remoteIdGreaterThan( - String? value, { - bool include = false, - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.greaterThan( - include: include, - property: r'remoteId', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder remoteIdLessThan( - String? value, { - bool include = false, - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.lessThan( - include: include, - property: r'remoteId', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder remoteIdBetween( - String? lower, - String? upper, { - bool includeLower = true, - bool includeUpper = true, - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.between( - property: r'remoteId', - lower: lower, - includeLower: includeLower, - upper: upper, - includeUpper: includeUpper, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder remoteIdStartsWith( - String value, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.startsWith( - property: r'remoteId', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder remoteIdEndsWith( - String value, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.endsWith( - property: r'remoteId', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder remoteIdContains( - String value, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.contains( - property: r'remoteId', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder remoteIdMatches( - String pattern, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.matches( - property: r'remoteId', - wildcard: pattern, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder remoteIdIsEmpty() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.equalTo(property: r'remoteId', value: ''), - ); - }); - } - - QueryBuilder remoteIdIsNotEmpty() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.greaterThan(property: r'remoteId', value: ''), - ); - }); - } - - QueryBuilder sharedEqualTo(bool value) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.equalTo(property: r'shared', value: value), - ); - }); - } - - QueryBuilder sortOrderEqualTo( - SortOrder value, - ) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.equalTo(property: r'sortOrder', value: value), - ); - }); - } - - QueryBuilder sortOrderGreaterThan( - SortOrder value, { - bool include = false, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.greaterThan( - include: include, - property: r'sortOrder', - value: value, - ), - ); - }); - } - - QueryBuilder sortOrderLessThan( - SortOrder value, { - bool include = false, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.lessThan( - include: include, - property: r'sortOrder', - value: value, - ), - ); - }); - } - - QueryBuilder sortOrderBetween( - SortOrder lower, - SortOrder upper, { - bool includeLower = true, - bool includeUpper = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.between( - property: r'sortOrder', - lower: lower, - includeLower: includeLower, - upper: upper, - includeUpper: includeUpper, - ), - ); - }); - } - - QueryBuilder startDateIsNull() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - const FilterCondition.isNull(property: r'startDate'), - ); - }); - } - - QueryBuilder startDateIsNotNull() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - const FilterCondition.isNotNull(property: r'startDate'), - ); - }); - } - - QueryBuilder startDateEqualTo( - DateTime? value, - ) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.equalTo(property: r'startDate', value: value), - ); - }); - } - - QueryBuilder startDateGreaterThan( - DateTime? value, { - bool include = false, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.greaterThan( - include: include, - property: r'startDate', - value: value, - ), - ); - }); - } - - QueryBuilder startDateLessThan( - DateTime? value, { - bool include = false, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.lessThan( - include: include, - property: r'startDate', - value: value, - ), - ); - }); - } - - QueryBuilder startDateBetween( - DateTime? lower, - DateTime? upper, { - bool includeLower = true, - bool includeUpper = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.between( - property: r'startDate', - lower: lower, - includeLower: includeLower, - upper: upper, - includeUpper: includeUpper, - ), - ); - }); - } -} - -extension AlbumQueryObject on QueryBuilder {} - -extension AlbumQueryLinks on QueryBuilder { - QueryBuilder owner(FilterQuery q) { - return QueryBuilder.apply(this, (query) { - return query.link(q, r'owner'); - }); - } - - QueryBuilder ownerIsNull() { - return QueryBuilder.apply(this, (query) { - return query.linkLength(r'owner', 0, true, 0, true); - }); - } - - QueryBuilder thumbnail( - FilterQuery q, - ) { - return QueryBuilder.apply(this, (query) { - return query.link(q, r'thumbnail'); - }); - } - - QueryBuilder thumbnailIsNull() { - return QueryBuilder.apply(this, (query) { - return query.linkLength(r'thumbnail', 0, true, 0, true); - }); - } - - QueryBuilder sharedUsers( - FilterQuery q, - ) { - return QueryBuilder.apply(this, (query) { - return query.link(q, r'sharedUsers'); - }); - } - - QueryBuilder sharedUsersLengthEqualTo( - int length, - ) { - return QueryBuilder.apply(this, (query) { - return query.linkLength(r'sharedUsers', length, true, length, true); - }); - } - - QueryBuilder sharedUsersIsEmpty() { - return QueryBuilder.apply(this, (query) { - return query.linkLength(r'sharedUsers', 0, true, 0, true); - }); - } - - QueryBuilder sharedUsersIsNotEmpty() { - return QueryBuilder.apply(this, (query) { - return query.linkLength(r'sharedUsers', 0, false, 999999, true); - }); - } - - QueryBuilder sharedUsersLengthLessThan( - int length, { - bool include = false, - }) { - return QueryBuilder.apply(this, (query) { - return query.linkLength(r'sharedUsers', 0, true, length, include); - }); - } - - QueryBuilder - sharedUsersLengthGreaterThan(int length, {bool include = false}) { - return QueryBuilder.apply(this, (query) { - return query.linkLength(r'sharedUsers', length, include, 999999, true); - }); - } - - QueryBuilder sharedUsersLengthBetween( - int lower, - int upper, { - bool includeLower = true, - bool includeUpper = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.linkLength( - r'sharedUsers', - lower, - includeLower, - upper, - includeUpper, - ); - }); - } - - QueryBuilder assets( - FilterQuery q, - ) { - return QueryBuilder.apply(this, (query) { - return query.link(q, r'assets'); - }); - } - - QueryBuilder assetsLengthEqualTo( - int length, - ) { - return QueryBuilder.apply(this, (query) { - return query.linkLength(r'assets', length, true, length, true); - }); - } - - QueryBuilder assetsIsEmpty() { - return QueryBuilder.apply(this, (query) { - return query.linkLength(r'assets', 0, true, 0, true); - }); - } - - QueryBuilder assetsIsNotEmpty() { - return QueryBuilder.apply(this, (query) { - return query.linkLength(r'assets', 0, false, 999999, true); - }); - } - - QueryBuilder assetsLengthLessThan( - int length, { - bool include = false, - }) { - return QueryBuilder.apply(this, (query) { - return query.linkLength(r'assets', 0, true, length, include); - }); - } - - QueryBuilder assetsLengthGreaterThan( - int length, { - bool include = false, - }) { - return QueryBuilder.apply(this, (query) { - return query.linkLength(r'assets', length, include, 999999, true); - }); - } - - QueryBuilder assetsLengthBetween( - int lower, - int upper, { - bool includeLower = true, - bool includeUpper = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.linkLength( - r'assets', - lower, - includeLower, - upper, - includeUpper, - ); - }); - } -} - -extension AlbumQuerySortBy on QueryBuilder { - QueryBuilder sortByActivityEnabled() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'activityEnabled', Sort.asc); - }); - } - - QueryBuilder sortByActivityEnabledDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'activityEnabled', Sort.desc); - }); - } - - QueryBuilder sortByCreatedAt() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'createdAt', Sort.asc); - }); - } - - QueryBuilder sortByCreatedAtDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'createdAt', Sort.desc); - }); - } - - QueryBuilder sortByDescription() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'description', Sort.asc); - }); - } - - QueryBuilder sortByDescriptionDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'description', Sort.desc); - }); - } - - QueryBuilder sortByEndDate() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'endDate', Sort.asc); - }); - } - - QueryBuilder sortByEndDateDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'endDate', Sort.desc); - }); - } - - QueryBuilder sortByLastModifiedAssetTimestamp() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'lastModifiedAssetTimestamp', Sort.asc); - }); - } - - QueryBuilder - sortByLastModifiedAssetTimestampDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'lastModifiedAssetTimestamp', Sort.desc); - }); - } - - QueryBuilder sortByLocalId() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'localId', Sort.asc); - }); - } - - QueryBuilder sortByLocalIdDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'localId', Sort.desc); - }); - } - - QueryBuilder sortByModifiedAt() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'modifiedAt', Sort.asc); - }); - } - - QueryBuilder sortByModifiedAtDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'modifiedAt', Sort.desc); - }); - } - - QueryBuilder sortByName() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'name', Sort.asc); - }); - } - - QueryBuilder sortByNameDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'name', Sort.desc); - }); - } - - QueryBuilder sortByRemoteId() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'remoteId', Sort.asc); - }); - } - - QueryBuilder sortByRemoteIdDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'remoteId', Sort.desc); - }); - } - - QueryBuilder sortByShared() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'shared', Sort.asc); - }); - } - - QueryBuilder sortBySharedDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'shared', Sort.desc); - }); - } - - QueryBuilder sortBySortOrder() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'sortOrder', Sort.asc); - }); - } - - QueryBuilder sortBySortOrderDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'sortOrder', Sort.desc); - }); - } - - QueryBuilder sortByStartDate() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'startDate', Sort.asc); - }); - } - - QueryBuilder sortByStartDateDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'startDate', Sort.desc); - }); - } -} - -extension AlbumQuerySortThenBy on QueryBuilder { - QueryBuilder thenByActivityEnabled() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'activityEnabled', Sort.asc); - }); - } - - QueryBuilder thenByActivityEnabledDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'activityEnabled', Sort.desc); - }); - } - - QueryBuilder thenByCreatedAt() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'createdAt', Sort.asc); - }); - } - - QueryBuilder thenByCreatedAtDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'createdAt', Sort.desc); - }); - } - - QueryBuilder thenByDescription() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'description', Sort.asc); - }); - } - - QueryBuilder thenByDescriptionDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'description', Sort.desc); - }); - } - - QueryBuilder thenByEndDate() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'endDate', Sort.asc); - }); - } - - QueryBuilder thenByEndDateDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'endDate', Sort.desc); - }); - } - - QueryBuilder thenById() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'id', Sort.asc); - }); - } - - QueryBuilder thenByIdDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'id', Sort.desc); - }); - } - - QueryBuilder thenByLastModifiedAssetTimestamp() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'lastModifiedAssetTimestamp', Sort.asc); - }); - } - - QueryBuilder - thenByLastModifiedAssetTimestampDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'lastModifiedAssetTimestamp', Sort.desc); - }); - } - - QueryBuilder thenByLocalId() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'localId', Sort.asc); - }); - } - - QueryBuilder thenByLocalIdDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'localId', Sort.desc); - }); - } - - QueryBuilder thenByModifiedAt() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'modifiedAt', Sort.asc); - }); - } - - QueryBuilder thenByModifiedAtDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'modifiedAt', Sort.desc); - }); - } - - QueryBuilder thenByName() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'name', Sort.asc); - }); - } - - QueryBuilder thenByNameDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'name', Sort.desc); - }); - } - - QueryBuilder thenByRemoteId() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'remoteId', Sort.asc); - }); - } - - QueryBuilder thenByRemoteIdDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'remoteId', Sort.desc); - }); - } - - QueryBuilder thenByShared() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'shared', Sort.asc); - }); - } - - QueryBuilder thenBySharedDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'shared', Sort.desc); - }); - } - - QueryBuilder thenBySortOrder() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'sortOrder', Sort.asc); - }); - } - - QueryBuilder thenBySortOrderDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'sortOrder', Sort.desc); - }); - } - - QueryBuilder thenByStartDate() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'startDate', Sort.asc); - }); - } - - QueryBuilder thenByStartDateDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'startDate', Sort.desc); - }); - } -} - -extension AlbumQueryWhereDistinct on QueryBuilder { - QueryBuilder distinctByActivityEnabled() { - return QueryBuilder.apply(this, (query) { - return query.addDistinctBy(r'activityEnabled'); - }); - } - - QueryBuilder distinctByCreatedAt() { - return QueryBuilder.apply(this, (query) { - return query.addDistinctBy(r'createdAt'); - }); - } - - QueryBuilder distinctByDescription({ - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addDistinctBy(r'description', caseSensitive: caseSensitive); - }); - } - - QueryBuilder distinctByEndDate() { - return QueryBuilder.apply(this, (query) { - return query.addDistinctBy(r'endDate'); - }); - } - - QueryBuilder distinctByLastModifiedAssetTimestamp() { - return QueryBuilder.apply(this, (query) { - return query.addDistinctBy(r'lastModifiedAssetTimestamp'); - }); - } - - QueryBuilder distinctByLocalId({ - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addDistinctBy(r'localId', caseSensitive: caseSensitive); - }); - } - - QueryBuilder distinctByModifiedAt() { - return QueryBuilder.apply(this, (query) { - return query.addDistinctBy(r'modifiedAt'); - }); - } - - QueryBuilder distinctByName({ - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addDistinctBy(r'name', caseSensitive: caseSensitive); - }); - } - - QueryBuilder distinctByRemoteId({ - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addDistinctBy(r'remoteId', caseSensitive: caseSensitive); - }); - } - - QueryBuilder distinctByShared() { - return QueryBuilder.apply(this, (query) { - return query.addDistinctBy(r'shared'); - }); - } - - QueryBuilder distinctBySortOrder() { - return QueryBuilder.apply(this, (query) { - return query.addDistinctBy(r'sortOrder'); - }); - } - - QueryBuilder distinctByStartDate() { - return QueryBuilder.apply(this, (query) { - return query.addDistinctBy(r'startDate'); - }); - } -} - -extension AlbumQueryProperty on QueryBuilder { - QueryBuilder idProperty() { - return QueryBuilder.apply(this, (query) { - return query.addPropertyName(r'id'); - }); - } - - QueryBuilder activityEnabledProperty() { - return QueryBuilder.apply(this, (query) { - return query.addPropertyName(r'activityEnabled'); - }); - } - - QueryBuilder createdAtProperty() { - return QueryBuilder.apply(this, (query) { - return query.addPropertyName(r'createdAt'); - }); - } - - QueryBuilder descriptionProperty() { - return QueryBuilder.apply(this, (query) { - return query.addPropertyName(r'description'); - }); - } - - QueryBuilder endDateProperty() { - return QueryBuilder.apply(this, (query) { - return query.addPropertyName(r'endDate'); - }); - } - - QueryBuilder - lastModifiedAssetTimestampProperty() { - return QueryBuilder.apply(this, (query) { - return query.addPropertyName(r'lastModifiedAssetTimestamp'); - }); - } - - QueryBuilder localIdProperty() { - return QueryBuilder.apply(this, (query) { - return query.addPropertyName(r'localId'); - }); - } - - QueryBuilder modifiedAtProperty() { - return QueryBuilder.apply(this, (query) { - return query.addPropertyName(r'modifiedAt'); - }); - } - - QueryBuilder nameProperty() { - return QueryBuilder.apply(this, (query) { - return query.addPropertyName(r'name'); - }); - } - - QueryBuilder remoteIdProperty() { - return QueryBuilder.apply(this, (query) { - return query.addPropertyName(r'remoteId'); - }); - } - - QueryBuilder sharedProperty() { - return QueryBuilder.apply(this, (query) { - return query.addPropertyName(r'shared'); - }); - } - - QueryBuilder sortOrderProperty() { - return QueryBuilder.apply(this, (query) { - return query.addPropertyName(r'sortOrder'); - }); - } - - QueryBuilder startDateProperty() { - return QueryBuilder.apply(this, (query) { - return query.addPropertyName(r'startDate'); - }); - } -} diff --git a/mobile/lib/entities/android_device_asset.entity.dart b/mobile/lib/entities/android_device_asset.entity.dart deleted file mode 100644 index 792de346b9..0000000000 --- a/mobile/lib/entities/android_device_asset.entity.dart +++ /dev/null @@ -1,10 +0,0 @@ -import 'package:immich_mobile/entities/device_asset.entity.dart'; -import 'package:isar/isar.dart'; - -part 'android_device_asset.entity.g.dart'; - -@Collection() -class AndroidDeviceAsset extends DeviceAsset { - AndroidDeviceAsset({required this.id, required super.hash}); - Id id; -} diff --git a/mobile/lib/entities/android_device_asset.entity.g.dart b/mobile/lib/entities/android_device_asset.entity.g.dart deleted file mode 100644 index f8b1e32c72..0000000000 --- a/mobile/lib/entities/android_device_asset.entity.g.dart +++ /dev/null @@ -1,463 +0,0 @@ -// GENERATED CODE - DO NOT MODIFY BY HAND - -part of 'android_device_asset.entity.dart'; - -// ************************************************************************** -// IsarCollectionGenerator -// ************************************************************************** - -// coverage:ignore-file -// ignore_for_file: duplicate_ignore, non_constant_identifier_names, constant_identifier_names, invalid_use_of_protected_member, unnecessary_cast, prefer_const_constructors, lines_longer_than_80_chars, require_trailing_commas, inference_failure_on_function_invocation, unnecessary_parenthesis, unnecessary_raw_strings, unnecessary_null_checks, join_return_with_assignment, prefer_final_locals, avoid_js_rounded_ints, avoid_positional_boolean_parameters, always_specify_types - -extension GetAndroidDeviceAssetCollection on Isar { - IsarCollection get androidDeviceAssets => - this.collection(); -} - -const AndroidDeviceAssetSchema = CollectionSchema( - name: r'AndroidDeviceAsset', - id: -6758387181232899335, - properties: { - r'hash': PropertySchema(id: 0, name: r'hash', type: IsarType.byteList), - }, - - estimateSize: _androidDeviceAssetEstimateSize, - serialize: _androidDeviceAssetSerialize, - deserialize: _androidDeviceAssetDeserialize, - deserializeProp: _androidDeviceAssetDeserializeProp, - idName: r'id', - indexes: { - r'hash': IndexSchema( - id: -7973251393006690288, - name: r'hash', - unique: false, - replace: false, - properties: [ - IndexPropertySchema( - name: r'hash', - type: IndexType.hash, - caseSensitive: false, - ), - ], - ), - }, - links: {}, - embeddedSchemas: {}, - - getId: _androidDeviceAssetGetId, - getLinks: _androidDeviceAssetGetLinks, - attach: _androidDeviceAssetAttach, - version: '3.3.0-dev.3', -); - -int _androidDeviceAssetEstimateSize( - AndroidDeviceAsset object, - List offsets, - Map> allOffsets, -) { - var bytesCount = offsets.last; - bytesCount += 3 + object.hash.length; - return bytesCount; -} - -void _androidDeviceAssetSerialize( - AndroidDeviceAsset object, - IsarWriter writer, - List offsets, - Map> allOffsets, -) { - writer.writeByteList(offsets[0], object.hash); -} - -AndroidDeviceAsset _androidDeviceAssetDeserialize( - Id id, - IsarReader reader, - List offsets, - Map> allOffsets, -) { - final object = AndroidDeviceAsset( - hash: reader.readByteList(offsets[0]) ?? [], - id: id, - ); - return object; -} - -P _androidDeviceAssetDeserializeProp

( - IsarReader reader, - int propertyId, - int offset, - Map> allOffsets, -) { - switch (propertyId) { - case 0: - return (reader.readByteList(offset) ?? []) as P; - default: - throw IsarError('Unknown property with id $propertyId'); - } -} - -Id _androidDeviceAssetGetId(AndroidDeviceAsset object) { - return object.id; -} - -List> _androidDeviceAssetGetLinks( - AndroidDeviceAsset object, -) { - return []; -} - -void _androidDeviceAssetAttach( - IsarCollection col, - Id id, - AndroidDeviceAsset object, -) { - object.id = id; -} - -extension AndroidDeviceAssetQueryWhereSort - on QueryBuilder { - QueryBuilder anyId() { - return QueryBuilder.apply(this, (query) { - return query.addWhereClause(const IdWhereClause.any()); - }); - } -} - -extension AndroidDeviceAssetQueryWhere - on QueryBuilder { - QueryBuilder - idEqualTo(Id id) { - return QueryBuilder.apply(this, (query) { - return query.addWhereClause(IdWhereClause.between(lower: id, upper: id)); - }); - } - - QueryBuilder - idNotEqualTo(Id id) { - return QueryBuilder.apply(this, (query) { - if (query.whereSort == Sort.asc) { - return query - .addWhereClause( - IdWhereClause.lessThan(upper: id, includeUpper: false), - ) - .addWhereClause( - IdWhereClause.greaterThan(lower: id, includeLower: false), - ); - } else { - return query - .addWhereClause( - IdWhereClause.greaterThan(lower: id, includeLower: false), - ) - .addWhereClause( - IdWhereClause.lessThan(upper: id, includeUpper: false), - ); - } - }); - } - - QueryBuilder - idGreaterThan(Id id, {bool include = false}) { - return QueryBuilder.apply(this, (query) { - return query.addWhereClause( - IdWhereClause.greaterThan(lower: id, includeLower: include), - ); - }); - } - - QueryBuilder - idLessThan(Id id, {bool include = false}) { - return QueryBuilder.apply(this, (query) { - return query.addWhereClause( - IdWhereClause.lessThan(upper: id, includeUpper: include), - ); - }); - } - - QueryBuilder - idBetween( - Id lowerId, - Id upperId, { - bool includeLower = true, - bool includeUpper = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addWhereClause( - IdWhereClause.between( - lower: lowerId, - includeLower: includeLower, - upper: upperId, - includeUpper: includeUpper, - ), - ); - }); - } - - QueryBuilder - hashEqualTo(List hash) { - return QueryBuilder.apply(this, (query) { - return query.addWhereClause( - IndexWhereClause.equalTo(indexName: r'hash', value: [hash]), - ); - }); - } - - QueryBuilder - hashNotEqualTo(List hash) { - return QueryBuilder.apply(this, (query) { - if (query.whereSort == Sort.asc) { - return query - .addWhereClause( - IndexWhereClause.between( - indexName: r'hash', - lower: [], - upper: [hash], - includeUpper: false, - ), - ) - .addWhereClause( - IndexWhereClause.between( - indexName: r'hash', - lower: [hash], - includeLower: false, - upper: [], - ), - ); - } else { - return query - .addWhereClause( - IndexWhereClause.between( - indexName: r'hash', - lower: [hash], - includeLower: false, - upper: [], - ), - ) - .addWhereClause( - IndexWhereClause.between( - indexName: r'hash', - lower: [], - upper: [hash], - includeUpper: false, - ), - ); - } - }); - } -} - -extension AndroidDeviceAssetQueryFilter - on QueryBuilder { - QueryBuilder - hashElementEqualTo(int value) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.equalTo(property: r'hash', value: value), - ); - }); - } - - QueryBuilder - hashElementGreaterThan(int value, {bool include = false}) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.greaterThan( - include: include, - property: r'hash', - value: value, - ), - ); - }); - } - - QueryBuilder - hashElementLessThan(int value, {bool include = false}) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.lessThan( - include: include, - property: r'hash', - value: value, - ), - ); - }); - } - - QueryBuilder - hashElementBetween( - int lower, - int upper, { - bool includeLower = true, - bool includeUpper = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.between( - property: r'hash', - lower: lower, - includeLower: includeLower, - upper: upper, - includeUpper: includeUpper, - ), - ); - }); - } - - QueryBuilder - hashLengthEqualTo(int length) { - return QueryBuilder.apply(this, (query) { - return query.listLength(r'hash', length, true, length, true); - }); - } - - QueryBuilder - hashIsEmpty() { - return QueryBuilder.apply(this, (query) { - return query.listLength(r'hash', 0, true, 0, true); - }); - } - - QueryBuilder - hashIsNotEmpty() { - return QueryBuilder.apply(this, (query) { - return query.listLength(r'hash', 0, false, 999999, true); - }); - } - - QueryBuilder - hashLengthLessThan(int length, {bool include = false}) { - return QueryBuilder.apply(this, (query) { - return query.listLength(r'hash', 0, true, length, include); - }); - } - - QueryBuilder - hashLengthGreaterThan(int length, {bool include = false}) { - return QueryBuilder.apply(this, (query) { - return query.listLength(r'hash', length, include, 999999, true); - }); - } - - QueryBuilder - hashLengthBetween( - int lower, - int upper, { - bool includeLower = true, - bool includeUpper = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.listLength( - r'hash', - lower, - includeLower, - upper, - includeUpper, - ); - }); - } - - QueryBuilder - idEqualTo(Id value) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.equalTo(property: r'id', value: value), - ); - }); - } - - QueryBuilder - idGreaterThan(Id value, {bool include = false}) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.greaterThan( - include: include, - property: r'id', - value: value, - ), - ); - }); - } - - QueryBuilder - idLessThan(Id value, {bool include = false}) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.lessThan( - include: include, - property: r'id', - value: value, - ), - ); - }); - } - - QueryBuilder - idBetween( - Id lower, - Id upper, { - bool includeLower = true, - bool includeUpper = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.between( - property: r'id', - lower: lower, - includeLower: includeLower, - upper: upper, - includeUpper: includeUpper, - ), - ); - }); - } -} - -extension AndroidDeviceAssetQueryObject - on QueryBuilder {} - -extension AndroidDeviceAssetQueryLinks - on QueryBuilder {} - -extension AndroidDeviceAssetQuerySortBy - on QueryBuilder {} - -extension AndroidDeviceAssetQuerySortThenBy - on QueryBuilder { - QueryBuilder - thenById() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'id', Sort.asc); - }); - } - - QueryBuilder - thenByIdDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'id', Sort.desc); - }); - } -} - -extension AndroidDeviceAssetQueryWhereDistinct - on QueryBuilder { - QueryBuilder - distinctByHash() { - return QueryBuilder.apply(this, (query) { - return query.addDistinctBy(r'hash'); - }); - } -} - -extension AndroidDeviceAssetQueryProperty - on QueryBuilder { - QueryBuilder idProperty() { - return QueryBuilder.apply(this, (query) { - return query.addPropertyName(r'id'); - }); - } - - QueryBuilder, QQueryOperations> hashProperty() { - return QueryBuilder.apply(this, (query) { - return query.addPropertyName(r'hash'); - }); - } -} diff --git a/mobile/lib/entities/asset.entity.dart b/mobile/lib/entities/asset.entity.dart deleted file mode 100644 index 0d549457a1..0000000000 --- a/mobile/lib/entities/asset.entity.dart +++ /dev/null @@ -1,575 +0,0 @@ -import 'dart:convert'; -import 'dart:io'; - -import 'package:immich_mobile/constants/enums.dart'; -import 'package:immich_mobile/domain/models/exif.model.dart'; -import 'package:immich_mobile/extensions/string_extensions.dart'; -import 'package:immich_mobile/infrastructure/entities/exif.entity.dart' as entity; -import 'package:immich_mobile/infrastructure/utils/exif.converter.dart'; -import 'package:immich_mobile/utils/diff.dart'; -import 'package:immich_mobile/utils/hash.dart'; -import 'package:isar/isar.dart'; -import 'package:openapi/api.dart'; -import 'package:path/path.dart' as p; -import 'package:photo_manager/photo_manager.dart' show AssetEntity; - -part 'asset.entity.g.dart'; - -/// Asset (online or local) -@Collection(inheritance: false) -class Asset { - Asset.remote(AssetResponseDto remote) - : remoteId = remote.id, - checksum = remote.checksum, - fileCreatedAt = remote.fileCreatedAt, - fileModifiedAt = remote.fileModifiedAt, - updatedAt = remote.updatedAt, - durationInSeconds = remote.duration.toDuration()?.inSeconds ?? 0, - type = remote.type.toAssetType(), - fileName = remote.originalFileName, - height = remote.exifInfo?.exifImageHeight?.toInt(), - width = remote.exifInfo?.exifImageWidth?.toInt(), - livePhotoVideoId = remote.livePhotoVideoId, - ownerId = fastHash(remote.ownerId), - exifInfo = remote.exifInfo == null ? null : ExifDtoConverter.fromDto(remote.exifInfo!), - isFavorite = remote.isFavorite, - isArchived = remote.isArchived, - isTrashed = remote.isTrashed, - isOffline = remote.isOffline, - // workaround to nullify stackPrimaryAssetId for the parent asset until we refactor the mobile app - // stack handling to properly handle it - stackPrimaryAssetId = remote.stack?.primaryAssetId == remote.id ? null : remote.stack?.primaryAssetId, - stackCount = remote.stack?.assetCount ?? 0, - stackId = remote.stack?.id, - thumbhash = remote.thumbhash, - visibility = getVisibility(remote.visibility); - - Asset({ - this.id = Isar.autoIncrement, - required this.checksum, - this.remoteId, - required this.localId, - required this.ownerId, - required this.fileCreatedAt, - required this.fileModifiedAt, - required this.updatedAt, - required this.durationInSeconds, - required this.type, - this.width, - this.height, - required this.fileName, - this.livePhotoVideoId, - this.exifInfo, - this.isFavorite = false, - this.isArchived = false, - this.isTrashed = false, - this.stackId, - this.stackPrimaryAssetId, - this.stackCount = 0, - this.isOffline = false, - this.thumbhash, - this.visibility = AssetVisibilityEnum.timeline, - }); - - @ignore - AssetEntity? _local; - - @ignore - AssetEntity? get local { - if (isLocal && _local == null) { - _local = AssetEntity( - id: localId!, - typeInt: isImage ? 1 : 2, - width: width ?? 0, - height: height ?? 0, - duration: durationInSeconds, - createDateSecond: fileCreatedAt.millisecondsSinceEpoch ~/ 1000, - modifiedDateSecond: fileModifiedAt.millisecondsSinceEpoch ~/ 1000, - title: fileName, - ); - } - return _local; - } - - set local(AssetEntity? assetEntity) => _local = assetEntity; - - @ignore - bool _didUpdateLocal = false; - - @ignore - Future get localAsync async { - final local = this.local; - if (local == null) { - throw Exception('Asset $fileName has no local data'); - } - - final updatedLocal = _didUpdateLocal ? local : await local.obtainForNewProperties(); - if (updatedLocal == null) { - throw Exception('Could not fetch local data for $fileName'); - } - - this.local = updatedLocal; - _didUpdateLocal = true; - return updatedLocal; - } - - Id id = Isar.autoIncrement; - - /// stores the raw SHA1 bytes as a base64 String - /// because Isar cannot sort lists of byte arrays - String checksum; - - String? thumbhash; - - @Index(unique: false, replace: false, type: IndexType.hash) - String? remoteId; - - @Index(unique: false, replace: false, type: IndexType.hash) - String? localId; - - @Index(unique: true, replace: false, composite: [CompositeIndex("checksum", type: IndexType.hash)]) - int ownerId; - - DateTime fileCreatedAt; - - DateTime fileModifiedAt; - - DateTime updatedAt; - - int durationInSeconds; - - @Enumerated(EnumType.ordinal) - AssetType type; - - short? width; - - short? height; - - String fileName; - - String? livePhotoVideoId; - - bool isFavorite; - - bool isArchived; - - bool isTrashed; - - bool isOffline; - - @ignore - ExifInfo? exifInfo; - - String? stackId; - - String? stackPrimaryAssetId; - - int stackCount; - - @Enumerated(EnumType.ordinal) - AssetVisibilityEnum visibility; - - /// Returns null if the asset has no sync access to the exif info - @ignore - double? get aspectRatio { - final orientatedWidth = this.orientatedWidth; - final orientatedHeight = this.orientatedHeight; - - if (orientatedWidth != null && orientatedHeight != null && orientatedWidth > 0 && orientatedHeight > 0) { - return orientatedWidth.toDouble() / orientatedHeight.toDouble(); - } - - return null; - } - - /// `true` if this [Asset] is present on the device - @ignore - bool get isLocal => localId != null; - - @ignore - bool get isInDb => id != Isar.autoIncrement; - - @ignore - String get name => p.withoutExtension(fileName); - - /// `true` if this [Asset] is present on the server - @ignore - bool get isRemote => remoteId != null; - - @ignore - bool get isImage => type == AssetType.image; - - @ignore - bool get isVideo => type == AssetType.video; - - @ignore - bool get isMotionPhoto => livePhotoVideoId != null; - - @ignore - AssetState get storage { - if (isRemote && isLocal) { - return AssetState.merged; - } else if (isRemote) { - return AssetState.remote; - } else if (isLocal) { - return AssetState.local; - } else { - throw Exception("Asset has illegal state: $this"); - } - } - - @ignore - Duration get duration => Duration(seconds: durationInSeconds); - - // ignore: invalid_annotation_target - @ignore - set byteHash(List hash) => checksum = base64.encode(hash); - - /// Returns null if the asset has no sync access to the exif info - @ignore - @pragma('vm:prefer-inline') - bool? get isFlipped { - final exifInfo = this.exifInfo; - if (exifInfo != null) { - return exifInfo.isFlipped; - } - - if (_didUpdateLocal && Platform.isAndroid) { - final local = this.local; - if (local == null) { - throw Exception('Asset $fileName has no local data'); - } - return local.orientation == 90 || local.orientation == 270; - } - - return null; - } - - /// Returns null if the asset has no sync access to the exif info - @ignore - @pragma('vm:prefer-inline') - int? get orientatedHeight { - final isFlipped = this.isFlipped; - if (isFlipped == null) { - return null; - } - - return isFlipped ? width : height; - } - - /// Returns null if the asset has no sync access to the exif info - @ignore - @pragma('vm:prefer-inline') - int? get orientatedWidth { - final isFlipped = this.isFlipped; - if (isFlipped == null) { - return null; - } - - return isFlipped ? height : width; - } - - @override - bool operator ==(other) { - if (other is! Asset) return false; - if (identical(this, other)) return true; - return id == other.id && - checksum == other.checksum && - remoteId == other.remoteId && - localId == other.localId && - ownerId == other.ownerId && - fileCreatedAt.isAtSameMomentAs(other.fileCreatedAt) && - fileModifiedAt.isAtSameMomentAs(other.fileModifiedAt) && - updatedAt.isAtSameMomentAs(other.updatedAt) && - durationInSeconds == other.durationInSeconds && - type == other.type && - width == other.width && - height == other.height && - fileName == other.fileName && - livePhotoVideoId == other.livePhotoVideoId && - isFavorite == other.isFavorite && - isLocal == other.isLocal && - isArchived == other.isArchived && - isTrashed == other.isTrashed && - stackCount == other.stackCount && - stackPrimaryAssetId == other.stackPrimaryAssetId && - stackId == other.stackId; - } - - @override - @ignore - int get hashCode => - id.hashCode ^ - checksum.hashCode ^ - remoteId.hashCode ^ - localId.hashCode ^ - ownerId.hashCode ^ - fileCreatedAt.hashCode ^ - fileModifiedAt.hashCode ^ - updatedAt.hashCode ^ - durationInSeconds.hashCode ^ - type.hashCode ^ - width.hashCode ^ - height.hashCode ^ - fileName.hashCode ^ - livePhotoVideoId.hashCode ^ - isFavorite.hashCode ^ - isLocal.hashCode ^ - isArchived.hashCode ^ - isTrashed.hashCode ^ - stackCount.hashCode ^ - stackPrimaryAssetId.hashCode ^ - stackId.hashCode; - - /// Returns `true` if this [Asset] can updated with values from parameter [a] - bool canUpdate(Asset a) { - assert(isInDb); - assert(checksum == a.checksum); - assert(a.storage != AssetState.merged); - return a.updatedAt.isAfter(updatedAt) || - a.isRemote && !isRemote || - a.isLocal && !isLocal || - width == null && a.width != null || - height == null && a.height != null || - livePhotoVideoId == null && a.livePhotoVideoId != null || - isFavorite != a.isFavorite || - isArchived != a.isArchived || - isTrashed != a.isTrashed || - isOffline != a.isOffline || - a.exifInfo?.latitude != exifInfo?.latitude || - a.exifInfo?.longitude != exifInfo?.longitude || - // no local stack count or different count from remote - a.thumbhash != thumbhash || - stackId != a.stackId || - stackCount != a.stackCount || - stackPrimaryAssetId == null && a.stackPrimaryAssetId != null || - visibility != a.visibility; - } - - /// Returns a new [Asset] with values from this and merged & updated with [a] - Asset updatedCopy(Asset a) { - assert(canUpdate(a)); - if (a.updatedAt.isAfter(updatedAt)) { - // take most values from newer asset - // keep vales that can never be set by the asset not in DB - if (a.isRemote) { - return a.copyWith( - id: id, - localId: localId, - width: a.width ?? width, - height: a.height ?? height, - exifInfo: a.exifInfo?.copyWith(assetId: id) ?? exifInfo, - ); - } else if (isRemote) { - return copyWith( - localId: localId ?? a.localId, - width: width ?? a.width, - height: height ?? a.height, - exifInfo: exifInfo ?? a.exifInfo?.copyWith(assetId: id), - ); - } else { - // TODO: Revisit this and remove all bool field assignments - return a.copyWith( - id: id, - remoteId: remoteId, - livePhotoVideoId: livePhotoVideoId, - // workaround to nullify stackPrimaryAssetId for the parent asset until we refactor the mobile app - // stack handling to properly handle it - stackId: stackId, - stackPrimaryAssetId: stackPrimaryAssetId == remoteId ? null : stackPrimaryAssetId, - stackCount: stackCount, - isFavorite: isFavorite, - isArchived: isArchived, - isTrashed: isTrashed, - isOffline: isOffline, - ); - } - } else { - // fill in potentially missing values, i.e. merge assets - if (a.isRemote) { - // values from remote take precedence - return copyWith( - remoteId: a.remoteId, - width: a.width, - height: a.height, - livePhotoVideoId: a.livePhotoVideoId, - // workaround to nullify stackPrimaryAssetId for the parent asset until we refactor the mobile app - // stack handling to properly handle it - stackId: a.stackId, - stackPrimaryAssetId: a.stackPrimaryAssetId == a.remoteId ? null : a.stackPrimaryAssetId, - stackCount: a.stackCount, - // isFavorite + isArchived are not set by device-only assets - isFavorite: a.isFavorite, - isArchived: a.isArchived, - isTrashed: a.isTrashed, - isOffline: a.isOffline, - exifInfo: a.exifInfo?.copyWith(assetId: id) ?? exifInfo, - thumbhash: a.thumbhash, - ); - } else { - // add only missing values (and set isLocal to true) - return copyWith( - localId: localId ?? a.localId, - width: width ?? a.width, - height: height ?? a.height, - exifInfo: exifInfo ?? a.exifInfo?.copyWith(assetId: id), // updated to use assetId - ); - } - } - } - - Asset copyWith({ - Id? id, - String? checksum, - String? remoteId, - String? localId, - int? ownerId, - DateTime? fileCreatedAt, - DateTime? fileModifiedAt, - DateTime? updatedAt, - int? durationInSeconds, - AssetType? type, - short? width, - short? height, - String? fileName, - String? livePhotoVideoId, - bool? isFavorite, - bool? isArchived, - bool? isTrashed, - bool? isOffline, - ExifInfo? exifInfo, - String? stackId, - String? stackPrimaryAssetId, - int? stackCount, - String? thumbhash, - AssetVisibilityEnum? visibility, - }) => Asset( - id: id ?? this.id, - checksum: checksum ?? this.checksum, - remoteId: remoteId ?? this.remoteId, - localId: localId ?? this.localId, - ownerId: ownerId ?? this.ownerId, - fileCreatedAt: fileCreatedAt ?? this.fileCreatedAt, - fileModifiedAt: fileModifiedAt ?? this.fileModifiedAt, - updatedAt: updatedAt ?? this.updatedAt, - durationInSeconds: durationInSeconds ?? this.durationInSeconds, - type: type ?? this.type, - width: width ?? this.width, - height: height ?? this.height, - fileName: fileName ?? this.fileName, - livePhotoVideoId: livePhotoVideoId ?? this.livePhotoVideoId, - isFavorite: isFavorite ?? this.isFavorite, - isArchived: isArchived ?? this.isArchived, - isTrashed: isTrashed ?? this.isTrashed, - isOffline: isOffline ?? this.isOffline, - exifInfo: exifInfo ?? this.exifInfo, - stackId: stackId ?? this.stackId, - stackPrimaryAssetId: stackPrimaryAssetId ?? this.stackPrimaryAssetId, - stackCount: stackCount ?? this.stackCount, - thumbhash: thumbhash ?? this.thumbhash, - visibility: visibility ?? this.visibility, - ); - - Future put(Isar db) async { - await db.assets.put(this); - if (exifInfo != null) { - await db.exifInfos.put(entity.ExifInfo.fromDto(exifInfo!.copyWith(assetId: id))); - } - } - - static int compareById(Asset a, Asset b) => a.id.compareTo(b.id); - - static int compareByLocalId(Asset a, Asset b) => compareToNullable(a.localId, b.localId); - - static int compareByChecksum(Asset a, Asset b) => a.checksum.compareTo(b.checksum); - - static int compareByOwnerChecksum(Asset a, Asset b) { - final int ownerIdOrder = a.ownerId.compareTo(b.ownerId); - if (ownerIdOrder != 0) return ownerIdOrder; - return compareByChecksum(a, b); - } - - static int compareByOwnerChecksumCreatedModified(Asset a, Asset b) { - final int ownerIdOrder = a.ownerId.compareTo(b.ownerId); - if (ownerIdOrder != 0) return ownerIdOrder; - final int checksumOrder = compareByChecksum(a, b); - if (checksumOrder != 0) return checksumOrder; - final int createdOrder = a.fileCreatedAt.compareTo(b.fileCreatedAt); - if (createdOrder != 0) return createdOrder; - return a.fileModifiedAt.compareTo(b.fileModifiedAt); - } - - @override - String toString() { - return """ -{ - "id": ${id == Isar.autoIncrement ? '"N/A"' : id}, - "remoteId": "${remoteId ?? "N/A"}", - "localId": "${localId ?? "N/A"}", - "checksum": "$checksum", - "ownerId": $ownerId, - "livePhotoVideoId": "${livePhotoVideoId ?? "N/A"}", - "stackId": "${stackId ?? "N/A"}", - "stackPrimaryAssetId": "${stackPrimaryAssetId ?? "N/A"}", - "stackCount": "$stackCount", - "fileCreatedAt": "$fileCreatedAt", - "fileModifiedAt": "$fileModifiedAt", - "updatedAt": "$updatedAt", - "durationInSeconds": $durationInSeconds, - "type": "$type", - "fileName": "$fileName", - "isFavorite": $isFavorite, - "isRemote": $isRemote, - "storage": "$storage", - "width": ${width ?? "N/A"}, - "height": ${height ?? "N/A"}, - "isArchived": $isArchived, - "isTrashed": $isTrashed, - "isOffline": $isOffline, - "visibility": "$visibility", -}"""; - } - - static getVisibility(AssetVisibility visibility) => switch (visibility) { - AssetVisibility.archive => AssetVisibilityEnum.archive, - AssetVisibility.hidden => AssetVisibilityEnum.hidden, - AssetVisibility.locked => AssetVisibilityEnum.locked, - AssetVisibility.timeline || _ => AssetVisibilityEnum.timeline, - }; -} - -enum AssetType { - // do not change this order! - other, - image, - video, - audio, -} - -extension AssetTypeEnumHelper on AssetTypeEnum { - AssetType toAssetType() => switch (this) { - AssetTypeEnum.IMAGE => AssetType.image, - AssetTypeEnum.VIDEO => AssetType.video, - AssetTypeEnum.AUDIO => AssetType.audio, - AssetTypeEnum.OTHER => AssetType.other, - _ => throw Exception(), - }; -} - -/// Describes where the information of this asset came from: -/// only from the local device, only from the remote server or merged from both -enum AssetState { local, remote, merged } - -extension AssetsHelper on IsarCollection { - Future deleteAllByRemoteId(Iterable ids) => ids.isEmpty ? Future.value(0) : remote(ids).deleteAll(); - Future deleteAllByLocalId(Iterable ids) => ids.isEmpty ? Future.value(0) : local(ids).deleteAll(); - Future> getAllByRemoteId(Iterable ids) => ids.isEmpty ? Future.value([]) : remote(ids).findAll(); - Future> getAllByLocalId(Iterable ids) => ids.isEmpty ? Future.value([]) : local(ids).findAll(); - Future getByRemoteId(String id) => where().remoteIdEqualTo(id).findFirst(); - - QueryBuilder remote(Iterable ids) => - where().anyOf(ids, (q, String e) => q.remoteIdEqualTo(e)); - QueryBuilder local(Iterable ids) { - return where().anyOf(ids, (q, String e) => q.localIdEqualTo(e)); - } -} diff --git a/mobile/lib/entities/asset.entity.g.dart b/mobile/lib/entities/asset.entity.g.dart deleted file mode 100644 index db6bc72331..0000000000 --- a/mobile/lib/entities/asset.entity.g.dart +++ /dev/null @@ -1,3711 +0,0 @@ -// GENERATED CODE - DO NOT MODIFY BY HAND - -part of 'asset.entity.dart'; - -// ************************************************************************** -// IsarCollectionGenerator -// ************************************************************************** - -// coverage:ignore-file -// ignore_for_file: duplicate_ignore, non_constant_identifier_names, constant_identifier_names, invalid_use_of_protected_member, unnecessary_cast, prefer_const_constructors, lines_longer_than_80_chars, require_trailing_commas, inference_failure_on_function_invocation, unnecessary_parenthesis, unnecessary_raw_strings, unnecessary_null_checks, join_return_with_assignment, prefer_final_locals, avoid_js_rounded_ints, avoid_positional_boolean_parameters, always_specify_types - -extension GetAssetCollection on Isar { - IsarCollection get assets => this.collection(); -} - -const AssetSchema = CollectionSchema( - name: r'Asset', - id: -2933289051367723566, - properties: { - r'checksum': PropertySchema( - id: 0, - name: r'checksum', - type: IsarType.string, - ), - r'durationInSeconds': PropertySchema( - id: 1, - name: r'durationInSeconds', - type: IsarType.long, - ), - r'fileCreatedAt': PropertySchema( - id: 2, - name: r'fileCreatedAt', - type: IsarType.dateTime, - ), - r'fileModifiedAt': PropertySchema( - id: 3, - name: r'fileModifiedAt', - type: IsarType.dateTime, - ), - r'fileName': PropertySchema( - id: 4, - name: r'fileName', - type: IsarType.string, - ), - r'height': PropertySchema(id: 5, name: r'height', type: IsarType.int), - r'isArchived': PropertySchema( - id: 6, - name: r'isArchived', - type: IsarType.bool, - ), - r'isFavorite': PropertySchema( - id: 7, - name: r'isFavorite', - type: IsarType.bool, - ), - r'isOffline': PropertySchema( - id: 8, - name: r'isOffline', - type: IsarType.bool, - ), - r'isTrashed': PropertySchema( - id: 9, - name: r'isTrashed', - type: IsarType.bool, - ), - r'livePhotoVideoId': PropertySchema( - id: 10, - name: r'livePhotoVideoId', - type: IsarType.string, - ), - r'localId': PropertySchema(id: 11, name: r'localId', type: IsarType.string), - r'ownerId': PropertySchema(id: 12, name: r'ownerId', type: IsarType.long), - r'remoteId': PropertySchema( - id: 13, - name: r'remoteId', - type: IsarType.string, - ), - r'stackCount': PropertySchema( - id: 14, - name: r'stackCount', - type: IsarType.long, - ), - r'stackId': PropertySchema(id: 15, name: r'stackId', type: IsarType.string), - r'stackPrimaryAssetId': PropertySchema( - id: 16, - name: r'stackPrimaryAssetId', - type: IsarType.string, - ), - r'thumbhash': PropertySchema( - id: 17, - name: r'thumbhash', - type: IsarType.string, - ), - r'type': PropertySchema( - id: 18, - name: r'type', - type: IsarType.byte, - enumMap: _AssettypeEnumValueMap, - ), - r'updatedAt': PropertySchema( - id: 19, - name: r'updatedAt', - type: IsarType.dateTime, - ), - r'visibility': PropertySchema( - id: 20, - name: r'visibility', - type: IsarType.byte, - enumMap: _AssetvisibilityEnumValueMap, - ), - r'width': PropertySchema(id: 21, name: r'width', type: IsarType.int), - }, - - estimateSize: _assetEstimateSize, - serialize: _assetSerialize, - deserialize: _assetDeserialize, - deserializeProp: _assetDeserializeProp, - idName: r'id', - indexes: { - r'remoteId': IndexSchema( - id: 6301175856541681032, - name: r'remoteId', - unique: false, - replace: false, - properties: [ - IndexPropertySchema( - name: r'remoteId', - type: IndexType.hash, - caseSensitive: true, - ), - ], - ), - r'localId': IndexSchema( - id: 1199848425898359622, - name: r'localId', - unique: false, - replace: false, - properties: [ - IndexPropertySchema( - name: r'localId', - type: IndexType.hash, - caseSensitive: true, - ), - ], - ), - r'ownerId_checksum': IndexSchema( - id: -3295822444433175883, - name: r'ownerId_checksum', - unique: true, - replace: false, - properties: [ - IndexPropertySchema( - name: r'ownerId', - type: IndexType.value, - caseSensitive: false, - ), - IndexPropertySchema( - name: r'checksum', - type: IndexType.hash, - caseSensitive: true, - ), - ], - ), - }, - links: {}, - embeddedSchemas: {}, - - getId: _assetGetId, - getLinks: _assetGetLinks, - attach: _assetAttach, - version: '3.3.0-dev.3', -); - -int _assetEstimateSize( - Asset object, - List offsets, - Map> allOffsets, -) { - var bytesCount = offsets.last; - bytesCount += 3 + object.checksum.length * 3; - bytesCount += 3 + object.fileName.length * 3; - { - final value = object.livePhotoVideoId; - if (value != null) { - bytesCount += 3 + value.length * 3; - } - } - { - final value = object.localId; - if (value != null) { - bytesCount += 3 + value.length * 3; - } - } - { - final value = object.remoteId; - if (value != null) { - bytesCount += 3 + value.length * 3; - } - } - { - final value = object.stackId; - if (value != null) { - bytesCount += 3 + value.length * 3; - } - } - { - final value = object.stackPrimaryAssetId; - if (value != null) { - bytesCount += 3 + value.length * 3; - } - } - { - final value = object.thumbhash; - if (value != null) { - bytesCount += 3 + value.length * 3; - } - } - return bytesCount; -} - -void _assetSerialize( - Asset object, - IsarWriter writer, - List offsets, - Map> allOffsets, -) { - writer.writeString(offsets[0], object.checksum); - writer.writeLong(offsets[1], object.durationInSeconds); - writer.writeDateTime(offsets[2], object.fileCreatedAt); - writer.writeDateTime(offsets[3], object.fileModifiedAt); - writer.writeString(offsets[4], object.fileName); - writer.writeInt(offsets[5], object.height); - writer.writeBool(offsets[6], object.isArchived); - writer.writeBool(offsets[7], object.isFavorite); - writer.writeBool(offsets[8], object.isOffline); - writer.writeBool(offsets[9], object.isTrashed); - writer.writeString(offsets[10], object.livePhotoVideoId); - writer.writeString(offsets[11], object.localId); - writer.writeLong(offsets[12], object.ownerId); - writer.writeString(offsets[13], object.remoteId); - writer.writeLong(offsets[14], object.stackCount); - writer.writeString(offsets[15], object.stackId); - writer.writeString(offsets[16], object.stackPrimaryAssetId); - writer.writeString(offsets[17], object.thumbhash); - writer.writeByte(offsets[18], object.type.index); - writer.writeDateTime(offsets[19], object.updatedAt); - writer.writeByte(offsets[20], object.visibility.index); - writer.writeInt(offsets[21], object.width); -} - -Asset _assetDeserialize( - Id id, - IsarReader reader, - List offsets, - Map> allOffsets, -) { - final object = Asset( - checksum: reader.readString(offsets[0]), - durationInSeconds: reader.readLong(offsets[1]), - fileCreatedAt: reader.readDateTime(offsets[2]), - fileModifiedAt: reader.readDateTime(offsets[3]), - fileName: reader.readString(offsets[4]), - height: reader.readIntOrNull(offsets[5]), - id: id, - isArchived: reader.readBoolOrNull(offsets[6]) ?? false, - isFavorite: reader.readBoolOrNull(offsets[7]) ?? false, - isOffline: reader.readBoolOrNull(offsets[8]) ?? false, - isTrashed: reader.readBoolOrNull(offsets[9]) ?? false, - livePhotoVideoId: reader.readStringOrNull(offsets[10]), - localId: reader.readStringOrNull(offsets[11]), - ownerId: reader.readLong(offsets[12]), - remoteId: reader.readStringOrNull(offsets[13]), - stackCount: reader.readLongOrNull(offsets[14]) ?? 0, - stackId: reader.readStringOrNull(offsets[15]), - stackPrimaryAssetId: reader.readStringOrNull(offsets[16]), - thumbhash: reader.readStringOrNull(offsets[17]), - type: - _AssettypeValueEnumMap[reader.readByteOrNull(offsets[18])] ?? - AssetType.other, - updatedAt: reader.readDateTime(offsets[19]), - visibility: - _AssetvisibilityValueEnumMap[reader.readByteOrNull(offsets[20])] ?? - AssetVisibilityEnum.timeline, - width: reader.readIntOrNull(offsets[21]), - ); - return object; -} - -P _assetDeserializeProp

( - IsarReader reader, - int propertyId, - int offset, - Map> allOffsets, -) { - switch (propertyId) { - case 0: - return (reader.readString(offset)) as P; - case 1: - return (reader.readLong(offset)) as P; - case 2: - return (reader.readDateTime(offset)) as P; - case 3: - return (reader.readDateTime(offset)) as P; - case 4: - return (reader.readString(offset)) as P; - case 5: - return (reader.readIntOrNull(offset)) as P; - case 6: - return (reader.readBoolOrNull(offset) ?? false) as P; - case 7: - return (reader.readBoolOrNull(offset) ?? false) as P; - case 8: - return (reader.readBoolOrNull(offset) ?? false) as P; - case 9: - return (reader.readBoolOrNull(offset) ?? false) as P; - case 10: - return (reader.readStringOrNull(offset)) as P; - case 11: - return (reader.readStringOrNull(offset)) as P; - case 12: - return (reader.readLong(offset)) as P; - case 13: - return (reader.readStringOrNull(offset)) as P; - case 14: - return (reader.readLongOrNull(offset) ?? 0) as P; - case 15: - return (reader.readStringOrNull(offset)) as P; - case 16: - return (reader.readStringOrNull(offset)) as P; - case 17: - return (reader.readStringOrNull(offset)) as P; - case 18: - return (_AssettypeValueEnumMap[reader.readByteOrNull(offset)] ?? - AssetType.other) - as P; - case 19: - return (reader.readDateTime(offset)) as P; - case 20: - return (_AssetvisibilityValueEnumMap[reader.readByteOrNull(offset)] ?? - AssetVisibilityEnum.timeline) - as P; - case 21: - return (reader.readIntOrNull(offset)) as P; - default: - throw IsarError('Unknown property with id $propertyId'); - } -} - -const _AssettypeEnumValueMap = {'other': 0, 'image': 1, 'video': 2, 'audio': 3}; -const _AssettypeValueEnumMap = { - 0: AssetType.other, - 1: AssetType.image, - 2: AssetType.video, - 3: AssetType.audio, -}; -const _AssetvisibilityEnumValueMap = { - 'timeline': 0, - 'hidden': 1, - 'archive': 2, - 'locked': 3, -}; -const _AssetvisibilityValueEnumMap = { - 0: AssetVisibilityEnum.timeline, - 1: AssetVisibilityEnum.hidden, - 2: AssetVisibilityEnum.archive, - 3: AssetVisibilityEnum.locked, -}; - -Id _assetGetId(Asset object) { - return object.id; -} - -List> _assetGetLinks(Asset object) { - return []; -} - -void _assetAttach(IsarCollection col, Id id, Asset object) { - object.id = id; -} - -extension AssetByIndex on IsarCollection { - Future getByOwnerIdChecksum(int ownerId, String checksum) { - return getByIndex(r'ownerId_checksum', [ownerId, checksum]); - } - - Asset? getByOwnerIdChecksumSync(int ownerId, String checksum) { - return getByIndexSync(r'ownerId_checksum', [ownerId, checksum]); - } - - Future deleteByOwnerIdChecksum(int ownerId, String checksum) { - return deleteByIndex(r'ownerId_checksum', [ownerId, checksum]); - } - - bool deleteByOwnerIdChecksumSync(int ownerId, String checksum) { - return deleteByIndexSync(r'ownerId_checksum', [ownerId, checksum]); - } - - Future> getAllByOwnerIdChecksum( - List ownerIdValues, - List checksumValues, - ) { - final len = ownerIdValues.length; - assert( - checksumValues.length == len, - 'All index values must have the same length', - ); - final values = >[]; - for (var i = 0; i < len; i++) { - values.add([ownerIdValues[i], checksumValues[i]]); - } - - return getAllByIndex(r'ownerId_checksum', values); - } - - List getAllByOwnerIdChecksumSync( - List ownerIdValues, - List checksumValues, - ) { - final len = ownerIdValues.length; - assert( - checksumValues.length == len, - 'All index values must have the same length', - ); - final values = >[]; - for (var i = 0; i < len; i++) { - values.add([ownerIdValues[i], checksumValues[i]]); - } - - return getAllByIndexSync(r'ownerId_checksum', values); - } - - Future deleteAllByOwnerIdChecksum( - List ownerIdValues, - List checksumValues, - ) { - final len = ownerIdValues.length; - assert( - checksumValues.length == len, - 'All index values must have the same length', - ); - final values = >[]; - for (var i = 0; i < len; i++) { - values.add([ownerIdValues[i], checksumValues[i]]); - } - - return deleteAllByIndex(r'ownerId_checksum', values); - } - - int deleteAllByOwnerIdChecksumSync( - List ownerIdValues, - List checksumValues, - ) { - final len = ownerIdValues.length; - assert( - checksumValues.length == len, - 'All index values must have the same length', - ); - final values = >[]; - for (var i = 0; i < len; i++) { - values.add([ownerIdValues[i], checksumValues[i]]); - } - - return deleteAllByIndexSync(r'ownerId_checksum', values); - } - - Future putByOwnerIdChecksum(Asset object) { - return putByIndex(r'ownerId_checksum', object); - } - - Id putByOwnerIdChecksumSync(Asset object, {bool saveLinks = true}) { - return putByIndexSync(r'ownerId_checksum', object, saveLinks: saveLinks); - } - - Future> putAllByOwnerIdChecksum(List objects) { - return putAllByIndex(r'ownerId_checksum', objects); - } - - List putAllByOwnerIdChecksumSync( - List objects, { - bool saveLinks = true, - }) { - return putAllByIndexSync( - r'ownerId_checksum', - objects, - saveLinks: saveLinks, - ); - } -} - -extension AssetQueryWhereSort on QueryBuilder { - QueryBuilder anyId() { - return QueryBuilder.apply(this, (query) { - return query.addWhereClause(const IdWhereClause.any()); - }); - } -} - -extension AssetQueryWhere on QueryBuilder { - QueryBuilder idEqualTo(Id id) { - return QueryBuilder.apply(this, (query) { - return query.addWhereClause(IdWhereClause.between(lower: id, upper: id)); - }); - } - - QueryBuilder idNotEqualTo(Id id) { - return QueryBuilder.apply(this, (query) { - if (query.whereSort == Sort.asc) { - return query - .addWhereClause( - IdWhereClause.lessThan(upper: id, includeUpper: false), - ) - .addWhereClause( - IdWhereClause.greaterThan(lower: id, includeLower: false), - ); - } else { - return query - .addWhereClause( - IdWhereClause.greaterThan(lower: id, includeLower: false), - ) - .addWhereClause( - IdWhereClause.lessThan(upper: id, includeUpper: false), - ); - } - }); - } - - QueryBuilder idGreaterThan( - Id id, { - bool include = false, - }) { - return QueryBuilder.apply(this, (query) { - return query.addWhereClause( - IdWhereClause.greaterThan(lower: id, includeLower: include), - ); - }); - } - - QueryBuilder idLessThan( - Id id, { - bool include = false, - }) { - return QueryBuilder.apply(this, (query) { - return query.addWhereClause( - IdWhereClause.lessThan(upper: id, includeUpper: include), - ); - }); - } - - QueryBuilder idBetween( - Id lowerId, - Id upperId, { - bool includeLower = true, - bool includeUpper = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addWhereClause( - IdWhereClause.between( - lower: lowerId, - includeLower: includeLower, - upper: upperId, - includeUpper: includeUpper, - ), - ); - }); - } - - QueryBuilder remoteIdIsNull() { - return QueryBuilder.apply(this, (query) { - return query.addWhereClause( - IndexWhereClause.equalTo(indexName: r'remoteId', value: [null]), - ); - }); - } - - QueryBuilder remoteIdIsNotNull() { - return QueryBuilder.apply(this, (query) { - return query.addWhereClause( - IndexWhereClause.between( - indexName: r'remoteId', - lower: [null], - includeLower: false, - upper: [], - ), - ); - }); - } - - QueryBuilder remoteIdEqualTo( - String? remoteId, - ) { - return QueryBuilder.apply(this, (query) { - return query.addWhereClause( - IndexWhereClause.equalTo(indexName: r'remoteId', value: [remoteId]), - ); - }); - } - - QueryBuilder remoteIdNotEqualTo( - String? remoteId, - ) { - return QueryBuilder.apply(this, (query) { - if (query.whereSort == Sort.asc) { - return query - .addWhereClause( - IndexWhereClause.between( - indexName: r'remoteId', - lower: [], - upper: [remoteId], - includeUpper: false, - ), - ) - .addWhereClause( - IndexWhereClause.between( - indexName: r'remoteId', - lower: [remoteId], - includeLower: false, - upper: [], - ), - ); - } else { - return query - .addWhereClause( - IndexWhereClause.between( - indexName: r'remoteId', - lower: [remoteId], - includeLower: false, - upper: [], - ), - ) - .addWhereClause( - IndexWhereClause.between( - indexName: r'remoteId', - lower: [], - upper: [remoteId], - includeUpper: false, - ), - ); - } - }); - } - - QueryBuilder localIdIsNull() { - return QueryBuilder.apply(this, (query) { - return query.addWhereClause( - IndexWhereClause.equalTo(indexName: r'localId', value: [null]), - ); - }); - } - - QueryBuilder localIdIsNotNull() { - return QueryBuilder.apply(this, (query) { - return query.addWhereClause( - IndexWhereClause.between( - indexName: r'localId', - lower: [null], - includeLower: false, - upper: [], - ), - ); - }); - } - - QueryBuilder localIdEqualTo( - String? localId, - ) { - return QueryBuilder.apply(this, (query) { - return query.addWhereClause( - IndexWhereClause.equalTo(indexName: r'localId', value: [localId]), - ); - }); - } - - QueryBuilder localIdNotEqualTo( - String? localId, - ) { - return QueryBuilder.apply(this, (query) { - if (query.whereSort == Sort.asc) { - return query - .addWhereClause( - IndexWhereClause.between( - indexName: r'localId', - lower: [], - upper: [localId], - includeUpper: false, - ), - ) - .addWhereClause( - IndexWhereClause.between( - indexName: r'localId', - lower: [localId], - includeLower: false, - upper: [], - ), - ); - } else { - return query - .addWhereClause( - IndexWhereClause.between( - indexName: r'localId', - lower: [localId], - includeLower: false, - upper: [], - ), - ) - .addWhereClause( - IndexWhereClause.between( - indexName: r'localId', - lower: [], - upper: [localId], - includeUpper: false, - ), - ); - } - }); - } - - QueryBuilder ownerIdEqualToAnyChecksum( - int ownerId, - ) { - return QueryBuilder.apply(this, (query) { - return query.addWhereClause( - IndexWhereClause.equalTo( - indexName: r'ownerId_checksum', - value: [ownerId], - ), - ); - }); - } - - QueryBuilder ownerIdNotEqualToAnyChecksum( - int ownerId, - ) { - return QueryBuilder.apply(this, (query) { - if (query.whereSort == Sort.asc) { - return query - .addWhereClause( - IndexWhereClause.between( - indexName: r'ownerId_checksum', - lower: [], - upper: [ownerId], - includeUpper: false, - ), - ) - .addWhereClause( - IndexWhereClause.between( - indexName: r'ownerId_checksum', - lower: [ownerId], - includeLower: false, - upper: [], - ), - ); - } else { - return query - .addWhereClause( - IndexWhereClause.between( - indexName: r'ownerId_checksum', - lower: [ownerId], - includeLower: false, - upper: [], - ), - ) - .addWhereClause( - IndexWhereClause.between( - indexName: r'ownerId_checksum', - lower: [], - upper: [ownerId], - includeUpper: false, - ), - ); - } - }); - } - - QueryBuilder ownerIdGreaterThanAnyChecksum( - int ownerId, { - bool include = false, - }) { - return QueryBuilder.apply(this, (query) { - return query.addWhereClause( - IndexWhereClause.between( - indexName: r'ownerId_checksum', - lower: [ownerId], - includeLower: include, - upper: [], - ), - ); - }); - } - - QueryBuilder ownerIdLessThanAnyChecksum( - int ownerId, { - bool include = false, - }) { - return QueryBuilder.apply(this, (query) { - return query.addWhereClause( - IndexWhereClause.between( - indexName: r'ownerId_checksum', - lower: [], - upper: [ownerId], - includeUpper: include, - ), - ); - }); - } - - QueryBuilder ownerIdBetweenAnyChecksum( - int lowerOwnerId, - int upperOwnerId, { - bool includeLower = true, - bool includeUpper = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addWhereClause( - IndexWhereClause.between( - indexName: r'ownerId_checksum', - lower: [lowerOwnerId], - includeLower: includeLower, - upper: [upperOwnerId], - includeUpper: includeUpper, - ), - ); - }); - } - - QueryBuilder ownerIdChecksumEqualTo( - int ownerId, - String checksum, - ) { - return QueryBuilder.apply(this, (query) { - return query.addWhereClause( - IndexWhereClause.equalTo( - indexName: r'ownerId_checksum', - value: [ownerId, checksum], - ), - ); - }); - } - - QueryBuilder - ownerIdEqualToChecksumNotEqualTo(int ownerId, String checksum) { - return QueryBuilder.apply(this, (query) { - if (query.whereSort == Sort.asc) { - return query - .addWhereClause( - IndexWhereClause.between( - indexName: r'ownerId_checksum', - lower: [ownerId], - upper: [ownerId, checksum], - includeUpper: false, - ), - ) - .addWhereClause( - IndexWhereClause.between( - indexName: r'ownerId_checksum', - lower: [ownerId, checksum], - includeLower: false, - upper: [ownerId], - ), - ); - } else { - return query - .addWhereClause( - IndexWhereClause.between( - indexName: r'ownerId_checksum', - lower: [ownerId, checksum], - includeLower: false, - upper: [ownerId], - ), - ) - .addWhereClause( - IndexWhereClause.between( - indexName: r'ownerId_checksum', - lower: [ownerId], - upper: [ownerId, checksum], - includeUpper: false, - ), - ); - } - }); - } -} - -extension AssetQueryFilter on QueryBuilder { - QueryBuilder checksumEqualTo( - String value, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.equalTo( - property: r'checksum', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder checksumGreaterThan( - String value, { - bool include = false, - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.greaterThan( - include: include, - property: r'checksum', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder checksumLessThan( - String value, { - bool include = false, - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.lessThan( - include: include, - property: r'checksum', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder checksumBetween( - String lower, - String upper, { - bool includeLower = true, - bool includeUpper = true, - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.between( - property: r'checksum', - lower: lower, - includeLower: includeLower, - upper: upper, - includeUpper: includeUpper, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder checksumStartsWith( - String value, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.startsWith( - property: r'checksum', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder checksumEndsWith( - String value, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.endsWith( - property: r'checksum', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder checksumContains( - String value, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.contains( - property: r'checksum', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder checksumMatches( - String pattern, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.matches( - property: r'checksum', - wildcard: pattern, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder checksumIsEmpty() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.equalTo(property: r'checksum', value: ''), - ); - }); - } - - QueryBuilder checksumIsNotEmpty() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.greaterThan(property: r'checksum', value: ''), - ); - }); - } - - QueryBuilder durationInSecondsEqualTo( - int value, - ) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.equalTo(property: r'durationInSeconds', value: value), - ); - }); - } - - QueryBuilder - durationInSecondsGreaterThan(int value, {bool include = false}) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.greaterThan( - include: include, - property: r'durationInSeconds', - value: value, - ), - ); - }); - } - - QueryBuilder durationInSecondsLessThan( - int value, { - bool include = false, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.lessThan( - include: include, - property: r'durationInSeconds', - value: value, - ), - ); - }); - } - - QueryBuilder durationInSecondsBetween( - int lower, - int upper, { - bool includeLower = true, - bool includeUpper = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.between( - property: r'durationInSeconds', - lower: lower, - includeLower: includeLower, - upper: upper, - includeUpper: includeUpper, - ), - ); - }); - } - - QueryBuilder fileCreatedAtEqualTo( - DateTime value, - ) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.equalTo(property: r'fileCreatedAt', value: value), - ); - }); - } - - QueryBuilder fileCreatedAtGreaterThan( - DateTime value, { - bool include = false, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.greaterThan( - include: include, - property: r'fileCreatedAt', - value: value, - ), - ); - }); - } - - QueryBuilder fileCreatedAtLessThan( - DateTime value, { - bool include = false, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.lessThan( - include: include, - property: r'fileCreatedAt', - value: value, - ), - ); - }); - } - - QueryBuilder fileCreatedAtBetween( - DateTime lower, - DateTime upper, { - bool includeLower = true, - bool includeUpper = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.between( - property: r'fileCreatedAt', - lower: lower, - includeLower: includeLower, - upper: upper, - includeUpper: includeUpper, - ), - ); - }); - } - - QueryBuilder fileModifiedAtEqualTo( - DateTime value, - ) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.equalTo(property: r'fileModifiedAt', value: value), - ); - }); - } - - QueryBuilder fileModifiedAtGreaterThan( - DateTime value, { - bool include = false, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.greaterThan( - include: include, - property: r'fileModifiedAt', - value: value, - ), - ); - }); - } - - QueryBuilder fileModifiedAtLessThan( - DateTime value, { - bool include = false, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.lessThan( - include: include, - property: r'fileModifiedAt', - value: value, - ), - ); - }); - } - - QueryBuilder fileModifiedAtBetween( - DateTime lower, - DateTime upper, { - bool includeLower = true, - bool includeUpper = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.between( - property: r'fileModifiedAt', - lower: lower, - includeLower: includeLower, - upper: upper, - includeUpper: includeUpper, - ), - ); - }); - } - - QueryBuilder fileNameEqualTo( - String value, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.equalTo( - property: r'fileName', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder fileNameGreaterThan( - String value, { - bool include = false, - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.greaterThan( - include: include, - property: r'fileName', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder fileNameLessThan( - String value, { - bool include = false, - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.lessThan( - include: include, - property: r'fileName', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder fileNameBetween( - String lower, - String upper, { - bool includeLower = true, - bool includeUpper = true, - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.between( - property: r'fileName', - lower: lower, - includeLower: includeLower, - upper: upper, - includeUpper: includeUpper, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder fileNameStartsWith( - String value, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.startsWith( - property: r'fileName', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder fileNameEndsWith( - String value, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.endsWith( - property: r'fileName', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder fileNameContains( - String value, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.contains( - property: r'fileName', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder fileNameMatches( - String pattern, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.matches( - property: r'fileName', - wildcard: pattern, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder fileNameIsEmpty() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.equalTo(property: r'fileName', value: ''), - ); - }); - } - - QueryBuilder fileNameIsNotEmpty() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.greaterThan(property: r'fileName', value: ''), - ); - }); - } - - QueryBuilder heightIsNull() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - const FilterCondition.isNull(property: r'height'), - ); - }); - } - - QueryBuilder heightIsNotNull() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - const FilterCondition.isNotNull(property: r'height'), - ); - }); - } - - QueryBuilder heightEqualTo(int? value) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.equalTo(property: r'height', value: value), - ); - }); - } - - QueryBuilder heightGreaterThan( - int? value, { - bool include = false, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.greaterThan( - include: include, - property: r'height', - value: value, - ), - ); - }); - } - - QueryBuilder heightLessThan( - int? value, { - bool include = false, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.lessThan( - include: include, - property: r'height', - value: value, - ), - ); - }); - } - - QueryBuilder heightBetween( - int? lower, - int? upper, { - bool includeLower = true, - bool includeUpper = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.between( - property: r'height', - lower: lower, - includeLower: includeLower, - upper: upper, - includeUpper: includeUpper, - ), - ); - }); - } - - QueryBuilder idEqualTo(Id value) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.equalTo(property: r'id', value: value), - ); - }); - } - - QueryBuilder idGreaterThan( - Id value, { - bool include = false, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.greaterThan( - include: include, - property: r'id', - value: value, - ), - ); - }); - } - - QueryBuilder idLessThan( - Id value, { - bool include = false, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.lessThan( - include: include, - property: r'id', - value: value, - ), - ); - }); - } - - QueryBuilder idBetween( - Id lower, - Id upper, { - bool includeLower = true, - bool includeUpper = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.between( - property: r'id', - lower: lower, - includeLower: includeLower, - upper: upper, - includeUpper: includeUpper, - ), - ); - }); - } - - QueryBuilder isArchivedEqualTo( - bool value, - ) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.equalTo(property: r'isArchived', value: value), - ); - }); - } - - QueryBuilder isFavoriteEqualTo( - bool value, - ) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.equalTo(property: r'isFavorite', value: value), - ); - }); - } - - QueryBuilder isOfflineEqualTo( - bool value, - ) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.equalTo(property: r'isOffline', value: value), - ); - }); - } - - QueryBuilder isTrashedEqualTo( - bool value, - ) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.equalTo(property: r'isTrashed', value: value), - ); - }); - } - - QueryBuilder livePhotoVideoIdIsNull() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - const FilterCondition.isNull(property: r'livePhotoVideoId'), - ); - }); - } - - QueryBuilder - livePhotoVideoIdIsNotNull() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - const FilterCondition.isNotNull(property: r'livePhotoVideoId'), - ); - }); - } - - QueryBuilder livePhotoVideoIdEqualTo( - String? value, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.equalTo( - property: r'livePhotoVideoId', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder livePhotoVideoIdGreaterThan( - String? value, { - bool include = false, - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.greaterThan( - include: include, - property: r'livePhotoVideoId', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder livePhotoVideoIdLessThan( - String? value, { - bool include = false, - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.lessThan( - include: include, - property: r'livePhotoVideoId', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder livePhotoVideoIdBetween( - String? lower, - String? upper, { - bool includeLower = true, - bool includeUpper = true, - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.between( - property: r'livePhotoVideoId', - lower: lower, - includeLower: includeLower, - upper: upper, - includeUpper: includeUpper, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder livePhotoVideoIdStartsWith( - String value, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.startsWith( - property: r'livePhotoVideoId', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder livePhotoVideoIdEndsWith( - String value, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.endsWith( - property: r'livePhotoVideoId', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder livePhotoVideoIdContains( - String value, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.contains( - property: r'livePhotoVideoId', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder livePhotoVideoIdMatches( - String pattern, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.matches( - property: r'livePhotoVideoId', - wildcard: pattern, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder livePhotoVideoIdIsEmpty() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.equalTo(property: r'livePhotoVideoId', value: ''), - ); - }); - } - - QueryBuilder - livePhotoVideoIdIsNotEmpty() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.greaterThan(property: r'livePhotoVideoId', value: ''), - ); - }); - } - - QueryBuilder localIdIsNull() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - const FilterCondition.isNull(property: r'localId'), - ); - }); - } - - QueryBuilder localIdIsNotNull() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - const FilterCondition.isNotNull(property: r'localId'), - ); - }); - } - - QueryBuilder localIdEqualTo( - String? value, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.equalTo( - property: r'localId', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder localIdGreaterThan( - String? value, { - bool include = false, - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.greaterThan( - include: include, - property: r'localId', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder localIdLessThan( - String? value, { - bool include = false, - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.lessThan( - include: include, - property: r'localId', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder localIdBetween( - String? lower, - String? upper, { - bool includeLower = true, - bool includeUpper = true, - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.between( - property: r'localId', - lower: lower, - includeLower: includeLower, - upper: upper, - includeUpper: includeUpper, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder localIdStartsWith( - String value, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.startsWith( - property: r'localId', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder localIdEndsWith( - String value, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.endsWith( - property: r'localId', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder localIdContains( - String value, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.contains( - property: r'localId', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder localIdMatches( - String pattern, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.matches( - property: r'localId', - wildcard: pattern, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder localIdIsEmpty() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.equalTo(property: r'localId', value: ''), - ); - }); - } - - QueryBuilder localIdIsNotEmpty() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.greaterThan(property: r'localId', value: ''), - ); - }); - } - - QueryBuilder ownerIdEqualTo(int value) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.equalTo(property: r'ownerId', value: value), - ); - }); - } - - QueryBuilder ownerIdGreaterThan( - int value, { - bool include = false, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.greaterThan( - include: include, - property: r'ownerId', - value: value, - ), - ); - }); - } - - QueryBuilder ownerIdLessThan( - int value, { - bool include = false, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.lessThan( - include: include, - property: r'ownerId', - value: value, - ), - ); - }); - } - - QueryBuilder ownerIdBetween( - int lower, - int upper, { - bool includeLower = true, - bool includeUpper = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.between( - property: r'ownerId', - lower: lower, - includeLower: includeLower, - upper: upper, - includeUpper: includeUpper, - ), - ); - }); - } - - QueryBuilder remoteIdIsNull() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - const FilterCondition.isNull(property: r'remoteId'), - ); - }); - } - - QueryBuilder remoteIdIsNotNull() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - const FilterCondition.isNotNull(property: r'remoteId'), - ); - }); - } - - QueryBuilder remoteIdEqualTo( - String? value, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.equalTo( - property: r'remoteId', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder remoteIdGreaterThan( - String? value, { - bool include = false, - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.greaterThan( - include: include, - property: r'remoteId', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder remoteIdLessThan( - String? value, { - bool include = false, - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.lessThan( - include: include, - property: r'remoteId', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder remoteIdBetween( - String? lower, - String? upper, { - bool includeLower = true, - bool includeUpper = true, - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.between( - property: r'remoteId', - lower: lower, - includeLower: includeLower, - upper: upper, - includeUpper: includeUpper, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder remoteIdStartsWith( - String value, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.startsWith( - property: r'remoteId', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder remoteIdEndsWith( - String value, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.endsWith( - property: r'remoteId', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder remoteIdContains( - String value, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.contains( - property: r'remoteId', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder remoteIdMatches( - String pattern, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.matches( - property: r'remoteId', - wildcard: pattern, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder remoteIdIsEmpty() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.equalTo(property: r'remoteId', value: ''), - ); - }); - } - - QueryBuilder remoteIdIsNotEmpty() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.greaterThan(property: r'remoteId', value: ''), - ); - }); - } - - QueryBuilder stackCountEqualTo( - int value, - ) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.equalTo(property: r'stackCount', value: value), - ); - }); - } - - QueryBuilder stackCountGreaterThan( - int value, { - bool include = false, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.greaterThan( - include: include, - property: r'stackCount', - value: value, - ), - ); - }); - } - - QueryBuilder stackCountLessThan( - int value, { - bool include = false, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.lessThan( - include: include, - property: r'stackCount', - value: value, - ), - ); - }); - } - - QueryBuilder stackCountBetween( - int lower, - int upper, { - bool includeLower = true, - bool includeUpper = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.between( - property: r'stackCount', - lower: lower, - includeLower: includeLower, - upper: upper, - includeUpper: includeUpper, - ), - ); - }); - } - - QueryBuilder stackIdIsNull() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - const FilterCondition.isNull(property: r'stackId'), - ); - }); - } - - QueryBuilder stackIdIsNotNull() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - const FilterCondition.isNotNull(property: r'stackId'), - ); - }); - } - - QueryBuilder stackIdEqualTo( - String? value, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.equalTo( - property: r'stackId', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder stackIdGreaterThan( - String? value, { - bool include = false, - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.greaterThan( - include: include, - property: r'stackId', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder stackIdLessThan( - String? value, { - bool include = false, - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.lessThan( - include: include, - property: r'stackId', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder stackIdBetween( - String? lower, - String? upper, { - bool includeLower = true, - bool includeUpper = true, - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.between( - property: r'stackId', - lower: lower, - includeLower: includeLower, - upper: upper, - includeUpper: includeUpper, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder stackIdStartsWith( - String value, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.startsWith( - property: r'stackId', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder stackIdEndsWith( - String value, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.endsWith( - property: r'stackId', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder stackIdContains( - String value, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.contains( - property: r'stackId', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder stackIdMatches( - String pattern, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.matches( - property: r'stackId', - wildcard: pattern, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder stackIdIsEmpty() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.equalTo(property: r'stackId', value: ''), - ); - }); - } - - QueryBuilder stackIdIsNotEmpty() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.greaterThan(property: r'stackId', value: ''), - ); - }); - } - - QueryBuilder - stackPrimaryAssetIdIsNull() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - const FilterCondition.isNull(property: r'stackPrimaryAssetId'), - ); - }); - } - - QueryBuilder - stackPrimaryAssetIdIsNotNull() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - const FilterCondition.isNotNull(property: r'stackPrimaryAssetId'), - ); - }); - } - - QueryBuilder stackPrimaryAssetIdEqualTo( - String? value, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.equalTo( - property: r'stackPrimaryAssetId', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder - stackPrimaryAssetIdGreaterThan( - String? value, { - bool include = false, - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.greaterThan( - include: include, - property: r'stackPrimaryAssetId', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder stackPrimaryAssetIdLessThan( - String? value, { - bool include = false, - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.lessThan( - include: include, - property: r'stackPrimaryAssetId', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder stackPrimaryAssetIdBetween( - String? lower, - String? upper, { - bool includeLower = true, - bool includeUpper = true, - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.between( - property: r'stackPrimaryAssetId', - lower: lower, - includeLower: includeLower, - upper: upper, - includeUpper: includeUpper, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder - stackPrimaryAssetIdStartsWith(String value, {bool caseSensitive = true}) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.startsWith( - property: r'stackPrimaryAssetId', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder stackPrimaryAssetIdEndsWith( - String value, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.endsWith( - property: r'stackPrimaryAssetId', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder stackPrimaryAssetIdContains( - String value, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.contains( - property: r'stackPrimaryAssetId', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder stackPrimaryAssetIdMatches( - String pattern, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.matches( - property: r'stackPrimaryAssetId', - wildcard: pattern, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder - stackPrimaryAssetIdIsEmpty() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.equalTo(property: r'stackPrimaryAssetId', value: ''), - ); - }); - } - - QueryBuilder - stackPrimaryAssetIdIsNotEmpty() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.greaterThan( - property: r'stackPrimaryAssetId', - value: '', - ), - ); - }); - } - - QueryBuilder thumbhashIsNull() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - const FilterCondition.isNull(property: r'thumbhash'), - ); - }); - } - - QueryBuilder thumbhashIsNotNull() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - const FilterCondition.isNotNull(property: r'thumbhash'), - ); - }); - } - - QueryBuilder thumbhashEqualTo( - String? value, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.equalTo( - property: r'thumbhash', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder thumbhashGreaterThan( - String? value, { - bool include = false, - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.greaterThan( - include: include, - property: r'thumbhash', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder thumbhashLessThan( - String? value, { - bool include = false, - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.lessThan( - include: include, - property: r'thumbhash', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder thumbhashBetween( - String? lower, - String? upper, { - bool includeLower = true, - bool includeUpper = true, - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.between( - property: r'thumbhash', - lower: lower, - includeLower: includeLower, - upper: upper, - includeUpper: includeUpper, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder thumbhashStartsWith( - String value, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.startsWith( - property: r'thumbhash', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder thumbhashEndsWith( - String value, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.endsWith( - property: r'thumbhash', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder thumbhashContains( - String value, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.contains( - property: r'thumbhash', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder thumbhashMatches( - String pattern, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.matches( - property: r'thumbhash', - wildcard: pattern, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder thumbhashIsEmpty() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.equalTo(property: r'thumbhash', value: ''), - ); - }); - } - - QueryBuilder thumbhashIsNotEmpty() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.greaterThan(property: r'thumbhash', value: ''), - ); - }); - } - - QueryBuilder typeEqualTo( - AssetType value, - ) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.equalTo(property: r'type', value: value), - ); - }); - } - - QueryBuilder typeGreaterThan( - AssetType value, { - bool include = false, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.greaterThan( - include: include, - property: r'type', - value: value, - ), - ); - }); - } - - QueryBuilder typeLessThan( - AssetType value, { - bool include = false, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.lessThan( - include: include, - property: r'type', - value: value, - ), - ); - }); - } - - QueryBuilder typeBetween( - AssetType lower, - AssetType upper, { - bool includeLower = true, - bool includeUpper = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.between( - property: r'type', - lower: lower, - includeLower: includeLower, - upper: upper, - includeUpper: includeUpper, - ), - ); - }); - } - - QueryBuilder updatedAtEqualTo( - DateTime value, - ) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.equalTo(property: r'updatedAt', value: value), - ); - }); - } - - QueryBuilder updatedAtGreaterThan( - DateTime value, { - bool include = false, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.greaterThan( - include: include, - property: r'updatedAt', - value: value, - ), - ); - }); - } - - QueryBuilder updatedAtLessThan( - DateTime value, { - bool include = false, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.lessThan( - include: include, - property: r'updatedAt', - value: value, - ), - ); - }); - } - - QueryBuilder updatedAtBetween( - DateTime lower, - DateTime upper, { - bool includeLower = true, - bool includeUpper = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.between( - property: r'updatedAt', - lower: lower, - includeLower: includeLower, - upper: upper, - includeUpper: includeUpper, - ), - ); - }); - } - - QueryBuilder visibilityEqualTo( - AssetVisibilityEnum value, - ) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.equalTo(property: r'visibility', value: value), - ); - }); - } - - QueryBuilder visibilityGreaterThan( - AssetVisibilityEnum value, { - bool include = false, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.greaterThan( - include: include, - property: r'visibility', - value: value, - ), - ); - }); - } - - QueryBuilder visibilityLessThan( - AssetVisibilityEnum value, { - bool include = false, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.lessThan( - include: include, - property: r'visibility', - value: value, - ), - ); - }); - } - - QueryBuilder visibilityBetween( - AssetVisibilityEnum lower, - AssetVisibilityEnum upper, { - bool includeLower = true, - bool includeUpper = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.between( - property: r'visibility', - lower: lower, - includeLower: includeLower, - upper: upper, - includeUpper: includeUpper, - ), - ); - }); - } - - QueryBuilder widthIsNull() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - const FilterCondition.isNull(property: r'width'), - ); - }); - } - - QueryBuilder widthIsNotNull() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - const FilterCondition.isNotNull(property: r'width'), - ); - }); - } - - QueryBuilder widthEqualTo(int? value) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.equalTo(property: r'width', value: value), - ); - }); - } - - QueryBuilder widthGreaterThan( - int? value, { - bool include = false, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.greaterThan( - include: include, - property: r'width', - value: value, - ), - ); - }); - } - - QueryBuilder widthLessThan( - int? value, { - bool include = false, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.lessThan( - include: include, - property: r'width', - value: value, - ), - ); - }); - } - - QueryBuilder widthBetween( - int? lower, - int? upper, { - bool includeLower = true, - bool includeUpper = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.between( - property: r'width', - lower: lower, - includeLower: includeLower, - upper: upper, - includeUpper: includeUpper, - ), - ); - }); - } -} - -extension AssetQueryObject on QueryBuilder {} - -extension AssetQueryLinks on QueryBuilder {} - -extension AssetQuerySortBy on QueryBuilder { - QueryBuilder sortByChecksum() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'checksum', Sort.asc); - }); - } - - QueryBuilder sortByChecksumDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'checksum', Sort.desc); - }); - } - - QueryBuilder sortByDurationInSeconds() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'durationInSeconds', Sort.asc); - }); - } - - QueryBuilder sortByDurationInSecondsDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'durationInSeconds', Sort.desc); - }); - } - - QueryBuilder sortByFileCreatedAt() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'fileCreatedAt', Sort.asc); - }); - } - - QueryBuilder sortByFileCreatedAtDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'fileCreatedAt', Sort.desc); - }); - } - - QueryBuilder sortByFileModifiedAt() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'fileModifiedAt', Sort.asc); - }); - } - - QueryBuilder sortByFileModifiedAtDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'fileModifiedAt', Sort.desc); - }); - } - - QueryBuilder sortByFileName() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'fileName', Sort.asc); - }); - } - - QueryBuilder sortByFileNameDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'fileName', Sort.desc); - }); - } - - QueryBuilder sortByHeight() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'height', Sort.asc); - }); - } - - QueryBuilder sortByHeightDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'height', Sort.desc); - }); - } - - QueryBuilder sortByIsArchived() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'isArchived', Sort.asc); - }); - } - - QueryBuilder sortByIsArchivedDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'isArchived', Sort.desc); - }); - } - - QueryBuilder sortByIsFavorite() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'isFavorite', Sort.asc); - }); - } - - QueryBuilder sortByIsFavoriteDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'isFavorite', Sort.desc); - }); - } - - QueryBuilder sortByIsOffline() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'isOffline', Sort.asc); - }); - } - - QueryBuilder sortByIsOfflineDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'isOffline', Sort.desc); - }); - } - - QueryBuilder sortByIsTrashed() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'isTrashed', Sort.asc); - }); - } - - QueryBuilder sortByIsTrashedDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'isTrashed', Sort.desc); - }); - } - - QueryBuilder sortByLivePhotoVideoId() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'livePhotoVideoId', Sort.asc); - }); - } - - QueryBuilder sortByLivePhotoVideoIdDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'livePhotoVideoId', Sort.desc); - }); - } - - QueryBuilder sortByLocalId() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'localId', Sort.asc); - }); - } - - QueryBuilder sortByLocalIdDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'localId', Sort.desc); - }); - } - - QueryBuilder sortByOwnerId() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'ownerId', Sort.asc); - }); - } - - QueryBuilder sortByOwnerIdDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'ownerId', Sort.desc); - }); - } - - QueryBuilder sortByRemoteId() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'remoteId', Sort.asc); - }); - } - - QueryBuilder sortByRemoteIdDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'remoteId', Sort.desc); - }); - } - - QueryBuilder sortByStackCount() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'stackCount', Sort.asc); - }); - } - - QueryBuilder sortByStackCountDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'stackCount', Sort.desc); - }); - } - - QueryBuilder sortByStackId() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'stackId', Sort.asc); - }); - } - - QueryBuilder sortByStackIdDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'stackId', Sort.desc); - }); - } - - QueryBuilder sortByStackPrimaryAssetId() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'stackPrimaryAssetId', Sort.asc); - }); - } - - QueryBuilder sortByStackPrimaryAssetIdDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'stackPrimaryAssetId', Sort.desc); - }); - } - - QueryBuilder sortByThumbhash() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'thumbhash', Sort.asc); - }); - } - - QueryBuilder sortByThumbhashDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'thumbhash', Sort.desc); - }); - } - - QueryBuilder sortByType() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'type', Sort.asc); - }); - } - - QueryBuilder sortByTypeDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'type', Sort.desc); - }); - } - - QueryBuilder sortByUpdatedAt() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'updatedAt', Sort.asc); - }); - } - - QueryBuilder sortByUpdatedAtDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'updatedAt', Sort.desc); - }); - } - - QueryBuilder sortByVisibility() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'visibility', Sort.asc); - }); - } - - QueryBuilder sortByVisibilityDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'visibility', Sort.desc); - }); - } - - QueryBuilder sortByWidth() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'width', Sort.asc); - }); - } - - QueryBuilder sortByWidthDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'width', Sort.desc); - }); - } -} - -extension AssetQuerySortThenBy on QueryBuilder { - QueryBuilder thenByChecksum() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'checksum', Sort.asc); - }); - } - - QueryBuilder thenByChecksumDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'checksum', Sort.desc); - }); - } - - QueryBuilder thenByDurationInSeconds() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'durationInSeconds', Sort.asc); - }); - } - - QueryBuilder thenByDurationInSecondsDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'durationInSeconds', Sort.desc); - }); - } - - QueryBuilder thenByFileCreatedAt() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'fileCreatedAt', Sort.asc); - }); - } - - QueryBuilder thenByFileCreatedAtDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'fileCreatedAt', Sort.desc); - }); - } - - QueryBuilder thenByFileModifiedAt() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'fileModifiedAt', Sort.asc); - }); - } - - QueryBuilder thenByFileModifiedAtDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'fileModifiedAt', Sort.desc); - }); - } - - QueryBuilder thenByFileName() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'fileName', Sort.asc); - }); - } - - QueryBuilder thenByFileNameDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'fileName', Sort.desc); - }); - } - - QueryBuilder thenByHeight() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'height', Sort.asc); - }); - } - - QueryBuilder thenByHeightDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'height', Sort.desc); - }); - } - - QueryBuilder thenById() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'id', Sort.asc); - }); - } - - QueryBuilder thenByIdDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'id', Sort.desc); - }); - } - - QueryBuilder thenByIsArchived() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'isArchived', Sort.asc); - }); - } - - QueryBuilder thenByIsArchivedDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'isArchived', Sort.desc); - }); - } - - QueryBuilder thenByIsFavorite() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'isFavorite', Sort.asc); - }); - } - - QueryBuilder thenByIsFavoriteDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'isFavorite', Sort.desc); - }); - } - - QueryBuilder thenByIsOffline() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'isOffline', Sort.asc); - }); - } - - QueryBuilder thenByIsOfflineDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'isOffline', Sort.desc); - }); - } - - QueryBuilder thenByIsTrashed() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'isTrashed', Sort.asc); - }); - } - - QueryBuilder thenByIsTrashedDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'isTrashed', Sort.desc); - }); - } - - QueryBuilder thenByLivePhotoVideoId() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'livePhotoVideoId', Sort.asc); - }); - } - - QueryBuilder thenByLivePhotoVideoIdDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'livePhotoVideoId', Sort.desc); - }); - } - - QueryBuilder thenByLocalId() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'localId', Sort.asc); - }); - } - - QueryBuilder thenByLocalIdDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'localId', Sort.desc); - }); - } - - QueryBuilder thenByOwnerId() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'ownerId', Sort.asc); - }); - } - - QueryBuilder thenByOwnerIdDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'ownerId', Sort.desc); - }); - } - - QueryBuilder thenByRemoteId() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'remoteId', Sort.asc); - }); - } - - QueryBuilder thenByRemoteIdDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'remoteId', Sort.desc); - }); - } - - QueryBuilder thenByStackCount() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'stackCount', Sort.asc); - }); - } - - QueryBuilder thenByStackCountDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'stackCount', Sort.desc); - }); - } - - QueryBuilder thenByStackId() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'stackId', Sort.asc); - }); - } - - QueryBuilder thenByStackIdDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'stackId', Sort.desc); - }); - } - - QueryBuilder thenByStackPrimaryAssetId() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'stackPrimaryAssetId', Sort.asc); - }); - } - - QueryBuilder thenByStackPrimaryAssetIdDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'stackPrimaryAssetId', Sort.desc); - }); - } - - QueryBuilder thenByThumbhash() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'thumbhash', Sort.asc); - }); - } - - QueryBuilder thenByThumbhashDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'thumbhash', Sort.desc); - }); - } - - QueryBuilder thenByType() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'type', Sort.asc); - }); - } - - QueryBuilder thenByTypeDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'type', Sort.desc); - }); - } - - QueryBuilder thenByUpdatedAt() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'updatedAt', Sort.asc); - }); - } - - QueryBuilder thenByUpdatedAtDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'updatedAt', Sort.desc); - }); - } - - QueryBuilder thenByVisibility() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'visibility', Sort.asc); - }); - } - - QueryBuilder thenByVisibilityDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'visibility', Sort.desc); - }); - } - - QueryBuilder thenByWidth() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'width', Sort.asc); - }); - } - - QueryBuilder thenByWidthDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'width', Sort.desc); - }); - } -} - -extension AssetQueryWhereDistinct on QueryBuilder { - QueryBuilder distinctByChecksum({ - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addDistinctBy(r'checksum', caseSensitive: caseSensitive); - }); - } - - QueryBuilder distinctByDurationInSeconds() { - return QueryBuilder.apply(this, (query) { - return query.addDistinctBy(r'durationInSeconds'); - }); - } - - QueryBuilder distinctByFileCreatedAt() { - return QueryBuilder.apply(this, (query) { - return query.addDistinctBy(r'fileCreatedAt'); - }); - } - - QueryBuilder distinctByFileModifiedAt() { - return QueryBuilder.apply(this, (query) { - return query.addDistinctBy(r'fileModifiedAt'); - }); - } - - QueryBuilder distinctByFileName({ - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addDistinctBy(r'fileName', caseSensitive: caseSensitive); - }); - } - - QueryBuilder distinctByHeight() { - return QueryBuilder.apply(this, (query) { - return query.addDistinctBy(r'height'); - }); - } - - QueryBuilder distinctByIsArchived() { - return QueryBuilder.apply(this, (query) { - return query.addDistinctBy(r'isArchived'); - }); - } - - QueryBuilder distinctByIsFavorite() { - return QueryBuilder.apply(this, (query) { - return query.addDistinctBy(r'isFavorite'); - }); - } - - QueryBuilder distinctByIsOffline() { - return QueryBuilder.apply(this, (query) { - return query.addDistinctBy(r'isOffline'); - }); - } - - QueryBuilder distinctByIsTrashed() { - return QueryBuilder.apply(this, (query) { - return query.addDistinctBy(r'isTrashed'); - }); - } - - QueryBuilder distinctByLivePhotoVideoId({ - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addDistinctBy( - r'livePhotoVideoId', - caseSensitive: caseSensitive, - ); - }); - } - - QueryBuilder distinctByLocalId({ - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addDistinctBy(r'localId', caseSensitive: caseSensitive); - }); - } - - QueryBuilder distinctByOwnerId() { - return QueryBuilder.apply(this, (query) { - return query.addDistinctBy(r'ownerId'); - }); - } - - QueryBuilder distinctByRemoteId({ - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addDistinctBy(r'remoteId', caseSensitive: caseSensitive); - }); - } - - QueryBuilder distinctByStackCount() { - return QueryBuilder.apply(this, (query) { - return query.addDistinctBy(r'stackCount'); - }); - } - - QueryBuilder distinctByStackId({ - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addDistinctBy(r'stackId', caseSensitive: caseSensitive); - }); - } - - QueryBuilder distinctByStackPrimaryAssetId({ - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addDistinctBy( - r'stackPrimaryAssetId', - caseSensitive: caseSensitive, - ); - }); - } - - QueryBuilder distinctByThumbhash({ - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addDistinctBy(r'thumbhash', caseSensitive: caseSensitive); - }); - } - - QueryBuilder distinctByType() { - return QueryBuilder.apply(this, (query) { - return query.addDistinctBy(r'type'); - }); - } - - QueryBuilder distinctByUpdatedAt() { - return QueryBuilder.apply(this, (query) { - return query.addDistinctBy(r'updatedAt'); - }); - } - - QueryBuilder distinctByVisibility() { - return QueryBuilder.apply(this, (query) { - return query.addDistinctBy(r'visibility'); - }); - } - - QueryBuilder distinctByWidth() { - return QueryBuilder.apply(this, (query) { - return query.addDistinctBy(r'width'); - }); - } -} - -extension AssetQueryProperty on QueryBuilder { - QueryBuilder idProperty() { - return QueryBuilder.apply(this, (query) { - return query.addPropertyName(r'id'); - }); - } - - QueryBuilder checksumProperty() { - return QueryBuilder.apply(this, (query) { - return query.addPropertyName(r'checksum'); - }); - } - - QueryBuilder durationInSecondsProperty() { - return QueryBuilder.apply(this, (query) { - return query.addPropertyName(r'durationInSeconds'); - }); - } - - QueryBuilder fileCreatedAtProperty() { - return QueryBuilder.apply(this, (query) { - return query.addPropertyName(r'fileCreatedAt'); - }); - } - - QueryBuilder fileModifiedAtProperty() { - return QueryBuilder.apply(this, (query) { - return query.addPropertyName(r'fileModifiedAt'); - }); - } - - QueryBuilder fileNameProperty() { - return QueryBuilder.apply(this, (query) { - return query.addPropertyName(r'fileName'); - }); - } - - QueryBuilder heightProperty() { - return QueryBuilder.apply(this, (query) { - return query.addPropertyName(r'height'); - }); - } - - QueryBuilder isArchivedProperty() { - return QueryBuilder.apply(this, (query) { - return query.addPropertyName(r'isArchived'); - }); - } - - QueryBuilder isFavoriteProperty() { - return QueryBuilder.apply(this, (query) { - return query.addPropertyName(r'isFavorite'); - }); - } - - QueryBuilder isOfflineProperty() { - return QueryBuilder.apply(this, (query) { - return query.addPropertyName(r'isOffline'); - }); - } - - QueryBuilder isTrashedProperty() { - return QueryBuilder.apply(this, (query) { - return query.addPropertyName(r'isTrashed'); - }); - } - - QueryBuilder livePhotoVideoIdProperty() { - return QueryBuilder.apply(this, (query) { - return query.addPropertyName(r'livePhotoVideoId'); - }); - } - - QueryBuilder localIdProperty() { - return QueryBuilder.apply(this, (query) { - return query.addPropertyName(r'localId'); - }); - } - - QueryBuilder ownerIdProperty() { - return QueryBuilder.apply(this, (query) { - return query.addPropertyName(r'ownerId'); - }); - } - - QueryBuilder remoteIdProperty() { - return QueryBuilder.apply(this, (query) { - return query.addPropertyName(r'remoteId'); - }); - } - - QueryBuilder stackCountProperty() { - return QueryBuilder.apply(this, (query) { - return query.addPropertyName(r'stackCount'); - }); - } - - QueryBuilder stackIdProperty() { - return QueryBuilder.apply(this, (query) { - return query.addPropertyName(r'stackId'); - }); - } - - QueryBuilder stackPrimaryAssetIdProperty() { - return QueryBuilder.apply(this, (query) { - return query.addPropertyName(r'stackPrimaryAssetId'); - }); - } - - QueryBuilder thumbhashProperty() { - return QueryBuilder.apply(this, (query) { - return query.addPropertyName(r'thumbhash'); - }); - } - - QueryBuilder typeProperty() { - return QueryBuilder.apply(this, (query) { - return query.addPropertyName(r'type'); - }); - } - - QueryBuilder updatedAtProperty() { - return QueryBuilder.apply(this, (query) { - return query.addPropertyName(r'updatedAt'); - }); - } - - QueryBuilder - visibilityProperty() { - return QueryBuilder.apply(this, (query) { - return query.addPropertyName(r'visibility'); - }); - } - - QueryBuilder widthProperty() { - return QueryBuilder.apply(this, (query) { - return query.addPropertyName(r'width'); - }); - } -} diff --git a/mobile/lib/entities/backup_album.entity.dart b/mobile/lib/entities/backup_album.entity.dart deleted file mode 100644 index ad2a5d6718..0000000000 --- a/mobile/lib/entities/backup_album.entity.dart +++ /dev/null @@ -1,22 +0,0 @@ -import 'package:immich_mobile/utils/hash.dart'; -import 'package:isar/isar.dart'; - -part 'backup_album.entity.g.dart'; - -@Collection(inheritance: false) -class BackupAlbum { - String id; - DateTime lastBackup; - @Enumerated(EnumType.ordinal) - BackupSelection selection; - - BackupAlbum(this.id, this.lastBackup, this.selection); - - Id get isarId => fastHash(id); - - BackupAlbum copyWith({String? id, DateTime? lastBackup, BackupSelection? selection}) { - return BackupAlbum(id ?? this.id, lastBackup ?? this.lastBackup, selection ?? this.selection); - } -} - -enum BackupSelection { none, select, exclude } diff --git a/mobile/lib/entities/backup_album.entity.g.dart b/mobile/lib/entities/backup_album.entity.g.dart deleted file mode 100644 index 583aa55c4d..0000000000 --- a/mobile/lib/entities/backup_album.entity.g.dart +++ /dev/null @@ -1,679 +0,0 @@ -// GENERATED CODE - DO NOT MODIFY BY HAND - -part of 'backup_album.entity.dart'; - -// ************************************************************************** -// IsarCollectionGenerator -// ************************************************************************** - -// coverage:ignore-file -// ignore_for_file: duplicate_ignore, non_constant_identifier_names, constant_identifier_names, invalid_use_of_protected_member, unnecessary_cast, prefer_const_constructors, lines_longer_than_80_chars, require_trailing_commas, inference_failure_on_function_invocation, unnecessary_parenthesis, unnecessary_raw_strings, unnecessary_null_checks, join_return_with_assignment, prefer_final_locals, avoid_js_rounded_ints, avoid_positional_boolean_parameters, always_specify_types - -extension GetBackupAlbumCollection on Isar { - IsarCollection get backupAlbums => this.collection(); -} - -const BackupAlbumSchema = CollectionSchema( - name: r'BackupAlbum', - id: 8308487201128361847, - properties: { - r'id': PropertySchema(id: 0, name: r'id', type: IsarType.string), - r'lastBackup': PropertySchema( - id: 1, - name: r'lastBackup', - type: IsarType.dateTime, - ), - r'selection': PropertySchema( - id: 2, - name: r'selection', - type: IsarType.byte, - enumMap: _BackupAlbumselectionEnumValueMap, - ), - }, - - estimateSize: _backupAlbumEstimateSize, - serialize: _backupAlbumSerialize, - deserialize: _backupAlbumDeserialize, - deserializeProp: _backupAlbumDeserializeProp, - idName: r'isarId', - indexes: {}, - links: {}, - embeddedSchemas: {}, - - getId: _backupAlbumGetId, - getLinks: _backupAlbumGetLinks, - attach: _backupAlbumAttach, - version: '3.3.0-dev.3', -); - -int _backupAlbumEstimateSize( - BackupAlbum object, - List offsets, - Map> allOffsets, -) { - var bytesCount = offsets.last; - bytesCount += 3 + object.id.length * 3; - return bytesCount; -} - -void _backupAlbumSerialize( - BackupAlbum object, - IsarWriter writer, - List offsets, - Map> allOffsets, -) { - writer.writeString(offsets[0], object.id); - writer.writeDateTime(offsets[1], object.lastBackup); - writer.writeByte(offsets[2], object.selection.index); -} - -BackupAlbum _backupAlbumDeserialize( - Id id, - IsarReader reader, - List offsets, - Map> allOffsets, -) { - final object = BackupAlbum( - reader.readString(offsets[0]), - reader.readDateTime(offsets[1]), - _BackupAlbumselectionValueEnumMap[reader.readByteOrNull(offsets[2])] ?? - BackupSelection.none, - ); - return object; -} - -P _backupAlbumDeserializeProp

( - IsarReader reader, - int propertyId, - int offset, - Map> allOffsets, -) { - switch (propertyId) { - case 0: - return (reader.readString(offset)) as P; - case 1: - return (reader.readDateTime(offset)) as P; - case 2: - return (_BackupAlbumselectionValueEnumMap[reader.readByteOrNull( - offset, - )] ?? - BackupSelection.none) - as P; - default: - throw IsarError('Unknown property with id $propertyId'); - } -} - -const _BackupAlbumselectionEnumValueMap = { - 'none': 0, - 'select': 1, - 'exclude': 2, -}; -const _BackupAlbumselectionValueEnumMap = { - 0: BackupSelection.none, - 1: BackupSelection.select, - 2: BackupSelection.exclude, -}; - -Id _backupAlbumGetId(BackupAlbum object) { - return object.isarId; -} - -List> _backupAlbumGetLinks(BackupAlbum object) { - return []; -} - -void _backupAlbumAttach( - IsarCollection col, - Id id, - BackupAlbum object, -) {} - -extension BackupAlbumQueryWhereSort - on QueryBuilder { - QueryBuilder anyIsarId() { - return QueryBuilder.apply(this, (query) { - return query.addWhereClause(const IdWhereClause.any()); - }); - } -} - -extension BackupAlbumQueryWhere - on QueryBuilder { - QueryBuilder isarIdEqualTo( - Id isarId, - ) { - return QueryBuilder.apply(this, (query) { - return query.addWhereClause( - IdWhereClause.between(lower: isarId, upper: isarId), - ); - }); - } - - QueryBuilder isarIdNotEqualTo( - Id isarId, - ) { - return QueryBuilder.apply(this, (query) { - if (query.whereSort == Sort.asc) { - return query - .addWhereClause( - IdWhereClause.lessThan(upper: isarId, includeUpper: false), - ) - .addWhereClause( - IdWhereClause.greaterThan(lower: isarId, includeLower: false), - ); - } else { - return query - .addWhereClause( - IdWhereClause.greaterThan(lower: isarId, includeLower: false), - ) - .addWhereClause( - IdWhereClause.lessThan(upper: isarId, includeUpper: false), - ); - } - }); - } - - QueryBuilder isarIdGreaterThan( - Id isarId, { - bool include = false, - }) { - return QueryBuilder.apply(this, (query) { - return query.addWhereClause( - IdWhereClause.greaterThan(lower: isarId, includeLower: include), - ); - }); - } - - QueryBuilder isarIdLessThan( - Id isarId, { - bool include = false, - }) { - return QueryBuilder.apply(this, (query) { - return query.addWhereClause( - IdWhereClause.lessThan(upper: isarId, includeUpper: include), - ); - }); - } - - QueryBuilder isarIdBetween( - Id lowerIsarId, - Id upperIsarId, { - bool includeLower = true, - bool includeUpper = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addWhereClause( - IdWhereClause.between( - lower: lowerIsarId, - includeLower: includeLower, - upper: upperIsarId, - includeUpper: includeUpper, - ), - ); - }); - } -} - -extension BackupAlbumQueryFilter - on QueryBuilder { - QueryBuilder idEqualTo( - String value, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.equalTo( - property: r'id', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder idGreaterThan( - String value, { - bool include = false, - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.greaterThan( - include: include, - property: r'id', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder idLessThan( - String value, { - bool include = false, - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.lessThan( - include: include, - property: r'id', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder idBetween( - String lower, - String upper, { - bool includeLower = true, - bool includeUpper = true, - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.between( - property: r'id', - lower: lower, - includeLower: includeLower, - upper: upper, - includeUpper: includeUpper, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder idStartsWith( - String value, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.startsWith( - property: r'id', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder idEndsWith( - String value, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.endsWith( - property: r'id', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder idContains( - String value, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.contains( - property: r'id', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder idMatches( - String pattern, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.matches( - property: r'id', - wildcard: pattern, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder idIsEmpty() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.equalTo(property: r'id', value: ''), - ); - }); - } - - QueryBuilder idIsNotEmpty() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.greaterThan(property: r'id', value: ''), - ); - }); - } - - QueryBuilder isarIdEqualTo( - Id value, - ) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.equalTo(property: r'isarId', value: value), - ); - }); - } - - QueryBuilder - isarIdGreaterThan(Id value, {bool include = false}) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.greaterThan( - include: include, - property: r'isarId', - value: value, - ), - ); - }); - } - - QueryBuilder isarIdLessThan( - Id value, { - bool include = false, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.lessThan( - include: include, - property: r'isarId', - value: value, - ), - ); - }); - } - - QueryBuilder isarIdBetween( - Id lower, - Id upper, { - bool includeLower = true, - bool includeUpper = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.between( - property: r'isarId', - lower: lower, - includeLower: includeLower, - upper: upper, - includeUpper: includeUpper, - ), - ); - }); - } - - QueryBuilder - lastBackupEqualTo(DateTime value) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.equalTo(property: r'lastBackup', value: value), - ); - }); - } - - QueryBuilder - lastBackupGreaterThan(DateTime value, {bool include = false}) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.greaterThan( - include: include, - property: r'lastBackup', - value: value, - ), - ); - }); - } - - QueryBuilder - lastBackupLessThan(DateTime value, {bool include = false}) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.lessThan( - include: include, - property: r'lastBackup', - value: value, - ), - ); - }); - } - - QueryBuilder - lastBackupBetween( - DateTime lower, - DateTime upper, { - bool includeLower = true, - bool includeUpper = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.between( - property: r'lastBackup', - lower: lower, - includeLower: includeLower, - upper: upper, - includeUpper: includeUpper, - ), - ); - }); - } - - QueryBuilder - selectionEqualTo(BackupSelection value) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.equalTo(property: r'selection', value: value), - ); - }); - } - - QueryBuilder - selectionGreaterThan(BackupSelection value, {bool include = false}) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.greaterThan( - include: include, - property: r'selection', - value: value, - ), - ); - }); - } - - QueryBuilder - selectionLessThan(BackupSelection value, {bool include = false}) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.lessThan( - include: include, - property: r'selection', - value: value, - ), - ); - }); - } - - QueryBuilder - selectionBetween( - BackupSelection lower, - BackupSelection upper, { - bool includeLower = true, - bool includeUpper = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.between( - property: r'selection', - lower: lower, - includeLower: includeLower, - upper: upper, - includeUpper: includeUpper, - ), - ); - }); - } -} - -extension BackupAlbumQueryObject - on QueryBuilder {} - -extension BackupAlbumQueryLinks - on QueryBuilder {} - -extension BackupAlbumQuerySortBy - on QueryBuilder { - QueryBuilder sortById() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'id', Sort.asc); - }); - } - - QueryBuilder sortByIdDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'id', Sort.desc); - }); - } - - QueryBuilder sortByLastBackup() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'lastBackup', Sort.asc); - }); - } - - QueryBuilder sortByLastBackupDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'lastBackup', Sort.desc); - }); - } - - QueryBuilder sortBySelection() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'selection', Sort.asc); - }); - } - - QueryBuilder sortBySelectionDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'selection', Sort.desc); - }); - } -} - -extension BackupAlbumQuerySortThenBy - on QueryBuilder { - QueryBuilder thenById() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'id', Sort.asc); - }); - } - - QueryBuilder thenByIdDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'id', Sort.desc); - }); - } - - QueryBuilder thenByIsarId() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'isarId', Sort.asc); - }); - } - - QueryBuilder thenByIsarIdDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'isarId', Sort.desc); - }); - } - - QueryBuilder thenByLastBackup() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'lastBackup', Sort.asc); - }); - } - - QueryBuilder thenByLastBackupDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'lastBackup', Sort.desc); - }); - } - - QueryBuilder thenBySelection() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'selection', Sort.asc); - }); - } - - QueryBuilder thenBySelectionDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'selection', Sort.desc); - }); - } -} - -extension BackupAlbumQueryWhereDistinct - on QueryBuilder { - QueryBuilder distinctById({ - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addDistinctBy(r'id', caseSensitive: caseSensitive); - }); - } - - QueryBuilder distinctByLastBackup() { - return QueryBuilder.apply(this, (query) { - return query.addDistinctBy(r'lastBackup'); - }); - } - - QueryBuilder distinctBySelection() { - return QueryBuilder.apply(this, (query) { - return query.addDistinctBy(r'selection'); - }); - } -} - -extension BackupAlbumQueryProperty - on QueryBuilder { - QueryBuilder isarIdProperty() { - return QueryBuilder.apply(this, (query) { - return query.addPropertyName(r'isarId'); - }); - } - - QueryBuilder idProperty() { - return QueryBuilder.apply(this, (query) { - return query.addPropertyName(r'id'); - }); - } - - QueryBuilder lastBackupProperty() { - return QueryBuilder.apply(this, (query) { - return query.addPropertyName(r'lastBackup'); - }); - } - - QueryBuilder - selectionProperty() { - return QueryBuilder.apply(this, (query) { - return query.addPropertyName(r'selection'); - }); - } -} diff --git a/mobile/lib/entities/device_asset.entity.dart b/mobile/lib/entities/device_asset.entity.dart deleted file mode 100644 index 0973dd4ff8..0000000000 --- a/mobile/lib/entities/device_asset.entity.dart +++ /dev/null @@ -1,8 +0,0 @@ -import 'package:isar/isar.dart'; - -class DeviceAsset { - DeviceAsset({required this.hash}); - - @Index(unique: false, type: IndexType.hash) - List hash; -} diff --git a/mobile/lib/entities/duplicated_asset.entity.dart b/mobile/lib/entities/duplicated_asset.entity.dart deleted file mode 100644 index 9368dc1a52..0000000000 --- a/mobile/lib/entities/duplicated_asset.entity.dart +++ /dev/null @@ -1,11 +0,0 @@ -import 'package:immich_mobile/utils/hash.dart'; -import 'package:isar/isar.dart'; - -part 'duplicated_asset.entity.g.dart'; - -@Collection(inheritance: false) -class DuplicatedAsset { - String id; - DuplicatedAsset(this.id); - Id get isarId => fastHash(id); -} diff --git a/mobile/lib/entities/duplicated_asset.entity.g.dart b/mobile/lib/entities/duplicated_asset.entity.g.dart deleted file mode 100644 index 80d2f344e6..0000000000 --- a/mobile/lib/entities/duplicated_asset.entity.g.dart +++ /dev/null @@ -1,444 +0,0 @@ -// GENERATED CODE - DO NOT MODIFY BY HAND - -part of 'duplicated_asset.entity.dart'; - -// ************************************************************************** -// IsarCollectionGenerator -// ************************************************************************** - -// coverage:ignore-file -// ignore_for_file: duplicate_ignore, non_constant_identifier_names, constant_identifier_names, invalid_use_of_protected_member, unnecessary_cast, prefer_const_constructors, lines_longer_than_80_chars, require_trailing_commas, inference_failure_on_function_invocation, unnecessary_parenthesis, unnecessary_raw_strings, unnecessary_null_checks, join_return_with_assignment, prefer_final_locals, avoid_js_rounded_ints, avoid_positional_boolean_parameters, always_specify_types - -extension GetDuplicatedAssetCollection on Isar { - IsarCollection get duplicatedAssets => this.collection(); -} - -const DuplicatedAssetSchema = CollectionSchema( - name: r'DuplicatedAsset', - id: -2679334728174694496, - properties: { - r'id': PropertySchema(id: 0, name: r'id', type: IsarType.string), - }, - - estimateSize: _duplicatedAssetEstimateSize, - serialize: _duplicatedAssetSerialize, - deserialize: _duplicatedAssetDeserialize, - deserializeProp: _duplicatedAssetDeserializeProp, - idName: r'isarId', - indexes: {}, - links: {}, - embeddedSchemas: {}, - - getId: _duplicatedAssetGetId, - getLinks: _duplicatedAssetGetLinks, - attach: _duplicatedAssetAttach, - version: '3.3.0-dev.3', -); - -int _duplicatedAssetEstimateSize( - DuplicatedAsset object, - List offsets, - Map> allOffsets, -) { - var bytesCount = offsets.last; - bytesCount += 3 + object.id.length * 3; - return bytesCount; -} - -void _duplicatedAssetSerialize( - DuplicatedAsset object, - IsarWriter writer, - List offsets, - Map> allOffsets, -) { - writer.writeString(offsets[0], object.id); -} - -DuplicatedAsset _duplicatedAssetDeserialize( - Id id, - IsarReader reader, - List offsets, - Map> allOffsets, -) { - final object = DuplicatedAsset(reader.readString(offsets[0])); - return object; -} - -P _duplicatedAssetDeserializeProp

( - IsarReader reader, - int propertyId, - int offset, - Map> allOffsets, -) { - switch (propertyId) { - case 0: - return (reader.readString(offset)) as P; - default: - throw IsarError('Unknown property with id $propertyId'); - } -} - -Id _duplicatedAssetGetId(DuplicatedAsset object) { - return object.isarId; -} - -List> _duplicatedAssetGetLinks(DuplicatedAsset object) { - return []; -} - -void _duplicatedAssetAttach( - IsarCollection col, - Id id, - DuplicatedAsset object, -) {} - -extension DuplicatedAssetQueryWhereSort - on QueryBuilder { - QueryBuilder anyIsarId() { - return QueryBuilder.apply(this, (query) { - return query.addWhereClause(const IdWhereClause.any()); - }); - } -} - -extension DuplicatedAssetQueryWhere - on QueryBuilder { - QueryBuilder - isarIdEqualTo(Id isarId) { - return QueryBuilder.apply(this, (query) { - return query.addWhereClause( - IdWhereClause.between(lower: isarId, upper: isarId), - ); - }); - } - - QueryBuilder - isarIdNotEqualTo(Id isarId) { - return QueryBuilder.apply(this, (query) { - if (query.whereSort == Sort.asc) { - return query - .addWhereClause( - IdWhereClause.lessThan(upper: isarId, includeUpper: false), - ) - .addWhereClause( - IdWhereClause.greaterThan(lower: isarId, includeLower: false), - ); - } else { - return query - .addWhereClause( - IdWhereClause.greaterThan(lower: isarId, includeLower: false), - ) - .addWhereClause( - IdWhereClause.lessThan(upper: isarId, includeUpper: false), - ); - } - }); - } - - QueryBuilder - isarIdGreaterThan(Id isarId, {bool include = false}) { - return QueryBuilder.apply(this, (query) { - return query.addWhereClause( - IdWhereClause.greaterThan(lower: isarId, includeLower: include), - ); - }); - } - - QueryBuilder - isarIdLessThan(Id isarId, {bool include = false}) { - return QueryBuilder.apply(this, (query) { - return query.addWhereClause( - IdWhereClause.lessThan(upper: isarId, includeUpper: include), - ); - }); - } - - QueryBuilder - isarIdBetween( - Id lowerIsarId, - Id upperIsarId, { - bool includeLower = true, - bool includeUpper = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addWhereClause( - IdWhereClause.between( - lower: lowerIsarId, - includeLower: includeLower, - upper: upperIsarId, - includeUpper: includeUpper, - ), - ); - }); - } -} - -extension DuplicatedAssetQueryFilter - on QueryBuilder { - QueryBuilder - idEqualTo(String value, {bool caseSensitive = true}) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.equalTo( - property: r'id', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder - idGreaterThan( - String value, { - bool include = false, - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.greaterThan( - include: include, - property: r'id', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder - idLessThan(String value, {bool include = false, bool caseSensitive = true}) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.lessThan( - include: include, - property: r'id', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder - idBetween( - String lower, - String upper, { - bool includeLower = true, - bool includeUpper = true, - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.between( - property: r'id', - lower: lower, - includeLower: includeLower, - upper: upper, - includeUpper: includeUpper, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder - idStartsWith(String value, {bool caseSensitive = true}) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.startsWith( - property: r'id', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder - idEndsWith(String value, {bool caseSensitive = true}) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.endsWith( - property: r'id', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder - idContains(String value, {bool caseSensitive = true}) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.contains( - property: r'id', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder - idMatches(String pattern, {bool caseSensitive = true}) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.matches( - property: r'id', - wildcard: pattern, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder - idIsEmpty() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.equalTo(property: r'id', value: ''), - ); - }); - } - - QueryBuilder - idIsNotEmpty() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.greaterThan(property: r'id', value: ''), - ); - }); - } - - QueryBuilder - isarIdEqualTo(Id value) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.equalTo(property: r'isarId', value: value), - ); - }); - } - - QueryBuilder - isarIdGreaterThan(Id value, {bool include = false}) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.greaterThan( - include: include, - property: r'isarId', - value: value, - ), - ); - }); - } - - QueryBuilder - isarIdLessThan(Id value, {bool include = false}) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.lessThan( - include: include, - property: r'isarId', - value: value, - ), - ); - }); - } - - QueryBuilder - isarIdBetween( - Id lower, - Id upper, { - bool includeLower = true, - bool includeUpper = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.between( - property: r'isarId', - lower: lower, - includeLower: includeLower, - upper: upper, - includeUpper: includeUpper, - ), - ); - }); - } -} - -extension DuplicatedAssetQueryObject - on QueryBuilder {} - -extension DuplicatedAssetQueryLinks - on QueryBuilder {} - -extension DuplicatedAssetQuerySortBy - on QueryBuilder { - QueryBuilder sortById() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'id', Sort.asc); - }); - } - - QueryBuilder sortByIdDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'id', Sort.desc); - }); - } -} - -extension DuplicatedAssetQuerySortThenBy - on QueryBuilder { - QueryBuilder thenById() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'id', Sort.asc); - }); - } - - QueryBuilder thenByIdDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'id', Sort.desc); - }); - } - - QueryBuilder thenByIsarId() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'isarId', Sort.asc); - }); - } - - QueryBuilder - thenByIsarIdDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'isarId', Sort.desc); - }); - } -} - -extension DuplicatedAssetQueryWhereDistinct - on QueryBuilder { - QueryBuilder distinctById({ - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addDistinctBy(r'id', caseSensitive: caseSensitive); - }); - } -} - -extension DuplicatedAssetQueryProperty - on QueryBuilder { - QueryBuilder isarIdProperty() { - return QueryBuilder.apply(this, (query) { - return query.addPropertyName(r'isarId'); - }); - } - - QueryBuilder idProperty() { - return QueryBuilder.apply(this, (query) { - return query.addPropertyName(r'id'); - }); - } -} diff --git a/mobile/lib/entities/etag.entity.dart b/mobile/lib/entities/etag.entity.dart deleted file mode 100644 index 3b8ef39c61..0000000000 --- a/mobile/lib/entities/etag.entity.dart +++ /dev/null @@ -1,14 +0,0 @@ -import 'package:immich_mobile/utils/hash.dart'; -import 'package:isar/isar.dart'; - -part 'etag.entity.g.dart'; - -@Collection(inheritance: false) -class ETag { - ETag({required this.id, this.assetCount, this.time}); - Id get isarId => fastHash(id); - @Index(unique: true, replace: true, type: IndexType.hash) - String id; - int? assetCount; - DateTime? time; -} diff --git a/mobile/lib/entities/etag.entity.g.dart b/mobile/lib/entities/etag.entity.g.dart deleted file mode 100644 index 03b4ea9918..0000000000 --- a/mobile/lib/entities/etag.entity.g.dart +++ /dev/null @@ -1,796 +0,0 @@ -// GENERATED CODE - DO NOT MODIFY BY HAND - -part of 'etag.entity.dart'; - -// ************************************************************************** -// IsarCollectionGenerator -// ************************************************************************** - -// coverage:ignore-file -// ignore_for_file: duplicate_ignore, non_constant_identifier_names, constant_identifier_names, invalid_use_of_protected_member, unnecessary_cast, prefer_const_constructors, lines_longer_than_80_chars, require_trailing_commas, inference_failure_on_function_invocation, unnecessary_parenthesis, unnecessary_raw_strings, unnecessary_null_checks, join_return_with_assignment, prefer_final_locals, avoid_js_rounded_ints, avoid_positional_boolean_parameters, always_specify_types - -extension GetETagCollection on Isar { - IsarCollection get eTags => this.collection(); -} - -const ETagSchema = CollectionSchema( - name: r'ETag', - id: -644290296585643859, - properties: { - r'assetCount': PropertySchema( - id: 0, - name: r'assetCount', - type: IsarType.long, - ), - r'id': PropertySchema(id: 1, name: r'id', type: IsarType.string), - r'time': PropertySchema(id: 2, name: r'time', type: IsarType.dateTime), - }, - - estimateSize: _eTagEstimateSize, - serialize: _eTagSerialize, - deserialize: _eTagDeserialize, - deserializeProp: _eTagDeserializeProp, - idName: r'isarId', - indexes: { - r'id': IndexSchema( - id: -3268401673993471357, - name: r'id', - unique: true, - replace: true, - properties: [ - IndexPropertySchema( - name: r'id', - type: IndexType.hash, - caseSensitive: true, - ), - ], - ), - }, - links: {}, - embeddedSchemas: {}, - - getId: _eTagGetId, - getLinks: _eTagGetLinks, - attach: _eTagAttach, - version: '3.3.0-dev.3', -); - -int _eTagEstimateSize( - ETag object, - List offsets, - Map> allOffsets, -) { - var bytesCount = offsets.last; - bytesCount += 3 + object.id.length * 3; - return bytesCount; -} - -void _eTagSerialize( - ETag object, - IsarWriter writer, - List offsets, - Map> allOffsets, -) { - writer.writeLong(offsets[0], object.assetCount); - writer.writeString(offsets[1], object.id); - writer.writeDateTime(offsets[2], object.time); -} - -ETag _eTagDeserialize( - Id id, - IsarReader reader, - List offsets, - Map> allOffsets, -) { - final object = ETag( - assetCount: reader.readLongOrNull(offsets[0]), - id: reader.readString(offsets[1]), - time: reader.readDateTimeOrNull(offsets[2]), - ); - return object; -} - -P _eTagDeserializeProp

( - IsarReader reader, - int propertyId, - int offset, - Map> allOffsets, -) { - switch (propertyId) { - case 0: - return (reader.readLongOrNull(offset)) as P; - case 1: - return (reader.readString(offset)) as P; - case 2: - return (reader.readDateTimeOrNull(offset)) as P; - default: - throw IsarError('Unknown property with id $propertyId'); - } -} - -Id _eTagGetId(ETag object) { - return object.isarId; -} - -List> _eTagGetLinks(ETag object) { - return []; -} - -void _eTagAttach(IsarCollection col, Id id, ETag object) {} - -extension ETagByIndex on IsarCollection { - Future getById(String id) { - return getByIndex(r'id', [id]); - } - - ETag? getByIdSync(String id) { - return getByIndexSync(r'id', [id]); - } - - Future deleteById(String id) { - return deleteByIndex(r'id', [id]); - } - - bool deleteByIdSync(String id) { - return deleteByIndexSync(r'id', [id]); - } - - Future> getAllById(List idValues) { - final values = idValues.map((e) => [e]).toList(); - return getAllByIndex(r'id', values); - } - - List getAllByIdSync(List idValues) { - final values = idValues.map((e) => [e]).toList(); - return getAllByIndexSync(r'id', values); - } - - Future deleteAllById(List idValues) { - final values = idValues.map((e) => [e]).toList(); - return deleteAllByIndex(r'id', values); - } - - int deleteAllByIdSync(List idValues) { - final values = idValues.map((e) => [e]).toList(); - return deleteAllByIndexSync(r'id', values); - } - - Future putById(ETag object) { - return putByIndex(r'id', object); - } - - Id putByIdSync(ETag object, {bool saveLinks = true}) { - return putByIndexSync(r'id', object, saveLinks: saveLinks); - } - - Future> putAllById(List objects) { - return putAllByIndex(r'id', objects); - } - - List putAllByIdSync(List objects, {bool saveLinks = true}) { - return putAllByIndexSync(r'id', objects, saveLinks: saveLinks); - } -} - -extension ETagQueryWhereSort on QueryBuilder { - QueryBuilder anyIsarId() { - return QueryBuilder.apply(this, (query) { - return query.addWhereClause(const IdWhereClause.any()); - }); - } -} - -extension ETagQueryWhere on QueryBuilder { - QueryBuilder isarIdEqualTo(Id isarId) { - return QueryBuilder.apply(this, (query) { - return query.addWhereClause( - IdWhereClause.between(lower: isarId, upper: isarId), - ); - }); - } - - QueryBuilder isarIdNotEqualTo(Id isarId) { - return QueryBuilder.apply(this, (query) { - if (query.whereSort == Sort.asc) { - return query - .addWhereClause( - IdWhereClause.lessThan(upper: isarId, includeUpper: false), - ) - .addWhereClause( - IdWhereClause.greaterThan(lower: isarId, includeLower: false), - ); - } else { - return query - .addWhereClause( - IdWhereClause.greaterThan(lower: isarId, includeLower: false), - ) - .addWhereClause( - IdWhereClause.lessThan(upper: isarId, includeUpper: false), - ); - } - }); - } - - QueryBuilder isarIdGreaterThan( - Id isarId, { - bool include = false, - }) { - return QueryBuilder.apply(this, (query) { - return query.addWhereClause( - IdWhereClause.greaterThan(lower: isarId, includeLower: include), - ); - }); - } - - QueryBuilder isarIdLessThan( - Id isarId, { - bool include = false, - }) { - return QueryBuilder.apply(this, (query) { - return query.addWhereClause( - IdWhereClause.lessThan(upper: isarId, includeUpper: include), - ); - }); - } - - QueryBuilder isarIdBetween( - Id lowerIsarId, - Id upperIsarId, { - bool includeLower = true, - bool includeUpper = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addWhereClause( - IdWhereClause.between( - lower: lowerIsarId, - includeLower: includeLower, - upper: upperIsarId, - includeUpper: includeUpper, - ), - ); - }); - } - - QueryBuilder idEqualTo(String id) { - return QueryBuilder.apply(this, (query) { - return query.addWhereClause( - IndexWhereClause.equalTo(indexName: r'id', value: [id]), - ); - }); - } - - QueryBuilder idNotEqualTo(String id) { - return QueryBuilder.apply(this, (query) { - if (query.whereSort == Sort.asc) { - return query - .addWhereClause( - IndexWhereClause.between( - indexName: r'id', - lower: [], - upper: [id], - includeUpper: false, - ), - ) - .addWhereClause( - IndexWhereClause.between( - indexName: r'id', - lower: [id], - includeLower: false, - upper: [], - ), - ); - } else { - return query - .addWhereClause( - IndexWhereClause.between( - indexName: r'id', - lower: [id], - includeLower: false, - upper: [], - ), - ) - .addWhereClause( - IndexWhereClause.between( - indexName: r'id', - lower: [], - upper: [id], - includeUpper: false, - ), - ); - } - }); - } -} - -extension ETagQueryFilter on QueryBuilder { - QueryBuilder assetCountIsNull() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - const FilterCondition.isNull(property: r'assetCount'), - ); - }); - } - - QueryBuilder assetCountIsNotNull() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - const FilterCondition.isNotNull(property: r'assetCount'), - ); - }); - } - - QueryBuilder assetCountEqualTo( - int? value, - ) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.equalTo(property: r'assetCount', value: value), - ); - }); - } - - QueryBuilder assetCountGreaterThan( - int? value, { - bool include = false, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.greaterThan( - include: include, - property: r'assetCount', - value: value, - ), - ); - }); - } - - QueryBuilder assetCountLessThan( - int? value, { - bool include = false, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.lessThan( - include: include, - property: r'assetCount', - value: value, - ), - ); - }); - } - - QueryBuilder assetCountBetween( - int? lower, - int? upper, { - bool includeLower = true, - bool includeUpper = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.between( - property: r'assetCount', - lower: lower, - includeLower: includeLower, - upper: upper, - includeUpper: includeUpper, - ), - ); - }); - } - - QueryBuilder idEqualTo( - String value, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.equalTo( - property: r'id', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder idGreaterThan( - String value, { - bool include = false, - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.greaterThan( - include: include, - property: r'id', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder idLessThan( - String value, { - bool include = false, - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.lessThan( - include: include, - property: r'id', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder idBetween( - String lower, - String upper, { - bool includeLower = true, - bool includeUpper = true, - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.between( - property: r'id', - lower: lower, - includeLower: includeLower, - upper: upper, - includeUpper: includeUpper, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder idStartsWith( - String value, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.startsWith( - property: r'id', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder idEndsWith( - String value, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.endsWith( - property: r'id', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder idContains( - String value, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.contains( - property: r'id', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder idMatches( - String pattern, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.matches( - property: r'id', - wildcard: pattern, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder idIsEmpty() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.equalTo(property: r'id', value: ''), - ); - }); - } - - QueryBuilder idIsNotEmpty() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.greaterThan(property: r'id', value: ''), - ); - }); - } - - QueryBuilder isarIdEqualTo(Id value) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.equalTo(property: r'isarId', value: value), - ); - }); - } - - QueryBuilder isarIdGreaterThan( - Id value, { - bool include = false, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.greaterThan( - include: include, - property: r'isarId', - value: value, - ), - ); - }); - } - - QueryBuilder isarIdLessThan( - Id value, { - bool include = false, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.lessThan( - include: include, - property: r'isarId', - value: value, - ), - ); - }); - } - - QueryBuilder isarIdBetween( - Id lower, - Id upper, { - bool includeLower = true, - bool includeUpper = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.between( - property: r'isarId', - lower: lower, - includeLower: includeLower, - upper: upper, - includeUpper: includeUpper, - ), - ); - }); - } - - QueryBuilder timeIsNull() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - const FilterCondition.isNull(property: r'time'), - ); - }); - } - - QueryBuilder timeIsNotNull() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - const FilterCondition.isNotNull(property: r'time'), - ); - }); - } - - QueryBuilder timeEqualTo(DateTime? value) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.equalTo(property: r'time', value: value), - ); - }); - } - - QueryBuilder timeGreaterThan( - DateTime? value, { - bool include = false, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.greaterThan( - include: include, - property: r'time', - value: value, - ), - ); - }); - } - - QueryBuilder timeLessThan( - DateTime? value, { - bool include = false, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.lessThan( - include: include, - property: r'time', - value: value, - ), - ); - }); - } - - QueryBuilder timeBetween( - DateTime? lower, - DateTime? upper, { - bool includeLower = true, - bool includeUpper = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.between( - property: r'time', - lower: lower, - includeLower: includeLower, - upper: upper, - includeUpper: includeUpper, - ), - ); - }); - } -} - -extension ETagQueryObject on QueryBuilder {} - -extension ETagQueryLinks on QueryBuilder {} - -extension ETagQuerySortBy on QueryBuilder { - QueryBuilder sortByAssetCount() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'assetCount', Sort.asc); - }); - } - - QueryBuilder sortByAssetCountDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'assetCount', Sort.desc); - }); - } - - QueryBuilder sortById() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'id', Sort.asc); - }); - } - - QueryBuilder sortByIdDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'id', Sort.desc); - }); - } - - QueryBuilder sortByTime() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'time', Sort.asc); - }); - } - - QueryBuilder sortByTimeDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'time', Sort.desc); - }); - } -} - -extension ETagQuerySortThenBy on QueryBuilder { - QueryBuilder thenByAssetCount() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'assetCount', Sort.asc); - }); - } - - QueryBuilder thenByAssetCountDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'assetCount', Sort.desc); - }); - } - - QueryBuilder thenById() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'id', Sort.asc); - }); - } - - QueryBuilder thenByIdDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'id', Sort.desc); - }); - } - - QueryBuilder thenByIsarId() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'isarId', Sort.asc); - }); - } - - QueryBuilder thenByIsarIdDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'isarId', Sort.desc); - }); - } - - QueryBuilder thenByTime() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'time', Sort.asc); - }); - } - - QueryBuilder thenByTimeDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'time', Sort.desc); - }); - } -} - -extension ETagQueryWhereDistinct on QueryBuilder { - QueryBuilder distinctByAssetCount() { - return QueryBuilder.apply(this, (query) { - return query.addDistinctBy(r'assetCount'); - }); - } - - QueryBuilder distinctById({ - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addDistinctBy(r'id', caseSensitive: caseSensitive); - }); - } - - QueryBuilder distinctByTime() { - return QueryBuilder.apply(this, (query) { - return query.addDistinctBy(r'time'); - }); - } -} - -extension ETagQueryProperty on QueryBuilder { - QueryBuilder isarIdProperty() { - return QueryBuilder.apply(this, (query) { - return query.addPropertyName(r'isarId'); - }); - } - - QueryBuilder assetCountProperty() { - return QueryBuilder.apply(this, (query) { - return query.addPropertyName(r'assetCount'); - }); - } - - QueryBuilder idProperty() { - return QueryBuilder.apply(this, (query) { - return query.addPropertyName(r'id'); - }); - } - - QueryBuilder timeProperty() { - return QueryBuilder.apply(this, (query) { - return query.addPropertyName(r'time'); - }); - } -} diff --git a/mobile/lib/entities/ios_device_asset.entity.dart b/mobile/lib/entities/ios_device_asset.entity.dart deleted file mode 100644 index dfd0a660f8..0000000000 --- a/mobile/lib/entities/ios_device_asset.entity.dart +++ /dev/null @@ -1,14 +0,0 @@ -import 'package:immich_mobile/entities/device_asset.entity.dart'; -import 'package:immich_mobile/utils/hash.dart'; -import 'package:isar/isar.dart'; - -part 'ios_device_asset.entity.g.dart'; - -@Collection() -class IOSDeviceAsset extends DeviceAsset { - IOSDeviceAsset({required this.id, required super.hash}); - - @Index(replace: true, unique: true, type: IndexType.hash) - String id; - Id get isarId => fastHash(id); -} diff --git a/mobile/lib/entities/ios_device_asset.entity.g.dart b/mobile/lib/entities/ios_device_asset.entity.g.dart deleted file mode 100644 index 252fe127bb..0000000000 --- a/mobile/lib/entities/ios_device_asset.entity.g.dart +++ /dev/null @@ -1,766 +0,0 @@ -// GENERATED CODE - DO NOT MODIFY BY HAND - -part of 'ios_device_asset.entity.dart'; - -// ************************************************************************** -// IsarCollectionGenerator -// ************************************************************************** - -// coverage:ignore-file -// ignore_for_file: duplicate_ignore, non_constant_identifier_names, constant_identifier_names, invalid_use_of_protected_member, unnecessary_cast, prefer_const_constructors, lines_longer_than_80_chars, require_trailing_commas, inference_failure_on_function_invocation, unnecessary_parenthesis, unnecessary_raw_strings, unnecessary_null_checks, join_return_with_assignment, prefer_final_locals, avoid_js_rounded_ints, avoid_positional_boolean_parameters, always_specify_types - -extension GetIOSDeviceAssetCollection on Isar { - IsarCollection get iOSDeviceAssets => this.collection(); -} - -const IOSDeviceAssetSchema = CollectionSchema( - name: r'IOSDeviceAsset', - id: -1671546753821948030, - properties: { - r'hash': PropertySchema(id: 0, name: r'hash', type: IsarType.byteList), - r'id': PropertySchema(id: 1, name: r'id', type: IsarType.string), - }, - - estimateSize: _iOSDeviceAssetEstimateSize, - serialize: _iOSDeviceAssetSerialize, - deserialize: _iOSDeviceAssetDeserialize, - deserializeProp: _iOSDeviceAssetDeserializeProp, - idName: r'isarId', - indexes: { - r'id': IndexSchema( - id: -3268401673993471357, - name: r'id', - unique: true, - replace: true, - properties: [ - IndexPropertySchema( - name: r'id', - type: IndexType.hash, - caseSensitive: true, - ), - ], - ), - r'hash': IndexSchema( - id: -7973251393006690288, - name: r'hash', - unique: false, - replace: false, - properties: [ - IndexPropertySchema( - name: r'hash', - type: IndexType.hash, - caseSensitive: false, - ), - ], - ), - }, - links: {}, - embeddedSchemas: {}, - - getId: _iOSDeviceAssetGetId, - getLinks: _iOSDeviceAssetGetLinks, - attach: _iOSDeviceAssetAttach, - version: '3.3.0-dev.3', -); - -int _iOSDeviceAssetEstimateSize( - IOSDeviceAsset object, - List offsets, - Map> allOffsets, -) { - var bytesCount = offsets.last; - bytesCount += 3 + object.hash.length; - bytesCount += 3 + object.id.length * 3; - return bytesCount; -} - -void _iOSDeviceAssetSerialize( - IOSDeviceAsset object, - IsarWriter writer, - List offsets, - Map> allOffsets, -) { - writer.writeByteList(offsets[0], object.hash); - writer.writeString(offsets[1], object.id); -} - -IOSDeviceAsset _iOSDeviceAssetDeserialize( - Id id, - IsarReader reader, - List offsets, - Map> allOffsets, -) { - final object = IOSDeviceAsset( - hash: reader.readByteList(offsets[0]) ?? [], - id: reader.readString(offsets[1]), - ); - return object; -} - -P _iOSDeviceAssetDeserializeProp

( - IsarReader reader, - int propertyId, - int offset, - Map> allOffsets, -) { - switch (propertyId) { - case 0: - return (reader.readByteList(offset) ?? []) as P; - case 1: - return (reader.readString(offset)) as P; - default: - throw IsarError('Unknown property with id $propertyId'); - } -} - -Id _iOSDeviceAssetGetId(IOSDeviceAsset object) { - return object.isarId; -} - -List> _iOSDeviceAssetGetLinks(IOSDeviceAsset object) { - return []; -} - -void _iOSDeviceAssetAttach( - IsarCollection col, - Id id, - IOSDeviceAsset object, -) {} - -extension IOSDeviceAssetByIndex on IsarCollection { - Future getById(String id) { - return getByIndex(r'id', [id]); - } - - IOSDeviceAsset? getByIdSync(String id) { - return getByIndexSync(r'id', [id]); - } - - Future deleteById(String id) { - return deleteByIndex(r'id', [id]); - } - - bool deleteByIdSync(String id) { - return deleteByIndexSync(r'id', [id]); - } - - Future> getAllById(List idValues) { - final values = idValues.map((e) => [e]).toList(); - return getAllByIndex(r'id', values); - } - - List getAllByIdSync(List idValues) { - final values = idValues.map((e) => [e]).toList(); - return getAllByIndexSync(r'id', values); - } - - Future deleteAllById(List idValues) { - final values = idValues.map((e) => [e]).toList(); - return deleteAllByIndex(r'id', values); - } - - int deleteAllByIdSync(List idValues) { - final values = idValues.map((e) => [e]).toList(); - return deleteAllByIndexSync(r'id', values); - } - - Future putById(IOSDeviceAsset object) { - return putByIndex(r'id', object); - } - - Id putByIdSync(IOSDeviceAsset object, {bool saveLinks = true}) { - return putByIndexSync(r'id', object, saveLinks: saveLinks); - } - - Future> putAllById(List objects) { - return putAllByIndex(r'id', objects); - } - - List putAllByIdSync( - List objects, { - bool saveLinks = true, - }) { - return putAllByIndexSync(r'id', objects, saveLinks: saveLinks); - } -} - -extension IOSDeviceAssetQueryWhereSort - on QueryBuilder { - QueryBuilder anyIsarId() { - return QueryBuilder.apply(this, (query) { - return query.addWhereClause(const IdWhereClause.any()); - }); - } -} - -extension IOSDeviceAssetQueryWhere - on QueryBuilder { - QueryBuilder isarIdEqualTo( - Id isarId, - ) { - return QueryBuilder.apply(this, (query) { - return query.addWhereClause( - IdWhereClause.between(lower: isarId, upper: isarId), - ); - }); - } - - QueryBuilder - isarIdNotEqualTo(Id isarId) { - return QueryBuilder.apply(this, (query) { - if (query.whereSort == Sort.asc) { - return query - .addWhereClause( - IdWhereClause.lessThan(upper: isarId, includeUpper: false), - ) - .addWhereClause( - IdWhereClause.greaterThan(lower: isarId, includeLower: false), - ); - } else { - return query - .addWhereClause( - IdWhereClause.greaterThan(lower: isarId, includeLower: false), - ) - .addWhereClause( - IdWhereClause.lessThan(upper: isarId, includeUpper: false), - ); - } - }); - } - - QueryBuilder - isarIdGreaterThan(Id isarId, {bool include = false}) { - return QueryBuilder.apply(this, (query) { - return query.addWhereClause( - IdWhereClause.greaterThan(lower: isarId, includeLower: include), - ); - }); - } - - QueryBuilder - isarIdLessThan(Id isarId, {bool include = false}) { - return QueryBuilder.apply(this, (query) { - return query.addWhereClause( - IdWhereClause.lessThan(upper: isarId, includeUpper: include), - ); - }); - } - - QueryBuilder isarIdBetween( - Id lowerIsarId, - Id upperIsarId, { - bool includeLower = true, - bool includeUpper = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addWhereClause( - IdWhereClause.between( - lower: lowerIsarId, - includeLower: includeLower, - upper: upperIsarId, - includeUpper: includeUpper, - ), - ); - }); - } - - QueryBuilder idEqualTo( - String id, - ) { - return QueryBuilder.apply(this, (query) { - return query.addWhereClause( - IndexWhereClause.equalTo(indexName: r'id', value: [id]), - ); - }); - } - - QueryBuilder idNotEqualTo( - String id, - ) { - return QueryBuilder.apply(this, (query) { - if (query.whereSort == Sort.asc) { - return query - .addWhereClause( - IndexWhereClause.between( - indexName: r'id', - lower: [], - upper: [id], - includeUpper: false, - ), - ) - .addWhereClause( - IndexWhereClause.between( - indexName: r'id', - lower: [id], - includeLower: false, - upper: [], - ), - ); - } else { - return query - .addWhereClause( - IndexWhereClause.between( - indexName: r'id', - lower: [id], - includeLower: false, - upper: [], - ), - ) - .addWhereClause( - IndexWhereClause.between( - indexName: r'id', - lower: [], - upper: [id], - includeUpper: false, - ), - ); - } - }); - } - - QueryBuilder hashEqualTo( - List hash, - ) { - return QueryBuilder.apply(this, (query) { - return query.addWhereClause( - IndexWhereClause.equalTo(indexName: r'hash', value: [hash]), - ); - }); - } - - QueryBuilder - hashNotEqualTo(List hash) { - return QueryBuilder.apply(this, (query) { - if (query.whereSort == Sort.asc) { - return query - .addWhereClause( - IndexWhereClause.between( - indexName: r'hash', - lower: [], - upper: [hash], - includeUpper: false, - ), - ) - .addWhereClause( - IndexWhereClause.between( - indexName: r'hash', - lower: [hash], - includeLower: false, - upper: [], - ), - ); - } else { - return query - .addWhereClause( - IndexWhereClause.between( - indexName: r'hash', - lower: [hash], - includeLower: false, - upper: [], - ), - ) - .addWhereClause( - IndexWhereClause.between( - indexName: r'hash', - lower: [], - upper: [hash], - includeUpper: false, - ), - ); - } - }); - } -} - -extension IOSDeviceAssetQueryFilter - on QueryBuilder { - QueryBuilder - hashElementEqualTo(int value) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.equalTo(property: r'hash', value: value), - ); - }); - } - - QueryBuilder - hashElementGreaterThan(int value, {bool include = false}) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.greaterThan( - include: include, - property: r'hash', - value: value, - ), - ); - }); - } - - QueryBuilder - hashElementLessThan(int value, {bool include = false}) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.lessThan( - include: include, - property: r'hash', - value: value, - ), - ); - }); - } - - QueryBuilder - hashElementBetween( - int lower, - int upper, { - bool includeLower = true, - bool includeUpper = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.between( - property: r'hash', - lower: lower, - includeLower: includeLower, - upper: upper, - includeUpper: includeUpper, - ), - ); - }); - } - - QueryBuilder - hashLengthEqualTo(int length) { - return QueryBuilder.apply(this, (query) { - return query.listLength(r'hash', length, true, length, true); - }); - } - - QueryBuilder - hashIsEmpty() { - return QueryBuilder.apply(this, (query) { - return query.listLength(r'hash', 0, true, 0, true); - }); - } - - QueryBuilder - hashIsNotEmpty() { - return QueryBuilder.apply(this, (query) { - return query.listLength(r'hash', 0, false, 999999, true); - }); - } - - QueryBuilder - hashLengthLessThan(int length, {bool include = false}) { - return QueryBuilder.apply(this, (query) { - return query.listLength(r'hash', 0, true, length, include); - }); - } - - QueryBuilder - hashLengthGreaterThan(int length, {bool include = false}) { - return QueryBuilder.apply(this, (query) { - return query.listLength(r'hash', length, include, 999999, true); - }); - } - - QueryBuilder - hashLengthBetween( - int lower, - int upper, { - bool includeLower = true, - bool includeUpper = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.listLength( - r'hash', - lower, - includeLower, - upper, - includeUpper, - ); - }); - } - - QueryBuilder idEqualTo( - String value, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.equalTo( - property: r'id', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder - idGreaterThan( - String value, { - bool include = false, - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.greaterThan( - include: include, - property: r'id', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder - idLessThan(String value, {bool include = false, bool caseSensitive = true}) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.lessThan( - include: include, - property: r'id', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder idBetween( - String lower, - String upper, { - bool includeLower = true, - bool includeUpper = true, - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.between( - property: r'id', - lower: lower, - includeLower: includeLower, - upper: upper, - includeUpper: includeUpper, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder - idStartsWith(String value, {bool caseSensitive = true}) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.startsWith( - property: r'id', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder - idEndsWith(String value, {bool caseSensitive = true}) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.endsWith( - property: r'id', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder - idContains(String value, {bool caseSensitive = true}) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.contains( - property: r'id', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder idMatches( - String pattern, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.matches( - property: r'id', - wildcard: pattern, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder - idIsEmpty() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.equalTo(property: r'id', value: ''), - ); - }); - } - - QueryBuilder - idIsNotEmpty() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.greaterThan(property: r'id', value: ''), - ); - }); - } - - QueryBuilder - isarIdEqualTo(Id value) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.equalTo(property: r'isarId', value: value), - ); - }); - } - - QueryBuilder - isarIdGreaterThan(Id value, {bool include = false}) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.greaterThan( - include: include, - property: r'isarId', - value: value, - ), - ); - }); - } - - QueryBuilder - isarIdLessThan(Id value, {bool include = false}) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.lessThan( - include: include, - property: r'isarId', - value: value, - ), - ); - }); - } - - QueryBuilder - isarIdBetween( - Id lower, - Id upper, { - bool includeLower = true, - bool includeUpper = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.between( - property: r'isarId', - lower: lower, - includeLower: includeLower, - upper: upper, - includeUpper: includeUpper, - ), - ); - }); - } -} - -extension IOSDeviceAssetQueryObject - on QueryBuilder {} - -extension IOSDeviceAssetQueryLinks - on QueryBuilder {} - -extension IOSDeviceAssetQuerySortBy - on QueryBuilder { - QueryBuilder sortById() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'id', Sort.asc); - }); - } - - QueryBuilder sortByIdDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'id', Sort.desc); - }); - } -} - -extension IOSDeviceAssetQuerySortThenBy - on QueryBuilder { - QueryBuilder thenById() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'id', Sort.asc); - }); - } - - QueryBuilder thenByIdDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'id', Sort.desc); - }); - } - - QueryBuilder thenByIsarId() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'isarId', Sort.asc); - }); - } - - QueryBuilder - thenByIsarIdDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'isarId', Sort.desc); - }); - } -} - -extension IOSDeviceAssetQueryWhereDistinct - on QueryBuilder { - QueryBuilder distinctByHash() { - return QueryBuilder.apply(this, (query) { - return query.addDistinctBy(r'hash'); - }); - } - - QueryBuilder distinctById({ - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addDistinctBy(r'id', caseSensitive: caseSensitive); - }); - } -} - -extension IOSDeviceAssetQueryProperty - on QueryBuilder { - QueryBuilder isarIdProperty() { - return QueryBuilder.apply(this, (query) { - return query.addPropertyName(r'isarId'); - }); - } - - QueryBuilder, QQueryOperations> hashProperty() { - return QueryBuilder.apply(this, (query) { - return query.addPropertyName(r'hash'); - }); - } - - QueryBuilder idProperty() { - return QueryBuilder.apply(this, (query) { - return query.addPropertyName(r'id'); - }); - } -} diff --git a/mobile/lib/entities/store.entity.dart b/mobile/lib/entities/store.entity.dart index 7b59e119d6..17ad88cee9 100644 --- a/mobile/lib/entities/store.entity.dart +++ b/mobile/lib/entities/store.entity.dart @@ -1,38 +1,4 @@ -import 'dart:convert'; -import 'dart:typed_data'; - -import 'package:immich_mobile/domain/models/store.model.dart'; import 'package:immich_mobile/domain/services/store.service.dart'; // ignore: non_constant_identifier_names final Store = StoreService.I; - -class SSLClientCertStoreVal { - final Uint8List data; - final String? password; - - const SSLClientCertStoreVal(this.data, this.password); - - Future save() async { - final b64Str = base64Encode(data); - await Store.put(StoreKey.sslClientCertData, b64Str); - if (password != null) { - await Store.put(StoreKey.sslClientPasswd, password!); - } - } - - static SSLClientCertStoreVal? load() { - final b64Str = Store.tryGet(StoreKey.sslClientCertData); - if (b64Str == null) { - return null; - } - final Uint8List certData = base64Decode(b64Str); - final passwd = Store.tryGet(StoreKey.sslClientPasswd); - return SSLClientCertStoreVal(certData, passwd); - } - - static Future delete() async { - await Store.delete(StoreKey.sslClientCertData); - await Store.delete(StoreKey.sslClientPasswd); - } -} diff --git a/mobile/lib/extensions/asset_extensions.dart b/mobile/lib/extensions/asset_extensions.dart index f7f98b3da7..73a8ec4d05 100644 --- a/mobile/lib/extensions/asset_extensions.dart +++ b/mobile/lib/extensions/asset_extensions.dart @@ -1,26 +1,9 @@ import 'package:immich_mobile/domain/models/asset/base_asset.model.dart'; import 'package:immich_mobile/domain/models/exif.model.dart'; -import 'package:immich_mobile/entities/asset.entity.dart' as isar hide AssetTypeEnumHelper; import 'package:immich_mobile/extensions/string_extensions.dart'; import 'package:immich_mobile/infrastructure/utils/exif.converter.dart'; -import 'package:immich_mobile/utils/timezone.dart'; import 'package:openapi/api.dart' as api; -extension TZExtension on isar.Asset { - /// Returns the created time of the asset from the exif info (if available) or from - /// the fileCreatedAt field, adjusted to the timezone value from the exif info along with - /// the timezone offset in [Duration] - (DateTime, Duration) getTZAdjustedTimeAndOffset() { - DateTime dt = fileCreatedAt.toLocal(); - - if (exifInfo?.dateTimeOriginal != null) { - return applyTimezoneOffset(dateTime: exifInfo!.dateTimeOriginal!, timeZone: exifInfo?.timeZone); - } - - return (dt, dt.timeZoneOffset); - } -} - extension DTOToAsset on api.AssetResponseDto { RemoteAsset toDto() { return RemoteAsset( diff --git a/mobile/lib/extensions/collection_extensions.dart b/mobile/lib/extensions/collection_extensions.dart index 541db7ccaf..b861eb0570 100644 --- a/mobile/lib/extensions/collection_extensions.dart +++ b/mobile/lib/extensions/collection_extensions.dart @@ -1,9 +1,6 @@ import 'dart:typed_data'; import 'package:collection/collection.dart'; -import 'package:immich_mobile/domain/models/user.model.dart'; -import 'package:immich_mobile/entities/asset.entity.dart'; -import 'package:immich_mobile/utils/hash.dart'; extension ListExtension on List { List uniqueConsecutive({int Function(E a, E b)? compare, void Function(E a, E b)? onDuplicate}) { @@ -40,31 +37,6 @@ extension IntListExtension on Iterable { } } -extension AssetListExtension on Iterable { - /// Returns the assets that are already available in the Immich server - Iterable remoteOnly({void Function()? errorCallback}) { - final bool onlyRemote = every((e) => e.isRemote); - if (!onlyRemote) { - if (errorCallback != null) errorCallback(); - return where((a) => a.isRemote); - } - return this; - } - - /// Returns the assets that are owned by the user passed to the [owner] param - /// If [owner] is null, an empty list is returned - Iterable ownedOnly(UserDto? owner, {void Function()? errorCallback}) { - if (owner == null) return []; - final isarUserId = fastHash(owner.id); - final bool onlyOwned = every((e) => e.ownerId == isarUserId); - if (!onlyOwned) { - if (errorCallback != null) errorCallback(); - return where((a) => a.ownerId == isarUserId); - } - return this; - } -} - extension SortedByProperty on Iterable { Iterable sortedByField(Comparable Function(T e) key) { return sorted((a, b) => key(a).compareTo(key(b))); diff --git a/mobile/lib/extensions/translate_extensions.dart b/mobile/lib/extensions/translate_extensions.dart index 7677f3cbd8..b01203a90c 100644 --- a/mobile/lib/extensions/translate_extensions.dart +++ b/mobile/lib/extensions/translate_extensions.dart @@ -1,7 +1,7 @@ import 'package:easy_localization/easy_localization.dart'; -import 'package:intl/message_format.dart'; import 'package:flutter/material.dart'; import 'package:immich_mobile/utils/debug_print.dart'; +import 'package:intl/message_format.dart'; extension StringTranslateExtension on String { String t({BuildContext? context, Map? args}) { diff --git a/mobile/lib/infrastructure/entities/device_asset.entity.dart b/mobile/lib/infrastructure/entities/device_asset.entity.dart deleted file mode 100644 index e3e4a0d4f4..0000000000 --- a/mobile/lib/infrastructure/entities/device_asset.entity.dart +++ /dev/null @@ -1,25 +0,0 @@ -import 'dart:typed_data'; - -import 'package:immich_mobile/domain/models/device_asset.model.dart'; -import 'package:immich_mobile/utils/hash.dart'; -import 'package:isar/isar.dart'; - -part 'device_asset.entity.g.dart'; - -@Collection(inheritance: false) -class DeviceAssetEntity { - Id get id => fastHash(assetId); - - @Index(replace: true, unique: true, type: IndexType.hash) - final String assetId; - @Index(unique: false, type: IndexType.hash) - final List hash; - final DateTime modifiedTime; - - const DeviceAssetEntity({required this.assetId, required this.hash, required this.modifiedTime}); - - DeviceAsset toModel() => DeviceAsset(assetId: assetId, hash: Uint8List.fromList(hash), modifiedTime: modifiedTime); - - static DeviceAssetEntity fromDto(DeviceAsset dto) => - DeviceAssetEntity(assetId: dto.assetId, hash: dto.hash, modifiedTime: dto.modifiedTime); -} diff --git a/mobile/lib/infrastructure/entities/device_asset.entity.g.dart b/mobile/lib/infrastructure/entities/device_asset.entity.g.dart deleted file mode 100644 index b6c30aca6f..0000000000 --- a/mobile/lib/infrastructure/entities/device_asset.entity.g.dart +++ /dev/null @@ -1,874 +0,0 @@ -// GENERATED CODE - DO NOT MODIFY BY HAND - -part of 'device_asset.entity.dart'; - -// ************************************************************************** -// IsarCollectionGenerator -// ************************************************************************** - -// coverage:ignore-file -// ignore_for_file: duplicate_ignore, non_constant_identifier_names, constant_identifier_names, invalid_use_of_protected_member, unnecessary_cast, prefer_const_constructors, lines_longer_than_80_chars, require_trailing_commas, inference_failure_on_function_invocation, unnecessary_parenthesis, unnecessary_raw_strings, unnecessary_null_checks, join_return_with_assignment, prefer_final_locals, avoid_js_rounded_ints, avoid_positional_boolean_parameters, always_specify_types - -extension GetDeviceAssetEntityCollection on Isar { - IsarCollection get deviceAssetEntitys => this.collection(); -} - -const DeviceAssetEntitySchema = CollectionSchema( - name: r'DeviceAssetEntity', - id: 6967030785073446271, - properties: { - r'assetId': PropertySchema(id: 0, name: r'assetId', type: IsarType.string), - r'hash': PropertySchema(id: 1, name: r'hash', type: IsarType.byteList), - r'modifiedTime': PropertySchema( - id: 2, - name: r'modifiedTime', - type: IsarType.dateTime, - ), - }, - - estimateSize: _deviceAssetEntityEstimateSize, - serialize: _deviceAssetEntitySerialize, - deserialize: _deviceAssetEntityDeserialize, - deserializeProp: _deviceAssetEntityDeserializeProp, - idName: r'id', - indexes: { - r'assetId': IndexSchema( - id: 174362542210192109, - name: r'assetId', - unique: true, - replace: true, - properties: [ - IndexPropertySchema( - name: r'assetId', - type: IndexType.hash, - caseSensitive: true, - ), - ], - ), - r'hash': IndexSchema( - id: -7973251393006690288, - name: r'hash', - unique: false, - replace: false, - properties: [ - IndexPropertySchema( - name: r'hash', - type: IndexType.hash, - caseSensitive: false, - ), - ], - ), - }, - links: {}, - embeddedSchemas: {}, - - getId: _deviceAssetEntityGetId, - getLinks: _deviceAssetEntityGetLinks, - attach: _deviceAssetEntityAttach, - version: '3.3.0-dev.3', -); - -int _deviceAssetEntityEstimateSize( - DeviceAssetEntity object, - List offsets, - Map> allOffsets, -) { - var bytesCount = offsets.last; - bytesCount += 3 + object.assetId.length * 3; - bytesCount += 3 + object.hash.length; - return bytesCount; -} - -void _deviceAssetEntitySerialize( - DeviceAssetEntity object, - IsarWriter writer, - List offsets, - Map> allOffsets, -) { - writer.writeString(offsets[0], object.assetId); - writer.writeByteList(offsets[1], object.hash); - writer.writeDateTime(offsets[2], object.modifiedTime); -} - -DeviceAssetEntity _deviceAssetEntityDeserialize( - Id id, - IsarReader reader, - List offsets, - Map> allOffsets, -) { - final object = DeviceAssetEntity( - assetId: reader.readString(offsets[0]), - hash: reader.readByteList(offsets[1]) ?? [], - modifiedTime: reader.readDateTime(offsets[2]), - ); - return object; -} - -P _deviceAssetEntityDeserializeProp

( - IsarReader reader, - int propertyId, - int offset, - Map> allOffsets, -) { - switch (propertyId) { - case 0: - return (reader.readString(offset)) as P; - case 1: - return (reader.readByteList(offset) ?? []) as P; - case 2: - return (reader.readDateTime(offset)) as P; - default: - throw IsarError('Unknown property with id $propertyId'); - } -} - -Id _deviceAssetEntityGetId(DeviceAssetEntity object) { - return object.id; -} - -List> _deviceAssetEntityGetLinks( - DeviceAssetEntity object, -) { - return []; -} - -void _deviceAssetEntityAttach( - IsarCollection col, - Id id, - DeviceAssetEntity object, -) {} - -extension DeviceAssetEntityByIndex on IsarCollection { - Future getByAssetId(String assetId) { - return getByIndex(r'assetId', [assetId]); - } - - DeviceAssetEntity? getByAssetIdSync(String assetId) { - return getByIndexSync(r'assetId', [assetId]); - } - - Future deleteByAssetId(String assetId) { - return deleteByIndex(r'assetId', [assetId]); - } - - bool deleteByAssetIdSync(String assetId) { - return deleteByIndexSync(r'assetId', [assetId]); - } - - Future> getAllByAssetId(List assetIdValues) { - final values = assetIdValues.map((e) => [e]).toList(); - return getAllByIndex(r'assetId', values); - } - - List getAllByAssetIdSync(List assetIdValues) { - final values = assetIdValues.map((e) => [e]).toList(); - return getAllByIndexSync(r'assetId', values); - } - - Future deleteAllByAssetId(List assetIdValues) { - final values = assetIdValues.map((e) => [e]).toList(); - return deleteAllByIndex(r'assetId', values); - } - - int deleteAllByAssetIdSync(List assetIdValues) { - final values = assetIdValues.map((e) => [e]).toList(); - return deleteAllByIndexSync(r'assetId', values); - } - - Future putByAssetId(DeviceAssetEntity object) { - return putByIndex(r'assetId', object); - } - - Id putByAssetIdSync(DeviceAssetEntity object, {bool saveLinks = true}) { - return putByIndexSync(r'assetId', object, saveLinks: saveLinks); - } - - Future> putAllByAssetId(List objects) { - return putAllByIndex(r'assetId', objects); - } - - List putAllByAssetIdSync( - List objects, { - bool saveLinks = true, - }) { - return putAllByIndexSync(r'assetId', objects, saveLinks: saveLinks); - } -} - -extension DeviceAssetEntityQueryWhereSort - on QueryBuilder { - QueryBuilder anyId() { - return QueryBuilder.apply(this, (query) { - return query.addWhereClause(const IdWhereClause.any()); - }); - } -} - -extension DeviceAssetEntityQueryWhere - on QueryBuilder { - QueryBuilder - idEqualTo(Id id) { - return QueryBuilder.apply(this, (query) { - return query.addWhereClause(IdWhereClause.between(lower: id, upper: id)); - }); - } - - QueryBuilder - idNotEqualTo(Id id) { - return QueryBuilder.apply(this, (query) { - if (query.whereSort == Sort.asc) { - return query - .addWhereClause( - IdWhereClause.lessThan(upper: id, includeUpper: false), - ) - .addWhereClause( - IdWhereClause.greaterThan(lower: id, includeLower: false), - ); - } else { - return query - .addWhereClause( - IdWhereClause.greaterThan(lower: id, includeLower: false), - ) - .addWhereClause( - IdWhereClause.lessThan(upper: id, includeUpper: false), - ); - } - }); - } - - QueryBuilder - idGreaterThan(Id id, {bool include = false}) { - return QueryBuilder.apply(this, (query) { - return query.addWhereClause( - IdWhereClause.greaterThan(lower: id, includeLower: include), - ); - }); - } - - QueryBuilder - idLessThan(Id id, {bool include = false}) { - return QueryBuilder.apply(this, (query) { - return query.addWhereClause( - IdWhereClause.lessThan(upper: id, includeUpper: include), - ); - }); - } - - QueryBuilder - idBetween( - Id lowerId, - Id upperId, { - bool includeLower = true, - bool includeUpper = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addWhereClause( - IdWhereClause.between( - lower: lowerId, - includeLower: includeLower, - upper: upperId, - includeUpper: includeUpper, - ), - ); - }); - } - - QueryBuilder - assetIdEqualTo(String assetId) { - return QueryBuilder.apply(this, (query) { - return query.addWhereClause( - IndexWhereClause.equalTo(indexName: r'assetId', value: [assetId]), - ); - }); - } - - QueryBuilder - assetIdNotEqualTo(String assetId) { - return QueryBuilder.apply(this, (query) { - if (query.whereSort == Sort.asc) { - return query - .addWhereClause( - IndexWhereClause.between( - indexName: r'assetId', - lower: [], - upper: [assetId], - includeUpper: false, - ), - ) - .addWhereClause( - IndexWhereClause.between( - indexName: r'assetId', - lower: [assetId], - includeLower: false, - upper: [], - ), - ); - } else { - return query - .addWhereClause( - IndexWhereClause.between( - indexName: r'assetId', - lower: [assetId], - includeLower: false, - upper: [], - ), - ) - .addWhereClause( - IndexWhereClause.between( - indexName: r'assetId', - lower: [], - upper: [assetId], - includeUpper: false, - ), - ); - } - }); - } - - QueryBuilder - hashEqualTo(List hash) { - return QueryBuilder.apply(this, (query) { - return query.addWhereClause( - IndexWhereClause.equalTo(indexName: r'hash', value: [hash]), - ); - }); - } - - QueryBuilder - hashNotEqualTo(List hash) { - return QueryBuilder.apply(this, (query) { - if (query.whereSort == Sort.asc) { - return query - .addWhereClause( - IndexWhereClause.between( - indexName: r'hash', - lower: [], - upper: [hash], - includeUpper: false, - ), - ) - .addWhereClause( - IndexWhereClause.between( - indexName: r'hash', - lower: [hash], - includeLower: false, - upper: [], - ), - ); - } else { - return query - .addWhereClause( - IndexWhereClause.between( - indexName: r'hash', - lower: [hash], - includeLower: false, - upper: [], - ), - ) - .addWhereClause( - IndexWhereClause.between( - indexName: r'hash', - lower: [], - upper: [hash], - includeUpper: false, - ), - ); - } - }); - } -} - -extension DeviceAssetEntityQueryFilter - on QueryBuilder { - QueryBuilder - assetIdEqualTo(String value, {bool caseSensitive = true}) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.equalTo( - property: r'assetId', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder - assetIdGreaterThan( - String value, { - bool include = false, - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.greaterThan( - include: include, - property: r'assetId', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder - assetIdLessThan( - String value, { - bool include = false, - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.lessThan( - include: include, - property: r'assetId', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder - assetIdBetween( - String lower, - String upper, { - bool includeLower = true, - bool includeUpper = true, - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.between( - property: r'assetId', - lower: lower, - includeLower: includeLower, - upper: upper, - includeUpper: includeUpper, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder - assetIdStartsWith(String value, {bool caseSensitive = true}) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.startsWith( - property: r'assetId', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder - assetIdEndsWith(String value, {bool caseSensitive = true}) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.endsWith( - property: r'assetId', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder - assetIdContains(String value, {bool caseSensitive = true}) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.contains( - property: r'assetId', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder - assetIdMatches(String pattern, {bool caseSensitive = true}) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.matches( - property: r'assetId', - wildcard: pattern, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder - assetIdIsEmpty() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.equalTo(property: r'assetId', value: ''), - ); - }); - } - - QueryBuilder - assetIdIsNotEmpty() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.greaterThan(property: r'assetId', value: ''), - ); - }); - } - - QueryBuilder - hashElementEqualTo(int value) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.equalTo(property: r'hash', value: value), - ); - }); - } - - QueryBuilder - hashElementGreaterThan(int value, {bool include = false}) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.greaterThan( - include: include, - property: r'hash', - value: value, - ), - ); - }); - } - - QueryBuilder - hashElementLessThan(int value, {bool include = false}) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.lessThan( - include: include, - property: r'hash', - value: value, - ), - ); - }); - } - - QueryBuilder - hashElementBetween( - int lower, - int upper, { - bool includeLower = true, - bool includeUpper = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.between( - property: r'hash', - lower: lower, - includeLower: includeLower, - upper: upper, - includeUpper: includeUpper, - ), - ); - }); - } - - QueryBuilder - hashLengthEqualTo(int length) { - return QueryBuilder.apply(this, (query) { - return query.listLength(r'hash', length, true, length, true); - }); - } - - QueryBuilder - hashIsEmpty() { - return QueryBuilder.apply(this, (query) { - return query.listLength(r'hash', 0, true, 0, true); - }); - } - - QueryBuilder - hashIsNotEmpty() { - return QueryBuilder.apply(this, (query) { - return query.listLength(r'hash', 0, false, 999999, true); - }); - } - - QueryBuilder - hashLengthLessThan(int length, {bool include = false}) { - return QueryBuilder.apply(this, (query) { - return query.listLength(r'hash', 0, true, length, include); - }); - } - - QueryBuilder - hashLengthGreaterThan(int length, {bool include = false}) { - return QueryBuilder.apply(this, (query) { - return query.listLength(r'hash', length, include, 999999, true); - }); - } - - QueryBuilder - hashLengthBetween( - int lower, - int upper, { - bool includeLower = true, - bool includeUpper = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.listLength( - r'hash', - lower, - includeLower, - upper, - includeUpper, - ); - }); - } - - QueryBuilder - idEqualTo(Id value) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.equalTo(property: r'id', value: value), - ); - }); - } - - QueryBuilder - idGreaterThan(Id value, {bool include = false}) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.greaterThan( - include: include, - property: r'id', - value: value, - ), - ); - }); - } - - QueryBuilder - idLessThan(Id value, {bool include = false}) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.lessThan( - include: include, - property: r'id', - value: value, - ), - ); - }); - } - - QueryBuilder - idBetween( - Id lower, - Id upper, { - bool includeLower = true, - bool includeUpper = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.between( - property: r'id', - lower: lower, - includeLower: includeLower, - upper: upper, - includeUpper: includeUpper, - ), - ); - }); - } - - QueryBuilder - modifiedTimeEqualTo(DateTime value) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.equalTo(property: r'modifiedTime', value: value), - ); - }); - } - - QueryBuilder - modifiedTimeGreaterThan(DateTime value, {bool include = false}) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.greaterThan( - include: include, - property: r'modifiedTime', - value: value, - ), - ); - }); - } - - QueryBuilder - modifiedTimeLessThan(DateTime value, {bool include = false}) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.lessThan( - include: include, - property: r'modifiedTime', - value: value, - ), - ); - }); - } - - QueryBuilder - modifiedTimeBetween( - DateTime lower, - DateTime upper, { - bool includeLower = true, - bool includeUpper = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.between( - property: r'modifiedTime', - lower: lower, - includeLower: includeLower, - upper: upper, - includeUpper: includeUpper, - ), - ); - }); - } -} - -extension DeviceAssetEntityQueryObject - on QueryBuilder {} - -extension DeviceAssetEntityQueryLinks - on QueryBuilder {} - -extension DeviceAssetEntityQuerySortBy - on QueryBuilder { - QueryBuilder - sortByAssetId() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'assetId', Sort.asc); - }); - } - - QueryBuilder - sortByAssetIdDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'assetId', Sort.desc); - }); - } - - QueryBuilder - sortByModifiedTime() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'modifiedTime', Sort.asc); - }); - } - - QueryBuilder - sortByModifiedTimeDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'modifiedTime', Sort.desc); - }); - } -} - -extension DeviceAssetEntityQuerySortThenBy - on QueryBuilder { - QueryBuilder - thenByAssetId() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'assetId', Sort.asc); - }); - } - - QueryBuilder - thenByAssetIdDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'assetId', Sort.desc); - }); - } - - QueryBuilder thenById() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'id', Sort.asc); - }); - } - - QueryBuilder - thenByIdDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'id', Sort.desc); - }); - } - - QueryBuilder - thenByModifiedTime() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'modifiedTime', Sort.asc); - }); - } - - QueryBuilder - thenByModifiedTimeDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'modifiedTime', Sort.desc); - }); - } -} - -extension DeviceAssetEntityQueryWhereDistinct - on QueryBuilder { - QueryBuilder - distinctByAssetId({bool caseSensitive = true}) { - return QueryBuilder.apply(this, (query) { - return query.addDistinctBy(r'assetId', caseSensitive: caseSensitive); - }); - } - - QueryBuilder - distinctByHash() { - return QueryBuilder.apply(this, (query) { - return query.addDistinctBy(r'hash'); - }); - } - - QueryBuilder - distinctByModifiedTime() { - return QueryBuilder.apply(this, (query) { - return query.addDistinctBy(r'modifiedTime'); - }); - } -} - -extension DeviceAssetEntityQueryProperty - on QueryBuilder { - QueryBuilder idProperty() { - return QueryBuilder.apply(this, (query) { - return query.addPropertyName(r'id'); - }); - } - - QueryBuilder assetIdProperty() { - return QueryBuilder.apply(this, (query) { - return query.addPropertyName(r'assetId'); - }); - } - - QueryBuilder, QQueryOperations> hashProperty() { - return QueryBuilder.apply(this, (query) { - return query.addPropertyName(r'hash'); - }); - } - - QueryBuilder - modifiedTimeProperty() { - return QueryBuilder.apply(this, (query) { - return query.addPropertyName(r'modifiedTime'); - }); - } -} diff --git a/mobile/lib/infrastructure/entities/exif.entity.dart b/mobile/lib/infrastructure/entities/exif.entity.dart index 06262f4afc..e009029ea7 100644 --- a/mobile/lib/infrastructure/entities/exif.entity.dart +++ b/mobile/lib/infrastructure/entities/exif.entity.dart @@ -4,96 +4,6 @@ import 'package:immich_mobile/infrastructure/entities/exif.entity.drift.dart'; import 'package:immich_mobile/infrastructure/entities/remote_asset.entity.dart'; import 'package:immich_mobile/infrastructure/utils/drift_default.mixin.dart'; import 'package:immich_mobile/infrastructure/utils/exif.converter.dart'; -import 'package:isar/isar.dart'; - -part 'exif.entity.g.dart'; - -/// Exif information 1:1 relation with Asset -@Collection(inheritance: false) -class ExifInfo { - final Id? id; - final int? fileSize; - final DateTime? dateTimeOriginal; - final String? timeZone; - final String? make; - final String? model; - final String? lens; - final float? f; - final float? mm; - final short? iso; - final float? exposureSeconds; - final float? lat; - final float? long; - final String? city; - final String? state; - final String? country; - final String? description; - final String? orientation; - - const ExifInfo({ - this.id, - this.fileSize, - this.dateTimeOriginal, - this.timeZone, - this.make, - this.model, - this.lens, - this.f, - this.mm, - this.iso, - this.exposureSeconds, - this.lat, - this.long, - this.city, - this.state, - this.country, - this.description, - this.orientation, - }); - - static ExifInfo fromDto(domain.ExifInfo dto) => ExifInfo( - id: dto.assetId, - fileSize: dto.fileSize, - dateTimeOriginal: dto.dateTimeOriginal, - timeZone: dto.timeZone, - make: dto.make, - model: dto.model, - lens: dto.lens, - f: dto.f, - mm: dto.mm, - iso: dto.iso?.toInt(), - exposureSeconds: dto.exposureSeconds, - lat: dto.latitude, - long: dto.longitude, - city: dto.city, - state: dto.state, - country: dto.country, - description: dto.description, - orientation: dto.orientation, - ); - - domain.ExifInfo toDto() => domain.ExifInfo( - assetId: id, - fileSize: fileSize, - description: description, - orientation: orientation, - timeZone: timeZone, - dateTimeOriginal: dateTimeOriginal, - isFlipped: ExifDtoConverter.isOrientationFlipped(orientation), - latitude: lat, - longitude: long, - city: city, - state: state, - country: country, - make: make, - model: model, - lens: lens, - f: f, - mm: mm, - iso: iso?.toInt(), - exposureSeconds: exposureSeconds, - ); -} @TableIndex.sql('CREATE INDEX IF NOT EXISTS idx_lat_lng ON remote_exif_entity (latitude, longitude)') class RemoteExifEntity extends Table with DriftDefaultsMixin { diff --git a/mobile/lib/infrastructure/entities/exif.entity.g.dart b/mobile/lib/infrastructure/entities/exif.entity.g.dart deleted file mode 100644 index ffbfd0d8f0..0000000000 --- a/mobile/lib/infrastructure/entities/exif.entity.g.dart +++ /dev/null @@ -1,3200 +0,0 @@ -// GENERATED CODE - DO NOT MODIFY BY HAND - -part of 'exif.entity.dart'; - -// ************************************************************************** -// IsarCollectionGenerator -// ************************************************************************** - -// coverage:ignore-file -// ignore_for_file: duplicate_ignore, non_constant_identifier_names, constant_identifier_names, invalid_use_of_protected_member, unnecessary_cast, prefer_const_constructors, lines_longer_than_80_chars, require_trailing_commas, inference_failure_on_function_invocation, unnecessary_parenthesis, unnecessary_raw_strings, unnecessary_null_checks, join_return_with_assignment, prefer_final_locals, avoid_js_rounded_ints, avoid_positional_boolean_parameters, always_specify_types - -extension GetExifInfoCollection on Isar { - IsarCollection get exifInfos => this.collection(); -} - -const ExifInfoSchema = CollectionSchema( - name: r'ExifInfo', - id: -2409260054350835217, - properties: { - r'city': PropertySchema(id: 0, name: r'city', type: IsarType.string), - r'country': PropertySchema(id: 1, name: r'country', type: IsarType.string), - r'dateTimeOriginal': PropertySchema( - id: 2, - name: r'dateTimeOriginal', - type: IsarType.dateTime, - ), - r'description': PropertySchema( - id: 3, - name: r'description', - type: IsarType.string, - ), - r'exposureSeconds': PropertySchema( - id: 4, - name: r'exposureSeconds', - type: IsarType.float, - ), - r'f': PropertySchema(id: 5, name: r'f', type: IsarType.float), - r'fileSize': PropertySchema(id: 6, name: r'fileSize', type: IsarType.long), - r'iso': PropertySchema(id: 7, name: r'iso', type: IsarType.int), - r'lat': PropertySchema(id: 8, name: r'lat', type: IsarType.float), - r'lens': PropertySchema(id: 9, name: r'lens', type: IsarType.string), - r'long': PropertySchema(id: 10, name: r'long', type: IsarType.float), - r'make': PropertySchema(id: 11, name: r'make', type: IsarType.string), - r'mm': PropertySchema(id: 12, name: r'mm', type: IsarType.float), - r'model': PropertySchema(id: 13, name: r'model', type: IsarType.string), - r'orientation': PropertySchema( - id: 14, - name: r'orientation', - type: IsarType.string, - ), - r'state': PropertySchema(id: 15, name: r'state', type: IsarType.string), - r'timeZone': PropertySchema( - id: 16, - name: r'timeZone', - type: IsarType.string, - ), - }, - - estimateSize: _exifInfoEstimateSize, - serialize: _exifInfoSerialize, - deserialize: _exifInfoDeserialize, - deserializeProp: _exifInfoDeserializeProp, - idName: r'id', - indexes: {}, - links: {}, - embeddedSchemas: {}, - - getId: _exifInfoGetId, - getLinks: _exifInfoGetLinks, - attach: _exifInfoAttach, - version: '3.3.0-dev.3', -); - -int _exifInfoEstimateSize( - ExifInfo object, - List offsets, - Map> allOffsets, -) { - var bytesCount = offsets.last; - { - final value = object.city; - if (value != null) { - bytesCount += 3 + value.length * 3; - } - } - { - final value = object.country; - if (value != null) { - bytesCount += 3 + value.length * 3; - } - } - { - final value = object.description; - if (value != null) { - bytesCount += 3 + value.length * 3; - } - } - { - final value = object.lens; - if (value != null) { - bytesCount += 3 + value.length * 3; - } - } - { - final value = object.make; - if (value != null) { - bytesCount += 3 + value.length * 3; - } - } - { - final value = object.model; - if (value != null) { - bytesCount += 3 + value.length * 3; - } - } - { - final value = object.orientation; - if (value != null) { - bytesCount += 3 + value.length * 3; - } - } - { - final value = object.state; - if (value != null) { - bytesCount += 3 + value.length * 3; - } - } - { - final value = object.timeZone; - if (value != null) { - bytesCount += 3 + value.length * 3; - } - } - return bytesCount; -} - -void _exifInfoSerialize( - ExifInfo object, - IsarWriter writer, - List offsets, - Map> allOffsets, -) { - writer.writeString(offsets[0], object.city); - writer.writeString(offsets[1], object.country); - writer.writeDateTime(offsets[2], object.dateTimeOriginal); - writer.writeString(offsets[3], object.description); - writer.writeFloat(offsets[4], object.exposureSeconds); - writer.writeFloat(offsets[5], object.f); - writer.writeLong(offsets[6], object.fileSize); - writer.writeInt(offsets[7], object.iso); - writer.writeFloat(offsets[8], object.lat); - writer.writeString(offsets[9], object.lens); - writer.writeFloat(offsets[10], object.long); - writer.writeString(offsets[11], object.make); - writer.writeFloat(offsets[12], object.mm); - writer.writeString(offsets[13], object.model); - writer.writeString(offsets[14], object.orientation); - writer.writeString(offsets[15], object.state); - writer.writeString(offsets[16], object.timeZone); -} - -ExifInfo _exifInfoDeserialize( - Id id, - IsarReader reader, - List offsets, - Map> allOffsets, -) { - final object = ExifInfo( - city: reader.readStringOrNull(offsets[0]), - country: reader.readStringOrNull(offsets[1]), - dateTimeOriginal: reader.readDateTimeOrNull(offsets[2]), - description: reader.readStringOrNull(offsets[3]), - exposureSeconds: reader.readFloatOrNull(offsets[4]), - f: reader.readFloatOrNull(offsets[5]), - fileSize: reader.readLongOrNull(offsets[6]), - id: id, - iso: reader.readIntOrNull(offsets[7]), - lat: reader.readFloatOrNull(offsets[8]), - lens: reader.readStringOrNull(offsets[9]), - long: reader.readFloatOrNull(offsets[10]), - make: reader.readStringOrNull(offsets[11]), - mm: reader.readFloatOrNull(offsets[12]), - model: reader.readStringOrNull(offsets[13]), - orientation: reader.readStringOrNull(offsets[14]), - state: reader.readStringOrNull(offsets[15]), - timeZone: reader.readStringOrNull(offsets[16]), - ); - return object; -} - -P _exifInfoDeserializeProp

( - IsarReader reader, - int propertyId, - int offset, - Map> allOffsets, -) { - switch (propertyId) { - case 0: - return (reader.readStringOrNull(offset)) as P; - case 1: - return (reader.readStringOrNull(offset)) as P; - case 2: - return (reader.readDateTimeOrNull(offset)) as P; - case 3: - return (reader.readStringOrNull(offset)) as P; - case 4: - return (reader.readFloatOrNull(offset)) as P; - case 5: - return (reader.readFloatOrNull(offset)) as P; - case 6: - return (reader.readLongOrNull(offset)) as P; - case 7: - return (reader.readIntOrNull(offset)) as P; - case 8: - return (reader.readFloatOrNull(offset)) as P; - case 9: - return (reader.readStringOrNull(offset)) as P; - case 10: - return (reader.readFloatOrNull(offset)) as P; - case 11: - return (reader.readStringOrNull(offset)) as P; - case 12: - return (reader.readFloatOrNull(offset)) as P; - case 13: - return (reader.readStringOrNull(offset)) as P; - case 14: - return (reader.readStringOrNull(offset)) as P; - case 15: - return (reader.readStringOrNull(offset)) as P; - case 16: - return (reader.readStringOrNull(offset)) as P; - default: - throw IsarError('Unknown property with id $propertyId'); - } -} - -Id _exifInfoGetId(ExifInfo object) { - return object.id ?? Isar.autoIncrement; -} - -List> _exifInfoGetLinks(ExifInfo object) { - return []; -} - -void _exifInfoAttach(IsarCollection col, Id id, ExifInfo object) {} - -extension ExifInfoQueryWhereSort on QueryBuilder { - QueryBuilder anyId() { - return QueryBuilder.apply(this, (query) { - return query.addWhereClause(const IdWhereClause.any()); - }); - } -} - -extension ExifInfoQueryWhere on QueryBuilder { - QueryBuilder idEqualTo(Id id) { - return QueryBuilder.apply(this, (query) { - return query.addWhereClause(IdWhereClause.between(lower: id, upper: id)); - }); - } - - QueryBuilder idNotEqualTo(Id id) { - return QueryBuilder.apply(this, (query) { - if (query.whereSort == Sort.asc) { - return query - .addWhereClause( - IdWhereClause.lessThan(upper: id, includeUpper: false), - ) - .addWhereClause( - IdWhereClause.greaterThan(lower: id, includeLower: false), - ); - } else { - return query - .addWhereClause( - IdWhereClause.greaterThan(lower: id, includeLower: false), - ) - .addWhereClause( - IdWhereClause.lessThan(upper: id, includeUpper: false), - ); - } - }); - } - - QueryBuilder idGreaterThan( - Id id, { - bool include = false, - }) { - return QueryBuilder.apply(this, (query) { - return query.addWhereClause( - IdWhereClause.greaterThan(lower: id, includeLower: include), - ); - }); - } - - QueryBuilder idLessThan( - Id id, { - bool include = false, - }) { - return QueryBuilder.apply(this, (query) { - return query.addWhereClause( - IdWhereClause.lessThan(upper: id, includeUpper: include), - ); - }); - } - - QueryBuilder idBetween( - Id lowerId, - Id upperId, { - bool includeLower = true, - bool includeUpper = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addWhereClause( - IdWhereClause.between( - lower: lowerId, - includeLower: includeLower, - upper: upperId, - includeUpper: includeUpper, - ), - ); - }); - } -} - -extension ExifInfoQueryFilter - on QueryBuilder { - QueryBuilder cityIsNull() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - const FilterCondition.isNull(property: r'city'), - ); - }); - } - - QueryBuilder cityIsNotNull() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - const FilterCondition.isNotNull(property: r'city'), - ); - }); - } - - QueryBuilder cityEqualTo( - String? value, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.equalTo( - property: r'city', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder cityGreaterThan( - String? value, { - bool include = false, - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.greaterThan( - include: include, - property: r'city', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder cityLessThan( - String? value, { - bool include = false, - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.lessThan( - include: include, - property: r'city', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder cityBetween( - String? lower, - String? upper, { - bool includeLower = true, - bool includeUpper = true, - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.between( - property: r'city', - lower: lower, - includeLower: includeLower, - upper: upper, - includeUpper: includeUpper, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder cityStartsWith( - String value, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.startsWith( - property: r'city', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder cityEndsWith( - String value, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.endsWith( - property: r'city', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder cityContains( - String value, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.contains( - property: r'city', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder cityMatches( - String pattern, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.matches( - property: r'city', - wildcard: pattern, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder cityIsEmpty() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.equalTo(property: r'city', value: ''), - ); - }); - } - - QueryBuilder cityIsNotEmpty() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.greaterThan(property: r'city', value: ''), - ); - }); - } - - QueryBuilder countryIsNull() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - const FilterCondition.isNull(property: r'country'), - ); - }); - } - - QueryBuilder countryIsNotNull() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - const FilterCondition.isNotNull(property: r'country'), - ); - }); - } - - QueryBuilder countryEqualTo( - String? value, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.equalTo( - property: r'country', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder countryGreaterThan( - String? value, { - bool include = false, - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.greaterThan( - include: include, - property: r'country', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder countryLessThan( - String? value, { - bool include = false, - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.lessThan( - include: include, - property: r'country', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder countryBetween( - String? lower, - String? upper, { - bool includeLower = true, - bool includeUpper = true, - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.between( - property: r'country', - lower: lower, - includeLower: includeLower, - upper: upper, - includeUpper: includeUpper, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder countryStartsWith( - String value, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.startsWith( - property: r'country', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder countryEndsWith( - String value, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.endsWith( - property: r'country', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder countryContains( - String value, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.contains( - property: r'country', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder countryMatches( - String pattern, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.matches( - property: r'country', - wildcard: pattern, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder countryIsEmpty() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.equalTo(property: r'country', value: ''), - ); - }); - } - - QueryBuilder countryIsNotEmpty() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.greaterThan(property: r'country', value: ''), - ); - }); - } - - QueryBuilder - dateTimeOriginalIsNull() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - const FilterCondition.isNull(property: r'dateTimeOriginal'), - ); - }); - } - - QueryBuilder - dateTimeOriginalIsNotNull() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - const FilterCondition.isNotNull(property: r'dateTimeOriginal'), - ); - }); - } - - QueryBuilder - dateTimeOriginalEqualTo(DateTime? value) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.equalTo(property: r'dateTimeOriginal', value: value), - ); - }); - } - - QueryBuilder - dateTimeOriginalGreaterThan(DateTime? value, {bool include = false}) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.greaterThan( - include: include, - property: r'dateTimeOriginal', - value: value, - ), - ); - }); - } - - QueryBuilder - dateTimeOriginalLessThan(DateTime? value, {bool include = false}) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.lessThan( - include: include, - property: r'dateTimeOriginal', - value: value, - ), - ); - }); - } - - QueryBuilder - dateTimeOriginalBetween( - DateTime? lower, - DateTime? upper, { - bool includeLower = true, - bool includeUpper = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.between( - property: r'dateTimeOriginal', - lower: lower, - includeLower: includeLower, - upper: upper, - includeUpper: includeUpper, - ), - ); - }); - } - - QueryBuilder descriptionIsNull() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - const FilterCondition.isNull(property: r'description'), - ); - }); - } - - QueryBuilder - descriptionIsNotNull() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - const FilterCondition.isNotNull(property: r'description'), - ); - }); - } - - QueryBuilder descriptionEqualTo( - String? value, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.equalTo( - property: r'description', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder - descriptionGreaterThan( - String? value, { - bool include = false, - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.greaterThan( - include: include, - property: r'description', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder descriptionLessThan( - String? value, { - bool include = false, - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.lessThan( - include: include, - property: r'description', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder descriptionBetween( - String? lower, - String? upper, { - bool includeLower = true, - bool includeUpper = true, - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.between( - property: r'description', - lower: lower, - includeLower: includeLower, - upper: upper, - includeUpper: includeUpper, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder descriptionStartsWith( - String value, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.startsWith( - property: r'description', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder descriptionEndsWith( - String value, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.endsWith( - property: r'description', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder descriptionContains( - String value, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.contains( - property: r'description', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder descriptionMatches( - String pattern, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.matches( - property: r'description', - wildcard: pattern, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder descriptionIsEmpty() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.equalTo(property: r'description', value: ''), - ); - }); - } - - QueryBuilder - descriptionIsNotEmpty() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.greaterThan(property: r'description', value: ''), - ); - }); - } - - QueryBuilder - exposureSecondsIsNull() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - const FilterCondition.isNull(property: r'exposureSeconds'), - ); - }); - } - - QueryBuilder - exposureSecondsIsNotNull() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - const FilterCondition.isNotNull(property: r'exposureSeconds'), - ); - }); - } - - QueryBuilder - exposureSecondsEqualTo(double? value, {double epsilon = Query.epsilon}) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.equalTo( - property: r'exposureSeconds', - value: value, - - epsilon: epsilon, - ), - ); - }); - } - - QueryBuilder - exposureSecondsGreaterThan( - double? value, { - bool include = false, - double epsilon = Query.epsilon, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.greaterThan( - include: include, - property: r'exposureSeconds', - value: value, - - epsilon: epsilon, - ), - ); - }); - } - - QueryBuilder - exposureSecondsLessThan( - double? value, { - bool include = false, - double epsilon = Query.epsilon, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.lessThan( - include: include, - property: r'exposureSeconds', - value: value, - - epsilon: epsilon, - ), - ); - }); - } - - QueryBuilder - exposureSecondsBetween( - double? lower, - double? upper, { - bool includeLower = true, - bool includeUpper = true, - double epsilon = Query.epsilon, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.between( - property: r'exposureSeconds', - lower: lower, - includeLower: includeLower, - upper: upper, - includeUpper: includeUpper, - - epsilon: epsilon, - ), - ); - }); - } - - QueryBuilder fIsNull() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - const FilterCondition.isNull(property: r'f'), - ); - }); - } - - QueryBuilder fIsNotNull() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - const FilterCondition.isNotNull(property: r'f'), - ); - }); - } - - QueryBuilder fEqualTo( - double? value, { - double epsilon = Query.epsilon, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.equalTo(property: r'f', value: value, epsilon: epsilon), - ); - }); - } - - QueryBuilder fGreaterThan( - double? value, { - bool include = false, - double epsilon = Query.epsilon, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.greaterThan( - include: include, - property: r'f', - value: value, - - epsilon: epsilon, - ), - ); - }); - } - - QueryBuilder fLessThan( - double? value, { - bool include = false, - double epsilon = Query.epsilon, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.lessThan( - include: include, - property: r'f', - value: value, - - epsilon: epsilon, - ), - ); - }); - } - - QueryBuilder fBetween( - double? lower, - double? upper, { - bool includeLower = true, - bool includeUpper = true, - double epsilon = Query.epsilon, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.between( - property: r'f', - lower: lower, - includeLower: includeLower, - upper: upper, - includeUpper: includeUpper, - - epsilon: epsilon, - ), - ); - }); - } - - QueryBuilder fileSizeIsNull() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - const FilterCondition.isNull(property: r'fileSize'), - ); - }); - } - - QueryBuilder fileSizeIsNotNull() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - const FilterCondition.isNotNull(property: r'fileSize'), - ); - }); - } - - QueryBuilder fileSizeEqualTo( - int? value, - ) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.equalTo(property: r'fileSize', value: value), - ); - }); - } - - QueryBuilder fileSizeGreaterThan( - int? value, { - bool include = false, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.greaterThan( - include: include, - property: r'fileSize', - value: value, - ), - ); - }); - } - - QueryBuilder fileSizeLessThan( - int? value, { - bool include = false, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.lessThan( - include: include, - property: r'fileSize', - value: value, - ), - ); - }); - } - - QueryBuilder fileSizeBetween( - int? lower, - int? upper, { - bool includeLower = true, - bool includeUpper = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.between( - property: r'fileSize', - lower: lower, - includeLower: includeLower, - upper: upper, - includeUpper: includeUpper, - ), - ); - }); - } - - QueryBuilder idIsNull() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - const FilterCondition.isNull(property: r'id'), - ); - }); - } - - QueryBuilder idIsNotNull() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - const FilterCondition.isNotNull(property: r'id'), - ); - }); - } - - QueryBuilder idEqualTo(Id? value) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.equalTo(property: r'id', value: value), - ); - }); - } - - QueryBuilder idGreaterThan( - Id? value, { - bool include = false, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.greaterThan( - include: include, - property: r'id', - value: value, - ), - ); - }); - } - - QueryBuilder idLessThan( - Id? value, { - bool include = false, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.lessThan( - include: include, - property: r'id', - value: value, - ), - ); - }); - } - - QueryBuilder idBetween( - Id? lower, - Id? upper, { - bool includeLower = true, - bool includeUpper = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.between( - property: r'id', - lower: lower, - includeLower: includeLower, - upper: upper, - includeUpper: includeUpper, - ), - ); - }); - } - - QueryBuilder isoIsNull() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - const FilterCondition.isNull(property: r'iso'), - ); - }); - } - - QueryBuilder isoIsNotNull() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - const FilterCondition.isNotNull(property: r'iso'), - ); - }); - } - - QueryBuilder isoEqualTo( - int? value, - ) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.equalTo(property: r'iso', value: value), - ); - }); - } - - QueryBuilder isoGreaterThan( - int? value, { - bool include = false, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.greaterThan( - include: include, - property: r'iso', - value: value, - ), - ); - }); - } - - QueryBuilder isoLessThan( - int? value, { - bool include = false, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.lessThan( - include: include, - property: r'iso', - value: value, - ), - ); - }); - } - - QueryBuilder isoBetween( - int? lower, - int? upper, { - bool includeLower = true, - bool includeUpper = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.between( - property: r'iso', - lower: lower, - includeLower: includeLower, - upper: upper, - includeUpper: includeUpper, - ), - ); - }); - } - - QueryBuilder latIsNull() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - const FilterCondition.isNull(property: r'lat'), - ); - }); - } - - QueryBuilder latIsNotNull() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - const FilterCondition.isNotNull(property: r'lat'), - ); - }); - } - - QueryBuilder latEqualTo( - double? value, { - double epsilon = Query.epsilon, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.equalTo( - property: r'lat', - value: value, - - epsilon: epsilon, - ), - ); - }); - } - - QueryBuilder latGreaterThan( - double? value, { - bool include = false, - double epsilon = Query.epsilon, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.greaterThan( - include: include, - property: r'lat', - value: value, - - epsilon: epsilon, - ), - ); - }); - } - - QueryBuilder latLessThan( - double? value, { - bool include = false, - double epsilon = Query.epsilon, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.lessThan( - include: include, - property: r'lat', - value: value, - - epsilon: epsilon, - ), - ); - }); - } - - QueryBuilder latBetween( - double? lower, - double? upper, { - bool includeLower = true, - bool includeUpper = true, - double epsilon = Query.epsilon, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.between( - property: r'lat', - lower: lower, - includeLower: includeLower, - upper: upper, - includeUpper: includeUpper, - - epsilon: epsilon, - ), - ); - }); - } - - QueryBuilder lensIsNull() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - const FilterCondition.isNull(property: r'lens'), - ); - }); - } - - QueryBuilder lensIsNotNull() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - const FilterCondition.isNotNull(property: r'lens'), - ); - }); - } - - QueryBuilder lensEqualTo( - String? value, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.equalTo( - property: r'lens', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder lensGreaterThan( - String? value, { - bool include = false, - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.greaterThan( - include: include, - property: r'lens', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder lensLessThan( - String? value, { - bool include = false, - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.lessThan( - include: include, - property: r'lens', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder lensBetween( - String? lower, - String? upper, { - bool includeLower = true, - bool includeUpper = true, - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.between( - property: r'lens', - lower: lower, - includeLower: includeLower, - upper: upper, - includeUpper: includeUpper, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder lensStartsWith( - String value, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.startsWith( - property: r'lens', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder lensEndsWith( - String value, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.endsWith( - property: r'lens', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder lensContains( - String value, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.contains( - property: r'lens', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder lensMatches( - String pattern, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.matches( - property: r'lens', - wildcard: pattern, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder lensIsEmpty() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.equalTo(property: r'lens', value: ''), - ); - }); - } - - QueryBuilder lensIsNotEmpty() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.greaterThan(property: r'lens', value: ''), - ); - }); - } - - QueryBuilder longIsNull() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - const FilterCondition.isNull(property: r'long'), - ); - }); - } - - QueryBuilder longIsNotNull() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - const FilterCondition.isNotNull(property: r'long'), - ); - }); - } - - QueryBuilder longEqualTo( - double? value, { - double epsilon = Query.epsilon, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.equalTo( - property: r'long', - value: value, - - epsilon: epsilon, - ), - ); - }); - } - - QueryBuilder longGreaterThan( - double? value, { - bool include = false, - double epsilon = Query.epsilon, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.greaterThan( - include: include, - property: r'long', - value: value, - - epsilon: epsilon, - ), - ); - }); - } - - QueryBuilder longLessThan( - double? value, { - bool include = false, - double epsilon = Query.epsilon, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.lessThan( - include: include, - property: r'long', - value: value, - - epsilon: epsilon, - ), - ); - }); - } - - QueryBuilder longBetween( - double? lower, - double? upper, { - bool includeLower = true, - bool includeUpper = true, - double epsilon = Query.epsilon, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.between( - property: r'long', - lower: lower, - includeLower: includeLower, - upper: upper, - includeUpper: includeUpper, - - epsilon: epsilon, - ), - ); - }); - } - - QueryBuilder makeIsNull() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - const FilterCondition.isNull(property: r'make'), - ); - }); - } - - QueryBuilder makeIsNotNull() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - const FilterCondition.isNotNull(property: r'make'), - ); - }); - } - - QueryBuilder makeEqualTo( - String? value, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.equalTo( - property: r'make', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder makeGreaterThan( - String? value, { - bool include = false, - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.greaterThan( - include: include, - property: r'make', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder makeLessThan( - String? value, { - bool include = false, - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.lessThan( - include: include, - property: r'make', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder makeBetween( - String? lower, - String? upper, { - bool includeLower = true, - bool includeUpper = true, - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.between( - property: r'make', - lower: lower, - includeLower: includeLower, - upper: upper, - includeUpper: includeUpper, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder makeStartsWith( - String value, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.startsWith( - property: r'make', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder makeEndsWith( - String value, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.endsWith( - property: r'make', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder makeContains( - String value, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.contains( - property: r'make', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder makeMatches( - String pattern, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.matches( - property: r'make', - wildcard: pattern, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder makeIsEmpty() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.equalTo(property: r'make', value: ''), - ); - }); - } - - QueryBuilder makeIsNotEmpty() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.greaterThan(property: r'make', value: ''), - ); - }); - } - - QueryBuilder mmIsNull() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - const FilterCondition.isNull(property: r'mm'), - ); - }); - } - - QueryBuilder mmIsNotNull() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - const FilterCondition.isNotNull(property: r'mm'), - ); - }); - } - - QueryBuilder mmEqualTo( - double? value, { - double epsilon = Query.epsilon, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.equalTo( - property: r'mm', - value: value, - - epsilon: epsilon, - ), - ); - }); - } - - QueryBuilder mmGreaterThan( - double? value, { - bool include = false, - double epsilon = Query.epsilon, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.greaterThan( - include: include, - property: r'mm', - value: value, - - epsilon: epsilon, - ), - ); - }); - } - - QueryBuilder mmLessThan( - double? value, { - bool include = false, - double epsilon = Query.epsilon, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.lessThan( - include: include, - property: r'mm', - value: value, - - epsilon: epsilon, - ), - ); - }); - } - - QueryBuilder mmBetween( - double? lower, - double? upper, { - bool includeLower = true, - bool includeUpper = true, - double epsilon = Query.epsilon, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.between( - property: r'mm', - lower: lower, - includeLower: includeLower, - upper: upper, - includeUpper: includeUpper, - - epsilon: epsilon, - ), - ); - }); - } - - QueryBuilder modelIsNull() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - const FilterCondition.isNull(property: r'model'), - ); - }); - } - - QueryBuilder modelIsNotNull() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - const FilterCondition.isNotNull(property: r'model'), - ); - }); - } - - QueryBuilder modelEqualTo( - String? value, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.equalTo( - property: r'model', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder modelGreaterThan( - String? value, { - bool include = false, - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.greaterThan( - include: include, - property: r'model', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder modelLessThan( - String? value, { - bool include = false, - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.lessThan( - include: include, - property: r'model', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder modelBetween( - String? lower, - String? upper, { - bool includeLower = true, - bool includeUpper = true, - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.between( - property: r'model', - lower: lower, - includeLower: includeLower, - upper: upper, - includeUpper: includeUpper, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder modelStartsWith( - String value, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.startsWith( - property: r'model', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder modelEndsWith( - String value, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.endsWith( - property: r'model', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder modelContains( - String value, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.contains( - property: r'model', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder modelMatches( - String pattern, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.matches( - property: r'model', - wildcard: pattern, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder modelIsEmpty() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.equalTo(property: r'model', value: ''), - ); - }); - } - - QueryBuilder modelIsNotEmpty() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.greaterThan(property: r'model', value: ''), - ); - }); - } - - QueryBuilder orientationIsNull() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - const FilterCondition.isNull(property: r'orientation'), - ); - }); - } - - QueryBuilder - orientationIsNotNull() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - const FilterCondition.isNotNull(property: r'orientation'), - ); - }); - } - - QueryBuilder orientationEqualTo( - String? value, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.equalTo( - property: r'orientation', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder - orientationGreaterThan( - String? value, { - bool include = false, - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.greaterThan( - include: include, - property: r'orientation', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder orientationLessThan( - String? value, { - bool include = false, - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.lessThan( - include: include, - property: r'orientation', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder orientationBetween( - String? lower, - String? upper, { - bool includeLower = true, - bool includeUpper = true, - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.between( - property: r'orientation', - lower: lower, - includeLower: includeLower, - upper: upper, - includeUpper: includeUpper, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder orientationStartsWith( - String value, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.startsWith( - property: r'orientation', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder orientationEndsWith( - String value, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.endsWith( - property: r'orientation', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder orientationContains( - String value, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.contains( - property: r'orientation', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder orientationMatches( - String pattern, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.matches( - property: r'orientation', - wildcard: pattern, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder orientationIsEmpty() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.equalTo(property: r'orientation', value: ''), - ); - }); - } - - QueryBuilder - orientationIsNotEmpty() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.greaterThan(property: r'orientation', value: ''), - ); - }); - } - - QueryBuilder stateIsNull() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - const FilterCondition.isNull(property: r'state'), - ); - }); - } - - QueryBuilder stateIsNotNull() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - const FilterCondition.isNotNull(property: r'state'), - ); - }); - } - - QueryBuilder stateEqualTo( - String? value, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.equalTo( - property: r'state', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder stateGreaterThan( - String? value, { - bool include = false, - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.greaterThan( - include: include, - property: r'state', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder stateLessThan( - String? value, { - bool include = false, - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.lessThan( - include: include, - property: r'state', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder stateBetween( - String? lower, - String? upper, { - bool includeLower = true, - bool includeUpper = true, - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.between( - property: r'state', - lower: lower, - includeLower: includeLower, - upper: upper, - includeUpper: includeUpper, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder stateStartsWith( - String value, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.startsWith( - property: r'state', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder stateEndsWith( - String value, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.endsWith( - property: r'state', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder stateContains( - String value, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.contains( - property: r'state', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder stateMatches( - String pattern, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.matches( - property: r'state', - wildcard: pattern, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder stateIsEmpty() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.equalTo(property: r'state', value: ''), - ); - }); - } - - QueryBuilder stateIsNotEmpty() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.greaterThan(property: r'state', value: ''), - ); - }); - } - - QueryBuilder timeZoneIsNull() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - const FilterCondition.isNull(property: r'timeZone'), - ); - }); - } - - QueryBuilder timeZoneIsNotNull() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - const FilterCondition.isNotNull(property: r'timeZone'), - ); - }); - } - - QueryBuilder timeZoneEqualTo( - String? value, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.equalTo( - property: r'timeZone', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder timeZoneGreaterThan( - String? value, { - bool include = false, - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.greaterThan( - include: include, - property: r'timeZone', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder timeZoneLessThan( - String? value, { - bool include = false, - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.lessThan( - include: include, - property: r'timeZone', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder timeZoneBetween( - String? lower, - String? upper, { - bool includeLower = true, - bool includeUpper = true, - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.between( - property: r'timeZone', - lower: lower, - includeLower: includeLower, - upper: upper, - includeUpper: includeUpper, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder timeZoneStartsWith( - String value, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.startsWith( - property: r'timeZone', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder timeZoneEndsWith( - String value, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.endsWith( - property: r'timeZone', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder timeZoneContains( - String value, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.contains( - property: r'timeZone', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder timeZoneMatches( - String pattern, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.matches( - property: r'timeZone', - wildcard: pattern, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder timeZoneIsEmpty() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.equalTo(property: r'timeZone', value: ''), - ); - }); - } - - QueryBuilder timeZoneIsNotEmpty() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.greaterThan(property: r'timeZone', value: ''), - ); - }); - } -} - -extension ExifInfoQueryObject - on QueryBuilder {} - -extension ExifInfoQueryLinks - on QueryBuilder {} - -extension ExifInfoQuerySortBy on QueryBuilder { - QueryBuilder sortByCity() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'city', Sort.asc); - }); - } - - QueryBuilder sortByCityDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'city', Sort.desc); - }); - } - - QueryBuilder sortByCountry() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'country', Sort.asc); - }); - } - - QueryBuilder sortByCountryDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'country', Sort.desc); - }); - } - - QueryBuilder sortByDateTimeOriginal() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'dateTimeOriginal', Sort.asc); - }); - } - - QueryBuilder sortByDateTimeOriginalDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'dateTimeOriginal', Sort.desc); - }); - } - - QueryBuilder sortByDescription() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'description', Sort.asc); - }); - } - - QueryBuilder sortByDescriptionDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'description', Sort.desc); - }); - } - - QueryBuilder sortByExposureSeconds() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'exposureSeconds', Sort.asc); - }); - } - - QueryBuilder sortByExposureSecondsDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'exposureSeconds', Sort.desc); - }); - } - - QueryBuilder sortByF() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'f', Sort.asc); - }); - } - - QueryBuilder sortByFDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'f', Sort.desc); - }); - } - - QueryBuilder sortByFileSize() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'fileSize', Sort.asc); - }); - } - - QueryBuilder sortByFileSizeDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'fileSize', Sort.desc); - }); - } - - QueryBuilder sortByIso() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'iso', Sort.asc); - }); - } - - QueryBuilder sortByIsoDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'iso', Sort.desc); - }); - } - - QueryBuilder sortByLat() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'lat', Sort.asc); - }); - } - - QueryBuilder sortByLatDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'lat', Sort.desc); - }); - } - - QueryBuilder sortByLens() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'lens', Sort.asc); - }); - } - - QueryBuilder sortByLensDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'lens', Sort.desc); - }); - } - - QueryBuilder sortByLong() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'long', Sort.asc); - }); - } - - QueryBuilder sortByLongDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'long', Sort.desc); - }); - } - - QueryBuilder sortByMake() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'make', Sort.asc); - }); - } - - QueryBuilder sortByMakeDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'make', Sort.desc); - }); - } - - QueryBuilder sortByMm() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'mm', Sort.asc); - }); - } - - QueryBuilder sortByMmDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'mm', Sort.desc); - }); - } - - QueryBuilder sortByModel() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'model', Sort.asc); - }); - } - - QueryBuilder sortByModelDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'model', Sort.desc); - }); - } - - QueryBuilder sortByOrientation() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'orientation', Sort.asc); - }); - } - - QueryBuilder sortByOrientationDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'orientation', Sort.desc); - }); - } - - QueryBuilder sortByState() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'state', Sort.asc); - }); - } - - QueryBuilder sortByStateDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'state', Sort.desc); - }); - } - - QueryBuilder sortByTimeZone() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'timeZone', Sort.asc); - }); - } - - QueryBuilder sortByTimeZoneDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'timeZone', Sort.desc); - }); - } -} - -extension ExifInfoQuerySortThenBy - on QueryBuilder { - QueryBuilder thenByCity() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'city', Sort.asc); - }); - } - - QueryBuilder thenByCityDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'city', Sort.desc); - }); - } - - QueryBuilder thenByCountry() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'country', Sort.asc); - }); - } - - QueryBuilder thenByCountryDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'country', Sort.desc); - }); - } - - QueryBuilder thenByDateTimeOriginal() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'dateTimeOriginal', Sort.asc); - }); - } - - QueryBuilder thenByDateTimeOriginalDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'dateTimeOriginal', Sort.desc); - }); - } - - QueryBuilder thenByDescription() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'description', Sort.asc); - }); - } - - QueryBuilder thenByDescriptionDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'description', Sort.desc); - }); - } - - QueryBuilder thenByExposureSeconds() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'exposureSeconds', Sort.asc); - }); - } - - QueryBuilder thenByExposureSecondsDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'exposureSeconds', Sort.desc); - }); - } - - QueryBuilder thenByF() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'f', Sort.asc); - }); - } - - QueryBuilder thenByFDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'f', Sort.desc); - }); - } - - QueryBuilder thenByFileSize() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'fileSize', Sort.asc); - }); - } - - QueryBuilder thenByFileSizeDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'fileSize', Sort.desc); - }); - } - - QueryBuilder thenById() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'id', Sort.asc); - }); - } - - QueryBuilder thenByIdDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'id', Sort.desc); - }); - } - - QueryBuilder thenByIso() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'iso', Sort.asc); - }); - } - - QueryBuilder thenByIsoDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'iso', Sort.desc); - }); - } - - QueryBuilder thenByLat() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'lat', Sort.asc); - }); - } - - QueryBuilder thenByLatDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'lat', Sort.desc); - }); - } - - QueryBuilder thenByLens() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'lens', Sort.asc); - }); - } - - QueryBuilder thenByLensDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'lens', Sort.desc); - }); - } - - QueryBuilder thenByLong() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'long', Sort.asc); - }); - } - - QueryBuilder thenByLongDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'long', Sort.desc); - }); - } - - QueryBuilder thenByMake() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'make', Sort.asc); - }); - } - - QueryBuilder thenByMakeDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'make', Sort.desc); - }); - } - - QueryBuilder thenByMm() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'mm', Sort.asc); - }); - } - - QueryBuilder thenByMmDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'mm', Sort.desc); - }); - } - - QueryBuilder thenByModel() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'model', Sort.asc); - }); - } - - QueryBuilder thenByModelDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'model', Sort.desc); - }); - } - - QueryBuilder thenByOrientation() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'orientation', Sort.asc); - }); - } - - QueryBuilder thenByOrientationDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'orientation', Sort.desc); - }); - } - - QueryBuilder thenByState() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'state', Sort.asc); - }); - } - - QueryBuilder thenByStateDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'state', Sort.desc); - }); - } - - QueryBuilder thenByTimeZone() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'timeZone', Sort.asc); - }); - } - - QueryBuilder thenByTimeZoneDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'timeZone', Sort.desc); - }); - } -} - -extension ExifInfoQueryWhereDistinct - on QueryBuilder { - QueryBuilder distinctByCity({ - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addDistinctBy(r'city', caseSensitive: caseSensitive); - }); - } - - QueryBuilder distinctByCountry({ - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addDistinctBy(r'country', caseSensitive: caseSensitive); - }); - } - - QueryBuilder distinctByDateTimeOriginal() { - return QueryBuilder.apply(this, (query) { - return query.addDistinctBy(r'dateTimeOriginal'); - }); - } - - QueryBuilder distinctByDescription({ - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addDistinctBy(r'description', caseSensitive: caseSensitive); - }); - } - - QueryBuilder distinctByExposureSeconds() { - return QueryBuilder.apply(this, (query) { - return query.addDistinctBy(r'exposureSeconds'); - }); - } - - QueryBuilder distinctByF() { - return QueryBuilder.apply(this, (query) { - return query.addDistinctBy(r'f'); - }); - } - - QueryBuilder distinctByFileSize() { - return QueryBuilder.apply(this, (query) { - return query.addDistinctBy(r'fileSize'); - }); - } - - QueryBuilder distinctByIso() { - return QueryBuilder.apply(this, (query) { - return query.addDistinctBy(r'iso'); - }); - } - - QueryBuilder distinctByLat() { - return QueryBuilder.apply(this, (query) { - return query.addDistinctBy(r'lat'); - }); - } - - QueryBuilder distinctByLens({ - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addDistinctBy(r'lens', caseSensitive: caseSensitive); - }); - } - - QueryBuilder distinctByLong() { - return QueryBuilder.apply(this, (query) { - return query.addDistinctBy(r'long'); - }); - } - - QueryBuilder distinctByMake({ - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addDistinctBy(r'make', caseSensitive: caseSensitive); - }); - } - - QueryBuilder distinctByMm() { - return QueryBuilder.apply(this, (query) { - return query.addDistinctBy(r'mm'); - }); - } - - QueryBuilder distinctByModel({ - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addDistinctBy(r'model', caseSensitive: caseSensitive); - }); - } - - QueryBuilder distinctByOrientation({ - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addDistinctBy(r'orientation', caseSensitive: caseSensitive); - }); - } - - QueryBuilder distinctByState({ - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addDistinctBy(r'state', caseSensitive: caseSensitive); - }); - } - - QueryBuilder distinctByTimeZone({ - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addDistinctBy(r'timeZone', caseSensitive: caseSensitive); - }); - } -} - -extension ExifInfoQueryProperty - on QueryBuilder { - QueryBuilder idProperty() { - return QueryBuilder.apply(this, (query) { - return query.addPropertyName(r'id'); - }); - } - - QueryBuilder cityProperty() { - return QueryBuilder.apply(this, (query) { - return query.addPropertyName(r'city'); - }); - } - - QueryBuilder countryProperty() { - return QueryBuilder.apply(this, (query) { - return query.addPropertyName(r'country'); - }); - } - - QueryBuilder - dateTimeOriginalProperty() { - return QueryBuilder.apply(this, (query) { - return query.addPropertyName(r'dateTimeOriginal'); - }); - } - - QueryBuilder descriptionProperty() { - return QueryBuilder.apply(this, (query) { - return query.addPropertyName(r'description'); - }); - } - - QueryBuilder exposureSecondsProperty() { - return QueryBuilder.apply(this, (query) { - return query.addPropertyName(r'exposureSeconds'); - }); - } - - QueryBuilder fProperty() { - return QueryBuilder.apply(this, (query) { - return query.addPropertyName(r'f'); - }); - } - - QueryBuilder fileSizeProperty() { - return QueryBuilder.apply(this, (query) { - return query.addPropertyName(r'fileSize'); - }); - } - - QueryBuilder isoProperty() { - return QueryBuilder.apply(this, (query) { - return query.addPropertyName(r'iso'); - }); - } - - QueryBuilder latProperty() { - return QueryBuilder.apply(this, (query) { - return query.addPropertyName(r'lat'); - }); - } - - QueryBuilder lensProperty() { - return QueryBuilder.apply(this, (query) { - return query.addPropertyName(r'lens'); - }); - } - - QueryBuilder longProperty() { - return QueryBuilder.apply(this, (query) { - return query.addPropertyName(r'long'); - }); - } - - QueryBuilder makeProperty() { - return QueryBuilder.apply(this, (query) { - return query.addPropertyName(r'make'); - }); - } - - QueryBuilder mmProperty() { - return QueryBuilder.apply(this, (query) { - return query.addPropertyName(r'mm'); - }); - } - - QueryBuilder modelProperty() { - return QueryBuilder.apply(this, (query) { - return query.addPropertyName(r'model'); - }); - } - - QueryBuilder orientationProperty() { - return QueryBuilder.apply(this, (query) { - return query.addPropertyName(r'orientation'); - }); - } - - QueryBuilder stateProperty() { - return QueryBuilder.apply(this, (query) { - return query.addPropertyName(r'state'); - }); - } - - QueryBuilder timeZoneProperty() { - return QueryBuilder.apply(this, (query) { - return query.addPropertyName(r'timeZone'); - }); - } -} diff --git a/mobile/lib/infrastructure/entities/store.entity.dart b/mobile/lib/infrastructure/entities/store.entity.dart index d4b3eec84f..2de8eb713e 100644 --- a/mobile/lib/infrastructure/entities/store.entity.dart +++ b/mobile/lib/infrastructure/entities/store.entity.dart @@ -1,18 +1,5 @@ import 'package:drift/drift.dart'; import 'package:immich_mobile/infrastructure/utils/drift_default.mixin.dart'; -import 'package:isar/isar.dart'; - -part 'store.entity.g.dart'; - -/// Internal class for `Store`, do not use elsewhere. -@Collection(inheritance: false) -class StoreValue { - final Id id; - final int? intValue; - final String? strValue; - - const StoreValue(this.id, {this.intValue, this.strValue}); -} class StoreEntity extends Table with DriftDefaultsMixin { IntColumn get id => integer()(); diff --git a/mobile/lib/infrastructure/entities/store.entity.g.dart b/mobile/lib/infrastructure/entities/store.entity.g.dart deleted file mode 100644 index 626c3084fe..0000000000 --- a/mobile/lib/infrastructure/entities/store.entity.g.dart +++ /dev/null @@ -1,596 +0,0 @@ -// GENERATED CODE - DO NOT MODIFY BY HAND - -part of 'store.entity.dart'; - -// ************************************************************************** -// IsarCollectionGenerator -// ************************************************************************** - -// coverage:ignore-file -// ignore_for_file: duplicate_ignore, non_constant_identifier_names, constant_identifier_names, invalid_use_of_protected_member, unnecessary_cast, prefer_const_constructors, lines_longer_than_80_chars, require_trailing_commas, inference_failure_on_function_invocation, unnecessary_parenthesis, unnecessary_raw_strings, unnecessary_null_checks, join_return_with_assignment, prefer_final_locals, avoid_js_rounded_ints, avoid_positional_boolean_parameters, always_specify_types - -extension GetStoreValueCollection on Isar { - IsarCollection get storeValues => this.collection(); -} - -const StoreValueSchema = CollectionSchema( - name: r'StoreValue', - id: 902899285492123510, - properties: { - r'intValue': PropertySchema(id: 0, name: r'intValue', type: IsarType.long), - r'strValue': PropertySchema( - id: 1, - name: r'strValue', - type: IsarType.string, - ), - }, - - estimateSize: _storeValueEstimateSize, - serialize: _storeValueSerialize, - deserialize: _storeValueDeserialize, - deserializeProp: _storeValueDeserializeProp, - idName: r'id', - indexes: {}, - links: {}, - embeddedSchemas: {}, - - getId: _storeValueGetId, - getLinks: _storeValueGetLinks, - attach: _storeValueAttach, - version: '3.3.0-dev.3', -); - -int _storeValueEstimateSize( - StoreValue object, - List offsets, - Map> allOffsets, -) { - var bytesCount = offsets.last; - { - final value = object.strValue; - if (value != null) { - bytesCount += 3 + value.length * 3; - } - } - return bytesCount; -} - -void _storeValueSerialize( - StoreValue object, - IsarWriter writer, - List offsets, - Map> allOffsets, -) { - writer.writeLong(offsets[0], object.intValue); - writer.writeString(offsets[1], object.strValue); -} - -StoreValue _storeValueDeserialize( - Id id, - IsarReader reader, - List offsets, - Map> allOffsets, -) { - final object = StoreValue( - id, - intValue: reader.readLongOrNull(offsets[0]), - strValue: reader.readStringOrNull(offsets[1]), - ); - return object; -} - -P _storeValueDeserializeProp

( - IsarReader reader, - int propertyId, - int offset, - Map> allOffsets, -) { - switch (propertyId) { - case 0: - return (reader.readLongOrNull(offset)) as P; - case 1: - return (reader.readStringOrNull(offset)) as P; - default: - throw IsarError('Unknown property with id $propertyId'); - } -} - -Id _storeValueGetId(StoreValue object) { - return object.id; -} - -List> _storeValueGetLinks(StoreValue object) { - return []; -} - -void _storeValueAttach(IsarCollection col, Id id, StoreValue object) {} - -extension StoreValueQueryWhereSort - on QueryBuilder { - QueryBuilder anyId() { - return QueryBuilder.apply(this, (query) { - return query.addWhereClause(const IdWhereClause.any()); - }); - } -} - -extension StoreValueQueryWhere - on QueryBuilder { - QueryBuilder idEqualTo(Id id) { - return QueryBuilder.apply(this, (query) { - return query.addWhereClause(IdWhereClause.between(lower: id, upper: id)); - }); - } - - QueryBuilder idNotEqualTo(Id id) { - return QueryBuilder.apply(this, (query) { - if (query.whereSort == Sort.asc) { - return query - .addWhereClause( - IdWhereClause.lessThan(upper: id, includeUpper: false), - ) - .addWhereClause( - IdWhereClause.greaterThan(lower: id, includeLower: false), - ); - } else { - return query - .addWhereClause( - IdWhereClause.greaterThan(lower: id, includeLower: false), - ) - .addWhereClause( - IdWhereClause.lessThan(upper: id, includeUpper: false), - ); - } - }); - } - - QueryBuilder idGreaterThan( - Id id, { - bool include = false, - }) { - return QueryBuilder.apply(this, (query) { - return query.addWhereClause( - IdWhereClause.greaterThan(lower: id, includeLower: include), - ); - }); - } - - QueryBuilder idLessThan( - Id id, { - bool include = false, - }) { - return QueryBuilder.apply(this, (query) { - return query.addWhereClause( - IdWhereClause.lessThan(upper: id, includeUpper: include), - ); - }); - } - - QueryBuilder idBetween( - Id lowerId, - Id upperId, { - bool includeLower = true, - bool includeUpper = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addWhereClause( - IdWhereClause.between( - lower: lowerId, - includeLower: includeLower, - upper: upperId, - includeUpper: includeUpper, - ), - ); - }); - } -} - -extension StoreValueQueryFilter - on QueryBuilder { - QueryBuilder idEqualTo( - Id value, - ) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.equalTo(property: r'id', value: value), - ); - }); - } - - QueryBuilder idGreaterThan( - Id value, { - bool include = false, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.greaterThan( - include: include, - property: r'id', - value: value, - ), - ); - }); - } - - QueryBuilder idLessThan( - Id value, { - bool include = false, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.lessThan( - include: include, - property: r'id', - value: value, - ), - ); - }); - } - - QueryBuilder idBetween( - Id lower, - Id upper, { - bool includeLower = true, - bool includeUpper = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.between( - property: r'id', - lower: lower, - includeLower: includeLower, - upper: upper, - includeUpper: includeUpper, - ), - ); - }); - } - - QueryBuilder intValueIsNull() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - const FilterCondition.isNull(property: r'intValue'), - ); - }); - } - - QueryBuilder - intValueIsNotNull() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - const FilterCondition.isNotNull(property: r'intValue'), - ); - }); - } - - QueryBuilder intValueEqualTo( - int? value, - ) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.equalTo(property: r'intValue', value: value), - ); - }); - } - - QueryBuilder - intValueGreaterThan(int? value, {bool include = false}) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.greaterThan( - include: include, - property: r'intValue', - value: value, - ), - ); - }); - } - - QueryBuilder intValueLessThan( - int? value, { - bool include = false, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.lessThan( - include: include, - property: r'intValue', - value: value, - ), - ); - }); - } - - QueryBuilder intValueBetween( - int? lower, - int? upper, { - bool includeLower = true, - bool includeUpper = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.between( - property: r'intValue', - lower: lower, - includeLower: includeLower, - upper: upper, - includeUpper: includeUpper, - ), - ); - }); - } - - QueryBuilder strValueIsNull() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - const FilterCondition.isNull(property: r'strValue'), - ); - }); - } - - QueryBuilder - strValueIsNotNull() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - const FilterCondition.isNotNull(property: r'strValue'), - ); - }); - } - - QueryBuilder strValueEqualTo( - String? value, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.equalTo( - property: r'strValue', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder - strValueGreaterThan( - String? value, { - bool include = false, - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.greaterThan( - include: include, - property: r'strValue', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder strValueLessThan( - String? value, { - bool include = false, - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.lessThan( - include: include, - property: r'strValue', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder strValueBetween( - String? lower, - String? upper, { - bool includeLower = true, - bool includeUpper = true, - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.between( - property: r'strValue', - lower: lower, - includeLower: includeLower, - upper: upper, - includeUpper: includeUpper, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder - strValueStartsWith(String value, {bool caseSensitive = true}) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.startsWith( - property: r'strValue', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder strValueEndsWith( - String value, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.endsWith( - property: r'strValue', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder strValueContains( - String value, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.contains( - property: r'strValue', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder strValueMatches( - String pattern, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.matches( - property: r'strValue', - wildcard: pattern, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder - strValueIsEmpty() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.equalTo(property: r'strValue', value: ''), - ); - }); - } - - QueryBuilder - strValueIsNotEmpty() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.greaterThan(property: r'strValue', value: ''), - ); - }); - } -} - -extension StoreValueQueryObject - on QueryBuilder {} - -extension StoreValueQueryLinks - on QueryBuilder {} - -extension StoreValueQuerySortBy - on QueryBuilder { - QueryBuilder sortByIntValue() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'intValue', Sort.asc); - }); - } - - QueryBuilder sortByIntValueDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'intValue', Sort.desc); - }); - } - - QueryBuilder sortByStrValue() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'strValue', Sort.asc); - }); - } - - QueryBuilder sortByStrValueDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'strValue', Sort.desc); - }); - } -} - -extension StoreValueQuerySortThenBy - on QueryBuilder { - QueryBuilder thenById() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'id', Sort.asc); - }); - } - - QueryBuilder thenByIdDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'id', Sort.desc); - }); - } - - QueryBuilder thenByIntValue() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'intValue', Sort.asc); - }); - } - - QueryBuilder thenByIntValueDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'intValue', Sort.desc); - }); - } - - QueryBuilder thenByStrValue() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'strValue', Sort.asc); - }); - } - - QueryBuilder thenByStrValueDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'strValue', Sort.desc); - }); - } -} - -extension StoreValueQueryWhereDistinct - on QueryBuilder { - QueryBuilder distinctByIntValue() { - return QueryBuilder.apply(this, (query) { - return query.addDistinctBy(r'intValue'); - }); - } - - QueryBuilder distinctByStrValue({ - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addDistinctBy(r'strValue', caseSensitive: caseSensitive); - }); - } -} - -extension StoreValueQueryProperty - on QueryBuilder { - QueryBuilder idProperty() { - return QueryBuilder.apply(this, (query) { - return query.addPropertyName(r'id'); - }); - } - - QueryBuilder intValueProperty() { - return QueryBuilder.apply(this, (query) { - return query.addPropertyName(r'intValue'); - }); - } - - QueryBuilder strValueProperty() { - return QueryBuilder.apply(this, (query) { - return query.addPropertyName(r'strValue'); - }); - } -} diff --git a/mobile/lib/infrastructure/entities/user.entity.dart b/mobile/lib/infrastructure/entities/user.entity.dart index 667a9d6a59..8d4371672c 100644 --- a/mobile/lib/infrastructure/entities/user.entity.dart +++ b/mobile/lib/infrastructure/entities/user.entity.dart @@ -1,79 +1,6 @@ import 'package:drift/drift.dart' hide Index; import 'package:immich_mobile/domain/models/user.model.dart'; import 'package:immich_mobile/infrastructure/utils/drift_default.mixin.dart'; -import 'package:immich_mobile/utils/hash.dart'; -import 'package:isar/isar.dart'; - -part 'user.entity.g.dart'; - -@Collection(inheritance: false) -class User { - Id get isarId => fastHash(id); - @Index(unique: true, replace: false, type: IndexType.hash) - final String id; - final DateTime updatedAt; - final String email; - final String name; - final bool isPartnerSharedBy; - final bool isPartnerSharedWith; - final bool isAdmin; - final String profileImagePath; - @Enumerated(EnumType.ordinal) - final AvatarColor avatarColor; - final bool memoryEnabled; - final bool inTimeline; - final int quotaUsageInBytes; - final int quotaSizeInBytes; - - const User({ - required this.id, - required this.updatedAt, - required this.email, - required this.name, - required this.isAdmin, - this.isPartnerSharedBy = false, - this.isPartnerSharedWith = false, - this.profileImagePath = '', - this.avatarColor = AvatarColor.primary, - this.memoryEnabled = true, - this.inTimeline = false, - this.quotaUsageInBytes = 0, - this.quotaSizeInBytes = 0, - }); - - static User fromDto(UserDto dto) => User( - id: dto.id, - updatedAt: dto.updatedAt ?? DateTime(2025), - email: dto.email, - name: dto.name, - isAdmin: dto.isAdmin, - isPartnerSharedBy: dto.isPartnerSharedBy, - isPartnerSharedWith: dto.isPartnerSharedWith, - profileImagePath: dto.hasProfileImage ? "HAS_PROFILE_IMAGE" : "", - avatarColor: dto.avatarColor, - memoryEnabled: dto.memoryEnabled, - inTimeline: dto.inTimeline, - quotaUsageInBytes: dto.quotaUsageInBytes, - quotaSizeInBytes: dto.quotaSizeInBytes, - ); - - UserDto toDto() => UserDto( - id: id, - email: email, - name: name, - isAdmin: isAdmin, - updatedAt: updatedAt, - avatarColor: avatarColor, - memoryEnabled: memoryEnabled, - inTimeline: inTimeline, - isPartnerSharedBy: isPartnerSharedBy, - isPartnerSharedWith: isPartnerSharedWith, - hasProfileImage: profileImagePath.isNotEmpty, - profileChangedAt: updatedAt, - quotaUsageInBytes: quotaUsageInBytes, - quotaSizeInBytes: quotaSizeInBytes, - ); -} class UserEntity extends Table with DriftDefaultsMixin { const UserEntity(); diff --git a/mobile/lib/infrastructure/entities/user.entity.g.dart b/mobile/lib/infrastructure/entities/user.entity.g.dart deleted file mode 100644 index 7e0af41b77..0000000000 --- a/mobile/lib/infrastructure/entities/user.entity.g.dart +++ /dev/null @@ -1,1854 +0,0 @@ -// GENERATED CODE - DO NOT MODIFY BY HAND - -part of 'user.entity.dart'; - -// ************************************************************************** -// IsarCollectionGenerator -// ************************************************************************** - -// coverage:ignore-file -// ignore_for_file: duplicate_ignore, non_constant_identifier_names, constant_identifier_names, invalid_use_of_protected_member, unnecessary_cast, prefer_const_constructors, lines_longer_than_80_chars, require_trailing_commas, inference_failure_on_function_invocation, unnecessary_parenthesis, unnecessary_raw_strings, unnecessary_null_checks, join_return_with_assignment, prefer_final_locals, avoid_js_rounded_ints, avoid_positional_boolean_parameters, always_specify_types - -extension GetUserCollection on Isar { - IsarCollection get users => this.collection(); -} - -const UserSchema = CollectionSchema( - name: r'User', - id: -7838171048429979076, - properties: { - r'avatarColor': PropertySchema( - id: 0, - name: r'avatarColor', - type: IsarType.byte, - enumMap: _UseravatarColorEnumValueMap, - ), - r'email': PropertySchema(id: 1, name: r'email', type: IsarType.string), - r'id': PropertySchema(id: 2, name: r'id', type: IsarType.string), - r'inTimeline': PropertySchema( - id: 3, - name: r'inTimeline', - type: IsarType.bool, - ), - r'isAdmin': PropertySchema(id: 4, name: r'isAdmin', type: IsarType.bool), - r'isPartnerSharedBy': PropertySchema( - id: 5, - name: r'isPartnerSharedBy', - type: IsarType.bool, - ), - r'isPartnerSharedWith': PropertySchema( - id: 6, - name: r'isPartnerSharedWith', - type: IsarType.bool, - ), - r'memoryEnabled': PropertySchema( - id: 7, - name: r'memoryEnabled', - type: IsarType.bool, - ), - r'name': PropertySchema(id: 8, name: r'name', type: IsarType.string), - r'profileImagePath': PropertySchema( - id: 9, - name: r'profileImagePath', - type: IsarType.string, - ), - r'quotaSizeInBytes': PropertySchema( - id: 10, - name: r'quotaSizeInBytes', - type: IsarType.long, - ), - r'quotaUsageInBytes': PropertySchema( - id: 11, - name: r'quotaUsageInBytes', - type: IsarType.long, - ), - r'updatedAt': PropertySchema( - id: 12, - name: r'updatedAt', - type: IsarType.dateTime, - ), - }, - - estimateSize: _userEstimateSize, - serialize: _userSerialize, - deserialize: _userDeserialize, - deserializeProp: _userDeserializeProp, - idName: r'isarId', - indexes: { - r'id': IndexSchema( - id: -3268401673993471357, - name: r'id', - unique: true, - replace: false, - properties: [ - IndexPropertySchema( - name: r'id', - type: IndexType.hash, - caseSensitive: true, - ), - ], - ), - }, - links: {}, - embeddedSchemas: {}, - - getId: _userGetId, - getLinks: _userGetLinks, - attach: _userAttach, - version: '3.3.0-dev.3', -); - -int _userEstimateSize( - User object, - List offsets, - Map> allOffsets, -) { - var bytesCount = offsets.last; - bytesCount += 3 + object.email.length * 3; - bytesCount += 3 + object.id.length * 3; - bytesCount += 3 + object.name.length * 3; - bytesCount += 3 + object.profileImagePath.length * 3; - return bytesCount; -} - -void _userSerialize( - User object, - IsarWriter writer, - List offsets, - Map> allOffsets, -) { - writer.writeByte(offsets[0], object.avatarColor.index); - writer.writeString(offsets[1], object.email); - writer.writeString(offsets[2], object.id); - writer.writeBool(offsets[3], object.inTimeline); - writer.writeBool(offsets[4], object.isAdmin); - writer.writeBool(offsets[5], object.isPartnerSharedBy); - writer.writeBool(offsets[6], object.isPartnerSharedWith); - writer.writeBool(offsets[7], object.memoryEnabled); - writer.writeString(offsets[8], object.name); - writer.writeString(offsets[9], object.profileImagePath); - writer.writeLong(offsets[10], object.quotaSizeInBytes); - writer.writeLong(offsets[11], object.quotaUsageInBytes); - writer.writeDateTime(offsets[12], object.updatedAt); -} - -User _userDeserialize( - Id id, - IsarReader reader, - List offsets, - Map> allOffsets, -) { - final object = User( - avatarColor: - _UseravatarColorValueEnumMap[reader.readByteOrNull(offsets[0])] ?? - AvatarColor.primary, - email: reader.readString(offsets[1]), - id: reader.readString(offsets[2]), - inTimeline: reader.readBoolOrNull(offsets[3]) ?? false, - isAdmin: reader.readBool(offsets[4]), - isPartnerSharedBy: reader.readBoolOrNull(offsets[5]) ?? false, - isPartnerSharedWith: reader.readBoolOrNull(offsets[6]) ?? false, - memoryEnabled: reader.readBoolOrNull(offsets[7]) ?? true, - name: reader.readString(offsets[8]), - profileImagePath: reader.readStringOrNull(offsets[9]) ?? '', - quotaSizeInBytes: reader.readLongOrNull(offsets[10]) ?? 0, - quotaUsageInBytes: reader.readLongOrNull(offsets[11]) ?? 0, - updatedAt: reader.readDateTime(offsets[12]), - ); - return object; -} - -P _userDeserializeProp

( - IsarReader reader, - int propertyId, - int offset, - Map> allOffsets, -) { - switch (propertyId) { - case 0: - return (_UseravatarColorValueEnumMap[reader.readByteOrNull(offset)] ?? - AvatarColor.primary) - as P; - case 1: - return (reader.readString(offset)) as P; - case 2: - return (reader.readString(offset)) as P; - case 3: - return (reader.readBoolOrNull(offset) ?? false) as P; - case 4: - return (reader.readBool(offset)) as P; - case 5: - return (reader.readBoolOrNull(offset) ?? false) as P; - case 6: - return (reader.readBoolOrNull(offset) ?? false) as P; - case 7: - return (reader.readBoolOrNull(offset) ?? true) as P; - case 8: - return (reader.readString(offset)) as P; - case 9: - return (reader.readStringOrNull(offset) ?? '') as P; - case 10: - return (reader.readLongOrNull(offset) ?? 0) as P; - case 11: - return (reader.readLongOrNull(offset) ?? 0) as P; - case 12: - return (reader.readDateTime(offset)) as P; - default: - throw IsarError('Unknown property with id $propertyId'); - } -} - -const _UseravatarColorEnumValueMap = { - 'primary': 0, - 'pink': 1, - 'red': 2, - 'yellow': 3, - 'blue': 4, - 'green': 5, - 'purple': 6, - 'orange': 7, - 'gray': 8, - 'amber': 9, -}; -const _UseravatarColorValueEnumMap = { - 0: AvatarColor.primary, - 1: AvatarColor.pink, - 2: AvatarColor.red, - 3: AvatarColor.yellow, - 4: AvatarColor.blue, - 5: AvatarColor.green, - 6: AvatarColor.purple, - 7: AvatarColor.orange, - 8: AvatarColor.gray, - 9: AvatarColor.amber, -}; - -Id _userGetId(User object) { - return object.isarId; -} - -List> _userGetLinks(User object) { - return []; -} - -void _userAttach(IsarCollection col, Id id, User object) {} - -extension UserByIndex on IsarCollection { - Future getById(String id) { - return getByIndex(r'id', [id]); - } - - User? getByIdSync(String id) { - return getByIndexSync(r'id', [id]); - } - - Future deleteById(String id) { - return deleteByIndex(r'id', [id]); - } - - bool deleteByIdSync(String id) { - return deleteByIndexSync(r'id', [id]); - } - - Future> getAllById(List idValues) { - final values = idValues.map((e) => [e]).toList(); - return getAllByIndex(r'id', values); - } - - List getAllByIdSync(List idValues) { - final values = idValues.map((e) => [e]).toList(); - return getAllByIndexSync(r'id', values); - } - - Future deleteAllById(List idValues) { - final values = idValues.map((e) => [e]).toList(); - return deleteAllByIndex(r'id', values); - } - - int deleteAllByIdSync(List idValues) { - final values = idValues.map((e) => [e]).toList(); - return deleteAllByIndexSync(r'id', values); - } - - Future putById(User object) { - return putByIndex(r'id', object); - } - - Id putByIdSync(User object, {bool saveLinks = true}) { - return putByIndexSync(r'id', object, saveLinks: saveLinks); - } - - Future> putAllById(List objects) { - return putAllByIndex(r'id', objects); - } - - List putAllByIdSync(List objects, {bool saveLinks = true}) { - return putAllByIndexSync(r'id', objects, saveLinks: saveLinks); - } -} - -extension UserQueryWhereSort on QueryBuilder { - QueryBuilder anyIsarId() { - return QueryBuilder.apply(this, (query) { - return query.addWhereClause(const IdWhereClause.any()); - }); - } -} - -extension UserQueryWhere on QueryBuilder { - QueryBuilder isarIdEqualTo(Id isarId) { - return QueryBuilder.apply(this, (query) { - return query.addWhereClause( - IdWhereClause.between(lower: isarId, upper: isarId), - ); - }); - } - - QueryBuilder isarIdNotEqualTo(Id isarId) { - return QueryBuilder.apply(this, (query) { - if (query.whereSort == Sort.asc) { - return query - .addWhereClause( - IdWhereClause.lessThan(upper: isarId, includeUpper: false), - ) - .addWhereClause( - IdWhereClause.greaterThan(lower: isarId, includeLower: false), - ); - } else { - return query - .addWhereClause( - IdWhereClause.greaterThan(lower: isarId, includeLower: false), - ) - .addWhereClause( - IdWhereClause.lessThan(upper: isarId, includeUpper: false), - ); - } - }); - } - - QueryBuilder isarIdGreaterThan( - Id isarId, { - bool include = false, - }) { - return QueryBuilder.apply(this, (query) { - return query.addWhereClause( - IdWhereClause.greaterThan(lower: isarId, includeLower: include), - ); - }); - } - - QueryBuilder isarIdLessThan( - Id isarId, { - bool include = false, - }) { - return QueryBuilder.apply(this, (query) { - return query.addWhereClause( - IdWhereClause.lessThan(upper: isarId, includeUpper: include), - ); - }); - } - - QueryBuilder isarIdBetween( - Id lowerIsarId, - Id upperIsarId, { - bool includeLower = true, - bool includeUpper = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addWhereClause( - IdWhereClause.between( - lower: lowerIsarId, - includeLower: includeLower, - upper: upperIsarId, - includeUpper: includeUpper, - ), - ); - }); - } - - QueryBuilder idEqualTo(String id) { - return QueryBuilder.apply(this, (query) { - return query.addWhereClause( - IndexWhereClause.equalTo(indexName: r'id', value: [id]), - ); - }); - } - - QueryBuilder idNotEqualTo(String id) { - return QueryBuilder.apply(this, (query) { - if (query.whereSort == Sort.asc) { - return query - .addWhereClause( - IndexWhereClause.between( - indexName: r'id', - lower: [], - upper: [id], - includeUpper: false, - ), - ) - .addWhereClause( - IndexWhereClause.between( - indexName: r'id', - lower: [id], - includeLower: false, - upper: [], - ), - ); - } else { - return query - .addWhereClause( - IndexWhereClause.between( - indexName: r'id', - lower: [id], - includeLower: false, - upper: [], - ), - ) - .addWhereClause( - IndexWhereClause.between( - indexName: r'id', - lower: [], - upper: [id], - includeUpper: false, - ), - ); - } - }); - } -} - -extension UserQueryFilter on QueryBuilder { - QueryBuilder avatarColorEqualTo( - AvatarColor value, - ) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.equalTo(property: r'avatarColor', value: value), - ); - }); - } - - QueryBuilder avatarColorGreaterThan( - AvatarColor value, { - bool include = false, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.greaterThan( - include: include, - property: r'avatarColor', - value: value, - ), - ); - }); - } - - QueryBuilder avatarColorLessThan( - AvatarColor value, { - bool include = false, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.lessThan( - include: include, - property: r'avatarColor', - value: value, - ), - ); - }); - } - - QueryBuilder avatarColorBetween( - AvatarColor lower, - AvatarColor upper, { - bool includeLower = true, - bool includeUpper = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.between( - property: r'avatarColor', - lower: lower, - includeLower: includeLower, - upper: upper, - includeUpper: includeUpper, - ), - ); - }); - } - - QueryBuilder emailEqualTo( - String value, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.equalTo( - property: r'email', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder emailGreaterThan( - String value, { - bool include = false, - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.greaterThan( - include: include, - property: r'email', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder emailLessThan( - String value, { - bool include = false, - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.lessThan( - include: include, - property: r'email', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder emailBetween( - String lower, - String upper, { - bool includeLower = true, - bool includeUpper = true, - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.between( - property: r'email', - lower: lower, - includeLower: includeLower, - upper: upper, - includeUpper: includeUpper, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder emailStartsWith( - String value, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.startsWith( - property: r'email', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder emailEndsWith( - String value, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.endsWith( - property: r'email', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder emailContains( - String value, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.contains( - property: r'email', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder emailMatches( - String pattern, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.matches( - property: r'email', - wildcard: pattern, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder emailIsEmpty() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.equalTo(property: r'email', value: ''), - ); - }); - } - - QueryBuilder emailIsNotEmpty() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.greaterThan(property: r'email', value: ''), - ); - }); - } - - QueryBuilder idEqualTo( - String value, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.equalTo( - property: r'id', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder idGreaterThan( - String value, { - bool include = false, - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.greaterThan( - include: include, - property: r'id', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder idLessThan( - String value, { - bool include = false, - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.lessThan( - include: include, - property: r'id', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder idBetween( - String lower, - String upper, { - bool includeLower = true, - bool includeUpper = true, - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.between( - property: r'id', - lower: lower, - includeLower: includeLower, - upper: upper, - includeUpper: includeUpper, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder idStartsWith( - String value, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.startsWith( - property: r'id', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder idEndsWith( - String value, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.endsWith( - property: r'id', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder idContains( - String value, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.contains( - property: r'id', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder idMatches( - String pattern, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.matches( - property: r'id', - wildcard: pattern, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder idIsEmpty() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.equalTo(property: r'id', value: ''), - ); - }); - } - - QueryBuilder idIsNotEmpty() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.greaterThan(property: r'id', value: ''), - ); - }); - } - - QueryBuilder inTimelineEqualTo( - bool value, - ) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.equalTo(property: r'inTimeline', value: value), - ); - }); - } - - QueryBuilder isAdminEqualTo(bool value) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.equalTo(property: r'isAdmin', value: value), - ); - }); - } - - QueryBuilder isPartnerSharedByEqualTo( - bool value, - ) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.equalTo(property: r'isPartnerSharedBy', value: value), - ); - }); - } - - QueryBuilder isPartnerSharedWithEqualTo( - bool value, - ) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.equalTo(property: r'isPartnerSharedWith', value: value), - ); - }); - } - - QueryBuilder isarIdEqualTo(Id value) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.equalTo(property: r'isarId', value: value), - ); - }); - } - - QueryBuilder isarIdGreaterThan( - Id value, { - bool include = false, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.greaterThan( - include: include, - property: r'isarId', - value: value, - ), - ); - }); - } - - QueryBuilder isarIdLessThan( - Id value, { - bool include = false, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.lessThan( - include: include, - property: r'isarId', - value: value, - ), - ); - }); - } - - QueryBuilder isarIdBetween( - Id lower, - Id upper, { - bool includeLower = true, - bool includeUpper = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.between( - property: r'isarId', - lower: lower, - includeLower: includeLower, - upper: upper, - includeUpper: includeUpper, - ), - ); - }); - } - - QueryBuilder memoryEnabledEqualTo( - bool value, - ) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.equalTo(property: r'memoryEnabled', value: value), - ); - }); - } - - QueryBuilder nameEqualTo( - String value, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.equalTo( - property: r'name', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder nameGreaterThan( - String value, { - bool include = false, - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.greaterThan( - include: include, - property: r'name', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder nameLessThan( - String value, { - bool include = false, - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.lessThan( - include: include, - property: r'name', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder nameBetween( - String lower, - String upper, { - bool includeLower = true, - bool includeUpper = true, - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.between( - property: r'name', - lower: lower, - includeLower: includeLower, - upper: upper, - includeUpper: includeUpper, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder nameStartsWith( - String value, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.startsWith( - property: r'name', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder nameEndsWith( - String value, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.endsWith( - property: r'name', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder nameContains( - String value, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.contains( - property: r'name', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder nameMatches( - String pattern, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.matches( - property: r'name', - wildcard: pattern, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder nameIsEmpty() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.equalTo(property: r'name', value: ''), - ); - }); - } - - QueryBuilder nameIsNotEmpty() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.greaterThan(property: r'name', value: ''), - ); - }); - } - - QueryBuilder profileImagePathEqualTo( - String value, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.equalTo( - property: r'profileImagePath', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder profileImagePathGreaterThan( - String value, { - bool include = false, - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.greaterThan( - include: include, - property: r'profileImagePath', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder profileImagePathLessThan( - String value, { - bool include = false, - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.lessThan( - include: include, - property: r'profileImagePath', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder profileImagePathBetween( - String lower, - String upper, { - bool includeLower = true, - bool includeUpper = true, - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.between( - property: r'profileImagePath', - lower: lower, - includeLower: includeLower, - upper: upper, - includeUpper: includeUpper, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder profileImagePathStartsWith( - String value, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.startsWith( - property: r'profileImagePath', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder profileImagePathEndsWith( - String value, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.endsWith( - property: r'profileImagePath', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder profileImagePathContains( - String value, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.contains( - property: r'profileImagePath', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder profileImagePathMatches( - String pattern, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.matches( - property: r'profileImagePath', - wildcard: pattern, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder profileImagePathIsEmpty() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.equalTo(property: r'profileImagePath', value: ''), - ); - }); - } - - QueryBuilder profileImagePathIsNotEmpty() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.greaterThan(property: r'profileImagePath', value: ''), - ); - }); - } - - QueryBuilder quotaSizeInBytesEqualTo( - int value, - ) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.equalTo(property: r'quotaSizeInBytes', value: value), - ); - }); - } - - QueryBuilder quotaSizeInBytesGreaterThan( - int value, { - bool include = false, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.greaterThan( - include: include, - property: r'quotaSizeInBytes', - value: value, - ), - ); - }); - } - - QueryBuilder quotaSizeInBytesLessThan( - int value, { - bool include = false, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.lessThan( - include: include, - property: r'quotaSizeInBytes', - value: value, - ), - ); - }); - } - - QueryBuilder quotaSizeInBytesBetween( - int lower, - int upper, { - bool includeLower = true, - bool includeUpper = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.between( - property: r'quotaSizeInBytes', - lower: lower, - includeLower: includeLower, - upper: upper, - includeUpper: includeUpper, - ), - ); - }); - } - - QueryBuilder quotaUsageInBytesEqualTo( - int value, - ) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.equalTo(property: r'quotaUsageInBytes', value: value), - ); - }); - } - - QueryBuilder quotaUsageInBytesGreaterThan( - int value, { - bool include = false, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.greaterThan( - include: include, - property: r'quotaUsageInBytes', - value: value, - ), - ); - }); - } - - QueryBuilder quotaUsageInBytesLessThan( - int value, { - bool include = false, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.lessThan( - include: include, - property: r'quotaUsageInBytes', - value: value, - ), - ); - }); - } - - QueryBuilder quotaUsageInBytesBetween( - int lower, - int upper, { - bool includeLower = true, - bool includeUpper = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.between( - property: r'quotaUsageInBytes', - lower: lower, - includeLower: includeLower, - upper: upper, - includeUpper: includeUpper, - ), - ); - }); - } - - QueryBuilder updatedAtEqualTo( - DateTime value, - ) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.equalTo(property: r'updatedAt', value: value), - ); - }); - } - - QueryBuilder updatedAtGreaterThan( - DateTime value, { - bool include = false, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.greaterThan( - include: include, - property: r'updatedAt', - value: value, - ), - ); - }); - } - - QueryBuilder updatedAtLessThan( - DateTime value, { - bool include = false, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.lessThan( - include: include, - property: r'updatedAt', - value: value, - ), - ); - }); - } - - QueryBuilder updatedAtBetween( - DateTime lower, - DateTime upper, { - bool includeLower = true, - bool includeUpper = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.between( - property: r'updatedAt', - lower: lower, - includeLower: includeLower, - upper: upper, - includeUpper: includeUpper, - ), - ); - }); - } -} - -extension UserQueryObject on QueryBuilder {} - -extension UserQueryLinks on QueryBuilder {} - -extension UserQuerySortBy on QueryBuilder { - QueryBuilder sortByAvatarColor() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'avatarColor', Sort.asc); - }); - } - - QueryBuilder sortByAvatarColorDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'avatarColor', Sort.desc); - }); - } - - QueryBuilder sortByEmail() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'email', Sort.asc); - }); - } - - QueryBuilder sortByEmailDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'email', Sort.desc); - }); - } - - QueryBuilder sortById() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'id', Sort.asc); - }); - } - - QueryBuilder sortByIdDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'id', Sort.desc); - }); - } - - QueryBuilder sortByInTimeline() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'inTimeline', Sort.asc); - }); - } - - QueryBuilder sortByInTimelineDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'inTimeline', Sort.desc); - }); - } - - QueryBuilder sortByIsAdmin() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'isAdmin', Sort.asc); - }); - } - - QueryBuilder sortByIsAdminDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'isAdmin', Sort.desc); - }); - } - - QueryBuilder sortByIsPartnerSharedBy() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'isPartnerSharedBy', Sort.asc); - }); - } - - QueryBuilder sortByIsPartnerSharedByDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'isPartnerSharedBy', Sort.desc); - }); - } - - QueryBuilder sortByIsPartnerSharedWith() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'isPartnerSharedWith', Sort.asc); - }); - } - - QueryBuilder sortByIsPartnerSharedWithDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'isPartnerSharedWith', Sort.desc); - }); - } - - QueryBuilder sortByMemoryEnabled() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'memoryEnabled', Sort.asc); - }); - } - - QueryBuilder sortByMemoryEnabledDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'memoryEnabled', Sort.desc); - }); - } - - QueryBuilder sortByName() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'name', Sort.asc); - }); - } - - QueryBuilder sortByNameDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'name', Sort.desc); - }); - } - - QueryBuilder sortByProfileImagePath() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'profileImagePath', Sort.asc); - }); - } - - QueryBuilder sortByProfileImagePathDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'profileImagePath', Sort.desc); - }); - } - - QueryBuilder sortByQuotaSizeInBytes() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'quotaSizeInBytes', Sort.asc); - }); - } - - QueryBuilder sortByQuotaSizeInBytesDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'quotaSizeInBytes', Sort.desc); - }); - } - - QueryBuilder sortByQuotaUsageInBytes() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'quotaUsageInBytes', Sort.asc); - }); - } - - QueryBuilder sortByQuotaUsageInBytesDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'quotaUsageInBytes', Sort.desc); - }); - } - - QueryBuilder sortByUpdatedAt() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'updatedAt', Sort.asc); - }); - } - - QueryBuilder sortByUpdatedAtDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'updatedAt', Sort.desc); - }); - } -} - -extension UserQuerySortThenBy on QueryBuilder { - QueryBuilder thenByAvatarColor() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'avatarColor', Sort.asc); - }); - } - - QueryBuilder thenByAvatarColorDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'avatarColor', Sort.desc); - }); - } - - QueryBuilder thenByEmail() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'email', Sort.asc); - }); - } - - QueryBuilder thenByEmailDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'email', Sort.desc); - }); - } - - QueryBuilder thenById() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'id', Sort.asc); - }); - } - - QueryBuilder thenByIdDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'id', Sort.desc); - }); - } - - QueryBuilder thenByInTimeline() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'inTimeline', Sort.asc); - }); - } - - QueryBuilder thenByInTimelineDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'inTimeline', Sort.desc); - }); - } - - QueryBuilder thenByIsAdmin() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'isAdmin', Sort.asc); - }); - } - - QueryBuilder thenByIsAdminDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'isAdmin', Sort.desc); - }); - } - - QueryBuilder thenByIsPartnerSharedBy() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'isPartnerSharedBy', Sort.asc); - }); - } - - QueryBuilder thenByIsPartnerSharedByDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'isPartnerSharedBy', Sort.desc); - }); - } - - QueryBuilder thenByIsPartnerSharedWith() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'isPartnerSharedWith', Sort.asc); - }); - } - - QueryBuilder thenByIsPartnerSharedWithDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'isPartnerSharedWith', Sort.desc); - }); - } - - QueryBuilder thenByIsarId() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'isarId', Sort.asc); - }); - } - - QueryBuilder thenByIsarIdDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'isarId', Sort.desc); - }); - } - - QueryBuilder thenByMemoryEnabled() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'memoryEnabled', Sort.asc); - }); - } - - QueryBuilder thenByMemoryEnabledDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'memoryEnabled', Sort.desc); - }); - } - - QueryBuilder thenByName() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'name', Sort.asc); - }); - } - - QueryBuilder thenByNameDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'name', Sort.desc); - }); - } - - QueryBuilder thenByProfileImagePath() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'profileImagePath', Sort.asc); - }); - } - - QueryBuilder thenByProfileImagePathDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'profileImagePath', Sort.desc); - }); - } - - QueryBuilder thenByQuotaSizeInBytes() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'quotaSizeInBytes', Sort.asc); - }); - } - - QueryBuilder thenByQuotaSizeInBytesDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'quotaSizeInBytes', Sort.desc); - }); - } - - QueryBuilder thenByQuotaUsageInBytes() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'quotaUsageInBytes', Sort.asc); - }); - } - - QueryBuilder thenByQuotaUsageInBytesDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'quotaUsageInBytes', Sort.desc); - }); - } - - QueryBuilder thenByUpdatedAt() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'updatedAt', Sort.asc); - }); - } - - QueryBuilder thenByUpdatedAtDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'updatedAt', Sort.desc); - }); - } -} - -extension UserQueryWhereDistinct on QueryBuilder { - QueryBuilder distinctByAvatarColor() { - return QueryBuilder.apply(this, (query) { - return query.addDistinctBy(r'avatarColor'); - }); - } - - QueryBuilder distinctByEmail({ - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addDistinctBy(r'email', caseSensitive: caseSensitive); - }); - } - - QueryBuilder distinctById({ - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addDistinctBy(r'id', caseSensitive: caseSensitive); - }); - } - - QueryBuilder distinctByInTimeline() { - return QueryBuilder.apply(this, (query) { - return query.addDistinctBy(r'inTimeline'); - }); - } - - QueryBuilder distinctByIsAdmin() { - return QueryBuilder.apply(this, (query) { - return query.addDistinctBy(r'isAdmin'); - }); - } - - QueryBuilder distinctByIsPartnerSharedBy() { - return QueryBuilder.apply(this, (query) { - return query.addDistinctBy(r'isPartnerSharedBy'); - }); - } - - QueryBuilder distinctByIsPartnerSharedWith() { - return QueryBuilder.apply(this, (query) { - return query.addDistinctBy(r'isPartnerSharedWith'); - }); - } - - QueryBuilder distinctByMemoryEnabled() { - return QueryBuilder.apply(this, (query) { - return query.addDistinctBy(r'memoryEnabled'); - }); - } - - QueryBuilder distinctByName({ - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addDistinctBy(r'name', caseSensitive: caseSensitive); - }); - } - - QueryBuilder distinctByProfileImagePath({ - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addDistinctBy( - r'profileImagePath', - caseSensitive: caseSensitive, - ); - }); - } - - QueryBuilder distinctByQuotaSizeInBytes() { - return QueryBuilder.apply(this, (query) { - return query.addDistinctBy(r'quotaSizeInBytes'); - }); - } - - QueryBuilder distinctByQuotaUsageInBytes() { - return QueryBuilder.apply(this, (query) { - return query.addDistinctBy(r'quotaUsageInBytes'); - }); - } - - QueryBuilder distinctByUpdatedAt() { - return QueryBuilder.apply(this, (query) { - return query.addDistinctBy(r'updatedAt'); - }); - } -} - -extension UserQueryProperty on QueryBuilder { - QueryBuilder isarIdProperty() { - return QueryBuilder.apply(this, (query) { - return query.addPropertyName(r'isarId'); - }); - } - - QueryBuilder avatarColorProperty() { - return QueryBuilder.apply(this, (query) { - return query.addPropertyName(r'avatarColor'); - }); - } - - QueryBuilder emailProperty() { - return QueryBuilder.apply(this, (query) { - return query.addPropertyName(r'email'); - }); - } - - QueryBuilder idProperty() { - return QueryBuilder.apply(this, (query) { - return query.addPropertyName(r'id'); - }); - } - - QueryBuilder inTimelineProperty() { - return QueryBuilder.apply(this, (query) { - return query.addPropertyName(r'inTimeline'); - }); - } - - QueryBuilder isAdminProperty() { - return QueryBuilder.apply(this, (query) { - return query.addPropertyName(r'isAdmin'); - }); - } - - QueryBuilder isPartnerSharedByProperty() { - return QueryBuilder.apply(this, (query) { - return query.addPropertyName(r'isPartnerSharedBy'); - }); - } - - QueryBuilder isPartnerSharedWithProperty() { - return QueryBuilder.apply(this, (query) { - return query.addPropertyName(r'isPartnerSharedWith'); - }); - } - - QueryBuilder memoryEnabledProperty() { - return QueryBuilder.apply(this, (query) { - return query.addPropertyName(r'memoryEnabled'); - }); - } - - QueryBuilder nameProperty() { - return QueryBuilder.apply(this, (query) { - return query.addPropertyName(r'name'); - }); - } - - QueryBuilder profileImagePathProperty() { - return QueryBuilder.apply(this, (query) { - return query.addPropertyName(r'profileImagePath'); - }); - } - - QueryBuilder quotaSizeInBytesProperty() { - return QueryBuilder.apply(this, (query) { - return query.addPropertyName(r'quotaSizeInBytes'); - }); - } - - QueryBuilder quotaUsageInBytesProperty() { - return QueryBuilder.apply(this, (query) { - return query.addPropertyName(r'quotaUsageInBytes'); - }); - } - - QueryBuilder updatedAtProperty() { - return QueryBuilder.apply(this, (query) { - return query.addPropertyName(r'updatedAt'); - }); - } -} diff --git a/mobile/lib/infrastructure/repositories/db.repository.dart b/mobile/lib/infrastructure/repositories/db.repository.dart index d41891e2ea..eca8810b91 100644 --- a/mobile/lib/infrastructure/repositories/db.repository.dart +++ b/mobile/lib/infrastructure/repositories/db.repository.dart @@ -3,7 +3,6 @@ import 'dart:async'; import 'package:drift/drift.dart'; import 'package:drift_flutter/drift_flutter.dart'; import 'package:flutter/foundation.dart'; -import 'package:immich_mobile/domain/interfaces/db.interface.dart'; import 'package:immich_mobile/infrastructure/entities/asset_edit.entity.dart'; import 'package:immich_mobile/infrastructure/entities/asset_face.entity.dart'; import 'package:immich_mobile/infrastructure/entities/auth_user.entity.dart'; @@ -27,22 +26,6 @@ import 'package:immich_mobile/infrastructure/entities/user.entity.dart'; import 'package:immich_mobile/infrastructure/entities/user_metadata.entity.dart'; import 'package:immich_mobile/infrastructure/repositories/db.repository.drift.dart'; import 'package:immich_mobile/infrastructure/repositories/db.repository.steps.dart'; -import 'package:isar/isar.dart' hide Index; - -// #zoneTxn is the symbol used by Isar to mark a transaction within the current zone -// ref: isar/isar_common.dart -const Symbol _kzoneTxn = #zoneTxn; - -class IsarDatabaseRepository implements IDatabaseRepository { - final Isar _db; - const IsarDatabaseRepository(Isar db) : _db = db; - - // Isar do not support nested transactions. This is a workaround to prevent us from making nested transactions - // Reuse the current transaction if it is already active, else start a new transaction - @override - Future transaction(Future Function() callback) => - Zone.current[_kzoneTxn] == null ? _db.writeTxn(callback) : callback(); -} @DriftDatabase( tables: [ @@ -70,7 +53,7 @@ class IsarDatabaseRepository implements IDatabaseRepository { ], include: {'package:immich_mobile/infrastructure/entities/merged_asset.drift'}, ) -class Drift extends $Drift implements IDatabaseRepository { +class Drift extends $Drift { Drift([QueryExecutor? executor]) : super(executor ?? driftDatabase(name: 'immich', native: const DriftNativeOptions(shareAcrossIsolates: true))); @@ -261,10 +244,9 @@ class Drift extends $Drift implements IDatabaseRepository { ); } -class DriftDatabaseRepository implements IDatabaseRepository { +class DriftDatabaseRepository { final Drift _db; const DriftDatabaseRepository(this._db); - @override Future transaction(Future Function() callback) => _db.transaction(callback); } diff --git a/mobile/lib/infrastructure/repositories/device_asset.repository.dart b/mobile/lib/infrastructure/repositories/device_asset.repository.dart deleted file mode 100644 index 73ee148ab3..0000000000 --- a/mobile/lib/infrastructure/repositories/device_asset.repository.dart +++ /dev/null @@ -1,31 +0,0 @@ -import 'package:immich_mobile/domain/models/device_asset.model.dart'; -import 'package:immich_mobile/infrastructure/entities/device_asset.entity.dart'; -import 'package:immich_mobile/infrastructure/repositories/db.repository.dart'; -import 'package:isar/isar.dart'; - -class IsarDeviceAssetRepository extends IsarDatabaseRepository { - final Isar _db; - - const IsarDeviceAssetRepository(this._db) : super(_db); - - Future deleteIds(List ids) { - return transaction(() async { - await _db.deviceAssetEntitys.deleteAllByAssetId(ids.toList()); - }); - } - - Future> getByIds(List localIds) { - return _db.deviceAssetEntitys - .where() - .anyOf(localIds, (query, id) => query.assetIdEqualTo(id)) - .findAll() - .then((value) => value.map((e) => e.toModel()).toList()); - } - - Future updateAll(List assetHash) { - return transaction(() async { - await _db.deviceAssetEntitys.putAll(assetHash.map(DeviceAssetEntity.fromDto).toList()); - return true; - }); - } -} diff --git a/mobile/lib/infrastructure/repositories/exif.repository.dart b/mobile/lib/infrastructure/repositories/exif.repository.dart deleted file mode 100644 index 0ede30680e..0000000000 --- a/mobile/lib/infrastructure/repositories/exif.repository.dart +++ /dev/null @@ -1,40 +0,0 @@ -import 'package:immich_mobile/domain/models/exif.model.dart'; -import 'package:immich_mobile/infrastructure/entities/exif.entity.dart' as entity; -import 'package:immich_mobile/infrastructure/repositories/db.repository.dart'; -import 'package:isar/isar.dart'; - -class IsarExifRepository extends IsarDatabaseRepository { - final Isar _db; - - const IsarExifRepository(this._db) : super(_db); - - Future delete(int assetId) async { - await transaction(() async { - await _db.exifInfos.delete(assetId); - }); - } - - Future deleteAll() async { - await transaction(() async { - await _db.exifInfos.clear(); - }); - } - - Future get(int assetId) async { - return (await _db.exifInfos.get(assetId))?.toDto(); - } - - Future update(ExifInfo exifInfo) { - return transaction(() async { - await _db.exifInfos.put(entity.ExifInfo.fromDto(exifInfo)); - return exifInfo; - }); - } - - Future> updateAll(List exifInfos) { - return transaction(() async { - await _db.exifInfos.putAll(exifInfos.map(entity.ExifInfo.fromDto).toList()); - return exifInfos; - }); - } -} diff --git a/mobile/lib/infrastructure/repositories/logger_db.repository.dart b/mobile/lib/infrastructure/repositories/logger_db.repository.dart index e494782fa6..d11174356d 100644 --- a/mobile/lib/infrastructure/repositories/logger_db.repository.dart +++ b/mobile/lib/infrastructure/repositories/logger_db.repository.dart @@ -1,11 +1,10 @@ import 'package:drift/drift.dart'; import 'package:drift_flutter/drift_flutter.dart'; -import 'package:immich_mobile/domain/interfaces/db.interface.dart'; import 'package:immich_mobile/infrastructure/entities/log.entity.dart'; import 'package:immich_mobile/infrastructure/repositories/logger_db.repository.drift.dart'; @DriftDatabase(tables: [LogMessageEntity]) -class DriftLogger extends $DriftLogger implements IDatabaseRepository { +class DriftLogger extends $DriftLogger { DriftLogger([QueryExecutor? executor]) : super( executor ?? driftDatabase(name: 'immich_logs', native: const DriftNativeOptions(shareAcrossIsolates: true)), diff --git a/mobile/lib/infrastructure/repositories/remote_asset.repository.dart b/mobile/lib/infrastructure/repositories/remote_asset.repository.dart index 00c0b81850..6d19d17931 100644 --- a/mobile/lib/infrastructure/repositories/remote_asset.repository.dart +++ b/mobile/lib/infrastructure/repositories/remote_asset.repository.dart @@ -4,7 +4,7 @@ import 'package:immich_mobile/domain/models/asset_edit.model.dart'; import 'package:immich_mobile/domain/models/exif.model.dart'; import 'package:immich_mobile/domain/models/stack.model.dart'; import 'package:immich_mobile/infrastructure/entities/asset_edit.entity.dart'; -import 'package:immich_mobile/infrastructure/entities/exif.entity.dart' hide ExifInfo; +import 'package:immich_mobile/infrastructure/entities/exif.entity.dart'; import 'package:immich_mobile/infrastructure/entities/exif.entity.drift.dart'; import 'package:immich_mobile/infrastructure/entities/remote_asset.entity.dart'; import 'package:immich_mobile/infrastructure/entities/remote_asset.entity.drift.dart'; diff --git a/mobile/lib/infrastructure/repositories/store.repository.dart b/mobile/lib/infrastructure/repositories/store.repository.dart index d4e34a02f5..9680aa0425 100644 --- a/mobile/lib/infrastructure/repositories/store.repository.dart +++ b/mobile/lib/infrastructure/repositories/store.repository.dart @@ -1,150 +1,42 @@ import 'package:drift/drift.dart'; import 'package:immich_mobile/domain/models/store.model.dart'; import 'package:immich_mobile/domain/models/user.model.dart'; -import 'package:immich_mobile/infrastructure/entities/store.entity.dart'; import 'package:immich_mobile/infrastructure/entities/store.entity.drift.dart'; import 'package:immich_mobile/infrastructure/repositories/db.repository.dart'; import 'package:immich_mobile/infrastructure/repositories/user.repository.dart'; -import 'package:isar/isar.dart'; -// Temporary interface until Isar is removed to make the service work with both Isar and Sqlite -abstract class IStoreRepository { - Future deleteAll(); - Stream>> watchAll(); - Future delete(StoreKey key); - Future upsert(StoreKey key, T value); - Future tryGet(StoreKey key); - Stream watch(StoreKey key); - Future>> getAll(); -} - -class IsarStoreRepository extends IsarDatabaseRepository implements IStoreRepository { - final Isar _db; - final validStoreKeys = StoreKey.values.map((e) => e.id).toSet(); - - IsarStoreRepository(super.db) : _db = db; - - @override - Future deleteAll() async { - return await transaction(() async { - await _db.storeValues.clear(); - return true; - }); - } - - @override - Stream>> watchAll() { - return _db.storeValues - .filter() - .anyOf(validStoreKeys, (query, id) => query.idEqualTo(id)) - .watch(fireImmediately: true) - .asyncMap((entities) => Future.wait(entities.map((entity) => _toUpdateEvent(entity)))); - } - - @override - Future delete(StoreKey key) async { - return await transaction(() async => await _db.storeValues.delete(key.id)); - } - - @override - Future upsert(StoreKey key, T value) async { - return await transaction(() async { - await _db.storeValues.put(await _fromValue(key, value)); - return true; - }); - } - - @override - Future tryGet(StoreKey key) async { - final entity = (await _db.storeValues.get(key.id)); - if (entity == null) { - return null; - } - return await _toValue(key, entity); - } - - @override - Stream watch(StoreKey key) async* { - yield* _db.storeValues - .watchObject(key.id, fireImmediately: true) - .asyncMap((e) async => e == null ? null : await _toValue(key, e)); - } - - Future> _toUpdateEvent(StoreValue entity) async { - final key = StoreKey.values.firstWhere((e) => e.id == entity.id) as StoreKey; - final value = await _toValue(key, entity); - return StoreDto(key, value); - } - - Future _toValue(StoreKey key, StoreValue entity) async => - switch (key.type) { - const (int) => entity.intValue, - const (String) => entity.strValue, - const (bool) => entity.intValue == 1, - const (DateTime) => entity.intValue == null ? null : DateTime.fromMillisecondsSinceEpoch(entity.intValue!), - const (UserDto) => - entity.strValue == null ? null : await IsarUserRepository(_db).getByUserId(entity.strValue!), - _ => null, - } - as T?; - - Future _fromValue(StoreKey key, T value) async { - final (int? intValue, String? strValue) = switch (key.type) { - const (int) => (value as int, null), - const (String) => (null, value as String), - const (bool) => ((value as bool) ? 1 : 0, null), - const (DateTime) => ((value as DateTime).millisecondsSinceEpoch, null), - const (UserDto) => (null, (await IsarUserRepository(_db).update(value as UserDto)).id), - _ => throw UnsupportedError("Unsupported primitive type: ${key.type} for key: ${key.name}"), - }; - return StoreValue(key.id, intValue: intValue, strValue: strValue); - } - - @override - Future>> getAll() async { - final entities = await _db.storeValues.filter().anyOf(validStoreKeys, (query, id) => query.idEqualTo(id)).findAll(); - return Future.wait(entities.map((e) => _toUpdateEvent(e)).toList()); - } -} - -class DriftStoreRepository extends DriftDatabaseRepository implements IStoreRepository { +class DriftStoreRepository extends DriftDatabaseRepository { final Drift _db; final validStoreKeys = StoreKey.values.map((e) => e.id).toSet(); DriftStoreRepository(super.db) : _db = db; - @override Future deleteAll() async { await _db.storeEntity.deleteAll(); return true; } - @override Future>> getAll() async { final query = _db.storeEntity.select()..where((entity) => entity.id.isIn(validStoreKeys)); return query.asyncMap((entity) => _toUpdateEvent(entity)).get(); } - @override Stream>> watchAll() { final query = _db.storeEntity.select()..where((entity) => entity.id.isIn(validStoreKeys)); return query.asyncMap((entity) => _toUpdateEvent(entity)).watch(); } - @override Future delete(StoreKey key) async { await _db.storeEntity.deleteWhere((entity) => entity.id.equals(key.id)); return; } - @override Future upsert(StoreKey key, T value) async { await _db.storeEntity.insertOnConflictUpdate(await _fromValue(key, value)); return true; } - @override Future tryGet(StoreKey key) async { final entity = await _db.managers.storeEntity.filter((entity) => entity.id.equals(key.id)).getSingleOrNull(); if (entity == null) { @@ -153,7 +45,6 @@ class DriftStoreRepository extends DriftDatabaseRepository implements IStoreRepo return await _toValue(key, entity); } - @override Stream watch(StoreKey key) async* { final query = _db.storeEntity.select()..where((entity) => entity.id.equals(key.id)); diff --git a/mobile/lib/infrastructure/repositories/user.repository.dart b/mobile/lib/infrastructure/repositories/user.repository.dart index d4eb1ceed6..ce7cb124db 100644 --- a/mobile/lib/infrastructure/repositories/user.repository.dart +++ b/mobile/lib/infrastructure/repositories/user.repository.dart @@ -1,72 +1,9 @@ import 'package:drift/drift.dart'; -import 'package:immich_mobile/constants/enums.dart'; import 'package:immich_mobile/domain/models/user.model.dart'; import 'package:immich_mobile/domain/models/user_metadata.model.dart'; import 'package:immich_mobile/infrastructure/entities/auth_user.entity.drift.dart'; -import 'package:immich_mobile/infrastructure/entities/user.entity.dart' as entity; import 'package:immich_mobile/infrastructure/repositories/db.repository.dart'; import 'package:immich_mobile/infrastructure/repositories/user_metadata.repository.dart'; -import 'package:isar/isar.dart'; - -class IsarUserRepository extends IsarDatabaseRepository { - final Isar _db; - const IsarUserRepository(super.db) : _db = db; - - Future delete(List ids) async { - await transaction(() async { - await _db.users.deleteAllById(ids); - }); - } - - Future deleteAll() async { - await transaction(() async { - await _db.users.clear(); - }); - } - - Future> getAll({SortUserBy? sortBy}) async { - return (await _db.users - .where() - .optional( - sortBy != null, - (query) => switch (sortBy!) { - SortUserBy.id => query.sortById(), - }, - ) - .findAll()) - .map((u) => u.toDto()) - .toList(); - } - - Future getByUserId(String id) async { - return (await _db.users.getById(id))?.toDto(); - } - - Future> getByUserIds(List ids) async { - return (await _db.users.getAllById(ids)).map((u) => u?.toDto()).toList(); - } - - Future insert(UserDto user) async { - await transaction(() async { - await _db.users.put(entity.User.fromDto(user)); - }); - return true; - } - - Future update(UserDto user) async { - await transaction(() async { - await _db.users.put(entity.User.fromDto(user)); - }); - return user; - } - - Future updateAll(List users) async { - await transaction(() async { - await _db.users.putAll(users.map(entity.User.fromDto).toList()); - }); - return true; - } -} class DriftAuthUserRepository extends DriftDatabaseRepository { final Drift _db; @@ -117,6 +54,7 @@ extension on AuthUserEntityData { id: id, email: email, name: name, + updatedAt: profileChangedAt, profileChangedAt: profileChangedAt, hasProfileImage: hasProfileImage, avatarColor: avatarColor, diff --git a/mobile/lib/main.dart b/mobile/lib/main.dart index 7e7c709eeb..4a284b9bda 100644 --- a/mobile/lib/main.dart +++ b/mobile/lib/main.dart @@ -14,7 +14,6 @@ import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/constants/constants.dart'; import 'package:immich_mobile/constants/locales.dart'; import 'package:immich_mobile/domain/services/background_worker.service.dart'; -import 'package:immich_mobile/entities/store.entity.dart'; import 'package:immich_mobile/extensions/build_context_extensions.dart'; import 'package:immich_mobile/extensions/translate_extensions.dart'; import 'package:immich_mobile/generated/codegen_loader.g.dart'; @@ -24,7 +23,6 @@ import 'package:immich_mobile/pages/common/splash_screen.page.dart'; import 'package:immich_mobile/platform/background_worker_lock_api.g.dart'; import 'package:immich_mobile/providers/app_life_cycle.provider.dart'; import 'package:immich_mobile/providers/asset_viewer/share_intent_upload.provider.dart'; -import 'package:immich_mobile/providers/db.provider.dart'; import 'package:immich_mobile/providers/infrastructure/db.provider.dart'; import 'package:immich_mobile/providers/infrastructure/platform.provider.dart'; import 'package:immich_mobile/providers/locale_provider.dart'; @@ -32,9 +30,7 @@ import 'package:immich_mobile/providers/routes.provider.dart'; import 'package:immich_mobile/providers/theme.provider.dart'; import 'package:immich_mobile/routing/app_navigation_observer.dart'; import 'package:immich_mobile/routing/router.dart'; -import 'package:immich_mobile/services/background.service.dart'; import 'package:immich_mobile/services/deep_link.service.dart'; -import 'package:immich_mobile/services/local_notification.service.dart'; import 'package:immich_mobile/theme/dynamic_theme.dart'; import 'package:immich_mobile/theme/theme_data.dart'; import 'package:immich_mobile/utils/bootstrap.dart'; @@ -53,23 +49,13 @@ void main() async { ImmichWidgetsBinding(); unawaited(BackgroundWorkerLockService(BackgroundWorkerLockApi()).lock()); await EasyLocalization.ensureInitialized(); - final (isar, drift, logDb) = await Bootstrap.initDB(); - await Bootstrap.initDomain(isar, drift, logDb); + final (drift, _) = await Bootstrap.initDomain(); await initApp(); // Warm-up isolate pool for worker manager await workerManagerPatch.init(dynamicSpawning: true, isolatesCount: max(Platform.numberOfProcessors - 1, 5)); - await migrateDatabaseIfNeeded(isar, drift); + await migrateDatabaseIfNeeded(); - runApp( - ProviderScope( - overrides: [ - dbProvider.overrideWithValue(isar), - isarProvider.overrideWithValue(isar), - driftProvider.overrideWith(driftOverride(drift)), - ], - child: const MainWidget(), - ), - ); + runApp(ProviderScope(overrides: [driftProvider.overrideWith(driftOverride(drift))], child: const MainWidget())); } catch (error, stack) { runApp(BootstrapErrorWidget(error: error.toString(), stack: stack.toString())); } @@ -176,7 +162,6 @@ class ImmichAppState extends ConsumerState with WidgetsBindingObserve } } SystemChrome.setSystemUIOverlayStyle(overlayStyle); - await ref.read(localNotificationService).setup(); } Future _deepLinkBuilder(PlatformDeepLink deepLink) async { @@ -215,20 +200,14 @@ class ImmichAppState extends ConsumerState with WidgetsBindingObserve initApp().then((_) => dPrint(() => "App Init Completed")); WidgetsBinding.instance.addPostFrameCallback((_) { // needs to be delayed so that EasyLocalization is working - if (Store.isBetaTimelineEnabled) { - ref.read(backgroundServiceProvider).disableService(); - ref.read(backgroundWorkerFgServiceProvider).enable(); - if (Platform.isAndroid) { - ref - .read(backgroundWorkerFgServiceProvider) - .saveNotificationMessage( - StaticTranslations.instance.uploading_media, - StaticTranslations.instance.backup_background_service_default_notification, - ); - } - } else { - ref.read(backgroundWorkerFgServiceProvider).disable(); - ref.read(backgroundServiceProvider).resumeServiceIfEnabled(); + ref.read(backgroundWorkerFgServiceProvider).enable(); + if (Platform.isAndroid) { + ref + .read(backgroundWorkerFgServiceProvider) + .saveNotificationMessage( + StaticTranslations.instance.uploading_media, + StaticTranslations.instance.backup_background_service_default_notification, + ); } }); diff --git a/mobile/lib/models/albums/album_add_asset_response.model.dart b/mobile/lib/models/albums/album_add_asset_response.model.dart deleted file mode 100644 index 38dd989af5..0000000000 --- a/mobile/lib/models/albums/album_add_asset_response.model.dart +++ /dev/null @@ -1,38 +0,0 @@ -// ignore_for_file: public_member_api_docs, sort_constructors_first -import 'dart:convert'; - -import 'package:collection/collection.dart'; - -class AlbumAddAssetsResponse { - List alreadyInAlbum; - int successfullyAdded; - - AlbumAddAssetsResponse({required this.alreadyInAlbum, required this.successfullyAdded}); - - AlbumAddAssetsResponse copyWith({List? alreadyInAlbum, int? successfullyAdded}) { - return AlbumAddAssetsResponse( - alreadyInAlbum: alreadyInAlbum ?? this.alreadyInAlbum, - successfullyAdded: successfullyAdded ?? this.successfullyAdded, - ); - } - - Map toMap() { - return {'alreadyInAlbum': alreadyInAlbum, 'successfullyAdded': successfullyAdded}; - } - - String toJson() => json.encode(toMap()); - - @override - String toString() => 'AddAssetsResponse(alreadyInAlbum: $alreadyInAlbum, successfullyAdded: $successfullyAdded)'; - - @override - bool operator ==(covariant AlbumAddAssetsResponse other) { - if (identical(this, other)) return true; - final listEquals = const DeepCollectionEquality().equals; - - return listEquals(other.alreadyInAlbum, alreadyInAlbum) && other.successfullyAdded == successfullyAdded; - } - - @override - int get hashCode => alreadyInAlbum.hashCode ^ successfullyAdded.hashCode; -} diff --git a/mobile/lib/models/albums/album_viewer_page_state.model.dart b/mobile/lib/models/albums/album_viewer_page_state.model.dart deleted file mode 100644 index 70427899ae..0000000000 --- a/mobile/lib/models/albums/album_viewer_page_state.model.dart +++ /dev/null @@ -1,60 +0,0 @@ -import 'dart:convert'; - -class AlbumViewerPageState { - final bool isEditAlbum; - final String editTitleText; - final String editDescriptionText; - - const AlbumViewerPageState({ - required this.isEditAlbum, - required this.editTitleText, - required this.editDescriptionText, - }); - - AlbumViewerPageState copyWith({bool? isEditAlbum, String? editTitleText, String? editDescriptionText}) { - return AlbumViewerPageState( - isEditAlbum: isEditAlbum ?? this.isEditAlbum, - editTitleText: editTitleText ?? this.editTitleText, - editDescriptionText: editDescriptionText ?? this.editDescriptionText, - ); - } - - Map toMap() { - final result = {}; - - result.addAll({'isEditAlbum': isEditAlbum}); - result.addAll({'editTitleText': editTitleText}); - result.addAll({'editDescriptionText': editDescriptionText}); - - return result; - } - - factory AlbumViewerPageState.fromMap(Map map) { - return AlbumViewerPageState( - isEditAlbum: map['isEditAlbum'] ?? false, - editTitleText: map['editTitleText'] ?? '', - editDescriptionText: map['editDescriptionText'] ?? '', - ); - } - - String toJson() => json.encode(toMap()); - - factory AlbumViewerPageState.fromJson(String source) => AlbumViewerPageState.fromMap(json.decode(source)); - - @override - String toString() => - 'AlbumViewerPageState(isEditAlbum: $isEditAlbum, editTitleText: $editTitleText, editDescriptionText: $editDescriptionText)'; - - @override - bool operator ==(Object other) { - if (identical(this, other)) return true; - - return other is AlbumViewerPageState && - other.isEditAlbum == isEditAlbum && - other.editTitleText == editTitleText && - other.editDescriptionText == editDescriptionText; - } - - @override - int get hashCode => isEditAlbum.hashCode ^ editTitleText.hashCode ^ editDescriptionText.hashCode; -} diff --git a/mobile/lib/models/albums/asset_selection_page_result.model.dart b/mobile/lib/models/albums/asset_selection_page_result.model.dart deleted file mode 100644 index cc750f397f..0000000000 --- a/mobile/lib/models/albums/asset_selection_page_result.model.dart +++ /dev/null @@ -1,18 +0,0 @@ -import 'package:collection/collection.dart'; -import 'package:immich_mobile/entities/asset.entity.dart'; - -class AssetSelectionPageResult { - final Set selectedAssets; - - const AssetSelectionPageResult({required this.selectedAssets}); - @override - bool operator ==(Object other) { - if (identical(this, other)) return true; - final setEquals = const DeepCollectionEquality().equals; - - return other is AssetSelectionPageResult && setEquals(other.selectedAssets, selectedAssets); - } - - @override - int get hashCode => selectedAssets.hashCode; -} diff --git a/mobile/lib/models/asset_selection_state.dart b/mobile/lib/models/asset_selection_state.dart deleted file mode 100644 index aded3064ce..0000000000 --- a/mobile/lib/models/asset_selection_state.dart +++ /dev/null @@ -1,47 +0,0 @@ -import 'package:immich_mobile/entities/asset.entity.dart'; - -class AssetSelectionState { - final bool hasRemote; - final bool hasLocal; - final bool hasMerged; - final int selectedCount; - - const AssetSelectionState({ - this.hasRemote = false, - this.hasLocal = false, - this.hasMerged = false, - this.selectedCount = 0, - }); - - AssetSelectionState copyWith({bool? hasRemote, bool? hasLocal, bool? hasMerged, int? selectedCount}) { - return AssetSelectionState( - hasRemote: hasRemote ?? this.hasRemote, - hasLocal: hasLocal ?? this.hasLocal, - hasMerged: hasMerged ?? this.hasMerged, - selectedCount: selectedCount ?? this.selectedCount, - ); - } - - AssetSelectionState.fromSelection(Set selection) - : hasLocal = selection.any((e) => e.storage == AssetState.local), - hasMerged = selection.any((e) => e.storage == AssetState.merged), - hasRemote = selection.any((e) => e.storage == AssetState.remote), - selectedCount = selection.length; - - @override - String toString() => - 'SelectionAssetState(hasRemote: $hasRemote, hasLocal: $hasLocal, hasMerged: $hasMerged, selectedCount: $selectedCount)'; - - @override - bool operator ==(covariant AssetSelectionState other) { - if (identical(this, other)) return true; - - return other.hasRemote == hasRemote && - other.hasLocal == hasLocal && - other.hasMerged == hasMerged && - other.selectedCount == selectedCount; - } - - @override - int get hashCode => hasRemote.hashCode ^ hasLocal.hashCode ^ hasMerged.hashCode ^ selectedCount.hashCode; -} diff --git a/mobile/lib/models/backup/available_album.model.dart b/mobile/lib/models/backup/available_album.model.dart deleted file mode 100644 index 502d0b66be..0000000000 --- a/mobile/lib/models/backup/available_album.model.dart +++ /dev/null @@ -1,35 +0,0 @@ -import 'package:immich_mobile/entities/album.entity.dart'; - -class AvailableAlbum { - final Album album; - final int assetCount; - final DateTime? lastBackup; - const AvailableAlbum({required this.album, required this.assetCount, this.lastBackup}); - - AvailableAlbum copyWith({Album? album, int? assetCount, DateTime? lastBackup}) { - return AvailableAlbum( - album: album ?? this.album, - assetCount: assetCount ?? this.assetCount, - lastBackup: lastBackup ?? this.lastBackup, - ); - } - - String get name => album.name; - - String get id => album.localId!; - - bool get isAll => album.isAll; - - @override - String toString() => 'AvailableAlbum(albumEntity: $album, lastBackup: $lastBackup)'; - - @override - bool operator ==(Object other) { - if (identical(this, other)) return true; - - return other is AvailableAlbum && other.album == album; - } - - @override - int get hashCode => album.hashCode; -} diff --git a/mobile/lib/models/backup/backup_candidate.model.dart b/mobile/lib/models/backup/backup_candidate.model.dart deleted file mode 100644 index 01c257dc05..0000000000 --- a/mobile/lib/models/backup/backup_candidate.model.dart +++ /dev/null @@ -1,19 +0,0 @@ -import 'package:immich_mobile/entities/asset.entity.dart'; - -class BackupCandidate { - BackupCandidate({required this.asset, required this.albumNames}); - - Asset asset; - List albumNames; - - @override - int get hashCode => asset.hashCode; - - @override - bool operator ==(Object other) { - if (other is! BackupCandidate) { - return false; - } - return asset == other.asset; - } -} diff --git a/mobile/lib/models/backup/backup_state.model.dart b/mobile/lib/models/backup/backup_state.model.dart deleted file mode 100644 index 51a17de4fc..0000000000 --- a/mobile/lib/models/backup/backup_state.model.dart +++ /dev/null @@ -1,173 +0,0 @@ -// ignore_for_file: public_member_api_docs, sort_constructors_first - -import 'package:collection/collection.dart'; -import 'package:immich_mobile/models/backup/backup_candidate.model.dart'; - -import 'package:immich_mobile/models/backup/available_album.model.dart'; -import 'package:immich_mobile/models/backup/current_upload_asset.model.dart'; -import 'package:immich_mobile/models/server_info/server_disk_info.model.dart'; - -enum BackUpProgressEnum { idle, inProgress, manualInProgress, inBackground, done } - -class BackUpState { - // enum - final BackUpProgressEnum backupProgress; - final List allAssetsInDatabase; - final double progressInPercentage; - final String progressInFileSize; - final double progressInFileSpeed; - final List progressInFileSpeeds; - final DateTime progressInFileSpeedUpdateTime; - final int progressInFileSpeedUpdateSentBytes; - final double iCloudDownloadProgress; - final ServerDiskInfo serverInfo; - final bool autoBackup; - final bool backgroundBackup; - final bool backupRequireWifi; - final bool backupRequireCharging; - final int backupTriggerDelay; - - /// All available albums on the device - final List availableAlbums; - final Set selectedBackupAlbums; - final Set excludedBackupAlbums; - - /// Assets that are not overlapping in selected backup albums and excluded backup albums - final Set allUniqueAssets; - - /// All assets from the selected albums that have been backup - final Set selectedAlbumsBackupAssetsIds; - - // Current Backup Asset - final CurrentUploadAsset currentUploadAsset; - - const BackUpState({ - required this.backupProgress, - required this.allAssetsInDatabase, - required this.progressInPercentage, - required this.progressInFileSize, - required this.progressInFileSpeed, - required this.progressInFileSpeeds, - required this.progressInFileSpeedUpdateTime, - required this.progressInFileSpeedUpdateSentBytes, - required this.iCloudDownloadProgress, - required this.serverInfo, - required this.autoBackup, - required this.backgroundBackup, - required this.backupRequireWifi, - required this.backupRequireCharging, - required this.backupTriggerDelay, - required this.availableAlbums, - required this.selectedBackupAlbums, - required this.excludedBackupAlbums, - required this.allUniqueAssets, - required this.selectedAlbumsBackupAssetsIds, - required this.currentUploadAsset, - }); - - BackUpState copyWith({ - BackUpProgressEnum? backupProgress, - List? allAssetsInDatabase, - double? progressInPercentage, - String? progressInFileSize, - double? progressInFileSpeed, - List? progressInFileSpeeds, - DateTime? progressInFileSpeedUpdateTime, - int? progressInFileSpeedUpdateSentBytes, - double? iCloudDownloadProgress, - ServerDiskInfo? serverInfo, - bool? autoBackup, - bool? backgroundBackup, - bool? backupRequireWifi, - bool? backupRequireCharging, - int? backupTriggerDelay, - List? availableAlbums, - Set? selectedBackupAlbums, - Set? excludedBackupAlbums, - Set? allUniqueAssets, - Set? selectedAlbumsBackupAssetsIds, - CurrentUploadAsset? currentUploadAsset, - }) { - return BackUpState( - backupProgress: backupProgress ?? this.backupProgress, - allAssetsInDatabase: allAssetsInDatabase ?? this.allAssetsInDatabase, - progressInPercentage: progressInPercentage ?? this.progressInPercentage, - progressInFileSize: progressInFileSize ?? this.progressInFileSize, - progressInFileSpeed: progressInFileSpeed ?? this.progressInFileSpeed, - progressInFileSpeeds: progressInFileSpeeds ?? this.progressInFileSpeeds, - progressInFileSpeedUpdateTime: progressInFileSpeedUpdateTime ?? this.progressInFileSpeedUpdateTime, - progressInFileSpeedUpdateSentBytes: progressInFileSpeedUpdateSentBytes ?? this.progressInFileSpeedUpdateSentBytes, - iCloudDownloadProgress: iCloudDownloadProgress ?? this.iCloudDownloadProgress, - serverInfo: serverInfo ?? this.serverInfo, - autoBackup: autoBackup ?? this.autoBackup, - backgroundBackup: backgroundBackup ?? this.backgroundBackup, - backupRequireWifi: backupRequireWifi ?? this.backupRequireWifi, - backupRequireCharging: backupRequireCharging ?? this.backupRequireCharging, - backupTriggerDelay: backupTriggerDelay ?? this.backupTriggerDelay, - availableAlbums: availableAlbums ?? this.availableAlbums, - selectedBackupAlbums: selectedBackupAlbums ?? this.selectedBackupAlbums, - excludedBackupAlbums: excludedBackupAlbums ?? this.excludedBackupAlbums, - allUniqueAssets: allUniqueAssets ?? this.allUniqueAssets, - selectedAlbumsBackupAssetsIds: selectedAlbumsBackupAssetsIds ?? this.selectedAlbumsBackupAssetsIds, - currentUploadAsset: currentUploadAsset ?? this.currentUploadAsset, - ); - } - - @override - String toString() { - return 'BackUpState(backupProgress: $backupProgress, allAssetsInDatabase: $allAssetsInDatabase, progressInPercentage: $progressInPercentage, progressInFileSize: $progressInFileSize, progressInFileSpeed: $progressInFileSpeed, progressInFileSpeeds: $progressInFileSpeeds, progressInFileSpeedUpdateTime: $progressInFileSpeedUpdateTime, progressInFileSpeedUpdateSentBytes: $progressInFileSpeedUpdateSentBytes, iCloudDownloadProgress: $iCloudDownloadProgress, serverInfo: $serverInfo, autoBackup: $autoBackup, backgroundBackup: $backgroundBackup, backupRequireWifi: $backupRequireWifi, backupRequireCharging: $backupRequireCharging, backupTriggerDelay: $backupTriggerDelay, availableAlbums: $availableAlbums, selectedBackupAlbums: $selectedBackupAlbums, excludedBackupAlbums: $excludedBackupAlbums, allUniqueAssets: $allUniqueAssets, selectedAlbumsBackupAssetsIds: $selectedAlbumsBackupAssetsIds, currentUploadAsset: $currentUploadAsset)'; - } - - @override - bool operator ==(covariant BackUpState other) { - if (identical(this, other)) return true; - final collectionEquals = const DeepCollectionEquality().equals; - - return other.backupProgress == backupProgress && - collectionEquals(other.allAssetsInDatabase, allAssetsInDatabase) && - other.progressInPercentage == progressInPercentage && - other.progressInFileSize == progressInFileSize && - other.progressInFileSpeed == progressInFileSpeed && - collectionEquals(other.progressInFileSpeeds, progressInFileSpeeds) && - other.progressInFileSpeedUpdateTime == progressInFileSpeedUpdateTime && - other.progressInFileSpeedUpdateSentBytes == progressInFileSpeedUpdateSentBytes && - other.iCloudDownloadProgress == iCloudDownloadProgress && - other.serverInfo == serverInfo && - other.autoBackup == autoBackup && - other.backgroundBackup == backgroundBackup && - other.backupRequireWifi == backupRequireWifi && - other.backupRequireCharging == backupRequireCharging && - other.backupTriggerDelay == backupTriggerDelay && - collectionEquals(other.availableAlbums, availableAlbums) && - collectionEquals(other.selectedBackupAlbums, selectedBackupAlbums) && - collectionEquals(other.excludedBackupAlbums, excludedBackupAlbums) && - collectionEquals(other.allUniqueAssets, allUniqueAssets) && - collectionEquals(other.selectedAlbumsBackupAssetsIds, selectedAlbumsBackupAssetsIds) && - other.currentUploadAsset == currentUploadAsset; - } - - @override - int get hashCode { - return backupProgress.hashCode ^ - allAssetsInDatabase.hashCode ^ - progressInPercentage.hashCode ^ - progressInFileSize.hashCode ^ - progressInFileSpeed.hashCode ^ - progressInFileSpeeds.hashCode ^ - progressInFileSpeedUpdateTime.hashCode ^ - progressInFileSpeedUpdateSentBytes.hashCode ^ - iCloudDownloadProgress.hashCode ^ - serverInfo.hashCode ^ - autoBackup.hashCode ^ - backgroundBackup.hashCode ^ - backupRequireWifi.hashCode ^ - backupRequireCharging.hashCode ^ - backupTriggerDelay.hashCode ^ - availableAlbums.hashCode ^ - selectedBackupAlbums.hashCode ^ - excludedBackupAlbums.hashCode ^ - allUniqueAssets.hashCode ^ - selectedAlbumsBackupAssetsIds.hashCode ^ - currentUploadAsset.hashCode; - } -} diff --git a/mobile/lib/models/backup/current_upload_asset.model.dart b/mobile/lib/models/backup/current_upload_asset.model.dart deleted file mode 100644 index 2214897357..0000000000 --- a/mobile/lib/models/backup/current_upload_asset.model.dart +++ /dev/null @@ -1,95 +0,0 @@ -// ignore_for_file: public_member_api_docs, sort_constructors_first -import 'dart:convert'; - -class CurrentUploadAsset { - final String id; - final DateTime fileCreatedAt; - final String fileName; - final String fileType; - final int? fileSize; - final bool? iCloudAsset; - - const CurrentUploadAsset({ - required this.id, - required this.fileCreatedAt, - required this.fileName, - required this.fileType, - this.fileSize, - this.iCloudAsset, - }); - - @pragma('vm:prefer-inline') - bool get isIcloudAsset => iCloudAsset != null && iCloudAsset!; - - CurrentUploadAsset copyWith({ - String? id, - DateTime? fileCreatedAt, - String? fileName, - String? fileType, - int? fileSize, - bool? iCloudAsset, - }) { - return CurrentUploadAsset( - id: id ?? this.id, - fileCreatedAt: fileCreatedAt ?? this.fileCreatedAt, - fileName: fileName ?? this.fileName, - fileType: fileType ?? this.fileType, - fileSize: fileSize ?? this.fileSize, - iCloudAsset: iCloudAsset ?? this.iCloudAsset, - ); - } - - Map toMap() { - return { - 'id': id, - 'fileCreatedAt': fileCreatedAt.millisecondsSinceEpoch, - 'fileName': fileName, - 'fileType': fileType, - 'fileSize': fileSize, - 'iCloudAsset': iCloudAsset, - }; - } - - factory CurrentUploadAsset.fromMap(Map map) { - return CurrentUploadAsset( - id: map['id'] as String, - fileCreatedAt: DateTime.fromMillisecondsSinceEpoch(map['fileCreatedAt'] as int), - fileName: map['fileName'] as String, - fileType: map['fileType'] as String, - fileSize: map['fileSize'] as int, - iCloudAsset: map['iCloudAsset'] != null ? map['iCloudAsset'] as bool : null, - ); - } - - String toJson() => json.encode(toMap()); - - factory CurrentUploadAsset.fromJson(String source) => - CurrentUploadAsset.fromMap(json.decode(source) as Map); - - @override - String toString() { - return 'CurrentUploadAsset(id: $id, fileCreatedAt: $fileCreatedAt, fileName: $fileName, fileType: $fileType, fileSize: $fileSize, iCloudAsset: $iCloudAsset)'; - } - - @override - bool operator ==(covariant CurrentUploadAsset other) { - if (identical(this, other)) return true; - - return other.id == id && - other.fileCreatedAt == fileCreatedAt && - other.fileName == fileName && - other.fileType == fileType && - other.fileSize == fileSize && - other.iCloudAsset == iCloudAsset; - } - - @override - int get hashCode { - return id.hashCode ^ - fileCreatedAt.hashCode ^ - fileName.hashCode ^ - fileType.hashCode ^ - fileSize.hashCode ^ - iCloudAsset.hashCode; - } -} diff --git a/mobile/lib/models/backup/error_upload_asset.model.dart b/mobile/lib/models/backup/error_upload_asset.model.dart deleted file mode 100644 index 38f241e748..0000000000 --- a/mobile/lib/models/backup/error_upload_asset.model.dart +++ /dev/null @@ -1,65 +0,0 @@ -import 'package:immich_mobile/entities/asset.entity.dart'; - -class ErrorUploadAsset { - final String id; - final DateTime fileCreatedAt; - final String fileName; - final String fileType; - final Asset asset; - final String errorMessage; - - const ErrorUploadAsset({ - required this.id, - required this.fileCreatedAt, - required this.fileName, - required this.fileType, - required this.asset, - required this.errorMessage, - }); - - ErrorUploadAsset copyWith({ - String? id, - DateTime? fileCreatedAt, - String? fileName, - String? fileType, - Asset? asset, - String? errorMessage, - }) { - return ErrorUploadAsset( - id: id ?? this.id, - fileCreatedAt: fileCreatedAt ?? this.fileCreatedAt, - fileName: fileName ?? this.fileName, - fileType: fileType ?? this.fileType, - asset: asset ?? this.asset, - errorMessage: errorMessage ?? this.errorMessage, - ); - } - - @override - String toString() { - return 'ErrorUploadAsset(id: $id, fileCreatedAt: $fileCreatedAt, fileName: $fileName, fileType: $fileType, asset: $asset, errorMessage: $errorMessage)'; - } - - @override - bool operator ==(Object other) { - if (identical(this, other)) return true; - - return other is ErrorUploadAsset && - other.id == id && - other.fileCreatedAt == fileCreatedAt && - other.fileName == fileName && - other.fileType == fileType && - other.asset == asset && - other.errorMessage == errorMessage; - } - - @override - int get hashCode { - return id.hashCode ^ - fileCreatedAt.hashCode ^ - fileName.hashCode ^ - fileType.hashCode ^ - asset.hashCode ^ - errorMessage.hashCode; - } -} diff --git a/mobile/lib/models/backup/manual_upload_state.model.dart b/mobile/lib/models/backup/manual_upload_state.model.dart deleted file mode 100644 index 120327c611..0000000000 --- a/mobile/lib/models/backup/manual_upload_state.model.dart +++ /dev/null @@ -1,102 +0,0 @@ -import 'package:collection/collection.dart'; - -import 'package:immich_mobile/models/backup/current_upload_asset.model.dart'; - -class ManualUploadState { - // Current Backup Asset - final CurrentUploadAsset currentUploadAsset; - final int currentAssetIndex; - - final bool showDetailedNotification; - - /// Manual Upload Stats - final int totalAssetsToUpload; - final int successfulUploads; - final double progressInPercentage; - final String progressInFileSize; - final double progressInFileSpeed; - final List progressInFileSpeeds; - final DateTime progressInFileSpeedUpdateTime; - final int progressInFileSpeedUpdateSentBytes; - - const ManualUploadState({ - required this.progressInPercentage, - required this.progressInFileSize, - required this.progressInFileSpeed, - required this.progressInFileSpeeds, - required this.progressInFileSpeedUpdateTime, - required this.progressInFileSpeedUpdateSentBytes, - required this.currentUploadAsset, - required this.totalAssetsToUpload, - required this.currentAssetIndex, - required this.successfulUploads, - required this.showDetailedNotification, - }); - - ManualUploadState copyWith({ - double? progressInPercentage, - String? progressInFileSize, - double? progressInFileSpeed, - List? progressInFileSpeeds, - DateTime? progressInFileSpeedUpdateTime, - int? progressInFileSpeedUpdateSentBytes, - CurrentUploadAsset? currentUploadAsset, - int? totalAssetsToUpload, - int? successfulUploads, - int? currentAssetIndex, - bool? showDetailedNotification, - }) { - return ManualUploadState( - progressInPercentage: progressInPercentage ?? this.progressInPercentage, - progressInFileSize: progressInFileSize ?? this.progressInFileSize, - progressInFileSpeed: progressInFileSpeed ?? this.progressInFileSpeed, - progressInFileSpeeds: progressInFileSpeeds ?? this.progressInFileSpeeds, - progressInFileSpeedUpdateTime: progressInFileSpeedUpdateTime ?? this.progressInFileSpeedUpdateTime, - progressInFileSpeedUpdateSentBytes: progressInFileSpeedUpdateSentBytes ?? this.progressInFileSpeedUpdateSentBytes, - currentUploadAsset: currentUploadAsset ?? this.currentUploadAsset, - totalAssetsToUpload: totalAssetsToUpload ?? this.totalAssetsToUpload, - currentAssetIndex: currentAssetIndex ?? this.currentAssetIndex, - successfulUploads: successfulUploads ?? this.successfulUploads, - showDetailedNotification: showDetailedNotification ?? this.showDetailedNotification, - ); - } - - @override - String toString() { - return 'ManualUploadState(progressInPercentage: $progressInPercentage, progressInFileSize: $progressInFileSize, progressInFileSpeed: $progressInFileSpeed, progressInFileSpeeds: $progressInFileSpeeds, progressInFileSpeedUpdateTime: $progressInFileSpeedUpdateTime, progressInFileSpeedUpdateSentBytes: $progressInFileSpeedUpdateSentBytes, currentUploadAsset: $currentUploadAsset, totalAssetsToUpload: $totalAssetsToUpload, successfulUploads: $successfulUploads, currentAssetIndex: $currentAssetIndex, showDetailedNotification: $showDetailedNotification)'; - } - - @override - bool operator ==(Object other) { - if (identical(this, other)) return true; - final collectionEquals = const DeepCollectionEquality().equals; - - return other is ManualUploadState && - other.progressInPercentage == progressInPercentage && - other.progressInFileSize == progressInFileSize && - other.progressInFileSpeed == progressInFileSpeed && - collectionEquals(other.progressInFileSpeeds, progressInFileSpeeds) && - other.progressInFileSpeedUpdateTime == progressInFileSpeedUpdateTime && - other.progressInFileSpeedUpdateSentBytes == progressInFileSpeedUpdateSentBytes && - other.currentUploadAsset == currentUploadAsset && - other.totalAssetsToUpload == totalAssetsToUpload && - other.currentAssetIndex == currentAssetIndex && - other.successfulUploads == successfulUploads && - other.showDetailedNotification == showDetailedNotification; - } - - @override - int get hashCode { - return progressInPercentage.hashCode ^ - progressInFileSize.hashCode ^ - progressInFileSpeed.hashCode ^ - progressInFileSpeeds.hashCode ^ - progressInFileSpeedUpdateTime.hashCode ^ - progressInFileSpeedUpdateSentBytes.hashCode ^ - currentUploadAsset.hashCode ^ - totalAssetsToUpload.hashCode ^ - currentAssetIndex.hashCode ^ - successfulUploads.hashCode ^ - showDetailedNotification.hashCode; - } -} diff --git a/mobile/lib/models/backup/success_upload_asset.model.dart b/mobile/lib/models/backup/success_upload_asset.model.dart deleted file mode 100644 index da1e104ba3..0000000000 --- a/mobile/lib/models/backup/success_upload_asset.model.dart +++ /dev/null @@ -1,31 +0,0 @@ -import 'package:immich_mobile/models/backup/backup_candidate.model.dart'; - -class SuccessUploadAsset { - final BackupCandidate candidate; - final String remoteAssetId; - final bool isDuplicate; - - const SuccessUploadAsset({required this.candidate, required this.remoteAssetId, required this.isDuplicate}); - - SuccessUploadAsset copyWith({BackupCandidate? candidate, String? remoteAssetId, bool? isDuplicate}) { - return SuccessUploadAsset( - candidate: candidate ?? this.candidate, - remoteAssetId: remoteAssetId ?? this.remoteAssetId, - isDuplicate: isDuplicate ?? this.isDuplicate, - ); - } - - @override - String toString() => - 'SuccessUploadAsset(asset: $candidate, remoteAssetId: $remoteAssetId, isDuplicate: $isDuplicate)'; - - @override - bool operator ==(covariant SuccessUploadAsset other) { - if (identical(this, other)) return true; - - return other.candidate == candidate && other.remoteAssetId == remoteAssetId && other.isDuplicate == isDuplicate; - } - - @override - int get hashCode => candidate.hashCode ^ remoteAssetId.hashCode ^ isDuplicate.hashCode; -} diff --git a/mobile/lib/models/memories/memory.model.dart b/mobile/lib/models/memories/memory.model.dart deleted file mode 100644 index 8a9db5d51b..0000000000 --- a/mobile/lib/models/memories/memory.model.dart +++ /dev/null @@ -1,29 +0,0 @@ -// ignore_for_file: public_member_api_docs, sort_constructors_first - -import 'package:collection/collection.dart'; - -import 'package:immich_mobile/entities/asset.entity.dart'; - -class Memory { - final String title; - final List assets; - const Memory({required this.title, required this.assets}); - - Memory copyWith({String? title, List? assets}) { - return Memory(title: title ?? this.title, assets: assets ?? this.assets); - } - - @override - String toString() => 'Memory(title: $title, assets: $assets)'; - - @override - bool operator ==(Object other) { - if (identical(this, other)) return true; - final listEquals = const DeepCollectionEquality().equals; - - return other is Memory && other.title == title && listEquals(other.assets, assets); - } - - @override - int get hashCode => title.hashCode ^ assets.hashCode; -} diff --git a/mobile/lib/models/search/search_filter.model.dart b/mobile/lib/models/search/search_filter.model.dart index 1b730e0c68..16f3be4655 100644 --- a/mobile/lib/models/search/search_filter.model.dart +++ b/mobile/lib/models/search/search_filter.model.dart @@ -1,8 +1,8 @@ // ignore_for_file: public_member_api_docs, sort_constructors_first import 'dart:convert'; +import 'package:immich_mobile/domain/models/asset/base_asset.model.dart'; import 'package:immich_mobile/domain/models/person.model.dart'; -import 'package:immich_mobile/entities/asset.entity.dart'; class SearchLocationFilter { String? country; diff --git a/mobile/lib/models/search/search_result.model.dart b/mobile/lib/models/search/search_result.model.dart deleted file mode 100644 index 02553869bf..0000000000 --- a/mobile/lib/models/search/search_result.model.dart +++ /dev/null @@ -1,28 +0,0 @@ -import 'package:collection/collection.dart'; - -import 'package:immich_mobile/entities/asset.entity.dart'; - -class SearchResult { - final List assets; - final int? nextPage; - - const SearchResult({required this.assets, this.nextPage}); - - SearchResult copyWith({List? assets, int? nextPage}) { - return SearchResult(assets: assets ?? this.assets, nextPage: nextPage ?? this.nextPage); - } - - @override - String toString() => 'SearchResult(assets: $assets, nextPage: $nextPage)'; - - @override - bool operator ==(covariant SearchResult other) { - if (identical(this, other)) return true; - final listEquals = const DeepCollectionEquality().equals; - - return listEquals(other.assets, assets) && other.nextPage == nextPage; - } - - @override - int get hashCode => assets.hashCode ^ nextPage.hashCode; -} diff --git a/mobile/lib/models/search/search_result_page_state.model.dart b/mobile/lib/models/search/search_result_page_state.model.dart deleted file mode 100644 index 7c8a27b50c..0000000000 --- a/mobile/lib/models/search/search_result_page_state.model.dart +++ /dev/null @@ -1,57 +0,0 @@ -import 'package:collection/collection.dart'; -import 'package:immich_mobile/entities/asset.entity.dart'; - -class SearchResultPageState { - final bool isLoading; - final bool isSuccess; - final bool isError; - final bool isSmart; - final List searchResult; - - const SearchResultPageState({ - required this.isLoading, - required this.isSuccess, - required this.isError, - required this.isSmart, - required this.searchResult, - }); - - SearchResultPageState copyWith({ - bool? isLoading, - bool? isSuccess, - bool? isError, - bool? isSmart, - List? searchResult, - }) { - return SearchResultPageState( - isLoading: isLoading ?? this.isLoading, - isSuccess: isSuccess ?? this.isSuccess, - isError: isError ?? this.isError, - isSmart: isSmart ?? this.isSmart, - searchResult: searchResult ?? this.searchResult, - ); - } - - @override - String toString() { - return 'SearchresultPageState(isLoading: $isLoading, isSuccess: $isSuccess, isError: $isError, isSmart: $isSmart, searchResult: $searchResult)'; - } - - @override - bool operator ==(Object other) { - if (identical(this, other)) return true; - final listEquals = const DeepCollectionEquality().equals; - - return other is SearchResultPageState && - other.isLoading == isLoading && - other.isSuccess == isSuccess && - other.isError == isError && - other.isSmart == isSmart && - listEquals(other.searchResult, searchResult); - } - - @override - int get hashCode { - return isLoading.hashCode ^ isSuccess.hashCode ^ isError.hashCode ^ isSmart.hashCode ^ searchResult.hashCode; - } -} diff --git a/mobile/lib/pages/album/album_additional_shared_user_selection.page.dart b/mobile/lib/pages/album/album_additional_shared_user_selection.page.dart deleted file mode 100644 index f40ac9ccae..0000000000 --- a/mobile/lib/pages/album/album_additional_shared_user_selection.page.dart +++ /dev/null @@ -1,115 +0,0 @@ -import 'package:auto_route/auto_route.dart'; -import 'package:easy_localization/easy_localization.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_hooks/flutter_hooks.dart'; -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/domain/models/user.model.dart'; -import 'package:immich_mobile/entities/album.entity.dart'; -import 'package:immich_mobile/extensions/asyncvalue_extensions.dart'; -import 'package:immich_mobile/extensions/build_context_extensions.dart'; -import 'package:immich_mobile/providers/album/suggested_shared_users.provider.dart'; -import 'package:immich_mobile/widgets/common/user_circle_avatar.dart'; - -@RoutePage() -class AlbumAdditionalSharedUserSelectionPage extends HookConsumerWidget { - final Album album; - - const AlbumAdditionalSharedUserSelectionPage({super.key, required this.album}); - - @override - Widget build(BuildContext context, WidgetRef ref) { - final AsyncValue> suggestedShareUsers = ref.watch(otherUsersProvider); - final sharedUsersList = useState>({}); - - addNewUsersHandler() { - context.maybePop(sharedUsersList.value.map((e) => e.id).toList()); - } - - buildTileIcon(UserDto user) { - if (sharedUsersList.value.contains(user)) { - return CircleAvatar(backgroundColor: context.primaryColor, child: const Icon(Icons.check_rounded, size: 25)); - } else { - return UserCircleAvatar(user: user); - } - } - - buildUserList(List users) { - List usersChip = []; - - for (var user in sharedUsersList.value) { - usersChip.add( - Padding( - padding: const EdgeInsets.symmetric(horizontal: 8.0), - child: Chip( - backgroundColor: context.primaryColor.withValues(alpha: 0.15), - label: Text(user.name, style: const TextStyle(fontSize: 12, fontWeight: FontWeight.bold)), - ), - ), - ); - } - return ListView( - children: [ - Wrap(children: [...usersChip]), - Padding( - padding: const EdgeInsets.all(16.0), - child: Text( - 'suggestions'.tr(), - style: const TextStyle(fontSize: 14, color: Colors.grey, fontWeight: FontWeight.bold), - ), - ), - ListView.builder( - primary: false, - shrinkWrap: true, - itemBuilder: ((context, index) { - return ListTile( - leading: buildTileIcon(users[index]), - dense: true, - title: Text(users[index].name, style: const TextStyle(fontSize: 14, fontWeight: FontWeight.bold)), - subtitle: Text(users[index].email, style: const TextStyle(fontSize: 12)), - onTap: () { - if (sharedUsersList.value.contains(users[index])) { - sharedUsersList.value = sharedUsersList.value - .where((selectedUser) => selectedUser.id != users[index].id) - .toSet(); - } else { - sharedUsersList.value = {...sharedUsersList.value, users[index]}; - } - }, - ); - }), - itemCount: users.length, - ), - ], - ); - } - - return Scaffold( - appBar: AppBar( - title: const Text('invite_to_album').tr(), - elevation: 0, - centerTitle: false, - leading: IconButton( - icon: const Icon(Icons.close_rounded), - onPressed: () { - context.maybePop(null); - }, - ), - actions: [ - TextButton( - onPressed: sharedUsersList.value.isEmpty ? null : addNewUsersHandler, - child: const Text("add", style: TextStyle(fontSize: 14, fontWeight: FontWeight.bold)).tr(), - ), - ], - ), - body: suggestedShareUsers.widgetWhen( - onData: (users) { - for (var sharedUsers in album.sharedUsers) { - users.removeWhere((u) => u.id == sharedUsers.id || u.id == album.ownerId); - } - - return buildUserList(users); - }, - ), - ); - } -} diff --git a/mobile/lib/pages/album/album_asset_selection.page.dart b/mobile/lib/pages/album/album_asset_selection.page.dart deleted file mode 100644 index ccc4c44d43..0000000000 --- a/mobile/lib/pages/album/album_asset_selection.page.dart +++ /dev/null @@ -1,74 +0,0 @@ -import 'package:auto_route/auto_route.dart'; -import 'package:easy_localization/easy_localization.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_hooks/flutter_hooks.dart'; -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/entities/asset.entity.dart'; -import 'package:immich_mobile/extensions/asyncvalue_extensions.dart'; -import 'package:immich_mobile/extensions/build_context_extensions.dart'; -import 'package:immich_mobile/models/albums/asset_selection_page_result.model.dart'; -import 'package:immich_mobile/providers/timeline.provider.dart'; -import 'package:immich_mobile/widgets/asset_grid/asset_grid_data_structure.dart'; -import 'package:immich_mobile/widgets/asset_grid/immich_asset_grid.dart'; - -@RoutePage() -class AlbumAssetSelectionPage extends HookConsumerWidget { - const AlbumAssetSelectionPage({super.key, required this.existingAssets, this.canDeselect = false}); - - final Set existingAssets; - final bool canDeselect; - - @override - Widget build(BuildContext context, WidgetRef ref) { - final assetSelectionRenderList = ref.watch(assetSelectionTimelineProvider); - final selected = useState>(existingAssets); - final selectionEnabledHook = useState(true); - - Widget buildBody(RenderList renderList) { - return ImmichAssetGrid( - renderList: renderList, - listener: (active, assets) { - selectionEnabledHook.value = active; - selected.value = assets; - }, - selectionActive: true, - preselectedAssets: existingAssets, - canDeselect: canDeselect, - showMultiSelectIndicator: false, - ); - } - - return Scaffold( - appBar: AppBar( - elevation: 0, - leading: IconButton( - icon: const Icon(Icons.close_rounded), - onPressed: () { - AutoRouter.of(context).popForced(null); - }, - ), - title: selected.value.isEmpty - ? const Text('add_photos', style: TextStyle(fontSize: 18)).tr() - : const Text( - 'share_assets_selected', - style: TextStyle(fontSize: 18), - ).tr(namedArgs: {'count': selected.value.length.toString()}), - centerTitle: false, - actions: [ - if (selected.value.isNotEmpty || canDeselect) - TextButton( - onPressed: () { - var payload = AssetSelectionPageResult(selectedAssets: selected.value); - AutoRouter.of(context).popForced(payload); - }, - child: Text( - canDeselect ? "done" : "add", - style: TextStyle(fontWeight: FontWeight.bold, color: context.primaryColor), - ).tr(), - ), - ], - ), - body: assetSelectionRenderList.widgetWhen(onData: (data) => buildBody(data)), - ); - } -} diff --git a/mobile/lib/pages/album/album_control_button.dart b/mobile/lib/pages/album/album_control_button.dart deleted file mode 100644 index 578eb839a0..0000000000 --- a/mobile/lib/pages/album/album_control_button.dart +++ /dev/null @@ -1,37 +0,0 @@ -import 'package:easy_localization/easy_localization.dart'; -import 'package:flutter/material.dart'; -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/widgets/album/album_action_filled_button.dart'; - -class AlbumControlButton extends ConsumerWidget { - final void Function()? onAddPhotosPressed; - final void Function()? onAddUsersPressed; - - const AlbumControlButton({super.key, this.onAddPhotosPressed, this.onAddUsersPressed}); - - @override - Widget build(BuildContext context, WidgetRef ref) { - return SizedBox( - height: 36, - child: ListView( - scrollDirection: Axis.horizontal, - children: [ - if (onAddPhotosPressed != null) - AlbumActionFilledButton( - key: const ValueKey('add_photos_button'), - iconData: Icons.add_photo_alternate_outlined, - onPressed: onAddPhotosPressed, - labelText: "add_photos".tr(), - ), - if (onAddUsersPressed != null) - AlbumActionFilledButton( - key: const ValueKey('add_users_button'), - iconData: Icons.person_add_alt_rounded, - onPressed: onAddUsersPressed, - labelText: "album_viewer_page_share_add_users".tr(), - ), - ], - ), - ); - } -} diff --git a/mobile/lib/pages/album/album_date_range.dart b/mobile/lib/pages/album/album_date_range.dart deleted file mode 100644 index dbfd9214f1..0000000000 --- a/mobile/lib/pages/album/album_date_range.dart +++ /dev/null @@ -1,53 +0,0 @@ -import 'package:easy_localization/easy_localization.dart'; -import 'package:flutter/material.dart'; -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/extensions/build_context_extensions.dart'; -import 'package:immich_mobile/providers/album/current_album.provider.dart'; - -class AlbumDateRange extends ConsumerWidget { - const AlbumDateRange({super.key}); - - @override - Widget build(BuildContext context, WidgetRef ref) { - final data = ref.watch( - currentAlbumProvider.select((album) { - if (album == null || album.assets.isEmpty) { - return null; - } - - final startDate = album.startDate; - final endDate = album.endDate; - if (startDate == null || endDate == null) { - return null; - } - return (startDate, endDate, album.shared); - }), - ); - - if (data == null) { - return const SizedBox(); - } - final (startDate, endDate, shared) = data; - - return Padding( - padding: const EdgeInsets.only(left: 16.0), - child: Text( - _getDateRangeText(startDate, endDate), - style: context.textTheme.labelLarge?.copyWith(color: context.colorScheme.onSurfaceVariant), - ), - ); - } - - @pragma('vm:prefer-inline') - String _getDateRangeText(DateTime startDate, DateTime endDate) { - if (startDate.day == endDate.day && startDate.month == endDate.month && startDate.year == endDate.year) { - return DateFormat.yMMMd().format(startDate); - } - - final String startDateText = (startDate.year == endDate.year ? DateFormat.MMMd() : DateFormat.yMMMd()).format( - startDate, - ); - final String endDateText = DateFormat.yMMMd().format(endDate); - return "$startDateText - $endDateText"; - } -} diff --git a/mobile/lib/pages/album/album_description.dart b/mobile/lib/pages/album/album_description.dart deleted file mode 100644 index 383367e8b7..0000000000 --- a/mobile/lib/pages/album/album_description.dart +++ /dev/null @@ -1,42 +0,0 @@ -import 'package:easy_localization/easy_localization.dart'; -import 'package:flutter/material.dart'; -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/extensions/build_context_extensions.dart'; -import 'package:immich_mobile/providers/album/current_album.provider.dart'; -import 'package:immich_mobile/widgets/album/album_viewer_editable_description.dart'; -import 'package:immich_mobile/providers/auth.provider.dart'; - -class AlbumDescription extends ConsumerWidget { - const AlbumDescription({super.key, required this.descriptionFocusNode}); - - final FocusNode descriptionFocusNode; - - @override - Widget build(BuildContext context, WidgetRef ref) { - final userId = ref.watch(authProvider).userId; - final (isOwner, isRemote, albumDescription) = ref.watch( - currentAlbumProvider.select((album) { - if (album == null) { - return const (false, false, ''); - } - - return (album.ownerId == userId, album.isRemote, album.description); - }), - ); - - if (isOwner && isRemote) { - return Padding( - padding: const EdgeInsets.only(left: 8, right: 8), - child: AlbumViewerEditableDescription( - albumDescription: albumDescription ?? 'add_a_description'.tr(), - descriptionFocusNode: descriptionFocusNode, - ), - ); - } - - return Padding( - padding: const EdgeInsets.only(left: 16, right: 8), - child: Text(albumDescription ?? 'add_a_description'.tr(), style: context.textTheme.bodyLarge), - ); - } -} diff --git a/mobile/lib/pages/album/album_options.page.dart b/mobile/lib/pages/album/album_options.page.dart deleted file mode 100644 index ca65a92a79..0000000000 --- a/mobile/lib/pages/album/album_options.page.dart +++ /dev/null @@ -1,192 +0,0 @@ -import 'dart:async'; - -import 'package:auto_route/auto_route.dart'; -import 'package:easy_localization/easy_localization.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_hooks/flutter_hooks.dart'; -import 'package:fluttertoast/fluttertoast.dart'; -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/domain/models/user.model.dart'; -import 'package:immich_mobile/extensions/build_context_extensions.dart'; -import 'package:immich_mobile/extensions/theme_extensions.dart'; -import 'package:immich_mobile/infrastructure/entities/user.entity.dart' as entity; -import 'package:immich_mobile/providers/album/album.provider.dart'; -import 'package:immich_mobile/providers/album/current_album.provider.dart'; -import 'package:immich_mobile/providers/auth.provider.dart'; -import 'package:immich_mobile/routing/router.dart'; -import 'package:immich_mobile/utils/immich_loading_overlay.dart'; -import 'package:immich_mobile/widgets/common/immich_toast.dart'; -import 'package:immich_mobile/widgets/common/user_circle_avatar.dart'; - -@RoutePage() -class AlbumOptionsPage extends HookConsumerWidget { - const AlbumOptionsPage({super.key}); - - @override - Widget build(BuildContext context, WidgetRef ref) { - final album = ref.watch(currentAlbumProvider); - if (album == null) { - return const SizedBox(); - } - - final sharedUsers = useState(album.sharedUsers.map((u) => u.toDto()).toList()); - final owner = album.owner.value; - final userId = ref.watch(authProvider).userId; - final activityEnabled = useState(album.activityEnabled); - final isProcessing = useProcessingOverlay(); - final isOwner = owner?.id == userId; - - void showErrorMessage() { - context.pop(); - ImmichToast.show( - context: context, - msg: "shared_album_section_people_action_error".tr(), - toastType: ToastType.error, - gravity: ToastGravity.BOTTOM, - ); - } - - void leaveAlbum() async { - isProcessing.value = true; - - try { - final isSuccess = await ref.read(albumProvider.notifier).leaveAlbum(album); - - if (isSuccess) { - unawaited(context.navigateTo(const TabControllerRoute(children: [AlbumsRoute()]))); - } else { - showErrorMessage(); - } - } catch (_) { - showErrorMessage(); - } - - isProcessing.value = false; - } - - void removeUserFromAlbum(UserDto user) async { - isProcessing.value = true; - - try { - await ref.read(albumProvider.notifier).removeUser(album, user); - album.sharedUsers.remove(entity.User.fromDto(user)); - sharedUsers.value = album.sharedUsers.map((u) => u.toDto()).toList(); - } catch (error) { - showErrorMessage(); - } - - context.pop(); - isProcessing.value = false; - } - - void handleUserClick(UserDto user) { - var actions = []; - - if (user.id == userId) { - actions = [ - ListTile( - leading: const Icon(Icons.exit_to_app_rounded), - title: const Text("shared_album_section_people_action_leave").tr(), - onTap: leaveAlbum, - ), - ]; - } - - if (isOwner) { - actions = [ - ListTile( - leading: const Icon(Icons.person_remove_rounded), - title: const Text("shared_album_section_people_action_remove_user").tr(), - onTap: () => removeUserFromAlbum(user), - ), - ]; - } - - showModalBottomSheet( - backgroundColor: context.colorScheme.surfaceContainer, - isScrollControlled: false, - context: context, - builder: (context) { - return SafeArea( - child: Padding( - padding: const EdgeInsets.only(top: 24.0), - child: Column(mainAxisSize: MainAxisSize.min, children: [...actions]), - ), - ); - }, - ); - } - - buildOwnerInfo() { - return ListTile( - leading: owner != null ? UserCircleAvatar(user: owner.toDto()) : const SizedBox(), - title: Text(album.owner.value?.name ?? "", style: const TextStyle(fontWeight: FontWeight.w500)), - subtitle: Text(album.owner.value?.email ?? "", style: TextStyle(color: context.colorScheme.onSurfaceSecondary)), - trailing: Text("owner", style: context.textTheme.labelLarge).tr(), - ); - } - - buildSharedUsersList() { - return ListView.builder( - primary: false, - shrinkWrap: true, - itemCount: sharedUsers.value.length, - itemBuilder: (context, index) { - final user = sharedUsers.value[index]; - return ListTile( - leading: UserCircleAvatar(user: user), - title: Text(user.name, style: const TextStyle(fontWeight: FontWeight.w500)), - subtitle: Text(user.email, style: TextStyle(color: context.colorScheme.onSurfaceSecondary)), - trailing: userId == user.id || isOwner ? const Icon(Icons.more_horiz_rounded) : const SizedBox(), - onTap: userId == user.id || isOwner ? () => handleUserClick(user) : null, - ); - }, - ); - } - - buildSectionTitle(String text) { - return Padding( - padding: const EdgeInsets.all(16.0), - child: Text(text, style: context.textTheme.bodySmall), - ); - } - - return Scaffold( - appBar: AppBar( - leading: IconButton( - icon: const Icon(Icons.arrow_back_ios_new_rounded), - onPressed: () => context.maybePop(null), - ), - centerTitle: true, - title: Text("options".tr()), - ), - body: ListView( - children: [ - if (isOwner && album.shared) - SwitchListTile.adaptive( - value: activityEnabled.value, - onChanged: (bool value) async { - activityEnabled.value = value; - if (await ref.read(albumProvider.notifier).setActivitystatus(album, value)) { - album.activityEnabled = value; - } - }, - activeThumbColor: activityEnabled.value ? context.primaryColor : context.themeData.disabledColor, - dense: true, - title: Text( - "comments_and_likes", - style: context.textTheme.titleMedium?.copyWith(fontWeight: FontWeight.w500), - ).tr(), - subtitle: Text( - "let_others_respond", - style: context.textTheme.labelLarge?.copyWith(color: context.colorScheme.onSurfaceSecondary), - ).tr(), - ), - buildSectionTitle("shared_album_section_people_title".tr()), - buildOwnerInfo(), - buildSharedUsersList(), - ], - ), - ); - } -} diff --git a/mobile/lib/pages/album/album_shared_user_icons.dart b/mobile/lib/pages/album/album_shared_user_icons.dart deleted file mode 100644 index 7cf6f387ae..0000000000 --- a/mobile/lib/pages/album/album_shared_user_icons.dart +++ /dev/null @@ -1,52 +0,0 @@ -import 'package:auto_route/auto_route.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_hooks/flutter_hooks.dart'; -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/domain/models/user.model.dart'; -import 'package:immich_mobile/providers/album/current_album.provider.dart'; -import 'package:immich_mobile/routing/router.dart'; -import 'package:immich_mobile/widgets/common/user_circle_avatar.dart'; - -class AlbumSharedUserIcons extends HookConsumerWidget { - const AlbumSharedUserIcons({super.key}); - - @override - Widget build(BuildContext context, WidgetRef ref) { - final sharedUsers = useRef>(const []); - sharedUsers.value = ref.watch( - currentAlbumProvider.select((album) { - if (album == null) { - return const []; - } - - if (album.sharedUsers.length == sharedUsers.value.length) { - return sharedUsers.value; - } - - return album.sharedUsers.map((u) => u.toDto()).toList(growable: false); - }), - ); - - if (sharedUsers.value.isEmpty) { - return const SizedBox(); - } - - return GestureDetector( - onTap: () => context.pushRoute(const AlbumOptionsRoute()), - child: SizedBox( - height: 50, - child: ListView.builder( - padding: const EdgeInsets.only(left: 16, bottom: 8), - scrollDirection: Axis.horizontal, - itemBuilder: ((context, index) { - return Padding( - padding: const EdgeInsets.only(right: 8.0), - child: UserCircleAvatar(user: sharedUsers.value[index], size: 36), - ); - }), - itemCount: sharedUsers.value.length, - ), - ), - ); - } -} diff --git a/mobile/lib/pages/album/album_shared_user_selection.page.dart b/mobile/lib/pages/album/album_shared_user_selection.page.dart deleted file mode 100644 index ec084b1859..0000000000 --- a/mobile/lib/pages/album/album_shared_user_selection.page.dart +++ /dev/null @@ -1,140 +0,0 @@ -import 'dart:async'; - -import 'package:auto_route/auto_route.dart'; -import 'package:easy_localization/easy_localization.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_hooks/flutter_hooks.dart'; -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/domain/models/user.model.dart'; -import 'package:immich_mobile/entities/asset.entity.dart'; -import 'package:immich_mobile/extensions/asyncvalue_extensions.dart'; -import 'package:immich_mobile/extensions/build_context_extensions.dart'; -import 'package:immich_mobile/providers/album/album.provider.dart'; -import 'package:immich_mobile/providers/album/album_title.provider.dart'; -import 'package:immich_mobile/providers/album/suggested_shared_users.provider.dart'; -import 'package:immich_mobile/routing/router.dart'; -import 'package:immich_mobile/widgets/common/user_circle_avatar.dart'; - -@RoutePage() -class AlbumSharedUserSelectionPage extends HookConsumerWidget { - const AlbumSharedUserSelectionPage({super.key, required this.assets}); - - final Set assets; - - @override - Widget build(BuildContext context, WidgetRef ref) { - final sharedUsersList = useState>({}); - final suggestedShareUsers = ref.watch(otherUsersProvider); - - createSharedAlbum() async { - var newAlbum = await ref.watch(albumProvider.notifier).createAlbum(ref.watch(albumTitleProvider), assets); - - if (newAlbum != null) { - ref.watch(albumTitleProvider.notifier).clearAlbumTitle(); - unawaited(context.maybePop(true)); - unawaited(context.navigateTo(const TabControllerRoute(children: [AlbumsRoute()]))); - } - - ScaffoldMessenger( - child: SnackBar( - content: Text( - 'select_user_for_sharing_page_err_album', - style: context.textTheme.bodyLarge?.copyWith(color: context.primaryColor), - ).tr(), - ), - ); - } - - buildTileIcon(UserDto user) { - if (sharedUsersList.value.contains(user)) { - return CircleAvatar(backgroundColor: context.primaryColor, child: const Icon(Icons.check_rounded, size: 25)); - } else { - return UserCircleAvatar(user: user); - } - } - - buildUserList(List users) { - List usersChip = []; - - for (var user in sharedUsersList.value) { - usersChip.add( - Padding( - padding: const EdgeInsets.symmetric(horizontal: 8.0), - child: Chip( - backgroundColor: context.primaryColor.withValues(alpha: 0.15), - label: Text( - user.email, - style: const TextStyle(fontSize: 12, color: Colors.black87, fontWeight: FontWeight.bold), - ), - ), - ), - ); - } - return ListView( - children: [ - Wrap(children: [...usersChip]), - Padding( - padding: const EdgeInsets.all(16.0), - child: const Text( - 'suggestions', - style: TextStyle(fontSize: 14, color: Colors.grey, fontWeight: FontWeight.bold), - ).tr(), - ), - ListView.builder( - primary: false, - shrinkWrap: true, - itemBuilder: ((context, index) { - return ListTile( - leading: buildTileIcon(users[index]), - title: Text(users[index].email, style: const TextStyle(fontSize: 14, fontWeight: FontWeight.bold)), - onTap: () { - if (sharedUsersList.value.contains(users[index])) { - sharedUsersList.value = sharedUsersList.value - .where((selectedUser) => selectedUser.id != users[index].id) - .toSet(); - } else { - sharedUsersList.value = {...sharedUsersList.value, users[index]}; - } - }, - ); - }), - itemCount: users.length, - ), - ], - ); - } - - return Scaffold( - appBar: AppBar( - title: Text('invite_to_album', style: TextStyle(color: context.primaryColor)).tr(), - elevation: 0, - centerTitle: false, - leading: IconButton( - icon: const Icon(Icons.close_rounded), - onPressed: () { - unawaited(context.maybePop()); - }, - ), - actions: [ - TextButton( - style: TextButton.styleFrom(foregroundColor: context.primaryColor), - onPressed: sharedUsersList.value.isEmpty ? null : createSharedAlbum, - child: const Text( - "create_album", - style: TextStyle( - fontSize: 14, - fontWeight: FontWeight.bold, - // color: context.primaryColor, - ), - ).tr(), - ), - ], - ), - body: suggestedShareUsers.widgetWhen( - onData: (users) { - return buildUserList(users); - }, - ), - ); - } -} diff --git a/mobile/lib/pages/album/album_title.dart b/mobile/lib/pages/album/album_title.dart deleted file mode 100644 index 6c7fc3faaa..0000000000 --- a/mobile/lib/pages/album/album_title.dart +++ /dev/null @@ -1,38 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/extensions/build_context_extensions.dart'; -import 'package:immich_mobile/providers/album/current_album.provider.dart'; -import 'package:immich_mobile/widgets/album/album_viewer_editable_title.dart'; -import 'package:immich_mobile/providers/auth.provider.dart'; - -class AlbumTitle extends ConsumerWidget { - const AlbumTitle({super.key, required this.titleFocusNode}); - - final FocusNode titleFocusNode; - - @override - Widget build(BuildContext context, WidgetRef ref) { - final userId = ref.watch(authProvider).userId; - final (isOwner, isRemote, albumName) = ref.watch( - currentAlbumProvider.select((album) { - if (album == null) { - return const (false, false, ''); - } - - return (album.ownerId == userId, album.isRemote, album.name); - }), - ); - - if (isOwner && isRemote) { - return Padding( - padding: const EdgeInsets.only(left: 8, right: 8), - child: AlbumViewerEditableTitle(albumName: albumName, titleFocusNode: titleFocusNode), - ); - } - - return Padding( - padding: const EdgeInsets.only(left: 16, right: 8), - child: Text(albumName, style: context.textTheme.headlineLarge?.copyWith(fontWeight: FontWeight.w700)), - ); - } -} diff --git a/mobile/lib/pages/album/album_viewer.dart b/mobile/lib/pages/album/album_viewer.dart deleted file mode 100644 index 97853fb96a..0000000000 --- a/mobile/lib/pages/album/album_viewer.dart +++ /dev/null @@ -1,165 +0,0 @@ -import 'dart:async'; - -import 'package:auto_route/auto_route.dart'; -import 'package:easy_localization/easy_localization.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_hooks/flutter_hooks.dart'; -import 'package:fluttertoast/fluttertoast.dart'; -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/extensions/build_context_extensions.dart'; -import 'package:immich_mobile/models/albums/asset_selection_page_result.model.dart'; -import 'package:immich_mobile/pages/album/album_control_button.dart'; -import 'package:immich_mobile/pages/album/album_date_range.dart'; -import 'package:immich_mobile/pages/album/album_description.dart'; -import 'package:immich_mobile/pages/album/album_shared_user_icons.dart'; -import 'package:immich_mobile/pages/album/album_title.dart'; -import 'package:immich_mobile/providers/album/album.provider.dart'; -import 'package:immich_mobile/providers/album/current_album.provider.dart'; -import 'package:immich_mobile/providers/asset_viewer/current_asset.provider.dart'; -import 'package:immich_mobile/providers/timeline.provider.dart'; -import 'package:immich_mobile/utils/immich_loading_overlay.dart'; -import 'package:immich_mobile/providers/multiselect.provider.dart'; -import 'package:immich_mobile/providers/auth.provider.dart'; -import 'package:immich_mobile/widgets/album/album_viewer_appbar.dart'; -import 'package:immich_mobile/routing/router.dart'; -import 'package:immich_mobile/entities/asset.entity.dart'; -import 'package:immich_mobile/widgets/asset_grid/multiselect_grid.dart'; -import 'package:immich_mobile/widgets/common/immich_toast.dart'; - -class AlbumViewer extends HookConsumerWidget { - const AlbumViewer({super.key}); - - @override - Widget build(BuildContext context, WidgetRef ref) { - final album = ref.watch(currentAlbumProvider); - if (album == null) { - return const SizedBox(); - } - - final titleFocusNode = useFocusNode(); - final descriptionFocusNode = useFocusNode(); - final userId = ref.watch(authProvider).userId; - final isMultiselecting = ref.watch(multiselectProvider); - final isProcessing = useProcessingOverlay(); - final isOwner = ref.watch( - currentAlbumProvider.select((album) { - return album?.ownerId == userId; - }), - ); - - Future onRemoveFromAlbumPressed(Iterable assets) async { - final bool isSuccess = await ref.read(albumProvider.notifier).removeAsset(album, assets); - - if (!isSuccess) { - ImmichToast.show( - context: context, - msg: "album_viewer_appbar_share_err_remove".tr(), - toastType: ToastType.error, - gravity: ToastGravity.BOTTOM, - ); - } - return isSuccess; - } - - /// Find out if the assets in album exist on the device - /// If they exist, add to selected asset state to show they are already selected. - void onAddPhotosPressed() async { - AssetSelectionPageResult? returnPayload = await context.pushRoute( - AlbumAssetSelectionRoute(existingAssets: album.assets, canDeselect: false), - ); - - if (returnPayload != null && returnPayload.selectedAssets.isNotEmpty) { - // Check if there is new assets add - isProcessing.value = true; - - await ref.watch(albumProvider.notifier).addAssets(album, returnPayload.selectedAssets); - - isProcessing.value = false; - } - } - - void onAddUsersPressed() async { - List? sharedUserIds = await context.pushRoute?>( - AlbumAdditionalSharedUserSelectionRoute(album: album), - ); - - if (sharedUserIds != null) { - isProcessing.value = true; - - await ref.watch(albumProvider.notifier).addUsers(album, sharedUserIds); - - isProcessing.value = false; - } - } - - onActivitiesPressed() { - if (album.remoteId != null) { - ref.read(currentAssetProvider.notifier).set(null); - context.pushRoute(const ActivitiesRoute()); - } - } - - return Stack( - children: [ - MultiselectGrid( - key: const ValueKey("albumViewerMultiselectGrid"), - renderListProvider: albumTimelineProvider(album.id), - topWidget: Container( - decoration: BoxDecoration( - gradient: LinearGradient( - begin: Alignment.topCenter, - end: Alignment.bottomCenter, - colors: [ - context.primaryColor.withValues(alpha: 0.06), - context.primaryColor.withValues(alpha: 0.04), - Colors.indigo.withValues(alpha: 0.02), - Colors.transparent, - ], - stops: const [0.0, 0.3, 0.7, 1.0], - ), - ), - child: Column( - mainAxisAlignment: MainAxisAlignment.end, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - const SizedBox(height: 32), - const AlbumDateRange(), - AlbumTitle(key: const ValueKey("albumTitle"), titleFocusNode: titleFocusNode), - AlbumDescription(key: const ValueKey("albumDescription"), descriptionFocusNode: descriptionFocusNode), - const AlbumSharedUserIcons(), - if (album.isRemote) - Padding( - padding: const EdgeInsets.only(left: 16.0), - child: AlbumControlButton( - key: const ValueKey("albumControlButton"), - onAddPhotosPressed: onAddPhotosPressed, - onAddUsersPressed: isOwner ? onAddUsersPressed : null, - ), - ), - const SizedBox(height: 8), - ], - ), - ), - onRemoveFromAlbum: onRemoveFromAlbumPressed, - editEnabled: album.ownerId == userId, - ), - AnimatedPositioned( - key: const ValueKey("albumViewerAppbarPositioned"), - duration: const Duration(milliseconds: 300), - top: isMultiselecting ? -(kToolbarHeight + context.padding.top) : 0, - left: 0, - right: 0, - child: AlbumViewerAppbar( - key: const ValueKey("albumViewerAppbar"), - titleFocusNode: titleFocusNode, - descriptionFocusNode: descriptionFocusNode, - userId: userId, - onAddPhotos: onAddPhotosPressed, - onAddUsers: onAddUsersPressed, - onActivities: onActivitiesPressed, - ), - ), - ], - ); - } -} diff --git a/mobile/lib/pages/album/album_viewer.page.dart b/mobile/lib/pages/album/album_viewer.page.dart deleted file mode 100644 index c99dacd9b7..0000000000 --- a/mobile/lib/pages/album/album_viewer.page.dart +++ /dev/null @@ -1,29 +0,0 @@ -import 'package:auto_route/auto_route.dart'; -import 'package:flutter/material.dart'; -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/pages/album/album_viewer.dart'; -import 'package:immich_mobile/providers/album/album.provider.dart'; -import 'package:immich_mobile/providers/album/current_album.provider.dart'; -import 'package:immich_mobile/providers/timeline.provider.dart'; - -@RoutePage() -class AlbumViewerPage extends HookConsumerWidget { - final int albumId; - - const AlbumViewerPage({super.key, required this.albumId}); - - @override - Widget build(BuildContext context, WidgetRef ref) { - // Listen provider to prevent autoDispose when navigating to other routes from within the viewer page - ref.listen(currentAlbumProvider, (_, __) {}); - - // This call helps rendering the asset selection instantly - ref.listen(assetSelectionTimelineProvider, (_, __) {}); - - ref.listen(albumWatcher(albumId), (_, albumFuture) { - albumFuture.whenData((value) => ref.read(currentAlbumProvider.notifier).set(value)); - }); - - return const Scaffold(body: AlbumViewer()); - } -} diff --git a/mobile/lib/pages/albums/albums.page.dart b/mobile/lib/pages/albums/albums.page.dart deleted file mode 100644 index 5f155c2f0d..0000000000 --- a/mobile/lib/pages/albums/albums.page.dart +++ /dev/null @@ -1,359 +0,0 @@ -import 'dart:async'; -import 'dart:math'; - -import 'package:auto_route/auto_route.dart'; -import 'package:easy_localization/easy_localization.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_hooks/flutter_hooks.dart'; -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/extensions/build_context_extensions.dart'; -import 'package:immich_mobile/extensions/theme_extensions.dart'; -import 'package:immich_mobile/extensions/translate_extensions.dart'; -import 'package:immich_mobile/models/albums/album_search.model.dart'; -import 'package:immich_mobile/pages/common/large_leading_tile.dart'; -import 'package:immich_mobile/providers/album/album.provider.dart'; -import 'package:immich_mobile/providers/album/album_sort_by_options.provider.dart'; -import 'package:immich_mobile/providers/user.provider.dart'; -import 'package:immich_mobile/routing/router.dart'; -import 'package:immich_mobile/widgets/album/album_thumbnail_card.dart'; -import 'package:immich_mobile/widgets/common/immich_app_bar.dart'; -import 'package:immich_mobile/widgets/common/immich_thumbnail.dart'; -import 'package:immich_mobile/widgets/common/search_field.dart'; - -@RoutePage() -class AlbumsPage extends HookConsumerWidget { - const AlbumsPage({super.key}); - - @override - Widget build(BuildContext context, WidgetRef ref) { - final albums = ref.watch(albumProvider).where((album) => album.isRemote).toList(); - final albumSortOption = ref.watch(albumSortByOptionsProvider); - final albumSortIsReverse = ref.watch(albumSortOrderProvider); - final sorted = albumSortOption.sortFn(albums, albumSortIsReverse); - final isGrid = useState(false); - final searchController = useTextEditingController(); - final debounceTimer = useRef(null); - final filterMode = useState(QuickFilterMode.all); - final userId = ref.watch(currentUserProvider)?.id; - final searchFocusNode = useFocusNode(); - - toggleViewMode() { - isGrid.value = !isGrid.value; - } - - onSearch(String searchTerm, QuickFilterMode mode) { - debounceTimer.value?.cancel(); - debounceTimer.value = Timer(const Duration(milliseconds: 300), () { - ref.read(albumProvider.notifier).searchAlbums(searchTerm, mode); - }); - } - - changeFilter(QuickFilterMode mode) { - filterMode.value = mode; - } - - useEffect(() { - searchController.addListener(() { - onSearch(searchController.text, filterMode.value); - }); - - return () { - searchController.removeListener(() { - onSearch(searchController.text, filterMode.value); - }); - debounceTimer.value?.cancel(); - }; - }, []); - - clearSearch() { - filterMode.value = QuickFilterMode.all; - searchController.clear(); - onSearch('', QuickFilterMode.all); - } - - return Scaffold( - appBar: ImmichAppBar( - showUploadButton: false, - actions: [ - IconButton( - icon: const Icon(Icons.add_rounded, size: 28), - onPressed: () => context.pushRoute(CreateAlbumRoute()), - ), - ], - ), - body: RefreshIndicator( - displacement: 70, - onRefresh: () async { - await ref.read(albumProvider.notifier).refreshRemoteAlbums(); - }, - child: ListView( - shrinkWrap: true, - padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 12), - children: [ - Container( - decoration: BoxDecoration( - border: Border.all(color: context.colorScheme.onSurface.withAlpha(0), width: 0), - borderRadius: const BorderRadius.all(Radius.circular(24)), - gradient: LinearGradient( - colors: [ - context.colorScheme.primary.withValues(alpha: 0.075), - context.colorScheme.primary.withValues(alpha: 0.09), - context.colorScheme.primary.withValues(alpha: 0.075), - ], - begin: Alignment.topLeft, - end: Alignment.bottomRight, - transform: const GradientRotation(0.5 * pi), - ), - ), - child: SearchField( - autofocus: false, - contentPadding: const EdgeInsets.all(16), - hintText: 'search_albums'.tr(), - prefixIcon: const Icon(Icons.search_rounded), - suffixIcon: searchController.text.isNotEmpty - ? IconButton(icon: const Icon(Icons.clear_rounded), onPressed: clearSearch) - : null, - controller: searchController, - onChanged: (_) => onSearch(searchController.text, filterMode.value), - focusNode: searchFocusNode, - onTapOutside: (_) => searchFocusNode.unfocus(), - ), - ), - const SizedBox(height: 8), - Wrap( - spacing: 4, - runSpacing: 4, - children: [ - QuickFilterButton( - label: 'all'.tr(), - isSelected: filterMode.value == QuickFilterMode.all, - onTap: () { - changeFilter(QuickFilterMode.all); - onSearch(searchController.text, QuickFilterMode.all); - }, - ), - QuickFilterButton( - label: 'shared_with_me'.tr(), - isSelected: filterMode.value == QuickFilterMode.sharedWithMe, - onTap: () { - changeFilter(QuickFilterMode.sharedWithMe); - onSearch(searchController.text, QuickFilterMode.sharedWithMe); - }, - ), - QuickFilterButton( - label: 'my_albums'.tr(), - isSelected: filterMode.value == QuickFilterMode.myAlbums, - onTap: () { - changeFilter(QuickFilterMode.myAlbums); - onSearch(searchController.text, QuickFilterMode.myAlbums); - }, - ), - ], - ), - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - const SortButton(), - IconButton( - icon: Icon(isGrid.value ? Icons.view_list_outlined : Icons.grid_view_outlined, size: 24), - onPressed: toggleViewMode, - ), - ], - ), - const SizedBox(height: 5), - AnimatedSwitcher( - duration: const Duration(milliseconds: 500), - child: isGrid.value - ? GridView.builder( - shrinkWrap: true, - physics: const ClampingScrollPhysics(), - gridDelegate: const SliverGridDelegateWithMaxCrossAxisExtent( - maxCrossAxisExtent: 250, - mainAxisSpacing: 12, - crossAxisSpacing: 12, - childAspectRatio: .7, - ), - itemBuilder: (context, index) { - return AlbumThumbnailCard( - album: sorted[index], - onTap: () => context.pushRoute(AlbumViewerRoute(albumId: sorted[index].id)), - showOwner: true, - ); - }, - itemCount: sorted.length, - ) - : ListView.builder( - shrinkWrap: true, - physics: const NeverScrollableScrollPhysics(), - itemCount: sorted.length, - itemBuilder: (context, index) { - return Padding( - padding: const EdgeInsets.only(bottom: 8.0), - child: LargeLeadingTile( - title: Text( - sorted[index].name, - maxLines: 2, - overflow: TextOverflow.ellipsis, - style: context.textTheme.titleSmall?.copyWith(fontWeight: FontWeight.w600), - ), - subtitle: sorted[index].ownerId != null - ? Text( - '${'items_count'.t(context: context, args: {'count': sorted[index].assetCount})} • ${sorted[index].ownerId != userId ? 'shared_by_user'.t(context: context, args: {'user': sorted[index].ownerName!}) : 'owned'.t(context: context)}', - overflow: TextOverflow.ellipsis, - style: context.textTheme.bodyMedium?.copyWith( - color: context.colorScheme.onSurfaceSecondary, - ), - ) - : null, - onTap: () => context.pushRoute(AlbumViewerRoute(albumId: sorted[index].id)), - leadingPadding: const EdgeInsets.only(right: 16), - leading: ClipRRect( - borderRadius: const BorderRadius.all(Radius.circular(15)), - child: ImmichThumbnail(asset: sorted[index].thumbnail.value, width: 80, height: 80), - ), - // minVerticalPadding: 1, - ), - ); - }, - ), - ), - ], - ), - ), - resizeToAvoidBottomInset: false, - ); - } -} - -class QuickFilterButton extends StatelessWidget { - const QuickFilterButton({super.key, required this.isSelected, required this.onTap, required this.label}); - - final bool isSelected; - final VoidCallback onTap; - final String label; - - @override - Widget build(BuildContext context) { - return TextButton( - onPressed: onTap, - style: ButtonStyle( - backgroundColor: WidgetStateProperty.all(isSelected ? context.colorScheme.primary : Colors.transparent), - shape: WidgetStateProperty.all( - RoundedRectangleBorder( - borderRadius: const BorderRadius.all(Radius.circular(20)), - side: BorderSide(color: context.colorScheme.onSurface.withAlpha(25), width: 1), - ), - ), - ), - child: Text( - label, - style: TextStyle( - color: isSelected ? context.colorScheme.onPrimary : context.colorScheme.onSurface, - fontSize: 14, - ), - ), - ); - } -} - -class SortButton extends ConsumerWidget { - const SortButton({super.key}); - - @override - Widget build(BuildContext context, WidgetRef ref) { - final albumSortOption = ref.watch(albumSortByOptionsProvider); - final albumSortIsReverse = ref.watch(albumSortOrderProvider); - - return MenuAnchor( - style: MenuStyle( - elevation: const WidgetStatePropertyAll(1), - shape: WidgetStateProperty.all( - const RoundedRectangleBorder(borderRadius: BorderRadius.all(Radius.circular(24))), - ), - padding: const WidgetStatePropertyAll(EdgeInsets.all(4)), - ), - consumeOutsideTap: true, - menuChildren: AlbumSortMode.values - .map( - (mode) => MenuItemButton( - leadingIcon: albumSortOption == mode - ? albumSortIsReverse - ? Icon( - Icons.keyboard_arrow_down, - color: albumSortOption == mode - ? context.colorScheme.onPrimary - : context.colorScheme.onSurface, - ) - : Icon( - Icons.keyboard_arrow_up_rounded, - color: albumSortOption == mode - ? context.colorScheme.onPrimary - : context.colorScheme.onSurface, - ) - : const Icon(Icons.abc, color: Colors.transparent), - onPressed: () { - final selected = albumSortOption == mode; - // Switch direction - if (selected) { - ref.read(albumSortOrderProvider.notifier).changeSortDirection(!albumSortIsReverse); - } else { - ref.read(albumSortByOptionsProvider.notifier).changeSortMode(mode); - } - }, - style: ButtonStyle( - padding: WidgetStateProperty.all(const EdgeInsets.fromLTRB(16, 16, 32, 16)), - backgroundColor: WidgetStateProperty.all( - albumSortOption == mode ? context.colorScheme.primary : Colors.transparent, - ), - shape: WidgetStateProperty.all( - const RoundedRectangleBorder(borderRadius: BorderRadius.all(Radius.circular(24))), - ), - ), - child: Text( - mode.label.tr(), - style: context.textTheme.titleSmall?.copyWith( - fontWeight: FontWeight.w600, - color: albumSortOption == mode - ? context.colorScheme.onPrimary - : context.colorScheme.onSurface.withAlpha(185), - ), - ), - ), - ) - .toList(), - builder: (context, controller, child) { - return GestureDetector( - onTap: () { - if (controller.isOpen) { - controller.close(); - } else { - controller.open(); - } - }, - child: Row( - children: [ - Padding( - padding: const EdgeInsets.only(right: 5), - child: Transform.rotate( - angle: 90 * pi / 180, - child: Icon( - Icons.compare_arrows_rounded, - size: 18, - color: context.colorScheme.onSurface.withAlpha(225), - ), - ), - ), - Text( - albumSortOption.label.tr(), - style: context.textTheme.bodyLarge?.copyWith( - fontWeight: FontWeight.w500, - color: context.colorScheme.onSurface.withAlpha(225), - ), - ), - ], - ), - ); - }, - ); - } -} diff --git a/mobile/lib/pages/backup/album_preview.page.dart b/mobile/lib/pages/backup/album_preview.page.dart deleted file mode 100644 index def31afcd4..0000000000 --- a/mobile/lib/pages/backup/album_preview.page.dart +++ /dev/null @@ -1,64 +0,0 @@ -import 'package:auto_route/auto_route.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_hooks/flutter_hooks.dart'; -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/entities/album.entity.dart'; -import 'package:immich_mobile/entities/asset.entity.dart'; -import 'package:immich_mobile/extensions/build_context_extensions.dart'; -import 'package:immich_mobile/extensions/theme_extensions.dart'; -import 'package:immich_mobile/repositories/album_media.repository.dart'; -import 'package:immich_mobile/widgets/common/immich_thumbnail.dart'; - -@RoutePage() -class AlbumPreviewPage extends HookConsumerWidget { - final Album album; - const AlbumPreviewPage({super.key, required this.album}); - - @override - Widget build(BuildContext context, WidgetRef ref) { - final assets = useState>([]); - - getAssetsInAlbum() async { - assets.value = await ref.read(albumMediaRepositoryProvider).getAssets(album.localId!); - } - - useEffect(() { - getAssetsInAlbum(); - return null; - }, []); - - return Scaffold( - appBar: AppBar( - elevation: 0, - title: Column( - children: [ - Text(album.name, style: const TextStyle(fontSize: 14, fontWeight: FontWeight.bold)), - Padding( - padding: const EdgeInsets.only(top: 4.0), - child: Text( - "ID ${album.id}", - style: TextStyle( - fontSize: 10, - color: context.colorScheme.onSurfaceSecondary, - fontWeight: FontWeight.bold, - ), - ), - ), - ], - ), - leading: IconButton(onPressed: () => context.maybePop(), icon: const Icon(Icons.arrow_back_ios_new_rounded)), - ), - body: GridView.builder( - gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount( - crossAxisCount: 5, - crossAxisSpacing: 2, - mainAxisSpacing: 2, - ), - itemCount: assets.value.length, - itemBuilder: (context, index) { - return ImmichThumbnail(asset: assets.value[index], width: 100, height: 100); - }, - ), - ); - } -} diff --git a/mobile/lib/pages/backup/backup_album_selection.page.dart b/mobile/lib/pages/backup/backup_album_selection.page.dart deleted file mode 100644 index d222211577..0000000000 --- a/mobile/lib/pages/backup/backup_album_selection.page.dart +++ /dev/null @@ -1,225 +0,0 @@ -import 'package:auto_route/auto_route.dart'; -import 'package:easy_localization/easy_localization.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_hooks/flutter_hooks.dart'; -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/extensions/build_context_extensions.dart'; -import 'package:immich_mobile/providers/album/album.provider.dart'; -import 'package:immich_mobile/providers/backup/backup.provider.dart'; -import 'package:immich_mobile/services/app_settings.service.dart'; -import 'package:immich_mobile/utils/hooks/app_settings_update_hook.dart'; -import 'package:immich_mobile/widgets/backup/album_info_card.dart'; -import 'package:immich_mobile/widgets/backup/album_info_list_tile.dart'; -import 'package:immich_mobile/widgets/settings/settings_switch_list_tile.dart'; - -@RoutePage() -class BackupAlbumSelectionPage extends HookConsumerWidget { - const BackupAlbumSelectionPage({super.key}); - @override - Widget build(BuildContext context, WidgetRef ref) { - final selectedBackupAlbums = ref.watch(backupProvider).selectedBackupAlbums; - final excludedBackupAlbums = ref.watch(backupProvider).excludedBackupAlbums; - final enableSyncUploadAlbum = useAppSettingsState(AppSettingsEnum.syncAlbums); - final isDarkTheme = context.isDarkTheme; - final albums = ref.watch(backupProvider).availableAlbums; - - useEffect(() { - ref.watch(backupProvider.notifier).getBackupInfo(); - return null; - }, []); - - buildAlbumSelectionList() { - if (albums.isEmpty) { - return const SliverToBoxAdapter(child: Center(child: CircularProgressIndicator())); - } - - return SliverPadding( - padding: const EdgeInsets.symmetric(vertical: 12.0), - sliver: SliverList( - delegate: SliverChildBuilderDelegate(((context, index) { - return AlbumInfoListTile(album: albums[index]); - }), childCount: albums.length), - ), - ); - } - - buildAlbumSelectionGrid() { - if (albums.isEmpty) { - return const SliverToBoxAdapter(child: Center(child: CircularProgressIndicator())); - } - - return SliverPadding( - padding: const EdgeInsets.all(12.0), - sliver: SliverGrid.builder( - gridDelegate: const SliverGridDelegateWithMaxCrossAxisExtent( - maxCrossAxisExtent: 300, - mainAxisSpacing: 12, - crossAxisSpacing: 12, - ), - itemCount: albums.length, - itemBuilder: ((context, index) { - return AlbumInfoCard(album: albums[index]); - }), - ), - ); - } - - buildSelectedAlbumNameChip() { - return selectedBackupAlbums.map((album) { - void removeSelection() => ref.read(backupProvider.notifier).removeAlbumForBackup(album); - - return Padding( - padding: const EdgeInsets.only(right: 8.0), - child: GestureDetector( - onTap: removeSelection, - child: Chip( - label: Text( - album.name, - style: TextStyle( - fontSize: 12, - color: isDarkTheme ? Colors.black : Colors.white, - fontWeight: FontWeight.bold, - ), - ), - backgroundColor: context.primaryColor, - deleteIconColor: isDarkTheme ? Colors.black : Colors.white, - deleteIcon: const Icon(Icons.cancel_rounded, size: 15), - onDeleted: removeSelection, - ), - ), - ); - }).toSet(); - } - - buildExcludedAlbumNameChip() { - return excludedBackupAlbums.map((album) { - void removeSelection() { - ref.watch(backupProvider.notifier).removeExcludedAlbumForBackup(album); - } - - return GestureDetector( - onTap: removeSelection, - child: Padding( - padding: const EdgeInsets.only(right: 8.0), - child: Chip( - label: Text( - album.name, - style: TextStyle(fontSize: 12, color: context.scaffoldBackgroundColor, fontWeight: FontWeight.bold), - ), - backgroundColor: Colors.red[300], - deleteIconColor: context.scaffoldBackgroundColor, - deleteIcon: const Icon(Icons.cancel_rounded, size: 15), - onDeleted: removeSelection, - ), - ), - ); - }).toSet(); - } - - handleSyncAlbumToggle(bool isEnable) async { - if (isEnable) { - await ref.read(albumProvider.notifier).refreshRemoteAlbums(); - for (final album in selectedBackupAlbums) { - await ref.read(albumProvider.notifier).createSyncAlbum(album.name); - } - } - } - - return Scaffold( - appBar: AppBar( - leading: IconButton(onPressed: () => context.maybePop(), icon: const Icon(Icons.arrow_back_ios_rounded)), - title: const Text("backup_album_selection_page_select_albums").tr(), - elevation: 0, - ), - body: CustomScrollView( - physics: const ClampingScrollPhysics(), - slivers: [ - SliverToBoxAdapter( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Padding( - padding: const EdgeInsets.symmetric(vertical: 8.0, horizontal: 16.0), - child: Text("backup_album_selection_page_selection_info", style: context.textTheme.titleSmall).tr(), - ), - - // Selected Album Chips - Padding( - padding: const EdgeInsets.symmetric(horizontal: 16.0), - child: Wrap(children: [...buildSelectedAlbumNameChip(), ...buildExcludedAlbumNameChip()]), - ), - - SettingsSwitchListTile( - valueNotifier: enableSyncUploadAlbum, - title: "sync_albums".tr(), - subtitle: "sync_upload_album_setting_subtitle".tr(), - contentPadding: const EdgeInsets.symmetric(horizontal: 16), - titleStyle: context.textTheme.bodyLarge?.copyWith(fontWeight: FontWeight.bold), - subtitleStyle: context.textTheme.labelLarge?.copyWith(color: context.colorScheme.primary), - onChanged: handleSyncAlbumToggle, - ), - - ListTile( - title: Text( - "backup_album_selection_page_albums_device".tr( - namedArgs: {'count': ref.watch(backupProvider).availableAlbums.length.toString()}, - ), - style: context.textTheme.titleSmall, - ), - subtitle: Padding( - padding: const EdgeInsets.symmetric(vertical: 8.0), - child: Text( - "backup_album_selection_page_albums_tap", - style: context.textTheme.labelLarge?.copyWith(color: context.primaryColor), - ).tr(), - ), - trailing: IconButton( - splashRadius: 16, - icon: Icon(Icons.info, size: 20, color: context.primaryColor), - onPressed: () { - // show the dialog - showDialog( - context: context, - builder: (BuildContext context) { - return AlertDialog( - shape: const RoundedRectangleBorder(borderRadius: BorderRadius.all(Radius.circular(10))), - elevation: 5, - title: Text( - 'backup_album_selection_page_selection_info', - style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold, color: context.primaryColor), - ).tr(), - content: SingleChildScrollView( - child: ListBody( - children: [ - const Text( - 'backup_album_selection_page_assets_scatter', - style: TextStyle(fontSize: 14), - ).tr(), - ], - ), - ), - ); - }, - ); - }, - ), - ), - - // buildSearchBar(), - ], - ), - ), - SliverLayoutBuilder( - builder: (context, constraints) { - if (constraints.crossAxisExtent > 600) { - return buildAlbumSelectionGrid(); - } else { - return buildAlbumSelectionList(); - } - }, - ), - ], - ), - ); - } -} diff --git a/mobile/lib/pages/backup/backup_controller.page.dart b/mobile/lib/pages/backup/backup_controller.page.dart deleted file mode 100644 index 1e008be1bb..0000000000 --- a/mobile/lib/pages/backup/backup_controller.page.dart +++ /dev/null @@ -1,286 +0,0 @@ -import 'dart:io'; -import 'dart:math'; - -import 'package:auto_route/auto_route.dart'; -import 'package:easy_localization/easy_localization.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_hooks/flutter_hooks.dart'; -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/extensions/build_context_extensions.dart'; -import 'package:immich_mobile/extensions/theme_extensions.dart'; -import 'package:immich_mobile/models/backup/backup_state.model.dart'; -import 'package:immich_mobile/providers/album/album.provider.dart'; -import 'package:immich_mobile/providers/backup/backup.provider.dart'; -import 'package:immich_mobile/providers/backup/error_backup_list.provider.dart'; -import 'package:immich_mobile/providers/backup/ios_background_settings.provider.dart'; -import 'package:immich_mobile/providers/backup/manual_upload.provider.dart'; -import 'package:immich_mobile/providers/websocket.provider.dart'; -import 'package:immich_mobile/routing/router.dart'; -import 'package:immich_mobile/widgets/backup/backup_info_card.dart'; -import 'package:immich_mobile/widgets/backup/current_backup_asset_info_box.dart'; -import 'package:wakelock_plus/wakelock_plus.dart'; - -@RoutePage() -class BackupControllerPage extends HookConsumerWidget { - const BackupControllerPage({super.key}); - - @override - Widget build(BuildContext context, WidgetRef ref) { - BackUpState backupState = ref.watch(backupProvider); - final hasAnyAlbum = backupState.selectedBackupAlbums.isNotEmpty; - final didGetBackupInfo = useState(false); - - bool hasExclusiveAccess = backupState.backupProgress != BackUpProgressEnum.inBackground; - bool shouldBackup = - backupState.allUniqueAssets.length - backupState.selectedAlbumsBackupAssetsIds.length == 0 || - !hasExclusiveAccess - ? false - : true; - - useEffect(() { - // Update the background settings information just to make sure we - // have the latest, since the platform channel will not update - // automatically - if (Platform.isIOS) { - ref.watch(iOSBackgroundSettingsProvider.notifier).refresh(); - } - - ref.watch(websocketProvider.notifier).stopListenToEvent('on_upload_success'); - - return () { - WakelockPlus.disable(); - }; - }, []); - - useEffect(() { - if (backupState.backupProgress == BackUpProgressEnum.idle && !didGetBackupInfo.value) { - ref.watch(backupProvider.notifier).getBackupInfo(); - didGetBackupInfo.value = true; - } - return null; - }, [backupState.backupProgress]); - - useEffect(() { - if (backupState.backupProgress == BackUpProgressEnum.inProgress) { - WakelockPlus.enable(); - } else { - WakelockPlus.disable(); - } - - return null; - }, [backupState.backupProgress]); - - Widget buildSelectedAlbumName() { - var text = "backup_controller_page_backup_selected".tr(); - var albums = ref.watch(backupProvider).selectedBackupAlbums; - - if (albums.isNotEmpty) { - for (var album in albums) { - if (album.name == "Recent" || album.name == "Recents") { - text += "${album.name} (${'all'.tr()}), "; - } else { - text += "${album.name}, "; - } - } - - return Padding( - padding: const EdgeInsets.only(top: 8.0), - child: Text( - text.trim().substring(0, text.length - 2), - style: context.textTheme.labelLarge?.copyWith(color: context.primaryColor), - ), - ); - } else { - return Padding( - padding: const EdgeInsets.only(top: 8.0), - child: Text( - "backup_controller_page_none_selected".tr(), - style: context.textTheme.labelLarge?.copyWith(color: context.primaryColor), - ), - ); - } - } - - Widget buildExcludedAlbumName() { - var text = "backup_controller_page_excluded".tr(); - var albums = ref.watch(backupProvider).excludedBackupAlbums; - - if (albums.isNotEmpty) { - for (var album in albums) { - text += "${album.name}, "; - } - - return Padding( - padding: const EdgeInsets.only(top: 8.0), - child: Text( - text.trim().substring(0, text.length - 2), - style: context.textTheme.labelLarge?.copyWith(color: Colors.red[300]), - ), - ); - } else { - return const SizedBox(); - } - } - - buildFolderSelectionTile() { - return Padding( - padding: const EdgeInsets.only(top: 8.0), - child: Card( - shape: RoundedRectangleBorder( - borderRadius: const BorderRadius.all(Radius.circular(20)), - side: BorderSide(color: context.colorScheme.outlineVariant, width: 1), - ), - elevation: 0, - borderOnForeground: false, - child: ListTile( - minVerticalPadding: 18, - title: Text("backup_controller_page_albums", style: context.textTheme.titleMedium).tr(), - subtitle: Padding( - padding: const EdgeInsets.only(top: 8.0), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - "backup_controller_page_to_backup", - style: context.textTheme.bodyMedium?.copyWith(color: context.colorScheme.onSurfaceSecondary), - ).tr(), - buildSelectedAlbumName(), - buildExcludedAlbumName(), - ], - ), - ), - trailing: ElevatedButton( - onPressed: () async { - await context.pushRoute(const BackupAlbumSelectionRoute()); - // waited until returning from selection - await ref.read(backupProvider.notifier).backupAlbumSelectionDone(); - // waited until backup albums are stored in DB - await ref.read(albumProvider.notifier).refreshDeviceAlbums(); - }, - child: const Text("select", style: TextStyle(fontWeight: FontWeight.bold)).tr(), - ), - ), - ), - ); - } - - void startBackup() { - ref.watch(errorBackupListProvider.notifier).empty(); - if (ref.watch(backupProvider).backupProgress != BackUpProgressEnum.inBackground) { - ref.watch(backupProvider.notifier).startBackupProcess(); - } - } - - Widget buildBackupButton() { - return Padding( - padding: const EdgeInsets.only(top: 24), - child: Container( - child: - backupState.backupProgress == BackUpProgressEnum.inProgress || - backupState.backupProgress == BackUpProgressEnum.manualInProgress - ? ElevatedButton( - style: ElevatedButton.styleFrom( - foregroundColor: Colors.grey[50], - backgroundColor: Colors.red[300], - // padding: const EdgeInsets.all(14), - ), - onPressed: () { - if (backupState.backupProgress == BackUpProgressEnum.manualInProgress) { - ref.read(manualUploadProvider.notifier).cancelBackup(); - } else { - ref.read(backupProvider.notifier).cancelBackup(); - } - }, - child: const Text("cancel", style: TextStyle(fontSize: 14, fontWeight: FontWeight.bold)).tr(), - ) - : ElevatedButton( - onPressed: shouldBackup ? startBackup : null, - child: const Text( - "backup_controller_page_start_backup", - style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold), - ).tr(), - ), - ), - ); - } - - buildBackgroundBackupInfo() { - return ListTile( - leading: const Icon(Icons.info_outline_rounded), - title: Text('background_backup_running_error'.tr()), - ); - } - - buildLoadingIndicator() { - return const Padding( - padding: EdgeInsets.only(top: 42.0), - child: Center(child: CircularProgressIndicator()), - ); - } - - return Scaffold( - appBar: AppBar( - elevation: 0, - title: const Text("backup_controller_page_backup").tr(), - leading: IconButton( - onPressed: () { - ref.watch(websocketProvider.notifier).listenUploadEvent(); - context.maybePop(true); - }, - splashRadius: 24, - icon: const Icon(Icons.arrow_back_ios_rounded), - ), - actions: [ - Padding( - padding: const EdgeInsets.only(right: 8.0), - child: IconButton( - onPressed: () => context.pushRoute(const BackupOptionsRoute()), - splashRadius: 24, - icon: const Icon(Icons.settings_outlined), - ), - ), - ], - ), - body: Stack( - children: [ - Padding( - padding: const EdgeInsets.only(left: 16.0, right: 16, bottom: 32), - child: ListView( - // crossAxisAlignment: CrossAxisAlignment.start, - children: hasAnyAlbum - ? [ - buildFolderSelectionTile(), - BackupInfoCard( - title: "total".tr(), - subtitle: "backup_controller_page_total_sub".tr(), - info: ref.watch(backupProvider).availableAlbums.isEmpty - ? "..." - : "${backupState.allUniqueAssets.length}", - ), - BackupInfoCard( - title: "backup_controller_page_backup".tr(), - subtitle: "backup_controller_page_backup_sub".tr(), - info: ref.watch(backupProvider).availableAlbums.isEmpty - ? "..." - : "${backupState.selectedAlbumsBackupAssetsIds.length}", - ), - BackupInfoCard( - title: "backup_controller_page_remainder".tr(), - subtitle: "backup_controller_page_remainder_sub".tr(), - info: ref.watch(backupProvider).availableAlbums.isEmpty - ? "..." - : "${max(0, backupState.allUniqueAssets.length - backupState.selectedAlbumsBackupAssetsIds.length)}", - ), - const Divider(), - const CurrentUploadingAssetInfoBox(), - if (!hasExclusiveAccess) buildBackgroundBackupInfo(), - buildBackupButton(), - ] - : [buildFolderSelectionTile(), if (!didGetBackupInfo.value) buildLoadingIndicator()], - ), - ), - ], - ), - ); - } -} diff --git a/mobile/lib/pages/backup/backup_options.page.dart b/mobile/lib/pages/backup/backup_options.page.dart deleted file mode 100644 index 846a32a742..0000000000 --- a/mobile/lib/pages/backup/backup_options.page.dart +++ /dev/null @@ -1,24 +0,0 @@ -import 'package:auto_route/auto_route.dart'; -import 'package:easy_localization/easy_localization.dart'; -import 'package:flutter/material.dart'; -import 'package:immich_mobile/widgets/settings/backup_settings/backup_settings.dart'; - -@RoutePage() -class BackupOptionsPage extends StatelessWidget { - const BackupOptionsPage({super.key}); - @override - Widget build(BuildContext context) { - return Scaffold( - appBar: AppBar( - elevation: 0, - title: const Text("backup_options_page_title").tr(), - leading: IconButton( - onPressed: () => context.maybePop(true), - splashRadius: 24, - icon: const Icon(Icons.arrow_back_ios_rounded), - ), - ), - body: const BackupSettings(), - ); - } -} diff --git a/mobile/lib/pages/backup/failed_backup_status.page.dart b/mobile/lib/pages/backup/failed_backup_status.page.dart deleted file mode 100644 index a97a133b89..0000000000 --- a/mobile/lib/pages/backup/failed_backup_status.page.dart +++ /dev/null @@ -1,116 +0,0 @@ -import 'package:auto_route/auto_route.dart'; -import 'package:flutter/material.dart'; -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/extensions/build_context_extensions.dart'; -import 'package:immich_mobile/presentation/widgets/images/local_image_provider.dart'; -import 'package:immich_mobile/providers/backup/error_backup_list.provider.dart'; -import 'package:intl/intl.dart'; -import 'package:immich_mobile/domain/models/asset/base_asset.model.dart' as base_asset; - -@RoutePage() -class FailedBackupStatusPage extends HookConsumerWidget { - const FailedBackupStatusPage({super.key}); - @override - Widget build(BuildContext context, WidgetRef ref) { - final errorBackupList = ref.watch(errorBackupListProvider); - - return Scaffold( - appBar: AppBar( - elevation: 0, - title: Text( - "Failed Backup (${errorBackupList.length})", - style: const TextStyle(fontSize: 16, fontWeight: FontWeight.bold), - ), - leading: IconButton( - onPressed: () { - context.maybePop(true); - }, - splashRadius: 24, - icon: const Icon(Icons.arrow_back_ios_rounded), - ), - ), - body: ListView.builder( - shrinkWrap: true, - itemCount: errorBackupList.length, - itemBuilder: ((context, index) { - var errorAsset = errorBackupList.elementAt(index); - - return Padding( - padding: const EdgeInsets.symmetric(horizontal: 12.0, vertical: 4), - child: Card( - shape: const RoundedRectangleBorder( - borderRadius: BorderRadius.all( - Radius.circular(15), // if you need this - ), - side: BorderSide(color: Colors.black12, width: 1), - ), - elevation: 0, - child: Row( - crossAxisAlignment: CrossAxisAlignment.center, - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - ConstrainedBox( - constraints: const BoxConstraints(minWidth: 100, minHeight: 100, maxWidth: 100, maxHeight: 150), - child: ClipRRect( - borderRadius: const BorderRadius.only( - bottomLeft: Radius.circular(15), - topLeft: Radius.circular(15), - ), - clipBehavior: Clip.hardEdge, - child: Image( - fit: BoxFit.cover, - image: LocalThumbProvider(id: errorAsset.asset.localId!, assetType: base_asset.AssetType.video), - ), - ), - ), - Expanded( - child: Padding( - padding: const EdgeInsets.all(16.0), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - mainAxisAlignment: MainAxisAlignment.spaceEvenly, - children: [ - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Text( - DateFormat.yMMMMd().format( - DateTime.parse(errorAsset.fileCreatedAt.toString()).toLocal(), - ), - style: TextStyle( - fontWeight: FontWeight.w600, - color: context.isDarkTheme ? Colors.white70 : Colors.grey[800], - ), - ), - Icon(Icons.error, color: Colors.red.withAlpha(200), size: 18), - ], - ), - Padding( - padding: const EdgeInsets.symmetric(vertical: 8.0), - child: Text( - errorAsset.fileName, - maxLines: 1, - overflow: TextOverflow.ellipsis, - style: TextStyle(fontWeight: FontWeight.bold, color: context.primaryColor), - ), - ), - Text( - errorAsset.errorMessage, - style: TextStyle( - fontWeight: FontWeight.w500, - color: context.isDarkTheme ? Colors.white70 : Colors.grey[800], - ), - ), - ], - ), - ), - ), - ], - ), - ), - ); - }), - ), - ); - } -} diff --git a/mobile/lib/pages/common/activities.page.dart b/mobile/lib/pages/common/activities.page.dart deleted file mode 100644 index 9d1123dbca..0000000000 --- a/mobile/lib/pages/common/activities.page.dart +++ /dev/null @@ -1,97 +0,0 @@ -import 'package:auto_route/auto_route.dart'; -import 'package:collection/collection.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_hooks/flutter_hooks.dart' hide Store; -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/extensions/asyncvalue_extensions.dart'; -import 'package:immich_mobile/extensions/build_context_extensions.dart'; -import 'package:immich_mobile/models/activities/activity.model.dart'; -import 'package:immich_mobile/providers/activity.provider.dart'; -import 'package:immich_mobile/providers/album/current_album.provider.dart'; -import 'package:immich_mobile/providers/asset_viewer/current_asset.provider.dart'; -import 'package:immich_mobile/providers/user.provider.dart'; -import 'package:immich_mobile/widgets/activities/activity_text_field.dart'; -import 'package:immich_mobile/widgets/activities/activity_tile.dart'; -import 'package:immich_mobile/widgets/activities/dismissible_activity.dart'; - -@RoutePage() -class ActivitiesPage extends HookConsumerWidget { - const ActivitiesPage({super.key}); - - @override - Widget build(BuildContext context, WidgetRef ref) { - // Album has to be set in the provider before reaching this page - final album = ref.watch(currentAlbumProvider)!; - final asset = ref.watch(currentAssetProvider); - final user = ref.watch(currentUserProvider); - - final activityNotifier = ref.read(albumActivityProvider(album.remoteId!, asset?.remoteId).notifier); - final activities = ref.watch(albumActivityProvider(album.remoteId!, asset?.remoteId)); - - final listViewScrollController = useScrollController(); - - Future onAddComment(String comment) async { - await activityNotifier.addComment(comment); - // Scroll to the end of the list to show the newly added activity - await listViewScrollController.animateTo( - listViewScrollController.position.maxScrollExtent + 200, - duration: const Duration(milliseconds: 600), - curve: Curves.fastOutSlowIn, - ); - } - - return Scaffold( - appBar: AppBar(title: asset == null ? Text(album.name) : null), - body: activities.widgetWhen( - onData: (data) { - final liked = data.firstWhereOrNull( - (a) => a.type == ActivityType.like && a.user.id == user?.id && a.assetId == asset?.remoteId, - ); - - return SafeArea( - child: Stack( - children: [ - ListView.builder( - controller: listViewScrollController, - // +1 to display an additional over-scroll space after the last element - itemCount: data.length + 1, - itemBuilder: (context, index) { - // Additional vertical gap after the last element - if (index == data.length) { - return const SizedBox(height: 80); - } - - final activity = data[index]; - final canDelete = activity.user.id == user?.id || album.ownerId == user?.id; - - return Padding( - padding: const EdgeInsets.all(5), - child: DismissibleActivity( - activity.id, - ActivityTile(activity), - onDismiss: canDelete - ? (activityId) async => await activityNotifier.removeActivity(activity.id) - : null, - ), - ); - }, - ), - Align( - alignment: Alignment.bottomCenter, - child: Container( - color: context.scaffoldBackgroundColor, - child: ActivityTextField( - isEnabled: album.activityEnabled, - likeId: liked?.id, - onSubmit: onAddComment, - ), - ), - ), - ], - ), - ); - }, - ), - ); - } -} diff --git a/mobile/lib/pages/common/change_experience.page.dart b/mobile/lib/pages/common/change_experience.page.dart deleted file mode 100644 index 2cc3dede1e..0000000000 --- a/mobile/lib/pages/common/change_experience.page.dart +++ /dev/null @@ -1,168 +0,0 @@ -import 'dart:async'; - -import 'package:auto_route/auto_route.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter/services.dart'; -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/domain/models/store.model.dart'; -import 'package:immich_mobile/entities/store.entity.dart'; -import 'package:immich_mobile/extensions/build_context_extensions.dart'; -import 'package:immich_mobile/infrastructure/repositories/store.repository.dart'; -import 'package:immich_mobile/providers/album/album.provider.dart'; -import 'package:immich_mobile/providers/asset.provider.dart'; -import 'package:immich_mobile/providers/background_sync.provider.dart'; -import 'package:immich_mobile/providers/backup/backup.provider.dart'; -import 'package:immich_mobile/providers/backup/manual_upload.provider.dart'; -import 'package:immich_mobile/providers/gallery_permission.provider.dart'; -import 'package:immich_mobile/providers/infrastructure/db.provider.dart'; -import 'package:immich_mobile/providers/infrastructure/platform.provider.dart'; -import 'package:immich_mobile/providers/infrastructure/readonly_mode.provider.dart'; -import 'package:immich_mobile/providers/websocket.provider.dart'; -import 'package:immich_mobile/services/app_settings.service.dart'; -import 'package:immich_mobile/services/background.service.dart'; -import 'package:immich_mobile/utils/migration.dart'; -import 'package:logging/logging.dart'; -import 'package:permission_handler/permission_handler.dart'; - -@RoutePage() -class ChangeExperiencePage extends ConsumerStatefulWidget { - final bool switchingToBeta; - - const ChangeExperiencePage({super.key, required this.switchingToBeta}); - - @override - ConsumerState createState() => _ChangeExperiencePageState(); -} - -class _ChangeExperiencePageState extends ConsumerState { - AsyncValue hasMigrated = const AsyncValue.loading(); - - @override - void initState() { - super.initState(); - WidgetsBinding.instance.addPostFrameCallback((_) => _handleMigration()); - } - - Future _handleMigration() async { - try { - await _performMigrationLogic().timeout( - const Duration(minutes: 3), - onTimeout: () async { - await IsarStoreRepository(ref.read(isarProvider)).upsert(StoreKey.betaTimeline, widget.switchingToBeta); - await DriftStoreRepository(ref.read(driftProvider)).upsert(StoreKey.betaTimeline, widget.switchingToBeta); - }, - ); - - if (mounted) { - setState(() { - HapticFeedback.heavyImpact(); - hasMigrated = const AsyncValue.data(true); - }); - } - } catch (e, s) { - Logger("ChangeExperiencePage").severe("Error during migration", e, s); - if (mounted) { - setState(() { - hasMigrated = AsyncValue.error(e, s); - }); - } - } - } - - Future _performMigrationLogic() async { - if (widget.switchingToBeta) { - final assetNotifier = ref.read(assetProvider.notifier); - if (assetNotifier.mounted) { - assetNotifier.dispose(); - } - final albumNotifier = ref.read(albumProvider.notifier); - if (albumNotifier.mounted) { - albumNotifier.dispose(); - } - - // Cancel uploads - await Store.put(StoreKey.backgroundBackup, false); - ref - .read(backupProvider.notifier) - .configureBackgroundBackup(enabled: false, onBatteryInfo: () {}, onError: (_) {}); - ref.read(backupProvider.notifier).setAutoBackup(false); - ref.read(backupProvider.notifier).cancelBackup(); - ref.read(manualUploadProvider.notifier).cancelBackup(); - // Start listening to new websocket events - ref.read(websocketProvider.notifier).stopListenToOldEvents(); - ref.read(websocketProvider.notifier).startListeningToBetaEvents(); - - await ref.read(driftProvider).reset(); - await Store.put(StoreKey.shouldResetSync, true); - final delay = Store.get(StoreKey.backupTriggerDelay, AppSettingsEnum.backupTriggerDelay.defaultValue); - if (delay >= 1000) { - await Store.put(StoreKey.backupTriggerDelay, (delay / 1000).toInt()); - } - final permission = await ref.read(galleryPermissionNotifier.notifier).requestGalleryPermission(); - - if (permission.isGranted) { - await ref.read(backgroundSyncProvider).syncLocal(full: true); - await migrateDeviceAssetToSqlite(ref.read(isarProvider), ref.read(driftProvider)); - await migrateBackupAlbumsToSqlite(ref.read(isarProvider), ref.read(driftProvider)); - await migrateStoreToSqlite(ref.read(isarProvider), ref.read(driftProvider)); - await ref.read(backgroundServiceProvider).disableService(); - } - } else { - await ref.read(backgroundSyncProvider).cancel(); - ref.read(websocketProvider.notifier).stopListeningToBetaEvents(); - ref.read(websocketProvider.notifier).startListeningToOldEvents(); - ref.read(readonlyModeProvider.notifier).setReadonlyMode(false); - await migrateStoreToIsar(ref.read(isarProvider), ref.read(driftProvider)); - await ref.read(backgroundServiceProvider).resumeServiceIfEnabled(); - await ref.read(backgroundWorkerFgServiceProvider).disable(); - } - - await IsarStoreRepository(ref.read(isarProvider)).upsert(StoreKey.betaTimeline, widget.switchingToBeta); - await DriftStoreRepository(ref.read(driftProvider)).upsert(StoreKey.betaTimeline, widget.switchingToBeta); - } - - @override - Widget build(BuildContext context) { - return Scaffold( - body: Center( - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - AnimatedSwitcher( - duration: Durations.long4, - child: hasMigrated.when( - data: (data) => const Icon(Icons.check_circle_rounded, color: Colors.green, size: 48.0), - error: (error, stackTrace) => const Icon(Icons.error, color: Colors.red, size: 48.0), - loading: () => const SizedBox(width: 50.0, height: 50.0, child: CircularProgressIndicator()), - ), - ), - const SizedBox(height: 16.0), - SizedBox( - width: 300.0, - child: AnimatedSwitcher( - duration: Durations.long4, - child: hasMigrated.when( - data: (data) => Text( - "Migration success!\nPlease close and reopen the app to apply changes", - style: context.textTheme.titleMedium, - textAlign: TextAlign.center, - ), - error: (error, stackTrace) => Text( - "Migration failed!\nError: $error", - style: context.textTheme.titleMedium, - textAlign: TextAlign.center, - ), - loading: () => Text( - "Data migration in progress...\nPlease wait and don't close this page", - style: context.textTheme.titleMedium, - textAlign: TextAlign.center, - ), - ), - ), - ), - ], - ), - ), - ); - } -} diff --git a/mobile/lib/pages/common/create_album.page.dart b/mobile/lib/pages/common/create_album.page.dart deleted file mode 100644 index 0a28dfeb5a..0000000000 --- a/mobile/lib/pages/common/create_album.page.dart +++ /dev/null @@ -1,238 +0,0 @@ -import 'dart:async'; - -import 'package:auto_route/auto_route.dart'; -import 'package:easy_localization/easy_localization.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_hooks/flutter_hooks.dart'; -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/entities/asset.entity.dart'; -import 'package:immich_mobile/extensions/build_context_extensions.dart'; -import 'package:immich_mobile/models/albums/asset_selection_page_result.model.dart'; -import 'package:immich_mobile/providers/album/album.provider.dart'; -import 'package:immich_mobile/providers/album/album_title.provider.dart'; -import 'package:immich_mobile/providers/album/album_viewer.provider.dart'; -import 'package:immich_mobile/routing/router.dart'; -import 'package:immich_mobile/widgets/album/album_action_filled_button.dart'; -import 'package:immich_mobile/widgets/album/album_title_text_field.dart'; -import 'package:immich_mobile/widgets/album/album_viewer_editable_description.dart'; -import 'package:immich_mobile/widgets/album/shared_album_thumbnail_image.dart'; - -@RoutePage() -// ignore: must_be_immutable -class CreateAlbumPage extends HookConsumerWidget { - final List? assets; - - const CreateAlbumPage({super.key, this.assets}); - - @override - Widget build(BuildContext context, WidgetRef ref) { - final albumTitleController = useTextEditingController.fromValue(TextEditingValue.empty); - final albumTitleTextFieldFocusNode = useFocusNode(); - final albumDescriptionTextFieldFocusNode = useFocusNode(); - final isAlbumTitleTextFieldFocus = useState(false); - final isAlbumTitleEmpty = useState(true); - final selectedAssets = useState>(assets != null ? Set.from(assets!) : const {}); - - void onBackgroundTapped() { - albumTitleTextFieldFocusNode.unfocus(); - albumDescriptionTextFieldFocusNode.unfocus(); - isAlbumTitleTextFieldFocus.value = false; - - if (albumTitleController.text.isEmpty) { - albumTitleController.text = 'create_album_page_untitled'.tr(); - isAlbumTitleEmpty.value = false; - ref.watch(albumTitleProvider.notifier).setAlbumTitle('create_album_page_untitled'.tr()); - } - } - - onSelectPhotosButtonPressed() async { - AssetSelectionPageResult? selectedAsset = await context.pushRoute( - AlbumAssetSelectionRoute(existingAssets: selectedAssets.value, canDeselect: true), - ); - if (selectedAsset == null) { - selectedAssets.value = const {}; - } else { - selectedAssets.value = selectedAsset.selectedAssets; - } - } - - buildTitleInputField() { - return Padding( - padding: const EdgeInsets.only(right: 10, left: 10), - child: AlbumTitleTextField( - isAlbumTitleEmpty: isAlbumTitleEmpty, - albumTitleTextFieldFocusNode: albumTitleTextFieldFocusNode, - albumTitleController: albumTitleController, - isAlbumTitleTextFieldFocus: isAlbumTitleTextFieldFocus, - ), - ); - } - - buildDescriptionInputField() { - return Padding( - padding: const EdgeInsets.only(right: 10, left: 10), - child: AlbumViewerEditableDescription( - albumDescription: '', - descriptionFocusNode: albumDescriptionTextFieldFocusNode, - ), - ); - } - - buildTitle() { - if (selectedAssets.value.isEmpty) { - return SliverToBoxAdapter( - child: Padding( - padding: const EdgeInsets.only(top: 200, left: 18), - child: Text('create_shared_album_page_share_add_assets', style: context.textTheme.labelLarge).tr(), - ), - ); - } - - return const SliverToBoxAdapter(); - } - - buildSelectPhotosButton() { - if (selectedAssets.value.isEmpty) { - return SliverToBoxAdapter( - child: Padding( - padding: const EdgeInsets.only(top: 16, left: 16, right: 16), - child: FilledButton.icon( - style: FilledButton.styleFrom( - alignment: Alignment.centerLeft, - padding: const EdgeInsets.symmetric(vertical: 24, horizontal: 16), - shape: const RoundedRectangleBorder(borderRadius: BorderRadius.all(Radius.circular(10))), - backgroundColor: context.colorScheme.surfaceContainerHigh, - ), - onPressed: onSelectPhotosButtonPressed, - icon: Icon(Icons.add_rounded, color: context.primaryColor), - label: Padding( - padding: const EdgeInsets.only(left: 8.0), - child: Text( - 'create_shared_album_page_share_select_photos', - style: context.textTheme.titleMedium?.copyWith( - fontWeight: FontWeight.w600, - color: context.primaryColor, - ), - ).tr(), - ), - ), - ), - ); - } - - return const SliverToBoxAdapter(); - } - - buildControlButton() { - return Padding( - padding: const EdgeInsets.only(left: 12.0, top: 16, bottom: 16), - child: SizedBox( - height: 42, - child: ListView( - scrollDirection: Axis.horizontal, - children: [ - AlbumActionFilledButton( - iconData: Icons.add_photo_alternate_outlined, - onPressed: onSelectPhotosButtonPressed, - labelText: "add_photos".tr(), - ), - ], - ), - ), - ); - } - - buildSelectedImageGrid() { - if (selectedAssets.value.isNotEmpty) { - return SliverPadding( - padding: const EdgeInsets.only(top: 16), - sliver: SliverGrid( - gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount( - crossAxisCount: 3, - crossAxisSpacing: 5.0, - mainAxisSpacing: 5, - ), - delegate: SliverChildBuilderDelegate((BuildContext context, int index) { - return GestureDetector( - onTap: onBackgroundTapped, - child: SharedAlbumThumbnailImage(asset: selectedAssets.value.elementAt(index)), - ); - }, childCount: selectedAssets.value.length), - ), - ); - } - - return const SliverToBoxAdapter(); - } - - Future createAlbum() async { - onBackgroundTapped(); - var newAlbum = await ref - .watch(albumProvider.notifier) - .createAlbum(ref.read(albumTitleProvider), selectedAssets.value); - - if (newAlbum != null) { - await ref.read(albumProvider.notifier).refreshRemoteAlbums(); - selectedAssets.value = {}; - ref.read(albumTitleProvider.notifier).clearAlbumTitle(); - ref.read(albumViewerProvider.notifier).disableEditAlbum(); - unawaited(context.replaceRoute(AlbumViewerRoute(albumId: newAlbum.id))); - } - } - - return Scaffold( - appBar: AppBar( - elevation: 0, - centerTitle: false, - backgroundColor: context.scaffoldBackgroundColor, - leading: IconButton( - onPressed: () { - selectedAssets.value = {}; - context.maybePop(); - }, - icon: const Icon(Icons.close_rounded), - ), - title: const Text('create_album').tr(), - actions: [ - TextButton( - onPressed: albumTitleController.text.isNotEmpty ? createAlbum : null, - child: Text( - 'create'.tr(), - style: TextStyle( - fontWeight: FontWeight.bold, - color: albumTitleController.text.isNotEmpty ? context.primaryColor : context.themeData.disabledColor, - ), - ), - ), - ], - ), - body: GestureDetector( - onTap: onBackgroundTapped, - child: CustomScrollView( - slivers: [ - SliverAppBar( - backgroundColor: context.scaffoldBackgroundColor, - elevation: 5, - automaticallyImplyLeading: false, - pinned: true, - floating: false, - bottom: PreferredSize( - preferredSize: const Size.fromHeight(125.0), - child: Column( - children: [ - buildTitleInputField(), - buildDescriptionInputField(), - if (selectedAssets.value.isNotEmpty) buildControlButton(), - ], - ), - ), - ), - buildTitle(), - buildSelectPhotosButton(), - buildSelectedImageGrid(), - ], - ), - ), - ); - } -} diff --git a/mobile/lib/pages/common/gallery_stacked_children.dart b/mobile/lib/pages/common/gallery_stacked_children.dart deleted file mode 100644 index 68123509ae..0000000000 --- a/mobile/lib/pages/common/gallery_stacked_children.dart +++ /dev/null @@ -1,85 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/presentation/widgets/images/remote_image_provider.dart'; -import 'package:immich_mobile/providers/asset_viewer/asset_stack.provider.dart'; -import 'package:immich_mobile/providers/asset_viewer/current_asset.provider.dart'; -import 'package:immich_mobile/providers/asset_viewer/show_controls.provider.dart'; - -class GalleryStackedChildren extends HookConsumerWidget { - final ValueNotifier stackIndex; - - const GalleryStackedChildren(this.stackIndex, {super.key}); - - @override - Widget build(BuildContext context, WidgetRef ref) { - final asset = ref.watch(currentAssetProvider); - if (asset == null) { - return const SizedBox(); - } - - final stackId = asset.stackId; - if (stackId == null) { - return const SizedBox(); - } - - final stackElements = ref.watch(assetStackStateProvider(stackId)); - final showControls = ref.watch(showControlsProvider); - - return IgnorePointer( - ignoring: !showControls, - child: AnimatedOpacity( - duration: const Duration(milliseconds: 100), - opacity: showControls ? 1.0 : 0.0, - child: SizedBox( - height: 80, - child: ListView.builder( - shrinkWrap: true, - scrollDirection: Axis.horizontal, - itemCount: stackElements.length, - padding: const EdgeInsets.only(left: 5, right: 5, bottom: 30), - itemBuilder: (context, index) { - final currentAsset = stackElements.elementAt(index); - final assetId = currentAsset.remoteId; - if (assetId == null) { - return const SizedBox(); - } - - return Padding( - key: ValueKey(currentAsset.id), - padding: const EdgeInsets.only(right: 5), - child: GestureDetector( - onTap: () { - stackIndex.value = index; - ref.read(currentAssetProvider.notifier).set(currentAsset); - }, - child: Container( - width: 60, - height: 60, - decoration: index == stackIndex.value - ? const BoxDecoration( - color: Colors.white, - borderRadius: BorderRadius.all(Radius.circular(6)), - border: Border.fromBorderSide(BorderSide(color: Colors.white, width: 2)), - ) - : const BoxDecoration( - color: Colors.white, - borderRadius: BorderRadius.all(Radius.circular(6)), - border: null, - ), - child: ClipRRect( - borderRadius: const BorderRadius.all(Radius.circular(4)), - child: Image( - fit: BoxFit.cover, - image: RemoteImageProvider.thumbnail(assetId: assetId, thumbhash: asset.thumbhash ?? ""), - ), - ), - ), - ), - ); - }, - ), - ), - ), - ); - } -} diff --git a/mobile/lib/pages/common/gallery_viewer.page.dart b/mobile/lib/pages/common/gallery_viewer.page.dart deleted file mode 100644 index 1d43bff167..0000000000 --- a/mobile/lib/pages/common/gallery_viewer.page.dart +++ /dev/null @@ -1,438 +0,0 @@ -import 'dart:async'; -import 'dart:io'; -import 'dart:math'; -import 'dart:ui' as ui; - -import 'package:auto_route/auto_route.dart'; -import 'package:easy_localization/easy_localization.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter/services.dart'; -import 'package:flutter_hooks/flutter_hooks.dart' hide Store; -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/entities/asset.entity.dart'; -import 'package:immich_mobile/extensions/build_context_extensions.dart'; -import 'package:immich_mobile/extensions/scroll_extensions.dart'; -import 'package:immich_mobile/pages/common/download_panel.dart'; -import 'package:immich_mobile/pages/common/gallery_stacked_children.dart'; -import 'package:immich_mobile/pages/common/native_video_viewer.page.dart'; -import 'package:immich_mobile/providers/app_settings.provider.dart'; -import 'package:immich_mobile/providers/asset_viewer/asset_stack.provider.dart'; -import 'package:immich_mobile/providers/asset_viewer/current_asset.provider.dart'; -import 'package:immich_mobile/providers/asset_viewer/is_motion_video_playing.provider.dart'; -import 'package:immich_mobile/providers/asset_viewer/show_controls.provider.dart'; -import 'package:immich_mobile/providers/cast.provider.dart'; -import 'package:immich_mobile/providers/haptic_feedback.provider.dart'; -import 'package:immich_mobile/services/app_settings.service.dart'; -import 'package:immich_mobile/widgets/asset_grid/asset_grid_data_structure.dart'; -import 'package:immich_mobile/widgets/asset_viewer/advanced_bottom_sheet.dart'; -import 'package:immich_mobile/widgets/asset_viewer/bottom_gallery_bar.dart'; -import 'package:immich_mobile/widgets/asset_viewer/detail_panel/detail_panel.dart'; -import 'package:immich_mobile/widgets/asset_viewer/gallery_app_bar.dart'; -import 'package:immich_mobile/widgets/common/immich_image.dart'; -import 'package:immich_mobile/widgets/common/immich_thumbnail.dart'; -import 'package:immich_mobile/widgets/photo_view/photo_view_gallery.dart'; -import 'package:immich_mobile/widgets/photo_view/src/photo_view_computed_scale.dart'; -import 'package:immich_mobile/widgets/photo_view/src/photo_view_scale_state.dart'; -import 'package:immich_mobile/widgets/photo_view/src/utils/photo_view_hero_attributes.dart'; - -@RoutePage() -// ignore: must_be_immutable -/// Expects [currentAssetProvider] to be set before navigating to this page -class GalleryViewerPage extends HookConsumerWidget { - final int initialIndex; - final int heroOffset; - final bool showStack; - final RenderList renderList; - - GalleryViewerPage({ - super.key, - required this.renderList, - this.initialIndex = 0, - this.heroOffset = 0, - this.showStack = false, - }) : controller = PageController(initialPage: initialIndex); - - final PageController controller; - - @override - Widget build(BuildContext context, WidgetRef ref) { - final totalAssets = useState(renderList.totalAssets); - final isZoomed = useState(false); - final stackIndex = useState(0); - final localPosition = useRef(null); - final currentIndex = useValueNotifier(initialIndex); - final loadAsset = renderList.loadAsset; - final isPlayingMotionVideo = ref.watch(isPlayingMotionVideoProvider); - final isCasting = ref.watch(castProvider.select((c) => c.isCasting)); - - final videoPlayerKeys = useRef>({}); - - GlobalKey getVideoPlayerKey(int id) { - videoPlayerKeys.value.putIfAbsent(id, () => GlobalKey()); - return videoPlayerKeys.value[id]!; - } - - Future precacheNextImage(int index) async { - if (!context.mounted) { - return; - } - - void onError(Object exception, StackTrace? stackTrace) { - // swallow error silently - log.severe('Error precaching next image: $exception, $stackTrace'); - } - - try { - if (index < totalAssets.value && index >= 0) { - final asset = loadAsset(index); - await precacheImage( - ImmichImage.imageProvider(asset: asset, width: context.width, height: context.height), - context, - onError: onError, - ); - } - } catch (e) { - // swallow error silently - log.severe('Error precaching next image: $e'); - await context.maybePop(); - } - } - - useEffect(() { - if (ref.read(showControlsProvider)) { - SystemChrome.setEnabledSystemUIMode(SystemUiMode.edgeToEdge); - } else { - SystemChrome.setEnabledSystemUIMode(SystemUiMode.immersive); - } - - // Delay this a bit so we can finish loading the page - Timer(const Duration(milliseconds: 400), () { - precacheNextImage(currentIndex.value + 1); - }); - - return null; - }, const []); - - useEffect(() { - final asset = loadAsset(currentIndex.value); - - if (asset.isRemote) { - ref.read(castProvider.notifier).loadMediaOld(asset, false); - } else { - if (isCasting) { - WidgetsBinding.instance.addPostFrameCallback((_) { - if (context.mounted) { - ref.read(castProvider.notifier).stop(); - context.scaffoldMessenger.showSnackBar( - SnackBar( - duration: const Duration(seconds: 1), - content: Text( - "local_asset_cast_failed".tr(), - style: context.textTheme.bodyLarge?.copyWith(color: context.primaryColor), - ), - ), - ); - } - }); - } - } - return null; - }, [ref.watch(castProvider).isCasting]); - - void showInfo() { - final asset = ref.read(currentAssetProvider); - if (asset == null) { - return; - } - showModalBottomSheet( - shape: const RoundedRectangleBorder(borderRadius: BorderRadius.all(Radius.circular(15.0))), - barrierColor: Colors.transparent, - isScrollControlled: true, - showDragHandle: true, - enableDrag: true, - context: context, - useSafeArea: true, - builder: (context) { - return DraggableScrollableSheet( - minChildSize: 0.5, - maxChildSize: 1, - initialChildSize: 0.75, - expand: false, - builder: (context, scrollController) { - return Padding( - padding: EdgeInsets.only(bottom: context.viewInsets.bottom), - child: ref.watch(appSettingsServiceProvider).getSetting(AppSettingsEnum.advancedTroubleshooting) - ? AdvancedBottomSheet(assetDetail: asset, scrollController: scrollController) - : DetailPanel(asset: asset, scrollController: scrollController), - ); - }, - ); - }, - ); - } - - void handleSwipeUpDown(DragUpdateDetails details) { - const int sensitivity = 15; - const int dxThreshold = 50; - const double ratioThreshold = 3.0; - - if (isZoomed.value) { - return; - } - - // Guard [localPosition] null - if (localPosition.value == null) { - return; - } - - // Check for delta from initial down point - final d = details.localPosition - localPosition.value!; - // If the magnitude of the dx swipe is large, we probably didn't mean to go down - if (d.dx.abs() > dxThreshold) { - return; - } - - final ratio = d.dy / max(d.dx.abs(), 1); - if (d.dy > sensitivity && ratio > ratioThreshold) { - context.maybePop(); - } else if (d.dy < -sensitivity && ratio < -ratioThreshold) { - showInfo(); - } - } - - ref.listen(showControlsProvider, (_, show) { - if (show || Platform.isIOS) { - SystemChrome.setEnabledSystemUIMode(SystemUiMode.edgeToEdge); - return; - } - - // This prevents the bottom bar from "dropping" while the controls are being hidden - Timer(const Duration(milliseconds: 100), () { - SystemChrome.setEnabledSystemUIMode(SystemUiMode.immersive); - }); - }); - - PhotoViewGalleryPageOptions buildImage(Asset asset) { - return PhotoViewGalleryPageOptions( - onDragStart: (_, details, __, ___) { - localPosition.value = details.localPosition; - }, - onDragUpdate: (_, details, __) { - handleSwipeUpDown(details); - }, - onTapDown: (ctx, tapDownDetails, _) { - final tapToNavigate = ref.read(appSettingsServiceProvider).getSetting(AppSettingsEnum.tapToNavigate); - if (!tapToNavigate) { - ref.read(showControlsProvider.notifier).toggle(); - return; - } - - double tapX = tapDownDetails.globalPosition.dx; - double screenWidth = ctx.width; - - // We want to change images if the user taps in the leftmost or - // rightmost quarter of the screen - bool tappedLeftSide = tapX < screenWidth / 4; - bool tappedRightSide = tapX > screenWidth * (3 / 4); - - int? currentPage = controller.page?.toInt(); - int maxPage = renderList.totalAssets - 1; - - if (tappedLeftSide && currentPage != null) { - // Nested if because we don't want to fallback to show/hide controls - if (currentPage != 0) { - controller.jumpToPage(currentPage - 1); - } - } else if (tappedRightSide && currentPage != null) { - // Nested if because we don't want to fallback to show/hide controls - if (currentPage != maxPage) { - controller.jumpToPage(currentPage + 1); - } - } else { - ref.read(showControlsProvider.notifier).toggle(); - } - }, - onLongPressStart: asset.isMotionPhoto - ? (_, __, ___) { - ref.read(isPlayingMotionVideoProvider.notifier).playing = true; - } - : null, - imageProvider: ImmichImage.imageProvider(asset: asset), - heroAttributes: _getHeroAttributes(asset), - filterQuality: FilterQuality.high, - tightMode: true, - initialScale: PhotoViewComputedScale.contained * 0.99, - minScale: PhotoViewComputedScale.contained * 0.99, - errorBuilder: (context, error, stackTrace) => ImmichImage(asset, fit: BoxFit.contain), - ); - } - - PhotoViewGalleryPageOptions buildVideo(BuildContext context, Asset asset) { - return PhotoViewGalleryPageOptions.customChild( - onDragStart: (_, details, __, ___) => localPosition.value = details.localPosition, - onDragUpdate: (_, details, __) => handleSwipeUpDown(details), - heroAttributes: _getHeroAttributes(asset), - filterQuality: FilterQuality.high, - initialScale: PhotoViewComputedScale.contained * 0.99, - maxScale: 1.0, - minScale: PhotoViewComputedScale.contained * 0.99, - basePosition: Alignment.center, - child: SizedBox( - width: context.width, - height: context.height, - child: NativeVideoViewerPage( - key: getVideoPlayerKey(asset.id), - asset: asset, - image: Image( - key: ValueKey(asset), - image: ImmichImage.imageProvider(asset: asset, width: context.width, height: context.height), - fit: BoxFit.contain, - height: context.height, - width: context.width, - alignment: Alignment.center, - ), - ), - ), - ); - } - - PhotoViewGalleryPageOptions buildAsset(BuildContext context, int index) { - var newAsset = loadAsset(index); - - final stackId = newAsset.stackId; - if (stackId != null && currentIndex.value == index) { - final stackElements = ref.read(assetStackStateProvider(newAsset.stackId!)); - if (stackIndex.value < stackElements.length) { - newAsset = stackElements.elementAt(stackIndex.value); - } - } - - if (newAsset.isImage && !isPlayingMotionVideo) { - return buildImage(newAsset); - } - return buildVideo(context, newAsset); - } - - return PopScope( - // Change immersive mode back to normal "edgeToEdge" mode - onPopInvokedWithResult: (didPop, _) => SystemChrome.setEnabledSystemUIMode(SystemUiMode.edgeToEdge), - child: Scaffold( - backgroundColor: Colors.black, - body: Stack( - children: [ - PhotoViewGallery.builder( - key: const ValueKey('gallery'), - scaleStateChangedCallback: (state) { - final asset = ref.read(currentAssetProvider); - if (asset == null) { - return; - } - - if (asset.isImage && !ref.read(isPlayingMotionVideoProvider)) { - isZoomed.value = state != PhotoViewScaleState.initial; - ref.read(showControlsProvider.notifier).show = !isZoomed.value; - } - }, - gaplessPlayback: true, - loadingBuilder: (context, event, index) { - final asset = loadAsset(index); - return ClipRect( - child: Stack( - fit: StackFit.expand, - children: [ - BackdropFilter(filter: ui.ImageFilter.blur(sigmaX: 10, sigmaY: 10)), - ImmichThumbnail(key: ValueKey(asset), asset: asset, fit: BoxFit.contain), - ], - ), - ); - }, - pageController: controller, - scrollPhysics: isZoomed.value - ? const NeverScrollableScrollPhysics() // Don't allow paging while scrolled in - : (Platform.isIOS - ? const FastScrollPhysics() // Use bouncing physics for iOS - : const FastClampingScrollPhysics() // Use heavy physics for Android - ), - itemCount: totalAssets.value, - scrollDirection: Axis.horizontal, - onPageChanged: (value, _) { - final next = currentIndex.value < value ? value + 1 : value - 1; - - ref.read(hapticFeedbackProvider.notifier).selectionClick(); - - final newAsset = loadAsset(value); - - currentIndex.value = value; - stackIndex.value = 0; - - ref.read(currentAssetProvider.notifier).set(newAsset); - - // Wait for page change animation to finish, then precache the next image - Timer(const Duration(milliseconds: 400), () { - precacheNextImage(next); - }); - - context.scaffoldMessenger.hideCurrentSnackBar(); - - // send image to casting if the server has it - if (newAsset.isRemote) { - ref.read(castProvider.notifier).loadMediaOld(newAsset, false); - } else { - context.scaffoldMessenger.clearSnackBars(); - - if (isCasting) { - ref.read(castProvider.notifier).stop(); - context.scaffoldMessenger.showSnackBar( - SnackBar( - duration: const Duration(seconds: 2), - content: Text( - "local_asset_cast_failed".tr(), - style: context.textTheme.bodyLarge?.copyWith(color: context.primaryColor), - ), - ), - ); - } - } - }, - builder: buildAsset, - ), - Positioned( - top: 0, - left: 0, - right: 0, - child: GalleryAppBar(key: const ValueKey('app-bar'), showInfo: showInfo), - ), - Positioned( - bottom: 0, - left: 0, - right: 0, - child: Column( - children: [ - GalleryStackedChildren(stackIndex), - BottomGalleryBar( - key: const ValueKey('bottom-bar'), - renderList: renderList, - totalAssets: totalAssets, - controller: controller, - showStack: showStack, - stackIndex: stackIndex, - assetIndex: currentIndex, - ), - ], - ), - ), - const DownloadPanel(), - ], - ), - ), - ); - } - - @pragma('vm:prefer-inline') - PhotoViewHeroAttributes _getHeroAttributes(Asset asset) { - return PhotoViewHeroAttributes( - tag: asset.isInDb ? asset.id + heroOffset : '${asset.remoteId}-$heroOffset', - transitionOnUserGestures: true, - ); - } -} diff --git a/mobile/lib/pages/common/native_video_viewer.page.dart b/mobile/lib/pages/common/native_video_viewer.page.dart deleted file mode 100644 index b1eed29c5c..0000000000 --- a/mobile/lib/pages/common/native_video_viewer.page.dart +++ /dev/null @@ -1,282 +0,0 @@ -import 'dart:async'; -import 'dart:io'; - -import 'package:auto_route/auto_route.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_hooks/flutter_hooks.dart' hide Store; -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/domain/models/store.model.dart'; -import 'package:immich_mobile/entities/asset.entity.dart'; -import 'package:immich_mobile/entities/store.entity.dart'; -import 'package:immich_mobile/providers/app_settings.provider.dart'; -import 'package:immich_mobile/providers/asset_viewer/current_asset.provider.dart'; -import 'package:immich_mobile/providers/asset_viewer/is_motion_video_playing.provider.dart'; -import 'package:immich_mobile/providers/asset_viewer/video_player_provider.dart'; -import 'package:immich_mobile/providers/cast.provider.dart'; -import 'package:immich_mobile/services/api.service.dart'; -import 'package:immich_mobile/services/app_settings.service.dart'; -import 'package:immich_mobile/services/asset.service.dart'; -import 'package:immich_mobile/widgets/asset_viewer/custom_video_player_controls.dart'; -import 'package:logging/logging.dart'; -import 'package:native_video_player/native_video_player.dart'; - -@RoutePage() -class NativeVideoViewerPage extends HookConsumerWidget { - static final log = Logger('NativeVideoViewer'); - final Asset asset; - final bool showControls; - final int playbackDelayFactor; - final Widget image; - - const NativeVideoViewerPage({ - super.key, - required this.asset, - required this.image, - this.showControls = true, - this.playbackDelayFactor = 1, - }); - - @override - Widget build(BuildContext context, WidgetRef ref) { - final videoId = asset.id.toString(); - final controller = useState(null); - final shouldPlayOnForeground = useRef(true); - - final currentAsset = useState(ref.read(currentAssetProvider)); - final isCurrent = currentAsset.value == asset; - - // Used to show the placeholder during hero animations for remote videos to avoid a stutter - final isVisible = useState(Platform.isIOS && asset.isLocal); - - final isCasting = ref.watch(castProvider.select((c) => c.isCasting)); - - final isVideoReady = useState(false); - - Future createSource() async { - if (!context.mounted) { - return null; - } - - try { - final local = asset.local; - if (local != null && asset.livePhotoVideoId == null) { - final file = await local.file; - if (file == null) { - throw Exception('No file found for the video'); - } - - final source = await VideoSource.init(path: file.path, type: VideoSourceType.file); - return source; - } - - // Use a network URL for the video player controller - final serverEndpoint = Store.get(StoreKey.serverEndpoint); - final isOriginalVideo = ref - .read(appSettingsServiceProvider) - .getSetting(AppSettingsEnum.loadOriginalVideo); - final String postfixUrl = isOriginalVideo ? 'original' : 'video/playback'; - final String videoUrl = asset.livePhotoVideoId != null - ? '$serverEndpoint/assets/${asset.livePhotoVideoId}/$postfixUrl' - : '$serverEndpoint/assets/${asset.remoteId}/$postfixUrl'; - - final source = await VideoSource.init( - path: videoUrl, - type: VideoSourceType.network, - headers: ApiService.getRequestHeaders(), - ); - return source; - } catch (error) { - log.severe('Error creating video source for asset ${asset.fileName}: $error'); - return null; - } - } - - final videoSource = useMemoized>(() => createSource()); - final aspectRatio = useState(asset.aspectRatio); - useMemoized(() async { - if (!context.mounted || aspectRatio.value != null) { - return null; - } - - try { - aspectRatio.value = await ref.read(assetServiceProvider).getAspectRatio(asset); - } catch (error) { - log.severe('Error getting aspect ratio for asset ${asset.fileName}: $error'); - } - }); - - void onPlaybackReady() async { - final videoController = controller.value; - if (videoController == null || !isCurrent || !context.mounted) { - return; - } - - final notifier = ref.read(videoPlayerProvider(videoId).notifier); - notifier.onNativePlaybackReady(); - - isVideoReady.value = true; - - try { - final autoPlayVideo = ref.read(appSettingsServiceProvider).getSetting(AppSettingsEnum.autoPlayVideo); - if (autoPlayVideo) { - await notifier.play(); - } - await notifier.setVolume(1); - } catch (error) { - log.severe('Error playing video: $error'); - } - } - - void onPlaybackStatusChanged() { - if (!context.mounted) return; - ref.read(videoPlayerProvider(videoId).notifier).onNativeStatusChanged(); - } - - void onPlaybackPositionChanged() { - if (!context.mounted) return; - ref.read(videoPlayerProvider(videoId).notifier).onNativePositionChanged(); - } - - void onPlaybackEnded() { - if (!context.mounted) return; - - ref.read(videoPlayerProvider(videoId).notifier).onNativePlaybackEnded(); - - final videoController = controller.value; - if (videoController?.playbackInfo?.status == PlaybackStatus.stopped && - !ref.read(appSettingsServiceProvider).getSetting(AppSettingsEnum.loopVideo)) { - ref.read(isPlayingMotionVideoProvider.notifier).playing = false; - } - } - - void removeListeners(NativeVideoPlayerController controller) { - controller.onPlaybackPositionChanged.removeListener(onPlaybackPositionChanged); - controller.onPlaybackStatusChanged.removeListener(onPlaybackStatusChanged); - controller.onPlaybackReady.removeListener(onPlaybackReady); - controller.onPlaybackEnded.removeListener(onPlaybackEnded); - } - - void initController(NativeVideoPlayerController nc) async { - if (controller.value != null || !context.mounted) { - return; - } - - final source = await videoSource; - if (source == null) { - return; - } - - final notifier = ref.read(videoPlayerProvider(videoId).notifier); - notifier.attachController(nc); - - nc.onPlaybackPositionChanged.addListener(onPlaybackPositionChanged); - nc.onPlaybackStatusChanged.addListener(onPlaybackStatusChanged); - nc.onPlaybackReady.addListener(onPlaybackReady); - nc.onPlaybackEnded.addListener(onPlaybackEnded); - - unawaited( - nc.loadVideoSource(source).catchError((error) { - log.severe('Error loading video source: $error'); - }), - ); - final loopVideo = ref.read(appSettingsServiceProvider).getSetting(AppSettingsEnum.loopVideo); - await notifier.setLoop(loopVideo); - - controller.value = nc; - } - - ref.listen(currentAssetProvider, (_, value) { - final playerController = controller.value; - if (playerController != null && value != asset) { - removeListeners(playerController); - } - - final curAsset = currentAsset.value; - if (curAsset == asset) { - return; - } - - final imageToVideo = curAsset != null && !curAsset.isVideo; - - // No need to delay video playback when swiping from an image to a video - if (imageToVideo && Platform.isIOS) { - currentAsset.value = value; - onPlaybackReady(); - return; - } - - // Delay the video playback to avoid a stutter in the swipe animation - Timer( - Platform.isIOS - ? Duration(milliseconds: 300 * playbackDelayFactor) - : imageToVideo - ? Duration(milliseconds: 200 * playbackDelayFactor) - : Duration(milliseconds: 400 * playbackDelayFactor), - () { - if (!context.mounted) { - return; - } - - currentAsset.value = value; - if (currentAsset.value == asset) { - onPlaybackReady(); - } - }, - ); - }); - - useEffect(() { - // If opening a remote video from a hero animation, delay visibility to avoid a stutter - final timer = isVisible.value ? null : Timer(const Duration(milliseconds: 300), () => isVisible.value = true); - - return () { - timer?.cancel(); - final playerController = controller.value; - if (playerController == null) { - return; - } - removeListeners(playerController); - playerController.stop().catchError((error) { - log.fine('Error stopping video: $error'); - }); - }; - }, const []); - - useOnAppLifecycleStateChange((_, state) async { - final notifier = ref.read(videoPlayerProvider(videoId).notifier); - if (state == AppLifecycleState.resumed && shouldPlayOnForeground.value) { - await notifier.play(); - } else if (state == AppLifecycleState.paused) { - final videoPlaying = await controller.value?.isPlaying(); - if (videoPlaying ?? true) { - shouldPlayOnForeground.value = true; - await notifier.pause(); - } else { - shouldPlayOnForeground.value = false; - } - } - }); - - return Stack( - children: [ - // This remains under the video to avoid flickering - // For motion videos, this is the image portion of the asset - if (!isVideoReady.value || asset.isMotionPhoto) Center(key: ValueKey(asset.id), child: image), - if (aspectRatio.value != null && !isCasting) - Visibility.maintain( - key: ValueKey(asset), - visible: isVisible.value, - child: Center( - key: ValueKey(asset), - child: AspectRatio( - key: ValueKey(asset), - aspectRatio: aspectRatio.value!, - child: isCurrent ? NativeVideoPlayerView(key: ValueKey(asset), onViewReady: initController) : null, - ), - ), - ), - if (showControls) Center(child: CustomVideoPlayerControls(videoId: videoId)), - ], - ); - } -} diff --git a/mobile/lib/pages/common/settings.page.dart b/mobile/lib/pages/common/settings.page.dart index e8f5eb2ee2..65970ee294 100644 --- a/mobile/lib/pages/common/settings.page.dart +++ b/mobile/lib/pages/common/settings.page.dart @@ -2,14 +2,11 @@ import 'package:auto_route/auto_route.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:flutter_hooks/flutter_hooks.dart' hide Store; -import 'package:immich_mobile/domain/models/store.model.dart'; -import 'package:immich_mobile/entities/store.entity.dart'; import 'package:immich_mobile/extensions/build_context_extensions.dart'; import 'package:immich_mobile/routing/router.dart'; import 'package:immich_mobile/widgets/settings/advanced_settings.dart'; import 'package:immich_mobile/widgets/settings/asset_list_settings/asset_list_settings.dart'; import 'package:immich_mobile/widgets/settings/asset_viewer_settings/asset_viewer_settings.dart'; -import 'package:immich_mobile/widgets/settings/backup_settings/backup_settings.dart'; import 'package:immich_mobile/widgets/settings/backup_settings/drift_backup_settings.dart'; import 'package:immich_mobile/widgets/settings/beta_sync_settings/sync_status_and_actions.dart'; import 'package:immich_mobile/widgets/settings/free_up_space_settings.dart'; @@ -38,8 +35,7 @@ enum SettingSection { Widget get widget => switch (this) { SettingSection.advanced => const AdvancedSettings(), SettingSection.assetViewer => const AssetViewerSettings(), - SettingSection.backup => - Store.tryGet(StoreKey.betaTimeline) ?? false ? const DriftBackupSettings() : const BackupSettings(), + SettingSection.backup => const DriftBackupSettings(), SettingSection.freeUpSpace => const FreeUpSpaceSettings(), SettingSection.languages => const LanguageSettings(), SettingSection.networking => const NetworkingSettings(), @@ -74,13 +70,12 @@ class _MobileLayout extends StatelessWidget { .expand( (setting) => setting == SettingSection.beta ? [ - if (Store.isBetaTimelineEnabled) - SettingsCard( - icon: Icons.sync_outlined, - title: 'sync_status'.tr(), - subtitle: 'sync_status_subtitle'.tr(), - settingRoute: const SyncStatusRoute(), - ), + SettingsCard( + icon: Icons.sync_outlined, + title: 'sync_status'.tr(), + subtitle: 'sync_status_subtitle'.tr(), + settingRoute: const SyncStatusRoute(), + ), ] : [ SettingsCard( diff --git a/mobile/lib/pages/common/splash_screen.page.dart b/mobile/lib/pages/common/splash_screen.page.dart index 37c6b95806..725f7f9e85 100644 --- a/mobile/lib/pages/common/splash_screen.page.dart +++ b/mobile/lib/pages/common/splash_screen.page.dart @@ -12,13 +12,9 @@ import 'package:immich_mobile/domain/models/store.model.dart'; import 'package:immich_mobile/entities/store.entity.dart'; import 'package:immich_mobile/generated/codegen_loader.g.dart'; import 'package:immich_mobile/generated/translations.g.dart'; -import 'package:path/path.dart' as path; -import 'package:path_provider/path_provider.dart'; import 'package:immich_mobile/providers/auth.provider.dart'; import 'package:immich_mobile/providers/background_sync.provider.dart'; -import 'package:immich_mobile/providers/backup/backup.provider.dart'; import 'package:immich_mobile/providers/backup/drift_backup.provider.dart'; -import 'package:immich_mobile/providers/gallery_permission.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'; @@ -27,6 +23,8 @@ import 'package:immich_mobile/theme/theme_data.dart'; import 'package:immich_mobile/widgets/common/immich_logo.dart'; import 'package:immich_mobile/widgets/common/immich_title_text.dart'; import 'package:logging/logging.dart'; +import 'package:path/path.dart' as path; +import 'package:path_provider/path_provider.dart'; import 'package:url_launcher/url_launcher.dart' show launchUrl, LaunchMode; class BootstrapErrorWidget extends StatelessWidget { @@ -323,29 +321,27 @@ class SplashScreenPageState extends ConsumerState { wsProvider.connect(); unawaited(infoProvider.getServerInfo()); - if (Store.isBetaTimelineEnabled) { - bool syncSuccess = false; + bool syncSuccess = false; + await Future.wait([ + backgroundManager.syncLocal(full: true), + backgroundManager.syncRemote().then((success) => syncSuccess = success), + ]); + + if (syncSuccess) { await Future.wait([ - backgroundManager.syncLocal(full: true), - backgroundManager.syncRemote().then((success) => syncSuccess = success), + backgroundManager.hashAssets().then((_) { + _resumeBackup(backupProvider); + }), + _resumeBackup(backupProvider), + // TODO: Bring back when the soft freeze issue is addressed + // backgroundManager.syncCloudIds(), ]); + } else { + await backgroundManager.hashAssets(); + } - if (syncSuccess) { - await Future.wait([ - backgroundManager.hashAssets().then((_) { - _resumeBackup(backupProvider); - }), - _resumeBackup(backupProvider), - // TODO: Bring back when the soft freeze issue is addressed - // backgroundManager.syncCloudIds(), - ]); - } else { - await backgroundManager.hashAssets(); - } - - if (Store.get(StoreKey.syncAlbums, false)) { - await backgroundManager.syncLinkedAlbum(); - } + if (Store.get(StoreKey.syncAlbums, false)) { + await backgroundManager.syncLinkedAlbum(); } } catch (e) { log.severe('Failed establishing connection to the server: $e'); @@ -368,58 +364,7 @@ class SplashScreenPageState extends ConsumerState { // clean install - change the default of the flag // current install not using beta timeline if (context.router.current.name == SplashScreenRoute.name) { - final needBetaMigration = Store.get(StoreKey.needBetaMigration, false); - if (needBetaMigration) { - bool migrate = - (await showDialog( - context: context, - builder: (ctx) => AlertDialog( - title: const Text("New Timeline Experience"), - content: const Text( - "The old timeline has been deprecated and will be removed in an upcoming release. Would you like to switch to the new timeline now?", - ), - actions: [ - TextButton(onPressed: () => Navigator.of(ctx).pop(false), child: const Text("No")), - ElevatedButton(onPressed: () => Navigator.of(ctx).pop(true), child: const Text("Yes")), - ], - ), - )) ?? - false; - if (migrate != true) { - migrate = - (await showDialog( - context: context, - builder: (ctx) => AlertDialog( - title: const Text("Are you sure?"), - content: const Text( - "If you choose to remain on the old timeline, you will be automatically migrated to the new timeline in an upcoming release. Would you like to switch now?", - ), - actions: [ - TextButton(onPressed: () => Navigator.of(ctx).pop(false), child: const Text("No")), - ElevatedButton(onPressed: () => Navigator.of(ctx).pop(true), child: const Text("Yes")), - ], - ), - )) ?? - false; - } - await Store.put(StoreKey.needBetaMigration, false); - if (migrate) { - unawaited(context.router.replaceAll([ChangeExperienceRoute(switchingToBeta: true)])); - return; - } - } - - unawaited(context.replaceRoute(Store.isBetaTimelineEnabled ? const TabShellRoute() : const TabControllerRoute())); - } - - if (Store.isBetaTimelineEnabled) { - return; - } - - final hasPermission = await ref.read(galleryPermissionNotifier.notifier).hasPermission; - if (hasPermission) { - // Resume backup (if enable) then navigate - await ref.watch(backupProvider.notifier).resumeBackup(); + unawaited(context.replaceRoute(const TabShellRoute())); } } diff --git a/mobile/lib/pages/common/tab_controller.page.dart b/mobile/lib/pages/common/tab_controller.page.dart deleted file mode 100644 index ef637ba1c8..0000000000 --- a/mobile/lib/pages/common/tab_controller.page.dart +++ /dev/null @@ -1,142 +0,0 @@ -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/extensions/build_context_extensions.dart'; -import 'package:immich_mobile/providers/album/album.provider.dart'; -import 'package:immich_mobile/providers/asset.provider.dart'; -import 'package:immich_mobile/providers/asset_viewer/scroll_notifier.provider.dart'; -import 'package:immich_mobile/providers/haptic_feedback.provider.dart'; -import 'package:immich_mobile/providers/multiselect.provider.dart'; -import 'package:immich_mobile/providers/search/search_input_focus.provider.dart'; -import 'package:immich_mobile/providers/tab.provider.dart'; -import 'package:immich_mobile/routing/router.dart'; - -@RoutePage() -class TabControllerPage extends HookConsumerWidget { - const TabControllerPage({super.key}); - - @override - Widget build(BuildContext context, WidgetRef ref) { - final isRefreshingAssets = ref.watch(assetProvider); - final isRefreshingRemoteAlbums = ref.watch(isRefreshingRemoteAlbumProvider); - final isScreenLandscape = MediaQuery.orientationOf(context) == Orientation.landscape; - - Widget buildIcon({required Widget icon, required bool isProcessing}) { - if (!isProcessing) return icon; - return Stack( - alignment: Alignment.center, - clipBehavior: Clip.none, - children: [ - icon, - Positioned( - right: -18, - child: SizedBox( - height: 20, - width: 20, - child: CircularProgressIndicator( - strokeWidth: 2, - valueColor: AlwaysStoppedAnimation(context.primaryColor), - ), - ), - ), - ], - ); - } - - void onNavigationSelected(TabsRouter router, int index) { - // On Photos page menu tapped - if (router.activeIndex == 0 && index == 0) { - scrollToTopNotifierProvider.scrollToTop(); - } - - // On Search page tapped - if (router.activeIndex == 1 && index == 1) { - ref.read(searchInputFocusProvider).requestFocus(); - } - - ref.read(hapticFeedbackProvider.notifier).selectionClick(); - router.setActiveIndex(index); - ref.read(tabProvider.notifier).state = TabEnum.values[index]; - } - - final navigationDestinations = [ - NavigationDestination( - label: 'photos'.tr(), - icon: const Icon(Icons.photo_library_outlined), - selectedIcon: buildIcon( - isProcessing: isRefreshingAssets, - icon: Icon(Icons.photo_library, color: context.primaryColor), - ), - ), - NavigationDestination( - label: 'search'.tr(), - icon: const Icon(Icons.search_rounded), - selectedIcon: Icon(Icons.search, color: context.primaryColor), - ), - NavigationDestination( - label: 'albums'.tr(), - icon: const Icon(Icons.photo_album_outlined), - selectedIcon: buildIcon( - isProcessing: isRefreshingRemoteAlbums, - icon: Icon(Icons.photo_album_rounded, color: context.primaryColor), - ), - ), - NavigationDestination( - label: 'library'.tr(), - icon: const Icon(Icons.space_dashboard_outlined), - selectedIcon: buildIcon( - isProcessing: isRefreshingAssets, - icon: Icon(Icons.space_dashboard_rounded, color: context.primaryColor), - ), - ), - ]; - - Widget bottomNavigationBar(TabsRouter tabsRouter) { - return NavigationBar( - selectedIndex: tabsRouter.activeIndex, - onDestinationSelected: (index) => onNavigationSelected(tabsRouter, index), - destinations: navigationDestinations, - ); - } - - Widget navigationRail(TabsRouter tabsRouter) { - return NavigationRail( - destinations: navigationDestinations - .map((e) => NavigationRailDestination(icon: e.icon, label: Text(e.label), selectedIcon: e.selectedIcon)) - .toList(), - onDestinationSelected: (index) => onNavigationSelected(tabsRouter, index), - selectedIndex: tabsRouter.activeIndex, - labelType: NavigationRailLabelType.all, - groupAlignment: 0.0, - ); - } - - final multiselectEnabled = ref.watch(multiselectProvider); - return AutoTabsRouter( - routes: [const PhotosRoute(), SearchRoute(), const AlbumsRoute(), const LibraryRoute()], - duration: const Duration(milliseconds: 600), - transitionBuilder: (context, child, animation) => FadeTransition(opacity: animation, child: child), - builder: (context, child) { - final tabsRouter = AutoTabsRouter.of(context); - return PopScope( - canPop: tabsRouter.activeIndex == 0, - onPopInvokedWithResult: (didPop, _) => !didPop ? tabsRouter.setActiveIndex(0) : null, - child: Scaffold( - resizeToAvoidBottomInset: false, - body: isScreenLandscape - ? Row( - children: [ - navigationRail(tabsRouter), - const VerticalDivider(), - Expanded(child: child), - ], - ) - : child, - bottomNavigationBar: multiselectEnabled || isScreenLandscape ? null : bottomNavigationBar(tabsRouter), - ), - ); - }, - ); - } -} diff --git a/mobile/lib/pages/editing/crop.page.dart b/mobile/lib/pages/editing/crop.page.dart deleted file mode 100644 index a6a66c1358..0000000000 --- a/mobile/lib/pages/editing/crop.page.dart +++ /dev/null @@ -1,177 +0,0 @@ -import 'dart:async'; - -import 'package:auto_route/auto_route.dart'; -import 'package:crop_image/crop_image.dart'; -import 'package:easy_localization/easy_localization.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_hooks/flutter_hooks.dart'; -import 'package:immich_mobile/entities/asset.entity.dart'; -import 'package:immich_mobile/extensions/build_context_extensions.dart'; -import 'package:immich_mobile/pages/editing/edit.page.dart'; -import 'package:immich_mobile/routing/router.dart'; -import 'package:immich_mobile/utils/hooks/crop_controller_hook.dart'; - -/// A widget for cropping an image. -/// This widget uses [HookWidget] to manage its lifecycle and state. It allows -/// users to crop an image and then navigate to the [EditImagePage] with the -/// cropped image. - -@RoutePage() -class CropImagePage extends HookWidget { - final Image image; - final Asset asset; - const CropImagePage({super.key, required this.image, required this.asset}); - - @override - Widget build(BuildContext context) { - final cropController = useCropController(); - final aspectRatio = useState(null); - - return Scaffold( - appBar: AppBar( - backgroundColor: context.scaffoldBackgroundColor, - title: Text("crop".tr()), - leading: CloseButton(color: context.primaryColor), - actions: [ - IconButton( - icon: Icon(Icons.done_rounded, color: context.primaryColor, size: 24), - onPressed: () async { - final croppedImage = await cropController.croppedImage(); - unawaited(context.pushRoute(EditImageRoute(asset: asset, image: croppedImage, isEdited: true))); - }, - ), - ], - ), - backgroundColor: context.scaffoldBackgroundColor, - body: SafeArea( - child: LayoutBuilder( - builder: (BuildContext context, BoxConstraints constraints) { - return Column( - children: [ - Container( - padding: const EdgeInsets.only(top: 20), - width: constraints.maxWidth * 0.9, - height: constraints.maxHeight * 0.6, - child: CropImage(controller: cropController, image: image, gridColor: Colors.white), - ), - Expanded( - child: Container( - width: double.infinity, - decoration: BoxDecoration( - color: context.scaffoldBackgroundColor, - borderRadius: const BorderRadius.only( - topLeft: Radius.circular(20), - topRight: Radius.circular(20), - ), - ), - child: Center( - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Padding( - padding: const EdgeInsets.only(left: 20, right: 20, bottom: 10), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - IconButton( - icon: Icon(Icons.rotate_left, color: context.themeData.iconTheme.color), - onPressed: () { - cropController.rotateLeft(); - }, - ), - IconButton( - icon: Icon(Icons.rotate_right, color: context.themeData.iconTheme.color), - onPressed: () { - cropController.rotateRight(); - }, - ), - ], - ), - ), - Row( - mainAxisAlignment: MainAxisAlignment.spaceEvenly, - children: [ - _AspectRatioButton( - cropController: cropController, - aspectRatio: aspectRatio, - ratio: null, - label: 'Free', - ), - _AspectRatioButton( - cropController: cropController, - aspectRatio: aspectRatio, - ratio: 1.0, - label: '1:1', - ), - _AspectRatioButton( - cropController: cropController, - aspectRatio: aspectRatio, - ratio: 16.0 / 9.0, - label: '16:9', - ), - _AspectRatioButton( - cropController: cropController, - aspectRatio: aspectRatio, - ratio: 3.0 / 2.0, - label: '3:2', - ), - _AspectRatioButton( - cropController: cropController, - aspectRatio: aspectRatio, - ratio: 7.0 / 5.0, - label: '7:5', - ), - ], - ), - ], - ), - ), - ), - ), - ], - ); - }, - ), - ), - ); - } -} - -class _AspectRatioButton extends StatelessWidget { - final CropController cropController; - final ValueNotifier aspectRatio; - final double? ratio; - final String label; - - const _AspectRatioButton({ - required this.cropController, - required this.aspectRatio, - required this.ratio, - required this.label, - }); - - @override - Widget build(BuildContext context) { - return Column( - mainAxisSize: MainAxisSize.min, - children: [ - IconButton( - icon: Icon(switch (label) { - 'Free' => Icons.crop_free_rounded, - '1:1' => Icons.crop_square_rounded, - '16:9' => Icons.crop_16_9_rounded, - '3:2' => Icons.crop_3_2_rounded, - '7:5' => Icons.crop_7_5_rounded, - _ => Icons.crop_free_rounded, - }, color: aspectRatio.value == ratio ? context.primaryColor : context.themeData.iconTheme.color), - onPressed: () { - cropController.crop = const Rect.fromLTRB(0.1, 0.1, 0.9, 0.9); - aspectRatio.value = ratio; - cropController.aspectRatio = ratio; - }, - ), - Text(label, style: context.textTheme.displayMedium), - ], - ); - } -} diff --git a/mobile/lib/pages/editing/edit.page.dart b/mobile/lib/pages/editing/edit.page.dart deleted file mode 100644 index 2889785d0b..0000000000 --- a/mobile/lib/pages/editing/edit.page.dart +++ /dev/null @@ -1,131 +0,0 @@ -import 'dart:typed_data'; - -import 'package:auto_route/auto_route.dart'; -import 'package:easy_localization/easy_localization.dart'; -import 'package:flutter/material.dart'; -import 'package:fluttertoast/fluttertoast.dart'; -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/entities/asset.entity.dart'; -import 'package:immich_mobile/extensions/build_context_extensions.dart'; -import 'package:immich_mobile/providers/album/album.provider.dart'; -import 'package:immich_mobile/repositories/file_media.repository.dart'; -import 'package:immich_mobile/routing/router.dart'; -import 'package:immich_mobile/utils/image_converter.dart'; -import 'package:immich_mobile/widgets/common/immich_toast.dart'; -import 'package:path/path.dart' as p; - -/// A stateless widget that provides functionality for editing an image. -/// -/// This widget allows users to edit an image provided either as an [Asset] or -/// directly as an [Image]. It ensures that exactly one of these is provided. -/// -/// It also includes a conversion method to convert an [Image] to a [Uint8List] to save the image on the user's phone -/// They automatically navigate to the [HomePage] with the edited image saved and they eventually get backed up to the server. -@immutable -@RoutePage() -class EditImagePage extends ConsumerWidget { - final Asset asset; - final Image image; - final bool isEdited; - - const EditImagePage({super.key, required this.asset, required this.image, required this.isEdited}); - - Future _saveEditedImage(BuildContext context, Asset asset, Image image, WidgetRef ref) async { - try { - final Uint8List imageData = await imageToUint8List(image); - await ref - .read(fileMediaRepositoryProvider) - .saveImage(imageData, title: "${p.withoutExtension(asset.fileName)}_edited.jpg"); - await ref.read(albumProvider.notifier).refreshDeviceAlbums(); - context.navigator.popUntil((route) => route.isFirst); - ImmichToast.show(durationInSecond: 3, context: context, msg: 'Image Saved!', gravity: ToastGravity.CENTER); - } catch (e) { - ImmichToast.show( - durationInSecond: 6, - context: context, - msg: "error_saving_image".tr(namedArgs: {'error': e.toString()}), - gravity: ToastGravity.CENTER, - ); - } - } - - @override - Widget build(BuildContext context, WidgetRef ref) { - return Scaffold( - appBar: AppBar( - title: Text("edit".tr()), - backgroundColor: context.scaffoldBackgroundColor, - leading: IconButton( - icon: Icon(Icons.close_rounded, color: context.primaryColor, size: 24), - onPressed: () => context.navigator.popUntil((route) => route.isFirst), - ), - actions: [ - TextButton( - onPressed: isEdited ? () => _saveEditedImage(context, asset, image, ref) : null, - child: Text("save_to_gallery".tr(), style: TextStyle(color: isEdited ? context.primaryColor : Colors.grey)), - ), - ], - ), - backgroundColor: context.scaffoldBackgroundColor, - body: Center( - child: ConstrainedBox( - constraints: BoxConstraints(maxHeight: context.height * 0.7, maxWidth: context.width * 0.9), - child: Container( - decoration: BoxDecoration( - borderRadius: const BorderRadius.all(Radius.circular(7)), - boxShadow: [ - BoxShadow( - color: Colors.black.withValues(alpha: 0.2), - spreadRadius: 2, - blurRadius: 10, - offset: const Offset(0, 3), - ), - ], - ), - child: ClipRRect( - borderRadius: const BorderRadius.all(Radius.circular(7)), - child: Image(image: image.image, fit: BoxFit.contain), - ), - ), - ), - ), - bottomNavigationBar: Container( - height: 70, - margin: const EdgeInsets.only(bottom: 60, right: 10, left: 10, top: 10), - decoration: BoxDecoration( - color: context.scaffoldBackgroundColor, - borderRadius: const BorderRadius.all(Radius.circular(30)), - ), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceEvenly, - children: [ - Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - IconButton( - icon: Icon(Icons.crop_rotate_rounded, color: context.themeData.iconTheme.color, size: 25), - onPressed: () { - context.pushRoute(CropImageRoute(asset: asset, image: image)); - }, - ), - Text("crop".tr(), style: context.textTheme.displayMedium), - ], - ), - Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - IconButton( - icon: Icon(Icons.filter, color: context.themeData.iconTheme.color, size: 25), - onPressed: () { - context.pushRoute(FilterImageRoute(asset: asset, image: image)); - }, - ), - Text("filter".tr(), style: context.textTheme.displayMedium), - ], - ), - ], - ), - ), - ); - } -} diff --git a/mobile/lib/pages/editing/filter.page.dart b/mobile/lib/pages/editing/filter.page.dart deleted file mode 100644 index f8b144bb96..0000000000 --- a/mobile/lib/pages/editing/filter.page.dart +++ /dev/null @@ -1,159 +0,0 @@ -import 'dart:async'; -import 'dart:ui' as ui; - -import 'package:auto_route/auto_route.dart'; -import 'package:easy_localization/easy_localization.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_hooks/flutter_hooks.dart'; -import 'package:immich_mobile/constants/filters.dart'; -import 'package:immich_mobile/entities/asset.entity.dart'; -import 'package:immich_mobile/extensions/build_context_extensions.dart'; -import 'package:immich_mobile/routing/router.dart'; - -/// A widget for filtering an image. -/// This widget uses [HookWidget] to manage its lifecycle and state. It allows -/// users to add filters to an image and then navigate to the [EditImagePage] with the -/// final composition.' -@RoutePage() -class FilterImagePage extends HookWidget { - final Image image; - final Asset asset; - - const FilterImagePage({super.key, required this.image, required this.asset}); - - @override - Widget build(BuildContext context) { - final colorFilter = useState(filters[0]); - final selectedFilterIndex = useState(0); - - Future createFilteredImage(ui.Image inputImage, ColorFilter filter) { - final completer = Completer(); - final size = Size(inputImage.width.toDouble(), inputImage.height.toDouble()); - final recorder = ui.PictureRecorder(); - final canvas = Canvas(recorder); - - final paint = Paint()..colorFilter = filter; - canvas.drawImage(inputImage, Offset.zero, paint); - - recorder.endRecording().toImage(size.width.round(), size.height.round()).then((image) { - completer.complete(image); - }); - - return completer.future; - } - - void applyFilter(ColorFilter filter, int index) { - colorFilter.value = filter; - selectedFilterIndex.value = index; - } - - Future applyFilterAndConvert(ColorFilter filter) async { - final completer = Completer(); - image.image - .resolve(ImageConfiguration.empty) - .addListener( - ImageStreamListener((ImageInfo info, bool _) { - completer.complete(info.image); - }), - ); - final uiImage = await completer.future; - - final filteredUiImage = await createFilteredImage(uiImage, filter); - final byteData = await filteredUiImage.toByteData(format: ui.ImageByteFormat.png); - final pngBytes = byteData!.buffer.asUint8List(); - - return Image.memory(pngBytes, fit: BoxFit.contain); - } - - return Scaffold( - appBar: AppBar( - backgroundColor: context.scaffoldBackgroundColor, - title: Text("filter".tr()), - leading: CloseButton(color: context.primaryColor), - actions: [ - IconButton( - icon: Icon(Icons.done_rounded, color: context.primaryColor, size: 24), - onPressed: () async { - final filteredImage = await applyFilterAndConvert(colorFilter.value); - unawaited(context.pushRoute(EditImageRoute(asset: asset, image: filteredImage, isEdited: true))); - }, - ), - ], - ), - backgroundColor: context.scaffoldBackgroundColor, - body: Column( - children: [ - SizedBox( - height: context.height * 0.7, - child: Center( - child: ColorFiltered(colorFilter: colorFilter.value, child: image), - ), - ), - SizedBox( - height: 120, - child: ListView.builder( - scrollDirection: Axis.horizontal, - itemCount: filters.length, - itemBuilder: (context, index) { - return Padding( - padding: const EdgeInsets.symmetric(horizontal: 8.0), - child: _FilterButton( - image: image, - label: filterNames[index], - filter: filters[index], - isSelected: selectedFilterIndex.value == index, - onTap: () => applyFilter(filters[index], index), - ), - ); - }, - ), - ), - ], - ), - ); - } -} - -class _FilterButton extends StatelessWidget { - final Image image; - final String label; - final ColorFilter filter; - final bool isSelected; - final VoidCallback onTap; - - const _FilterButton({ - required this.image, - required this.label, - required this.filter, - required this.isSelected, - required this.onTap, - }); - - @override - Widget build(BuildContext context) { - return Column( - children: [ - GestureDetector( - onTap: onTap, - child: Container( - width: 80, - height: 80, - decoration: BoxDecoration( - borderRadius: const BorderRadius.all(Radius.circular(10)), - border: isSelected ? Border.all(color: context.primaryColor, width: 3) : null, - ), - child: ClipRRect( - borderRadius: const BorderRadius.all(Radius.circular(10)), - child: ColorFiltered( - colorFilter: filter, - child: FittedBox(fit: BoxFit.cover, child: image), - ), - ), - ), - ), - const SizedBox(height: 10), - Text(label, style: context.themeData.textTheme.bodyMedium), - ], - ); - } -} diff --git a/mobile/lib/pages/library/archive.page.dart b/mobile/lib/pages/library/archive.page.dart deleted file mode 100644 index 8ca1bb9752..0000000000 --- a/mobile/lib/pages/library/archive.page.dart +++ /dev/null @@ -1,37 +0,0 @@ -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/providers/multiselect.provider.dart'; -import 'package:immich_mobile/providers/timeline.provider.dart'; -import 'package:immich_mobile/widgets/asset_grid/multiselect_grid.dart'; - -@RoutePage() -class ArchivePage extends HookConsumerWidget { - const ArchivePage({super.key}); - - @override - Widget build(BuildContext context, WidgetRef ref) { - AppBar buildAppBar() { - final archiveRenderList = ref.watch(archiveTimelineProvider); - final count = archiveRenderList.value?.totalAssets.toString() ?? "?"; - return AppBar( - leading: IconButton(onPressed: () => context.maybePop(), icon: const Icon(Icons.arrow_back_ios_rounded)), - centerTitle: true, - automaticallyImplyLeading: false, - title: const Text('archive_page_title').tr(namedArgs: {'count': count}), - ); - } - - return Scaffold( - appBar: ref.watch(multiselectProvider) ? null : buildAppBar(), - body: MultiselectGrid( - renderListProvider: archiveTimelineProvider, - unarchive: true, - archiveEnabled: true, - deleteEnabled: true, - editEnabled: true, - ), - ); - } -} diff --git a/mobile/lib/pages/library/favorite.page.dart b/mobile/lib/pages/library/favorite.page.dart deleted file mode 100644 index 649d7727d5..0000000000 --- a/mobile/lib/pages/library/favorite.page.dart +++ /dev/null @@ -1,34 +0,0 @@ -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/providers/multiselect.provider.dart'; -import 'package:immich_mobile/providers/timeline.provider.dart'; -import 'package:immich_mobile/widgets/asset_grid/multiselect_grid.dart'; - -@RoutePage() -class FavoritesPage extends HookConsumerWidget { - const FavoritesPage({super.key}); - - @override - Widget build(BuildContext context, WidgetRef ref) { - AppBar buildAppBar() { - return AppBar( - leading: IconButton(onPressed: () => context.maybePop(), icon: const Icon(Icons.arrow_back_ios_rounded)), - centerTitle: true, - automaticallyImplyLeading: false, - title: const Text('favorites').tr(), - ); - } - - return Scaffold( - appBar: ref.watch(multiselectProvider) ? null : buildAppBar(), - body: MultiselectGrid( - renderListProvider: favoriteTimelineProvider, - favoriteEnabled: true, - editEnabled: true, - unfavorite: true, - ), - ); - } -} diff --git a/mobile/lib/pages/library/library.page.dart b/mobile/lib/pages/library/library.page.dart deleted file mode 100644 index 99a534e9cf..0000000000 --- a/mobile/lib/pages/library/library.page.dart +++ /dev/null @@ -1,383 +0,0 @@ -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/domain/models/user.model.dart'; -import 'package:immich_mobile/extensions/asyncvalue_extensions.dart'; -import 'package:immich_mobile/extensions/build_context_extensions.dart'; -import 'package:immich_mobile/generated/translations.g.dart'; -import 'package:immich_mobile/providers/album/album.provider.dart'; -import 'package:immich_mobile/providers/partner.provider.dart'; -import 'package:immich_mobile/providers/search/people.provider.dart'; -import 'package:immich_mobile/providers/server_info.provider.dart'; -import 'package:immich_mobile/routing/router.dart'; -import 'package:immich_mobile/presentation/widgets/images/remote_image_provider.dart'; -import 'package:immich_mobile/utils/image_url_builder.dart'; -import 'package:immich_mobile/widgets/album/album_thumbnail_card.dart'; -import 'package:immich_mobile/widgets/common/immich_app_bar.dart'; -import 'package:immich_mobile/widgets/common/user_avatar.dart'; -import 'package:immich_mobile/widgets/map/map_thumbnail.dart'; -import 'package:maplibre_gl/maplibre_gl.dart'; - -@RoutePage() -class LibraryPage extends ConsumerWidget { - const LibraryPage({super.key}); - @override - Widget build(BuildContext context, WidgetRef ref) { - context.locale; - final trashEnabled = ref.watch(serverInfoProvider.select((v) => v.serverFeatures.trash)); - - return Scaffold( - appBar: const ImmichAppBar(), - body: Padding( - padding: const EdgeInsets.symmetric(horizontal: 16), - child: ListView( - shrinkWrap: true, - children: [ - Padding( - padding: const EdgeInsets.only(top: 16.0), - child: Row( - children: [ - ActionButton( - onPressed: () => context.pushRoute(const FavoritesRoute()), - icon: Icons.favorite_outline_rounded, - label: context.t.favorites, - ), - const SizedBox(width: 8), - ActionButton( - onPressed: () => context.pushRoute(const ArchiveRoute()), - icon: Icons.archive_outlined, - label: context.t.archived, - ), - ], - ), - ), - const SizedBox(height: 8), - Row( - children: [ - ActionButton( - onPressed: () => context.pushRoute(const SharedLinkRoute()), - icon: Icons.link_outlined, - label: context.t.shared_links, - ), - SizedBox(width: trashEnabled ? 8 : 0), - trashEnabled - ? ActionButton( - onPressed: () => context.pushRoute(const TrashRoute()), - icon: Icons.delete_outline_rounded, - label: context.t.trash, - ) - : const SizedBox.shrink(), - ], - ), - const SizedBox(height: 12), - const Wrap( - spacing: 8, - runSpacing: 8, - children: [PeopleCollectionCard(), PlacesCollectionCard(), LocalAlbumsCollectionCard()], - ), - const SizedBox(height: 12), - const QuickAccessButtons(), - const SizedBox(height: 32), - ], - ), - ), - ); - } -} - -class QuickAccessButtons extends ConsumerWidget { - const QuickAccessButtons({super.key}); - @override - Widget build(BuildContext context, WidgetRef ref) { - final partners = ref.watch(partnerSharedWithProvider); - - return Container( - decoration: BoxDecoration( - border: Border.all(color: context.colorScheme.onSurface.withAlpha(10), width: 1), - borderRadius: const BorderRadius.all(Radius.circular(20)), - gradient: LinearGradient( - colors: [ - context.colorScheme.primary.withAlpha(10), - context.colorScheme.primary.withAlpha(15), - context.colorScheme.primary.withAlpha(20), - ], - begin: Alignment.topCenter, - end: Alignment.bottomCenter, - ), - ), - child: ListView( - shrinkWrap: true, - physics: const NeverScrollableScrollPhysics(), - children: [ - ListTile( - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.only( - topLeft: const Radius.circular(20), - topRight: const Radius.circular(20), - bottomLeft: Radius.circular(partners.isEmpty ? 20 : 0), - bottomRight: Radius.circular(partners.isEmpty ? 20 : 0), - ), - ), - leading: const Icon(Icons.folder_outlined, size: 26), - title: Text(context.t.folders, style: context.textTheme.titleSmall?.copyWith(fontWeight: FontWeight.w500)), - onTap: () => context.pushRoute(FolderRoute()), - ), - ListTile( - leading: const Icon(Icons.lock_outline_rounded, size: 26), - title: Text( - context.t.locked_folder, - style: context.textTheme.titleSmall?.copyWith(fontWeight: FontWeight.w500), - ), - onTap: () => context.pushRoute(const LockedRoute()), - ), - ListTile( - leading: const Icon(Icons.group_outlined, size: 26), - title: Text(context.t.partners, style: context.textTheme.titleSmall?.copyWith(fontWeight: FontWeight.w500)), - onTap: () => context.pushRoute(const PartnerRoute()), - ), - PartnerList(partners: partners), - ], - ), - ); - } -} - -class PartnerList extends ConsumerWidget { - const PartnerList({super.key, required this.partners}); - - final List partners; - - @override - Widget build(BuildContext context, WidgetRef ref) { - return ListView.builder( - physics: const NeverScrollableScrollPhysics(), - itemCount: partners.length, - shrinkWrap: true, - itemBuilder: (context, index) { - final partner = partners[index]; - final isLastItem = index == partners.length - 1; - return ListTile( - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.only( - bottomLeft: Radius.circular(isLastItem ? 20 : 0), - bottomRight: Radius.circular(isLastItem ? 20 : 0), - ), - ), - contentPadding: const EdgeInsets.only(left: 12.0, right: 18.0), - leading: userAvatar(context, partner, radius: 16), - title: const Text( - "partner_list_user_photos", - style: TextStyle(fontWeight: FontWeight.w500), - ).tr(namedArgs: {'user': partner.name}), - onTap: () => context.pushRoute((PartnerDetailRoute(partner: partner))), - ); - }, - ); - } -} - -class PeopleCollectionCard extends ConsumerWidget { - const PeopleCollectionCard({super.key}); - - @override - Widget build(BuildContext context, WidgetRef ref) { - final people = ref.watch(getAllPeopleProvider); - return LayoutBuilder( - builder: (context, constraints) { - final isTablet = constraints.maxWidth > 600; - final widthFactor = isTablet ? 0.25 : 0.5; - final size = context.width * widthFactor - 20.0; - - return GestureDetector( - onTap: () => context.pushRoute(const PeopleCollectionRoute()), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Container( - height: size, - width: size, - decoration: BoxDecoration( - borderRadius: const BorderRadius.all(Radius.circular(20)), - gradient: LinearGradient( - colors: [context.colorScheme.primary.withAlpha(30), context.colorScheme.primary.withAlpha(25)], - begin: Alignment.topCenter, - end: Alignment.bottomCenter, - ), - ), - child: people.widgetWhen( - onLoading: () => const Center(child: CircularProgressIndicator()), - onData: (people) { - return GridView.count( - crossAxisCount: 2, - padding: const EdgeInsets.all(12), - crossAxisSpacing: 8, - mainAxisSpacing: 8, - physics: const NeverScrollableScrollPhysics(), - children: people.take(4).map((person) { - return CircleAvatar(backgroundImage: RemoteImageProvider(url: getFaceThumbnailUrl(person.id))); - }).toList(), - ); - }, - ), - ), - Padding( - padding: const EdgeInsets.all(8.0), - child: Text( - context.t.people, - style: context.textTheme.titleSmall?.copyWith( - color: context.colorScheme.onSurface, - fontWeight: FontWeight.w500, - ), - ), - ), - ], - ), - ); - }, - ); - } -} - -class LocalAlbumsCollectionCard extends HookConsumerWidget { - const LocalAlbumsCollectionCard({super.key}); - - @override - Widget build(BuildContext context, WidgetRef ref) { - final albums = ref.watch(localAlbumsProvider); - - return LayoutBuilder( - builder: (context, constraints) { - final isTablet = constraints.maxWidth > 600; - final widthFactor = isTablet ? 0.25 : 0.5; - final size = context.width * widthFactor - 20.0; - - return GestureDetector( - onTap: () => context.pushRoute(const LocalAlbumsRoute()), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - SizedBox( - height: size, - width: size, - child: DecoratedBox( - decoration: BoxDecoration( - borderRadius: const BorderRadius.all(Radius.circular(20)), - gradient: LinearGradient( - colors: [context.colorScheme.primary.withAlpha(30), context.colorScheme.primary.withAlpha(25)], - begin: Alignment.topCenter, - end: Alignment.bottomCenter, - ), - ), - child: GridView.count( - crossAxisCount: 2, - padding: const EdgeInsets.all(12), - crossAxisSpacing: 8, - mainAxisSpacing: 8, - physics: const NeverScrollableScrollPhysics(), - children: albums.take(4).map((album) { - return AlbumThumbnailCard(album: album, showTitle: false); - }).toList(), - ), - ), - ), - Padding( - padding: const EdgeInsets.all(8.0), - child: Text( - context.t.on_this_device, - style: context.textTheme.titleSmall?.copyWith( - color: context.colorScheme.onSurface, - fontWeight: FontWeight.w500, - ), - ), - ), - ], - ), - ); - }, - ); - } -} - -class PlacesCollectionCard extends StatelessWidget { - const PlacesCollectionCard({super.key}); - @override - Widget build(BuildContext context) { - return LayoutBuilder( - builder: (context, constraints) { - final isTablet = constraints.maxWidth > 600; - final widthFactor = isTablet ? 0.25 : 0.5; - final size = context.width * widthFactor - 20.0; - - return GestureDetector( - onTap: () => context.pushRoute(PlacesCollectionRoute(currentLocation: null)), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - SizedBox( - height: size, - width: size, - child: DecoratedBox( - decoration: BoxDecoration( - borderRadius: const BorderRadius.all(Radius.circular(20)), - color: context.colorScheme.secondaryContainer.withAlpha(100), - ), - child: IgnorePointer( - child: MapThumbnail( - zoom: 8, - centre: const LatLng(21.44950, -157.91959), - showAttribution: false, - themeMode: context.isDarkTheme ? ThemeMode.dark : ThemeMode.light, - ), - ), - ), - ), - Padding( - padding: const EdgeInsets.all(8.0), - child: Text( - context.t.places, - style: context.textTheme.titleSmall?.copyWith( - color: context.colorScheme.onSurface, - fontWeight: FontWeight.w500, - ), - ), - ), - ], - ), - ); - }, - ); - } -} - -class ActionButton extends StatelessWidget { - final VoidCallback onPressed; - final IconData icon; - final String label; - - const ActionButton({super.key, required this.onPressed, required this.icon, required this.label}); - - @override - Widget build(BuildContext context) { - return Expanded( - child: FilledButton.icon( - onPressed: onPressed, - label: Padding( - padding: const EdgeInsets.only(left: 4.0), - child: Text(label, style: TextStyle(color: context.colorScheme.onSurface, fontSize: 15)), - ), - style: FilledButton.styleFrom( - elevation: 0, - padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 16), - backgroundColor: context.colorScheme.surfaceContainerLow, - alignment: Alignment.centerLeft, - shape: RoundedRectangleBorder( - borderRadius: const BorderRadius.all(Radius.circular(25)), - side: BorderSide(color: context.colorScheme.onSurface.withAlpha(10), width: 1), - ), - ), - icon: Icon(icon, color: context.primaryColor), - ), - ); - } -} diff --git a/mobile/lib/pages/library/local_albums.page.dart b/mobile/lib/pages/library/local_albums.page.dart deleted file mode 100644 index e52a8326df..0000000000 --- a/mobile/lib/pages/library/local_albums.page.dart +++ /dev/null @@ -1,49 +0,0 @@ -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/extensions/build_context_extensions.dart'; -import 'package:immich_mobile/extensions/theme_extensions.dart'; -import 'package:immich_mobile/extensions/translate_extensions.dart'; -import 'package:immich_mobile/pages/common/large_leading_tile.dart'; -import 'package:immich_mobile/providers/album/album.provider.dart'; -import 'package:immich_mobile/routing/router.dart'; -import 'package:immich_mobile/widgets/common/immich_thumbnail.dart'; - -@RoutePage() -class LocalAlbumsPage extends HookConsumerWidget { - const LocalAlbumsPage({super.key}); - @override - Widget build(BuildContext context, WidgetRef ref) { - final albums = ref.watch(localAlbumsProvider); - - return Scaffold( - appBar: AppBar(title: Text('on_this_device'.tr())), - body: ListView.builder( - padding: const EdgeInsets.all(18.0), - itemCount: albums.length, - itemBuilder: (context, index) { - return Padding( - padding: const EdgeInsets.only(bottom: 8.0), - child: LargeLeadingTile( - leadingPadding: const EdgeInsets.only(right: 16), - leading: ClipRRect( - borderRadius: const BorderRadius.all(Radius.circular(15)), - child: ImmichThumbnail(asset: albums[index].thumbnail.value, width: 80, height: 80), - ), - title: Text( - albums[index].name, - style: context.textTheme.titleSmall?.copyWith(fontWeight: FontWeight.w600), - ), - subtitle: Text( - 'items_count'.t(context: context, args: {'count': albums[index].assetCount}), - style: context.textTheme.bodyMedium?.copyWith(color: context.colorScheme.onSurfaceSecondary), - ), - onTap: () => context.pushRoute(AlbumViewerRoute(albumId: albums[index].id)), - ), - ); - }, - ), - ); - } -} diff --git a/mobile/lib/pages/library/locked/locked.page.dart b/mobile/lib/pages/library/locked/locked.page.dart deleted file mode 100644 index aea62e0051..0000000000 --- a/mobile/lib/pages/library/locked/locked.page.dart +++ /dev/null @@ -1,82 +0,0 @@ -import 'package:auto_route/auto_route.dart'; -import 'package:easy_localization/easy_localization.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_hooks/flutter_hooks.dart'; -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/extensions/build_context_extensions.dart'; -import 'package:immich_mobile/providers/auth.provider.dart'; -import 'package:immich_mobile/providers/multiselect.provider.dart'; -import 'package:immich_mobile/providers/timeline.provider.dart'; -import 'package:immich_mobile/widgets/asset_grid/multiselect_grid.dart'; - -@RoutePage() -class LockedPage extends HookConsumerWidget { - const LockedPage({super.key}); - - @override - Widget build(BuildContext context, WidgetRef ref) { - final appLifeCycle = useAppLifecycleState(); - final showOverlay = useState(false); - final authProviderNotifier = ref.read(authProvider.notifier); - // lock the page when it is destroyed - useEffect(() { - return () { - authProviderNotifier.lockPinCode(); - }; - }, []); - - useEffect(() { - if (context.mounted) { - if (appLifeCycle == AppLifecycleState.resumed) { - showOverlay.value = false; - } else { - showOverlay.value = true; - } - } - - return null; - }, [appLifeCycle]); - - return Scaffold( - appBar: ref.watch(multiselectProvider) ? null : const LockPageAppBar(), - body: showOverlay.value - ? const SizedBox() - : MultiselectGrid( - renderListProvider: lockedTimelineProvider, - topWidget: Padding( - padding: const EdgeInsets.all(16.0), - child: Center(child: Text('no_locked_photos_message'.tr(), style: context.textTheme.labelLarge)), - ), - editEnabled: false, - favoriteEnabled: false, - unfavorite: false, - archiveEnabled: false, - stackEnabled: false, - unarchive: false, - ), - ); - } -} - -class LockPageAppBar extends ConsumerWidget implements PreferredSizeWidget { - const LockPageAppBar({super.key}); - - @override - Widget build(BuildContext context, WidgetRef ref) { - return AppBar( - leading: IconButton( - onPressed: () { - ref.read(authProvider.notifier).lockPinCode(); - context.maybePop(); - }, - icon: const Icon(Icons.arrow_back_ios_rounded), - ), - centerTitle: true, - automaticallyImplyLeading: false, - title: const Text('locked_folder').tr(), - ); - } - - @override - Size get preferredSize => const Size.fromHeight(kToolbarHeight); -} diff --git a/mobile/lib/pages/library/locked/pin_auth.page.dart b/mobile/lib/pages/library/locked/pin_auth.page.dart index a39c91871b..3af320dc5f 100644 --- a/mobile/lib/pages/library/locked/pin_auth.page.dart +++ b/mobile/lib/pages/library/locked/pin_auth.page.dart @@ -5,7 +5,6 @@ import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:flutter_hooks/flutter_hooks.dart' show useState; import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/entities/store.entity.dart'; import 'package:immich_mobile/extensions/build_context_extensions.dart'; import 'package:immich_mobile/providers/local_auth.provider.dart'; import 'package:immich_mobile/routing/router.dart'; @@ -22,7 +21,6 @@ class PinAuthPage extends HookConsumerWidget { Widget build(BuildContext context, WidgetRef ref) { final localAuthState = ref.watch(localAuthProvider); final showPinRegistrationForm = useState(createPinCode); - final isBetaTimeline = Store.isBetaTimelineEnabled; Future registerBiometric(String pinCode) async { final isRegistered = await ref.read(localAuthProvider.notifier).registerBiometric(context, pinCode); @@ -36,11 +34,7 @@ class PinAuthPage extends HookConsumerWidget { ), ); - if (isBetaTimeline) { - unawaited(context.replaceRoute(const DriftLockedFolderRoute())); - } else { - unawaited(context.replaceRoute(const LockedRoute())); - } + unawaited(context.replaceRoute(const DriftLockedFolderRoute())); } } @@ -89,11 +83,7 @@ class PinAuthPage extends HookConsumerWidget { child: PinVerificationForm( autoFocus: true, onSuccess: (_) { - if (isBetaTimeline) { - context.replaceRoute(const DriftLockedFolderRoute()); - } else { - context.replaceRoute(const LockedRoute()); - } + context.replaceRoute(const DriftLockedFolderRoute()); }, ), ), diff --git a/mobile/lib/pages/library/partner/partner.page.dart b/mobile/lib/pages/library/partner/partner.page.dart deleted file mode 100644 index eae4228a2d..0000000000 --- a/mobile/lib/pages/library/partner/partner.page.dart +++ /dev/null @@ -1,139 +0,0 @@ -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/domain/models/user.model.dart'; -import 'package:immich_mobile/extensions/build_context_extensions.dart'; -import 'package:immich_mobile/providers/partner.provider.dart'; -import 'package:immich_mobile/services/partner.service.dart'; -import 'package:immich_mobile/widgets/common/confirm_dialog.dart'; -import 'package:immich_mobile/widgets/common/immich_toast.dart'; -import 'package:immich_mobile/widgets/common/user_avatar.dart'; - -@RoutePage() -class PartnerPage extends HookConsumerWidget { - const PartnerPage({super.key}); - - @override - Widget build(BuildContext context, WidgetRef ref) { - final List partners = ref.watch(partnerSharedByProvider); - final availableUsers = ref.watch(partnerAvailableProvider); - - addNewUsersHandler() async { - final users = availableUsers.value; - if (users == null || users.isEmpty) { - ImmichToast.show(context: context, msg: "partner_page_no_more_users".tr()); - return; - } - - final selectedUser = await showDialog( - context: context, - builder: (context) { - return SimpleDialog( - title: const Text("partner_page_select_partner").tr(), - children: [ - for (UserDto u in users) - SimpleDialogOption( - onPressed: () => context.pop(u), - child: Row( - children: [ - Padding(padding: const EdgeInsets.only(right: 8), child: userAvatar(context, u)), - Text(u.name), - ], - ), - ), - ], - ); - }, - ); - if (selectedUser != null) { - final ok = await ref.read(partnerServiceProvider).addPartner(selectedUser); - if (ok) { - ref.invalidate(partnerSharedByProvider); - } else { - ImmichToast.show(context: context, msg: "partner_page_partner_add_failed".tr(), toastType: ToastType.error); - } - } - } - - onDeleteUser(UserDto u) { - return showDialog( - context: context, - builder: (BuildContext context) { - return ConfirmDialog( - title: "stop_photo_sharing", - content: "partner_page_stop_sharing_content".tr(namedArgs: {'partner': u.name}), - onOk: () => ref.read(partnerServiceProvider).removePartner(u), - ); - }, - ); - } - - buildUserList(List users) { - return Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Padding( - padding: const EdgeInsets.only(left: 16.0, top: 16.0), - child: Text( - "partner_page_shared_to_title", - style: context.textTheme.titleSmall?.copyWith(color: context.colorScheme.onSurface.withAlpha(200)), - ).tr(), - ), - if (users.isNotEmpty) - ListView.builder( - shrinkWrap: true, - itemCount: users.length, - itemBuilder: ((context, index) { - return ListTile( - leading: userAvatar(context, users[index]), - title: Text(users[index].email, style: context.textTheme.bodyLarge), - trailing: IconButton( - icon: const Icon(Icons.person_remove), - onPressed: () => onDeleteUser(users[index]), - ), - ); - }), - ), - if (users.isEmpty) - Padding( - padding: const EdgeInsets.symmetric(horizontal: 16.0), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Padding( - padding: const EdgeInsets.symmetric(vertical: 8), - child: const Text("partner_page_empty_message", style: TextStyle(fontSize: 14)).tr(), - ), - Align( - alignment: Alignment.center, - child: ElevatedButton.icon( - onPressed: availableUsers.whenOrNull(data: (data) => addNewUsersHandler), - icon: const Icon(Icons.person_add), - label: const Text("add_partner").tr(), - ), - ), - ], - ), - ), - ], - ); - } - - return Scaffold( - appBar: AppBar( - title: const Text("partners").tr(), - elevation: 0, - centerTitle: false, - actions: [ - IconButton( - onPressed: availableUsers.whenOrNull(data: (data) => addNewUsersHandler), - icon: const Icon(Icons.person_add), - tooltip: "add_partner".tr(), - ), - ], - ), - body: buildUserList(partners), - ); - } -} diff --git a/mobile/lib/pages/library/partner/partner_detail.page.dart b/mobile/lib/pages/library/partner/partner_detail.page.dart deleted file mode 100644 index 1f15dab6a3..0000000000 --- a/mobile/lib/pages/library/partner/partner_detail.page.dart +++ /dev/null @@ -1,99 +0,0 @@ -import 'package:auto_route/auto_route.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_hooks/flutter_hooks.dart'; -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/domain/models/user.model.dart'; -import 'package:immich_mobile/extensions/build_context_extensions.dart'; -import 'package:immich_mobile/providers/asset.provider.dart'; -import 'package:immich_mobile/providers/multiselect.provider.dart'; -import 'package:immich_mobile/providers/partner.provider.dart'; -import 'package:immich_mobile/providers/timeline.provider.dart'; -import 'package:immich_mobile/widgets/asset_grid/multiselect_grid.dart'; -import 'package:immich_mobile/widgets/common/immich_toast.dart'; - -@RoutePage() -class PartnerDetailPage extends HookConsumerWidget { - const PartnerDetailPage({super.key, required this.partner}); - - final UserDto partner; - - @override - Widget build(BuildContext context, WidgetRef ref) { - final inTimeline = useState(partner.inTimeline); - bool toggleInProcess = false; - - useEffect(() { - Future.microtask(() async => {await ref.read(assetProvider.notifier).getAllAsset()}); - return null; - }, []); - - void toggleInTimeline() async { - if (toggleInProcess) return; - toggleInProcess = true; - try { - final ok = await ref - .read(partnerSharedWithProvider.notifier) - .updatePartner(partner, inTimeline: !inTimeline.value); - if (ok) { - inTimeline.value = !inTimeline.value; - final action = inTimeline.value ? "shown on" : "hidden from"; - ImmichToast.show( - context: context, - toastType: ToastType.success, - durationInSecond: 1, - msg: "${partner.name}'s assets $action your timeline", - ); - } else { - ImmichToast.show( - context: context, - toastType: ToastType.error, - durationInSecond: 1, - msg: "Failed to toggle the timeline setting", - ); - } - } finally { - toggleInProcess = false; - } - } - - return Scaffold( - appBar: ref.watch(multiselectProvider) - ? null - : AppBar(title: Text(partner.name), elevation: 0, centerTitle: false), - body: MultiselectGrid( - topWidget: Padding( - padding: const EdgeInsets.only(left: 8.0, right: 8.0, top: 16.0), - child: Container( - decoration: BoxDecoration( - border: Border.all(color: context.colorScheme.onSurface.withAlpha(10), width: 1), - borderRadius: const BorderRadius.all(Radius.circular(20)), - gradient: LinearGradient( - colors: [context.colorScheme.primary.withAlpha(10), context.colorScheme.primary.withAlpha(15)], - begin: Alignment.topCenter, - end: Alignment.bottomCenter, - ), - ), - child: Padding( - padding: const EdgeInsets.all(8.0), - child: ListTile( - title: Text( - "Show in timeline", - style: context.textTheme.titleSmall?.copyWith(color: context.colorScheme.primary), - ), - subtitle: Text( - "Show photos and videos from this user in your timeline", - style: context.textTheme.bodyMedium, - ), - trailing: Switch(value: inTimeline.value, onChanged: (_) => toggleInTimeline()), - ), - ), - ), - ), - renderListProvider: singleUserTimelineProvider(partner.id), - onRefresh: () => ref.read(assetProvider.notifier).getAllAsset(), - deleteEnabled: false, - favoriteEnabled: false, - ), - ); - } -} diff --git a/mobile/lib/pages/library/people/people_collection.page.dart b/mobile/lib/pages/library/people/people_collection.page.dart deleted file mode 100644 index bff52df6da..0000000000 --- a/mobile/lib/pages/library/people/people_collection.page.dart +++ /dev/null @@ -1,127 +0,0 @@ -import 'package:auto_route/auto_route.dart'; -import 'package:easy_localization/easy_localization.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_hooks/flutter_hooks.dart'; -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/extensions/build_context_extensions.dart'; -import 'package:immich_mobile/providers/search/people.provider.dart'; -import 'package:immich_mobile/presentation/widgets/images/remote_image_provider.dart'; -import 'package:immich_mobile/routing/router.dart'; -import 'package:immich_mobile/utils/image_url_builder.dart'; -import 'package:immich_mobile/widgets/common/search_field.dart'; -import 'package:immich_mobile/widgets/search/person_name_edit_form.dart'; - -@RoutePage() -class PeopleCollectionPage extends HookConsumerWidget { - const PeopleCollectionPage({super.key}); - @override - Widget build(BuildContext context, WidgetRef ref) { - final people = ref.watch(getAllPeopleProvider); - final formFocus = useFocusNode(); - final ValueNotifier search = useState(null); - - showNameEditModel(String personId, String personName) { - return showDialog( - context: context, - useRootNavigator: false, - builder: (BuildContext context) { - return PersonNameEditForm(personId: personId, personName: personName); - }, - ); - } - - return LayoutBuilder( - builder: (context, constraints) { - final isTablet = constraints.maxWidth > 600; - final isPortrait = context.orientation == Orientation.portrait; - - return Scaffold( - appBar: AppBar( - automaticallyImplyLeading: search.value == null, - title: search.value != null - ? SearchField( - focusNode: formFocus, - onTapOutside: (_) => formFocus.unfocus(), - onChanged: (value) => search.value = value, - filled: true, - hintText: 'filter_people'.tr(), - autofocus: true, - ) - : Text('people'.tr()), - actions: [ - IconButton( - icon: Icon(search.value != null ? Icons.close : Icons.search), - onPressed: () { - search.value = search.value == null ? '' : null; - }, - ), - ], - ), - body: SafeArea( - child: people.when( - data: (people) { - if (search.value != null) { - people = people.where((person) { - return person.name.toLowerCase().contains(search.value!.toLowerCase()); - }).toList(); - } - return GridView.builder( - gridDelegate: SliverGridDelegateWithFixedCrossAxisCount( - crossAxisCount: isTablet ? 6 : 3, - childAspectRatio: 0.85, - mainAxisSpacing: isPortrait && isTablet ? 36 : 0, - ), - padding: const EdgeInsets.symmetric(vertical: 32), - itemCount: people.length, - itemBuilder: (context, index) { - final person = people[index]; - - return Column( - children: [ - GestureDetector( - onTap: () { - context.pushRoute(PersonResultRoute(personId: person.id, personName: person.name)); - }, - child: Material( - shape: const CircleBorder(side: BorderSide.none), - elevation: 3, - child: CircleAvatar( - maxRadius: isTablet ? 120 / 2 : 96 / 2, - backgroundImage: RemoteImageProvider(url: getFaceThumbnailUrl(person.id)), - ), - ), - ), - const SizedBox(height: 12), - GestureDetector( - onTap: () => showNameEditModel(person.id, person.name), - child: person.name.isEmpty - ? Text( - 'add_a_name'.tr(), - style: context.textTheme.titleSmall?.copyWith( - fontWeight: FontWeight.w500, - color: context.colorScheme.primary, - ), - ) - : Padding( - padding: const EdgeInsets.symmetric(horizontal: 16.0), - child: Text( - person.name, - overflow: TextOverflow.ellipsis, - style: context.textTheme.titleSmall?.copyWith(fontWeight: FontWeight.w500), - ), - ), - ), - ], - ); - }, - ); - }, - error: (error, stack) => const Text("error"), - loading: () => const Center(child: CircularProgressIndicator()), - ), - ), - ); - }, - ); - } -} diff --git a/mobile/lib/pages/library/places/places_collection.page.dart b/mobile/lib/pages/library/places/places_collection.page.dart deleted file mode 100644 index a4a6f66915..0000000000 --- a/mobile/lib/pages/library/places/places_collection.page.dart +++ /dev/null @@ -1,136 +0,0 @@ -import 'package:auto_route/auto_route.dart'; -import 'package:easy_localization/easy_localization.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_hooks/flutter_hooks.dart' hide Store; -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/domain/models/store.model.dart'; -import 'package:immich_mobile/entities/asset.entity.dart'; -import 'package:immich_mobile/entities/store.entity.dart'; -import 'package:immich_mobile/extensions/build_context_extensions.dart'; -import 'package:immich_mobile/models/search/search_filter.model.dart'; -import 'package:immich_mobile/pages/common/large_leading_tile.dart'; -import 'package:immich_mobile/presentation/widgets/images/remote_image_provider.dart'; -import 'package:immich_mobile/presentation/widgets/images/thumbnail.widget.dart'; -import 'package:immich_mobile/providers/search/search_page_state.provider.dart'; -import 'package:immich_mobile/routing/router.dart'; -import 'package:immich_mobile/widgets/common/search_field.dart'; -import 'package:immich_mobile/widgets/map/map_thumbnail.dart'; -import 'package:maplibre_gl/maplibre_gl.dart'; - -@RoutePage() -class PlacesCollectionPage extends HookConsumerWidget { - const PlacesCollectionPage({super.key, this.currentLocation}); - final LatLng? currentLocation; - @override - Widget build(BuildContext context, WidgetRef ref) { - final places = ref.watch(getAllPlacesProvider); - final formFocus = useFocusNode(); - final ValueNotifier search = useState(null); - - return Scaffold( - appBar: AppBar( - automaticallyImplyLeading: search.value == null, - title: search.value != null - ? SearchField( - autofocus: true, - filled: true, - focusNode: formFocus, - onChanged: (value) => search.value = value, - onTapOutside: (_) => formFocus.unfocus(), - hintText: 'filter_places'.tr(), - ) - : Text('places'.tr()), - actions: [ - IconButton( - icon: Icon(search.value != null ? Icons.close : Icons.search), - onPressed: () { - search.value = search.value == null ? '' : null; - }, - ), - ], - ), - body: ListView( - shrinkWrap: true, - children: [ - if (search.value == null) - Padding( - padding: const EdgeInsets.all(16.0), - child: SizedBox( - height: 200, - width: context.width, - child: MapThumbnail( - onTap: (_, __) => context.pushRoute(MapRoute(initialLocation: currentLocation)), - zoom: 8, - centre: currentLocation ?? const LatLng(21.44950, -157.91959), - showAttribution: false, - themeMode: context.isDarkTheme ? ThemeMode.dark : ThemeMode.light, - ), - ), - ), - places.when( - data: (places) { - if (search.value != null) { - places = places.where((place) { - return place.label.toLowerCase().contains(search.value!.toLowerCase()); - }).toList(); - } - return ListView.builder( - shrinkWrap: true, - physics: const NeverScrollableScrollPhysics(), - itemCount: places.length, - itemBuilder: (context, index) { - final place = places[index]; - - return PlaceTile(id: place.id, name: place.label); - }, - ); - }, - error: (error, stask) => Text('error_getting_places'.tr()), - loading: () => const Center(child: CircularProgressIndicator()), - ), - ], - ), - ); - } -} - -class PlaceTile extends StatelessWidget { - const PlaceTile({super.key, required this.id, required this.name}); - - final String id; - final String name; - - @override - Widget build(BuildContext context) { - final thumbnailUrl = '${Store.get(StoreKey.serverEndpoint)}/assets/$id/thumbnail'; - - void navigateToPlace() { - context.pushRoute( - SearchRoute( - prefilter: SearchFilter( - people: {}, - location: SearchLocationFilter(city: name), - camera: SearchCameraFilter(), - date: SearchDateFilter(), - display: SearchDisplayFilters(isNotInAlbum: false, isArchive: false, isFavorite: false), - rating: SearchRatingFilter(), - mediaType: AssetType.other, - ), - ), - ); - } - - return LargeLeadingTile( - onTap: () => navigateToPlace(), - title: Text(name, style: context.textTheme.titleMedium?.copyWith(fontWeight: FontWeight.w500)), - leading: ClipRRect( - borderRadius: const BorderRadius.all(Radius.circular(20)), - child: SizedBox( - width: 80, - height: 80, - child: Thumbnail(imageProvider: RemoteImageProvider(url: thumbnailUrl)), - ), - ), - ); - } -} diff --git a/mobile/lib/pages/library/trash.page.dart b/mobile/lib/pages/library/trash.page.dart deleted file mode 100644 index 2279998c2d..0000000000 --- a/mobile/lib/pages/library/trash.page.dart +++ /dev/null @@ -1,225 +0,0 @@ -import 'package:auto_route/auto_route.dart'; -import 'package:easy_localization/easy_localization.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_hooks/flutter_hooks.dart'; -import 'package:fluttertoast/fluttertoast.dart'; -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/entities/asset.entity.dart'; -import 'package:immich_mobile/extensions/asyncvalue_extensions.dart'; -import 'package:immich_mobile/extensions/build_context_extensions.dart'; -import 'package:immich_mobile/providers/asset.provider.dart'; -import 'package:immich_mobile/providers/server_info.provider.dart'; -import 'package:immich_mobile/providers/timeline.provider.dart'; -import 'package:immich_mobile/providers/trash.provider.dart'; -import 'package:immich_mobile/utils/immich_loading_overlay.dart'; -import 'package:immich_mobile/widgets/asset_grid/delete_dialog.dart'; -import 'package:immich_mobile/widgets/asset_grid/immich_asset_grid.dart'; -import 'package:immich_mobile/widgets/common/confirm_dialog.dart'; -import 'package:immich_mobile/widgets/common/immich_toast.dart'; - -@RoutePage() -class TrashPage extends HookConsumerWidget { - const TrashPage({super.key}); - - @override - Widget build(BuildContext context, WidgetRef ref) { - final trashRenderList = ref.watch(trashTimelineProvider); - final trashDays = ref.watch(serverInfoProvider.select((v) => v.serverConfig.trashDays)); - final selectionEnabledHook = useState(false); - final selection = useState({}); - final processing = useProcessingOverlay(); - - void selectionListener(bool multiselect, Set selectedAssets) { - selectionEnabledHook.value = multiselect; - selection.value = selectedAssets; - } - - onEmptyTrash() async { - processing.value = true; - await ref.read(trashProvider.notifier).emptyTrash(); - processing.value = false; - selectionEnabledHook.value = false; - if (context.mounted) { - ImmichToast.show(context: context, msg: 'trash_emptied'.tr(), gravity: ToastGravity.BOTTOM); - } - } - - handleEmptyTrash() async { - await showDialog( - context: context, - builder: (context) => ConfirmDialog( - onOk: () => onEmptyTrash(), - title: "empty_trash".tr(), - ok: "ok".tr(), - content: "trash_page_empty_trash_dialog_content".tr(), - ), - ); - } - - Future onPermanentlyDelete() async { - processing.value = true; - try { - if (selection.value.isNotEmpty) { - final isRemoved = await ref.read(assetProvider.notifier).deleteAssets(selection.value, force: true); - - if (isRemoved) { - if (context.mounted) { - ImmichToast.show( - context: context, - msg: 'assets_deleted_permanently'.tr(namedArgs: {'count': "${selection.value.length}"}), - gravity: ToastGravity.BOTTOM, - ); - } - } - } - } finally { - processing.value = false; - selectionEnabledHook.value = false; - } - } - - handlePermanentDelete() async { - await showDialog( - context: context, - builder: (context) => DeleteDialog(alert: "delete_dialog_alert_remote", onDelete: () => onPermanentlyDelete()), - ); - } - - Future handleRestoreAll() async { - processing.value = true; - await ref.read(trashProvider.notifier).restoreTrash(); - processing.value = false; - selectionEnabledHook.value = false; - } - - Future handleRestore() async { - processing.value = true; - try { - if (selection.value.isNotEmpty) { - final result = await ref.read(trashProvider.notifier).restoreAssets(selection.value); - - if (result && context.mounted) { - ImmichToast.show( - context: context, - msg: 'assets_restored_successfully'.tr(namedArgs: {'count': "${selection.value.length}"}), - gravity: ToastGravity.BOTTOM, - ); - } - } - } finally { - processing.value = false; - selectionEnabledHook.value = false; - } - } - - String getAppBarTitle(String count) { - if (selectionEnabledHook.value) { - return selection.value.isNotEmpty ? "${selection.value.length}" : "trash_page_select_assets_btn".tr(); - } - return 'trash_page_title'.tr(namedArgs: {'count': count}); - } - - AppBar buildAppBar(String count) { - return AppBar( - leading: IconButton( - onPressed: !selectionEnabledHook.value - ? () => context.maybePop() - : () { - selectionEnabledHook.value = false; - selection.value = {}; - }, - icon: !selectionEnabledHook.value - ? const Icon(Icons.arrow_back_ios_rounded) - : const Icon(Icons.close_rounded), - ), - centerTitle: !selectionEnabledHook.value, - automaticallyImplyLeading: false, - title: Text(getAppBarTitle(count)), - actions: [ - if (!selectionEnabledHook.value) - PopupMenuButton( - itemBuilder: (context) { - return [ - PopupMenuItem(value: () => selectionEnabledHook.value = true, child: const Text('select').tr()), - PopupMenuItem(value: handleEmptyTrash, child: const Text('empty_trash').tr()), - ]; - }, - onSelected: (fn) => fn(), - ), - ], - ); - } - - Widget buildBottomBar() { - return SafeArea( - child: Align( - alignment: Alignment.bottomCenter, - child: SizedBox( - height: 64, - child: Container( - color: context.themeData.canvasColor, - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceEvenly, - children: [ - TextButton.icon( - icon: Icon(Icons.delete_forever, color: Colors.red[400]), - label: Text( - selection.value.isEmpty ? 'trash_page_delete_all'.tr() : 'delete'.tr(), - style: TextStyle(fontSize: 14, color: Colors.red[400], fontWeight: FontWeight.bold), - ), - onPressed: processing.value - ? null - : selection.value.isEmpty - ? handleEmptyTrash - : handlePermanentDelete, - ), - TextButton.icon( - icon: const Icon(Icons.history_rounded), - label: Text( - selection.value.isEmpty ? 'trash_page_restore_all'.tr() : 'restore'.tr(), - style: const TextStyle(fontSize: 14, fontWeight: FontWeight.bold), - ), - onPressed: processing.value - ? null - : selection.value.isEmpty - ? handleRestoreAll - : handleRestore, - ), - ], - ), - ), - ), - ), - ); - } - - return Scaffold( - appBar: trashRenderList.maybeWhen( - orElse: () => buildAppBar("?"), - data: (data) => buildAppBar(data.totalAssets.toString()), - ), - body: trashRenderList.widgetWhen( - onData: (data) => data.isEmpty - ? Center(child: Text('trash_page_no_assets'.tr())) - : Stack( - children: [ - SafeArea( - child: ImmichAssetGrid( - renderList: data, - listener: selectionListener, - selectionActive: selectionEnabledHook.value, - showMultiSelectIndicator: false, - showStack: true, - topWidget: Padding( - padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 24), - child: const Text("trash_page_info").tr(namedArgs: {"days": "$trashDays"}), - ), - ), - ), - if (selectionEnabledHook.value) buildBottomBar(), - ], - ), - ), - ); - } -} diff --git a/mobile/lib/pages/onboarding/permission_onboarding.page.dart b/mobile/lib/pages/onboarding/permission_onboarding.page.dart deleted file mode 100644 index 52d4ac0125..0000000000 --- a/mobile/lib/pages/onboarding/permission_onboarding.page.dart +++ /dev/null @@ -1,141 +0,0 @@ -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/extensions/build_context_extensions.dart'; -import 'package:immich_mobile/providers/gallery_permission.provider.dart'; -import 'package:immich_mobile/routing/router.dart'; -import 'package:immich_mobile/widgets/common/immich_logo.dart'; -import 'package:immich_mobile/widgets/common/immich_title_text.dart'; -import 'package:permission_handler/permission_handler.dart'; - -@RoutePage() -class PermissionOnboardingPage extends HookConsumerWidget { - const PermissionOnboardingPage({super.key}); - - @override - Widget build(BuildContext context, WidgetRef ref) { - final PermissionStatus permission = ref.watch(galleryPermissionNotifier); - - // Navigate to the main Tab Controller when permission is granted - void goToBackup() => context.replaceRoute(const BackupControllerRoute()); - - // When the permission is denied, we show a request permission page - buildRequestPermission() { - return Column( - crossAxisAlignment: CrossAxisAlignment.center, - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Text('permission_onboarding_request', style: context.textTheme.titleMedium, textAlign: TextAlign.center).tr(), - const SizedBox(height: 18), - ElevatedButton( - onPressed: () => - ref.read(galleryPermissionNotifier.notifier).requestGalleryPermission().then((permission) async { - if (permission.isGranted) { - // If permission is limited, we will show the limited - // permission page - goToBackup(); - } - }), - child: const Text('continue').tr(), - ), - ], - ); - } - - // When permission is granted from outside the app, this will show to - // let them continue on to the main timeline - buildPermissionGranted() { - return Column( - crossAxisAlignment: CrossAxisAlignment.center, - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Text( - 'permission_onboarding_permission_granted', - style: context.textTheme.titleMedium, - textAlign: TextAlign.center, - ).tr(), - const SizedBox(height: 18), - ElevatedButton(onPressed: () => goToBackup(), child: const Text('permission_onboarding_get_started').tr()), - ], - ); - } - - // iOS 14+ has limited permission options, which let someone just share - // a few photos with the app. If someone only has limited permissions, we - // inform that Immich works best when given full permission - buildPermissionLimited() { - return Column( - crossAxisAlignment: CrossAxisAlignment.center, - mainAxisAlignment: MainAxisAlignment.center, - children: [ - const Icon(Icons.warning_outlined, color: Colors.yellow, size: 48), - const SizedBox(height: 8), - Text( - 'permission_onboarding_permission_limited', - style: context.textTheme.titleMedium, - textAlign: TextAlign.center, - ).tr(), - const SizedBox(height: 18), - ElevatedButton( - onPressed: () => openAppSettings(), - child: const Text('permission_onboarding_go_to_settings').tr(), - ), - const SizedBox(height: 8.0), - TextButton(onPressed: () => goToBackup(), child: const Text('permission_onboarding_continue_anyway').tr()), - ], - ); - } - - buildPermissionDenied() { - return Column( - crossAxisAlignment: CrossAxisAlignment.center, - mainAxisAlignment: MainAxisAlignment.center, - children: [ - const Icon(Icons.warning_outlined, color: Colors.red, size: 48), - const SizedBox(height: 8), - Text( - 'permission_onboarding_permission_denied', - style: context.textTheme.titleMedium, - textAlign: TextAlign.center, - ).tr(), - const SizedBox(height: 18), - ElevatedButton( - onPressed: () => openAppSettings(), - child: const Text('permission_onboarding_go_to_settings').tr(), - ), - ], - ); - } - - final Widget child = switch (permission) { - PermissionStatus.limited => buildPermissionLimited(), - PermissionStatus.denied => buildRequestPermission(), - PermissionStatus.granted || PermissionStatus.provisional => buildPermissionGranted(), - PermissionStatus.restricted || PermissionStatus.permanentlyDenied => buildPermissionDenied(), - }; - - return Scaffold( - body: SafeArea( - child: Center( - child: SizedBox( - width: 380, - child: Column( - crossAxisAlignment: CrossAxisAlignment.center, - mainAxisAlignment: MainAxisAlignment.center, - children: [ - const ImmichLogo(heroTag: 'logo'), - const ImmichTitleText(), - AnimatedSwitcher( - duration: const Duration(milliseconds: 500), - child: Padding(padding: const EdgeInsets.all(18.0), child: child), - ), - TextButton(child: const Text('back').tr(), onPressed: () => context.maybePop()), - ], - ), - ), - ), - ), - ); - } -} diff --git a/mobile/lib/pages/photos/memory.page.dart b/mobile/lib/pages/photos/memory.page.dart deleted file mode 100644 index bd7973bc21..0000000000 --- a/mobile/lib/pages/photos/memory.page.dart +++ /dev/null @@ -1,324 +0,0 @@ -import 'package:auto_route/auto_route.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter/scheduler.dart'; -import 'package:flutter/services.dart'; -import 'package:flutter_hooks/flutter_hooks.dart'; -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/entities/asset.entity.dart'; -import 'package:immich_mobile/models/memories/memory.model.dart'; -import 'package:immich_mobile/providers/asset_viewer/current_asset.provider.dart'; -import 'package:immich_mobile/providers/haptic_feedback.provider.dart'; -import 'package:immich_mobile/widgets/common/immich_image.dart'; -import 'package:immich_mobile/widgets/memories/memory_bottom_info.dart'; -import 'package:immich_mobile/widgets/memories/memory_card.dart'; -import 'package:immich_mobile/widgets/memories/memory_epilogue.dart'; -import 'package:immich_mobile/widgets/memories/memory_progress_indicator.dart'; - -@RoutePage() -/// Expects [currentAssetProvider] to be set before navigating to this page -class MemoryPage extends HookConsumerWidget { - final List memories; - final int memoryIndex; - - const MemoryPage({required this.memories, required this.memoryIndex, super.key}); - - @override - Widget build(BuildContext context, WidgetRef ref) { - final currentMemory = useState(memories[memoryIndex]); - final currentAssetPage = useState(0); - final currentMemoryIndex = useState(memoryIndex); - final assetProgress = useState("${currentAssetPage.value + 1}|${currentMemory.value.assets.length}"); - const bgColor = Colors.black; - final currentAsset = useState(null); - - /// The list of all of the asset page controllers - final memoryAssetPageControllers = List.generate(memories.length, (i) => usePageController()); - - /// The main vertically scrolling page controller with each list of memories - final memoryPageController = usePageController(initialPage: memoryIndex); - - useEffect(() { - // Memories is an immersive activity - SystemChrome.setEnabledSystemUIMode(SystemUiMode.immersive); - return () { - // Clean up to normal edge to edge when we are done - SystemChrome.setEnabledSystemUIMode(SystemUiMode.edgeToEdge); - }; - }); - - toNextMemory() { - memoryPageController.nextPage(duration: const Duration(milliseconds: 500), curve: Curves.easeIn); - } - - void toPreviousMemory() { - if (currentMemoryIndex.value > 0) { - // Move to the previous memory page - memoryPageController.previousPage(duration: const Duration(milliseconds: 500), curve: Curves.easeIn); - - // Wait for the next frame to ensure the page is built - SchedulerBinding.instance.addPostFrameCallback((_) { - final previousIndex = currentMemoryIndex.value - 1; - final previousMemoryController = memoryAssetPageControllers[previousIndex]; - - // Ensure the controller is attached - if (previousMemoryController.hasClients) { - previousMemoryController.jumpToPage(memories[previousIndex].assets.length - 1); - } else { - // Wait for the next frame until it is attached - SchedulerBinding.instance.addPostFrameCallback((_) { - if (previousMemoryController.hasClients) { - previousMemoryController.jumpToPage(memories[previousIndex].assets.length - 1); - } - }); - } - }); - } - } - - toNextAsset(int currentAssetIndex) { - if (currentAssetIndex + 1 < currentMemory.value.assets.length) { - // Go to the next asset - PageController controller = memoryAssetPageControllers[currentMemoryIndex.value]; - - controller.nextPage(curve: Curves.easeInOut, duration: const Duration(milliseconds: 500)); - } else { - // Go to the next memory since we are at the end of our assets - toNextMemory(); - } - } - - toPreviousAsset(int currentAssetIndex) { - if (currentAssetIndex > 0) { - // Go to the previous asset - PageController controller = memoryAssetPageControllers[currentMemoryIndex.value]; - - controller.previousPage(curve: Curves.easeInOut, duration: const Duration(milliseconds: 500)); - } else { - // Go to the previous memory since we are at the end of our assets - toPreviousMemory(); - } - } - - updateProgressText() { - assetProgress.value = "${currentAssetPage.value + 1}|${currentMemory.value.assets.length}"; - } - - /// Downloads and caches the image for the asset at this [currentMemory]'s index - precacheAsset(int index) async { - // Guard index out of range - if (index < 0) { - return; - } - - // Context might be removed due to popping out of Memory Lane during Scroll handling - if (!context.mounted) { - return; - } - - late Asset asset; - if (index < currentMemory.value.assets.length) { - // Uses the next asset in this current memory - asset = currentMemory.value.assets[index]; - } else { - // Precache the first asset in the next memory if available - final currentMemoryIndex = memories.indexOf(currentMemory.value); - - // Guard no memory found - if (currentMemoryIndex == -1) { - return; - } - - final nextMemoryIndex = currentMemoryIndex + 1; - // Guard no next memory - if (nextMemoryIndex >= memories.length) { - return; - } - - // Get the first asset from the next memory - asset = memories[nextMemoryIndex].assets.first; - } - - // Precache the asset - final size = MediaQuery.sizeOf(context); - await precacheImage( - ImmichImage.imageProvider(asset: asset, width: size.width, height: size.height), - context, - size: size, - ); - } - - // Precache the next page right away if we are on the first page - if (currentAssetPage.value == 0) { - Future.delayed(const Duration(milliseconds: 200)).then((_) => precacheAsset(1)); - } - - Future onAssetChanged(int otherIndex) async { - ref.read(hapticFeedbackProvider.notifier).selectionClick(); - currentAssetPage.value = otherIndex; - updateProgressText(); - - // Wait for page change animation to finish - await Future.delayed(const Duration(milliseconds: 400)); - // And then precache the next asset - await precacheAsset(otherIndex + 1); - - final asset = currentMemory.value.assets[otherIndex]; - currentAsset.value = asset; - ref.read(currentAssetProvider.notifier).set(asset); - } - - /* Notification listener is used instead of OnPageChanged callback since OnPageChanged is called - * when the page in the **center** of the viewer changes. We want to reset currentAssetPage only when the final - * page during the end of scroll is different than the current page - */ - return NotificationListener( - onNotification: (ScrollNotification notification) { - // Calculate OverScroll manually using the number of pixels away from maxScrollExtent - // maxScrollExtend contains the sum of horizontal pixels of all assets for depth = 1 - // or sum of vertical pixels of all memories for depth = 0 - if (notification is ScrollUpdateNotification) { - final isEpiloguePage = (memoryPageController.page?.floor() ?? 0) >= memories.length; - - final offset = notification.metrics.pixels; - if (isEpiloguePage && (offset > notification.metrics.maxScrollExtent + 150)) { - context.maybePop(); - return true; - } - } - - return false; - }, - child: Scaffold( - backgroundColor: bgColor, - body: SafeArea( - child: PageView.builder( - physics: const BouncingScrollPhysics(parent: AlwaysScrollableScrollPhysics()), - scrollDirection: Axis.vertical, - controller: memoryPageController, - onPageChanged: (pageNumber) { - ref.read(hapticFeedbackProvider.notifier).mediumImpact(); - if (pageNumber < memories.length) { - currentMemoryIndex.value = pageNumber; - currentMemory.value = memories[pageNumber]; - } - - currentAssetPage.value = 0; - - updateProgressText(); - }, - itemCount: memories.length + 1, - itemBuilder: (context, mIndex) { - // Build last page - if (mIndex == memories.length) { - return MemoryEpilogue( - onStartOver: () => memoryPageController.animateToPage( - 0, - duration: const Duration(seconds: 1), - curve: Curves.easeInOut, - ), - ); - } - // Build horizontal page - final assetController = memoryAssetPageControllers[mIndex]; - return Column( - children: [ - Padding( - padding: const EdgeInsets.only(left: 24.0, right: 24.0, top: 8.0, bottom: 2.0), - child: AnimatedBuilder( - animation: assetController, - builder: (context, child) { - double value = 0.0; - if (assetController.hasClients) { - // We can only access [page] if this has clients - value = assetController.page ?? 0; - } - return MemoryProgressIndicator( - ticks: memories[mIndex].assets.length, - value: (value + 1) / memories[mIndex].assets.length, - ); - }, - ), - ), - Expanded( - child: Stack( - children: [ - PageView.builder( - physics: const BouncingScrollPhysics(parent: AlwaysScrollableScrollPhysics()), - controller: assetController, - onPageChanged: onAssetChanged, - scrollDirection: Axis.horizontal, - itemCount: memories[mIndex].assets.length, - itemBuilder: (context, index) { - final asset = memories[mIndex].assets[index]; - return Stack( - children: [ - Container( - color: Colors.black, - child: MemoryCard(asset: asset, title: memories[mIndex].title, showTitle: index == 0), - ), - Positioned.fill( - child: Row( - children: [ - // Left side of the screen - Expanded( - child: GestureDetector( - behavior: HitTestBehavior.translucent, - onTap: () { - toPreviousAsset(index); - }, - ), - ), - - // Right side of the screen - Expanded( - child: GestureDetector( - behavior: HitTestBehavior.translucent, - onTap: () { - toNextAsset(index); - }, - ), - ), - ], - ), - ), - ], - ); - }, - ), - Positioned( - top: 8, - left: 8, - child: MaterialButton( - minWidth: 0, - onPressed: () { - // auto_route doesn't invoke pop scope, so - // turn off full screen mode here - // https://github.com/Milad-Akarie/auto_route_library/issues/1799 - context.maybePop(); - SystemChrome.setEnabledSystemUIMode(SystemUiMode.edgeToEdge); - }, - shape: const CircleBorder(), - color: Colors.white.withValues(alpha: 0.2), - elevation: 0, - child: const Icon(Icons.close_rounded, color: Colors.white), - ), - ), - if (currentAsset.value != null && currentAsset.value!.isVideo) - Positioned( - bottom: 24, - right: 32, - child: Icon(Icons.videocam_outlined, color: Colors.grey[200]), - ), - ], - ), - ), - MemoryBottomInfo(memory: memories[mIndex]), - ], - ); - }, - ), - ), - ), - ); - } -} diff --git a/mobile/lib/pages/photos/photos.page.dart b/mobile/lib/pages/photos/photos.page.dart deleted file mode 100644 index 7f57247ec4..0000000000 --- a/mobile/lib/pages/photos/photos.page.dart +++ /dev/null @@ -1,130 +0,0 @@ -import 'dart:async'; - -import 'package:auto_route/auto_route.dart'; -import 'package:easy_localization/easy_localization.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_hooks/flutter_hooks.dart'; -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/extensions/build_context_extensions.dart'; -import 'package:immich_mobile/providers/album/album.provider.dart'; -import 'package:immich_mobile/providers/asset.provider.dart'; -import 'package:immich_mobile/providers/multiselect.provider.dart'; -import 'package:immich_mobile/providers/server_info.provider.dart'; -import 'package:immich_mobile/providers/timeline.provider.dart'; -import 'package:immich_mobile/providers/user.provider.dart'; -import 'package:immich_mobile/providers/websocket.provider.dart'; -import 'package:immich_mobile/widgets/asset_grid/multiselect_grid.dart'; -import 'package:immich_mobile/widgets/common/immich_app_bar.dart'; -import 'package:immich_mobile/widgets/common/immich_loading_indicator.dart'; -import 'package:immich_mobile/widgets/memories/memory_lane.dart'; - -@RoutePage() -class PhotosPage extends HookConsumerWidget { - const PhotosPage({super.key}); - - @override - Widget build(BuildContext context, WidgetRef ref) { - final currentUser = ref.watch(currentUserProvider); - final timelineUsers = ref.watch(timelineUsersIdsProvider); - final tipOneOpacity = useState(0.0); - final refreshCount = useState(0); - - useEffect(() { - ref.read(websocketProvider.notifier).connect(); - Future(() => ref.read(assetProvider.notifier).getAllAsset()); - Future(() => ref.read(albumProvider.notifier).refreshRemoteAlbums()); - ref.read(serverInfoProvider.notifier).getServerInfo(); - - return; - }, []); - - Widget buildLoadingIndicator() { - Timer(const Duration(seconds: 2), () => tipOneOpacity.value = 1); - - return Center( - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - const ImmichLoadingIndicator(), - Padding( - padding: const EdgeInsets.only(top: 16.0), - child: Text( - 'home_page_building_timeline', - style: context.textTheme.titleMedium?.copyWith(color: context.primaryColor), - ).tr(), - ), - const SizedBox(height: 8), - AnimatedOpacity( - duration: const Duration(milliseconds: 1000), - opacity: tipOneOpacity.value, - child: Column( - children: [ - SizedBox( - width: 320, - child: Padding( - padding: const EdgeInsets.only(top: 8.0), - child: Text( - 'home_page_first_time_notice', - textAlign: TextAlign.center, - style: context.textTheme.bodyMedium, - ).tr(), - ), - ), - ], - ), - ), - ], - ), - ); - } - - Future refreshAssets() async { - final fullRefresh = refreshCount.value > 0; - - if (fullRefresh) { - unawaited( - Future.wait([ - ref.read(assetProvider.notifier).getAllAsset(clear: true), - ref.read(albumProvider.notifier).refreshRemoteAlbums(), - ]), - ); - - // refresh was forced: user requested another refresh within 2 seconds - refreshCount.value = 0; - } else { - await ref.read(assetProvider.notifier).getAllAsset(clear: false); - - refreshCount.value++; - // set counter back to 0 if user does not request refresh again - Timer(const Duration(seconds: 4), () => refreshCount.value = 0); - } - } - - return Stack( - children: [ - MultiselectGrid( - topWidget: (currentUser != null && currentUser.memoryEnabled) ? const MemoryLane() : const SizedBox(), - renderListProvider: timelineUsers.length > 1 - ? multiUsersTimelineProvider(timelineUsers) - : singleUserTimelineProvider(currentUser?.id), - buildLoadingIndicator: buildLoadingIndicator, - onRefresh: refreshAssets, - stackEnabled: true, - archiveEnabled: true, - editEnabled: true, - ), - AnimatedPositioned( - duration: const Duration(milliseconds: 300), - top: ref.watch(multiselectProvider) ? -(kToolbarHeight + context.padding.top) : 0, - left: 0, - right: 0, - child: Container( - height: kToolbarHeight + context.padding.top, - color: context.themeData.appBarTheme.backgroundColor, - child: const ImmichAppBar(), - ), - ), - ], - ); - } -} diff --git a/mobile/lib/pages/search/all_motion_videos.page.dart b/mobile/lib/pages/search/all_motion_videos.page.dart deleted file mode 100644 index 60bb8a6cff..0000000000 --- a/mobile/lib/pages/search/all_motion_videos.page.dart +++ /dev/null @@ -1,25 +0,0 @@ -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/extensions/asyncvalue_extensions.dart'; -import 'package:immich_mobile/widgets/asset_grid/immich_asset_grid.dart'; -import 'package:immich_mobile/providers/search/all_motion_photos.provider.dart'; - -@RoutePage() -class AllMotionPhotosPage extends HookConsumerWidget { - const AllMotionPhotosPage({super.key}); - - @override - Widget build(BuildContext context, WidgetRef ref) { - final motionPhotos = ref.watch(allMotionPhotosProvider); - - return Scaffold( - appBar: AppBar( - title: const Text('search_page_motion_photos').tr(), - leading: IconButton(onPressed: () => context.maybePop(), icon: const Icon(Icons.arrow_back_ios_rounded)), - ), - body: motionPhotos.widgetWhen(onData: (assets) => ImmichAssetGrid(assets: assets)), - ); - } -} diff --git a/mobile/lib/pages/search/all_people.page.dart b/mobile/lib/pages/search/all_people.page.dart deleted file mode 100644 index b2814e6c13..0000000000 --- a/mobile/lib/pages/search/all_people.page.dart +++ /dev/null @@ -1,31 +0,0 @@ -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/extensions/asyncvalue_extensions.dart'; -import 'package:immich_mobile/models/search/search_curated_content.model.dart'; -import 'package:immich_mobile/providers/search/people.provider.dart'; -import 'package:immich_mobile/widgets/search/explore_grid.dart'; - -@RoutePage() -class AllPeoplePage extends HookConsumerWidget { - const AllPeoplePage({super.key}); - - @override - Widget build(BuildContext context, WidgetRef ref) { - final curatedPeople = ref.watch(getAllPeopleProvider); - - return Scaffold( - appBar: AppBar( - title: const Text('people').tr(), - leading: IconButton(onPressed: () => context.maybePop(), icon: const Icon(Icons.arrow_back_ios_rounded)), - ), - body: curatedPeople.widgetWhen( - onData: (people) => ExploreGrid( - isPeople: true, - curatedContent: people.map((e) => SearchCuratedContent(label: e.name, id: e.id)).toList(), - ), - ), - ); - } -} diff --git a/mobile/lib/pages/search/all_places.page.dart b/mobile/lib/pages/search/all_places.page.dart deleted file mode 100644 index c92f87d3ac..0000000000 --- a/mobile/lib/pages/search/all_places.page.dart +++ /dev/null @@ -1,26 +0,0 @@ -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/extensions/asyncvalue_extensions.dart'; -import 'package:immich_mobile/models/search/search_curated_content.model.dart'; -import 'package:immich_mobile/providers/search/search_page_state.provider.dart'; -import 'package:immich_mobile/widgets/search/explore_grid.dart'; - -@RoutePage() -class AllPlacesPage extends HookConsumerWidget { - const AllPlacesPage({super.key}); - - @override - Widget build(BuildContext context, WidgetRef ref) { - AsyncValue> places = ref.watch(getAllPlacesProvider); - - return Scaffold( - appBar: AppBar( - title: const Text('places').tr(), - leading: IconButton(onPressed: () => context.maybePop(), icon: const Icon(Icons.arrow_back_ios_rounded)), - ), - body: places.widgetWhen(onData: (data) => ExploreGrid(curatedContent: data)), - ); - } -} diff --git a/mobile/lib/pages/search/all_videos.page.dart b/mobile/lib/pages/search/all_videos.page.dart deleted file mode 100644 index acad043a58..0000000000 --- a/mobile/lib/pages/search/all_videos.page.dart +++ /dev/null @@ -1,22 +0,0 @@ -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/providers/timeline.provider.dart'; -import 'package:immich_mobile/widgets/asset_grid/multiselect_grid.dart'; - -@RoutePage() -class AllVideosPage extends HookConsumerWidget { - const AllVideosPage({super.key}); - - @override - Widget build(BuildContext context, WidgetRef ref) { - return Scaffold( - appBar: AppBar( - title: const Text('videos').tr(), - leading: IconButton(onPressed: () => context.maybePop(), icon: const Icon(Icons.arrow_back_ios_rounded)), - ), - body: MultiselectGrid(renderListProvider: allVideosTimelineProvider), - ); - } -} diff --git a/mobile/lib/pages/search/map/map.page.dart b/mobile/lib/pages/search/map/map.page.dart deleted file mode 100644 index 993b91d8f7..0000000000 --- a/mobile/lib/pages/search/map/map.page.dart +++ /dev/null @@ -1,384 +0,0 @@ -import 'dart:async'; -import 'dart:math'; - -import 'package:auto_route/auto_route.dart'; -import 'package:collection/collection.dart'; -import 'package:easy_localization/easy_localization.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_hooks/flutter_hooks.dart'; -import 'package:fluttertoast/fluttertoast.dart'; -import 'package:geolocator/geolocator.dart'; -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/entities/asset.entity.dart'; -import 'package:immich_mobile/extensions/asyncvalue_extensions.dart'; -import 'package:immich_mobile/extensions/build_context_extensions.dart'; -import 'package:immich_mobile/extensions/maplibrecontroller_extensions.dart'; -import 'package:immich_mobile/models/map/map_event.model.dart'; -import 'package:immich_mobile/models/map/map_marker.model.dart'; -import 'package:immich_mobile/providers/asset_viewer/current_asset.provider.dart'; -import 'package:immich_mobile/providers/asset_viewer/show_controls.provider.dart'; -import 'package:immich_mobile/providers/db.provider.dart'; -import 'package:immich_mobile/providers/map/map_marker.provider.dart'; -import 'package:immich_mobile/providers/map/map_state.provider.dart'; -import 'package:immich_mobile/routing/router.dart'; -import 'package:immich_mobile/utils/debounce.dart'; -import 'package:immich_mobile/utils/immich_loading_overlay.dart'; -import 'package:immich_mobile/utils/map_utils.dart'; -import 'package:immich_mobile/widgets/asset_grid/asset_grid_data_structure.dart'; -import 'package:immich_mobile/widgets/common/immich_toast.dart'; -import 'package:immich_mobile/widgets/map/map_app_bar.dart'; -import 'package:immich_mobile/widgets/map/map_asset_grid.dart'; -import 'package:immich_mobile/widgets/map/map_bottom_sheet.dart'; -import 'package:immich_mobile/widgets/map/map_theme_override.dart'; -import 'package:immich_mobile/widgets/map/positioned_asset_marker_icon.dart'; -import 'package:maplibre_gl/maplibre_gl.dart'; - -@RoutePage() -class MapPage extends HookConsumerWidget { - const MapPage({super.key, this.initialLocation}); - final LatLng? initialLocation; - - @override - Widget build(BuildContext context, WidgetRef ref) { - final mapController = useRef(null); - final markers = useRef>([]); - final markersInBounds = useRef>([]); - final bottomSheetStreamController = useStreamController(); - final selectedMarker = useValueNotifier<_AssetMarkerMeta?>(null); - final assetsDebouncer = useDebouncer(); - final layerDebouncer = useDebouncer(interval: const Duration(seconds: 1)); - final isLoading = useProcessingOverlay(); - final scrollController = useScrollController(); - final markerDebouncer = useDebouncer(interval: const Duration(milliseconds: 800)); - final selectedAssets = useValueNotifier>({}); - const mapZoomToAssetLevel = 12.0; - - // updates the markersInBounds value with the map markers that are visible in the current - // map camera bounds - Future updateAssetsInBounds() async { - // Guard map not created - if (mapController.value == null) { - return; - } - - final bounds = await mapController.value!.getVisibleRegion(); - final inBounds = markers.value - .where((m) => bounds.contains(LatLng(m.latLng.latitude, m.latLng.longitude))) - .toList(); - // Notify bottom sheet to update asset grid only when there are new assets - if (markersInBounds.value.length != inBounds.length) { - bottomSheetStreamController.add(MapAssetsInBoundsUpdated(inBounds.map((e) => e.assetRemoteId).toList())); - } - markersInBounds.value = inBounds; - } - - // removes all sources and layers and re-adds them with the updated markers - Future reloadLayers() async { - if (mapController.value != null) { - layerDebouncer.run(() => mapController.value!.reloadAllLayersForMarkers(markers.value)); - } - } - - Future loadMarkers() async { - try { - isLoading.value = true; - markers.value = await ref.read(mapMarkersProvider.future); - assetsDebouncer.run(updateAssetsInBounds); - await reloadLayers(); - } finally { - isLoading.value = false; - } - } - - useEffect(() { - final currentAssetLink = ref.read(currentAssetProvider.notifier).ref.keepAlive(); - - loadMarkers(); - return currentAssetLink.close; - }, []); - - // Refetch markers when map state is changed - ref.listen(mapStateNotifierProvider, (_, current) { - if (current.shouldRefetchMarkers) { - markerDebouncer.run(() { - ref.invalidate(mapMarkersProvider); - // Reset marker - selectedMarker.value = null; - loadMarkers(); - ref.read(mapStateNotifierProvider.notifier).setRefetchMarkers(false); - }); - } - }); - - // updates the selected markers position based on the current map camera - Future updateAssetMarkerPosition(MapMarker marker, {bool shouldAnimate = true}) async { - final assetPoint = await mapController.value!.toScreenLocation(marker.latLng); - selectedMarker.value = _AssetMarkerMeta(point: assetPoint, marker: marker, shouldAnimate: shouldAnimate); - (assetPoint, marker, shouldAnimate); - } - - // finds the nearest asset marker from the tap point and store it as the selectedMarker - Future onMarkerClicked(Point point, LatLng _) async { - // Guard map not created - if (mapController.value == null) { - return; - } - final latlngBound = await mapController.value!.getBoundsFromPoint(point, 50); - final marker = markersInBounds.value.firstWhereOrNull( - (m) => latlngBound.contains(LatLng(m.latLng.latitude, m.latLng.longitude)), - ); - - if (marker != null) { - await updateAssetMarkerPosition(marker); - } else { - // If no asset was previously selected and no new asset is available, close the bottom sheet - if (selectedMarker.value == null) { - bottomSheetStreamController.add(const MapCloseBottomSheet()); - } - selectedMarker.value = null; - } - } - - void onMapCreated(MapLibreMapController controller) async { - mapController.value = controller; - controller.addListener(() { - if (controller.isCameraMoving && selectedMarker.value != null) { - updateAssetMarkerPosition(selectedMarker.value!.marker, shouldAnimate: false); - } - }); - } - - Future onMarkerTapped() async { - final assetId = selectedMarker.value?.marker.assetRemoteId; - if (assetId == null) { - return; - } - - final asset = await ref.read(dbProvider).assets.getByRemoteId(assetId); - if (asset == null) { - return; - } - - // Since we only have a single asset, we can just show GroupAssetBy.none - final renderList = await RenderList.fromAssets([asset], GroupAssetsBy.none); - - ref.read(currentAssetProvider.notifier).set(asset); - if (asset.isVideo) { - ref.read(showControlsProvider.notifier).show = false; - } - unawaited(context.pushRoute(GalleryViewerRoute(initialIndex: 0, heroOffset: 0, renderList: renderList))); - } - - /// BOTTOM SHEET CALLBACKS - - Future onMapMoved() async { - assetsDebouncer.run(updateAssetsInBounds); - } - - void onBottomSheetScrolled(String assetRemoteId) { - final assetMarker = markersInBounds.value.firstWhereOrNull((m) => m.assetRemoteId == assetRemoteId); - if (assetMarker != null) { - updateAssetMarkerPosition(assetMarker); - } - } - - void onZoomToAsset(String assetRemoteId) { - final assetMarker = markersInBounds.value.firstWhereOrNull((m) => m.assetRemoteId == assetRemoteId); - if (mapController.value != null && assetMarker != null) { - // Offset the latitude a little to show the marker just above the viewports center - final offset = context.isMobile ? 0.02 : 0; - final latlng = LatLng(assetMarker.latLng.latitude - offset, assetMarker.latLng.longitude); - mapController.value!.animateCamera( - CameraUpdate.newLatLngZoom(latlng, mapZoomToAssetLevel), - duration: const Duration(milliseconds: 800), - ); - } - } - - void onZoomToLocation() async { - final (location, error) = await MapUtils.checkPermAndGetLocation(context: context); - if (error != null) { - if (error == LocationPermission.unableToDetermine && context.mounted) { - ImmichToast.show( - context: context, - gravity: ToastGravity.BOTTOM, - toastType: ToastType.error, - msg: "map_cannot_get_user_location".tr(), - ); - } - return; - } - - if (mapController.value != null && location != null) { - await mapController.value!.animateCamera( - CameraUpdate.newLatLngZoom(LatLng(location.latitude, location.longitude), mapZoomToAssetLevel), - duration: const Duration(milliseconds: 800), - ); - } - } - - void onAssetsSelected(bool selected, Set selection) { - selectedAssets.value = selected ? selection : {}; - } - - return MapThemeOverride( - mapBuilder: (style) => context.isMobile - // Single-column - ? Scaffold( - extendBodyBehindAppBar: true, - appBar: MapAppBar(selectedAssets: selectedAssets), - body: Stack( - children: [ - _MapWithMarker( - initialLocation: initialLocation, - style: style, - selectedMarker: selectedMarker, - onMapCreated: onMapCreated, - onMapMoved: onMapMoved, - onMapClicked: onMarkerClicked, - onStyleLoaded: reloadLayers, - onMarkerTapped: onMarkerTapped, - ), - // Should be a part of the body and not scaffold::bottomsheet for the - // location button to be hit testable - MapBottomSheet( - mapEventStream: bottomSheetStreamController.stream, - onGridAssetChanged: onBottomSheetScrolled, - onZoomToAsset: onZoomToAsset, - onAssetsSelected: onAssetsSelected, - onZoomToLocation: onZoomToLocation, - selectedAssets: selectedAssets, - ), - ], - ), - ) - // Two-pane - : Row( - children: [ - Expanded( - child: Scaffold( - extendBodyBehindAppBar: true, - appBar: MapAppBar(selectedAssets: selectedAssets), - body: Stack( - children: [ - _MapWithMarker( - initialLocation: initialLocation, - style: style, - selectedMarker: selectedMarker, - onMapCreated: onMapCreated, - onMapMoved: onMapMoved, - onMapClicked: onMarkerClicked, - onStyleLoaded: reloadLayers, - onMarkerTapped: onMarkerTapped, - ), - Positioned( - right: 0, - bottom: context.padding.bottom + 16, - child: ElevatedButton( - onPressed: onZoomToLocation, - style: ElevatedButton.styleFrom(shape: const CircleBorder()), - child: const Icon(Icons.my_location), - ), - ), - ], - ), - ), - ), - Expanded( - child: LayoutBuilder( - builder: (ctx, constraints) => MapAssetGrid( - controller: scrollController, - mapEventStream: bottomSheetStreamController.stream, - onGridAssetChanged: onBottomSheetScrolled, - onZoomToAsset: onZoomToAsset, - onAssetsSelected: onAssetsSelected, - selectedAssets: selectedAssets, - ), - ), - ), - ], - ), - ); - } -} - -class _AssetMarkerMeta { - final Point point; - final MapMarker marker; - final bool shouldAnimate; - - const _AssetMarkerMeta({required this.point, required this.marker, required this.shouldAnimate}); - - @override - String toString() => '_AssetMarkerMeta(point: $point, marker: $marker, shouldAnimate: $shouldAnimate)'; -} - -class _MapWithMarker extends StatelessWidget { - final AsyncValue style; - final MapCreatedCallback onMapCreated; - final OnCameraIdleCallback onMapMoved; - final OnMapClickCallback onMapClicked; - final OnStyleLoadedCallback onStyleLoaded; - final Function()? onMarkerTapped; - final ValueNotifier<_AssetMarkerMeta?> selectedMarker; - final LatLng? initialLocation; - - const _MapWithMarker({ - required this.style, - required this.onMapCreated, - required this.onMapMoved, - required this.onMapClicked, - required this.onStyleLoaded, - required this.selectedMarker, - this.onMarkerTapped, - this.initialLocation, - }); - - @override - Widget build(BuildContext context) { - return LayoutBuilder( - builder: (ctx, constraints) => SizedBox( - height: constraints.maxHeight, - width: constraints.maxWidth, - child: Stack( - children: [ - style.widgetWhen( - onData: (style) => MapLibreMap( - attributionButtonMargins: const Point(8, kToolbarHeight), - initialCameraPosition: CameraPosition( - target: initialLocation ?? const LatLng(0, 0), - zoom: initialLocation != null ? 12 : 0, - ), - styleString: style, - // This is needed to update the selectedMarker's position on map camera updates - // The changes are notified through the mapController ValueListener which is added in [onMapCreated] - trackCameraPosition: true, - onMapCreated: onMapCreated, - onCameraIdle: onMapMoved, - onMapClick: onMapClicked, - onStyleLoadedCallback: onStyleLoaded, - tiltGesturesEnabled: false, - dragEnabled: false, - myLocationEnabled: false, - attributionButtonPosition: AttributionButtonPosition.topRight, - rotateGesturesEnabled: false, - ), - ), - ValueListenableBuilder( - valueListenable: selectedMarker, - builder: (ctx, value, _) => value != null - ? PositionedAssetMarkerIcon( - point: value.point, - assetRemoteId: value.marker.assetRemoteId, - assetThumbhash: '', - durationInMilliseconds: value.shouldAnimate ? 100 : 0, - onTap: onMarkerTapped, - ) - : const SizedBox.shrink(), - ), - ], - ), - ), - ); - } -} diff --git a/mobile/lib/pages/search/person_result.page.dart b/mobile/lib/pages/search/person_result.page.dart deleted file mode 100644 index 8375eb14fd..0000000000 --- a/mobile/lib/pages/search/person_result.page.dart +++ /dev/null @@ -1,101 +0,0 @@ -import 'package:auto_route/auto_route.dart'; -import 'package:easy_localization/easy_localization.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_hooks/flutter_hooks.dart' hide Store; -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/extensions/build_context_extensions.dart'; -import 'package:immich_mobile/presentation/widgets/images/remote_image_provider.dart'; -import 'package:immich_mobile/providers/search/people.provider.dart'; -import 'package:immich_mobile/widgets/search/person_name_edit_form.dart'; -import 'package:immich_mobile/widgets/asset_grid/multiselect_grid.dart'; -import 'package:immich_mobile/utils/image_url_builder.dart'; - -@RoutePage() -class PersonResultPage extends HookConsumerWidget { - final String personId; - final String personName; - - const PersonResultPage({super.key, required this.personId, required this.personName}); - - @override - Widget build(BuildContext context, WidgetRef ref) { - final name = useState(personName); - - showEditNameDialog() { - showDialog( - context: context, - useRootNavigator: false, - builder: (BuildContext context) { - return PersonNameEditForm(personId: personId, personName: name.value); - }, - ).then((result) { - if (result != null && result.success) { - name.value = result.updatedName; - } - }); - } - - void buildBottomSheet() { - showModalBottomSheet( - backgroundColor: context.scaffoldBackgroundColor, - isScrollControlled: false, - context: context, - useSafeArea: true, - builder: (context) { - return SafeArea( - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - ListTile( - leading: const Icon(Icons.edit_outlined), - title: const Text('edit_name', style: TextStyle(fontWeight: FontWeight.bold)).tr(), - onTap: showEditNameDialog, - ), - ], - ), - ); - }, - ); - } - - buildTitleBlock() { - return GestureDetector( - onTap: showEditNameDialog, - child: name.value.isEmpty - ? Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text('add_a_name', style: context.textTheme.titleMedium?.copyWith(color: context.primaryColor)).tr(), - Text('find_them_fast', style: context.textTheme.labelLarge).tr(), - ], - ) - : Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [Text(name.value, style: context.textTheme.titleLarge, overflow: TextOverflow.ellipsis)], - ), - ); - } - - return Scaffold( - appBar: AppBar( - title: Text(name.value), - leading: IconButton(onPressed: () => context.maybePop(), icon: const Icon(Icons.arrow_back_ios_rounded)), - actions: [IconButton(onPressed: buildBottomSheet, icon: const Icon(Icons.more_vert_rounded))], - ), - body: MultiselectGrid( - renderListProvider: personAssetsProvider(personId), - topWidget: Padding( - padding: const EdgeInsets.only(left: 8.0, top: 24), - child: Row( - children: [ - CircleAvatar(radius: 36, backgroundImage: RemoteImageProvider(url: getFaceThumbnailUrl(personId))), - Expanded( - child: Padding(padding: const EdgeInsets.only(left: 16.0, right: 16.0), child: buildTitleBlock()), - ), - ], - ), - ), - ), - ); - } -} diff --git a/mobile/lib/pages/search/recently_taken.page.dart b/mobile/lib/pages/search/recently_taken.page.dart deleted file mode 100644 index 988af2faf0..0000000000 --- a/mobile/lib/pages/search/recently_taken.page.dart +++ /dev/null @@ -1,25 +0,0 @@ -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/extensions/asyncvalue_extensions.dart'; -import 'package:immich_mobile/widgets/asset_grid/immich_asset_grid.dart'; -import 'package:immich_mobile/providers/search/recently_taken_asset.provider.dart'; - -@RoutePage() -class RecentlyTakenPage extends HookConsumerWidget { - const RecentlyTakenPage({super.key}); - - @override - Widget build(BuildContext context, WidgetRef ref) { - final recents = ref.watch(recentlyTakenAssetProvider); - - return Scaffold( - appBar: AppBar( - title: const Text('recently_taken_page_title').tr(), - leading: IconButton(onPressed: () => context.maybePop(), icon: const Icon(Icons.arrow_back_ios_rounded)), - ), - body: recents.widgetWhen(onData: (searchResponse) => ImmichAssetGrid(assets: searchResponse)), - ); - } -} diff --git a/mobile/lib/pages/search/search.page.dart b/mobile/lib/pages/search/search.page.dart deleted file mode 100644 index dbd32ac94b..0000000000 --- a/mobile/lib/pages/search/search.page.dart +++ /dev/null @@ -1,760 +0,0 @@ -import 'dart:async'; - -import 'package:auto_route/auto_route.dart'; -import 'package:easy_localization/easy_localization.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_hooks/flutter_hooks.dart'; -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/constants/enums.dart'; -import 'package:immich_mobile/domain/models/person.model.dart'; -import 'package:immich_mobile/entities/asset.entity.dart'; -import 'package:immich_mobile/extensions/build_context_extensions.dart'; -import 'package:immich_mobile/models/search/search_filter.model.dart'; -import 'package:immich_mobile/providers/search/paginated_search.provider.dart'; -import 'package:immich_mobile/providers/search/search_input_focus.provider.dart'; -import 'package:immich_mobile/routing/router.dart'; -import 'package:immich_mobile/widgets/asset_grid/multiselect_grid.dart'; -import 'package:immich_mobile/widgets/common/search_field.dart'; -import 'package:immich_mobile/widgets/search/search_filter/camera_picker.dart'; -import 'package:immich_mobile/widgets/search/search_filter/display_option_picker.dart'; -import 'package:immich_mobile/widgets/search/search_filter/filter_bottom_sheet_scaffold.dart'; -import 'package:immich_mobile/widgets/search/search_filter/location_picker.dart'; -import 'package:immich_mobile/widgets/search/search_filter/media_type_picker.dart'; -import 'package:immich_mobile/widgets/search/search_filter/people_picker.dart'; -import 'package:immich_mobile/widgets/search/search_filter/search_filter_chip.dart'; -import 'package:immich_mobile/widgets/search/search_filter/search_filter_utils.dart'; - -@RoutePage() -class SearchPage extends HookConsumerWidget { - const SearchPage({super.key, this.prefilter}); - - final SearchFilter? prefilter; - - @override - Widget build(BuildContext context, WidgetRef ref) { - final textSearchType = useState(TextSearchType.context); - final searchHintText = useState('sunrise_on_the_beach'.tr()); - final textSearchController = useTextEditingController(); - final filter = useState( - SearchFilter( - people: prefilter?.people ?? {}, - location: prefilter?.location ?? SearchLocationFilter(), - camera: prefilter?.camera ?? SearchCameraFilter(), - date: prefilter?.date ?? SearchDateFilter(), - display: prefilter?.display ?? SearchDisplayFilters(isNotInAlbum: false, isArchive: false, isFavorite: false), - mediaType: prefilter?.mediaType ?? AssetType.other, - rating: prefilter?.rating ?? SearchRatingFilter(), - language: "${context.locale.languageCode}-${context.locale.countryCode}", - ), - ); - - final previousFilter = useState(null); - - final peopleCurrentFilterWidget = useState(null); - final dateRangeCurrentFilterWidget = useState(null); - final cameraCurrentFilterWidget = useState(null); - final locationCurrentFilterWidget = useState(null); - final mediaTypeCurrentFilterWidget = useState(null); - final displayOptionCurrentFilterWidget = useState(null); - - final isSearching = useState(false); - - SnackBar searchInfoSnackBar(String message) { - return SnackBar( - content: Text(message, style: context.textTheme.labelLarge), - showCloseIcon: true, - behavior: SnackBarBehavior.fixed, - closeIconColor: context.colorScheme.onSurface, - ); - } - - search() async { - if (filter.value.isEmpty) { - return; - } - - if (prefilter == null && filter.value == previousFilter.value) { - return; - } - - isSearching.value = true; - ref.watch(paginatedSearchProvider.notifier).clear(); - final hasResult = await ref.watch(paginatedSearchProvider.notifier).search(filter.value); - - if (!hasResult) { - context.showSnackBar(searchInfoSnackBar('search_no_result'.tr())); - } - - previousFilter.value = filter.value; - isSearching.value = false; - } - - loadMoreSearchResult() async { - isSearching.value = true; - final hasResult = await ref.watch(paginatedSearchProvider.notifier).search(filter.value); - - if (!hasResult) { - context.showSnackBar(searchInfoSnackBar('search_no_more_result'.tr())); - } - - isSearching.value = false; - } - - searchPrefilter() { - if (prefilter != null) { - Future.delayed(Duration.zero, () { - search(); - - if (prefilter!.location.city != null) { - locationCurrentFilterWidget.value = Text(prefilter!.location.city!, style: context.textTheme.labelLarge); - } - }); - } - } - - useEffect(() { - Future.microtask(() => ref.invalidate(paginatedSearchProvider)); - searchPrefilter(); - - return null; - }, []); - - showPeoplePicker() { - handleOnSelect(Set value) { - filter.value = filter.value.copyWith(people: value); - - peopleCurrentFilterWidget.value = Text( - value.map((e) => e.name != '' ? e.name : 'no_name'.tr()).join(', '), - style: context.textTheme.labelLarge, - ); - } - - handleClear() { - filter.value = filter.value.copyWith(people: {}); - - peopleCurrentFilterWidget.value = null; - search(); - } - - showFilterBottomSheet( - context: context, - isScrollControlled: true, - child: FractionallySizedBox( - heightFactor: 0.8, - child: FilterBottomSheetScaffold( - title: 'search_filter_people_title'.tr(), - expanded: true, - onSearch: search, - onClear: handleClear, - child: PeoplePicker(onSelect: handleOnSelect, filter: filter.value.people), - ), - ), - ); - } - - showLocationPicker() { - handleOnSelect(Map value) { - filter.value = filter.value.copyWith( - location: SearchLocationFilter(country: value['country'], city: value['city'], state: value['state']), - ); - - final locationText = []; - if (value['country'] != null) { - locationText.add(value['country']!); - } - - if (value['state'] != null) { - locationText.add(value['state']!); - } - - if (value['city'] != null) { - locationText.add(value['city']!); - } - - locationCurrentFilterWidget.value = Text(locationText.join(', '), style: context.textTheme.labelLarge); - } - - handleClear() { - filter.value = filter.value.copyWith(location: SearchLocationFilter()); - - locationCurrentFilterWidget.value = null; - search(); - } - - showFilterBottomSheet( - context: context, - isScrollControlled: true, - isDismissible: true, - child: FilterBottomSheetScaffold( - title: 'search_filter_location_title'.tr(), - onSearch: search, - onClear: handleClear, - child: Padding( - padding: const EdgeInsets.symmetric(vertical: 16.0), - child: Container( - padding: EdgeInsets.only(bottom: context.viewInsets.bottom), - child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 16.0), - child: LocationPicker(onSelected: handleOnSelect, filter: filter.value.location), - ), - ), - ), - ), - ); - } - - showCameraPicker() { - handleOnSelect(Map value) { - filter.value = filter.value.copyWith( - camera: SearchCameraFilter(make: value['make'], model: value['model']), - ); - - cameraCurrentFilterWidget.value = Text( - '${value['make'] ?? ''} ${value['model'] ?? ''}', - style: context.textTheme.labelLarge, - ); - } - - handleClear() { - filter.value = filter.value.copyWith(camera: SearchCameraFilter()); - - cameraCurrentFilterWidget.value = null; - search(); - } - - showFilterBottomSheet( - context: context, - isScrollControlled: true, - isDismissible: true, - child: FilterBottomSheetScaffold( - title: 'search_filter_camera_title'.tr(), - onSearch: search, - onClear: handleClear, - child: Padding( - padding: const EdgeInsets.all(16.0), - child: CameraPicker(onSelect: handleOnSelect, filter: filter.value.camera), - ), - ), - ); - } - - showDatePicker() async { - final firstDate = DateTime(1900); - final lastDate = DateTime.now(); - - final date = await showDateRangePicker( - context: context, - firstDate: firstDate, - lastDate: lastDate, - currentDate: DateTime.now(), - initialDateRange: DateTimeRange( - start: filter.value.date.takenAfter ?? lastDate, - end: filter.value.date.takenBefore ?? lastDate, - ), - helpText: 'search_filter_date_title'.tr(), - cancelText: 'cancel'.tr(), - confirmText: 'select'.tr(), - saveText: 'save'.tr(), - errorFormatText: 'invalid_date_format'.tr(), - errorInvalidText: 'invalid_date'.tr(), - fieldStartHintText: 'start_date'.tr(), - fieldEndHintText: 'end_date'.tr(), - initialEntryMode: DatePickerEntryMode.calendar, - keyboardType: TextInputType.text, - ); - - if (date == null) { - filter.value = filter.value.copyWith(date: SearchDateFilter()); - - dateRangeCurrentFilterWidget.value = null; - unawaited(search()); - return; - } - - filter.value = filter.value.copyWith( - date: SearchDateFilter( - takenAfter: date.start, - takenBefore: date.end.add(const Duration(hours: 23, minutes: 59, seconds: 59)), - ), - ); - - // If date range is less than 24 hours, set the end date to the end of the day - if (date.end.difference(date.start).inHours < 24) { - dateRangeCurrentFilterWidget.value = Text( - DateFormat.yMMMd().format(date.start.toLocal()), - style: context.textTheme.labelLarge, - ); - } else { - dateRangeCurrentFilterWidget.value = Text( - 'search_filter_date_interval'.tr( - namedArgs: { - "start": DateFormat.yMMMd().format(date.start.toLocal()), - "end": DateFormat.yMMMd().format(date.end.toLocal()), - }, - ), - style: context.textTheme.labelLarge, - ); - } - - unawaited(search()); - } - - // MEDIA PICKER - showMediaTypePicker() { - handleOnSelected(AssetType assetType) { - filter.value = filter.value.copyWith(mediaType: assetType); - - mediaTypeCurrentFilterWidget.value = Text( - assetType == AssetType.image - ? 'image'.tr() - : assetType == AssetType.video - ? 'video'.tr() - : 'all'.tr(), - style: context.textTheme.labelLarge, - ); - } - - handleClear() { - filter.value = filter.value.copyWith(mediaType: AssetType.other); - - mediaTypeCurrentFilterWidget.value = null; - search(); - } - - showFilterBottomSheet( - context: context, - child: FilterBottomSheetScaffold( - title: 'search_filter_media_type_title'.tr(), - onSearch: search, - onClear: handleClear, - child: MediaTypePicker(onSelect: handleOnSelected, filter: filter.value.mediaType), - ), - ); - } - - // DISPLAY OPTION - showDisplayOptionPicker() { - handleOnSelect(Map value) { - final filterText = []; - value.forEach((key, value) { - switch (key) { - case DisplayOption.notInAlbum: - filter.value = filter.value.copyWith(display: filter.value.display.copyWith(isNotInAlbum: value)); - if (value) { - filterText.add('search_filter_display_option_not_in_album'.tr()); - } - break; - case DisplayOption.archive: - filter.value = filter.value.copyWith(display: filter.value.display.copyWith(isArchive: value)); - if (value) { - filterText.add('archive'.tr()); - } - break; - case DisplayOption.favorite: - filter.value = filter.value.copyWith(display: filter.value.display.copyWith(isFavorite: value)); - if (value) { - filterText.add('favorite'.tr()); - } - break; - } - }); - - if (filterText.isEmpty) { - displayOptionCurrentFilterWidget.value = null; - return; - } - - displayOptionCurrentFilterWidget.value = Text(filterText.join(', '), style: context.textTheme.labelLarge); - } - - handleClear() { - filter.value = filter.value.copyWith( - display: SearchDisplayFilters(isNotInAlbum: false, isArchive: false, isFavorite: false), - ); - - displayOptionCurrentFilterWidget.value = null; - search(); - } - - showFilterBottomSheet( - context: context, - child: FilterBottomSheetScaffold( - title: 'display_options'.tr(), - onSearch: search, - onClear: handleClear, - child: DisplayOptionPicker(onSelect: handleOnSelect, filter: filter.value.display), - ), - ); - } - - handleTextSubmitted(String value) { - switch (textSearchType.value) { - case TextSearchType.context: - filter.value = filter.value.copyWith(filename: '', context: value, description: '', ocr: ''); - - break; - case TextSearchType.filename: - filter.value = filter.value.copyWith(filename: value, context: '', description: '', ocr: ''); - - break; - case TextSearchType.description: - filter.value = filter.value.copyWith(filename: '', context: '', description: value, ocr: ''); - break; - case TextSearchType.ocr: - filter.value = filter.value.copyWith(filename: '', context: '', description: '', ocr: value); - break; - } - - search(); - } - - IconData getSearchPrefixIcon() => switch (textSearchType.value) { - TextSearchType.context => Icons.image_search_rounded, - TextSearchType.filename => Icons.abc_rounded, - TextSearchType.description => Icons.text_snippet_outlined, - TextSearchType.ocr => Icons.document_scanner_outlined, - }; - - return Scaffold( - resizeToAvoidBottomInset: false, - appBar: AppBar( - automaticallyImplyLeading: true, - actions: [ - Padding( - padding: const EdgeInsets.only(right: 16.0), - child: MenuAnchor( - style: MenuStyle( - elevation: const WidgetStatePropertyAll(1), - shape: WidgetStateProperty.all( - const RoundedRectangleBorder(borderRadius: BorderRadius.all(Radius.circular(24))), - ), - padding: const WidgetStatePropertyAll(EdgeInsets.all(4)), - ), - builder: (BuildContext context, MenuController controller, Widget? child) { - return IconButton( - onPressed: () { - if (controller.isOpen) { - controller.close(); - } else { - controller.open(); - } - }, - icon: const Icon(Icons.more_vert_rounded), - tooltip: 'show_text_search_menu'.tr(), - ); - }, - menuChildren: [ - MenuItemButton( - child: ListTile( - leading: const Icon(Icons.image_search_rounded), - title: Text( - 'search_by_context'.tr(), - style: context.textTheme.bodyLarge?.copyWith( - fontWeight: FontWeight.w500, - color: textSearchType.value == TextSearchType.context ? context.colorScheme.primary : null, - ), - ), - selectedColor: context.colorScheme.primary, - selected: textSearchType.value == TextSearchType.context, - ), - onPressed: () { - textSearchType.value = TextSearchType.context; - searchHintText.value = 'sunrise_on_the_beach'.tr(); - }, - ), - MenuItemButton( - child: ListTile( - leading: const Icon(Icons.abc_rounded), - title: Text( - 'search_filter_filename'.tr(), - style: context.textTheme.bodyLarge?.copyWith( - fontWeight: FontWeight.w500, - color: textSearchType.value == TextSearchType.filename ? context.colorScheme.primary : null, - ), - ), - selectedColor: context.colorScheme.primary, - selected: textSearchType.value == TextSearchType.filename, - ), - onPressed: () { - textSearchType.value = TextSearchType.filename; - searchHintText.value = 'file_name_or_extension'.tr(); - }, - ), - MenuItemButton( - child: ListTile( - leading: const Icon(Icons.text_snippet_outlined), - title: Text( - 'search_by_description'.tr(), - style: context.textTheme.bodyLarge?.copyWith( - fontWeight: FontWeight.w500, - color: textSearchType.value == TextSearchType.description ? context.colorScheme.primary : null, - ), - ), - selectedColor: context.colorScheme.primary, - selected: textSearchType.value == TextSearchType.description, - ), - onPressed: () { - textSearchType.value = TextSearchType.description; - searchHintText.value = 'search_by_description_example'.tr(); - }, - ), - MenuItemButton( - child: ListTile( - leading: const Icon(Icons.document_scanner_outlined), - title: Text( - 'search_filter_ocr'.tr(), - style: context.textTheme.bodyLarge?.copyWith( - fontWeight: FontWeight.w500, - color: textSearchType.value == TextSearchType.ocr ? context.colorScheme.primary : null, - ), - ), - selectedColor: context.colorScheme.primary, - selected: textSearchType.value == TextSearchType.ocr, - ), - onPressed: () { - textSearchType.value = TextSearchType.ocr; - searchHintText.value = 'search_by_ocr_example'.tr(); - }, - ), - ], - ), - ), - ], - title: Container( - decoration: BoxDecoration( - border: Border.all(color: context.colorScheme.onSurface.withAlpha(0), width: 0), - borderRadius: const BorderRadius.all(Radius.circular(24)), - gradient: LinearGradient( - colors: [ - context.colorScheme.primary.withValues(alpha: 0.075), - context.colorScheme.primary.withValues(alpha: 0.09), - context.colorScheme.primary.withValues(alpha: 0.075), - ], - begin: Alignment.topLeft, - end: Alignment.bottomRight, - ), - ), - child: SearchField( - hintText: searchHintText.value, - key: const Key('search_text_field'), - controller: textSearchController, - contentPadding: prefilter != null ? const EdgeInsets.only(left: 24) : const EdgeInsets.all(8), - prefixIcon: prefilter != null ? null : Icon(getSearchPrefixIcon(), color: context.colorScheme.primary), - onSubmitted: handleTextSubmitted, - focusNode: ref.watch(searchInputFocusProvider), - ), - ), - ), - body: Column( - children: [ - Padding( - padding: const EdgeInsets.only(top: 12.0), - child: SizedBox( - height: 50, - child: ListView( - key: const Key('search_filter_chip_list'), - shrinkWrap: true, - scrollDirection: Axis.horizontal, - padding: const EdgeInsets.symmetric(horizontal: 16), - children: [ - SearchFilterChip( - icon: Icons.people_alt_outlined, - onTap: showPeoplePicker, - label: 'people'.tr(), - currentFilter: peopleCurrentFilterWidget.value, - ), - SearchFilterChip( - icon: Icons.location_on_outlined, - onTap: showLocationPicker, - label: 'search_filter_location'.tr(), - currentFilter: locationCurrentFilterWidget.value, - ), - SearchFilterChip( - icon: Icons.camera_alt_outlined, - onTap: showCameraPicker, - label: 'camera'.tr(), - currentFilter: cameraCurrentFilterWidget.value, - ), - SearchFilterChip( - icon: Icons.date_range_outlined, - onTap: showDatePicker, - label: 'search_filter_date'.tr(), - currentFilter: dateRangeCurrentFilterWidget.value, - ), - SearchFilterChip( - key: const Key('media_type_chip'), - icon: Icons.video_collection_outlined, - onTap: showMediaTypePicker, - label: 'search_filter_media_type'.tr(), - currentFilter: mediaTypeCurrentFilterWidget.value, - ), - SearchFilterChip( - icon: Icons.display_settings_outlined, - onTap: showDisplayOptionPicker, - label: 'search_filter_display_options'.tr(), - currentFilter: displayOptionCurrentFilterWidget.value, - ), - ], - ), - ), - ), - if (isSearching.value) - const Expanded(child: Center(child: CircularProgressIndicator())) - else - SearchResultGrid(onScrollEnd: loadMoreSearchResult, isSearching: isSearching.value), - ], - ), - ); - } -} - -class SearchResultGrid extends StatelessWidget { - final VoidCallback onScrollEnd; - final bool isSearching; - - const SearchResultGrid({super.key, required this.onScrollEnd, this.isSearching = false}); - - @override - Widget build(BuildContext context) { - return Expanded( - child: Padding( - padding: const EdgeInsets.only(top: 8.0), - child: NotificationListener( - onNotification: (notification) { - final isBottomSheetNotification = - notification.context?.findAncestorWidgetOfExactType() != null; - - final metrics = notification.metrics; - final isVerticalScroll = metrics.axis == Axis.vertical; - - if (metrics.pixels >= metrics.maxScrollExtent && isVerticalScroll && !isBottomSheetNotification) { - onScrollEnd(); - } - - return true; - }, - child: MultiselectGrid( - renderListProvider: paginatedSearchRenderListProvider, - archiveEnabled: true, - deleteEnabled: true, - editEnabled: true, - favoriteEnabled: true, - stackEnabled: false, - dragScrollLabelEnabled: false, - emptyIndicator: Padding( - padding: const EdgeInsets.symmetric(horizontal: 16.0), - child: !isSearching ? const SearchEmptyContent() : const SizedBox.shrink(), - ), - ), - ), - ), - ); - } -} - -class SearchEmptyContent extends StatelessWidget { - const SearchEmptyContent({super.key}); - - @override - Widget build(BuildContext context) { - return NotificationListener( - onNotification: (_) => true, - child: ListView( - shrinkWrap: false, - children: [ - const SizedBox(height: 40), - Center( - child: Image.asset( - context.isDarkTheme ? 'assets/polaroid-dark.png' : 'assets/polaroid-light.png', - height: 125, - ), - ), - const SizedBox(height: 16), - Center(child: Text('search_page_search_photos_videos'.tr(), style: context.textTheme.labelLarge)), - const SizedBox(height: 32), - const QuickLinkList(), - ], - ), - ); - } -} - -class QuickLinkList extends StatelessWidget { - const QuickLinkList({super.key}); - - @override - Widget build(BuildContext context) { - return Container( - decoration: BoxDecoration( - borderRadius: const BorderRadius.all(Radius.circular(20)), - border: Border.all(color: context.colorScheme.outline.withAlpha(10), width: 1), - gradient: LinearGradient( - colors: [ - context.colorScheme.primary.withAlpha(10), - context.colorScheme.primary.withAlpha(15), - context.colorScheme.primary.withAlpha(20), - ], - begin: Alignment.topCenter, - end: Alignment.bottomCenter, - ), - ), - child: ListView( - shrinkWrap: true, - physics: const NeverScrollableScrollPhysics(), - children: [ - QuickLink( - title: 'recently_taken'.tr(), - icon: Icons.schedule_outlined, - isTop: true, - onTap: () => context.pushRoute(const RecentlyTakenRoute()), - ), - QuickLink( - title: 'videos'.tr(), - icon: Icons.play_circle_outline_rounded, - onTap: () => context.pushRoute(const AllVideosRoute()), - ), - QuickLink( - title: 'favorites'.tr(), - icon: Icons.favorite_border_rounded, - isBottom: true, - onTap: () => context.pushRoute(const FavoritesRoute()), - ), - ], - ), - ); - } -} - -class QuickLink extends StatelessWidget { - final String title; - final IconData icon; - final VoidCallback onTap; - final bool isTop; - final bool isBottom; - - const QuickLink({ - super.key, - required this.title, - required this.icon, - required this.onTap, - this.isTop = false, - this.isBottom = false, - }); - - @override - Widget build(BuildContext context) { - final borderRadius = BorderRadius.only( - topLeft: Radius.circular(isTop ? 20 : 0), - topRight: Radius.circular(isTop ? 20 : 0), - bottomLeft: Radius.circular(isBottom ? 20 : 0), - bottomRight: Radius.circular(isBottom ? 20 : 0), - ); - - return ListTile( - shape: RoundedRectangleBorder(borderRadius: borderRadius), - leading: Icon(icon, size: 26), - title: Text(title, style: context.textTheme.titleSmall?.copyWith(fontWeight: FontWeight.w500)), - onTap: onTap, - ); - } -} diff --git a/mobile/lib/pages/share_intent/share_intent.page.dart b/mobile/lib/pages/share_intent/share_intent.page.dart index 2be51fbfc9..2744b187de 100644 --- a/mobile/lib/pages/share_intent/share_intent.page.dart +++ b/mobile/lib/pages/share_intent/share_intent.page.dart @@ -2,7 +2,6 @@ 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/entities/store.entity.dart'; import 'package:immich_mobile/extensions/build_context_extensions.dart'; import 'package:immich_mobile/models/upload/share_intent_attachment.model.dart'; import 'package:immich_mobile/pages/common/large_leading_tile.dart'; @@ -66,7 +65,7 @@ class ShareIntentPage extends ConsumerWidget { ), leading: IconButton( onPressed: () { - context.navigateTo(Store.isBetaTimelineEnabled ? const TabShellRoute() : const TabControllerRoute()); + context.navigateTo(const TabShellRoute()); }, icon: const Icon(Icons.arrow_back), ), diff --git a/mobile/lib/presentation/pages/search/drift_search.page.dart b/mobile/lib/presentation/pages/search/drift_search.page.dart index 7e47a742ae..3ba4cf3497 100644 --- a/mobile/lib/presentation/pages/search/drift_search.page.dart +++ b/mobile/lib/presentation/pages/search/drift_search.page.dart @@ -6,11 +6,11 @@ import 'package:flutter/material.dart'; import 'package:flutter_hooks/flutter_hooks.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/person.model.dart'; import 'package:immich_mobile/domain/models/tag.model.dart'; import 'package:immich_mobile/domain/models/timeline.model.dart'; import 'package:immich_mobile/domain/services/timeline.service.dart'; -import 'package:immich_mobile/entities/asset.entity.dart'; import 'package:immich_mobile/extensions/build_context_extensions.dart'; import 'package:immich_mobile/extensions/translate_extensions.dart'; import 'package:immich_mobile/models/search/search_filter.model.dart'; diff --git a/mobile/lib/presentation/widgets/action_buttons/similar_photos_action_button.widget.dart b/mobile/lib/presentation/widgets/action_buttons/similar_photos_action_button.widget.dart index bb42140d0a..0acbbce613 100644 --- a/mobile/lib/presentation/widgets/action_buttons/similar_photos_action_button.widget.dart +++ b/mobile/lib/presentation/widgets/action_buttons/similar_photos_action_button.widget.dart @@ -3,7 +3,7 @@ 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/entities/asset.entity.dart'; +import 'package:immich_mobile/domain/models/asset/base_asset.model.dart'; import 'package:immich_mobile/extensions/translate_extensions.dart'; import 'package:immich_mobile/models/search/search_filter.model.dart'; import 'package:immich_mobile/presentation/pages/search/paginated_search.provider.dart'; diff --git a/mobile/lib/presentation/widgets/album/album_selector.widget.dart b/mobile/lib/presentation/widgets/album/album_selector.widget.dart index 0c039847a4..e5b4607619 100644 --- a/mobile/lib/presentation/widgets/album/album_selector.widget.dart +++ b/mobile/lib/presentation/widgets/album/album_selector.widget.dart @@ -17,9 +17,9 @@ import 'package:immich_mobile/presentation/widgets/album/new_album_name_modal.wi import 'package:immich_mobile/presentation/widgets/images/thumbnail.widget.dart'; import 'package:immich_mobile/providers/album/album_sort_by_options.provider.dart'; import 'package:immich_mobile/providers/app_settings.provider.dart'; +import 'package:immich_mobile/providers/asset_viewer/asset_viewer.provider.dart'; import 'package:immich_mobile/providers/infrastructure/album.provider.dart'; import 'package:immich_mobile/providers/infrastructure/asset.provider.dart'; -import 'package:immich_mobile/providers/asset_viewer/asset_viewer.provider.dart'; import 'package:immich_mobile/providers/timeline/multiselect.provider.dart'; import 'package:immich_mobile/providers/user.provider.dart'; import 'package:immich_mobile/routing/router.dart'; diff --git a/mobile/lib/presentation/widgets/map/map.widget.dart b/mobile/lib/presentation/widgets/map/map.widget.dart index 72f4e8bda6..3f406dd551 100644 --- a/mobile/lib/presentation/widgets/map/map.widget.dart +++ b/mobile/lib/presentation/widgets/map/map.widget.dart @@ -132,7 +132,7 @@ class _DriftMapState extends ConsumerState { // If we continue to update bounds, the map-scoped timeline service gets recreated and the previous one disposed, // which can invalidate the TimelineService instance that was passed into AssetViewerRoute (causing "loading forever"). final currentRoute = ref.read(currentRouteNameProvider); - if (currentRoute == AssetViewerRoute.name || currentRoute == GalleryViewerRoute.name) { + if (currentRoute == AssetViewerRoute.name) { return; } diff --git a/mobile/lib/providers/album/album.provider.dart b/mobile/lib/providers/album/album.provider.dart deleted file mode 100644 index 35634d77c8..0000000000 --- a/mobile/lib/providers/album/album.provider.dart +++ /dev/null @@ -1,151 +0,0 @@ -import 'dart:async'; - -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/constants/enums.dart'; -import 'package:immich_mobile/domain/models/user.model.dart'; -import 'package:immich_mobile/entities/album.entity.dart'; -import 'package:immich_mobile/entities/asset.entity.dart'; -import 'package:immich_mobile/models/albums/album_search.model.dart'; -import 'package:immich_mobile/services/album.service.dart'; - -final isRefreshingRemoteAlbumProvider = StateProvider((ref) => false); - -class AlbumNotifier extends StateNotifier> { - AlbumNotifier(this.albumService, this.ref) : super([]) { - albumService.getAllRemoteAlbums().then((value) { - if (mounted) { - state = value; - } - }); - - _streamSub = albumService.watchRemoteAlbums().listen((data) => state = data); - } - - final AlbumService albumService; - final Ref ref; - late final StreamSubscription> _streamSub; - - Future refreshRemoteAlbums() async { - ref.read(isRefreshingRemoteAlbumProvider.notifier).state = true; - await albumService.refreshRemoteAlbums(); - ref.read(isRefreshingRemoteAlbumProvider.notifier).state = false; - } - - Future refreshDeviceAlbums() => albumService.refreshDeviceAlbums(); - - Future deleteAlbum(Album album) => albumService.deleteAlbum(album); - - Future createAlbum(String albumTitle, Set assets) => albumService.createAlbum(albumTitle, assets, []); - - Future getAlbumByName(String albumName, {bool? remote, bool? shared, bool? owner}) => - albumService.getAlbumByName(albumName, remote: remote, shared: shared, owner: owner); - - /// Create an album on the server with the same name as the selected album for backup - /// First this will check if the album already exists on the server with name - /// If it does not exist, it will create the album on the server - Future createSyncAlbum(String albumName) async { - final album = await getAlbumByName(albumName, remote: true, owner: true); - if (album != null) { - return; - } - - await createAlbum(albumName, {}); - } - - Future leaveAlbum(Album album) async { - var res = await albumService.leaveAlbum(album); - - if (res) { - await deleteAlbum(album); - return true; - } else { - return false; - } - } - - void searchAlbums(String searchTerm, QuickFilterMode filterMode) async { - state = await albumService.search(searchTerm, filterMode); - } - - Future addUsers(Album album, List userIds) async { - await albumService.addUsers(album, userIds); - } - - Future removeUser(Album album, UserDto user) async { - final isRemoved = await albumService.removeUser(album, user); - - if (isRemoved && album.sharedUsers.isEmpty) { - state = state.where((element) => element.id != album.id).toList(); - } - - return isRemoved; - } - - Future addAssets(Album album, Iterable assets) async { - await albumService.addAssets(album, assets); - } - - Future removeAsset(Album album, Iterable assets) async { - return await albumService.removeAsset(album, assets); - } - - Future setActivitystatus(Album album, bool enabled) { - return albumService.setActivityStatus(album, enabled); - } - - Future toggleSortOrder(Album album) { - final order = album.sortOrder == SortOrder.asc ? SortOrder.desc : SortOrder.asc; - - return albumService.updateSortOrder(album, order); - } - - @override - void dispose() { - _streamSub.cancel(); - super.dispose(); - } -} - -final albumProvider = StateNotifierProvider.autoDispose>((ref) { - return AlbumNotifier(ref.watch(albumServiceProvider), ref); -}); - -final albumWatcher = StreamProvider.autoDispose.family((ref, id) async* { - final albumService = ref.watch(albumServiceProvider); - - final album = await albumService.getAlbumById(id); - if (album != null) { - yield album; - } - - await for (final album in albumService.watchAlbum(id)) { - if (album != null) { - yield album; - } - } -}); - -class LocalAlbumsNotifier extends StateNotifier> { - LocalAlbumsNotifier(this.albumService) : super([]) { - albumService.getAllLocalAlbums().then((value) { - if (mounted) { - state = value; - } - }); - - _streamSub = albumService.watchLocalAlbums().listen((data) => state = data); - } - - final AlbumService albumService; - late final StreamSubscription> _streamSub; - - @override - void dispose() { - _streamSub.cancel(); - super.dispose(); - } -} - -final localAlbumsProvider = StateNotifierProvider.autoDispose>((ref) { - return LocalAlbumsNotifier(ref.watch(albumServiceProvider)); -}); diff --git a/mobile/lib/providers/album/album_sort_by_options.provider.dart b/mobile/lib/providers/album/album_sort_by_options.provider.dart index c969dbd37d..ec4ae71d03 100644 --- a/mobile/lib/providers/album/album_sort_by_options.provider.dart +++ b/mobile/lib/providers/album/album_sort_by_options.provider.dart @@ -1,119 +1,19 @@ -import 'package:collection/collection.dart'; import 'package:immich_mobile/constants/enums.dart'; -import 'package:immich_mobile/providers/app_settings.provider.dart'; -import 'package:immich_mobile/services/app_settings.service.dart'; -import 'package:immich_mobile/entities/album.entity.dart'; -import 'package:riverpod_annotation/riverpod_annotation.dart'; - -part 'album_sort_by_options.provider.g.dart'; - -typedef AlbumSortFn = List Function(List albums, bool isReverse); - -class _AlbumSortHandlers { - const _AlbumSortHandlers._(); - - static const AlbumSortFn created = _sortByCreated; - static List _sortByCreated(List albums, bool isReverse) { - final sorted = albums.sortedBy((album) => album.createdAt); - return (isReverse ? sorted.reversed : sorted).toList(); - } - - static const AlbumSortFn title = _sortByTitle; - static List _sortByTitle(List albums, bool isReverse) { - final sorted = albums.sortedBy((album) => album.name); - return (isReverse ? sorted.reversed : sorted).toList(); - } - - static const AlbumSortFn lastModified = _sortByLastModified; - static List _sortByLastModified(List albums, bool isReverse) { - final sorted = albums.sortedBy((album) => album.modifiedAt); - return (isReverse ? sorted.reversed : sorted).toList(); - } - - static const AlbumSortFn assetCount = _sortByAssetCount; - static List _sortByAssetCount(List albums, bool isReverse) { - final sorted = albums.sorted((a, b) => a.assetCount.compareTo(b.assetCount)); - return (isReverse ? sorted.reversed : sorted).toList(); - } - - static const AlbumSortFn mostRecent = _sortByMostRecent; - static List _sortByMostRecent(List albums, bool isReverse) { - final sorted = albums.sorted((a, b) { - if (a.endDate == null && b.endDate == null) { - return 0; - } - - if (a.endDate == null) { - // Put nulls at the end for recent sorting - return 1; - } - - if (b.endDate == null) { - return -1; - } - - // Sort by descending recent date - return b.endDate!.compareTo(a.endDate!); - }); - return (isReverse ? sorted.reversed : sorted).toList(); - } - - static const AlbumSortFn mostOldest = _sortByMostOldest; - static List _sortByMostOldest(List albums, bool isReverse) { - final sorted = albums.sorted((a, b) { - if (a.startDate != null && b.startDate != null) { - return a.startDate!.compareTo(b.startDate!); - } - if (a.startDate == null) return 1; - if (b.startDate == null) return -1; - return 0; - }); - return (isReverse ? sorted.reversed : sorted).toList(); - } -} // Store index allows us to re-arrange the values without affecting the saved prefs enum AlbumSortMode { - title(1, "library_page_sort_title", _AlbumSortHandlers.title, SortOrder.asc), - assetCount(4, "library_page_sort_asset_count", _AlbumSortHandlers.assetCount, SortOrder.desc), - lastModified(3, "library_page_sort_last_modified", _AlbumSortHandlers.lastModified, SortOrder.desc), - created(0, "library_page_sort_created", _AlbumSortHandlers.created, SortOrder.desc), - mostRecent(2, "sort_recent", _AlbumSortHandlers.mostRecent, SortOrder.desc), - mostOldest(5, "sort_oldest", _AlbumSortHandlers.mostOldest, SortOrder.asc); + title(1, "library_page_sort_title", SortOrder.asc), + assetCount(4, "library_page_sort_asset_count", SortOrder.desc), + lastModified(3, "library_page_sort_last_modified", SortOrder.desc), + created(0, "library_page_sort_created", SortOrder.desc), + mostRecent(2, "sort_recent", SortOrder.desc), + mostOldest(5, "sort_oldest", SortOrder.asc); final int storeIndex; final String label; - final AlbumSortFn sortFn; final SortOrder defaultOrder; - const AlbumSortMode(this.storeIndex, this.label, this.sortFn, this.defaultOrder); + const AlbumSortMode(this.storeIndex, this.label, this.defaultOrder); SortOrder effectiveOrder(bool isReverse) => isReverse ? defaultOrder.reverse() : defaultOrder; } - -@riverpod -class AlbumSortByOptions extends _$AlbumSortByOptions { - @override - AlbumSortMode build() { - final sortOpt = ref.watch(appSettingsServiceProvider).getSetting(AppSettingsEnum.selectedAlbumSortOrder); - return AlbumSortMode.values.firstWhere((e) => e.storeIndex == sortOpt, orElse: () => AlbumSortMode.title); - } - - void changeSortMode(AlbumSortMode sortOption) { - state = sortOption; - ref.watch(appSettingsServiceProvider).setSetting(AppSettingsEnum.selectedAlbumSortOrder, sortOption.storeIndex); - } -} - -@riverpod -class AlbumSortOrder extends _$AlbumSortOrder { - @override - bool build() { - return ref.watch(appSettingsServiceProvider).getSetting(AppSettingsEnum.selectedAlbumSortReverse); - } - - void changeSortDirection(bool isReverse) { - state = isReverse; - ref.watch(appSettingsServiceProvider).setSetting(AppSettingsEnum.selectedAlbumSortReverse, isReverse); - } -} diff --git a/mobile/lib/providers/album/album_sort_by_options.provider.g.dart b/mobile/lib/providers/album/album_sort_by_options.provider.g.dart deleted file mode 100644 index 750329c9d5..0000000000 --- a/mobile/lib/providers/album/album_sort_by_options.provider.g.dart +++ /dev/null @@ -1,43 +0,0 @@ -// GENERATED CODE - DO NOT MODIFY BY HAND - -part of 'album_sort_by_options.provider.dart'; - -// ************************************************************************** -// RiverpodGenerator -// ************************************************************************** - -String _$albumSortByOptionsHash() => - r'dd8da5e730af555de1b86c3b157b6c93183523ac'; - -/// See also [AlbumSortByOptions]. -@ProviderFor(AlbumSortByOptions) -final albumSortByOptionsProvider = - AutoDisposeNotifierProvider.internal( - AlbumSortByOptions.new, - name: r'albumSortByOptionsProvider', - debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product') - ? null - : _$albumSortByOptionsHash, - dependencies: null, - allTransitiveDependencies: null, - ); - -typedef _$AlbumSortByOptions = AutoDisposeNotifier; -String _$albumSortOrderHash() => r'573dea45b4519e69386fc7104c72522e35713440'; - -/// See also [AlbumSortOrder]. -@ProviderFor(AlbumSortOrder) -final albumSortOrderProvider = - AutoDisposeNotifierProvider.internal( - AlbumSortOrder.new, - name: r'albumSortOrderProvider', - debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product') - ? null - : _$albumSortOrderHash, - dependencies: null, - allTransitiveDependencies: null, - ); - -typedef _$AlbumSortOrder = AutoDisposeNotifier; -// ignore_for_file: type=lint -// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member, deprecated_member_use_from_same_package diff --git a/mobile/lib/providers/album/album_viewer.provider.dart b/mobile/lib/providers/album/album_viewer.provider.dart deleted file mode 100644 index f4ce047464..0000000000 --- a/mobile/lib/providers/album/album_viewer.provider.dart +++ /dev/null @@ -1,74 +0,0 @@ -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/entities/album.entity.dart'; -import 'package:immich_mobile/models/albums/album_viewer_page_state.model.dart'; -import 'package:immich_mobile/services/album.service.dart'; - -class AlbumViewerNotifier extends StateNotifier { - AlbumViewerNotifier(this.ref) - : super(const AlbumViewerPageState(editTitleText: "", isEditAlbum: false, editDescriptionText: "")); - - final Ref ref; - - void enableEditAlbum() { - state = state.copyWith(isEditAlbum: true); - } - - void disableEditAlbum() { - state = state.copyWith(isEditAlbum: false); - } - - void setEditTitleText(String newTitle) { - state = state.copyWith(editTitleText: newTitle); - } - - void setEditDescriptionText(String newDescription) { - state = state.copyWith(editDescriptionText: newDescription); - } - - void remoteEditTitleText() { - state = state.copyWith(editTitleText: ""); - } - - void remoteEditDescriptionText() { - state = state.copyWith(editDescriptionText: ""); - } - - void resetState() { - state = state.copyWith(editTitleText: "", isEditAlbum: false, editDescriptionText: ""); - } - - Future changeAlbumTitle(Album album, String newAlbumTitle) async { - AlbumService service = ref.watch(albumServiceProvider); - - bool isSuccess = await service.changeTitleAlbum(album, newAlbumTitle); - - if (isSuccess) { - state = state.copyWith(editTitleText: "", isEditAlbum: false); - - return true; - } - - state = state.copyWith(editTitleText: "", isEditAlbum: false); - return false; - } - - Future changeAlbumDescription(Album album, String newAlbumDescription) async { - AlbumService service = ref.watch(albumServiceProvider); - - bool isSuccess = await service.changeDescriptionAlbum(album, newAlbumDescription); - - if (isSuccess) { - state = state.copyWith(editDescriptionText: "", isEditAlbum: false); - - return true; - } - - state = state.copyWith(editDescriptionText: "", isEditAlbum: false); - - return false; - } -} - -final albumViewerProvider = StateNotifierProvider((ref) { - return AlbumViewerNotifier(ref); -}); diff --git a/mobile/lib/providers/album/current_album.provider.dart b/mobile/lib/providers/album/current_album.provider.dart deleted file mode 100644 index bd22c7a7cd..0000000000 --- a/mobile/lib/providers/album/current_album.provider.dart +++ /dev/null @@ -1,15 +0,0 @@ -import 'package:immich_mobile/entities/album.entity.dart'; -import 'package:riverpod_annotation/riverpod_annotation.dart'; - -part 'current_album.provider.g.dart'; - -@riverpod -class CurrentAlbum extends _$CurrentAlbum { - @override - Album? build() => null; - - void set(Album? a) => state = a; -} - -/// Mock class for testing -abstract class CurrentAlbumInternal extends _$CurrentAlbum {} diff --git a/mobile/lib/providers/album/suggested_shared_users.provider.dart b/mobile/lib/providers/album/suggested_shared_users.provider.dart deleted file mode 100644 index 51146748c7..0000000000 --- a/mobile/lib/providers/album/suggested_shared_users.provider.dart +++ /dev/null @@ -1,14 +0,0 @@ -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/domain/models/user.model.dart'; -import 'package:immich_mobile/domain/services/user.service.dart'; -import 'package:immich_mobile/providers/infrastructure/user.provider.dart'; -import 'package:immich_mobile/providers/user.provider.dart'; - -final otherUsersProvider = FutureProvider.autoDispose>((ref) async { - UserService userService = ref.watch(userServiceProvider); - final currentUser = ref.watch(currentUserProvider); - - final allUsers = await userService.getAll(); - allUsers.removeWhere((u) => currentUser?.id == u.id); - return allUsers; -}); diff --git a/mobile/lib/providers/app_life_cycle.provider.dart b/mobile/lib/providers/app_life_cycle.provider.dart index 68007f283a..a5f67215a8 100644 --- a/mobile/lib/providers/app_life_cycle.provider.dart +++ b/mobile/lib/providers/app_life_cycle.provider.dart @@ -5,28 +5,17 @@ import 'package:immich_mobile/domain/models/store.model.dart'; import 'package:immich_mobile/domain/services/log.service.dart'; import 'package:immich_mobile/entities/store.entity.dart'; import 'package:immich_mobile/extensions/platform_extensions.dart'; -import 'package:immich_mobile/models/backup/backup_state.model.dart'; -import 'package:immich_mobile/providers/album/album.provider.dart'; import 'package:immich_mobile/providers/app_settings.provider.dart'; -import 'package:immich_mobile/providers/asset.provider.dart'; import 'package:immich_mobile/providers/auth.provider.dart'; import 'package:immich_mobile/providers/background_sync.provider.dart'; -import 'package:immich_mobile/providers/backup/backup.provider.dart'; import 'package:immich_mobile/providers/backup/drift_backup.provider.dart'; -import 'package:immich_mobile/providers/backup/ios_background_settings.provider.dart'; -import 'package:immich_mobile/providers/backup/manual_upload.provider.dart'; import 'package:immich_mobile/providers/gallery_permission.provider.dart'; import 'package:immich_mobile/providers/infrastructure/platform.provider.dart'; -import 'package:immich_mobile/providers/memory.provider.dart'; import 'package:immich_mobile/providers/notification_permission.provider.dart'; import 'package:immich_mobile/providers/server_info.provider.dart'; -import 'package:immich_mobile/providers/tab.provider.dart'; import 'package:immich_mobile/providers/websocket.provider.dart'; import 'package:immich_mobile/services/app_settings.service.dart'; -import 'package:immich_mobile/services/background.service.dart'; -import 'package:isar/isar.dart'; import 'package:logging/logging.dart'; -import 'package:permission_handler/permission_handler.dart'; enum AppLifeCycleEnum { active, inactive, paused, resumed, detached, hidden } @@ -87,43 +76,15 @@ class AppLifeCycleNotifier extends StateNotifier { final endpoint = await _ref.read(authProvider.notifier).setOpenApiServiceEndpoint(); _log.info("Using server URL: $endpoint"); - if (!Store.isBetaTimelineEnabled) { - final permission = _ref.watch(galleryPermissionNotifier); - if (permission.isGranted || permission.isLimited) { - await _ref.read(backupProvider.notifier).resumeBackup(); - await _ref.read(backgroundServiceProvider).resumeServiceIfEnabled(); - } - } - await _ref.read(serverInfoProvider.notifier).getServerVersion(); } - if (!Store.isBetaTimelineEnabled) { - switch (_ref.read(tabProvider)) { - case TabEnum.home: - await _ref.read(assetProvider.notifier).getAllAsset(); - - case TabEnum.albums: - await _ref.read(albumProvider.notifier).refreshRemoteAlbums(); - - case TabEnum.library: - case TabEnum.search: - break; - } - } else { - _ref.read(websocketProvider.notifier).connect(); - await _handleBetaTimelineResume(); - } + _ref.read(websocketProvider.notifier).connect(); + await _handleBetaTimelineResume(); await _ref.read(notificationPermissionProvider.notifier).getNotificationPermission(); await _ref.read(galleryPermissionNotifier.notifier).getGalleryPermissionStatus(); - - if (!Store.isBetaTimelineEnabled) { - await _ref.read(iOSBackgroundSettingsProvider.notifier).refresh(); - - _ref.invalidate(memoryFutureProvider); - } } Future _safeRun(Future action, String debugName) async { @@ -139,7 +100,6 @@ class AppLifeCycleNotifier extends StateNotifier { } Future _handleBetaTimelineResume() async { - _ref.read(backupProvider.notifier).cancelBackup(); unawaited(_ref.read(backgroundWorkerLockServiceProvider).lock()); // Give isolates time to complete any ongoing database transactions @@ -218,9 +178,7 @@ class AppLifeCycleNotifier extends StateNotifier { _pauseOperation = Completer(); try { - if (Store.isBetaTimelineEnabled) { - unawaited(_ref.read(backgroundWorkerLockServiceProvider).unlock()); - } + unawaited(_ref.read(backgroundWorkerLockServiceProvider).unlock()); await _performPause(); } catch (e, stackTrace) { _log.severe("Error during app pause", e, stackTrace); @@ -234,14 +192,7 @@ class AppLifeCycleNotifier extends StateNotifier { Future _performPause() { if (_ref.read(authProvider).isAuthenticated) { - if (!Store.isBetaTimelineEnabled) { - // Do not cancel backup if manual upload is in progress - if (_ref.read(backupProvider.notifier).backupProgress != BackUpProgressEnum.manualInProgress) { - _ref.read(backupProvider.notifier).cancelBackup(); - } - } else { - _ref.read(driftBackupProvider.notifier).stopForegroundBackup(); - } + _ref.read(driftBackupProvider.notifier).stopForegroundBackup(); _ref.read(websocketProvider.notifier).disconnect(); } @@ -252,31 +203,12 @@ class AppLifeCycleNotifier extends StateNotifier { Future handleAppDetached() async { state = AppLifeCycleEnum.detached; - if (Store.isBetaTimelineEnabled) { - unawaited(_ref.read(backgroundWorkerLockServiceProvider).unlock()); - } + unawaited(_ref.read(backgroundWorkerLockServiceProvider).unlock()); // Flush logs before closing database try { await LogService.I.flush(); } catch (_) {} - - // Close Isar database safely - try { - final isar = Isar.getInstance(); - if (isar != null && isar.isOpen) { - await isar.close(); - } - } catch (_) {} - - if (Store.isBetaTimelineEnabled) { - return; - } - - // no guarantee this is called at all - try { - _ref.read(manualUploadProvider.notifier).cancelBackup(); - } catch (_) {} } void handleAppHidden() { diff --git a/mobile/lib/providers/asset.provider.dart b/mobile/lib/providers/asset.provider.dart deleted file mode 100644 index d5a4e42b74..0000000000 --- a/mobile/lib/providers/asset.provider.dart +++ /dev/null @@ -1,182 +0,0 @@ -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/constants/enums.dart'; -import 'package:immich_mobile/domain/models/store.model.dart'; -import 'package:immich_mobile/domain/services/user.service.dart'; -import 'package:immich_mobile/entities/asset.entity.dart'; -import 'package:immich_mobile/entities/store.entity.dart'; -import 'package:immich_mobile/providers/infrastructure/user.provider.dart'; -import 'package:immich_mobile/providers/memory.provider.dart'; -import 'package:immich_mobile/services/album.service.dart'; -import 'package:immich_mobile/services/asset.service.dart'; -import 'package:immich_mobile/services/etag.service.dart'; -import 'package:immich_mobile/services/exif.service.dart'; -import 'package:immich_mobile/services/sync.service.dart'; -import 'package:logging/logging.dart'; -import 'package:immich_mobile/utils/debug_print.dart'; - -final assetProvider = StateNotifierProvider((ref) { - return AssetNotifier( - ref.watch(assetServiceProvider), - ref.watch(albumServiceProvider), - ref.watch(userServiceProvider), - ref.watch(syncServiceProvider), - ref.watch(etagServiceProvider), - ref.watch(exifServiceProvider), - ref, - ); -}); - -class AssetNotifier extends StateNotifier { - final AssetService _assetService; - final AlbumService _albumService; - final UserService _userService; - final SyncService _syncService; - final ETagService _etagService; - final ExifService _exifService; - final Ref _ref; - final log = Logger('AssetNotifier'); - bool _getAllAssetInProgress = false; - bool _deleteInProgress = false; - - AssetNotifier( - this._assetService, - this._albumService, - this._userService, - this._syncService, - this._etagService, - this._exifService, - this._ref, - ) : super(false); - - Future getAllAsset({bool clear = false}) async { - if (_getAllAssetInProgress || _deleteInProgress) { - // guard against multiple calls to this method while it's still working - return; - } - final stopwatch = Stopwatch()..start(); - try { - _getAllAssetInProgress = true; - state = true; - if (clear) { - await clearAllAssets(); - log.info("Manual refresh requested, cleared assets and albums from db"); - } - final users = await _syncService.getUsersFromServer(); - bool changedUsers = false; - if (users != null) { - changedUsers = await _syncService.syncUsersFromServer(users); - } - final bool newRemote = await _assetService.refreshRemoteAssets(); - final bool newLocal = await _albumService.refreshDeviceAlbums(); - dPrint(() => "changedUsers: $changedUsers, newRemote: $newRemote, newLocal: $newLocal"); - if (newRemote) { - _ref.invalidate(memoryFutureProvider); - } - - log.info("Load assets: ${stopwatch.elapsedMilliseconds}ms"); - } catch (error) { - // If there is error in getting the remote assets, still showing the new local assets - await _albumService.refreshDeviceAlbums(); - } finally { - _getAllAssetInProgress = false; - if (mounted) { - state = false; - } - } - } - - Future clearAllAssets() async { - await Store.delete(StoreKey.assetETag); - await Future.wait([ - _assetService.clearTable(), - _exifService.clearTable(), - _albumService.clearTable(), - _userService.deleteAll(), - _etagService.clearTable(), - ]); - } - - Future onNewAssetUploaded(Asset newAsset) async { - // eTag on device is not valid after partially modifying the assets - await Store.delete(StoreKey.assetETag); - await _syncService.syncNewAssetToDb(newAsset); - } - - Future deleteLocalAssets(List assets) async { - _deleteInProgress = true; - state = true; - try { - await _assetService.deleteLocalAssets(assets); - return true; - } catch (error) { - log.severe("Failed to delete local assets", error); - return false; - } finally { - _deleteInProgress = false; - state = false; - } - } - - /// Delete remote asset only - /// - /// Default behavior is trashing the asset - Future deleteRemoteAssets(Iterable deleteAssets, {bool shouldDeletePermanently = false}) async { - _deleteInProgress = true; - state = true; - try { - await _assetService.deleteRemoteAssets(deleteAssets, shouldDeletePermanently: shouldDeletePermanently); - return true; - } catch (error) { - log.severe("Failed to delete remote assets", error); - return false; - } finally { - _deleteInProgress = false; - state = false; - } - } - - Future deleteAssets(Iterable deleteAssets, {bool force = false}) async { - _deleteInProgress = true; - state = true; - try { - await _assetService.deleteAssets(deleteAssets, shouldDeletePermanently: force); - return true; - } catch (error) { - log.severe("Failed to delete assets", error); - return false; - } finally { - _deleteInProgress = false; - state = false; - } - } - - Future toggleFavorite(List assets, [bool? status]) { - status ??= !assets.every((a) => a.isFavorite); - return _assetService.changeFavoriteStatus(assets, status); - } - - Future toggleArchive(List assets, [bool? status]) { - status ??= !assets.every((a) => a.isArchived); - return _assetService.changeArchiveStatus(assets, status); - } - - Future setLockedView(List selection, AssetVisibilityEnum visibility) { - return _assetService.setVisibility(selection, visibility); - } -} - -final assetDetailProvider = StreamProvider.autoDispose.family((ref, asset) async* { - final assetService = ref.watch(assetServiceProvider); - yield await assetService.loadExif(asset); - - await for (final asset in assetService.watchAsset(asset.id)) { - if (asset != null) { - yield await ref.watch(assetServiceProvider).loadExif(asset); - } - } -}); - -final assetWatcher = StreamProvider.autoDispose.family((ref, asset) { - final assetService = ref.watch(assetServiceProvider); - return assetService.watchAsset(asset.id, fireImmediately: true); -}); diff --git a/mobile/lib/providers/asset_viewer/asset_people.provider.dart b/mobile/lib/providers/asset_viewer/asset_people.provider.dart deleted file mode 100644 index e2227920c7..0000000000 --- a/mobile/lib/providers/asset_viewer/asset_people.provider.dart +++ /dev/null @@ -1,49 +0,0 @@ -import 'package:immich_mobile/entities/asset.entity.dart'; -import 'package:immich_mobile/services/asset.service.dart'; -import 'package:logging/logging.dart'; -import 'package:openapi/api.dart'; -import 'package:riverpod_annotation/riverpod_annotation.dart'; - -part 'asset_people.provider.g.dart'; - -/// Maintains the list of people for an asset. -@riverpod -class AssetPeopleNotifier extends _$AssetPeopleNotifier { - final log = Logger('AssetPeopleNotifier'); - - @override - Future> build(Asset asset) async { - if (!asset.isRemote) { - return []; - } - - final list = await ref.watch(assetServiceProvider).getRemotePeopleOfAsset(asset.remoteId!); - if (list == null) { - return []; - } - - // explicitly a sorted slice to make it deterministic - // named people will be at the beginning, and names are sorted - // ascendingly - list.sort((a, b) { - final aNotEmpty = a.name.isNotEmpty; - final bNotEmpty = b.name.isNotEmpty; - if (aNotEmpty && !bNotEmpty) { - return -1; - } else if (!aNotEmpty && bNotEmpty) { - return 1; - } else if (!aNotEmpty && !bNotEmpty) { - return 0; - } - - return a.name.compareTo(b.name); - }); - return list; - } - - Future refresh() async { - // invalidate the state – this way we don't have to - // duplicate the code from build. - ref.invalidateSelf(); - } -} diff --git a/mobile/lib/providers/asset_viewer/asset_people.provider.g.dart b/mobile/lib/providers/asset_viewer/asset_people.provider.g.dart deleted file mode 100644 index 031a70e0d9..0000000000 --- a/mobile/lib/providers/asset_viewer/asset_people.provider.g.dart +++ /dev/null @@ -1,192 +0,0 @@ -// GENERATED CODE - DO NOT MODIFY BY HAND - -part of 'asset_people.provider.dart'; - -// ************************************************************************** -// RiverpodGenerator -// ************************************************************************** - -String _$assetPeopleNotifierHash() => - r'9835b180984a750c91e923e7b64dbda94f6d7574'; - -/// Copied from Dart SDK -class _SystemHash { - _SystemHash._(); - - static int combine(int hash, int value) { - // ignore: parameter_assignments - hash = 0x1fffffff & (hash + value); - // ignore: parameter_assignments - hash = 0x1fffffff & (hash + ((0x0007ffff & hash) << 10)); - return hash ^ (hash >> 6); - } - - static int finish(int hash) { - // ignore: parameter_assignments - hash = 0x1fffffff & (hash + ((0x03ffffff & hash) << 3)); - // ignore: parameter_assignments - hash = hash ^ (hash >> 11); - return 0x1fffffff & (hash + ((0x00003fff & hash) << 15)); - } -} - -abstract class _$AssetPeopleNotifier - extends - BuildlessAutoDisposeAsyncNotifier> { - late final Asset asset; - - FutureOr> build(Asset asset); -} - -/// Maintains the list of people for an asset. -/// -/// Copied from [AssetPeopleNotifier]. -@ProviderFor(AssetPeopleNotifier) -const assetPeopleNotifierProvider = AssetPeopleNotifierFamily(); - -/// Maintains the list of people for an asset. -/// -/// Copied from [AssetPeopleNotifier]. -class AssetPeopleNotifierFamily - extends Family>> { - /// Maintains the list of people for an asset. - /// - /// Copied from [AssetPeopleNotifier]. - const AssetPeopleNotifierFamily(); - - /// Maintains the list of people for an asset. - /// - /// Copied from [AssetPeopleNotifier]. - AssetPeopleNotifierProvider call(Asset asset) { - return AssetPeopleNotifierProvider(asset); - } - - @override - AssetPeopleNotifierProvider getProviderOverride( - covariant AssetPeopleNotifierProvider provider, - ) { - return call(provider.asset); - } - - static const Iterable? _dependencies = null; - - @override - Iterable? get dependencies => _dependencies; - - static const Iterable? _allTransitiveDependencies = null; - - @override - Iterable? get allTransitiveDependencies => - _allTransitiveDependencies; - - @override - String? get name => r'assetPeopleNotifierProvider'; -} - -/// Maintains the list of people for an asset. -/// -/// Copied from [AssetPeopleNotifier]. -class AssetPeopleNotifierProvider - extends - AutoDisposeAsyncNotifierProviderImpl< - AssetPeopleNotifier, - List - > { - /// Maintains the list of people for an asset. - /// - /// Copied from [AssetPeopleNotifier]. - AssetPeopleNotifierProvider(Asset asset) - : this._internal( - () => AssetPeopleNotifier()..asset = asset, - from: assetPeopleNotifierProvider, - name: r'assetPeopleNotifierProvider', - debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product') - ? null - : _$assetPeopleNotifierHash, - dependencies: AssetPeopleNotifierFamily._dependencies, - allTransitiveDependencies: - AssetPeopleNotifierFamily._allTransitiveDependencies, - asset: asset, - ); - - AssetPeopleNotifierProvider._internal( - super._createNotifier, { - required super.name, - required super.dependencies, - required super.allTransitiveDependencies, - required super.debugGetCreateSourceHash, - required super.from, - required this.asset, - }) : super.internal(); - - final Asset asset; - - @override - FutureOr> runNotifierBuild( - covariant AssetPeopleNotifier notifier, - ) { - return notifier.build(asset); - } - - @override - Override overrideWith(AssetPeopleNotifier Function() create) { - return ProviderOverride( - origin: this, - override: AssetPeopleNotifierProvider._internal( - () => create()..asset = asset, - from: from, - name: null, - dependencies: null, - allTransitiveDependencies: null, - debugGetCreateSourceHash: null, - asset: asset, - ), - ); - } - - @override - AutoDisposeAsyncNotifierProviderElement< - AssetPeopleNotifier, - List - > - createElement() { - return _AssetPeopleNotifierProviderElement(this); - } - - @override - bool operator ==(Object other) { - return other is AssetPeopleNotifierProvider && other.asset == asset; - } - - @override - int get hashCode { - var hash = _SystemHash.combine(0, runtimeType.hashCode); - hash = _SystemHash.combine(hash, asset.hashCode); - - return _SystemHash.finish(hash); - } -} - -@Deprecated('Will be removed in 3.0. Use Ref instead') -// ignore: unused_element -mixin AssetPeopleNotifierRef - on AutoDisposeAsyncNotifierProviderRef> { - /// The parameter `asset` of this provider. - Asset get asset; -} - -class _AssetPeopleNotifierProviderElement - extends - AutoDisposeAsyncNotifierProviderElement< - AssetPeopleNotifier, - List - > - with AssetPeopleNotifierRef { - _AssetPeopleNotifierProviderElement(super.provider); - - @override - Asset get asset => (origin as AssetPeopleNotifierProvider).asset; -} - -// ignore_for_file: type=lint -// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member, deprecated_member_use_from_same_package diff --git a/mobile/lib/providers/asset_viewer/asset_stack.provider.dart b/mobile/lib/providers/asset_viewer/asset_stack.provider.dart deleted file mode 100644 index 8772e3d0cb..0000000000 --- a/mobile/lib/providers/asset_viewer/asset_stack.provider.dart +++ /dev/null @@ -1,42 +0,0 @@ -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/entities/asset.entity.dart'; -import 'package:immich_mobile/services/asset.service.dart'; -import 'package:riverpod_annotation/riverpod_annotation.dart'; - -part 'asset_stack.provider.g.dart'; - -class AssetStackNotifier extends StateNotifier> { - final AssetService assetService; - final String _stackId; - - AssetStackNotifier(this.assetService, this._stackId) : super([]) { - _fetchStack(_stackId); - } - - void _fetchStack(String stackId) async { - if (!mounted) { - return; - } - - final stack = await assetService.getStackAssets(stackId); - if (stack.isNotEmpty) { - state = stack; - } - } - - void removeChild(int index) { - if (index < state.length) { - state.removeAt(index); - state = List.from(state); - } - } -} - -final assetStackStateProvider = StateNotifierProvider.autoDispose.family, String>( - (ref, stackId) => AssetStackNotifier(ref.watch(assetServiceProvider), stackId), -); - -@riverpod -int assetStackIndex(Ref _) { - return -1; -} diff --git a/mobile/lib/providers/asset_viewer/asset_stack.provider.g.dart b/mobile/lib/providers/asset_viewer/asset_stack.provider.g.dart deleted file mode 100644 index dcf82cdebd..0000000000 --- a/mobile/lib/providers/asset_viewer/asset_stack.provider.g.dart +++ /dev/null @@ -1,27 +0,0 @@ -// GENERATED CODE - DO NOT MODIFY BY HAND - -part of 'asset_stack.provider.dart'; - -// ************************************************************************** -// RiverpodGenerator -// ************************************************************************** - -String _$assetStackIndexHash() => r'086ddb782e3eb38b80d755666fe35be8fe7322d7'; - -/// See also [assetStackIndex]. -@ProviderFor(assetStackIndex) -final assetStackIndexProvider = AutoDisposeProvider.internal( - assetStackIndex, - name: r'assetStackIndexProvider', - debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product') - ? null - : _$assetStackIndexHash, - dependencies: null, - allTransitiveDependencies: null, -); - -@Deprecated('Will be removed in 3.0. Use Ref instead') -// ignore: unused_element -typedef AssetStackIndexRef = AutoDisposeProviderRef; -// ignore_for_file: type=lint -// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member, deprecated_member_use_from_same_package diff --git a/mobile/lib/providers/asset_viewer/current_asset.provider.dart b/mobile/lib/providers/asset_viewer/current_asset.provider.dart deleted file mode 100644 index 0e25660ab0..0000000000 --- a/mobile/lib/providers/asset_viewer/current_asset.provider.dart +++ /dev/null @@ -1,15 +0,0 @@ -import 'package:immich_mobile/entities/asset.entity.dart'; -import 'package:riverpod_annotation/riverpod_annotation.dart'; - -part 'current_asset.provider.g.dart'; - -@riverpod -class CurrentAsset extends _$CurrentAsset { - @override - Asset? build() => null; - - void set(Asset? a) => state = a; -} - -/// Mock class for testing -abstract class CurrentAssetInternal extends _$CurrentAsset {} diff --git a/mobile/lib/providers/asset_viewer/current_asset.provider.g.dart b/mobile/lib/providers/asset_viewer/current_asset.provider.g.dart deleted file mode 100644 index e0d8d47d3a..0000000000 --- a/mobile/lib/providers/asset_viewer/current_asset.provider.g.dart +++ /dev/null @@ -1,26 +0,0 @@ -// GENERATED CODE - DO NOT MODIFY BY HAND - -part of 'current_asset.provider.dart'; - -// ************************************************************************** -// RiverpodGenerator -// ************************************************************************** - -String _$currentAssetHash() => r'2def10ea594152c984ae2974d687ab6856d7bdd0'; - -/// See also [CurrentAsset]. -@ProviderFor(CurrentAsset) -final currentAssetProvider = - AutoDisposeNotifierProvider.internal( - CurrentAsset.new, - name: r'currentAssetProvider', - debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product') - ? null - : _$currentAssetHash, - dependencies: null, - allTransitiveDependencies: null, - ); - -typedef _$CurrentAsset = AutoDisposeNotifier; -// ignore_for_file: type=lint -// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member, deprecated_member_use_from_same_package diff --git a/mobile/lib/providers/asset_viewer/download.provider.dart b/mobile/lib/providers/asset_viewer/download.provider.dart index a461d5766a..25db76b077 100644 --- a/mobile/lib/providers/asset_viewer/download.provider.dart +++ b/mobile/lib/providers/asset_viewer/download.provider.dart @@ -1,26 +1,15 @@ import 'dart:async'; import 'package:background_downloader/background_downloader.dart'; -import 'package:easy_localization/easy_localization.dart'; -import 'package:flutter/material.dart'; -import 'package:fluttertoast/fluttertoast.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/entities/asset.entity.dart'; -import 'package:immich_mobile/extensions/build_context_extensions.dart'; import 'package:immich_mobile/models/download/download_state.model.dart'; import 'package:immich_mobile/models/download/livephotos_medatada.model.dart'; -import 'package:immich_mobile/services/album.service.dart'; import 'package:immich_mobile/services/download.service.dart'; -import 'package:immich_mobile/services/share.service.dart'; -import 'package:immich_mobile/widgets/common/immich_toast.dart'; -import 'package:immich_mobile/widgets/common/share_dialog.dart'; class DownloadStateNotifier extends StateNotifier { final DownloadService _downloadService; - final ShareService _shareService; - final AlbumService _albumService; - DownloadStateNotifier(this._downloadService, this._shareService, this._albumService) + DownloadStateNotifier(this._downloadService) : super( const DownloadState( downloadStatus: TaskStatus.complete, @@ -132,18 +121,9 @@ class DownloadStateNotifier extends StateNotifier { if (state.taskProgress.isEmpty) { state = state.copyWith(showProgress: false); } - _albumService.refreshDeviceAlbums(); }); } - Future> downloadAllAsset(List assets) async { - return await _downloadService.downloadAll(assets); - } - - void downloadAsset(Asset asset) async { - await _downloadService.download(asset); - } - void cancelDownload(String id) async { final isCanceled = await _downloadService.cancelDownload(id); @@ -159,36 +139,8 @@ class DownloadStateNotifier extends StateNotifier { state = state.copyWith(showProgress: false); } } - - void shareAsset(Asset asset, BuildContext context) async { - unawaited( - showDialog( - context: context, - builder: (BuildContext buildContext) { - _shareService.shareAsset(asset, context).then((bool status) { - if (!status) { - ImmichToast.show( - context: context, - msg: 'image_viewer_page_state_provider_share_error'.tr(), - toastType: ToastType.error, - gravity: ToastGravity.BOTTOM, - ); - } - buildContext.pop(); - }); - return const ShareDialog(); - }, - barrierDismissible: false, - useRootNavigator: false, - ), - ); - } } final downloadStateProvider = StateNotifierProvider( - ((ref) => DownloadStateNotifier( - ref.watch(downloadServiceProvider), - ref.watch(shareServiceProvider), - ref.watch(albumServiceProvider), - )), + ((ref) => DownloadStateNotifier(ref.watch(downloadServiceProvider))), ); diff --git a/mobile/lib/providers/asset_viewer/render_list_status_provider.dart b/mobile/lib/providers/asset_viewer/render_list_status_provider.dart deleted file mode 100644 index 189ac85452..0000000000 --- a/mobile/lib/providers/asset_viewer/render_list_status_provider.dart +++ /dev/null @@ -1,19 +0,0 @@ -import 'package:hooks_riverpod/hooks_riverpod.dart'; - -enum RenderListStatusEnum { complete, empty, error, loading } - -final renderListStatusProvider = StateNotifierProvider((ref) { - return RenderListStatus(ref); -}); - -class RenderListStatus extends StateNotifier { - RenderListStatus(this.ref) : super(RenderListStatusEnum.complete); - - final Ref ref; - - RenderListStatusEnum get status => state; - - set status(RenderListStatusEnum value) { - state = value; - } -} diff --git a/mobile/lib/providers/backup/backup.provider.dart b/mobile/lib/providers/backup/backup.provider.dart index 5f3ad3d058..a6dc272313 100644 --- a/mobile/lib/providers/backup/backup.provider.dart +++ b/mobile/lib/providers/backup/backup.provider.dart @@ -1,672 +1,23 @@ import 'dart:async'; -import 'dart:io'; -import 'package:collection/collection.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/domain/models/store.model.dart'; -import 'package:immich_mobile/entities/album.entity.dart'; -import 'package:immich_mobile/entities/backup_album.entity.dart'; -import 'package:immich_mobile/entities/store.entity.dart'; -import 'package:immich_mobile/models/auth/auth_state.model.dart'; -import 'package:immich_mobile/models/backup/available_album.model.dart'; -import 'package:immich_mobile/models/backup/backup_candidate.model.dart'; -import 'package:immich_mobile/models/backup/backup_state.model.dart'; -import 'package:immich_mobile/models/backup/current_upload_asset.model.dart'; -import 'package:immich_mobile/models/backup/error_upload_asset.model.dart'; -import 'package:immich_mobile/models/backup/success_upload_asset.model.dart'; import 'package:immich_mobile/models/server_info/server_disk_info.model.dart'; -import 'package:immich_mobile/providers/app_life_cycle.provider.dart'; -import 'package:immich_mobile/providers/auth.provider.dart'; -import 'package:immich_mobile/providers/backup/error_backup_list.provider.dart'; -import 'package:immich_mobile/providers/gallery_permission.provider.dart'; -import 'package:immich_mobile/repositories/album_media.repository.dart'; -import 'package:immich_mobile/repositories/backup.repository.dart'; -import 'package:immich_mobile/repositories/file_media.repository.dart'; -import 'package:immich_mobile/services/background.service.dart'; -import 'package:immich_mobile/services/backup.service.dart'; -import 'package:immich_mobile/services/backup_album.service.dart'; import 'package:immich_mobile/services/server_info.service.dart'; -import 'package:immich_mobile/utils/backup_progress.dart'; -import 'package:immich_mobile/utils/diff.dart'; -import 'package:logging/logging.dart'; -import 'package:permission_handler/permission_handler.dart'; -import 'package:photo_manager/photo_manager.dart' show PMProgressHandler; -import 'package:immich_mobile/utils/debug_print.dart'; -final backupProvider = StateNotifierProvider((ref) { - return BackupNotifier( - ref.watch(backupServiceProvider), - ref.watch(serverInfoServiceProvider), - ref.watch(authProvider), - ref.watch(backgroundServiceProvider), - ref.watch(galleryPermissionNotifier.notifier), - ref.watch(albumMediaRepositoryProvider), - ref.watch(fileMediaRepositoryProvider), - ref.watch(backupAlbumServiceProvider), - ref, - ); +final backupProvider = StateNotifierProvider((ref) { + return BackupNotifier(ref.watch(serverInfoServiceProvider)); }); -class BackupNotifier extends StateNotifier { - BackupNotifier( - this._backupService, - this._serverInfoService, - this._authState, - this._backgroundService, - this._galleryPermissionNotifier, - this._albumMediaRepository, - this._fileMediaRepository, - this._backupAlbumService, - this.ref, - ) : super( - BackUpState( - backupProgress: BackUpProgressEnum.idle, - allAssetsInDatabase: const [], - progressInPercentage: 0, - progressInFileSize: "0 B / 0 B", - progressInFileSpeed: 0, - progressInFileSpeeds: const [], - progressInFileSpeedUpdateTime: DateTime.now(), - progressInFileSpeedUpdateSentBytes: 0, - autoBackup: Store.get(StoreKey.autoBackup, false), - backgroundBackup: Store.get(StoreKey.backgroundBackup, false), - backupRequireWifi: Store.get(StoreKey.backupRequireWifi, true), - backupRequireCharging: Store.get(StoreKey.backupRequireCharging, false), - backupTriggerDelay: Store.get(StoreKey.backupTriggerDelay, 5000), - serverInfo: const ServerDiskInfo(diskAvailable: "0", diskSize: "0", diskUse: "0", diskUsagePercentage: 0), - availableAlbums: const [], - selectedBackupAlbums: const {}, - excludedBackupAlbums: const {}, - allUniqueAssets: const {}, - selectedAlbumsBackupAssetsIds: const {}, - currentUploadAsset: CurrentUploadAsset( - id: '...', - fileCreatedAt: DateTime.parse('2020-10-04'), - fileName: '...', - fileType: '...', - fileSize: 0, - iCloudAsset: false, - ), - iCloudDownloadProgress: 0.0, - ), - ); +class BackupNotifier extends StateNotifier { + BackupNotifier(this._serverInfoService) + : super(const ServerDiskInfo(diskAvailable: "0", diskSize: "0", diskUse: "0", diskUsagePercentage: 0)); - final log = Logger('BackupNotifier'); - final BackupService _backupService; final ServerInfoService _serverInfoService; - final AuthState _authState; - final BackgroundService _backgroundService; - final GalleryPermissionNotifier _galleryPermissionNotifier; - final AlbumMediaRepository _albumMediaRepository; - final FileMediaRepository _fileMediaRepository; - final BackupAlbumService _backupAlbumService; - final Ref ref; - Completer? _cancelToken; - - /// - /// UI INTERACTION - /// - /// Album selection - /// Due to the overlapping assets across multiple albums on the device - /// We have method to include and exclude albums - /// The total unique assets will be used for backing mechanism - /// - void addAlbumForBackup(AvailableAlbum album) { - if (state.excludedBackupAlbums.contains(album)) { - removeExcludedAlbumForBackup(album); - } - - state = state.copyWith(selectedBackupAlbums: {...state.selectedBackupAlbums, album}); - } - - void addExcludedAlbumForBackup(AvailableAlbum album) { - if (state.selectedBackupAlbums.contains(album)) { - removeAlbumForBackup(album); - } - state = state.copyWith(excludedBackupAlbums: {...state.excludedBackupAlbums, album}); - } - - void removeAlbumForBackup(AvailableAlbum album) { - Set currentSelectedAlbums = state.selectedBackupAlbums; - - currentSelectedAlbums.removeWhere((a) => a == album); - - state = state.copyWith(selectedBackupAlbums: currentSelectedAlbums); - } - - void removeExcludedAlbumForBackup(AvailableAlbum album) { - Set currentExcludedAlbums = state.excludedBackupAlbums; - - currentExcludedAlbums.removeWhere((a) => a == album); - - state = state.copyWith(excludedBackupAlbums: currentExcludedAlbums); - } - - Future backupAlbumSelectionDone() { - if (state.selectedBackupAlbums.isEmpty) { - // disable any backup - cancelBackup(); - setAutoBackup(false); - configureBackgroundBackup(enabled: false, onError: (msg) {}, onBatteryInfo: () {}); - } - return _updateBackupAssetCount(); - } - - void setAutoBackup(bool enabled) { - Store.put(StoreKey.autoBackup, enabled); - state = state.copyWith(autoBackup: enabled); - } - - void configureBackgroundBackup({ - bool? enabled, - bool? requireWifi, - bool? requireCharging, - int? triggerDelay, - required void Function(String msg) onError, - required void Function() onBatteryInfo, - }) async { - assert(enabled != null || requireWifi != null || requireCharging != null || triggerDelay != null); - final bool wasEnabled = state.backgroundBackup; - final bool wasWifi = state.backupRequireWifi; - final bool wasCharging = state.backupRequireCharging; - final int oldTriggerDelay = state.backupTriggerDelay; - state = state.copyWith( - backgroundBackup: enabled, - backupRequireWifi: requireWifi, - backupRequireCharging: requireCharging, - backupTriggerDelay: triggerDelay, - ); - - if (state.backgroundBackup) { - bool success = true; - if (!wasEnabled) { - if (!await _backgroundService.isIgnoringBatteryOptimizations()) { - onBatteryInfo(); - } - success &= await _backgroundService.enableService(immediate: true); - } - success &= - success && - await _backgroundService.configureService( - requireUnmetered: state.backupRequireWifi, - requireCharging: state.backupRequireCharging, - triggerUpdateDelay: state.backupTriggerDelay, - triggerMaxDelay: state.backupTriggerDelay * 10, - ); - if (success) { - await Store.put(StoreKey.backupRequireWifi, state.backupRequireWifi); - await Store.put(StoreKey.backupRequireCharging, state.backupRequireCharging); - await Store.put(StoreKey.backupTriggerDelay, state.backupTriggerDelay); - await Store.put(StoreKey.backgroundBackup, state.backgroundBackup); - } else { - state = state.copyWith( - backgroundBackup: wasEnabled, - backupRequireWifi: wasWifi, - backupRequireCharging: wasCharging, - backupTriggerDelay: oldTriggerDelay, - ); - onError("backup_controller_page_background_configure_error"); - } - } else { - final bool success = await _backgroundService.disableService(); - if (!success) { - state = state.copyWith(backgroundBackup: wasEnabled); - onError("backup_controller_page_background_configure_error"); - } - } - } - - /// - /// Get all album on the device - /// Get all selected and excluded album from the user's persistent storage - /// If this is the first time performing backup - set the default selected album to be - /// the one that has all assets (`Recent` on Android, `Recents` on iOS) - /// - Future _getBackupAlbumsInfo() async { - Stopwatch stopwatch = Stopwatch()..start(); - // Get all albums on the device - List availableAlbums = []; - List albums = await _albumMediaRepository.getAll(); - - // Map of id -> album for quick album lookup later on. - Map albumMap = {}; - - log.info('Found ${albums.length} local albums'); - - for (Album album in albums) { - AvailableAlbum availableAlbum = AvailableAlbum( - album: album, - assetCount: await ref.read(albumMediaRepositoryProvider).getAssetCount(album.localId!), - ); - - availableAlbums.add(availableAlbum); - - albumMap[album.localId!] = album; - } - state = state.copyWith(availableAlbums: availableAlbums); - - final List excludedBackupAlbums = await _backupAlbumService.getAllBySelection(BackupSelection.exclude); - final List selectedBackupAlbums = await _backupAlbumService.getAllBySelection(BackupSelection.select); - - final Set selectedAlbums = {}; - for (final BackupAlbum ba in selectedBackupAlbums) { - final albumAsset = albumMap[ba.id]; - - if (albumAsset != null) { - selectedAlbums.add( - AvailableAlbum( - album: albumAsset, - assetCount: await _albumMediaRepository.getAssetCount(albumAsset.localId!), - lastBackup: ba.lastBackup, - ), - ); - } else { - log.severe('Selected album not found'); - } - } - - final Set excludedAlbums = {}; - for (final BackupAlbum ba in excludedBackupAlbums) { - final albumAsset = albumMap[ba.id]; - - if (albumAsset != null) { - excludedAlbums.add( - AvailableAlbum( - album: albumAsset, - assetCount: await ref.read(albumMediaRepositoryProvider).getAssetCount(albumAsset.localId!), - lastBackup: ba.lastBackup, - ), - ); - } else { - log.severe('Excluded album not found'); - } - } - - state = state.copyWith(selectedBackupAlbums: selectedAlbums, excludedBackupAlbums: excludedAlbums); - - log.info("_getBackupAlbumsInfo: Found ${availableAlbums.length} available albums"); - dPrint(() => "_getBackupAlbumsInfo takes ${stopwatch.elapsedMilliseconds}ms"); - } - - /// - /// From all the selected and albums assets - /// Find the assets that are not overlapping between the two sets - /// Those assets are unique and are used as the total assets - /// - Future _updateBackupAssetCount() async { - // Save to persistent storage - await _updatePersistentAlbumsSelection(); - - final duplicatedAssetIds = await _backupService.getDuplicatedAssetIds(); - final Set assetsFromSelectedAlbums = {}; - final Set assetsFromExcludedAlbums = {}; - - for (final album in state.selectedBackupAlbums) { - final assetCount = await ref.read(albumMediaRepositoryProvider).getAssetCount(album.album.localId!); - - if (assetCount == 0) { - continue; - } - - final assets = await ref.read(albumMediaRepositoryProvider).getAssets(album.album.localId!); - - // Add album's name to the asset info - for (final asset in assets) { - List albumNames = [album.name]; - - final existingAsset = assetsFromSelectedAlbums.firstWhereOrNull((a) => a.asset.localId == asset.localId); - - if (existingAsset != null) { - albumNames.addAll(existingAsset.albumNames); - assetsFromSelectedAlbums.remove(existingAsset); - } - - assetsFromSelectedAlbums.add(BackupCandidate(asset: asset, albumNames: albumNames)); - } - } - - for (final album in state.excludedBackupAlbums) { - final assetCount = await ref.read(albumMediaRepositoryProvider).getAssetCount(album.album.localId!); - - if (assetCount == 0) { - continue; - } - - final assets = await ref.read(albumMediaRepositoryProvider).getAssets(album.album.localId!); - - for (final asset in assets) { - assetsFromExcludedAlbums.add(BackupCandidate(asset: asset, albumNames: [album.name])); - } - } - - final Set allUniqueAssets = assetsFromSelectedAlbums.difference(assetsFromExcludedAlbums); - - final allAssetsInDatabase = await _backupService.getDeviceBackupAsset(); - - if (allAssetsInDatabase == null) { - return; - } - - // Find asset that were backup from selected albums - final Set selectedAlbumsBackupAssets = Set.from(allUniqueAssets.map((e) => e.asset.localId)); - - selectedAlbumsBackupAssets.removeWhere((assetId) => !allAssetsInDatabase.contains(assetId)); - - // Remove duplicated asset from all unique assets - allUniqueAssets.removeWhere((candidate) => duplicatedAssetIds.contains(candidate.asset.localId)); - - if (allUniqueAssets.isEmpty) { - log.info("No assets are selected for back up"); - state = state.copyWith( - backupProgress: BackUpProgressEnum.idle, - allAssetsInDatabase: allAssetsInDatabase, - allUniqueAssets: {}, - selectedAlbumsBackupAssetsIds: selectedAlbumsBackupAssets, - ); - } else { - state = state.copyWith( - allAssetsInDatabase: allAssetsInDatabase, - allUniqueAssets: allUniqueAssets, - selectedAlbumsBackupAssetsIds: selectedAlbumsBackupAssets, - ); - } - } - - /// Get all necessary information for calculating the available albums, - /// which albums are selected or excluded - /// and then update the UI according to those information - Future getBackupInfo() async { - final isEnabled = await _backgroundService.isBackgroundBackupEnabled(); - - state = state.copyWith(backgroundBackup: isEnabled); - if (isEnabled != Store.get(StoreKey.backgroundBackup, !isEnabled)) { - await Store.put(StoreKey.backgroundBackup, isEnabled); - } - - if (state.backupProgress != BackUpProgressEnum.inBackground) { - await _getBackupAlbumsInfo(); - await updateDiskInfo(); - await _updateBackupAssetCount(); - } else { - log.warning("cannot get backup info - background backup is in progress!"); - } - } - - /// Save user selection of selected albums and excluded albums to database - Future _updatePersistentAlbumsSelection() async { - final epoch = DateTime.fromMillisecondsSinceEpoch(0, isUtc: true); - final selected = state.selectedBackupAlbums.map( - (e) => BackupAlbum(e.id, e.lastBackup ?? epoch, BackupSelection.select), - ); - final excluded = state.excludedBackupAlbums.map( - (e) => BackupAlbum(e.id, e.lastBackup ?? epoch, BackupSelection.exclude), - ); - final candidates = selected.followedBy(excluded).toList(); - candidates.sortBy((e) => e.id); - - final savedBackupAlbums = await _backupAlbumService.getAll(sort: BackupAlbumSort.id); - final List toDelete = []; - final List toUpsert = []; - - diffSortedListsSync( - savedBackupAlbums, - candidates, - compare: (BackupAlbum a, BackupAlbum b) => a.id.compareTo(b.id), - both: (BackupAlbum a, BackupAlbum b) { - b.lastBackup = a.lastBackup.isAfter(b.lastBackup) ? a.lastBackup : b.lastBackup; - toUpsert.add(b); - return true; - }, - onlyFirst: (BackupAlbum a) => toDelete.add(a.isarId), - onlySecond: (BackupAlbum b) => toUpsert.add(b), - ); - - await _backupAlbumService.deleteAll(toDelete); - await _backupAlbumService.updateAll(toUpsert); - } - - /// Invoke backup process - Future startBackupProcess() async { - dPrint(() => "Start backup process"); - assert(state.backupProgress == BackUpProgressEnum.idle); - state = state.copyWith(backupProgress: BackUpProgressEnum.inProgress); - - await getBackupInfo(); - - final hasPermission = _galleryPermissionNotifier.hasPermission; - if (hasPermission) { - await _fileMediaRepository.clearFileCache(); - - if (state.allUniqueAssets.isEmpty) { - log.info("No Asset On Device - Abort Backup Process"); - state = state.copyWith(backupProgress: BackUpProgressEnum.idle); - return; - } - - Set assetsWillBeBackup = Set.from(state.allUniqueAssets); - // Remove item that has already been backed up - for (final assetId in state.allAssetsInDatabase) { - assetsWillBeBackup.removeWhere((e) => e.asset.localId == assetId); - } - - if (assetsWillBeBackup.isEmpty) { - state = state.copyWith(backupProgress: BackUpProgressEnum.idle); - } - - // Perform Backup - _cancelToken?.complete(); - _cancelToken = Completer(); - - final pmProgressHandler = Platform.isIOS ? PMProgressHandler() : null; - - pmProgressHandler?.stream.listen((event) { - final double progress = event.progress; - state = state.copyWith(iCloudDownloadProgress: progress); - }); - - await _backupService.backupAsset( - assetsWillBeBackup, - _cancelToken!, - pmProgressHandler: pmProgressHandler, - onSuccess: _onAssetUploaded, - onProgress: _onUploadProgress, - onCurrentAsset: _onSetCurrentBackupAsset, - onError: _onBackupError, - ); - await notifyBackgroundServiceCanRun(); - } else { - await openAppSettings(); - } - } - - void setAvailableAlbums(availableAlbums) { - state = state.copyWith(availableAlbums: availableAlbums); - } - - void _onBackupError(ErrorUploadAsset errorAssetInfo) { - ref.watch(errorBackupListProvider.notifier).add(errorAssetInfo); - } - - void _onSetCurrentBackupAsset(CurrentUploadAsset currentUploadAsset) { - state = state.copyWith(currentUploadAsset: currentUploadAsset); - } - - void cancelBackup() { - if (state.backupProgress != BackUpProgressEnum.inProgress) { - notifyBackgroundServiceCanRun(); - } - _cancelToken?.complete(); - _cancelToken = null; - state = state.copyWith( - backupProgress: BackUpProgressEnum.idle, - progressInPercentage: 0.0, - progressInFileSize: "0 B / 0 B", - progressInFileSpeed: 0, - progressInFileSpeedUpdateTime: DateTime.now(), - progressInFileSpeedUpdateSentBytes: 0, - ); - } - - void _onAssetUploaded(SuccessUploadAsset result) async { - if (result.isDuplicate) { - state = state.copyWith( - allUniqueAssets: state.allUniqueAssets - .where((candidate) => candidate.asset.localId != result.candidate.asset.localId) - .toSet(), - ); - } else { - state = state.copyWith( - selectedAlbumsBackupAssetsIds: {...state.selectedAlbumsBackupAssetsIds, result.candidate.asset.localId!}, - allAssetsInDatabase: [...state.allAssetsInDatabase, result.candidate.asset.localId!], - ); - } - - if (state.allUniqueAssets.length - state.selectedAlbumsBackupAssetsIds.length == 0) { - final latestAssetBackup = state.allUniqueAssets - .map((candidate) => candidate.asset.fileModifiedAt) - .reduce((v, e) => e.isAfter(v) ? e : v); - state = state.copyWith( - selectedBackupAlbums: state.selectedBackupAlbums.map((e) => e.copyWith(lastBackup: latestAssetBackup)).toSet(), - excludedBackupAlbums: state.excludedBackupAlbums.map((e) => e.copyWith(lastBackup: latestAssetBackup)).toSet(), - backupProgress: BackUpProgressEnum.done, - progressInPercentage: 0.0, - progressInFileSize: "0 B / 0 B", - progressInFileSpeed: 0, - progressInFileSpeedUpdateTime: DateTime.now(), - progressInFileSpeedUpdateSentBytes: 0, - ); - await _updatePersistentAlbumsSelection(); - } - - await updateDiskInfo(); - } - - void _onUploadProgress(int sent, int total) { - double lastUploadSpeed = state.progressInFileSpeed; - List lastUploadSpeeds = state.progressInFileSpeeds.toList(); - DateTime lastUpdateTime = state.progressInFileSpeedUpdateTime; - int lastSentBytes = state.progressInFileSpeedUpdateSentBytes; - - final now = DateTime.now(); - final duration = now.difference(lastUpdateTime); - - // Keep the upload speed average span limited, to keep it somewhat relevant - if (lastUploadSpeeds.length > 10) { - lastUploadSpeeds.removeAt(0); - } - - if (duration.inSeconds > 0) { - lastUploadSpeeds.add(((sent - lastSentBytes) / duration.inSeconds).abs().roundToDouble()); - - lastUploadSpeed = lastUploadSpeeds.average.abs().roundToDouble(); - lastUpdateTime = now; - lastSentBytes = sent; - } - - state = state.copyWith( - progressInPercentage: (sent.toDouble() / total.toDouble() * 100), - progressInFileSize: humanReadableFileBytesProgress(sent, total), - progressInFileSpeed: lastUploadSpeed, - progressInFileSpeeds: lastUploadSpeeds, - progressInFileSpeedUpdateTime: lastUpdateTime, - progressInFileSpeedUpdateSentBytes: lastSentBytes, - ); - } Future updateDiskInfo() async { final diskInfo = await _serverInfoService.getDiskInfo(); - - // Update server info if (diskInfo != null) { - state = state.copyWith(serverInfo: diskInfo); + state = diskInfo; } } - - Future _resumeBackup() async { - // Check if user is login - final accessKey = Store.tryGet(StoreKey.accessToken); - - // User has been logged out return - if (accessKey == null || !_authState.isAuthenticated) { - log.info("[_resumeBackup] not authenticated - abort"); - return; - } - - // Check if this device is enable backup by the user - if (state.autoBackup) { - // check if backup is already in process - then return - if (state.backupProgress == BackUpProgressEnum.inProgress) { - log.info("[_resumeBackup] Auto Backup is already in progress - abort"); - return; - } - - if (state.backupProgress == BackUpProgressEnum.inBackground) { - log.info("[_resumeBackup] Background backup is running - abort"); - return; - } - - if (state.backupProgress == BackUpProgressEnum.manualInProgress) { - log.info("[_resumeBackup] Manual upload is running - abort"); - return; - } - - // Run backup - log.info("[_resumeBackup] Start back up"); - await startBackupProcess(); - } - return; - } - - Future resumeBackup() async { - final List selectedBackupAlbums = await _backupAlbumService.getAllBySelection(BackupSelection.select); - final List excludedBackupAlbums = await _backupAlbumService.getAllBySelection(BackupSelection.exclude); - Set selectedAlbums = state.selectedBackupAlbums; - Set excludedAlbums = state.excludedBackupAlbums; - if (selectedAlbums.isNotEmpty) { - selectedAlbums = _updateAlbumsBackupTime(selectedAlbums, selectedBackupAlbums); - } - - if (excludedAlbums.isNotEmpty) { - excludedAlbums = _updateAlbumsBackupTime(excludedAlbums, excludedBackupAlbums); - } - final BackUpProgressEnum previous = state.backupProgress; - state = state.copyWith( - backupProgress: BackUpProgressEnum.inBackground, - selectedBackupAlbums: selectedAlbums, - excludedBackupAlbums: excludedAlbums, - ); - // assumes the background service is currently running - // if true, waits until it has stopped to start the backup - final bool hasLock = await _backgroundService.acquireLock(); - if (hasLock) { - state = state.copyWith(backupProgress: previous); - } - return _resumeBackup(); - } - - Set _updateAlbumsBackupTime(Set albums, List backupAlbums) { - Set result = {}; - for (BackupAlbum ba in backupAlbums) { - try { - AvailableAlbum a = albums.firstWhere((e) => e.id == ba.id); - result.add(a.copyWith(lastBackup: ba.lastBackup)); - } on StateError { - log.severe("[_updateAlbumBackupTime] failed to find album in state", "State Error", StackTrace.current); - } - } - return result; - } - - Future notifyBackgroundServiceCanRun() async { - const allowedStates = [AppLifeCycleEnum.inactive, AppLifeCycleEnum.paused, AppLifeCycleEnum.detached]; - if (allowedStates.contains(ref.read(appStateProvider.notifier).state)) { - _backgroundService.releaseLock(); - } - } - - BackUpProgressEnum get backupProgress => state.backupProgress; - - void updateBackupProgress(BackUpProgressEnum backupProgress) { - state = state.copyWith(backupProgress: backupProgress); - } } diff --git a/mobile/lib/providers/backup/backup_verification.provider.dart b/mobile/lib/providers/backup/backup_verification.provider.dart deleted file mode 100644 index 50270e87ca..0000000000 --- a/mobile/lib/providers/backup/backup_verification.provider.dart +++ /dev/null @@ -1,101 +0,0 @@ -import 'dart:async'; - -import 'package:connectivity_plus/connectivity_plus.dart'; -import 'package:flutter/material.dart'; -import 'package:immich_mobile/entities/asset.entity.dart'; -import 'package:immich_mobile/providers/asset.provider.dart'; -import 'package:immich_mobile/providers/backup/backup.provider.dart'; -import 'package:immich_mobile/services/backup_verification.service.dart'; -import 'package:immich_mobile/widgets/common/confirm_dialog.dart'; -import 'package:immich_mobile/widgets/common/immich_toast.dart'; -import 'package:riverpod_annotation/riverpod_annotation.dart'; -import 'package:wakelock_plus/wakelock_plus.dart'; - -part 'backup_verification.provider.g.dart'; - -@riverpod -class BackupVerification extends _$BackupVerification { - @override - bool build() => false; - - void performBackupCheck(BuildContext context) async { - try { - state = true; - final backupState = ref.read(backupProvider); - - if (backupState.allUniqueAssets.length > backupState.selectedAlbumsBackupAssetsIds.length) { - if (context.mounted) { - ImmichToast.show( - context: context, - msg: "Backup all assets before starting this check!", - toastType: ToastType.error, - ); - } - return; - } - final connection = await Connectivity().checkConnectivity(); - if (!connection.contains(ConnectivityResult.wifi)) { - if (context.mounted) { - ImmichToast.show( - context: context, - msg: "Make sure to be connected to unmetered Wi-Fi", - toastType: ToastType.error, - ); - } - return; - } - unawaited(WakelockPlus.enable()); - - const limit = 100; - final toDelete = await ref.read(backupVerificationServiceProvider).findWronglyBackedUpAssets(limit: limit); - if (toDelete.isEmpty) { - if (context.mounted) { - ImmichToast.show( - context: context, - msg: "Did not find any corrupt asset backups!", - toastType: ToastType.success, - ); - } - } else { - if (context.mounted) { - await showDialog( - context: context, - builder: (ctx) => ConfirmDialog( - onOk: () => _performDeletion(context, toDelete), - title: "Corrupt backups!", - ok: "Delete", - content: - "Found ${toDelete.length} (max $limit at once) corrupt asset backups. " - "Run the check again to find more.\n" - "Do you want to delete the corrupt asset backups now?", - ), - ); - } - } - } finally { - unawaited(WakelockPlus.disable()); - state = false; - } - } - - Future _performDeletion(BuildContext context, List assets) async { - try { - state = true; - if (context.mounted) { - ImmichToast.show(context: context, msg: "Deleting ${assets.length} assets on the server..."); - } - await ref.read(assetProvider.notifier).deleteAssets(assets, force: true); - if (context.mounted) { - ImmichToast.show( - context: context, - msg: - "Deleted ${assets.length} assets on the server. " - "You can now start a manual backup", - toastType: ToastType.success, - ); - } - } finally { - state = false; - } - } -} diff --git a/mobile/lib/providers/backup/backup_verification.provider.g.dart b/mobile/lib/providers/backup/backup_verification.provider.g.dart deleted file mode 100644 index 13f6819fa7..0000000000 --- a/mobile/lib/providers/backup/backup_verification.provider.g.dart +++ /dev/null @@ -1,27 +0,0 @@ -// GENERATED CODE - DO NOT MODIFY BY HAND - -part of 'backup_verification.provider.dart'; - -// ************************************************************************** -// RiverpodGenerator -// ************************************************************************** - -String _$backupVerificationHash() => - r'b4b34909ed1af3f28877ea457d53a4a18b6417f8'; - -/// See also [BackupVerification]. -@ProviderFor(BackupVerification) -final backupVerificationProvider = - AutoDisposeNotifierProvider.internal( - BackupVerification.new, - name: r'backupVerificationProvider', - debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product') - ? null - : _$backupVerificationHash, - dependencies: null, - allTransitiveDependencies: null, - ); - -typedef _$BackupVerification = AutoDisposeNotifier; -// ignore_for_file: type=lint -// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member, deprecated_member_use_from_same_package diff --git a/mobile/lib/providers/backup/error_backup_list.provider.dart b/mobile/lib/providers/backup/error_backup_list.provider.dart deleted file mode 100644 index db116e4bb9..0000000000 --- a/mobile/lib/providers/backup/error_backup_list.provider.dart +++ /dev/null @@ -1,22 +0,0 @@ -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/models/backup/error_upload_asset.model.dart'; - -class ErrorBackupListNotifier extends StateNotifier> { - ErrorBackupListNotifier() : super({}); - - add(ErrorUploadAsset errorAsset) { - state = state.union({errorAsset}); - } - - remove(ErrorUploadAsset errorAsset) { - state = state.difference({errorAsset}); - } - - empty() { - state = {}; - } -} - -final errorBackupListProvider = StateNotifierProvider>( - (ref) => ErrorBackupListNotifier(), -); diff --git a/mobile/lib/providers/backup/ios_background_settings.provider.dart b/mobile/lib/providers/backup/ios_background_settings.provider.dart deleted file mode 100644 index 98d55882cc..0000000000 --- a/mobile/lib/providers/backup/ios_background_settings.provider.dart +++ /dev/null @@ -1,54 +0,0 @@ -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/services/background.service.dart'; - -class IOSBackgroundSettings { - final bool appRefreshEnabled; - final int numberOfBackgroundTasksQueued; - final DateTime? timeOfLastFetch; - final DateTime? timeOfLastProcessing; - - const IOSBackgroundSettings({ - required this.appRefreshEnabled, - required this.numberOfBackgroundTasksQueued, - this.timeOfLastFetch, - this.timeOfLastProcessing, - }); -} - -class IOSBackgroundSettingsNotifier extends StateNotifier { - final BackgroundService _service; - IOSBackgroundSettingsNotifier(this._service) : super(null); - - IOSBackgroundSettings? get settings => state; - - Future refresh() async { - final lastFetchTime = await _service.getIOSBackupLastRun(IosBackgroundTask.fetch); - final lastProcessingTime = await _service.getIOSBackupLastRun(IosBackgroundTask.processing); - int numberOfProcesses = await _service.getIOSBackupNumberOfProcesses(); - final appRefreshEnabled = await _service.getIOSBackgroundAppRefreshEnabled(); - - // If this is enabled and there are no background processes, - // the user just enabled app refresh in Settings. - // But we don't have any background services running, since it was disabled - // before. - if (await _service.isBackgroundBackupEnabled() && numberOfProcesses == 0) { - // We need to restart the background service - await _service.enableService(); - numberOfProcesses = await _service.getIOSBackupNumberOfProcesses(); - } - - final settings = IOSBackgroundSettings( - appRefreshEnabled: appRefreshEnabled, - numberOfBackgroundTasksQueued: numberOfProcesses, - timeOfLastFetch: lastFetchTime, - timeOfLastProcessing: lastProcessingTime, - ); - - state = settings; - return settings; - } -} - -final iOSBackgroundSettingsProvider = StateNotifierProvider( - (ref) => IOSBackgroundSettingsNotifier(ref.watch(backgroundServiceProvider)), -); diff --git a/mobile/lib/providers/backup/manual_upload.provider.dart b/mobile/lib/providers/backup/manual_upload.provider.dart deleted file mode 100644 index 40efcd7422..0000000000 --- a/mobile/lib/providers/backup/manual_upload.provider.dart +++ /dev/null @@ -1,391 +0,0 @@ -import 'dart:async'; -import 'dart:io'; - -import 'package:collection/collection.dart'; -import 'package:easy_localization/easy_localization.dart'; -import 'package:flutter/widgets.dart'; -import 'package:fluttertoast/fluttertoast.dart'; -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/entities/asset.entity.dart'; -import 'package:immich_mobile/entities/backup_album.entity.dart'; -import 'package:immich_mobile/models/backup/backup_candidate.model.dart'; -import 'package:immich_mobile/models/backup/backup_state.model.dart'; -import 'package:immich_mobile/models/backup/current_upload_asset.model.dart'; -import 'package:immich_mobile/models/backup/error_upload_asset.model.dart'; -import 'package:immich_mobile/models/backup/manual_upload_state.model.dart'; -import 'package:immich_mobile/models/backup/success_upload_asset.model.dart'; -import 'package:immich_mobile/providers/app_life_cycle.provider.dart'; -import 'package:immich_mobile/providers/app_settings.provider.dart'; -import 'package:immich_mobile/providers/backup/backup.provider.dart'; -import 'package:immich_mobile/providers/backup/error_backup_list.provider.dart'; -import 'package:immich_mobile/providers/gallery_permission.provider.dart'; -import 'package:immich_mobile/repositories/file_media.repository.dart'; -import 'package:immich_mobile/services/app_settings.service.dart'; -import 'package:immich_mobile/services/background.service.dart'; -import 'package:immich_mobile/services/backup.service.dart'; -import 'package:immich_mobile/services/backup_album.service.dart'; -import 'package:immich_mobile/services/local_notification.service.dart'; -import 'package:immich_mobile/utils/backup_progress.dart'; -import 'package:immich_mobile/utils/debug_print.dart'; -import 'package:immich_mobile/widgets/common/immich_toast.dart'; -import 'package:logging/logging.dart'; -import 'package:permission_handler/permission_handler.dart'; -import 'package:photo_manager/photo_manager.dart' show PMProgressHandler; - -final manualUploadProvider = StateNotifierProvider((ref) { - return ManualUploadNotifier( - ref.watch(localNotificationService), - ref.watch(backupProvider.notifier), - ref.watch(backupServiceProvider), - ref.watch(backupAlbumServiceProvider), - ref, - ); -}); - -class ManualUploadNotifier extends StateNotifier { - final Logger _log = Logger("ManualUploadNotifier"); - final LocalNotificationService _localNotificationService; - final BackupNotifier _backupProvider; - final BackupService _backupService; - final BackupAlbumService _backupAlbumService; - final Ref ref; - Completer? _cancelToken; - - ManualUploadNotifier( - this._localNotificationService, - this._backupProvider, - this._backupService, - this._backupAlbumService, - this.ref, - ) : super( - ManualUploadState( - progressInPercentage: 0, - progressInFileSize: "0 B / 0 B", - progressInFileSpeed: 0, - progressInFileSpeeds: const [], - progressInFileSpeedUpdateTime: DateTime.now(), - progressInFileSpeedUpdateSentBytes: 0, - currentUploadAsset: CurrentUploadAsset( - id: '...', - fileCreatedAt: DateTime.parse('2020-10-04'), - fileName: '...', - fileType: '...', - ), - totalAssetsToUpload: 0, - successfulUploads: 0, - currentAssetIndex: 0, - showDetailedNotification: false, - ), - ); - - String _lastPrintedDetailContent = ''; - String? _lastPrintedDetailTitle; - - static const notifyInterval = Duration(milliseconds: 500); - late final ThrottleProgressUpdate _throttledNotifiy = ThrottleProgressUpdate(_updateProgress, notifyInterval); - late final ThrottleProgressUpdate _throttledDetailNotify = ThrottleProgressUpdate( - _updateDetailProgress, - notifyInterval, - ); - - void _updateProgress(String? title, int progress, int total) { - // Guard against throttling calling this method after the upload is done - if (_backupProvider.backupProgress == BackUpProgressEnum.manualInProgress) { - _localNotificationService.showOrUpdateManualUploadStatus( - "backup_background_service_in_progress_notification".tr(), - formatAssetBackupProgress(state.currentAssetIndex, state.totalAssetsToUpload), - maxProgress: state.totalAssetsToUpload, - progress: state.currentAssetIndex, - showActions: true, - ); - } - } - - void _updateDetailProgress(String? title, int progress, int total) { - // Guard against throttling calling this method after the upload is done - if (_backupProvider.backupProgress == BackUpProgressEnum.manualInProgress) { - final String msg = total > 0 ? humanReadableBytesProgress(progress, total) : ""; - // only update if message actually differs (to stop many useless notification updates on large assets or slow connections) - if (msg != _lastPrintedDetailContent || title != _lastPrintedDetailTitle) { - _lastPrintedDetailContent = msg; - _lastPrintedDetailTitle = title; - _localNotificationService.showOrUpdateManualUploadStatus( - title ?? 'Uploading', - msg, - progress: total > 0 ? (progress * 1000) ~/ total : 0, - maxProgress: 1000, - isDetailed: true, - // Detailed noitifcation is displayed for Single asset uploads. Show actions for such case - showActions: state.totalAssetsToUpload == 1, - ); - } - } - } - - void _onAssetUploaded(SuccessUploadAsset result) { - state = state.copyWith(successfulUploads: state.successfulUploads + 1); - _backupProvider.updateDiskInfo(); - } - - void _onAssetUploadError(ErrorUploadAsset errorAssetInfo) { - ref.watch(errorBackupListProvider.notifier).add(errorAssetInfo); - } - - void _onProgress(int sent, int total) { - double lastUploadSpeed = state.progressInFileSpeed; - List lastUploadSpeeds = state.progressInFileSpeeds.toList(); - DateTime lastUpdateTime = state.progressInFileSpeedUpdateTime; - int lastSentBytes = state.progressInFileSpeedUpdateSentBytes; - - final now = DateTime.now(); - final duration = now.difference(lastUpdateTime); - - // Keep the upload speed average span limited, to keep it somewhat relevant - if (lastUploadSpeeds.length > 10) { - lastUploadSpeeds.removeAt(0); - } - - if (duration.inSeconds > 0) { - lastUploadSpeeds.add(((sent - lastSentBytes) / duration.inSeconds).abs().roundToDouble()); - - lastUploadSpeed = lastUploadSpeeds.average.abs().roundToDouble(); - lastUpdateTime = now; - lastSentBytes = sent; - } - - state = state.copyWith( - progressInPercentage: (sent.toDouble() / total.toDouble() * 100), - progressInFileSize: humanReadableFileBytesProgress(sent, total), - progressInFileSpeed: lastUploadSpeed, - progressInFileSpeeds: lastUploadSpeeds, - progressInFileSpeedUpdateTime: lastUpdateTime, - progressInFileSpeedUpdateSentBytes: lastSentBytes, - ); - - if (state.showDetailedNotification) { - final title = "backup_background_service_current_upload_notification".tr( - namedArgs: {'filename': state.currentUploadAsset.fileName}, - ); - _throttledDetailNotify(title: title, progress: sent, total: total); - } - } - - void _onSetCurrentBackupAsset(CurrentUploadAsset currentUploadAsset) { - state = state.copyWith(currentUploadAsset: currentUploadAsset, currentAssetIndex: state.currentAssetIndex + 1); - if (state.totalAssetsToUpload > 1) { - _throttledNotifiy(); - } - if (state.showDetailedNotification) { - _throttledDetailNotify.title = "backup_background_service_current_upload_notification".tr( - namedArgs: {'filename': currentUploadAsset.fileName}, - ); - _throttledDetailNotify.progress = 0; - _throttledDetailNotify.total = 0; - } - } - - Future _startUpload(Iterable allManualUploads) async { - bool hasErrors = false; - try { - _backupProvider.updateBackupProgress(BackUpProgressEnum.manualInProgress); - - if (ref.read(galleryPermissionNotifier.notifier).hasPermission) { - await ref.read(fileMediaRepositoryProvider).clearFileCache(); - - final allAssetsFromDevice = allManualUploads.where((e) => e.isLocal && !e.isRemote).toList(); - - if (allAssetsFromDevice.length != allManualUploads.length) { - _log.warning( - '[_startUpload] Refreshed upload list -> ${allManualUploads.length - allAssetsFromDevice.length} asset will not be uploaded', - ); - } - - final selectedBackupAlbums = await _backupAlbumService.getAllBySelection(BackupSelection.select); - final excludedBackupAlbums = await _backupAlbumService.getAllBySelection(BackupSelection.exclude); - - // Get candidates from selected albums and excluded albums - Set candidates = await _backupService.buildUploadCandidates( - selectedBackupAlbums, - excludedBackupAlbums, - useTimeFilter: false, - ); - - // Extrack candidate from allAssetsFromDevice - final uploadAssets = candidates.where( - (candidate) => - allAssetsFromDevice.firstWhereOrNull((asset) => asset.localId == candidate.asset.localId) != null, - ); - - if (uploadAssets.isEmpty) { - dPrint(() => "[_startUpload] No Assets to upload - Abort Process"); - _backupProvider.updateBackupProgress(BackUpProgressEnum.idle); - return false; - } - - state = state.copyWith( - progressInPercentage: 0, - progressInFileSize: "0 B / 0 B", - progressInFileSpeed: 0, - totalAssetsToUpload: uploadAssets.length, - successfulUploads: 0, - currentAssetIndex: 0, - currentUploadAsset: CurrentUploadAsset( - id: '...', - fileCreatedAt: DateTime.parse('2020-10-04'), - fileName: '...', - fileType: '...', - ), - ); - // Reset Error List - ref.watch(errorBackupListProvider.notifier).empty(); - - if (state.totalAssetsToUpload > 1) { - _throttledNotifiy(); - } - - // Show detailed asset if enabled in settings or if a single asset is uploaded - bool showDetailedNotification = - ref.read(appSettingsServiceProvider).getSetting(AppSettingsEnum.backgroundBackupSingleProgress) || - state.totalAssetsToUpload == 1; - state = state.copyWith(showDetailedNotification: showDetailedNotification); - final pmProgressHandler = Platform.isIOS ? PMProgressHandler() : null; - - _cancelToken?.complete(); - _cancelToken = Completer(); - final bool ok = await ref - .read(backupServiceProvider) - .backupAsset( - uploadAssets, - _cancelToken!, - pmProgressHandler: pmProgressHandler, - onSuccess: _onAssetUploaded, - onProgress: _onProgress, - onCurrentAsset: _onSetCurrentBackupAsset, - onError: _onAssetUploadError, - ); - - // Close detailed notification - await _localNotificationService.closeNotification(LocalNotificationService.manualUploadDetailedNotificationID); - - _log.info( - '[_startUpload] Manual Upload Completed - success: ${state.successfulUploads},' - ' failed: ${state.totalAssetsToUpload - state.successfulUploads}', - ); - - // User cancelled upload - if (!ok && _cancelToken == null) { - await _localNotificationService.showOrUpdateManualUploadStatus( - "backup_manual_title".tr(), - "backup_manual_cancelled".tr(), - presentBanner: true, - ); - hasErrors = true; - } else if (state.successfulUploads == 0 || (!ok && _cancelToken != null)) { - await _localNotificationService.showOrUpdateManualUploadStatus( - "backup_manual_title".tr(), - "failed".tr(), - presentBanner: true, - ); - hasErrors = true; - } else { - await _localNotificationService.showOrUpdateManualUploadStatus( - "backup_manual_title".tr(), - "backup_manual_success".tr(), - presentBanner: true, - ); - } - } else { - unawaited(openAppSettings()); - dPrint(() => "[_startUpload] Do not have permission to the gallery"); - } - } catch (e) { - dPrint(() => "ERROR _startUpload: ${e.toString()}"); - hasErrors = true; - } finally { - _backupProvider.updateBackupProgress(BackUpProgressEnum.idle); - _handleAppInActivity(); - await _localNotificationService.closeNotification(LocalNotificationService.manualUploadDetailedNotificationID); - await _backupProvider.notifyBackgroundServiceCanRun(); - } - return !hasErrors; - } - - void _handleAppInActivity() { - final appState = ref.read(appStateProvider.notifier).getAppState(); - // The app is currently in background. Perform the necessary cleanups which - // are on-hold for upload completion - if (appState != AppLifeCycleEnum.active && appState != AppLifeCycleEnum.resumed) { - ref.read(backupProvider.notifier).cancelBackup(); - } - } - - void cancelBackup() { - if (_backupProvider.backupProgress != BackUpProgressEnum.inProgress && - _backupProvider.backupProgress != BackUpProgressEnum.manualInProgress) { - _backupProvider.notifyBackgroundServiceCanRun(); - } - _cancelToken?.complete(); - _cancelToken = null; - if (_backupProvider.backupProgress != BackUpProgressEnum.manualInProgress) { - _backupProvider.updateBackupProgress(BackUpProgressEnum.idle); - } - state = state.copyWith( - progressInPercentage: 0, - progressInFileSize: "0 B / 0 B", - progressInFileSpeed: 0, - progressInFileSpeedUpdateTime: DateTime.now(), - progressInFileSpeedUpdateSentBytes: 0, - ); - } - - Future uploadAssets(BuildContext context, Iterable allManualUploads) async { - // assumes the background service is currently running and - // waits until it has stopped to start the backup. - final bool hasLock = await ref.read(backgroundServiceProvider).acquireLock(); - if (!hasLock) { - dPrint(() => "[uploadAssets] could not acquire lock, exiting"); - ImmichToast.show( - context: context, - msg: "failed".tr(), - toastType: ToastType.info, - gravity: ToastGravity.BOTTOM, - durationInSecond: 3, - ); - return false; - } - - bool showInProgress = false; - - // check if backup is already in process - then return - if (_backupProvider.backupProgress == BackUpProgressEnum.manualInProgress) { - dPrint(() => "[uploadAssets] Manual upload is already running - abort"); - showInProgress = true; - } - - if (_backupProvider.backupProgress == BackUpProgressEnum.inProgress) { - dPrint(() => "[uploadAssets] Auto Backup is already in progress - abort"); - showInProgress = true; - return false; - } - - if (_backupProvider.backupProgress == BackUpProgressEnum.inBackground) { - dPrint(() => "[uploadAssets] Background backup is running - abort"); - showInProgress = true; - } - - if (showInProgress) { - if (context.mounted) { - ImmichToast.show( - context: context, - msg: "backup_manual_in_progress".tr(), - toastType: ToastType.info, - gravity: ToastGravity.BOTTOM, - durationInSecond: 3, - ); - } - return false; - } - - return _startUpload(allManualUploads); - } -} diff --git a/mobile/lib/providers/cast.provider.dart b/mobile/lib/providers/cast.provider.dart index fea95f42aa..b298514d67 100644 --- a/mobile/lib/providers/cast.provider.dart +++ b/mobile/lib/providers/cast.provider.dart @@ -1,6 +1,5 @@ import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/domain/models/asset/base_asset.model.dart'; -import 'package:immich_mobile/entities/asset.entity.dart' as old_asset_entity; import 'package:immich_mobile/models/cast/cast_manager_state.dart'; import 'package:immich_mobile/services/gcast.service.dart'; @@ -55,26 +54,6 @@ class CastNotifier extends StateNotifier { _gCastService.loadMedia(asset, reload); } - // TODO: remove this when we migrate to new timeline - void loadMediaOld(old_asset_entity.Asset asset, bool reload) { - final remoteAsset = RemoteAsset( - id: asset.remoteId.toString(), - name: asset.name, - ownerId: asset.ownerId.toString(), - checksum: asset.checksum, - type: asset.type == old_asset_entity.AssetType.image - ? AssetType.image - : asset.type == old_asset_entity.AssetType.video - ? AssetType.video - : AssetType.other, - createdAt: asset.fileCreatedAt, - updatedAt: asset.updatedAt, - isEdited: false, - ); - - _gCastService.loadMedia(remoteAsset, reload); - } - Future connect(CastDestinationType type, dynamic device) async { switch (type) { case CastDestinationType.googleCast: diff --git a/mobile/lib/providers/db.provider.dart b/mobile/lib/providers/db.provider.dart deleted file mode 100644 index e03e037f36..0000000000 --- a/mobile/lib/providers/db.provider.dart +++ /dev/null @@ -1,5 +0,0 @@ -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:isar/isar.dart'; - -// overwritten in main.dart due to async loading -final dbProvider = Provider((_) => throw UnimplementedError()); diff --git a/mobile/lib/providers/image/exceptions/image_loading_exception.dart b/mobile/lib/providers/image/exceptions/image_loading_exception.dart deleted file mode 100644 index 98f633a88f..0000000000 --- a/mobile/lib/providers/image/exceptions/image_loading_exception.dart +++ /dev/null @@ -1,5 +0,0 @@ -/// An exception for the [ImageLoader] and the Immich image providers -class ImageLoadingException implements Exception { - final String message; - const ImageLoadingException(this.message); -} diff --git a/mobile/lib/providers/infrastructure/action.provider.dart b/mobile/lib/providers/infrastructure/action.provider.dart index d24e2cc6cd..434e930dcf 100644 --- a/mobile/lib/providers/infrastructure/action.provider.dart +++ b/mobile/lib/providers/infrastructure/action.provider.dart @@ -19,16 +19,12 @@ 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/services/timeline.service.dart'; import 'package:immich_mobile/widgets/asset_grid/delete_dialog.dart'; import 'package:logging/logging.dart'; import 'package:openapi/api.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart'; -final actionProvider = NotifierProvider( - ActionNotifier.new, - dependencies: [multiSelectProvider, timelineServiceProvider], -); +final actionProvider = NotifierProvider(ActionNotifier.new, dependencies: [multiSelectProvider]); class ActionResult { final int count; diff --git a/mobile/lib/providers/infrastructure/db.provider.dart b/mobile/lib/providers/infrastructure/db.provider.dart index d38bcbfb55..2b4ba0129f 100644 --- a/mobile/lib/providers/infrastructure/db.provider.dart +++ b/mobile/lib/providers/infrastructure/db.provider.dart @@ -2,13 +2,6 @@ import 'dart:async'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/infrastructure/repositories/db.repository.dart'; -import 'package:isar/isar.dart'; -import 'package:riverpod_annotation/riverpod_annotation.dart'; - -part 'db.provider.g.dart'; - -@Riverpod(keepAlive: true) -Isar isar(Ref ref) => throw UnimplementedError('isar'); Drift Function(Ref ref) driftOverride(Drift drift) => (ref) { ref.onDispose(() => unawaited(drift.close())); diff --git a/mobile/lib/providers/infrastructure/db.provider.g.dart b/mobile/lib/providers/infrastructure/db.provider.g.dart deleted file mode 100644 index 46abfb66a9..0000000000 --- a/mobile/lib/providers/infrastructure/db.provider.g.dart +++ /dev/null @@ -1,27 +0,0 @@ -// GENERATED CODE - DO NOT MODIFY BY HAND - -part of 'db.provider.dart'; - -// ************************************************************************** -// RiverpodGenerator -// ************************************************************************** - -String _$isarHash() => r'69d3a06aa7e69a4381478e03f7956eb07d7f7feb'; - -/// See also [isar]. -@ProviderFor(isar) -final isarProvider = Provider.internal( - isar, - name: r'isarProvider', - debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product') - ? null - : _$isarHash, - dependencies: null, - allTransitiveDependencies: null, -); - -@Deprecated('Will be removed in 3.0. Use Ref instead') -// ignore: unused_element -typedef IsarRef = ProviderRef; -// ignore_for_file: type=lint -// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member, deprecated_member_use_from_same_package diff --git a/mobile/lib/providers/infrastructure/device_asset.provider.dart b/mobile/lib/providers/infrastructure/device_asset.provider.dart deleted file mode 100644 index 7854af016a..0000000000 --- a/mobile/lib/providers/infrastructure/device_asset.provider.dart +++ /dev/null @@ -1,7 +0,0 @@ -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/infrastructure/repositories/device_asset.repository.dart'; -import 'package:immich_mobile/providers/infrastructure/db.provider.dart'; - -final deviceAssetRepositoryProvider = Provider( - (ref) => IsarDeviceAssetRepository(ref.watch(isarProvider)), -); diff --git a/mobile/lib/providers/infrastructure/exif.provider.dart b/mobile/lib/providers/infrastructure/exif.provider.dart deleted file mode 100644 index c126f6cac0..0000000000 --- a/mobile/lib/providers/infrastructure/exif.provider.dart +++ /dev/null @@ -1,9 +0,0 @@ -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/infrastructure/repositories/exif.repository.dart'; -import 'package:immich_mobile/providers/infrastructure/db.provider.dart'; -import 'package:riverpod_annotation/riverpod_annotation.dart'; - -part 'exif.provider.g.dart'; - -@Riverpod(keepAlive: true) -IsarExifRepository exifRepository(Ref ref) => IsarExifRepository(ref.watch(isarProvider)); diff --git a/mobile/lib/providers/infrastructure/exif.provider.g.dart b/mobile/lib/providers/infrastructure/exif.provider.g.dart deleted file mode 100644 index 0261558707..0000000000 --- a/mobile/lib/providers/infrastructure/exif.provider.g.dart +++ /dev/null @@ -1,27 +0,0 @@ -// GENERATED CODE - DO NOT MODIFY BY HAND - -part of 'exif.provider.dart'; - -// ************************************************************************** -// RiverpodGenerator -// ************************************************************************** - -String _$exifRepositoryHash() => r'bf4a3f6a50d954a23d317659b4f3e2f381066463'; - -/// See also [exifRepository]. -@ProviderFor(exifRepository) -final exifRepositoryProvider = Provider.internal( - exifRepository, - name: r'exifRepositoryProvider', - debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product') - ? null - : _$exifRepositoryHash, - dependencies: null, - allTransitiveDependencies: null, -); - -@Deprecated('Will be removed in 3.0. Use Ref instead') -// ignore: unused_element -typedef ExifRepositoryRef = ProviderRef; -// ignore_for_file: type=lint -// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member, deprecated_member_use_from_same_package diff --git a/mobile/lib/providers/infrastructure/store.provider.dart b/mobile/lib/providers/infrastructure/store.provider.dart index 0bf42f3e8b..f867d30fdc 100644 --- a/mobile/lib/providers/infrastructure/store.provider.dart +++ b/mobile/lib/providers/infrastructure/store.provider.dart @@ -1,13 +1,8 @@ import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/domain/services/store.service.dart'; -import 'package:immich_mobile/infrastructure/repositories/store.repository.dart'; -import 'package:immich_mobile/providers/infrastructure/db.provider.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart'; part 'store.provider.g.dart'; -@Riverpod(keepAlive: true) -IsarStoreRepository storeRepository(Ref ref) => IsarStoreRepository(ref.watch(isarProvider)); - @Riverpod(keepAlive: true) StoreService storeService(Ref _) => StoreService.I; diff --git a/mobile/lib/providers/infrastructure/store.provider.g.dart b/mobile/lib/providers/infrastructure/store.provider.g.dart index 98c978cb60..b5af7de3e0 100644 --- a/mobile/lib/providers/infrastructure/store.provider.g.dart +++ b/mobile/lib/providers/infrastructure/store.provider.g.dart @@ -6,23 +6,6 @@ part of 'store.provider.dart'; // RiverpodGenerator // ************************************************************************** -String _$storeRepositoryHash() => r'659cb134466e4b0d5f04e2fc93e426350d99545f'; - -/// See also [storeRepository]. -@ProviderFor(storeRepository) -final storeRepositoryProvider = Provider.internal( - storeRepository, - name: r'storeRepositoryProvider', - debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product') - ? null - : _$storeRepositoryHash, - dependencies: null, - allTransitiveDependencies: null, -); - -@Deprecated('Will be removed in 3.0. Use Ref instead') -// ignore: unused_element -typedef StoreRepositoryRef = ProviderRef; String _$storeServiceHash() => r'250e10497c42df360e9e1f9a618d0b19c1b5b0a0'; /// See also [storeService]. diff --git a/mobile/lib/providers/infrastructure/user.provider.dart b/mobile/lib/providers/infrastructure/user.provider.dart index 922b9866bb..6c3263229e 100644 --- a/mobile/lib/providers/infrastructure/user.provider.dart +++ b/mobile/lib/providers/infrastructure/user.provider.dart @@ -3,7 +3,6 @@ import 'package:immich_mobile/domain/models/user.model.dart'; import 'package:immich_mobile/domain/services/partner.service.dart'; import 'package:immich_mobile/domain/services/user.service.dart'; import 'package:immich_mobile/infrastructure/repositories/partner.repository.dart'; -import 'package:immich_mobile/infrastructure/repositories/user.repository.dart'; import 'package:immich_mobile/infrastructure/repositories/user_api.repository.dart'; import 'package:immich_mobile/providers/api.provider.dart'; import 'package:immich_mobile/providers/infrastructure/db.provider.dart'; @@ -14,18 +13,12 @@ import 'package:riverpod_annotation/riverpod_annotation.dart'; part 'user.provider.g.dart'; -@Riverpod(keepAlive: true) -IsarUserRepository userRepository(Ref ref) => IsarUserRepository(ref.watch(isarProvider)); - @Riverpod(keepAlive: true) UserApiRepository userApiRepository(Ref ref) => UserApiRepository(ref.watch(apiServiceProvider).usersApi); @Riverpod(keepAlive: true) -UserService userService(Ref ref) => UserService( - isarUserRepository: ref.watch(userRepositoryProvider), - userApiRepository: ref.watch(userApiRepositoryProvider), - storeService: ref.watch(storeServiceProvider), -); +UserService userService(Ref ref) => + UserService(userApiRepository: ref.watch(userApiRepositoryProvider), storeService: ref.watch(storeServiceProvider)); /// Drifts final driftPartnerRepositoryProvider = Provider( diff --git a/mobile/lib/providers/infrastructure/user.provider.g.dart b/mobile/lib/providers/infrastructure/user.provider.g.dart index f9148bf3a7..2e9115dad9 100644 --- a/mobile/lib/providers/infrastructure/user.provider.g.dart +++ b/mobile/lib/providers/infrastructure/user.provider.g.dart @@ -6,23 +6,6 @@ part of 'user.provider.dart'; // RiverpodGenerator // ************************************************************************** -String _$userRepositoryHash() => r'538791a4ad126ed086c9db682c67fc5c654d54f3'; - -/// See also [userRepository]. -@ProviderFor(userRepository) -final userRepositoryProvider = Provider.internal( - userRepository, - name: r'userRepositoryProvider', - debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product') - ? null - : _$userRepositoryHash, - dependencies: null, - allTransitiveDependencies: null, -); - -@Deprecated('Will be removed in 3.0. Use Ref instead') -// ignore: unused_element -typedef UserRepositoryRef = ProviderRef; String _$userApiRepositoryHash() => r'8a7340ca4544c8c6b20225c65bff2abb9e96baa2'; /// See also [userApiRepository]. @@ -40,7 +23,7 @@ final userApiRepositoryProvider = Provider.internal( @Deprecated('Will be removed in 3.0. Use Ref instead') // ignore: unused_element typedef UserApiRepositoryRef = ProviderRef; -String _$userServiceHash() => r'181414dddc7891be6237e13d568c287a804228d1'; +String _$userServiceHash() => r'47e607f3b484b51bcb634d47e3cbf1f6ef25da97'; /// See also [userService]. @ProviderFor(userService) diff --git a/mobile/lib/providers/memory.provider.dart b/mobile/lib/providers/memory.provider.dart deleted file mode 100644 index 7fef3060cc..0000000000 --- a/mobile/lib/providers/memory.provider.dart +++ /dev/null @@ -1,9 +0,0 @@ -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/models/memories/memory.model.dart'; -import 'package:immich_mobile/services/memory.service.dart'; - -final memoryFutureProvider = FutureProvider.autoDispose?>((ref) async { - final service = ref.watch(memoryServiceProvider); - - return await service.getMemoryLane(); -}); diff --git a/mobile/lib/providers/partner.provider.dart b/mobile/lib/providers/partner.provider.dart deleted file mode 100644 index 5a85cea1d4..0000000000 --- a/mobile/lib/providers/partner.provider.dart +++ /dev/null @@ -1,89 +0,0 @@ -import 'dart:async'; - -import 'package:collection/collection.dart'; -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/domain/models/user.model.dart'; -import 'package:immich_mobile/providers/album/suggested_shared_users.provider.dart'; -import 'package:immich_mobile/services/partner.service.dart'; - -class PartnerSharedWithNotifier extends StateNotifier> { - final PartnerService _partnerService; - late final StreamSubscription> streamSub; - - PartnerSharedWithNotifier(this._partnerService) : super([]) { - Function eq = const ListEquality().equals; - _partnerService - .getSharedWith() - .then((partners) { - if (!eq(state, partners)) { - state = partners; - } - }) - .then((_) { - streamSub = _partnerService.watchSharedWith().listen((partners) { - if (!eq(state, partners)) { - state = partners; - } - }); - }); - } - - Future updatePartner(UserDto partner, {required bool inTimeline}) { - return _partnerService.updatePartner(partner, inTimeline: inTimeline); - } - - @override - void dispose() { - if (mounted) { - streamSub.cancel(); - } - super.dispose(); - } -} - -final partnerSharedWithProvider = StateNotifierProvider>((ref) { - return PartnerSharedWithNotifier(ref.watch(partnerServiceProvider)); -}); - -class PartnerSharedByNotifier extends StateNotifier> { - final PartnerService _partnerService; - late final StreamSubscription> streamSub; - - PartnerSharedByNotifier(this._partnerService) : super([]) { - Function eq = const ListEquality().equals; - _partnerService - .getSharedBy() - .then((partners) { - if (!eq(state, partners)) { - state = partners; - } - }) - .then((_) { - streamSub = _partnerService.watchSharedBy().listen((partners) { - if (!eq(state, partners)) { - state = partners; - } - }); - }); - } - - @override - void dispose() { - if (mounted) { - streamSub.cancel(); - } - super.dispose(); - } -} - -final partnerSharedByProvider = StateNotifierProvider>((ref) { - return PartnerSharedByNotifier(ref.watch(partnerServiceProvider)); -}); - -final partnerAvailableProvider = FutureProvider.autoDispose>((ref) async { - final otherUsers = await ref.watch(otherUsersProvider.future); - final currentPartners = ref.watch(partnerSharedByProvider); - final available = Set.of(otherUsers); - available.removeAll(currentPartners); - return available.toList(); -}); diff --git a/mobile/lib/providers/search/all_motion_photos.provider.dart b/mobile/lib/providers/search/all_motion_photos.provider.dart deleted file mode 100644 index 48bc1bb80c..0000000000 --- a/mobile/lib/providers/search/all_motion_photos.provider.dart +++ /dev/null @@ -1,7 +0,0 @@ -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/entities/asset.entity.dart'; -import 'package:immich_mobile/services/asset.service.dart'; - -final allMotionPhotosProvider = FutureProvider>((ref) async { - return ref.watch(assetServiceProvider).getMotionAssets(); -}); diff --git a/mobile/lib/providers/search/paginated_search.provider.dart b/mobile/lib/providers/search/paginated_search.provider.dart deleted file mode 100644 index 9a37d83320..0000000000 --- a/mobile/lib/providers/search/paginated_search.provider.dart +++ /dev/null @@ -1,46 +0,0 @@ -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/models/search/search_result.model.dart'; -import 'package:immich_mobile/services/timeline.service.dart'; -import 'package:immich_mobile/widgets/asset_grid/asset_grid_data_structure.dart'; -import 'package:immich_mobile/models/search/search_filter.model.dart'; -import 'package:immich_mobile/services/search.service.dart'; -import 'package:riverpod_annotation/riverpod_annotation.dart'; - -part 'paginated_search.provider.g.dart'; - -final paginatedSearchProvider = StateNotifierProvider( - (ref) => PaginatedSearchNotifier(ref.watch(searchServiceProvider)), -); - -class PaginatedSearchNotifier extends StateNotifier { - final SearchService _searchService; - - PaginatedSearchNotifier(this._searchService) : super(const SearchResult(assets: [], nextPage: 1)); - - Future search(SearchFilter filter) async { - if (state.nextPage == null) { - return false; - } - - final result = await _searchService.search(filter, state.nextPage!); - - if (result == null) { - return false; - } - - state = SearchResult(assets: [...state.assets, ...result.assets], nextPage: result.nextPage); - - return true; - } - - clear() { - state = const SearchResult(assets: [], nextPage: 1); - } -} - -@riverpod -Future paginatedSearchRenderList(Ref ref) { - final result = ref.watch(paginatedSearchProvider); - final timelineService = ref.watch(timelineServiceProvider); - return timelineService.getTimelineFromAssets(result.assets, GroupAssetsBy.none); -} diff --git a/mobile/lib/providers/search/paginated_search.provider.g.dart b/mobile/lib/providers/search/paginated_search.provider.g.dart deleted file mode 100644 index e984997967..0000000000 --- a/mobile/lib/providers/search/paginated_search.provider.g.dart +++ /dev/null @@ -1,29 +0,0 @@ -// GENERATED CODE - DO NOT MODIFY BY HAND - -part of 'paginated_search.provider.dart'; - -// ************************************************************************** -// RiverpodGenerator -// ************************************************************************** - -String _$paginatedSearchRenderListHash() => - r'22d715ff7864e5a946be38322ce7813616f899c2'; - -/// See also [paginatedSearchRenderList]. -@ProviderFor(paginatedSearchRenderList) -final paginatedSearchRenderListProvider = - AutoDisposeFutureProvider.internal( - paginatedSearchRenderList, - name: r'paginatedSearchRenderListProvider', - debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product') - ? null - : _$paginatedSearchRenderListHash, - dependencies: null, - allTransitiveDependencies: null, - ); - -@Deprecated('Will be removed in 3.0. Use Ref instead') -// ignore: unused_element -typedef PaginatedSearchRenderListRef = AutoDisposeFutureProviderRef; -// ignore_for_file: type=lint -// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member, deprecated_member_use_from_same_package diff --git a/mobile/lib/providers/search/people.provider.dart b/mobile/lib/providers/search/people.provider.dart index 3ff8d67983..1f6f983154 100644 --- a/mobile/lib/providers/search/people.provider.dart +++ b/mobile/lib/providers/search/people.provider.dart @@ -1,9 +1,6 @@ import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/domain/models/person.model.dart'; -import 'package:immich_mobile/widgets/asset_grid/asset_grid_data_structure.dart'; import 'package:immich_mobile/services/person.service.dart'; -import 'package:immich_mobile/providers/app_settings.provider.dart'; -import 'package:immich_mobile/services/app_settings.service.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart'; part 'people.provider.g.dart'; @@ -17,16 +14,6 @@ Future> getAllPeople(Ref ref) async { return people; } -@riverpod -Future personAssets(Ref ref, String personId) async { - final PersonService personService = ref.read(personServiceProvider); - final assets = await personService.getPersonAssets(personId); - - final settings = ref.read(appSettingsServiceProvider); - final groupBy = GroupAssetsBy.values[settings.getSetting(AppSettingsEnum.groupAssetsBy)]; - return await RenderList.fromAssets(assets, groupBy); -} - @riverpod Future updatePersonName(Ref ref, String personId, String updatedName) async { final PersonService personService = ref.read(personServiceProvider); diff --git a/mobile/lib/providers/search/people.provider.g.dart b/mobile/lib/providers/search/people.provider.g.dart index 9595c36eec..23424c068f 100644 --- a/mobile/lib/providers/search/people.provider.g.dart +++ b/mobile/lib/providers/search/people.provider.g.dart @@ -24,7 +24,7 @@ final getAllPeopleProvider = @Deprecated('Will be removed in 3.0. Use Ref instead') // ignore: unused_element typedef GetAllPeopleRef = AutoDisposeFutureProviderRef>; -String _$personAssetsHash() => r'c1d35ee0e024bd6915e21bc724be4b458a14bc24'; +String _$updatePersonNameHash() => r'45f7693172de522a227406d8198811434cf2bbbc'; /// Copied from Dart SDK class _SystemHash { @@ -47,126 +47,6 @@ class _SystemHash { } } -/// See also [personAssets]. -@ProviderFor(personAssets) -const personAssetsProvider = PersonAssetsFamily(); - -/// See also [personAssets]. -class PersonAssetsFamily extends Family> { - /// See also [personAssets]. - const PersonAssetsFamily(); - - /// See also [personAssets]. - PersonAssetsProvider call(String personId) { - return PersonAssetsProvider(personId); - } - - @override - PersonAssetsProvider getProviderOverride( - covariant PersonAssetsProvider provider, - ) { - return call(provider.personId); - } - - static const Iterable? _dependencies = null; - - @override - Iterable? get dependencies => _dependencies; - - static const Iterable? _allTransitiveDependencies = null; - - @override - Iterable? get allTransitiveDependencies => - _allTransitiveDependencies; - - @override - String? get name => r'personAssetsProvider'; -} - -/// See also [personAssets]. -class PersonAssetsProvider extends AutoDisposeFutureProvider { - /// See also [personAssets]. - PersonAssetsProvider(String personId) - : this._internal( - (ref) => personAssets(ref as PersonAssetsRef, personId), - from: personAssetsProvider, - name: r'personAssetsProvider', - debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product') - ? null - : _$personAssetsHash, - dependencies: PersonAssetsFamily._dependencies, - allTransitiveDependencies: - PersonAssetsFamily._allTransitiveDependencies, - personId: personId, - ); - - PersonAssetsProvider._internal( - super._createNotifier, { - required super.name, - required super.dependencies, - required super.allTransitiveDependencies, - required super.debugGetCreateSourceHash, - required super.from, - required this.personId, - }) : super.internal(); - - final String personId; - - @override - Override overrideWith( - FutureOr Function(PersonAssetsRef provider) create, - ) { - return ProviderOverride( - origin: this, - override: PersonAssetsProvider._internal( - (ref) => create(ref as PersonAssetsRef), - from: from, - name: null, - dependencies: null, - allTransitiveDependencies: null, - debugGetCreateSourceHash: null, - personId: personId, - ), - ); - } - - @override - AutoDisposeFutureProviderElement createElement() { - return _PersonAssetsProviderElement(this); - } - - @override - bool operator ==(Object other) { - return other is PersonAssetsProvider && other.personId == personId; - } - - @override - int get hashCode { - var hash = _SystemHash.combine(0, runtimeType.hashCode); - hash = _SystemHash.combine(hash, personId.hashCode); - - return _SystemHash.finish(hash); - } -} - -@Deprecated('Will be removed in 3.0. Use Ref instead') -// ignore: unused_element -mixin PersonAssetsRef on AutoDisposeFutureProviderRef { - /// The parameter `personId` of this provider. - String get personId; -} - -class _PersonAssetsProviderElement - extends AutoDisposeFutureProviderElement - with PersonAssetsRef { - _PersonAssetsProviderElement(super.provider); - - @override - String get personId => (origin as PersonAssetsProvider).personId; -} - -String _$updatePersonNameHash() => r'45f7693172de522a227406d8198811434cf2bbbc'; - /// See also [updatePersonName]. @ProviderFor(updatePersonName) const updatePersonNameProvider = UpdatePersonNameFamily(); diff --git a/mobile/lib/providers/search/recently_taken_asset.provider.dart b/mobile/lib/providers/search/recently_taken_asset.provider.dart deleted file mode 100644 index 157e7c2a74..0000000000 --- a/mobile/lib/providers/search/recently_taken_asset.provider.dart +++ /dev/null @@ -1,9 +0,0 @@ -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/entities/asset.entity.dart'; -import 'package:immich_mobile/services/asset.service.dart'; - -final recentlyTakenAssetProvider = FutureProvider>((ref) async { - final assetService = ref.read(assetServiceProvider); - - return assetService.getRecentlyTakenAssets(); -}); diff --git a/mobile/lib/providers/timeline.provider.dart b/mobile/lib/providers/timeline.provider.dart deleted file mode 100644 index 71ea308dbf..0000000000 --- a/mobile/lib/providers/timeline.provider.dart +++ /dev/null @@ -1,68 +0,0 @@ -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/entities/asset.entity.dart'; -import 'package:immich_mobile/providers/album/album.provider.dart'; -import 'package:immich_mobile/providers/locale_provider.dart'; -import 'package:immich_mobile/services/timeline.service.dart'; -import 'package:immich_mobile/widgets/asset_grid/asset_grid_data_structure.dart'; - -final singleUserTimelineProvider = StreamProvider.family((ref, userId) { - if (userId == null) { - return const Stream.empty(); - } - - ref.watch(localeProvider); - final timelineService = ref.watch(timelineServiceProvider); - return timelineService.watchHomeTimeline(userId); -}, dependencies: [localeProvider]); - -final multiUsersTimelineProvider = StreamProvider.family>((ref, userIds) { - ref.watch(localeProvider); - final timelineService = ref.watch(timelineServiceProvider); - return timelineService.watchMultiUsersTimeline(userIds); -}, dependencies: [localeProvider]); - -final albumTimelineProvider = StreamProvider.autoDispose.family((ref, id) { - final album = ref.watch(albumWatcher(id)).value; - final timelineService = ref.watch(timelineServiceProvider); - - if (album != null) { - return timelineService.watchAlbumTimeline(album); - } - - return const Stream.empty(); -}); - -final archiveTimelineProvider = StreamProvider((ref) { - final timelineService = ref.watch(timelineServiceProvider); - return timelineService.watchArchiveTimeline(); -}); - -final favoriteTimelineProvider = StreamProvider((ref) { - final timelineService = ref.watch(timelineServiceProvider); - return timelineService.watchFavoriteTimeline(); -}); - -final trashTimelineProvider = StreamProvider((ref) { - final timelineService = ref.watch(timelineServiceProvider); - return timelineService.watchTrashTimeline(); -}); - -final allVideosTimelineProvider = StreamProvider((ref) { - final timelineService = ref.watch(timelineServiceProvider); - return timelineService.watchAllVideosTimeline(); -}); - -final assetSelectionTimelineProvider = StreamProvider((ref) { - final timelineService = ref.watch(timelineServiceProvider); - return timelineService.watchAssetSelectionTimeline(); -}); - -final assetsTimelineProvider = FutureProvider.family>((ref, assets) { - final timelineService = ref.watch(timelineServiceProvider); - return timelineService.getTimelineFromAssets(assets, null); -}); - -final lockedTimelineProvider = StreamProvider((ref) { - final timelineService = ref.watch(timelineServiceProvider); - return timelineService.watchLockedTimelineProvider(); -}); diff --git a/mobile/lib/providers/trash.provider.dart b/mobile/lib/providers/trash.provider.dart deleted file mode 100644 index 41b9160b9b..0000000000 --- a/mobile/lib/providers/trash.provider.dart +++ /dev/null @@ -1,45 +0,0 @@ -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/entities/asset.entity.dart'; -import 'package:immich_mobile/services/trash.service.dart'; -import 'package:logging/logging.dart'; - -class TrashNotifier extends StateNotifier { - final TrashService _trashService; - final _log = Logger('TrashNotifier'); - - TrashNotifier(this._trashService) : super(false); - - Future emptyTrash() async { - try { - await _trashService.emptyTrash(); - state = true; - } catch (error, stack) { - _log.severe("Cannot empty trash", error, stack); - state = false; - } - } - - Future restoreAssets(Iterable assetList) async { - try { - await _trashService.restoreAssets(assetList); - return true; - } catch (error, stack) { - _log.severe("Cannot restore assets", error, stack); - return false; - } - } - - Future restoreTrash() async { - try { - await _trashService.restoreTrash(); - state = true; - } catch (error, stack) { - _log.severe("Cannot restore trash", error, stack); - state = false; - } - } -} - -final trashProvider = StateNotifierProvider((ref) { - return TrashNotifier(ref.watch(trashServiceProvider)); -}); diff --git a/mobile/lib/providers/user.provider.dart b/mobile/lib/providers/user.provider.dart index 10dcb2aff5..5a56b65793 100644 --- a/mobile/lib/providers/user.provider.dart +++ b/mobile/lib/providers/user.provider.dart @@ -1,11 +1,9 @@ import 'dart:async'; -import 'package:collection/collection.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/domain/models/user.model.dart'; import 'package:immich_mobile/domain/services/user.service.dart'; import 'package:immich_mobile/providers/infrastructure/user.provider.dart'; -import 'package:immich_mobile/services/timeline.service.dart'; class CurrentUserProvider extends StateNotifier { CurrentUserProvider(this._userService) : super(null) { @@ -32,28 +30,3 @@ class CurrentUserProvider extends StateNotifier { final currentUserProvider = StateNotifierProvider((ref) { return CurrentUserProvider(ref.watch(userServiceProvider)); }); - -class TimelineUserIdsProvider extends StateNotifier> { - TimelineUserIdsProvider(this._timelineService) : super([]) { - final listEquality = const ListEquality(); - _timelineService.getTimelineUserIds().then((users) => state = users); - streamSub = _timelineService.watchTimelineUserIds().listen((users) { - if (!listEquality.equals(state, users)) { - state = users; - } - }); - } - - late final StreamSubscription> streamSub; - final TimelineService _timelineService; - - @override - void dispose() { - streamSub.cancel(); - super.dispose(); - } -} - -final timelineUsersIdsProvider = StateNotifierProvider>((ref) { - return TimelineUserIdsProvider(ref.watch(timelineServiceProvider)); -}); diff --git a/mobile/lib/providers/websocket.provider.dart b/mobile/lib/providers/websocket.provider.dart index 8059e54605..c79f40a25d 100644 --- a/mobile/lib/providers/websocket.provider.dart +++ b/mobile/lib/providers/websocket.provider.dart @@ -1,60 +1,27 @@ import 'dart:async'; -import 'package:collection/collection.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/domain/models/store.model.dart'; -import 'package:immich_mobile/entities/asset.entity.dart'; import 'package:immich_mobile/entities/store.entity.dart'; import 'package:immich_mobile/infrastructure/repositories/network.repository.dart'; import 'package:immich_mobile/models/server_info/server_version.model.dart'; -import 'package:immich_mobile/providers/asset.provider.dart'; import 'package:immich_mobile/providers/auth.provider.dart'; import 'package:immich_mobile/providers/background_sync.provider.dart'; -import 'package:immich_mobile/providers/db.provider.dart'; import 'package:immich_mobile/providers/server_info.provider.dart'; -import 'package:immich_mobile/services/sync.service.dart'; import 'package:immich_mobile/utils/debounce.dart'; import 'package:immich_mobile/utils/debug_print.dart'; import 'package:logging/logging.dart'; import 'package:openapi/api.dart'; import 'package:socket_io_client/socket_io_client.dart'; -enum PendingAction { assetDelete, assetUploaded, assetHidden, assetTrash } - -class PendingChange { - final String id; - final PendingAction action; - final dynamic value; - - const PendingChange(this.id, this.action, this.value); - - @override - String toString() => 'PendingChange(id: $id, action: $action, value: $value)'; - - @override - bool operator ==(Object other) { - if (identical(this, other)) return true; - - return other is PendingChange && other.id == id && other.action == action; - } - - @override - int get hashCode => id.hashCode ^ action.hashCode; -} - class WebsocketState { final Socket? socket; final bool isConnected; - final List pendingChanges; - const WebsocketState({this.socket, required this.isConnected, required this.pendingChanges}); + const WebsocketState({this.socket, required this.isConnected}); - WebsocketState copyWith({Socket? socket, bool? isConnected, List? pendingChanges}) { - return WebsocketState( - socket: socket ?? this.socket, - isConnected: isConnected ?? this.isConnected, - pendingChanges: pendingChanges ?? this.pendingChanges, - ); + WebsocketState copyWith({Socket? socket, bool? isConnected}) { + return WebsocketState(socket: socket ?? this.socket, isConnected: isConnected ?? this.isConnected); } @override @@ -72,11 +39,10 @@ class WebsocketState { } class WebsocketNotifier extends StateNotifier { - WebsocketNotifier(this._ref) : super(const WebsocketState(socket: null, isConnected: false, pendingChanges: [])); + WebsocketNotifier(this._ref) : super(const WebsocketState(socket: null, isConnected: false)); final _log = Logger('WebsocketNotifier'); final Ref _ref; - final Debouncer _debounce = Debouncer(interval: const Duration(milliseconds: 500)); final Debouncer _batchDebouncer = Debouncer( interval: const Duration(seconds: 5), @@ -115,32 +81,21 @@ class WebsocketNotifier extends StateNotifier { socket.onConnect((_) { dPrint(() => "Established Websocket Connection"); - state = WebsocketState(isConnected: true, socket: socket, pendingChanges: state.pendingChanges); + state = WebsocketState(isConnected: true, socket: socket); }); socket.onDisconnect((_) { dPrint(() => "Disconnect to Websocket Connection"); - state = WebsocketState(isConnected: false, socket: null, pendingChanges: state.pendingChanges); + state = const WebsocketState(isConnected: false, socket: null); }); socket.on('error', (errorMessage) { _log.severe("Websocket Error - $errorMessage"); - state = WebsocketState(isConnected: false, socket: null, pendingChanges: state.pendingChanges); + state = const WebsocketState(isConnected: false, socket: null); }); - if (!Store.isBetaTimelineEnabled) { - socket.on('on_upload_success', _handleOnUploadSuccess); - socket.on('on_asset_delete', _handleOnAssetDelete); - socket.on('on_asset_trash', _handleOnAssetTrash); - socket.on('on_asset_restore', _handleServerUpdates); - socket.on('on_asset_update', _handleServerUpdates); - socket.on('on_asset_stack_update', _handleServerUpdates); - socket.on('on_asset_hidden', _handleOnAssetHidden); - } else { - socket.on('AssetUploadReadyV1', _handleSyncAssetUploadReady); - socket.on('AssetEditReadyV1', _handleSyncAssetEditReady); - } - + socket.on('AssetUploadReadyV1', _handleSyncAssetUploadReady); + socket.on('AssetEditReadyV1', _handleSyncAssetEditReady); socket.on('on_config_update', _handleOnConfigUpdate); socket.on('on_new_release', _handleReleaseUpdates); } catch (e) { @@ -155,46 +110,7 @@ class WebsocketNotifier extends StateNotifier { _batchedAssetUploadReady.clear(); state.socket?.dispose(); - state = WebsocketState(isConnected: false, socket: null, pendingChanges: state.pendingChanges); - } - - void stopListenToEvent(String eventName) { - state.socket?.off(eventName); - } - - void stopListenToOldEvents() { - state.socket?.off('on_upload_success'); - state.socket?.off('on_asset_delete'); - state.socket?.off('on_asset_trash'); - state.socket?.off('on_asset_restore'); - state.socket?.off('on_asset_update'); - state.socket?.off('on_asset_stack_update'); - state.socket?.off('on_asset_hidden'); - } - - void startListeningToOldEvents() { - state.socket?.on('on_upload_success', _handleOnUploadSuccess); - state.socket?.on('on_asset_delete', _handleOnAssetDelete); - state.socket?.on('on_asset_trash', _handleOnAssetTrash); - state.socket?.on('on_asset_restore', _handleServerUpdates); - state.socket?.on('on_asset_update', _handleServerUpdates); - state.socket?.on('on_asset_stack_update', _handleServerUpdates); - state.socket?.on('on_asset_hidden', _handleOnAssetHidden); - } - - void stopListeningToBetaEvents() { - state.socket?.off('AssetUploadReadyV1'); - state.socket?.off('AssetEditReadyV1'); - } - - void startListeningToBetaEvents() { - state.socket?.on('AssetUploadReadyV1', _handleSyncAssetUploadReady); - state.socket?.on('AssetEditReadyV1', _handleSyncAssetEditReady); - } - - void listenUploadEvent() { - dPrint(() => "Start listening to event on_upload_success"); - state.socket?.on('on_upload_success', _handleOnUploadSuccess); + state = const WebsocketState(isConnected: false, socket: null); } Future waitForEvent(String event, bool Function(dynamic)? predicate, Duration timeout) { @@ -218,89 +134,11 @@ class WebsocketNotifier extends StateNotifier { ); } - void addPendingChange(PendingAction action, dynamic value) { - final now = DateTime.now(); - state = state.copyWith( - pendingChanges: [...state.pendingChanges, PendingChange(now.millisecondsSinceEpoch.toString(), action, value)], - ); - _debounce.run(handlePendingChanges); - } - - Future _handlePendingTrashes() async { - final trashChanges = state.pendingChanges.where((c) => c.action == PendingAction.assetTrash).toList(); - if (trashChanges.isNotEmpty) { - List remoteIds = trashChanges.expand((a) => (a.value as List).map((e) => e.toString())).toList(); - - await _ref.read(syncServiceProvider).handleRemoteAssetRemoval(remoteIds); - await _ref.read(assetProvider.notifier).getAllAsset(); - - state = state.copyWith(pendingChanges: state.pendingChanges.whereNot((c) => trashChanges.contains(c)).toList()); - } - } - - Future _handlePendingDeletes() async { - final deleteChanges = state.pendingChanges.where((c) => c.action == PendingAction.assetDelete).toList(); - if (deleteChanges.isNotEmpty) { - List remoteIds = deleteChanges.map((a) => a.value.toString()).toList(); - await _ref.read(syncServiceProvider).handleRemoteAssetRemoval(remoteIds); - state = state.copyWith(pendingChanges: state.pendingChanges.whereNot((c) => deleteChanges.contains(c)).toList()); - } - } - - Future _handlePendingUploaded() async { - final uploadedChanges = state.pendingChanges.where((c) => c.action == PendingAction.assetUploaded).toList(); - if (uploadedChanges.isNotEmpty) { - List remoteAssets = uploadedChanges.map((a) => AssetResponseDto.fromJson(a.value)).toList(); - for (final dto in remoteAssets) { - if (dto != null) { - final newAsset = Asset.remote(dto); - await _ref.watch(assetProvider.notifier).onNewAssetUploaded(newAsset); - } - } - state = state.copyWith( - pendingChanges: state.pendingChanges.whereNot((c) => uploadedChanges.contains(c)).toList(), - ); - } - } - - Future _handlingPendingHidden() async { - final hiddenChanges = state.pendingChanges.where((c) => c.action == PendingAction.assetHidden).toList(); - if (hiddenChanges.isNotEmpty) { - List remoteIds = hiddenChanges.map((a) => a.value.toString()).toList(); - final db = _ref.watch(dbProvider); - await db.writeTxn(() => db.assets.deleteAllByRemoteId(remoteIds)); - - state = state.copyWith(pendingChanges: state.pendingChanges.whereNot((c) => hiddenChanges.contains(c)).toList()); - } - } - - Future handlePendingChanges() async { - await _handlePendingUploaded(); - await _handlePendingDeletes(); - await _handlingPendingHidden(); - await _handlePendingTrashes(); - } - void _handleOnConfigUpdate(dynamic _) { _ref.read(serverInfoProvider.notifier).getServerFeatures(); _ref.read(serverInfoProvider.notifier).getServerConfig(); } - // Refresh updated assets - void _handleServerUpdates(dynamic _) { - _ref.read(assetProvider.notifier).getAllAsset(); - } - - void _handleOnUploadSuccess(dynamic data) => addPendingChange(PendingAction.assetUploaded, data); - - void _handleOnAssetDelete(dynamic data) => addPendingChange(PendingAction.assetDelete, data); - - void _handleOnAssetTrash(dynamic data) { - addPendingChange(PendingAction.assetTrash, data); - } - - void _handleOnAssetHidden(dynamic data) => addPendingChange(PendingAction.assetHidden, data); - _handleReleaseUpdates(dynamic data) { // Json guard if (data is! Map) { diff --git a/mobile/lib/repositories/album.repository.dart b/mobile/lib/repositories/album.repository.dart deleted file mode 100644 index 2d24004944..0000000000 --- a/mobile/lib/repositories/album.repository.dart +++ /dev/null @@ -1,139 +0,0 @@ -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/domain/models/store.model.dart'; -import 'package:immich_mobile/domain/models/user.model.dart'; -import 'package:immich_mobile/entities/album.entity.dart'; -import 'package:immich_mobile/entities/asset.entity.dart'; -import 'package:immich_mobile/entities/store.entity.dart'; -import 'package:immich_mobile/infrastructure/entities/user.entity.dart' as entity; -import 'package:immich_mobile/models/albums/album_search.model.dart'; -import 'package:immich_mobile/providers/db.provider.dart'; -import 'package:immich_mobile/repositories/database.repository.dart'; -import 'package:immich_mobile/utils/hash.dart'; -import 'package:isar/isar.dart'; - -enum AlbumSort { remoteId, localId } - -final albumRepositoryProvider = Provider((ref) => AlbumRepository(ref.watch(dbProvider))); - -class AlbumRepository extends DatabaseRepository { - const AlbumRepository(super.db); - - Future count({bool? local}) { - final baseQuery = db.albums.where(); - final QueryBuilder query = switch (local) { - null => baseQuery.noOp(), - true => baseQuery.localIdIsNotNull(), - false => baseQuery.remoteIdIsNotNull(), - }; - return query.count(); - } - - Future create(Album album) => txn(() => db.albums.store(album)); - - Future getByName(String name, {bool? shared, bool? remote, bool? owner}) { - var query = db.albums.filter().nameEqualTo(name); - if (shared != null) { - query = query.sharedEqualTo(shared); - } - final isarUserId = fastHash(Store.get(StoreKey.currentUser).id); - if (owner == true) { - query = query.owner((q) => q.isarIdEqualTo(isarUserId)); - } else if (owner == false) { - query = query.owner((q) => q.not().isarIdEqualTo(isarUserId)); - } - if (remote == true) { - query = query.localIdIsNull(); - } else if (remote == false) { - query = query.remoteIdIsNull(); - } - return query.findFirst(); - } - - Future update(Album album) => txn(() => db.albums.store(album)); - - Future delete(int albumId) => txn(() => db.albums.delete(albumId)); - - Future> getAll({bool? shared, bool? remote, int? ownerId, AlbumSort? sortBy}) { - final baseQuery = db.albums.where(); - final QueryBuilder afterWhere; - if (remote == null) { - afterWhere = baseQuery.noOp(); - } else if (remote) { - afterWhere = baseQuery.remoteIdIsNotNull(); - } else { - afterWhere = baseQuery.localIdIsNotNull(); - } - QueryBuilder filterQuery = afterWhere.filter().noOp(); - if (shared != null) { - filterQuery = filterQuery.sharedEqualTo(true); - } - if (ownerId != null) { - filterQuery = filterQuery.owner((q) => q.isarIdEqualTo(ownerId)); - } - final QueryBuilder query = switch (sortBy) { - null => filterQuery.noOp(), - AlbumSort.remoteId => filterQuery.sortByRemoteId(), - AlbumSort.localId => filterQuery.sortByLocalId(), - }; - return query.findAll(); - } - - Future get(int id) => db.albums.get(id); - - Future getByRemoteId(String remoteId) { - return db.albums.filter().remoteIdEqualTo(remoteId).findFirst(); - } - - Future removeUsers(Album album, List users) => - txn(() => album.sharedUsers.update(unlink: users.map(entity.User.fromDto))); - - Future addAssets(Album album, List assets) => txn(() => album.assets.update(link: assets)); - - Future removeAssets(Album album, List assets) => txn(() => album.assets.update(unlink: assets)); - - Future recalculateMetadata(Album album) async { - album.startDate = await album.assets.filter().fileCreatedAtProperty().min(); - album.endDate = await album.assets.filter().fileCreatedAtProperty().max(); - album.lastModifiedAssetTimestamp = await album.assets.filter().updatedAtProperty().max(); - return album; - } - - Future addUsers(Album album, List users) => - txn(() => album.sharedUsers.update(link: users.map(entity.User.fromDto))); - - Future deleteAllLocal() => txn(() => db.albums.where().localIdIsNotNull().deleteAll()); - - Future> search(String searchTerm, QuickFilterMode filterMode) async { - var query = db.albums.filter().nameContains(searchTerm, caseSensitive: false).remoteIdIsNotNull(); - final isarUserId = fastHash(Store.get(StoreKey.currentUser).id); - - switch (filterMode) { - case QuickFilterMode.sharedWithMe: - query = query.owner((q) => q.not().isarIdEqualTo(isarUserId)); - case QuickFilterMode.myAlbums: - query = query.owner((q) => q.isarIdEqualTo(isarUserId)); - case QuickFilterMode.all: - break; - } - - return await query.findAll(); - } - - Future clearTable() async { - await txn(() async { - await db.albums.clear(); - }); - } - - Stream> watchRemoteAlbums() { - return db.albums.where().remoteIdIsNotNull().watch(); - } - - Stream> watchLocalAlbums() { - return db.albums.where().localIdIsNotNull().watch(); - } - - Stream watchAlbum(int id) { - return db.albums.watchObject(id, fireImmediately: true); - } -} diff --git a/mobile/lib/repositories/album_api.repository.dart b/mobile/lib/repositories/album_api.repository.dart deleted file mode 100644 index 367c2447f2..0000000000 --- a/mobile/lib/repositories/album_api.repository.dart +++ /dev/null @@ -1,171 +0,0 @@ -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/constants/enums.dart'; -import 'package:immich_mobile/domain/models/album/album.model.dart' show AlbumAssetOrder, RemoteAlbum; -import 'package:immich_mobile/entities/album.entity.dart'; -import 'package:immich_mobile/entities/asset.entity.dart'; -import 'package:immich_mobile/infrastructure/entities/user.entity.dart' as entity; -import 'package:immich_mobile/infrastructure/utils/user.converter.dart'; -import 'package:immich_mobile/providers/api.provider.dart'; -import 'package:immich_mobile/repositories/api.repository.dart'; -import 'package:openapi/api.dart'; - -final albumApiRepositoryProvider = Provider((ref) => AlbumApiRepository(ref.watch(apiServiceProvider).albumsApi)); - -class AlbumApiRepository extends ApiRepository { - final AlbumsApi _api; - - AlbumApiRepository(this._api); - - Future get(String id) async { - final dto = await checkNull(_api.getAlbumInfo(id)); - return _toAlbum(dto); - } - - Future> getAll({bool? shared}) async { - final dtos = await checkNull(_api.getAllAlbums(shared: shared)); - return dtos.map(_toAlbum).toList(); - } - - Future create( - String name, { - required Iterable assetIds, - Iterable sharedUserIds = const [], - String? description, - }) async { - final users = sharedUserIds.map((id) => AlbumUserCreateDto(userId: id, role: AlbumUserRole.editor)); - final responseDto = await checkNull( - _api.createAlbum( - CreateAlbumDto( - albumName: name, - description: description, - assetIds: assetIds.toList(), - albumUsers: users.toList(), - ), - ), - ); - return _toAlbum(responseDto); - } - - // TODO: Change name after removing old method - Future createDriftAlbum(String name, {required Iterable assetIds, String? description}) async { - final responseDto = await checkNull( - _api.createAlbum(CreateAlbumDto(albumName: name, description: description, assetIds: assetIds.toList())), - ); - - return _toRemoteAlbum(responseDto); - } - - Future update( - String albumId, { - String? name, - String? thumbnailAssetId, - String? description, - bool? activityEnabled, - SortOrder? sortOrder, - }) async { - AssetOrder? order; - if (sortOrder != null) { - order = sortOrder == SortOrder.asc ? AssetOrder.asc : AssetOrder.desc; - } - - final response = await checkNull( - _api.updateAlbumInfo( - albumId, - UpdateAlbumDto( - albumName: name, - albumThumbnailAssetId: thumbnailAssetId, - description: description, - isActivityEnabled: activityEnabled, - order: order, - ), - ), - ); - - return _toAlbum(response); - } - - Future delete(String albumId) { - return _api.deleteAlbum(albumId); - } - - Future<({List added, List duplicates})> addAssets(String albumId, Iterable assetIds) async { - final response = await checkNull(_api.addAssetsToAlbum(albumId, BulkIdsDto(ids: assetIds.toList()))); - - final List added = []; - final List duplicates = []; - - for (final result in response) { - if (result.success) { - added.add(result.id); - } else if (result.error == BulkIdErrorReason.duplicate) { - duplicates.add(result.id); - } - } - return (added: added, duplicates: duplicates); - } - - Future<({List removed, List failed})> removeAssets(String albumId, Iterable assetIds) async { - final response = await checkNull(_api.removeAssetFromAlbum(albumId, BulkIdsDto(ids: assetIds.toList()))); - final List removed = [], failed = []; - for (final dto in response) { - if (dto.success) { - removed.add(dto.id); - } else { - failed.add(dto.id); - } - } - return (removed: removed, failed: failed); - } - - Future addUsers(String albumId, Iterable userIds) async { - final albumUsers = userIds.map((userId) => AlbumUserAddDto(userId: userId)).toList(); - final response = await checkNull(_api.addUsersToAlbum(albumId, AddUsersDto(albumUsers: albumUsers))); - return _toAlbum(response); - } - - Future removeUser(String albumId, {required String userId}) { - return _api.removeUserFromAlbum(albumId, userId); - } - - static Album _toAlbum(AlbumResponseDto dto) { - final Album album = Album( - remoteId: dto.id, - name: dto.albumName, - createdAt: dto.createdAt, - modifiedAt: dto.updatedAt, - lastModifiedAssetTimestamp: dto.lastModifiedAssetTimestamp, - shared: dto.shared, - startDate: dto.startDate, - description: dto.description, - endDate: dto.endDate, - activityEnabled: dto.isActivityEnabled, - sortOrder: dto.order == AssetOrder.asc ? SortOrder.asc : SortOrder.desc, - ); - album.remoteAssetCount = dto.assetCount; - album.owner.value = entity.User.fromDto(UserConverter.fromSimpleUserDto(dto.owner)); - album.remoteThumbnailAssetId = dto.albumThumbnailAssetId; - final users = dto.albumUsers.map((albumUser) => UserConverter.fromSimpleUserDto(albumUser.user)); - album.sharedUsers.addAll(users.map(entity.User.fromDto)); - final assets = dto.assets.map(Asset.remote).toList(); - album.assets.addAll(assets); - - return album; - } - - static RemoteAlbum _toRemoteAlbum(AlbumResponseDto dto) { - return RemoteAlbum( - id: dto.id, - name: dto.albumName, - ownerId: dto.owner.id, - description: dto.description, - createdAt: dto.createdAt, - updatedAt: dto.updatedAt, - thumbnailAssetId: dto.albumThumbnailAssetId, - isActivityEnabled: dto.isActivityEnabled, - order: dto.order == AssetOrder.asc ? AlbumAssetOrder.asc : AlbumAssetOrder.desc, - assetCount: dto.assetCount, - ownerName: dto.owner.name, - isShared: dto.albumUsers.length > 2, - ); - } -} diff --git a/mobile/lib/repositories/album_media.repository.dart b/mobile/lib/repositories/album_media.repository.dart deleted file mode 100644 index 89860f4e75..0000000000 --- a/mobile/lib/repositories/album_media.repository.dart +++ /dev/null @@ -1,99 +0,0 @@ -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/domain/models/store.model.dart'; -import 'package:immich_mobile/entities/album.entity.dart'; -import 'package:immich_mobile/entities/asset.entity.dart'; -import 'package:immich_mobile/entities/store.entity.dart'; -import 'package:immich_mobile/infrastructure/entities/user.entity.dart'; -import 'package:immich_mobile/repositories/asset_media.repository.dart'; -import 'package:photo_manager/photo_manager.dart' hide AssetType; - -final albumMediaRepositoryProvider = Provider((ref) => const AlbumMediaRepository()); - -class AlbumMediaRepository { - const AlbumMediaRepository(); - - bool get useCustomFilter => Store.get(StoreKey.photoManagerCustomFilter, true); - - FilterOptionGroup? _getAlbumFilter({ - DateTimeCond? updateTimeCond, - bool? containsPathModified, - List? orderBy, - }) => useCustomFilter - ? FilterOptionGroup( - imageOption: const FilterOption(needTitle: true, sizeConstraint: SizeConstraint(ignoreSize: true)), - videoOption: const FilterOption( - needTitle: true, - sizeConstraint: SizeConstraint(ignoreSize: true), - durationConstraint: DurationConstraint(allowNullable: true), - ), - containsPathModified: containsPathModified ?? false, - createTimeCond: DateTimeCond.def().copyWith(ignore: true), - updateTimeCond: updateTimeCond ?? DateTimeCond.def().copyWith(ignore: true), - orders: orderBy ?? [], - ) - : null; - - Future> getAll() async { - final filter = useCustomFilter - ? CustomFilter.sql(where: '${CustomColumns.base.width} > 0') - : FilterOptionGroup(containsPathModified: true); - - final List assetPathEntities = await PhotoManager.getAssetPathList( - hasAll: true, - filterOption: filter, - ); - return assetPathEntities.map(_toAlbum).toList(); - } - - Future> getAssetIds(String albumId) async { - final album = await AssetPathEntity.fromId(albumId, filterOption: _getAlbumFilter()); - final List assets = await album.getAssetListRange(start: 0, end: 0x7fffffffffffffff); - return assets.map((e) => e.id).toList(); - } - - Future getAssetCount(String albumId) async { - final album = await AssetPathEntity.fromId(albumId, filterOption: _getAlbumFilter()); - return album.assetCountAsync; - } - - Future> getAssets( - String albumId, { - int start = 0, - int end = 0x7fffffffffffffff, - DateTime? modifiedFrom, - DateTime? modifiedUntil, - bool orderByModificationDate = false, - }) async { - final onDevice = await AssetPathEntity.fromId( - albumId, - filterOption: _getAlbumFilter( - updateTimeCond: modifiedFrom == null && modifiedUntil == null - ? null - : DateTimeCond(min: modifiedFrom ?? DateTime.utc(-271820), max: modifiedUntil ?? DateTime.utc(275760)), - orderBy: orderByModificationDate ? [const OrderOption(type: OrderOptionType.updateDate)] : [], - ), - ); - - final List assets = await onDevice.getAssetListRange(start: start, end: end); - return assets.map(AssetMediaRepository.toAsset).toList().cast(); - } - - Future get(String id) async { - final assetPathEntity = await AssetPathEntity.fromId(id, filterOption: _getAlbumFilter(containsPathModified: true)); - return _toAlbum(assetPathEntity); - } - - static Album _toAlbum(AssetPathEntity assetPathEntity) { - final Album album = Album( - name: assetPathEntity.name, - createdAt: assetPathEntity.lastModified?.toUtc() ?? DateTime.now().toUtc(), - modifiedAt: assetPathEntity.lastModified?.toUtc() ?? DateTime.now().toUtc(), - shared: false, - activityEnabled: false, - ); - album.owner.value = User.fromDto(Store.get(StoreKey.currentUser)); - album.localId = assetPathEntity.id; - album.isAll = assetPathEntity.isAll; - return album; - } -} diff --git a/mobile/lib/repositories/asset.repository.dart b/mobile/lib/repositories/asset.repository.dart deleted file mode 100644 index 79af8b4921..0000000000 --- a/mobile/lib/repositories/asset.repository.dart +++ /dev/null @@ -1,220 +0,0 @@ -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/constants/enums.dart'; -import 'package:immich_mobile/entities/album.entity.dart'; -import 'package:immich_mobile/entities/asset.entity.dart'; -import 'package:immich_mobile/entities/duplicated_asset.entity.dart'; -import 'package:immich_mobile/infrastructure/entities/exif.entity.dart'; -import 'package:immich_mobile/providers/db.provider.dart'; -import 'package:immich_mobile/repositories/database.repository.dart'; -import 'package:immich_mobile/utils/hash.dart'; -import 'package:isar/isar.dart'; - -enum AssetSort { checksum, ownerIdChecksum } - -final assetRepositoryProvider = Provider((ref) => AssetRepository(ref.watch(dbProvider))); - -class AssetRepository extends DatabaseRepository { - const AssetRepository(super.db); - - Future> getByAlbum( - Album album, { - Iterable notOwnedBy = const [], - String? ownerId, - AssetState? state, - AssetSort? sortBy, - }) { - var query = album.assets.filter(); - final isarUserIds = notOwnedBy.map(fastHash).toList(); - if (notOwnedBy.length == 1) { - query = query.not().ownerIdEqualTo(isarUserIds.first); - } else if (notOwnedBy.isNotEmpty) { - query = query.not().anyOf(isarUserIds, (q, int id) => q.ownerIdEqualTo(id)); - } - if (ownerId != null) { - query = query.ownerIdEqualTo(fastHash(ownerId)); - } - - if (state != null) { - query = switch (state) { - AssetState.local => query.remoteIdIsNull(), - AssetState.remote => query.localIdIsNull(), - AssetState.merged => query.localIdIsNotNull().remoteIdIsNotNull(), - }; - } - - final QueryBuilder sortedQuery = switch (sortBy) { - null => query.noOp(), - AssetSort.checksum => query.sortByChecksum(), - AssetSort.ownerIdChecksum => query.sortByOwnerId().thenByChecksum(), - }; - - return sortedQuery.findAll(); - } - - Future deleteByIds(List ids) => txn(() async { - await db.assets.deleteAll(ids); - await db.exifInfos.deleteAll(ids); - }); - - Future getByRemoteId(String id) => db.assets.getByRemoteId(id); - - Future> getAllByRemoteId(Iterable ids, {AssetState? state}) async { - if (ids.isEmpty) { - return []; - } - - return _getAllByRemoteIdImpl(ids, state).findAll(); - } - - QueryBuilder _getAllByRemoteIdImpl(Iterable ids, AssetState? state) { - final query = db.assets.remote(ids).filter(); - return switch (state) { - null => query.noOp(), - AssetState.local => query.remoteIdIsNull(), - AssetState.remote => query.localIdIsNull(), - AssetState.merged => query.localIdIsNotEmpty().remoteIdIsNotNull(), - }; - } - - Future> getAll({required String ownerId, AssetState? state, AssetSort? sortBy, int? limit}) { - final baseQuery = db.assets.where(); - final isarUserIds = fastHash(ownerId); - final QueryBuilder filteredQuery = switch (state) { - null => baseQuery.ownerIdEqualToAnyChecksum(isarUserIds).noOp(), - AssetState.local => baseQuery.remoteIdIsNull().filter().localIdIsNotNull().ownerIdEqualTo(isarUserIds), - AssetState.remote => baseQuery.localIdIsNull().filter().remoteIdIsNotNull().ownerIdEqualTo(isarUserIds), - AssetState.merged => - baseQuery.ownerIdEqualToAnyChecksum(isarUserIds).filter().remoteIdIsNotNull().localIdIsNotNull(), - }; - - final QueryBuilder query = switch (sortBy) { - null => filteredQuery.noOp(), - AssetSort.checksum => filteredQuery.sortByChecksum(), - AssetSort.ownerIdChecksum => filteredQuery.sortByOwnerId().thenByChecksum(), - }; - - return limit == null ? query.findAll() : query.limit(limit).findAll(); - } - - Future> updateAll(List assets) async { - await txn(() => db.assets.putAll(assets)); - return assets; - } - - Future> getMatches({ - required List assets, - required String ownerId, - AssetState? state, - int limit = 100, - }) { - final baseQuery = db.assets.where(); - final QueryBuilder query = switch (state) { - null => baseQuery.noOp(), - AssetState.local => baseQuery.remoteIdIsNull().filter().localIdIsNotNull(), - AssetState.remote => baseQuery.localIdIsNull().filter().remoteIdIsNotNull(), - AssetState.merged => baseQuery.localIdIsNotNull().filter().remoteIdIsNotNull(), - }; - return _getMatchesImpl(query, fastHash(ownerId), assets, limit); - } - - Future update(Asset asset) async { - await txn(() => asset.put(db)); - return asset; - } - - Future upsertDuplicatedAssets(Iterable duplicatedAssets) => - txn(() => db.duplicatedAssets.putAll(duplicatedAssets.map(DuplicatedAsset.new).toList())); - - Future> getAllDuplicatedAssetIds() => db.duplicatedAssets.where().idProperty().findAll(); - - Future getByOwnerIdChecksum(int ownerId, String checksum) => - db.assets.getByOwnerIdChecksum(ownerId, checksum); - - Future> getAllByOwnerIdChecksum(List ownerIds, List checksums) => - db.assets.getAllByOwnerIdChecksum(ownerIds, checksums); - - Future> getAllLocal() => db.assets.where().localIdIsNotNull().findAll(); - - Future deleteAllByRemoteId(List ids, {AssetState? state}) => - txn(() => _getAllByRemoteIdImpl(ids, state).deleteAll()); - - Future> getStackAssets(String stackId) { - return db.assets - .filter() - .isArchivedEqualTo(false) - .isTrashedEqualTo(false) - .stackIdEqualTo(stackId) - // orders primary asset first as its ID is null - .sortByStackPrimaryAssetId() - .thenByFileCreatedAtDesc() - .findAll(); - } - - Future clearTable() async { - await txn(() async { - await db.assets.clear(); - }); - } - - Stream watchAsset(int id, {bool fireImmediately = false}) { - return db.assets.watchObject(id, fireImmediately: fireImmediately); - } - - Future> getTrashAssets(String userId) { - return db.assets - .where() - .remoteIdIsNotNull() - .filter() - .ownerIdEqualTo(fastHash(userId)) - .isTrashedEqualTo(true) - .findAll(); - } - - Future> getRecentlyTakenAssets(String userId) { - return db.assets - .where() - .ownerIdEqualToAnyChecksum(fastHash(userId)) - .filter() - .visibilityEqualTo(AssetVisibilityEnum.timeline) - .sortByFileCreatedAtDesc() - .findAll(); - } - - Future> getMotionAssets(String userId) { - return db.assets - .where() - .ownerIdEqualToAnyChecksum(fastHash(userId)) - .filter() - .visibilityEqualTo(AssetVisibilityEnum.timeline) - .livePhotoVideoIdIsNotNull() - .findAll(); - } -} - -Future> _getMatchesImpl( - QueryBuilder query, - int ownerId, - List assets, - int limit, -) => query - .ownerIdEqualTo(ownerId) - .anyOf( - assets, - (q, Asset a) => q - .fileNameEqualTo(a.fileName) - .and() - .durationInSecondsEqualTo(a.durationInSeconds) - .and() - .fileCreatedAtBetween( - a.fileCreatedAt.subtract(const Duration(hours: 12)), - a.fileCreatedAt.add(const Duration(hours: 12)), - ) - .and() - .not() - .checksumEqualTo(a.checksum), - ) - .sortByFileName() - .thenByFileCreatedAt() - .thenByFileModifiedAt() - .limit(limit) - .findAll(); diff --git a/mobile/lib/repositories/asset_api.repository.dart b/mobile/lib/repositories/asset_api.repository.dart index d66b39ecde..2943177d60 100644 --- a/mobile/lib/repositories/asset_api.repository.dart +++ b/mobile/lib/repositories/asset_api.repository.dart @@ -3,7 +3,6 @@ import 'package:http/http.dart'; import 'package:immich_mobile/constants/enums.dart'; import 'package:immich_mobile/domain/models/asset_edit.model.dart' hide AssetEditAction; import 'package:immich_mobile/domain/models/stack.model.dart'; -import 'package:immich_mobile/entities/asset.entity.dart'; import 'package:immich_mobile/providers/api.provider.dart'; import 'package:immich_mobile/repositories/api.repository.dart'; import 'package:maplibre_gl/maplibre_gl.dart'; @@ -12,7 +11,6 @@ import 'package:openapi/api.dart'; final assetApiRepositoryProvider = Provider( (ref) => AssetApiRepository( ref.watch(apiServiceProvider).assetsApi, - ref.watch(apiServiceProvider).searchApi, ref.watch(apiServiceProvider).stacksApi, ref.watch(apiServiceProvider).trashApi, ), @@ -20,32 +18,10 @@ final assetApiRepositoryProvider = Provider( class AssetApiRepository extends ApiRepository { final AssetsApi _api; - final SearchApi _searchApi; final StacksApi _stacksApi; final TrashApi _trashApi; - AssetApiRepository(this._api, this._searchApi, this._stacksApi, this._trashApi); - - Future update(String id, {String? description}) async { - final response = await checkNull(_api.updateAsset(id, UpdateAssetDto(description: description))); - return Asset.remote(response); - } - - Future> search({List personIds = const []}) async { - // TODO this always fetches all assets, change API and usage to actually do pagination - final List result = []; - bool hasNext = true; - int currentPage = 1; - while (hasNext) { - final response = await checkNull( - _searchApi.searchAssets(MetadataSearchDto(personIds: personIds, page: currentPage, size: 1000)), - ); - result.addAll(response.assets.items.map(Asset.remote)); - hasNext = response.assets.nextPage != null; - currentPage++; - } - return result; - } + AssetApiRepository(this._api, this._stacksApi, this._trashApi); Future delete(List ids, bool force) async { return _api.deleteAssets(AssetBulkDeleteDto(ids: ids, force: force)); diff --git a/mobile/lib/repositories/asset_media.repository.dart b/mobile/lib/repositories/asset_media.repository.dart index fecfe6df4d..a2d8bfe162 100644 --- a/mobile/lib/repositories/asset_media.repository.dart +++ b/mobile/lib/repositories/asset_media.repository.dart @@ -5,15 +5,10 @@ import 'package:device_info_plus/device_info_plus.dart'; import 'package:flutter/widgets.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/exif.model.dart'; -import 'package:immich_mobile/domain/models/store.model.dart'; -import 'package:immich_mobile/entities/asset.entity.dart' as asset_entity; -import 'package:immich_mobile/entities/store.entity.dart'; import 'package:immich_mobile/extensions/build_context_extensions.dart'; import 'package:immich_mobile/extensions/platform_extensions.dart'; import 'package:immich_mobile/extensions/response_extensions.dart'; import 'package:immich_mobile/repositories/asset_api.repository.dart'; -import 'package:immich_mobile/utils/hash.dart'; import 'package:logging/logging.dart'; import 'package:path_provider/path_provider.dart'; import 'package:photo_manager/photo_manager.dart'; @@ -50,39 +45,9 @@ class AssetMediaRepository { return PhotoManager.editor.deleteWithIds(ids); } - Future get(String id) async { + Future get(String id) async { final entity = await AssetEntity.fromId(id); - return toAsset(entity); - } - - static asset_entity.Asset? toAsset(AssetEntity? local) { - if (local == null) return null; - - final asset_entity.Asset asset = asset_entity.Asset( - checksum: "", - localId: local.id, - ownerId: fastHash(Store.get(StoreKey.currentUser).id), - fileCreatedAt: local.createDateTime, - fileModifiedAt: local.modifiedDateTime, - updatedAt: local.modifiedDateTime, - durationInSeconds: local.duration, - type: asset_entity.AssetType.values[local.typeInt], - fileName: local.title!, - width: local.width, - height: local.height, - isFavorite: local.isFavorite, - ); - - if (asset.fileCreatedAt.year == 1970) { - asset.fileCreatedAt = asset.fileModifiedAt; - } - - if (local.latitude != null) { - asset.exifInfo = ExifInfo(latitude: local.latitude, longitude: local.longitude); - } - - asset.local = local; - return asset; + return entity; } Future getOriginalFilename(String id) async { diff --git a/mobile/lib/repositories/auth.repository.dart b/mobile/lib/repositories/auth.repository.dart index a8544ef6c0..c16b728ae5 100644 --- a/mobile/lib/repositories/auth.repository.dart +++ b/mobile/lib/repositories/auth.repository.dart @@ -2,40 +2,21 @@ import 'dart:convert'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/domain/models/store.model.dart'; -import 'package:immich_mobile/entities/album.entity.dart'; -import 'package:immich_mobile/entities/asset.entity.dart'; -import 'package:immich_mobile/entities/etag.entity.dart'; import 'package:immich_mobile/entities/store.entity.dart'; -import 'package:immich_mobile/infrastructure/entities/exif.entity.dart'; -import 'package:immich_mobile/infrastructure/entities/user.entity.dart'; import 'package:immich_mobile/infrastructure/repositories/db.repository.dart'; import 'package:immich_mobile/infrastructure/repositories/sync_stream.repository.dart'; import 'package:immich_mobile/models/auth/auxilary_endpoint.model.dart'; -import 'package:immich_mobile/providers/db.provider.dart'; import 'package:immich_mobile/providers/infrastructure/db.provider.dart'; -import 'package:immich_mobile/repositories/database.repository.dart'; -final authRepositoryProvider = Provider( - (ref) => AuthRepository(ref.watch(dbProvider), ref.watch(driftProvider)), -); +final authRepositoryProvider = Provider((ref) => AuthRepository(ref.watch(driftProvider))); -class AuthRepository extends DatabaseRepository { +class AuthRepository { final Drift _drift; - const AuthRepository(super.db, this._drift); + const AuthRepository(this._drift); Future clearLocalData() async { await SyncStreamRepository(_drift).reset(); - - return db.writeTxn(() { - return Future.wait([ - db.assets.clear(), - db.exifInfos.clear(), - db.albums.clear(), - db.eTags.clear(), - db.users.clear(), - ]); - }); } bool getEndpointSwitchingFeature() { diff --git a/mobile/lib/repositories/backup.repository.dart b/mobile/lib/repositories/backup.repository.dart deleted file mode 100644 index 6cee6a4427..0000000000 --- a/mobile/lib/repositories/backup.repository.dart +++ /dev/null @@ -1,32 +0,0 @@ -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/entities/backup_album.entity.dart'; -import 'package:immich_mobile/providers/db.provider.dart'; -import 'package:immich_mobile/repositories/database.repository.dart'; -import 'package:isar/isar.dart'; - -enum BackupAlbumSort { id } - -final backupAlbumRepositoryProvider = Provider((ref) => BackupAlbumRepository(ref.watch(dbProvider))); - -class BackupAlbumRepository extends DatabaseRepository { - const BackupAlbumRepository(super.db); - - Future> getAll({BackupAlbumSort? sort}) { - final baseQuery = db.backupAlbums.where(); - final QueryBuilder query = switch (sort) { - null => baseQuery.noOp(), - BackupAlbumSort.id => baseQuery.sortById(), - }; - return query.findAll(); - } - - Future> getIdsBySelection(BackupSelection backup) => - db.backupAlbums.filter().selectionEqualTo(backup).idProperty().findAll(); - - Future> getAllBySelection(BackupSelection backup) => - db.backupAlbums.filter().selectionEqualTo(backup).findAll(); - - Future deleteAll(List ids) => txn(() => db.backupAlbums.deleteAll(ids)); - - Future updateAll(List backupAlbums) => txn(() => db.backupAlbums.putAll(backupAlbums)); -} diff --git a/mobile/lib/repositories/database.repository.dart b/mobile/lib/repositories/database.repository.dart deleted file mode 100644 index 71c15e1c40..0000000000 --- a/mobile/lib/repositories/database.repository.dart +++ /dev/null @@ -1,25 +0,0 @@ -import 'dart:async'; -import 'package:immich_mobile/interfaces/database.interface.dart'; -import 'package:isar/isar.dart'; - -/// copied from Isar; needed to check if an async transaction is already active -const Symbol _zoneTxn = #zoneTxn; - -abstract class DatabaseRepository implements IDatabaseRepository { - final Isar db; - const DatabaseRepository(this.db); - - bool get inTxn => Zone.current[_zoneTxn] != null; - - Future txn(Future Function() callback) => inTxn ? callback() : transaction(callback); - - @override - Future transaction(Future Function() callback) => db.writeTxn(callback); -} - -extension Asd on QueryBuilder { - QueryBuilder noOp() { - // ignore: invalid_use_of_protected_member - return QueryBuilder.apply(this, (query) => query); - } -} diff --git a/mobile/lib/repositories/etag.repository.dart b/mobile/lib/repositories/etag.repository.dart deleted file mode 100644 index 768d95b95c..0000000000 --- a/mobile/lib/repositories/etag.repository.dart +++ /dev/null @@ -1,27 +0,0 @@ -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/entities/etag.entity.dart'; -import 'package:immich_mobile/providers/db.provider.dart'; -import 'package:immich_mobile/repositories/database.repository.dart'; -import 'package:isar/isar.dart'; - -final etagRepositoryProvider = Provider((ref) => ETagRepository(ref.watch(dbProvider))); - -class ETagRepository extends DatabaseRepository { - const ETagRepository(super.db); - - Future> getAllIds() => db.eTags.where().idProperty().findAll(); - - Future get(String id) => db.eTags.getById(id); - - Future upsertAll(List etags) => txn(() => db.eTags.putAll(etags)); - - Future deleteByIds(List ids) => txn(() => db.eTags.deleteAllById(ids)); - - Future getById(String id) => db.eTags.getById(id); - - Future clearTable() async { - await txn(() async { - await db.eTags.clear(); - }); - } -} diff --git a/mobile/lib/repositories/file_media.repository.dart b/mobile/lib/repositories/file_media.repository.dart index f5cdb6d5c0..c54813a757 100644 --- a/mobile/lib/repositories/file_media.repository.dart +++ b/mobile/lib/repositories/file_media.repository.dart @@ -3,18 +3,12 @@ import 'dart:io'; import 'package:flutter/foundation.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/domain/models/asset/base_asset.model.dart'; -import 'package:immich_mobile/entities/asset.entity.dart' hide AssetType; -import 'package:immich_mobile/repositories/asset_media.repository.dart'; import 'package:photo_manager/photo_manager.dart' hide AssetType; final fileMediaRepositoryProvider = Provider((ref) => const FileMediaRepository()); class FileMediaRepository { const FileMediaRepository(); - Future saveImage(Uint8List data, {required String title, String? relativePath}) async { - final entity = await PhotoManager.editor.saveImage(data, filename: title, title: title, relativePath: relativePath); - return AssetMediaRepository.toAsset(entity); - } Future saveLocalAsset(Uint8List data, {required String title, String? relativePath}) async { final entity = await PhotoManager.editor.saveImage(data, filename: title, title: title, relativePath: relativePath); @@ -30,24 +24,18 @@ class FileMediaRepository { ); } - Future saveImageWithFile(String filePath, {String? title, String? relativePath}) async { + Future saveImageWithFile(String filePath, {String? title, String? relativePath}) async { final entity = await PhotoManager.editor.saveImageWithPath(filePath, title: title, relativePath: relativePath); - return AssetMediaRepository.toAsset(entity); + return entity; } - Future saveLivePhoto({required File image, required File video, required String title}) async { + Future saveLivePhoto({required File image, required File video, required String title}) async { final entity = await PhotoManager.editor.darwin.saveLivePhoto(imageFile: image, videoFile: video, title: title); - return AssetMediaRepository.toAsset(entity); + return entity; } - Future saveVideo(File file, {required String title, String? relativePath}) async { + Future saveVideo(File file, {required String title, String? relativePath}) async { final entity = await PhotoManager.editor.saveVideo(file, title: title, relativePath: relativePath); - return AssetMediaRepository.toAsset(entity); + return entity; } - - Future clearFileCache() => PhotoManager.clearFileCache(); - - Future enableBackgroundAccess() => PhotoManager.setIgnorePermissionCheck(true); - - Future requestExtendedPermissions() => PhotoManager.requestPermissionExtend(); } diff --git a/mobile/lib/repositories/partner.repository.dart b/mobile/lib/repositories/partner.repository.dart deleted file mode 100644 index 7f5ce62e0c..0000000000 --- a/mobile/lib/repositories/partner.repository.dart +++ /dev/null @@ -1,34 +0,0 @@ -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/domain/models/user.model.dart'; -import 'package:immich_mobile/infrastructure/entities/user.entity.dart' as entity; -import 'package:immich_mobile/providers/db.provider.dart'; -import 'package:immich_mobile/repositories/database.repository.dart'; -import 'package:isar/isar.dart'; - -final partnerRepositoryProvider = Provider((ref) => PartnerRepository(ref.watch(dbProvider))); - -class PartnerRepository extends DatabaseRepository { - const PartnerRepository(super.db); - - Future> getSharedBy() async { - return (await db.users.filter().isPartnerSharedByEqualTo(true).sortById().findAll()).map((u) => u.toDto()).toList(); - } - - Future> getSharedWith() async { - return (await db.users.filter().isPartnerSharedWithEqualTo(true).sortById().findAll()) - .map((u) => u.toDto()) - .toList(); - } - - Stream> watchSharedBy() { - return (db.users.filter().isPartnerSharedByEqualTo(true).sortById().watch()).map( - (users) => users.map((u) => u.toDto()).toList(), - ); - } - - Stream> watchSharedWith() { - return (db.users.filter().isPartnerSharedWithEqualTo(true).sortById().watch()).map( - (users) => users.map((u) => u.toDto()).toList(), - ); - } -} diff --git a/mobile/lib/repositories/timeline.repository.dart b/mobile/lib/repositories/timeline.repository.dart deleted file mode 100644 index c8c173b6f6..0000000000 --- a/mobile/lib/repositories/timeline.repository.dart +++ /dev/null @@ -1,146 +0,0 @@ -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/constants/enums.dart'; -import 'package:immich_mobile/entities/album.entity.dart'; -import 'package:immich_mobile/entities/asset.entity.dart'; -import 'package:immich_mobile/infrastructure/entities/user.entity.dart'; -import 'package:immich_mobile/providers/db.provider.dart'; -import 'package:immich_mobile/repositories/database.repository.dart'; -import 'package:immich_mobile/utils/hash.dart'; -import 'package:immich_mobile/widgets/asset_grid/asset_grid_data_structure.dart'; -import 'package:isar/isar.dart'; - -final timelineRepositoryProvider = Provider((ref) => TimelineRepository(ref.watch(dbProvider))); - -class TimelineRepository extends DatabaseRepository { - const TimelineRepository(super.db); - - Future> getTimelineUserIds(String id) { - return db.users.filter().inTimelineEqualTo(true).or().idEqualTo(id).idProperty().findAll(); - } - - Stream> watchTimelineUsers(String id) { - return db.users.filter().inTimelineEqualTo(true).or().idEqualTo(id).idProperty().watch(); - } - - Stream watchArchiveTimeline(String userId) { - final query = db.assets - .where() - .ownerIdEqualToAnyChecksum(fastHash(userId)) - .filter() - .isTrashedEqualTo(false) - .visibilityEqualTo(AssetVisibilityEnum.archive) - .sortByFileCreatedAtDesc(); - - return _watchRenderList(query, GroupAssetsBy.none); - } - - Stream watchFavoriteTimeline(String userId) { - final query = db.assets - .where() - .ownerIdEqualToAnyChecksum(fastHash(userId)) - .filter() - .isFavoriteEqualTo(true) - .not() - .visibilityEqualTo(AssetVisibilityEnum.locked) - .isTrashedEqualTo(false) - .sortByFileCreatedAtDesc(); - - return _watchRenderList(query, GroupAssetsBy.none); - } - - Stream watchAlbumTimeline(Album album, GroupAssetsBy groupAssetByOption) { - final query = album.assets.filter().isTrashedEqualTo(false).not().visibilityEqualTo(AssetVisibilityEnum.locked); - - final withSortedOption = switch (album.sortOrder) { - SortOrder.asc => query.sortByFileCreatedAt(), - SortOrder.desc => query.sortByFileCreatedAtDesc(), - }; - - return _watchRenderList(withSortedOption, groupAssetByOption); - } - - Stream watchTrashTimeline(String userId) { - final query = db.assets.filter().ownerIdEqualTo(fastHash(userId)).isTrashedEqualTo(true).sortByFileCreatedAtDesc(); - - return _watchRenderList(query, GroupAssetsBy.none); - } - - Stream watchAllVideosTimeline(String userId) { - final query = db.assets - .where() - .ownerIdEqualToAnyChecksum(fastHash(userId)) - .filter() - .isTrashedEqualTo(false) - .visibilityEqualTo(AssetVisibilityEnum.timeline) - .typeEqualTo(AssetType.video) - .sortByFileCreatedAtDesc(); - - return _watchRenderList(query, GroupAssetsBy.none); - } - - Stream watchHomeTimeline(String userId, GroupAssetsBy groupAssetByOption) { - final query = db.assets - .where() - .ownerIdEqualToAnyChecksum(fastHash(userId)) - .filter() - .isTrashedEqualTo(false) - .stackPrimaryAssetIdIsNull() - .visibilityEqualTo(AssetVisibilityEnum.timeline) - .sortByFileCreatedAtDesc(); - - return _watchRenderList(query, groupAssetByOption); - } - - Stream watchMultiUsersTimeline(List userIds, GroupAssetsBy groupAssetByOption) { - final isarUserIds = userIds.map(fastHash).toList(); - final query = db.assets - .where() - .anyOf(isarUserIds, (qb, id) => qb.ownerIdEqualToAnyChecksum(id)) - .filter() - .isTrashedEqualTo(false) - .visibilityEqualTo(AssetVisibilityEnum.timeline) - .stackPrimaryAssetIdIsNull() - .sortByFileCreatedAtDesc(); - return _watchRenderList(query, groupAssetByOption); - } - - Future getTimelineFromAssets(List assets, GroupAssetsBy getGroupByOption) { - return RenderList.fromAssets(assets, getGroupByOption); - } - - Stream watchAssetSelectionTimeline(String userId) { - final query = db.assets - .where() - .remoteIdIsNotNull() - .filter() - .ownerIdEqualTo(fastHash(userId)) - .visibilityEqualTo(AssetVisibilityEnum.timeline) - .isTrashedEqualTo(false) - .stackPrimaryAssetIdIsNull() - .sortByFileCreatedAtDesc(); - - return _watchRenderList(query, GroupAssetsBy.none); - } - - Stream watchLockedTimeline(String userId, GroupAssetsBy getGroupByOption) { - final query = db.assets - .where() - .ownerIdEqualToAnyChecksum(fastHash(userId)) - .filter() - .visibilityEqualTo(AssetVisibilityEnum.locked) - .isTrashedEqualTo(false) - .sortByFileCreatedAtDesc(); - - return _watchRenderList(query, getGroupByOption); - } - - Stream _watchRenderList( - QueryBuilder query, - GroupAssetsBy groupAssetsBy, - ) async* { - yield await RenderList.fromQuery(query, groupAssetsBy); - await for (final _ in query.watchLazy()) { - yield await RenderList.fromQuery(query, groupAssetsBy); - } - } -} diff --git a/mobile/lib/routing/app_navigation_observer.dart b/mobile/lib/routing/app_navigation_observer.dart index b05a28172d..b6b08d7831 100644 --- a/mobile/lib/routing/app_navigation_observer.dart +++ b/mobile/lib/routing/app_navigation_observer.dart @@ -19,7 +19,6 @@ class AppNavigationObserver extends AutoRouterObserver { @override void didPush(Route route, Route? previousRoute) { - _handleLockedViewState(route, previousRoute); _handleDriftLockedFolderState(route, previousRoute); Future(() { ref.read(currentRouteNameProvider.notifier).state = route.settings.name; @@ -28,21 +27,6 @@ class AppNavigationObserver extends AutoRouterObserver { }); } - _handleLockedViewState(Route route, Route? previousRoute) { - final isInLockedView = ref.read(inLockedViewProvider); - final isFromLockedViewToDetailView = - route.settings.name == GalleryViewerRoute.name && previousRoute?.settings.name == LockedRoute.name; - - final isFromDetailViewToInfoPanelView = - route.settings.name == null && previousRoute?.settings.name == GalleryViewerRoute.name && isInLockedView; - - if (route.settings.name == LockedRoute.name || isFromLockedViewToDetailView || isFromDetailViewToInfoPanelView) { - Future(() => ref.read(inLockedViewProvider.notifier).state = true); - } else { - Future(() => ref.read(inLockedViewProvider.notifier).state = false); - } - } - _handleDriftLockedFolderState(Route route, Route? previousRoute) { final isInLockedView = ref.read(inLockedViewProvider); final isFromLockedViewToDetailView = diff --git a/mobile/lib/routing/backup_permission_guard.dart b/mobile/lib/routing/backup_permission_guard.dart deleted file mode 100644 index f52516f2e5..0000000000 --- a/mobile/lib/routing/backup_permission_guard.dart +++ /dev/null @@ -1,21 +0,0 @@ -import 'dart:async'; - -import 'package:auto_route/auto_route.dart'; -import 'package:immich_mobile/providers/gallery_permission.provider.dart'; -import 'package:immich_mobile/routing/router.dart'; - -class BackupPermissionGuard extends AutoRouteGuard { - final GalleryPermissionNotifier _permission; - - const BackupPermissionGuard(this._permission); - - @override - void onNavigation(NavigationResolver resolver, StackRouter router) async { - final p = _permission.hasPermission; - if (p) { - resolver.next(true); - } else { - unawaited(router.push(const PermissionOnboardingRoute())); - } - } -} diff --git a/mobile/lib/routing/gallery_guard.dart b/mobile/lib/routing/gallery_guard.dart deleted file mode 100644 index 6a4b1bddab..0000000000 --- a/mobile/lib/routing/gallery_guard.dart +++ /dev/null @@ -1,34 +0,0 @@ -import 'dart:async'; - -import 'package:auto_route/auto_route.dart'; -import 'package:immich_mobile/routing/router.dart'; - -/// Handles duplicate navigation to this route (primarily for deep linking) -class GalleryGuard extends AutoRouteGuard { - const GalleryGuard(); - @override - void onNavigation(NavigationResolver resolver, StackRouter router) async { - final newRouteName = resolver.route.name; - final currentTopRouteName = router.stack.isNotEmpty ? router.stack.last.name : null; - - if (currentTopRouteName == newRouteName) { - // Replace instead of pushing duplicate - final args = resolver.route.args as GalleryViewerRouteArgs; - - unawaited( - router.replace( - GalleryViewerRoute( - renderList: args.renderList, - initialIndex: args.initialIndex, - heroOffset: args.heroOffset, - showStack: args.showStack, - ), - ), - ); - // Prevent further navigation since we replaced the route - resolver.next(false); - return; - } - resolver.next(true); - } -} diff --git a/mobile/lib/routing/router.dart b/mobile/lib/routing/router.dart index 90a17b1617..9c539a37a6 100644 --- a/mobile/lib/routing/router.dart +++ b/mobile/lib/routing/router.dart @@ -10,73 +10,28 @@ import 'package:immich_mobile/domain/models/memory.model.dart'; import 'package:immich_mobile/domain/models/person.model.dart'; import 'package:immich_mobile/domain/models/user.model.dart'; import 'package:immich_mobile/domain/services/timeline.service.dart'; -import 'package:immich_mobile/entities/album.entity.dart'; -import 'package:immich_mobile/entities/asset.entity.dart'; import 'package:immich_mobile/models/folder/recursive_folder.model.dart'; -import 'package:immich_mobile/models/memories/memory.model.dart'; -import 'package:immich_mobile/models/search/search_filter.model.dart'; import 'package:immich_mobile/models/shared_link/shared_link.model.dart'; import 'package:immich_mobile/models/upload/share_intent_attachment.model.dart'; -import 'package:immich_mobile/pages/album/album_additional_shared_user_selection.page.dart'; -import 'package:immich_mobile/pages/album/album_asset_selection.page.dart'; -import 'package:immich_mobile/pages/album/album_options.page.dart'; -import 'package:immich_mobile/pages/album/album_shared_user_selection.page.dart'; -import 'package:immich_mobile/pages/album/album_viewer.page.dart'; -import 'package:immich_mobile/pages/albums/albums.page.dart'; -import 'package:immich_mobile/pages/backup/album_preview.page.dart'; -import 'package:immich_mobile/pages/backup/backup_album_selection.page.dart'; -import 'package:immich_mobile/pages/backup/backup_controller.page.dart'; -import 'package:immich_mobile/pages/backup/backup_options.page.dart'; import 'package:immich_mobile/pages/backup/drift_backup.page.dart'; import 'package:immich_mobile/pages/backup/drift_backup_album_selection.page.dart'; import 'package:immich_mobile/pages/backup/drift_backup_asset_detail.page.dart'; import 'package:immich_mobile/pages/backup/drift_backup_options.page.dart'; import 'package:immich_mobile/pages/backup/drift_upload_detail.page.dart'; -import 'package:immich_mobile/pages/backup/failed_backup_status.page.dart'; -import 'package:immich_mobile/pages/common/activities.page.dart'; import 'package:immich_mobile/pages/common/app_log.page.dart'; import 'package:immich_mobile/pages/common/app_log_detail.page.dart'; -import 'package:immich_mobile/pages/common/change_experience.page.dart'; -import 'package:immich_mobile/pages/common/create_album.page.dart'; -import 'package:immich_mobile/pages/common/gallery_viewer.page.dart'; import 'package:immich_mobile/pages/common/headers_settings.page.dart'; -import 'package:immich_mobile/pages/common/native_video_viewer.page.dart'; import 'package:immich_mobile/pages/common/settings.page.dart'; import 'package:immich_mobile/pages/common/splash_screen.page.dart'; -import 'package:immich_mobile/pages/common/tab_controller.page.dart'; import 'package:immich_mobile/pages/common/tab_shell.page.dart'; -import 'package:immich_mobile/pages/editing/crop.page.dart'; -import 'package:immich_mobile/pages/editing/edit.page.dart'; -import 'package:immich_mobile/pages/editing/filter.page.dart'; -import 'package:immich_mobile/pages/library/archive.page.dart'; -import 'package:immich_mobile/pages/library/favorite.page.dart'; import 'package:immich_mobile/pages/library/folder/folder.page.dart'; -import 'package:immich_mobile/pages/library/library.page.dart'; -import 'package:immich_mobile/pages/library/local_albums.page.dart'; -import 'package:immich_mobile/pages/library/locked/locked.page.dart'; import 'package:immich_mobile/pages/library/locked/pin_auth.page.dart'; import 'package:immich_mobile/pages/library/partner/drift_partner.page.dart'; -import 'package:immich_mobile/pages/library/partner/partner.page.dart'; -import 'package:immich_mobile/pages/library/partner/partner_detail.page.dart'; -import 'package:immich_mobile/pages/library/people/people_collection.page.dart'; -import 'package:immich_mobile/pages/library/places/places_collection.page.dart'; import 'package:immich_mobile/pages/library/shared_link/shared_link.page.dart'; import 'package:immich_mobile/pages/library/shared_link/shared_link_edit.page.dart'; -import 'package:immich_mobile/pages/library/trash.page.dart'; import 'package:immich_mobile/pages/login/change_password.page.dart'; import 'package:immich_mobile/pages/login/login.page.dart'; -import 'package:immich_mobile/pages/onboarding/permission_onboarding.page.dart'; -import 'package:immich_mobile/pages/photos/memory.page.dart'; -import 'package:immich_mobile/pages/photos/photos.page.dart'; -import 'package:immich_mobile/pages/search/all_motion_videos.page.dart'; -import 'package:immich_mobile/pages/search/all_people.page.dart'; -import 'package:immich_mobile/pages/search/all_places.page.dart'; -import 'package:immich_mobile/pages/search/all_videos.page.dart'; -import 'package:immich_mobile/pages/search/map/map.page.dart'; import 'package:immich_mobile/pages/search/map/map_location_picker.page.dart'; -import 'package:immich_mobile/pages/search/person_result.page.dart'; -import 'package:immich_mobile/pages/search/recently_taken.page.dart'; -import 'package:immich_mobile/pages/search/search.page.dart'; import 'package:immich_mobile/pages/settings/sync_status.page.dart'; import 'package:immich_mobile/pages/share_intent/share_intent.page.dart'; import 'package:immich_mobile/presentation/pages/cleanup_preview.page.dart'; @@ -114,15 +69,11 @@ import 'package:immich_mobile/presentation/widgets/asset_viewer/asset_viewer.pag import 'package:immich_mobile/providers/api.provider.dart'; import 'package:immich_mobile/providers/gallery_permission.provider.dart'; import 'package:immich_mobile/routing/auth_guard.dart'; -import 'package:immich_mobile/routing/backup_permission_guard.dart'; -import 'package:immich_mobile/routing/custom_transition_builders.dart'; import 'package:immich_mobile/routing/duplicate_guard.dart'; -import 'package:immich_mobile/routing/gallery_guard.dart'; import 'package:immich_mobile/routing/locked_guard.dart'; import 'package:immich_mobile/services/api.service.dart'; import 'package:immich_mobile/services/local_auth.service.dart'; import 'package:immich_mobile/services/secure_storage.service.dart'; -import 'package:immich_mobile/widgets/asset_grid/asset_grid_data_structure.dart'; import 'package:maplibre_gl/maplibre_gl.dart'; part 'router.gr.dart'; @@ -140,9 +91,7 @@ final appRouterProvider = Provider( class AppRouter extends RootStackRouter { late final AuthGuard _authGuard; late final DuplicateGuard _duplicateGuard; - late final BackupPermissionGuard _backupPermissionGuard; late final LockedGuard _lockedGuard; - late final GalleryGuard _galleryGuard; AppRouter( ApiService apiService, @@ -153,8 +102,6 @@ class AppRouter extends RootStackRouter { _authGuard = AuthGuard(apiService); _duplicateGuard = const DuplicateGuard(); _lockedGuard = LockedGuard(apiService, secureStorageService, localAuthService); - _backupPermissionGuard = BackupPermissionGuard(galleryPermissionNotifier); - _galleryGuard = const GalleryGuard(); } @override @@ -163,20 +110,8 @@ class AppRouter extends RootStackRouter { @override late final List routes = [ AutoRoute(page: SplashScreenRoute.page, initial: true), - AutoRoute(page: PermissionOnboardingRoute.page, guards: [_authGuard, _duplicateGuard]), AutoRoute(page: LoginRoute.page), AutoRoute(page: ChangePasswordRoute.page), - AutoRoute(page: SearchRoute.page, guards: [_authGuard, _duplicateGuard], maintainState: false), - AutoRoute( - page: TabControllerRoute.page, - guards: [_authGuard, _duplicateGuard], - children: [ - AutoRoute(page: PhotosRoute.page, guards: [_authGuard, _duplicateGuard]), - AutoRoute(page: SearchRoute.page, guards: [_authGuard, _duplicateGuard], maintainState: false), - AutoRoute(page: LibraryRoute.page, guards: [_authGuard, _duplicateGuard]), - AutoRoute(page: AlbumsRoute.page, guards: [_authGuard, _duplicateGuard]), - ], - ), AutoRoute( page: TabShellRoute.page, guards: [_authGuard, _duplicateGuard], @@ -187,105 +122,17 @@ class AppRouter extends RootStackRouter { AutoRoute(page: DriftAlbumsRoute.page, guards: [_authGuard, _duplicateGuard]), ], ), - CustomRoute( - page: GalleryViewerRoute.page, - guards: [_authGuard, _galleryGuard], - transitionsBuilder: CustomTransitionsBuilders.zoomedPage, - ), - AutoRoute(page: BackupControllerRoute.page, guards: [_authGuard, _duplicateGuard, _backupPermissionGuard]), - AutoRoute(page: AllPlacesRoute.page, guards: [_authGuard, _duplicateGuard]), - AutoRoute(page: CreateAlbumRoute.page, guards: [_authGuard, _duplicateGuard]), - AutoRoute(page: EditImageRoute.page), - AutoRoute(page: CropImageRoute.page), - AutoRoute(page: FilterImageRoute.page), AutoRoute(page: ProfilePictureCropRoute.page), - CustomRoute( - page: FavoritesRoute.page, - guards: [_authGuard, _duplicateGuard], - transitionsBuilder: TransitionsBuilders.slideLeft, - ), - AutoRoute(page: AllVideosRoute.page, guards: [_authGuard, _duplicateGuard]), - AutoRoute(page: AllMotionPhotosRoute.page, guards: [_authGuard, _duplicateGuard]), - AutoRoute(page: RecentlyTakenRoute.page, guards: [_authGuard, _duplicateGuard]), - CustomRoute( - page: AlbumAssetSelectionRoute.page, - guards: [_authGuard, _duplicateGuard], - transitionsBuilder: TransitionsBuilders.slideBottom, - ), - CustomRoute( - page: AlbumSharedUserSelectionRoute.page, - guards: [_authGuard, _duplicateGuard], - transitionsBuilder: TransitionsBuilders.slideBottom, - ), - AutoRoute(page: AlbumViewerRoute.page, guards: [_authGuard, _duplicateGuard]), - CustomRoute( - page: AlbumAdditionalSharedUserSelectionRoute.page, - guards: [_authGuard, _duplicateGuard], - transitionsBuilder: TransitionsBuilders.slideBottom, - ), - AutoRoute(page: BackupAlbumSelectionRoute.page, guards: [_authGuard, _duplicateGuard]), - AutoRoute(page: AlbumPreviewRoute.page, guards: [_authGuard, _duplicateGuard]), - CustomRoute( - page: FailedBackupStatusRoute.page, - guards: [_authGuard, _duplicateGuard], - transitionsBuilder: TransitionsBuilders.slideBottom, - ), AutoRoute(page: SettingsRoute.page, guards: [_duplicateGuard]), AutoRoute(page: SettingsSubRoute.page, guards: [_duplicateGuard]), AutoRoute(page: AppLogRoute.page, guards: [_duplicateGuard]), AutoRoute(page: AppLogDetailRoute.page, guards: [_duplicateGuard]), - CustomRoute( - page: ArchiveRoute.page, - guards: [_authGuard, _duplicateGuard], - transitionsBuilder: TransitionsBuilders.slideLeft, - ), - CustomRoute( - page: PartnerRoute.page, - guards: [_authGuard, _duplicateGuard], - transitionsBuilder: TransitionsBuilders.slideLeft, - ), AutoRoute(page: FolderRoute.page, guards: [_authGuard]), - AutoRoute(page: PartnerDetailRoute.page, guards: [_authGuard, _duplicateGuard]), - AutoRoute(page: PersonResultRoute.page, guards: [_authGuard, _duplicateGuard]), - AutoRoute(page: AllPeopleRoute.page, guards: [_authGuard, _duplicateGuard]), - AutoRoute(page: MemoryRoute.page, guards: [_authGuard, _duplicateGuard]), - AutoRoute(page: MapRoute.page, guards: [_authGuard, _duplicateGuard]), - AutoRoute(page: AlbumOptionsRoute.page, guards: [_authGuard, _duplicateGuard]), - AutoRoute(page: TrashRoute.page, guards: [_authGuard, _duplicateGuard]), AutoRoute(page: SharedLinkRoute.page, guards: [_authGuard, _duplicateGuard]), AutoRoute(page: SharedLinkEditRoute.page, guards: [_authGuard, _duplicateGuard]), - CustomRoute( - page: ActivitiesRoute.page, - guards: [_authGuard, _duplicateGuard], - transitionsBuilder: TransitionsBuilders.slideLeft, - durationInMilliseconds: 200, - ), CustomRoute(page: MapLocationPickerRoute.page, guards: [_authGuard, _duplicateGuard]), - AutoRoute(page: BackupOptionsRoute.page, guards: [_authGuard, _duplicateGuard]), AutoRoute(page: HeaderSettingsRoute.page, guards: [_duplicateGuard]), - CustomRoute( - page: PeopleCollectionRoute.page, - guards: [_authGuard, _duplicateGuard], - transitionsBuilder: TransitionsBuilders.slideLeft, - ), - CustomRoute( - page: AlbumsRoute.page, - guards: [_authGuard, _duplicateGuard], - transitionsBuilder: TransitionsBuilders.slideLeft, - ), - CustomRoute( - page: LocalAlbumsRoute.page, - guards: [_authGuard, _duplicateGuard], - transitionsBuilder: TransitionsBuilders.slideLeft, - ), - CustomRoute( - page: PlacesCollectionRoute.page, - guards: [_authGuard, _duplicateGuard], - transitionsBuilder: TransitionsBuilders.slideLeft, - ), - AutoRoute(page: NativeVideoViewerRoute.page, guards: [_authGuard, _duplicateGuard]), AutoRoute(page: ShareIntentRoute.page, guards: [_authGuard, _duplicateGuard]), - AutoRoute(page: LockedRoute.page, guards: [_authGuard, _lockedGuard, _duplicateGuard]), AutoRoute(page: PinAuthRoute.page, guards: [_authGuard, _duplicateGuard]), AutoRoute(page: LocalMediaSummaryRoute.page, guards: [_authGuard, _duplicateGuard]), AutoRoute(page: RemoteMediaSummaryRoute.page, guards: [_authGuard, _duplicateGuard]), @@ -322,7 +169,6 @@ class AppRouter extends RootStackRouter { AutoRoute(page: DriftPlaceRoute.page, guards: [_authGuard, _duplicateGuard]), AutoRoute(page: DriftPlaceDetailRoute.page, guards: [_authGuard, _duplicateGuard]), AutoRoute(page: DriftUserSelectionRoute.page, guards: [_authGuard, _duplicateGuard]), - AutoRoute(page: ChangeExperienceRoute.page, guards: [_authGuard, _duplicateGuard]), AutoRoute(page: DriftPartnerRoute.page, guards: [_authGuard, _duplicateGuard]), AutoRoute(page: DriftUploadDetailRoute.page, guards: [_authGuard, _duplicateGuard]), AutoRoute(page: SyncStatusRoute.page, guards: [_duplicateGuard]), diff --git a/mobile/lib/routing/router.gr.dart b/mobile/lib/routing/router.gr.dart index 18b16c23ef..07c3a52b49 100644 --- a/mobile/lib/routing/router.gr.dart +++ b/mobile/lib/routing/router.gr.dart @@ -10,330 +10,6 @@ part of 'router.dart'; -/// generated route for -/// [ActivitiesPage] -class ActivitiesRoute extends PageRouteInfo { - const ActivitiesRoute({List? children}) - : super(ActivitiesRoute.name, initialChildren: children); - - static const String name = 'ActivitiesRoute'; - - static PageInfo page = PageInfo( - name, - builder: (data) { - return const ActivitiesPage(); - }, - ); -} - -/// generated route for -/// [AlbumAdditionalSharedUserSelectionPage] -class AlbumAdditionalSharedUserSelectionRoute - extends PageRouteInfo { - AlbumAdditionalSharedUserSelectionRoute({ - Key? key, - required Album album, - List? children, - }) : super( - AlbumAdditionalSharedUserSelectionRoute.name, - args: AlbumAdditionalSharedUserSelectionRouteArgs( - key: key, - album: album, - ), - initialChildren: children, - ); - - static const String name = 'AlbumAdditionalSharedUserSelectionRoute'; - - static PageInfo page = PageInfo( - name, - builder: (data) { - final args = data.argsAs(); - return AlbumAdditionalSharedUserSelectionPage( - key: args.key, - album: args.album, - ); - }, - ); -} - -class AlbumAdditionalSharedUserSelectionRouteArgs { - const AlbumAdditionalSharedUserSelectionRouteArgs({ - this.key, - required this.album, - }); - - final Key? key; - - final Album album; - - @override - String toString() { - return 'AlbumAdditionalSharedUserSelectionRouteArgs{key: $key, album: $album}'; - } -} - -/// generated route for -/// [AlbumAssetSelectionPage] -class AlbumAssetSelectionRoute - extends PageRouteInfo { - AlbumAssetSelectionRoute({ - Key? key, - required Set existingAssets, - bool canDeselect = false, - List? children, - }) : super( - AlbumAssetSelectionRoute.name, - args: AlbumAssetSelectionRouteArgs( - key: key, - existingAssets: existingAssets, - canDeselect: canDeselect, - ), - initialChildren: children, - ); - - static const String name = 'AlbumAssetSelectionRoute'; - - static PageInfo page = PageInfo( - name, - builder: (data) { - final args = data.argsAs(); - return AlbumAssetSelectionPage( - key: args.key, - existingAssets: args.existingAssets, - canDeselect: args.canDeselect, - ); - }, - ); -} - -class AlbumAssetSelectionRouteArgs { - const AlbumAssetSelectionRouteArgs({ - this.key, - required this.existingAssets, - this.canDeselect = false, - }); - - final Key? key; - - final Set existingAssets; - - final bool canDeselect; - - @override - String toString() { - return 'AlbumAssetSelectionRouteArgs{key: $key, existingAssets: $existingAssets, canDeselect: $canDeselect}'; - } -} - -/// generated route for -/// [AlbumOptionsPage] -class AlbumOptionsRoute extends PageRouteInfo { - const AlbumOptionsRoute({List? children}) - : super(AlbumOptionsRoute.name, initialChildren: children); - - static const String name = 'AlbumOptionsRoute'; - - static PageInfo page = PageInfo( - name, - builder: (data) { - return const AlbumOptionsPage(); - }, - ); -} - -/// generated route for -/// [AlbumPreviewPage] -class AlbumPreviewRoute extends PageRouteInfo { - AlbumPreviewRoute({ - Key? key, - required Album album, - List? children, - }) : super( - AlbumPreviewRoute.name, - args: AlbumPreviewRouteArgs(key: key, album: album), - initialChildren: children, - ); - - static const String name = 'AlbumPreviewRoute'; - - static PageInfo page = PageInfo( - name, - builder: (data) { - final args = data.argsAs(); - return AlbumPreviewPage(key: args.key, album: args.album); - }, - ); -} - -class AlbumPreviewRouteArgs { - const AlbumPreviewRouteArgs({this.key, required this.album}); - - final Key? key; - - final Album album; - - @override - String toString() { - return 'AlbumPreviewRouteArgs{key: $key, album: $album}'; - } -} - -/// generated route for -/// [AlbumSharedUserSelectionPage] -class AlbumSharedUserSelectionRoute - extends PageRouteInfo { - AlbumSharedUserSelectionRoute({ - Key? key, - required Set assets, - List? children, - }) : super( - AlbumSharedUserSelectionRoute.name, - args: AlbumSharedUserSelectionRouteArgs(key: key, assets: assets), - initialChildren: children, - ); - - static const String name = 'AlbumSharedUserSelectionRoute'; - - static PageInfo page = PageInfo( - name, - builder: (data) { - final args = data.argsAs(); - return AlbumSharedUserSelectionPage(key: args.key, assets: args.assets); - }, - ); -} - -class AlbumSharedUserSelectionRouteArgs { - const AlbumSharedUserSelectionRouteArgs({this.key, required this.assets}); - - final Key? key; - - final Set assets; - - @override - String toString() { - return 'AlbumSharedUserSelectionRouteArgs{key: $key, assets: $assets}'; - } -} - -/// generated route for -/// [AlbumViewerPage] -class AlbumViewerRoute extends PageRouteInfo { - AlbumViewerRoute({ - Key? key, - required int albumId, - List? children, - }) : super( - AlbumViewerRoute.name, - args: AlbumViewerRouteArgs(key: key, albumId: albumId), - initialChildren: children, - ); - - static const String name = 'AlbumViewerRoute'; - - static PageInfo page = PageInfo( - name, - builder: (data) { - final args = data.argsAs(); - return AlbumViewerPage(key: args.key, albumId: args.albumId); - }, - ); -} - -class AlbumViewerRouteArgs { - const AlbumViewerRouteArgs({this.key, required this.albumId}); - - final Key? key; - - final int albumId; - - @override - String toString() { - return 'AlbumViewerRouteArgs{key: $key, albumId: $albumId}'; - } -} - -/// generated route for -/// [AlbumsPage] -class AlbumsRoute extends PageRouteInfo { - const AlbumsRoute({List? children}) - : super(AlbumsRoute.name, initialChildren: children); - - static const String name = 'AlbumsRoute'; - - static PageInfo page = PageInfo( - name, - builder: (data) { - return const AlbumsPage(); - }, - ); -} - -/// generated route for -/// [AllMotionPhotosPage] -class AllMotionPhotosRoute extends PageRouteInfo { - const AllMotionPhotosRoute({List? children}) - : super(AllMotionPhotosRoute.name, initialChildren: children); - - static const String name = 'AllMotionPhotosRoute'; - - static PageInfo page = PageInfo( - name, - builder: (data) { - return const AllMotionPhotosPage(); - }, - ); -} - -/// generated route for -/// [AllPeoplePage] -class AllPeopleRoute extends PageRouteInfo { - const AllPeopleRoute({List? children}) - : super(AllPeopleRoute.name, initialChildren: children); - - static const String name = 'AllPeopleRoute'; - - static PageInfo page = PageInfo( - name, - builder: (data) { - return const AllPeoplePage(); - }, - ); -} - -/// generated route for -/// [AllPlacesPage] -class AllPlacesRoute extends PageRouteInfo { - const AllPlacesRoute({List? children}) - : super(AllPlacesRoute.name, initialChildren: children); - - static const String name = 'AllPlacesRoute'; - - static PageInfo page = PageInfo( - name, - builder: (data) { - return const AllPlacesPage(); - }, - ); -} - -/// generated route for -/// [AllVideosPage] -class AllVideosRoute extends PageRouteInfo { - const AllVideosRoute({List? children}) - : super(AllVideosRoute.name, initialChildren: children); - - static const String name = 'AllVideosRoute'; - - static PageInfo page = PageInfo( - name, - builder: (data) { - return const AllVideosPage(); - }, - ); -} - /// generated route for /// [AppLogDetailPage] class AppLogDetailRoute extends PageRouteInfo { @@ -387,22 +63,6 @@ class AppLogRoute extends PageRouteInfo { ); } -/// generated route for -/// [ArchivePage] -class ArchiveRoute extends PageRouteInfo { - const ArchiveRoute({List? children}) - : super(ArchiveRoute.name, initialChildren: children); - - static const String name = 'ArchiveRoute'; - - static PageInfo page = PageInfo( - name, - builder: (data) { - return const ArchivePage(); - }, - ); -} - /// generated route for /// [AssetTroubleshootPage] class AssetTroubleshootRoute extends PageRouteInfo { @@ -504,97 +164,6 @@ class AssetViewerRouteArgs { } } -/// generated route for -/// [BackupAlbumSelectionPage] -class BackupAlbumSelectionRoute extends PageRouteInfo { - const BackupAlbumSelectionRoute({List? children}) - : super(BackupAlbumSelectionRoute.name, initialChildren: children); - - static const String name = 'BackupAlbumSelectionRoute'; - - static PageInfo page = PageInfo( - name, - builder: (data) { - return const BackupAlbumSelectionPage(); - }, - ); -} - -/// generated route for -/// [BackupControllerPage] -class BackupControllerRoute extends PageRouteInfo { - const BackupControllerRoute({List? children}) - : super(BackupControllerRoute.name, initialChildren: children); - - static const String name = 'BackupControllerRoute'; - - static PageInfo page = PageInfo( - name, - builder: (data) { - return const BackupControllerPage(); - }, - ); -} - -/// generated route for -/// [BackupOptionsPage] -class BackupOptionsRoute extends PageRouteInfo { - const BackupOptionsRoute({List? children}) - : super(BackupOptionsRoute.name, initialChildren: children); - - static const String name = 'BackupOptionsRoute'; - - static PageInfo page = PageInfo( - name, - builder: (data) { - return const BackupOptionsPage(); - }, - ); -} - -/// generated route for -/// [ChangeExperiencePage] -class ChangeExperienceRoute extends PageRouteInfo { - ChangeExperienceRoute({ - Key? key, - required bool switchingToBeta, - List? children, - }) : super( - ChangeExperienceRoute.name, - args: ChangeExperienceRouteArgs( - key: key, - switchingToBeta: switchingToBeta, - ), - initialChildren: children, - ); - - static const String name = 'ChangeExperienceRoute'; - - static PageInfo page = PageInfo( - name, - builder: (data) { - final args = data.argsAs(); - return ChangeExperiencePage( - key: args.key, - switchingToBeta: args.switchingToBeta, - ); - }, - ); -} - -class ChangeExperienceRouteArgs { - const ChangeExperienceRouteArgs({this.key, required this.switchingToBeta}); - - final Key? key; - - final bool switchingToBeta; - - @override - String toString() { - return 'ChangeExperienceRouteArgs{key: $key, switchingToBeta: $switchingToBeta}'; - } -} - /// generated route for /// [ChangePasswordPage] class ChangePasswordRoute extends PageRouteInfo { @@ -648,89 +217,6 @@ class CleanupPreviewRouteArgs { } } -/// generated route for -/// [CreateAlbumPage] -class CreateAlbumRoute extends PageRouteInfo { - CreateAlbumRoute({ - Key? key, - List? assets, - List? children, - }) : super( - CreateAlbumRoute.name, - args: CreateAlbumRouteArgs(key: key, assets: assets), - initialChildren: children, - ); - - static const String name = 'CreateAlbumRoute'; - - static PageInfo page = PageInfo( - name, - builder: (data) { - final args = data.argsAs( - orElse: () => const CreateAlbumRouteArgs(), - ); - return CreateAlbumPage(key: args.key, assets: args.assets); - }, - ); -} - -class CreateAlbumRouteArgs { - const CreateAlbumRouteArgs({this.key, this.assets}); - - final Key? key; - - final List? assets; - - @override - String toString() { - return 'CreateAlbumRouteArgs{key: $key, assets: $assets}'; - } -} - -/// generated route for -/// [CropImagePage] -class CropImageRoute extends PageRouteInfo { - CropImageRoute({ - Key? key, - required Image image, - required Asset asset, - List? children, - }) : super( - CropImageRoute.name, - args: CropImageRouteArgs(key: key, image: image, asset: asset), - initialChildren: children, - ); - - static const String name = 'CropImageRoute'; - - static PageInfo page = PageInfo( - name, - builder: (data) { - final args = data.argsAs(); - return CropImagePage(key: args.key, image: args.image, asset: args.asset); - }, - ); -} - -class CropImageRouteArgs { - const CropImageRouteArgs({ - this.key, - required this.image, - required this.asset, - }); - - final Key? key; - - final Image image; - - final Asset asset; - - @override - String toString() { - return 'CropImageRouteArgs{key: $key, image: $image, asset: $asset}'; - } -} - /// generated route for /// [DownloadInfoPage] class DownloadInfoRoute extends PageRouteInfo { @@ -1514,144 +1000,6 @@ class DriftVideoRoute extends PageRouteInfo { ); } -/// generated route for -/// [EditImagePage] -class EditImageRoute extends PageRouteInfo { - EditImageRoute({ - Key? key, - required Asset asset, - required Image image, - required bool isEdited, - List? children, - }) : super( - EditImageRoute.name, - args: EditImageRouteArgs( - key: key, - asset: asset, - image: image, - isEdited: isEdited, - ), - initialChildren: children, - ); - - static const String name = 'EditImageRoute'; - - static PageInfo page = PageInfo( - name, - builder: (data) { - final args = data.argsAs(); - return EditImagePage( - key: args.key, - asset: args.asset, - image: args.image, - isEdited: args.isEdited, - ); - }, - ); -} - -class EditImageRouteArgs { - const EditImageRouteArgs({ - this.key, - required this.asset, - required this.image, - required this.isEdited, - }); - - final Key? key; - - final Asset asset; - - final Image image; - - final bool isEdited; - - @override - String toString() { - return 'EditImageRouteArgs{key: $key, asset: $asset, image: $image, isEdited: $isEdited}'; - } -} - -/// generated route for -/// [FailedBackupStatusPage] -class FailedBackupStatusRoute extends PageRouteInfo { - const FailedBackupStatusRoute({List? children}) - : super(FailedBackupStatusRoute.name, initialChildren: children); - - static const String name = 'FailedBackupStatusRoute'; - - static PageInfo page = PageInfo( - name, - builder: (data) { - return const FailedBackupStatusPage(); - }, - ); -} - -/// generated route for -/// [FavoritesPage] -class FavoritesRoute extends PageRouteInfo { - const FavoritesRoute({List? children}) - : super(FavoritesRoute.name, initialChildren: children); - - static const String name = 'FavoritesRoute'; - - static PageInfo page = PageInfo( - name, - builder: (data) { - return const FavoritesPage(); - }, - ); -} - -/// generated route for -/// [FilterImagePage] -class FilterImageRoute extends PageRouteInfo { - FilterImageRoute({ - Key? key, - required Image image, - required Asset asset, - List? children, - }) : super( - FilterImageRoute.name, - args: FilterImageRouteArgs(key: key, image: image, asset: asset), - initialChildren: children, - ); - - static const String name = 'FilterImageRoute'; - - static PageInfo page = PageInfo( - name, - builder: (data) { - final args = data.argsAs(); - return FilterImagePage( - key: args.key, - image: args.image, - asset: args.asset, - ); - }, - ); -} - -class FilterImageRouteArgs { - const FilterImageRouteArgs({ - this.key, - required this.image, - required this.asset, - }); - - final Key? key; - - final Image image; - - final Asset asset; - - @override - String toString() { - return 'FilterImageRouteArgs{key: $key, image: $image, asset: $asset}'; - } -} - /// generated route for /// [FolderPage] class FolderRoute extends PageRouteInfo { @@ -1691,70 +1039,6 @@ class FolderRouteArgs { } } -/// generated route for -/// [GalleryViewerPage] -class GalleryViewerRoute extends PageRouteInfo { - GalleryViewerRoute({ - Key? key, - required RenderList renderList, - int initialIndex = 0, - int heroOffset = 0, - bool showStack = false, - List? children, - }) : super( - GalleryViewerRoute.name, - args: GalleryViewerRouteArgs( - key: key, - renderList: renderList, - initialIndex: initialIndex, - heroOffset: heroOffset, - showStack: showStack, - ), - initialChildren: children, - ); - - static const String name = 'GalleryViewerRoute'; - - static PageInfo page = PageInfo( - name, - builder: (data) { - final args = data.argsAs(); - return GalleryViewerPage( - key: args.key, - renderList: args.renderList, - initialIndex: args.initialIndex, - heroOffset: args.heroOffset, - showStack: args.showStack, - ); - }, - ); -} - -class GalleryViewerRouteArgs { - const GalleryViewerRouteArgs({ - this.key, - required this.renderList, - this.initialIndex = 0, - this.heroOffset = 0, - this.showStack = false, - }); - - final Key? key; - - final RenderList renderList; - - final int initialIndex; - - final int heroOffset; - - final bool showStack; - - @override - String toString() { - return 'GalleryViewerRouteArgs{key: $key, renderList: $renderList, initialIndex: $initialIndex, heroOffset: $heroOffset, showStack: $showStack}'; - } -} - /// generated route for /// [HeaderSettingsPage] class HeaderSettingsRoute extends PageRouteInfo { @@ -1771,38 +1055,6 @@ class HeaderSettingsRoute extends PageRouteInfo { ); } -/// generated route for -/// [LibraryPage] -class LibraryRoute extends PageRouteInfo { - const LibraryRoute({List? children}) - : super(LibraryRoute.name, initialChildren: children); - - static const String name = 'LibraryRoute'; - - static PageInfo page = PageInfo( - name, - builder: (data) { - return const LibraryPage(); - }, - ); -} - -/// generated route for -/// [LocalAlbumsPage] -class LocalAlbumsRoute extends PageRouteInfo { - const LocalAlbumsRoute({List? children}) - : super(LocalAlbumsRoute.name, initialChildren: children); - - static const String name = 'LocalAlbumsRoute'; - - static PageInfo page = PageInfo( - name, - builder: (data) { - return const LocalAlbumsPage(); - }, - ); -} - /// generated route for /// [LocalMediaSummaryPage] class LocalMediaSummaryRoute extends PageRouteInfo { @@ -1856,22 +1108,6 @@ class LocalTimelineRouteArgs { } } -/// generated route for -/// [LockedPage] -class LockedRoute extends PageRouteInfo { - const LockedRoute({List? children}) - : super(LockedRoute.name, initialChildren: children); - - static const String name = 'LockedRoute'; - - static PageInfo page = PageInfo( - name, - builder: (data) { - return const LockedPage(); - }, - ); -} - /// generated route for /// [LoginPage] class LoginRoute extends PageRouteInfo { @@ -1952,311 +1188,6 @@ class MapLocationPickerRouteArgs { } } -/// generated route for -/// [MapPage] -class MapRoute extends PageRouteInfo { - MapRoute({Key? key, LatLng? initialLocation, List? children}) - : super( - MapRoute.name, - args: MapRouteArgs(key: key, initialLocation: initialLocation), - initialChildren: children, - ); - - static const String name = 'MapRoute'; - - static PageInfo page = PageInfo( - name, - builder: (data) { - final args = data.argsAs( - orElse: () => const MapRouteArgs(), - ); - return MapPage(key: args.key, initialLocation: args.initialLocation); - }, - ); -} - -class MapRouteArgs { - const MapRouteArgs({this.key, this.initialLocation}); - - final Key? key; - - final LatLng? initialLocation; - - @override - String toString() { - return 'MapRouteArgs{key: $key, initialLocation: $initialLocation}'; - } -} - -/// generated route for -/// [MemoryPage] -class MemoryRoute extends PageRouteInfo { - MemoryRoute({ - required List memories, - required int memoryIndex, - Key? key, - List? children, - }) : super( - MemoryRoute.name, - args: MemoryRouteArgs( - memories: memories, - memoryIndex: memoryIndex, - key: key, - ), - initialChildren: children, - ); - - static const String name = 'MemoryRoute'; - - static PageInfo page = PageInfo( - name, - builder: (data) { - final args = data.argsAs(); - return MemoryPage( - memories: args.memories, - memoryIndex: args.memoryIndex, - key: args.key, - ); - }, - ); -} - -class MemoryRouteArgs { - const MemoryRouteArgs({ - required this.memories, - required this.memoryIndex, - this.key, - }); - - final List memories; - - final int memoryIndex; - - final Key? key; - - @override - String toString() { - return 'MemoryRouteArgs{memories: $memories, memoryIndex: $memoryIndex, key: $key}'; - } -} - -/// generated route for -/// [NativeVideoViewerPage] -class NativeVideoViewerRoute extends PageRouteInfo { - NativeVideoViewerRoute({ - Key? key, - required Asset asset, - required Widget image, - bool showControls = true, - int playbackDelayFactor = 1, - List? children, - }) : super( - NativeVideoViewerRoute.name, - args: NativeVideoViewerRouteArgs( - key: key, - asset: asset, - image: image, - showControls: showControls, - playbackDelayFactor: playbackDelayFactor, - ), - initialChildren: children, - ); - - static const String name = 'NativeVideoViewerRoute'; - - static PageInfo page = PageInfo( - name, - builder: (data) { - final args = data.argsAs(); - return NativeVideoViewerPage( - key: args.key, - asset: args.asset, - image: args.image, - showControls: args.showControls, - playbackDelayFactor: args.playbackDelayFactor, - ); - }, - ); -} - -class NativeVideoViewerRouteArgs { - const NativeVideoViewerRouteArgs({ - this.key, - required this.asset, - required this.image, - this.showControls = true, - this.playbackDelayFactor = 1, - }); - - final Key? key; - - final Asset asset; - - final Widget image; - - final bool showControls; - - final int playbackDelayFactor; - - @override - String toString() { - return 'NativeVideoViewerRouteArgs{key: $key, asset: $asset, image: $image, showControls: $showControls, playbackDelayFactor: $playbackDelayFactor}'; - } -} - -/// generated route for -/// [PartnerDetailPage] -class PartnerDetailRoute extends PageRouteInfo { - PartnerDetailRoute({ - Key? key, - required UserDto partner, - List? children, - }) : super( - PartnerDetailRoute.name, - args: PartnerDetailRouteArgs(key: key, partner: partner), - initialChildren: children, - ); - - static const String name = 'PartnerDetailRoute'; - - static PageInfo page = PageInfo( - name, - builder: (data) { - final args = data.argsAs(); - return PartnerDetailPage(key: args.key, partner: args.partner); - }, - ); -} - -class PartnerDetailRouteArgs { - const PartnerDetailRouteArgs({this.key, required this.partner}); - - final Key? key; - - final UserDto partner; - - @override - String toString() { - return 'PartnerDetailRouteArgs{key: $key, partner: $partner}'; - } -} - -/// generated route for -/// [PartnerPage] -class PartnerRoute extends PageRouteInfo { - const PartnerRoute({List? children}) - : super(PartnerRoute.name, initialChildren: children); - - static const String name = 'PartnerRoute'; - - static PageInfo page = PageInfo( - name, - builder: (data) { - return const PartnerPage(); - }, - ); -} - -/// generated route for -/// [PeopleCollectionPage] -class PeopleCollectionRoute extends PageRouteInfo { - const PeopleCollectionRoute({List? children}) - : super(PeopleCollectionRoute.name, initialChildren: children); - - static const String name = 'PeopleCollectionRoute'; - - static PageInfo page = PageInfo( - name, - builder: (data) { - return const PeopleCollectionPage(); - }, - ); -} - -/// generated route for -/// [PermissionOnboardingPage] -class PermissionOnboardingRoute extends PageRouteInfo { - const PermissionOnboardingRoute({List? children}) - : super(PermissionOnboardingRoute.name, initialChildren: children); - - static const String name = 'PermissionOnboardingRoute'; - - static PageInfo page = PageInfo( - name, - builder: (data) { - return const PermissionOnboardingPage(); - }, - ); -} - -/// generated route for -/// [PersonResultPage] -class PersonResultRoute extends PageRouteInfo { - PersonResultRoute({ - Key? key, - required String personId, - required String personName, - List? children, - }) : super( - PersonResultRoute.name, - args: PersonResultRouteArgs( - key: key, - personId: personId, - personName: personName, - ), - initialChildren: children, - ); - - static const String name = 'PersonResultRoute'; - - static PageInfo page = PageInfo( - name, - builder: (data) { - final args = data.argsAs(); - return PersonResultPage( - key: args.key, - personId: args.personId, - personName: args.personName, - ); - }, - ); -} - -class PersonResultRouteArgs { - const PersonResultRouteArgs({ - this.key, - required this.personId, - required this.personName, - }); - - final Key? key; - - final String personId; - - final String personName; - - @override - String toString() { - return 'PersonResultRouteArgs{key: $key, personId: $personId, personName: $personName}'; - } -} - -/// generated route for -/// [PhotosPage] -class PhotosRoute extends PageRouteInfo { - const PhotosRoute({List? children}) - : super(PhotosRoute.name, initialChildren: children); - - static const String name = 'PhotosRoute'; - - static PageInfo page = PageInfo( - name, - builder: (data) { - return const PhotosPage(); - }, - ); -} - /// generated route for /// [PinAuthPage] class PinAuthRoute extends PageRouteInfo { @@ -2296,51 +1227,6 @@ class PinAuthRouteArgs { } } -/// generated route for -/// [PlacesCollectionPage] -class PlacesCollectionRoute extends PageRouteInfo { - PlacesCollectionRoute({ - Key? key, - LatLng? currentLocation, - List? children, - }) : super( - PlacesCollectionRoute.name, - args: PlacesCollectionRouteArgs( - key: key, - currentLocation: currentLocation, - ), - initialChildren: children, - ); - - static const String name = 'PlacesCollectionRoute'; - - static PageInfo page = PageInfo( - name, - builder: (data) { - final args = data.argsAs( - orElse: () => const PlacesCollectionRouteArgs(), - ); - return PlacesCollectionPage( - key: args.key, - currentLocation: args.currentLocation, - ); - }, - ); -} - -class PlacesCollectionRouteArgs { - const PlacesCollectionRouteArgs({this.key, this.currentLocation}); - - final Key? key; - - final LatLng? currentLocation; - - @override - String toString() { - return 'PlacesCollectionRouteArgs{key: $key, currentLocation: $currentLocation}'; - } -} - /// generated route for /// [ProfilePictureCropPage] class ProfilePictureCropRoute @@ -2379,22 +1265,6 @@ class ProfilePictureCropRouteArgs { } } -/// generated route for -/// [RecentlyTakenPage] -class RecentlyTakenRoute extends PageRouteInfo { - const RecentlyTakenRoute({List? children}) - : super(RecentlyTakenRoute.name, initialChildren: children); - - static const String name = 'RecentlyTakenRoute'; - - static PageInfo page = PageInfo( - name, - builder: (data) { - return const RecentlyTakenPage(); - }, - ); -} - /// generated route for /// [RemoteAlbumPage] class RemoteAlbumRoute extends PageRouteInfo { @@ -2448,45 +1318,6 @@ class RemoteMediaSummaryRoute extends PageRouteInfo { ); } -/// generated route for -/// [SearchPage] -class SearchRoute extends PageRouteInfo { - SearchRoute({ - Key? key, - SearchFilter? prefilter, - List? children, - }) : super( - SearchRoute.name, - args: SearchRouteArgs(key: key, prefilter: prefilter), - initialChildren: children, - ); - - static const String name = 'SearchRoute'; - - static PageInfo page = PageInfo( - name, - builder: (data) { - final args = data.argsAs( - orElse: () => const SearchRouteArgs(), - ); - return SearchPage(key: args.key, prefilter: args.prefilter); - }, - ); -} - -class SearchRouteArgs { - const SearchRouteArgs({this.key, this.prefilter}); - - final Key? key; - - final SearchFilter? prefilter; - - @override - String toString() { - return 'SearchRouteArgs{key: $key, prefilter: $prefilter}'; - } -} - /// generated route for /// [SettingsPage] class SettingsRoute extends PageRouteInfo { @@ -2685,22 +1516,6 @@ class SyncStatusRoute extends PageRouteInfo { ); } -/// generated route for -/// [TabControllerPage] -class TabControllerRoute extends PageRouteInfo { - const TabControllerRoute({List? children}) - : super(TabControllerRoute.name, initialChildren: children); - - static const String name = 'TabControllerRoute'; - - static PageInfo page = PageInfo( - name, - builder: (data) { - return const TabControllerPage(); - }, - ); -} - /// generated route for /// [TabShellPage] class TabShellRoute extends PageRouteInfo { @@ -2716,19 +1531,3 @@ class TabShellRoute extends PageRouteInfo { }, ); } - -/// generated route for -/// [TrashPage] -class TrashRoute extends PageRouteInfo { - const TrashRoute({List? children}) - : super(TrashRoute.name, initialChildren: children); - - static const String name = 'TrashRoute'; - - static PageInfo page = PageInfo( - name, - builder: (data) { - return const TrashPage(); - }, - ); -} diff --git a/mobile/lib/services/activity.service.dart b/mobile/lib/services/activity.service.dart index 382a7fe107..0ef1badacb 100644 --- a/mobile/lib/services/activity.service.dart +++ b/mobile/lib/services/activity.service.dart @@ -9,7 +9,6 @@ import 'package:immich_mobile/providers/infrastructure/current_album.provider.da import 'package:immich_mobile/repositories/activity_api.repository.dart'; import 'package:immich_mobile/routing/router.dart'; import 'package:logging/logging.dart'; -import 'package:immich_mobile/entities/store.entity.dart' as immich_store; class ActivityService with ErrorLoggerMixin { final ActivityApiRepository _activityApiRepository; @@ -60,20 +59,16 @@ class ActivityService with ErrorLoggerMixin { } Future buildAssetViewerRoute(String assetId, WidgetRef ref) async { - if (immich_store.Store.isBetaTimelineEnabled) { - final asset = await _assetService.getRemoteAsset(assetId); - if (asset == null) { - return null; - } - - AssetViewer.setAsset(ref, asset); - return AssetViewerRoute( - initialIndex: 0, - timelineService: _timelineFactory.fromAssets([asset], TimelineOrigin.albumActivities), - currentAlbum: ref.read(currentRemoteAlbumProvider), - ); + final asset = await _assetService.getRemoteAsset(assetId); + if (asset == null) { + return null; } - return null; + AssetViewer.setAsset(ref, asset); + return AssetViewerRoute( + initialIndex: 0, + timelineService: _timelineFactory.fromAssets([asset], TimelineOrigin.albumActivities), + currentAlbum: ref.read(currentRemoteAlbumProvider), + ); } } diff --git a/mobile/lib/services/album.service.dart b/mobile/lib/services/album.service.dart deleted file mode 100644 index 8d77b569e6..0000000000 --- a/mobile/lib/services/album.service.dart +++ /dev/null @@ -1,425 +0,0 @@ -import 'dart:async'; -import 'dart:collection'; -import 'dart:io'; - -import 'package:collection/collection.dart'; -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/constants/enums.dart'; -import 'package:immich_mobile/domain/models/user.model.dart'; -import 'package:immich_mobile/domain/services/user.service.dart'; -import 'package:immich_mobile/entities/album.entity.dart'; -import 'package:immich_mobile/entities/asset.entity.dart'; -import 'package:immich_mobile/entities/backup_album.entity.dart'; -import 'package:immich_mobile/infrastructure/entities/user.entity.dart' as entity; -import 'package:immich_mobile/models/albums/album_add_asset_response.model.dart'; -import 'package:immich_mobile/models/albums/album_search.model.dart'; -import 'package:immich_mobile/providers/infrastructure/user.provider.dart'; -import 'package:immich_mobile/repositories/album.repository.dart'; -import 'package:immich_mobile/repositories/album_api.repository.dart'; -import 'package:immich_mobile/repositories/album_media.repository.dart'; -import 'package:immich_mobile/repositories/asset.repository.dart'; -import 'package:immich_mobile/repositories/backup.repository.dart'; -import 'package:immich_mobile/services/entity.service.dart'; -import 'package:immich_mobile/services/sync.service.dart'; -import 'package:immich_mobile/utils/hash.dart'; -import 'package:logging/logging.dart'; -import 'package:immich_mobile/utils/debug_print.dart'; - -final albumServiceProvider = Provider( - (ref) => AlbumService( - ref.watch(syncServiceProvider), - ref.watch(userServiceProvider), - ref.watch(entityServiceProvider), - ref.watch(albumRepositoryProvider), - ref.watch(assetRepositoryProvider), - ref.watch(backupAlbumRepositoryProvider), - ref.watch(albumMediaRepositoryProvider), - ref.watch(albumApiRepositoryProvider), - ), -); - -class AlbumService { - final SyncService _syncService; - final UserService _userService; - final EntityService _entityService; - final AlbumRepository _albumRepository; - final AssetRepository _assetRepository; - final BackupAlbumRepository _backupAlbumRepository; - final AlbumMediaRepository _albumMediaRepository; - final AlbumApiRepository _albumApiRepository; - final Logger _log = Logger('AlbumService'); - Completer _localCompleter = Completer()..complete(false); - Completer _remoteCompleter = Completer()..complete(false); - - AlbumService( - this._syncService, - this._userService, - this._entityService, - this._albumRepository, - this._assetRepository, - this._backupAlbumRepository, - this._albumMediaRepository, - this._albumApiRepository, - ); - - /// Checks all selected device albums for changes of albums and their assets - /// Updates the local database and returns `true` if there were any changes - Future refreshDeviceAlbums() async { - if (!_localCompleter.isCompleted) { - // guard against concurrent calls - _log.info("refreshDeviceAlbums is already in progress"); - return _localCompleter.future; - } - _localCompleter = Completer(); - final Stopwatch sw = Stopwatch()..start(); - bool changes = false; - try { - final (selectedIds, excludedIds, onDevice) = await ( - _backupAlbumRepository.getIdsBySelection(BackupSelection.select).then((value) => value.toSet()), - _backupAlbumRepository.getIdsBySelection(BackupSelection.exclude).then((value) => value.toSet()), - _albumMediaRepository.getAll(), - ).wait; - _log.info("Found ${onDevice.length} device albums"); - if (selectedIds.isEmpty) { - final numLocal = await _albumRepository.count(local: true); - if (numLocal > 0) { - await _syncService.removeAllLocalAlbumsAndAssets(); - } - return false; - } - Set? excludedAssets; - if (excludedIds.isNotEmpty) { - if (Platform.isIOS) { - // iOS and Android device album working principle differ significantly - // on iOS, an asset can be in multiple albums - // on Android, an asset can only be in exactly one album (folder!) at the same time - // thus, on Android, excluding an album can be done by ignoring that album - // however, on iOS, it it necessary to load the assets from all excluded - // albums and check every asset from any selected album against the set - // of excluded assets - excludedAssets = await _loadExcludedAssetIds(onDevice, excludedIds); - _log.info("Found ${excludedAssets.length} assets to exclude"); - } - // remove all excluded albums - onDevice.removeWhere((e) => excludedIds.contains(e.localId)); - _log.info("Ignoring ${excludedIds.length} excluded albums resulting in ${onDevice.length} device albums"); - } - - final allAlbum = onDevice.firstWhereOrNull((album) => album.isAll); - final hasAll = allAlbum != null && selectedIds.contains(allAlbum.localId); - if (hasAll) { - if (Platform.isAndroid) { - // remove the virtual "Recent" album and keep and individual albums - // on Android, the virtual "Recent" `lastModified` value is always null - onDevice.removeWhere((album) => album.isAll); - _log.info("'Recents' is selected, keeping all individual albums"); - } - } else { - // keep only the explicitly selected albums - onDevice.removeWhere((album) => !selectedIds.contains(album.localId)); - _log.info("'Recents' is not selected, keeping only selected albums"); - } - changes = await _syncService.syncLocalAlbumAssetsToDb(onDevice, excludedAssets); - _log.info("Syncing completed. Changes: $changes"); - } finally { - _localCompleter.complete(changes); - } - dPrint(() => "refreshDeviceAlbums took ${sw.elapsedMilliseconds}ms"); - return changes; - } - - Future> _loadExcludedAssetIds(List albums, Set excludedAlbumIds) async { - final Set result = HashSet(); - for (final batchAlbums in albums.where((album) => excludedAlbumIds.contains(album.localId)).slices(5)) { - await batchAlbums - .map((album) => _albumMediaRepository.getAssetIds(album.localId!).then((assetIds) => result.addAll(assetIds))) - .wait; - } - return result; - } - - /// Checks remote albums (owned if `isShared` is false) for changes, - /// updates the local database and returns `true` if there were any changes - Future refreshRemoteAlbums() async { - if (!_remoteCompleter.isCompleted) { - // guard against concurrent calls - return _remoteCompleter.future; - } - _remoteCompleter = Completer(); - final Stopwatch sw = Stopwatch()..start(); - bool changes = false; - try { - final users = await _syncService.getUsersFromServer(); - if (users != null) { - await _syncService.syncUsersFromServer(users); - } - final (sharedAlbum, ownedAlbum) = await ( - // Note: `shared: true` is required to get albums that don't belong to - // us due to unusual behaviour on the API but this will also return our - // own shared albums - _albumApiRepository.getAll(shared: true), - // Passing null (or nothing) for `shared` returns only albums that - // explicitly belong to us - _albumApiRepository.getAll(shared: null), - ).wait; - - final albums = HashSet(equals: (a, b) => a.remoteId == b.remoteId, hashCode: (a) => a.remoteId.hashCode); - - albums.addAll(sharedAlbum); - albums.addAll(ownedAlbum); - - changes = await _syncService.syncRemoteAlbumsToDb(albums.toList()); - } finally { - _remoteCompleter.complete(changes); - } - dPrint(() => "refreshRemoteAlbums took ${sw.elapsedMilliseconds}ms"); - return changes; - } - - Future createAlbum( - String albumName, - Iterable assets, [ - Iterable sharedUsers = const [], - ]) async { - final Album album = await _albumApiRepository.create( - albumName, - assetIds: assets.map((asset) => asset.remoteId!), - sharedUserIds: sharedUsers.map((user) => user.id), - ); - await _entityService.fillAlbumWithDatabaseEntities(album); - return _albumRepository.create(album); - } - - /* - * Creates names like Untitled, Untitled (1), Untitled (2), ... - */ - Future _getNextAlbumName() async { - const baseName = "Untitled"; - for (int round = 0; ; round++) { - final proposedName = "$baseName${round == 0 ? "" : " ($round)"}"; - - if (null == await _albumRepository.getByName(proposedName, owner: true)) { - return proposedName; - } - } - } - - Future createAlbumWithGeneratedName(Iterable assets) async { - return createAlbum(await _getNextAlbumName(), assets, []); - } - - Future addAssets(Album album, Iterable assets) async { - try { - final result = await _albumApiRepository.addAssets(album.remoteId!, assets.map((asset) => asset.remoteId!)); - - final List addedAssets = result.added - .map((id) => assets.firstWhere((asset) => asset.remoteId == id)) - .toList(); - - await _updateAssets(album.id, add: addedAssets); - - return AlbumAddAssetsResponse(alreadyInAlbum: result.duplicates, successfullyAdded: addedAssets.length); - } catch (e) { - dPrint(() => "Error addAssets ${e.toString()}"); - } - return null; - } - - Future _updateAssets(int albumId, {List add = const [], List remove = const []}) => - _albumRepository.transaction(() async { - final album = await _albumRepository.get(albumId); - if (album == null) return; - await _albumRepository.addAssets(album, add); - await _albumRepository.removeAssets(album, remove); - await _albumRepository.recalculateMetadata(album); - await _albumRepository.update(album); - }); - - Future setActivityStatus(Album album, bool enabled) async { - try { - final updatedAlbum = await _albumApiRepository.update(album.remoteId!, activityEnabled: enabled); - album.activityEnabled = updatedAlbum.activityEnabled; - await _albumRepository.update(album); - return true; - } catch (e) { - dPrint(() => "Error setActivityEnabled ${e.toString()}"); - } - return false; - } - - Future deleteAlbum(Album album) async { - try { - final userId = _userService.getMyUser().id; - if (album.owner.value?.isarId == fastHash(userId)) { - await _albumApiRepository.delete(album.remoteId!); - } - if (album.shared) { - final foreignAssets = await _assetRepository.getByAlbum(album, notOwnedBy: [userId]); - await _albumRepository.delete(album.id); - - final List albums = await _albumRepository.getAll(shared: true); - final List existing = []; - for (Album album in albums) { - existing.addAll(await _assetRepository.getByAlbum(album, notOwnedBy: [userId])); - } - final List idsToRemove = _syncService.sharedAssetsToRemove(foreignAssets, existing); - if (idsToRemove.isNotEmpty) { - await _assetRepository.deleteByIds(idsToRemove); - } - } else { - await _albumRepository.delete(album.id); - } - return true; - } catch (e) { - dPrint(() => "Error deleteAlbum ${e.toString()}"); - } - return false; - } - - Future leaveAlbum(Album album) async { - try { - await _albumApiRepository.removeUser(album.remoteId!, userId: "me"); - return true; - } catch (e) { - dPrint(() => "Error leaveAlbum ${e.toString()}"); - return false; - } - } - - Future removeAsset(Album album, Iterable assets) async { - try { - final result = await _albumApiRepository.removeAssets(album.remoteId!, assets.map((asset) => asset.remoteId!)); - final toRemove = result.removed.map((id) => assets.firstWhere((asset) => asset.remoteId == id)); - await _updateAssets(album.id, remove: toRemove.toList()); - return true; - } catch (e) { - dPrint(() => "Error removeAssetFromAlbum ${e.toString()}"); - } - return false; - } - - Future removeUser(Album album, UserDto user) async { - try { - await _albumApiRepository.removeUser(album.remoteId!, userId: user.id); - - album.sharedUsers.remove(entity.User.fromDto(user)); - await _albumRepository.removeUsers(album, [user]); - final a = await _albumRepository.get(album.id); - // trigger watcher - await _albumRepository.update(a!); - - return true; - } catch (error) { - dPrint(() => "Error removeUser ${error.toString()}"); - return false; - } - } - - Future addUsers(Album album, List userIds) async { - try { - final updatedAlbum = await _albumApiRepository.addUsers(album.remoteId!, userIds); - - album.sharedUsers.addAll(updatedAlbum.remoteUsers); - album.shared = true; - - await _albumRepository.addUsers(album, album.sharedUsers.map((u) => u.toDto()).toList()); - await _albumRepository.update(album); - - return true; - } catch (error) { - dPrint(() => "Error addUsers ${error.toString()}"); - } - return false; - } - - Future changeTitleAlbum(Album album, String newAlbumTitle) async { - try { - final updatedAlbum = await _albumApiRepository.update(album.remoteId!, name: newAlbumTitle); - - album.name = updatedAlbum.name; - await _albumRepository.update(album); - return true; - } catch (e) { - dPrint(() => "Error changeTitleAlbum ${e.toString()}"); - return false; - } - } - - Future changeDescriptionAlbum(Album album, String newAlbumDescription) async { - try { - final updatedAlbum = await _albumApiRepository.update(album.remoteId!, description: newAlbumDescription); - - album.description = updatedAlbum.description; - await _albumRepository.update(album); - return true; - } catch (e) { - dPrint(() => "Error changeDescriptionAlbum ${e.toString()}"); - return false; - } - } - - Future getAlbumByName(String name, {bool? remote, bool? shared, bool? owner}) => - _albumRepository.getByName(name, remote: remote, shared: shared, owner: owner); - - /// - /// Add the uploaded asset to the selected albums - /// - Future syncUploadAlbums(List albumNames, List assetIds) async { - for (final albumName in albumNames) { - Album? album = await getAlbumByName(albumName, remote: true, owner: true); - album ??= await createAlbum(albumName, []); - if (album != null && album.remoteId != null) { - await _albumApiRepository.addAssets(album.remoteId!, assetIds); - } - } - } - - Future> getAllRemoteAlbums() async { - return _albumRepository.getAll(remote: true); - } - - Future> getAllLocalAlbums() async { - return _albumRepository.getAll(remote: false); - } - - Stream> watchRemoteAlbums() { - return _albumRepository.watchRemoteAlbums(); - } - - Stream> watchLocalAlbums() { - return _albumRepository.watchLocalAlbums(); - } - - /// Get album by Isar ID - Future getAlbumById(int id) { - return _albumRepository.get(id); - } - - Future getAlbumByRemoteId(String remoteId) { - return _albumRepository.getByRemoteId(remoteId); - } - - Stream watchAlbum(int id) { - return _albumRepository.watchAlbum(id); - } - - Future> search(String searchTerm, QuickFilterMode filterMode) async { - return _albumRepository.search(searchTerm, filterMode); - } - - Future updateSortOrder(Album album, SortOrder order) async { - try { - final updateAlbum = await _albumApiRepository.update(album.remoteId!, sortOrder: order); - album.sortOrder = updateAlbum.sortOrder; - - return _albumRepository.update(album); - } catch (error, stackTrace) { - _log.severe("Error updating album sort order", error, stackTrace); - } - return null; - } - - Future clearTable() async { - await _albumRepository.clearTable(); - } -} diff --git a/mobile/lib/services/asset.service.dart b/mobile/lib/services/asset.service.dart deleted file mode 100644 index b9fab35442..0000000000 --- a/mobile/lib/services/asset.service.dart +++ /dev/null @@ -1,465 +0,0 @@ -import 'dart:async'; - -import 'package:collection/collection.dart'; -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/constants/enums.dart'; -import 'package:immich_mobile/domain/models/user.model.dart'; -import 'package:immich_mobile/domain/services/user.service.dart'; -import 'package:immich_mobile/entities/asset.entity.dart'; -import 'package:immich_mobile/entities/backup_album.entity.dart'; -import 'package:immich_mobile/infrastructure/repositories/exif.repository.dart'; -import 'package:immich_mobile/infrastructure/repositories/user.repository.dart'; -import 'package:immich_mobile/models/backup/backup_candidate.model.dart'; -import 'package:immich_mobile/providers/api.provider.dart'; -import 'package:immich_mobile/providers/infrastructure/exif.provider.dart'; -import 'package:immich_mobile/providers/infrastructure/user.provider.dart'; -import 'package:immich_mobile/repositories/asset.repository.dart'; -import 'package:immich_mobile/repositories/asset_api.repository.dart'; -import 'package:immich_mobile/repositories/asset_media.repository.dart'; -import 'package:immich_mobile/repositories/backup.repository.dart'; -import 'package:immich_mobile/repositories/etag.repository.dart'; -import 'package:immich_mobile/services/album.service.dart'; -import 'package:immich_mobile/services/api.service.dart'; -import 'package:immich_mobile/services/backup.service.dart'; -import 'package:immich_mobile/services/sync.service.dart'; -import 'package:logging/logging.dart'; -import 'package:maplibre_gl/maplibre_gl.dart'; -import 'package:openapi/api.dart'; -import 'package:immich_mobile/utils/debug_print.dart'; - -final assetServiceProvider = Provider( - (ref) => AssetService( - ref.watch(assetApiRepositoryProvider), - ref.watch(assetRepositoryProvider), - ref.watch(exifRepositoryProvider), - ref.watch(userRepositoryProvider), - ref.watch(etagRepositoryProvider), - ref.watch(backupAlbumRepositoryProvider), - ref.watch(apiServiceProvider), - ref.watch(syncServiceProvider), - ref.watch(backupServiceProvider), - ref.watch(albumServiceProvider), - ref.watch(userServiceProvider), - ref.watch(assetMediaRepositoryProvider), - ), -); - -class AssetService { - final AssetApiRepository _assetApiRepository; - final AssetRepository _assetRepository; - final IsarExifRepository _exifInfoRepository; - final IsarUserRepository _isarUserRepository; - final ETagRepository _etagRepository; - final BackupAlbumRepository _backupRepository; - final ApiService _apiService; - final SyncService _syncService; - final BackupService _backupService; - final AlbumService _albumService; - final UserService _userService; - final AssetMediaRepository _assetMediaRepository; - final log = Logger('AssetService'); - - AssetService( - this._assetApiRepository, - this._assetRepository, - this._exifInfoRepository, - this._isarUserRepository, - this._etagRepository, - this._backupRepository, - this._apiService, - this._syncService, - this._backupService, - this._albumService, - this._userService, - this._assetMediaRepository, - ); - - /// Checks the server for updated assets and updates the local database if - /// required. Returns `true` if there were any changes. - Future refreshRemoteAssets() async { - final syncedUserIds = await _etagRepository.getAllIds(); - final List syncedUsers = syncedUserIds.isEmpty - ? [] - : (await _isarUserRepository.getByUserIds(syncedUserIds)).nonNulls.toList(); - final Stopwatch sw = Stopwatch()..start(); - final bool changes = await _syncService.syncRemoteAssetsToDb( - users: syncedUsers, - getChangedAssets: _getRemoteAssetChanges, - loadAssets: _getRemoteAssets, - ); - dPrint(() => "refreshRemoteAssets full took ${sw.elapsedMilliseconds}ms"); - return changes; - } - - /// Returns `(null, null)` if changes are invalid -> requires full sync - Future<(List? toUpsert, List? toDelete)> _getRemoteAssetChanges( - List users, - DateTime since, - ) async { - final dto = AssetDeltaSyncDto(updatedAfter: since, userIds: users.map((e) => e.id).toList()); - final changes = await _apiService.syncApi.getDeltaSync(dto); - return changes == null || changes.needsFullSync - ? (null, null) - : (changes.upserted.map(Asset.remote).toList(), changes.deleted); - } - - /// Returns the list of people of the given asset id. - // If the server is not reachable `null` is returned. - Future?> getRemotePeopleOfAsset(String remoteId) async { - try { - final AssetResponseDto? dto = await _apiService.assetsApi.getAssetInfo(remoteId); - - return dto?.people; - } catch (error, stack) { - log.severe('Error while getting remote asset info: ${error.toString()}', error, stack); - - return null; - } - } - - /// Returns `null` if the server state did not change, else list of assets - Future?> _getRemoteAssets(UserDto user, DateTime until) async { - const int chunkSize = 10000; - try { - final List allAssets = []; - String? lastId; - // will break on error or once all assets are loaded - while (true) { - final dto = AssetFullSyncDto(limit: chunkSize, updatedUntil: until, lastId: lastId, userId: user.id); - log.fine("Requesting $chunkSize assets from $lastId"); - final List? assets = await _apiService.syncApi.getFullSyncForUser(dto); - if (assets == null) return null; - log.fine("Received ${assets.length} assets from ${assets.firstOrNull?.id} to ${assets.lastOrNull?.id}"); - allAssets.addAll(assets.map(Asset.remote)); - if (assets.length != chunkSize) break; - lastId = assets.last.id; - } - return allAssets; - } catch (error, stack) { - log.severe('Error while getting remote assets', error, stack); - return null; - } - } - - /// Loads the exif information from the database. If there is none, loads - /// the exif info from the server (remote assets only) - Future loadExif(Asset a) async { - a.exifInfo ??= (await _exifInfoRepository.get(a.id)); - // fileSize is always filled on the server but not set on client - if (a.exifInfo?.fileSize == null) { - if (a.isRemote) { - final dto = await _apiService.assetsApi.getAssetInfo(a.remoteId!); - if (dto != null && dto.exifInfo != null) { - final newExif = Asset.remote(dto).exifInfo!.copyWith(assetId: a.id); - a.exifInfo = newExif; - if (newExif != a.exifInfo) { - if (a.isInDb) { - await _assetRepository.transaction(() => _assetRepository.update(a)); - } else { - dPrint(() => "[loadExif] parameter Asset is not from DB!"); - } - } - } - } else { - // TODO implement local exif info parsing - } - } - return a; - } - - Future updateAssets(List assets, UpdateAssetDto updateAssetDto) async { - return await _apiService.assetsApi.updateAssets( - AssetBulkUpdateDto( - ids: assets.map((e) => e.remoteId!).toList(), - dateTimeOriginal: updateAssetDto.dateTimeOriginal, - isFavorite: updateAssetDto.isFavorite, - visibility: updateAssetDto.visibility, - latitude: updateAssetDto.latitude, - longitude: updateAssetDto.longitude, - ), - ); - } - - Future> changeFavoriteStatus(List assets, bool isFavorite) async { - try { - await updateAssets(assets, UpdateAssetDto(isFavorite: isFavorite)); - - for (var element in assets) { - element.isFavorite = isFavorite; - } - - await _syncService.upsertAssetsWithExif(assets); - - return assets; - } catch (error, stack) { - log.severe("Error while changing favorite status", error, stack); - return []; - } - } - - Future> changeArchiveStatus(List assets, bool isArchived) async { - try { - await updateAssets( - assets, - UpdateAssetDto(visibility: isArchived ? AssetVisibility.archive : AssetVisibility.timeline), - ); - - for (var element in assets) { - element.isArchived = isArchived; - element.visibility = isArchived ? AssetVisibilityEnum.archive : AssetVisibilityEnum.timeline; - } - - await _syncService.upsertAssetsWithExif(assets); - - return assets; - } catch (error, stack) { - log.severe("Error while changing archive status", error, stack); - return []; - } - } - - Future?> changeDateTime(List assets, String updatedDt) async { - try { - await updateAssets(assets, UpdateAssetDto(dateTimeOriginal: updatedDt)); - - for (var element in assets) { - element.fileCreatedAt = DateTime.parse(updatedDt); - element.exifInfo = element.exifInfo?.copyWith(dateTimeOriginal: DateTime.parse(updatedDt)); - } - - await _syncService.upsertAssetsWithExif(assets); - - return assets; - } catch (error, stack) { - log.severe("Error while changing date/time status", error, stack); - return Future.value(null); - } - } - - Future?> changeLocation(List assets, LatLng location) async { - try { - await updateAssets(assets, UpdateAssetDto(latitude: location.latitude, longitude: location.longitude)); - - for (var element in assets) { - element.exifInfo = element.exifInfo?.copyWith(latitude: location.latitude, longitude: location.longitude); - } - - await _syncService.upsertAssetsWithExif(assets); - - return assets; - } catch (error, stack) { - log.severe("Error while changing location status", error, stack); - return Future.value(null); - } - } - - Future syncUploadedAssetToAlbums() async { - try { - final selectedAlbums = await _backupRepository.getAllBySelection(BackupSelection.select); - final excludedAlbums = await _backupRepository.getAllBySelection(BackupSelection.exclude); - - final candidates = await _backupService.buildUploadCandidates( - selectedAlbums, - excludedAlbums, - useTimeFilter: false, - ); - - await refreshRemoteAssets(); - final owner = _userService.getMyUser(); - final remoteAssets = await _assetRepository.getAll(ownerId: owner.id, state: AssetState.merged); - - /// Map - Map> assetToAlbums = {}; - - for (BackupCandidate candidate in candidates) { - final asset = remoteAssets.firstWhereOrNull((a) => a.localId == candidate.asset.localId); - - if (asset != null) { - for (final albumName in candidate.albumNames) { - assetToAlbums.putIfAbsent(albumName, () => []).add(asset.remoteId!); - } - } - } - - // Upload assets to albums - for (final entry in assetToAlbums.entries) { - final albumName = entry.key; - final assetIds = entry.value; - - await _albumService.syncUploadAlbums([albumName], assetIds); - } - } catch (error, stack) { - log.severe("Error while syncing uploaded asset to albums", error, stack); - } - } - - Future setDescription(Asset asset, String newDescription) async { - final remoteAssetId = asset.remoteId; - final localExifId = asset.exifInfo?.assetId; - - // Guard [remoteAssetId] and [localExifId] null - if (remoteAssetId == null || localExifId == null) { - return; - } - - final result = await _assetApiRepository.update(remoteAssetId, description: newDescription); - - final description = result.exifInfo?.description; - - if (description != null) { - var exifInfo = await _exifInfoRepository.get(localExifId); - - if (exifInfo != null) { - await _exifInfoRepository.update(exifInfo.copyWith(description: description)); - } - } - } - - Future getDescription(Asset asset) async { - final localExifId = asset.exifInfo?.assetId; - - // Guard [remoteAssetId] and [localExifId] null - if (localExifId == null) { - return ""; - } - - final exifInfo = await _exifInfoRepository.get(localExifId); - - return exifInfo?.description ?? ""; - } - - Future getAspectRatio(Asset asset) async { - if (asset.isRemote) { - asset = await loadExif(asset); - } else if (asset.isLocal) { - await asset.localAsync; - } - - final aspectRatio = asset.aspectRatio; - if (aspectRatio != null) { - return aspectRatio; - } - - final width = asset.width; - final height = asset.height; - if (width != null && height != null) { - // we don't know the orientation, so assume it's normal - return width / height; - } - - return 1.0; - } - - Future> getStackAssets(String stackId) { - return _assetRepository.getStackAssets(stackId); - } - - Future clearTable() { - return _assetRepository.clearTable(); - } - - /// Delete assets from local file system and unreference from the database - Future deleteLocalAssets(Iterable assets) async { - // Delete files from local gallery - final candidates = assets.where((asset) => asset.isLocal); - - final deletedIds = await _assetMediaRepository.deleteAll(candidates.map((asset) => asset.localId!).toList()); - - // Modify local database by removing the reference to the local assets - if (deletedIds.isNotEmpty) { - // Delete records from local database - final isarIds = assets.where((asset) => asset.storage == AssetState.local).map((asset) => asset.id).toList(); - await _assetRepository.deleteByIds(isarIds); - - // Modify Merged asset to be remote only - final updatedAssets = assets.where((asset) => asset.storage == AssetState.merged).map((asset) { - asset.localId = null; - return asset; - }).toList(); - - await _assetRepository.updateAll(updatedAssets); - } - } - - /// Delete assets from the server and unreference from the database - Future deleteRemoteAssets(Iterable assets, {bool shouldDeletePermanently = false}) async { - final candidates = assets.where((a) => a.isRemote); - - if (candidates.isEmpty) { - return; - } - - await _apiService.assetsApi.deleteAssets( - AssetBulkDeleteDto(ids: candidates.map((a) => a.remoteId!).toList(), force: shouldDeletePermanently), - ); - - /// Update asset info bassed on the deletion type. - final payload = shouldDeletePermanently - ? assets.where((asset) => asset.storage == AssetState.merged).map((asset) { - asset.remoteId = null; - asset.visibility = AssetVisibilityEnum.timeline; - return asset; - }) - : assets.where((asset) => asset.isRemote).map((asset) { - asset.isTrashed = true; - return asset; - }); - - await _assetRepository.transaction(() async { - await _assetRepository.updateAll(payload.toList()); - - if (shouldDeletePermanently) { - final remoteAssetIds = assets - .where((asset) => asset.storage == AssetState.remote) - .map((asset) => asset.id) - .toList(); - await _assetRepository.deleteByIds(remoteAssetIds); - } - }); - } - - /// Delete assets on both local file system and the server. - /// Unreference from the database. - Future deleteAssets(Iterable assets, {bool shouldDeletePermanently = false}) async { - final hasLocal = assets.any((asset) => asset.isLocal); - final hasRemote = assets.any((asset) => asset.isRemote); - - if (hasLocal) { - await deleteLocalAssets(assets); - } - - if (hasRemote) { - await deleteRemoteAssets(assets, shouldDeletePermanently: shouldDeletePermanently); - } - } - - Stream watchAsset(int id, {bool fireImmediately = false}) { - return _assetRepository.watchAsset(id, fireImmediately: fireImmediately); - } - - Future> getRecentlyTakenAssets() { - final me = _userService.getMyUser(); - return _assetRepository.getRecentlyTakenAssets(me.id); - } - - Future> getMotionAssets() { - final me = _userService.getMyUser(); - return _assetRepository.getMotionAssets(me.id); - } - - Future setVisibility(List assets, AssetVisibilityEnum visibility) async { - await _assetApiRepository.updateVisibility(assets.map((asset) => asset.remoteId!).toList(), visibility); - - final updatedAssets = assets.map((asset) { - asset.visibility = visibility; - return asset; - }).toList(); - - await _assetRepository.updateAll(updatedAssets); - } - - Future getAssetByRemoteId(String remoteId) async { - final assets = await _assetRepository.getAllByRemoteId([remoteId]); - return assets.isNotEmpty ? assets.first : null; - } -} diff --git a/mobile/lib/services/background.service.dart b/mobile/lib/services/background.service.dart deleted file mode 100644 index 03278d25fc..0000000000 --- a/mobile/lib/services/background.service.dart +++ /dev/null @@ -1,595 +0,0 @@ -import 'dart:async'; -import 'dart:developer'; -import 'dart:io'; -import 'dart:isolate'; -import 'dart:ui' show DartPluginRegistrant, IsolateNameServer, PluginUtilities; - -import 'package:collection/collection.dart'; -import 'package:easy_localization/easy_localization.dart'; -import 'package:flutter/services.dart'; -import 'package:flutter/widgets.dart'; -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/domain/models/store.model.dart'; -import 'package:immich_mobile/entities/backup_album.entity.dart'; -import 'package:immich_mobile/entities/store.entity.dart'; -import 'package:immich_mobile/models/backup/backup_candidate.model.dart'; -import 'package:immich_mobile/models/backup/current_upload_asset.model.dart'; -import 'package:immich_mobile/models/backup/error_upload_asset.model.dart'; -import 'package:immich_mobile/providers/api.provider.dart'; -import 'package:immich_mobile/providers/app_settings.provider.dart'; -import 'package:immich_mobile/providers/db.provider.dart'; -import 'package:immich_mobile/providers/infrastructure/db.provider.dart'; -import 'package:immich_mobile/repositories/backup.repository.dart'; -import 'package:immich_mobile/repositories/file_media.repository.dart'; -import 'package:immich_mobile/services/app_settings.service.dart'; -import 'package:immich_mobile/services/auth.service.dart'; -import 'package:immich_mobile/services/backup.service.dart'; -import 'package:immich_mobile/services/localization.service.dart'; -import 'package:immich_mobile/utils/backup_progress.dart'; -import 'package:immich_mobile/utils/bootstrap.dart'; -import 'package:immich_mobile/utils/debug_print.dart'; -import 'package:immich_mobile/utils/diff.dart'; -import 'package:path_provider_foundation/path_provider_foundation.dart'; -import 'package:photo_manager/photo_manager.dart' show PMProgressHandler; - -final backgroundServiceProvider = Provider((ref) => BackgroundService()); - -/// Background backup service -class BackgroundService { - static const String _portNameLock = "immichLock"; - static const MethodChannel _foregroundChannel = MethodChannel('immich/foregroundChannel'); - static const MethodChannel _backgroundChannel = MethodChannel('immich/backgroundChannel'); - static const notifyInterval = Duration(milliseconds: 400); - bool _isBackgroundInitialized = false; - Completer? _cancellationToken; - bool _canceledBySystem = false; - int _wantsLockTime = 0; - bool _hasLock = false; - SendPort? _waitingIsolate; - ReceivePort? _rp; - bool _errorGracePeriodExceeded = true; - int _uploadedAssetsCount = 0; - int _assetsToUploadCount = 0; - String _lastPrintedDetailContent = ""; - String? _lastPrintedDetailTitle; - late final ThrottleProgressUpdate _throttledNotifiy = ThrottleProgressUpdate(_updateProgress, notifyInterval); - late final ThrottleProgressUpdate _throttledDetailNotify = ThrottleProgressUpdate( - _updateDetailProgress, - notifyInterval, - ); - - bool get isBackgroundInitialized { - return _isBackgroundInitialized; - } - - /// Ensures that the background service is enqueued if enabled in settings - Future resumeServiceIfEnabled() async { - return await isBackgroundBackupEnabled() && await enableService(); - } - - /// Enqueues the background service - Future enableService({bool immediate = false}) async { - try { - final callback = PluginUtilities.getCallbackHandle(_nativeEntry)!; - final String title = "backup_background_service_default_notification".tr(); - final bool ok = await _foregroundChannel.invokeMethod('enable', [callback.toRawHandle(), title, immediate]); - return ok; - } catch (error) { - return false; - } - } - - /// Configures the background service - Future configureService({ - bool requireUnmetered = true, - bool requireCharging = false, - int triggerUpdateDelay = 5000, - int triggerMaxDelay = 50000, - }) async { - try { - final bool ok = await _foregroundChannel.invokeMethod('configure', [ - requireUnmetered, - requireCharging, - triggerUpdateDelay, - triggerMaxDelay, - ]); - return ok; - } catch (error) { - return false; - } - } - - /// Cancels the background service (if currently running) and removes it from work queue - Future disableService() async { - try { - final ok = await _foregroundChannel.invokeMethod('disable'); - return ok; - } catch (error) { - return false; - } - } - - /// Returns `true` if the background service is enabled - Future isBackgroundBackupEnabled() async { - try { - return await _foregroundChannel.invokeMethod("isEnabled"); - } catch (error) { - return false; - } - } - - /// Returns `true` if battery optimizations are disabled - Future isIgnoringBatteryOptimizations() async { - // iOS does not need battery optimizations enabled - if (Platform.isIOS) { - return true; - } - try { - return await _foregroundChannel.invokeMethod('isIgnoringBatteryOptimizations'); - } catch (error) { - return false; - } - } - - // Yet to be implemented - Future digestFile(String path) { - return _foregroundChannel.invokeMethod("digestFile", [path]); - } - - Future?> digestFiles(List paths) { - return _foregroundChannel.invokeListMethod("digestFiles", paths); - } - - /// Updates the notification shown by the background service - Future _updateNotification({ - String? title, - String? content, - int progress = 0, - int max = 0, - bool indeterminate = false, - bool isDetail = false, - bool onlyIfFG = false, - }) async { - try { - if (_isBackgroundInitialized) { - return _backgroundChannel.invokeMethod('updateNotification', [ - title, - content, - progress, - max, - indeterminate, - isDetail, - onlyIfFG, - ]); - } - } catch (error) { - dPrint(() => "[_updateNotification] failed to communicate with plugin"); - } - return false; - } - - /// Shows a new priority notification - Future _showErrorNotification({required String title, String? content, String? individualTag}) async { - try { - if (_isBackgroundInitialized && _errorGracePeriodExceeded) { - return await _backgroundChannel.invokeMethod('showError', [title, content, individualTag]); - } - } catch (error) { - dPrint(() => "[_showErrorNotification] failed to communicate with plugin"); - } - return false; - } - - Future _clearErrorNotifications() async { - try { - if (_isBackgroundInitialized) { - return await _backgroundChannel.invokeMethod('clearErrorNotifications'); - } - } catch (error) { - dPrint(() => "[_clearErrorNotifications] failed to communicate with plugin"); - } - return false; - } - - /// await to ensure this thread (foreground or background) has exclusive access - Future acquireLock() async { - if (_hasLock) { - dPrint(() => "WARNING: [acquireLock] called more than once"); - return true; - } - final int lockTime = Timeline.now; - _wantsLockTime = lockTime; - final ReceivePort rp = ReceivePort(_portNameLock); - _rp = rp; - final SendPort sp = rp.sendPort; - - while (!IsolateNameServer.registerPortWithName(sp, _portNameLock)) { - try { - await _checkLockReleasedWithHeartbeat(lockTime); - } catch (error) { - return false; - } - if (_wantsLockTime != lockTime) { - return false; - } - } - _hasLock = true; - rp.listen(_heartbeatListener); - return true; - } - - Future _checkLockReleasedWithHeartbeat(final int lockTime) async { - SendPort? other = IsolateNameServer.lookupPortByName(_portNameLock); - if (other != null) { - final ReceivePort tempRp = ReceivePort(); - final SendPort tempSp = tempRp.sendPort; - final bs = tempRp.asBroadcastStream(); - while (_wantsLockTime == lockTime) { - other.send(tempSp); - final dynamic answer = await bs.first.timeout(const Duration(seconds: 3), onTimeout: () => null); - if (_wantsLockTime != lockTime) { - break; - } - if (answer == null) { - // other isolate failed to answer, assuming it exited without releasing the lock - if (other == IsolateNameServer.lookupPortByName(_portNameLock)) { - IsolateNameServer.removePortNameMapping(_portNameLock); - } - break; - } else if (answer == true) { - // other isolate released the lock - break; - } else if (answer == false) { - // other isolate is still active - } - final dynamic isFinished = await bs.first.timeout(const Duration(seconds: 3), onTimeout: () => false); - if (isFinished == true) { - break; - } - } - tempRp.close(); - } - } - - void _heartbeatListener(dynamic msg) { - if (msg is SendPort) { - _waitingIsolate = msg; - msg.send(false); - } - } - - /// releases the exclusive access lock - void releaseLock() { - _wantsLockTime = 0; - if (_hasLock) { - IsolateNameServer.removePortNameMapping(_portNameLock); - _waitingIsolate?.send(true); - _waitingIsolate = null; - _hasLock = false; - } - _rp?.close(); - _rp = null; - } - - void _setupBackgroundCallHandler() { - _backgroundChannel.setMethodCallHandler(_callHandler); - _isBackgroundInitialized = true; - _backgroundChannel.invokeMethod('initialized'); - } - - Future _callHandler(MethodCall call) async { - DartPluginRegistrant.ensureInitialized(); - if (Platform.isIOS) { - // NOTE: I'm not sure this is strictly necessary anymore, but - // out of an abundance of caution, we will keep it in until someone - // can say for sure - PathProviderFoundation.registerWith(); - } - switch (call.method) { - case "backgroundProcessing": - case "onAssetsChanged": - try { - unawaited(_clearErrorNotifications()); - - // iOS should time out after some threshold so it doesn't wait - // indefinitely and can run later - // Android is fine to wait here until the lock releases - final waitForLock = Platform.isIOS - ? acquireLock().timeout(const Duration(seconds: 5), onTimeout: () => false) - : acquireLock(); - - final bool hasAccess = await waitForLock; - if (!hasAccess) { - dPrint(() => "[_callHandler] could not acquire lock, exiting"); - return false; - } - - final translationsOk = await loadTranslations(); - if (!translationsOk) { - dPrint(() => "[_callHandler] could not load translations"); - } - - final bool ok = await _onAssetsChanged(); - return ok; - } catch (error) { - dPrint(() => error.toString()); - return false; - } finally { - releaseLock(); - } - case "systemStop": - _canceledBySystem = true; - _cancellationToken?.complete(); - _cancellationToken = null; - return true; - default: - dPrint(() => "Unknown method ${call.method}"); - return false; - } - } - - Future _onAssetsChanged() async { - final (isar, drift, logDb) = await Bootstrap.initDB(); - await Bootstrap.initDomain(isar, drift, logDb, shouldBufferLogs: false, listenStoreUpdates: false); - - final ref = ProviderContainer( - overrides: [ - dbProvider.overrideWithValue(isar), - isarProvider.overrideWithValue(isar), - driftProvider.overrideWith(driftOverride(drift)), - ], - ); - - await ref.read(authServiceProvider).setOpenApiServiceEndpoint(); - dPrint(() => "[BG UPLOAD] Using endpoint: ${ref.read(apiServiceProvider).apiClient.basePath}"); - - final selectedAlbums = await ref.read(backupAlbumRepositoryProvider).getAllBySelection(BackupSelection.select); - final excludedAlbums = await ref.read(backupAlbumRepositoryProvider).getAllBySelection(BackupSelection.exclude); - if (selectedAlbums.isEmpty) { - return true; - } - - await ref.read(fileMediaRepositoryProvider).enableBackgroundAccess(); - - do { - final bool backupOk = await _runBackup( - ref.read(backupServiceProvider), - ref.read(appSettingsServiceProvider), - selectedAlbums, - excludedAlbums, - ); - if (backupOk) { - await Store.delete(StoreKey.backupFailedSince); - final backupAlbums = [...selectedAlbums, ...excludedAlbums]; - backupAlbums.sortBy((e) => e.id); - - final dbAlbums = await ref.read(backupAlbumRepositoryProvider).getAll(sort: BackupAlbumSort.id); - final List toDelete = []; - final List toUpsert = []; - // stores the most recent `lastBackup` per album but always keeps the `selection` from the most recent DB state - diffSortedListsSync( - dbAlbums, - backupAlbums, - compare: (BackupAlbum a, BackupAlbum b) => a.id.compareTo(b.id), - both: (BackupAlbum a, BackupAlbum b) { - a.lastBackup = a.lastBackup.isAfter(b.lastBackup) ? a.lastBackup : b.lastBackup; - toUpsert.add(a); - return true; - }, - onlyFirst: (BackupAlbum a) => toUpsert.add(a), - onlySecond: (BackupAlbum b) => toDelete.add(b.isarId), - ); - await ref.read(backupAlbumRepositoryProvider).deleteAll(toDelete); - await ref.read(backupAlbumRepositoryProvider).updateAll(toUpsert); - } else if (Store.tryGet(StoreKey.backupFailedSince) == null) { - await Store.put(StoreKey.backupFailedSince, DateTime.now()); - return false; - } - // Android should check for new assets added while performing backup - } while (Platform.isAndroid && true == await _backgroundChannel.invokeMethod("hasContentChanged")); - return true; - } - - Future _runBackup( - BackupService backupService, - AppSettingsService settingsService, - List selectedAlbums, - List excludedAlbums, - ) async { - _errorGracePeriodExceeded = _isErrorGracePeriodExceeded(settingsService); - final bool notifyTotalProgress = settingsService.getSetting(AppSettingsEnum.backgroundBackupTotalProgress); - final bool notifySingleProgress = settingsService.getSetting(AppSettingsEnum.backgroundBackupSingleProgress); - - if (_canceledBySystem) { - return false; - } - - Set toUpload = await backupService.buildUploadCandidates(selectedAlbums, excludedAlbums); - - try { - toUpload = await backupService.removeAlreadyUploadedAssets(toUpload); - } catch (e) { - unawaited( - _showErrorNotification( - title: "backup_background_service_error_title".tr(), - content: "backup_background_service_connection_failed_message".tr(), - ), - ); - return false; - } - - if (_canceledBySystem) { - return false; - } - - if (toUpload.isEmpty) { - return true; - } - _assetsToUploadCount = toUpload.length; - _uploadedAssetsCount = 0; - unawaited( - _updateNotification( - title: "backup_background_service_in_progress_notification".tr(), - content: notifyTotalProgress ? formatAssetBackupProgress(_uploadedAssetsCount, _assetsToUploadCount) : null, - progress: 0, - max: notifyTotalProgress ? _assetsToUploadCount : 0, - indeterminate: !notifyTotalProgress, - onlyIfFG: !notifyTotalProgress, - ), - ); - - _cancellationToken?.complete(); - _cancellationToken = Completer(); - final pmProgressHandler = Platform.isIOS ? PMProgressHandler() : null; - - final bool ok = await backupService.backupAsset( - toUpload, - _cancellationToken!, - pmProgressHandler: pmProgressHandler, - onSuccess: (result) => _onAssetUploaded(shouldNotify: notifyTotalProgress), - onProgress: (bytes, totalBytes) => _onProgress(bytes, totalBytes, shouldNotify: notifySingleProgress), - onCurrentAsset: (asset) => _onSetCurrentBackupAsset(asset, shouldNotify: notifySingleProgress), - onError: _onBackupError, - isBackground: true, - ); - - if (!ok && !_cancellationToken!.isCompleted) { - unawaited( - _showErrorNotification( - title: "backup_background_service_error_title".tr(), - content: "backup_background_service_backup_failed_message".tr(), - ), - ); - } - - return ok; - } - - void _onAssetUploaded({bool shouldNotify = false}) { - if (!shouldNotify) { - return; - } - - _uploadedAssetsCount++; - _throttledNotifiy(); - } - - void _onProgress(int bytes, int totalBytes, {bool shouldNotify = false}) { - if (!shouldNotify) { - return; - } - - _throttledDetailNotify(progress: bytes, total: totalBytes); - } - - void _updateDetailProgress(String? title, int progress, int total) { - final String msg = total > 0 ? humanReadableBytesProgress(progress, total) : ""; - // only update if message actually differs (to stop many useless notification updates on large assets or slow connections) - if (msg != _lastPrintedDetailContent || _lastPrintedDetailTitle != title) { - _lastPrintedDetailContent = msg; - _lastPrintedDetailTitle = title; - _updateNotification( - progress: total > 0 ? (progress * 1000) ~/ total : 0, - max: 1000, - isDetail: true, - title: title, - content: msg, - ); - } - } - - void _updateProgress(String? title, int progress, int total) { - _updateNotification( - progress: _uploadedAssetsCount, - max: _assetsToUploadCount, - title: title, - content: formatAssetBackupProgress(_uploadedAssetsCount, _assetsToUploadCount), - ); - } - - void _onBackupError(ErrorUploadAsset errorAssetInfo) { - _showErrorNotification( - title: "backup_background_service_upload_failure_notification".tr( - namedArgs: {'filename': errorAssetInfo.fileName}, - ), - individualTag: errorAssetInfo.id, - ); - } - - void _onSetCurrentBackupAsset(CurrentUploadAsset currentUploadAsset, {bool shouldNotify = false}) { - if (!shouldNotify) { - return; - } - - _throttledDetailNotify.title = "backup_background_service_current_upload_notification".tr( - namedArgs: {'filename': currentUploadAsset.fileName}, - ); - _throttledDetailNotify.progress = 0; - _throttledDetailNotify.total = 0; - } - - bool _isErrorGracePeriodExceeded(AppSettingsService appSettingsService) { - final int value = appSettingsService.getSetting(AppSettingsEnum.uploadErrorNotificationGracePeriod); - if (value == 0) { - return true; - } else if (value == 5) { - return false; - } - final DateTime? failedSince = Store.tryGet(StoreKey.backupFailedSince); - if (failedSince == null) { - return false; - } - final Duration duration = DateTime.now().difference(failedSince); - if (value == 1) { - return duration > const Duration(minutes: 30); - } else if (value == 2) { - return duration > const Duration(hours: 2); - } else if (value == 3) { - return duration > const Duration(hours: 8); - } else if (value == 4) { - return duration > const Duration(hours: 24); - } - assert(false, "Invalid value"); - return true; - } - - Future getIOSBackupLastRun(IosBackgroundTask task) async { - if (!Platform.isIOS) { - return null; - } - // Seconds since last run - final double? lastRun = task == IosBackgroundTask.fetch - ? await _foregroundChannel.invokeMethod('lastBackgroundFetchTime') - : await _foregroundChannel.invokeMethod('lastBackgroundProcessingTime'); - if (lastRun == null) { - return null; - } - final time = DateTime.fromMillisecondsSinceEpoch(lastRun.toInt() * 1000); - return time; - } - - Future getIOSBackupNumberOfProcesses() async { - if (!Platform.isIOS) { - return 0; - } - return await _foregroundChannel.invokeMethod('numberOfBackgroundProcesses'); - } - - Future getIOSBackgroundAppRefreshEnabled() async { - if (!Platform.isIOS) { - return false; - } - return await _foregroundChannel.invokeMethod('backgroundAppRefreshEnabled'); - } -} - -enum IosBackgroundTask { fetch, processing } - -/// entry point called by Kotlin/Java code; needs to be a top-level function -@pragma('vm:entry-point') -void _nativeEntry() { - WidgetsFlutterBinding.ensureInitialized(); - DartPluginRegistrant.ensureInitialized(); - BackgroundService backgroundService = BackgroundService(); - backgroundService._setupBackgroundCallHandler(); -} diff --git a/mobile/lib/services/backup.service.dart b/mobile/lib/services/backup.service.dart deleted file mode 100644 index 9b6a26be03..0000000000 --- a/mobile/lib/services/backup.service.dart +++ /dev/null @@ -1,473 +0,0 @@ -import 'dart:async'; -import 'dart:convert'; -import 'dart:io'; - -import 'package:collection/collection.dart'; -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:http/http.dart'; -import 'package:immich_mobile/domain/models/store.model.dart'; -import 'package:immich_mobile/entities/album.entity.dart'; -import 'package:immich_mobile/entities/asset.entity.dart'; -import 'package:immich_mobile/entities/backup_album.entity.dart'; -import 'package:immich_mobile/entities/store.entity.dart'; -import 'package:immich_mobile/infrastructure/repositories/network.repository.dart'; -import 'package:immich_mobile/repositories/upload.repository.dart'; -import 'package:immich_mobile/models/backup/backup_candidate.model.dart'; -import 'package:immich_mobile/models/backup/current_upload_asset.model.dart'; -import 'package:immich_mobile/models/backup/error_upload_asset.model.dart'; -import 'package:immich_mobile/models/backup/success_upload_asset.model.dart'; -import 'package:immich_mobile/providers/api.provider.dart'; -import 'package:immich_mobile/providers/app_settings.provider.dart'; -import 'package:immich_mobile/repositories/album_media.repository.dart'; -import 'package:immich_mobile/repositories/asset.repository.dart'; -import 'package:immich_mobile/repositories/asset_media.repository.dart'; -import 'package:immich_mobile/repositories/file_media.repository.dart'; -import 'package:immich_mobile/services/album.service.dart'; -import 'package:immich_mobile/services/api.service.dart'; -import 'package:immich_mobile/services/app_settings.service.dart'; -import 'package:logging/logging.dart'; -import 'package:openapi/api.dart'; -import 'package:path/path.dart' as p; -import 'package:permission_handler/permission_handler.dart' as pm; -import 'package:photo_manager/photo_manager.dart' show PMProgressHandler; -import 'package:immich_mobile/utils/debug_print.dart'; - -final backupServiceProvider = Provider( - (ref) => BackupService( - ref.watch(apiServiceProvider), - ref.watch(appSettingsServiceProvider), - ref.watch(albumServiceProvider), - ref.watch(albumMediaRepositoryProvider), - ref.watch(fileMediaRepositoryProvider), - ref.watch(assetRepositoryProvider), - ref.watch(assetMediaRepositoryProvider), - ), -); - -class BackupService { - final ApiService _apiService; - final Logger _log = Logger("BackupService"); - final AppSettingsService _appSetting; - final AlbumService _albumService; - final AlbumMediaRepository _albumMediaRepository; - final FileMediaRepository _fileMediaRepository; - final AssetRepository _assetRepository; - final AssetMediaRepository _assetMediaRepository; - - BackupService( - this._apiService, - this._appSetting, - this._albumService, - this._albumMediaRepository, - this._fileMediaRepository, - this._assetRepository, - this._assetMediaRepository, - ); - - Future?> getDeviceBackupAsset() async { - final String deviceId = Store.get(StoreKey.deviceId); - - try { - return await _apiService.assetsApi.getAllUserAssetsByDeviceId(deviceId); - } catch (e) { - dPrint(() => 'Error [getDeviceBackupAsset] ${e.toString()}'); - return null; - } - } - - Future _saveDuplicatedAssetIds(List deviceAssetIds) => - _assetRepository.transaction(() => _assetRepository.upsertDuplicatedAssets(deviceAssetIds)); - - /// Get duplicated asset id from database - Future> getDuplicatedAssetIds() async { - final duplicates = await _assetRepository.getAllDuplicatedAssetIds(); - return duplicates.toSet(); - } - - /// Returns all assets newer than the last successful backup per album - /// if `useTimeFilter` is set to true, all assets will be returned - Future> buildUploadCandidates( - List selectedBackupAlbums, - List excludedBackupAlbums, { - bool useTimeFilter = true, - }) async { - final now = DateTime.now(); - - final Set toAdd = await _fetchAssetsAndUpdateLastBackup( - selectedBackupAlbums, - now, - useTimeFilter: useTimeFilter, - ); - - if (toAdd.isEmpty) return {}; - - final Set toRemove = await _fetchAssetsAndUpdateLastBackup( - excludedBackupAlbums, - now, - useTimeFilter: useTimeFilter, - ); - - return toAdd.difference(toRemove); - } - - Future> _fetchAssetsAndUpdateLastBackup( - List backupAlbums, - DateTime now, { - bool useTimeFilter = true, - }) async { - Set candidates = {}; - - for (final BackupAlbum backupAlbum in backupAlbums) { - final Album localAlbum; - try { - localAlbum = await _albumMediaRepository.get(backupAlbum.id); - } on StateError { - // the album no longer exists - continue; - } - - if (useTimeFilter && localAlbum.modifiedAt.isBefore(backupAlbum.lastBackup)) { - continue; - } - final List assets; - try { - assets = await _albumMediaRepository.getAssets( - backupAlbum.id, - modifiedFrom: useTimeFilter - ? - // subtract 2 seconds to prevent missing assets due to rounding issues - backupAlbum.lastBackup.subtract(const Duration(seconds: 2)) - : null, - modifiedUntil: useTimeFilter ? now : null, - ); - } on StateError { - // either there are no assets matching the filter criteria OR the album no longer exists - continue; - } - - // Add album's name to the asset info - for (final asset in assets) { - List albumNames = [localAlbum.name]; - - final existingAsset = candidates.firstWhereOrNull((candidate) => candidate.asset.localId == asset.localId); - - if (existingAsset != null) { - albumNames.addAll(existingAsset.albumNames); - candidates.remove(existingAsset); - } - - candidates.add(BackupCandidate(asset: asset, albumNames: albumNames)); - } - - backupAlbum.lastBackup = now; - } - - return candidates; - } - - /// Returns a new list of assets not yet uploaded - Future> removeAlreadyUploadedAssets(Set candidates) async { - if (candidates.isEmpty) { - return candidates; - } - - final Set duplicatedAssetIds = await getDuplicatedAssetIds(); - candidates.removeWhere((candidate) => duplicatedAssetIds.contains(candidate.asset.localId)); - - if (candidates.isEmpty) { - return candidates; - } - - final Set existing = {}; - try { - final String deviceId = Store.get(StoreKey.deviceId); - final CheckExistingAssetsResponseDto? duplicates = await _apiService.assetsApi.checkExistingAssets( - CheckExistingAssetsDto(deviceAssetIds: candidates.map((c) => c.asset.localId!).toList(), deviceId: deviceId), - ); - if (duplicates != null) { - existing.addAll(duplicates.existingIds); - } - } on ApiException { - // workaround for older server versions or when checking for too many assets at once - final List? allAssetsInDatabase = await getDeviceBackupAsset(); - if (allAssetsInDatabase != null) { - existing.addAll(allAssetsInDatabase); - } - } - - if (existing.isNotEmpty) { - candidates.removeWhere((c) => existing.contains(c.asset.localId)); - } - - return candidates; - } - - Future _checkPermissions() async { - if (Platform.isAndroid && !(await pm.Permission.accessMediaLocation.status).isGranted) { - // double check that permission is granted here, to guard against - // uploading corrupt assets without EXIF information - _log.warning( - "Media location permission is not granted. " - "Cannot access original assets for backup.", - ); - - return false; - } - - // DON'T KNOW WHY BUT THIS HELPS BACKGROUND BACKUP TO WORK ON IOS - if (Platform.isIOS) { - await _fileMediaRepository.requestExtendedPermissions(); - } - - return true; - } - - /// Upload images before video assets for background tasks - /// these are further sorted by using their creation date - List _sortPhotosFirst(List candidates) { - return candidates.sorted((a, b) { - final cmp = a.asset.type.index - b.asset.type.index; - if (cmp != 0) return cmp; - return a.asset.fileCreatedAt.compareTo(b.asset.fileCreatedAt); - }); - } - - Future backupAsset( - Iterable assets, - Completer cancelToken, { - bool isBackground = false, - PMProgressHandler? pmProgressHandler, - required void Function(SuccessUploadAsset result) onSuccess, - required void Function(int bytes, int totalBytes) onProgress, - required void Function(CurrentUploadAsset asset) onCurrentAsset, - required void Function(ErrorUploadAsset error) onError, - }) async { - final bool isIgnoreIcloudAssets = _appSetting.getSetting(AppSettingsEnum.ignoreIcloudAssets); - final shouldSyncAlbums = _appSetting.getSetting(AppSettingsEnum.syncAlbums); - final String deviceId = Store.get(StoreKey.deviceId); - final String savedEndpoint = Store.get(StoreKey.serverEndpoint); - final List duplicatedAssetIds = []; - bool anyErrors = false; - - final hasPermission = await _checkPermissions(); - if (!hasPermission) { - return false; - } - - List candidates = assets.toList(); - if (isBackground) { - candidates = _sortPhotosFirst(candidates); - } - - for (final candidate in candidates) { - final Asset asset = candidate.asset; - File? file; - File? livePhotoFile; - - try { - final isAvailableLocally = await asset.local!.isLocallyAvailable(isOrigin: true); - - // Handle getting files from iCloud - if (!isAvailableLocally && Platform.isIOS) { - // Skip iCloud assets if the user has disabled this feature - if (isIgnoreIcloudAssets) { - continue; - } - - onCurrentAsset( - CurrentUploadAsset( - id: asset.localId!, - fileCreatedAt: asset.fileCreatedAt.year == 1970 ? asset.fileModifiedAt : asset.fileCreatedAt, - fileName: asset.fileName, - fileType: _getAssetType(asset.type), - iCloudAsset: true, - ), - ); - - file = await asset.local!.loadFile(progressHandler: pmProgressHandler); - if (asset.local!.isLivePhoto) { - livePhotoFile = await asset.local!.loadFile(withSubtype: true, progressHandler: pmProgressHandler); - } - } else { - file = await asset.local!.originFile.timeout(const Duration(seconds: 5)); - - if (asset.local!.isLivePhoto) { - livePhotoFile = await asset.local!.originFileWithSubtype.timeout(const Duration(seconds: 5)); - } - } - - if (file != null) { - String? originalFileName = await _assetMediaRepository.getOriginalFilename(asset.localId!); - originalFileName ??= asset.fileName; - - if (asset.local!.isLivePhoto) { - if (livePhotoFile == null) { - _log.warning("Failed to obtain motion part of the livePhoto - $originalFileName"); - } - } - - final fileStream = file.openRead(); - final assetRawUploadData = MultipartFile( - "assetData", - fileStream, - file.lengthSync(), - filename: originalFileName, - ); - - final baseRequest = ProgressMultipartRequest( - 'POST', - Uri.parse('$savedEndpoint/assets'), - abortTrigger: cancelToken.future, - onProgress: ((bytes, totalBytes) => onProgress(bytes, totalBytes)), - ); - - baseRequest.fields['deviceAssetId'] = asset.localId!; - baseRequest.fields['deviceId'] = deviceId; - baseRequest.fields['fileCreatedAt'] = asset.fileCreatedAt.toUtc().toIso8601String(); - baseRequest.fields['fileModifiedAt'] = asset.fileModifiedAt.toUtc().toIso8601String(); - baseRequest.fields['isFavorite'] = asset.isFavorite.toString(); - baseRequest.fields['duration'] = asset.duration.toString(); - baseRequest.files.add(assetRawUploadData); - - onCurrentAsset( - CurrentUploadAsset( - id: asset.localId!, - fileCreatedAt: asset.fileCreatedAt.year == 1970 ? asset.fileModifiedAt : asset.fileCreatedAt, - fileName: originalFileName, - fileType: _getAssetType(asset.type), - fileSize: file.lengthSync(), - iCloudAsset: false, - ), - ); - - String? livePhotoVideoId; - if (asset.local!.isLivePhoto && livePhotoFile != null) { - livePhotoVideoId = await uploadLivePhotoVideo(originalFileName, livePhotoFile, baseRequest, cancelToken); - } - - if (livePhotoVideoId != null) { - baseRequest.fields['livePhotoVideoId'] = livePhotoVideoId; - } - - final response = await NetworkRepository.client.send(baseRequest); - - final responseBody = jsonDecode(await response.stream.bytesToString()); - - if (![200, 201].contains(response.statusCode)) { - final error = responseBody; - final errorMessage = error['message'] ?? error['error']; - - dPrint( - () => - "Error(${error['statusCode']}) uploading ${asset.localId} | $originalFileName | Created on ${asset.fileCreatedAt} | ${error['error']}", - ); - - onError( - ErrorUploadAsset( - asset: asset, - id: asset.localId!, - fileCreatedAt: asset.fileCreatedAt, - fileName: originalFileName, - fileType: _getAssetType(candidate.asset.type), - errorMessage: errorMessage, - ), - ); - - if (errorMessage == "Quota has been exceeded!") { - anyErrors = true; - break; - } - - continue; - } - - bool isDuplicate = false; - if (response.statusCode == 200) { - isDuplicate = true; - duplicatedAssetIds.add(asset.localId!); - } - - onSuccess( - SuccessUploadAsset( - candidate: candidate, - remoteAssetId: responseBody['id'] as String, - isDuplicate: isDuplicate, - ), - ); - - if (shouldSyncAlbums) { - await _albumService.syncUploadAlbums(candidate.albumNames, [responseBody['id'] as String]); - } - } - } on RequestAbortedException { - dPrint(() => "Backup was cancelled by the user"); - anyErrors = true; - break; - } catch (error, stackTrace) { - dPrint(() => "Error backup asset: ${error.toString()}: $stackTrace"); - anyErrors = true; - continue; - } finally { - if (Platform.isIOS) { - try { - await file?.delete(); - await livePhotoFile?.delete(); - } catch (e) { - dPrint(() => "ERROR deleting file: ${e.toString()}"); - } - } - } - } - - if (duplicatedAssetIds.isNotEmpty) { - await _saveDuplicatedAssetIds(duplicatedAssetIds); - } - - return !anyErrors; - } - - Future uploadLivePhotoVideo( - String originalFileName, - File? livePhotoVideoFile, - MultipartRequest baseRequest, - Completer cancelToken, - ) async { - if (livePhotoVideoFile == null) { - return null; - } - final livePhotoTitle = p.setExtension(originalFileName, p.extension(livePhotoVideoFile.path)); - final fileStream = livePhotoVideoFile.openRead(); - final livePhotoRawUploadData = MultipartFile( - "assetData", - fileStream, - livePhotoVideoFile.lengthSync(), - filename: livePhotoTitle, - ); - final livePhotoReq = ProgressMultipartRequest(baseRequest.method, baseRequest.url, abortTrigger: cancelToken.future) - ..headers.addAll(baseRequest.headers) - ..fields.addAll(baseRequest.fields); - - livePhotoReq.files.add(livePhotoRawUploadData); - - var response = await NetworkRepository.client.send(livePhotoReq); - - var responseBody = jsonDecode(await response.stream.bytesToString()); - - if (![200, 201].contains(response.statusCode)) { - var error = responseBody; - - dPrint( - () => "Error(${error['statusCode']}) uploading livePhoto for assetId | $livePhotoTitle | ${error['error']}", - ); - } - - return responseBody.containsKey('id') ? responseBody['id'] : null; - } - - String _getAssetType(AssetType assetType) => switch (assetType) { - AssetType.audio => "AUDIO", - AssetType.image => "IMAGE", - AssetType.video => "VIDEO", - AssetType.other => "OTHER", - }; -} diff --git a/mobile/lib/services/backup_album.service.dart b/mobile/lib/services/backup_album.service.dart deleted file mode 100644 index ef9d1031de..0000000000 --- a/mobile/lib/services/backup_album.service.dart +++ /dev/null @@ -1,33 +0,0 @@ -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/entities/backup_album.entity.dart'; -import 'package:immich_mobile/repositories/backup.repository.dart'; - -final backupAlbumServiceProvider = Provider((ref) { - return BackupAlbumService(ref.watch(backupAlbumRepositoryProvider)); -}); - -class BackupAlbumService { - final BackupAlbumRepository _backupAlbumRepository; - - const BackupAlbumService(this._backupAlbumRepository); - - Future> getAll({BackupAlbumSort? sort}) { - return _backupAlbumRepository.getAll(sort: sort); - } - - Future> getIdsBySelection(BackupSelection backup) { - return _backupAlbumRepository.getIdsBySelection(backup); - } - - Future> getAllBySelection(BackupSelection backup) { - return _backupAlbumRepository.getAllBySelection(backup); - } - - Future deleteAll(List ids) { - return _backupAlbumRepository.deleteAll(ids); - } - - Future updateAll(List backupAlbums) { - return _backupAlbumRepository.updateAll(backupAlbums); - } -} diff --git a/mobile/lib/services/backup_verification.service.dart b/mobile/lib/services/backup_verification.service.dart deleted file mode 100644 index 2efd52cc81..0000000000 --- a/mobile/lib/services/backup_verification.service.dart +++ /dev/null @@ -1,192 +0,0 @@ -import 'dart:async'; -import 'dart:typed_data'; - -import 'package:collection/collection.dart'; -import 'package:flutter/foundation.dart'; -import 'package:flutter/services.dart'; -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/domain/models/exif.model.dart'; -import 'package:immich_mobile/domain/models/store.model.dart'; -import 'package:immich_mobile/domain/services/user.service.dart'; -import 'package:immich_mobile/entities/asset.entity.dart'; -import 'package:immich_mobile/entities/store.entity.dart'; -import 'package:immich_mobile/infrastructure/repositories/exif.repository.dart'; -import 'package:immich_mobile/infrastructure/utils/exif.converter.dart'; -import 'package:immich_mobile/providers/infrastructure/exif.provider.dart'; -import 'package:immich_mobile/providers/infrastructure/user.provider.dart'; -import 'package:immich_mobile/repositories/asset.repository.dart'; -import 'package:immich_mobile/repositories/file_media.repository.dart'; -import 'package:immich_mobile/services/api.service.dart'; -import 'package:immich_mobile/utils/bootstrap.dart'; -import 'package:immich_mobile/utils/diff.dart'; - -/// Finds duplicates originating from missing EXIF information -class BackupVerificationService { - final UserService _userService; - final FileMediaRepository _fileMediaRepository; - final AssetRepository _assetRepository; - final IsarExifRepository _exifInfoRepository; - - const BackupVerificationService( - this._userService, - this._fileMediaRepository, - this._assetRepository, - this._exifInfoRepository, - ); - - /// Returns at most [limit] assets that were backed up without exif - Future> findWronglyBackedUpAssets({int limit = 100}) async { - final owner = _userService.getMyUser().id; - final List onlyLocal = await _assetRepository.getAll(ownerId: owner, state: AssetState.local, limit: limit); - final List remoteMatches = await _assetRepository.getMatches( - assets: onlyLocal, - ownerId: owner, - state: AssetState.remote, - limit: limit, - ); - final List localMatches = await _assetRepository.getMatches( - assets: remoteMatches, - ownerId: owner, - state: AssetState.local, - limit: limit, - ); - - final List deleteCandidates = [], originals = []; - - await diffSortedLists( - remoteMatches, - localMatches, - compare: (a, b) => a.fileName.compareTo(b.fileName), - both: (a, b) async { - a.exifInfo = await _exifInfoRepository.get(a.id); - deleteCandidates.add(a); - originals.add(b); - return false; - }, - onlyFirst: (a) {}, - onlySecond: (b) {}, - ); - final isolateToken = ServicesBinding.rootIsolateToken!; - final List toDelete; - if (deleteCandidates.length > 10) { - // performs 2 checks in parallel for a nice speedup - final half = deleteCandidates.length ~/ 2; - final lower = compute(_computeSaveToDelete, ( - deleteCandidates: deleteCandidates.slice(0, half), - originals: originals.slice(0, half), - endpoint: Store.get(StoreKey.serverEndpoint), - rootIsolateToken: isolateToken, - fileMediaRepository: _fileMediaRepository, - )); - final upper = compute(_computeSaveToDelete, ( - deleteCandidates: deleteCandidates.slice(half), - originals: originals.slice(half), - endpoint: Store.get(StoreKey.serverEndpoint), - rootIsolateToken: isolateToken, - fileMediaRepository: _fileMediaRepository, - )); - toDelete = await lower + await upper; - } else { - toDelete = await compute(_computeSaveToDelete, ( - deleteCandidates: deleteCandidates, - originals: originals, - endpoint: Store.get(StoreKey.serverEndpoint), - rootIsolateToken: isolateToken, - fileMediaRepository: _fileMediaRepository, - )); - } - return toDelete; - } - - static Future> _computeSaveToDelete( - ({ - List deleteCandidates, - List originals, - String endpoint, - RootIsolateToken rootIsolateToken, - FileMediaRepository fileMediaRepository, - }) - tuple, - ) async { - assert(tuple.deleteCandidates.length == tuple.originals.length); - final List result = []; - BackgroundIsolateBinaryMessenger.ensureInitialized(tuple.rootIsolateToken); - final (isar, drift, logDb) = await Bootstrap.initDB(); - await Bootstrap.initDomain(isar, drift, logDb); - await tuple.fileMediaRepository.enableBackgroundAccess(); - final ApiService apiService = ApiService(); - apiService.setEndpoint(tuple.endpoint); - for (int i = 0; i < tuple.deleteCandidates.length; i++) { - if (await _compareAssets(tuple.deleteCandidates[i], tuple.originals[i], apiService)) { - result.add(tuple.deleteCandidates[i]); - } - } - return result; - } - - static Future _compareAssets(Asset remote, Asset local, ApiService apiService) async { - if (remote.checksum == local.checksum) return false; - ExifInfo? exif = remote.exifInfo; - if (exif != null && exif.latitude != null) return false; - if (exif == null || exif.fileSize == null) { - final dto = await apiService.assetsApi.getAssetInfo(remote.remoteId!); - if (dto != null && dto.exifInfo != null) { - exif = ExifDtoConverter.fromDto(dto.exifInfo!); - } - } - final file = await local.local!.originFile; - if (exif != null && file != null && exif.fileSize != null) { - final origSize = await file.length(); - if (exif.fileSize! == origSize || exif.fileSize! != origSize) { - final latLng = await local.local!.latlngAsync(); - - if (exif.latitude == null && - latLng.latitude != null && - (remote.fileCreatedAt.isAtSameMomentAs(local.fileCreatedAt) || - remote.fileModifiedAt.isAtSameMomentAs(local.fileModifiedAt) || - _sameExceptTimeZone(remote.fileCreatedAt, local.fileCreatedAt))) { - if (remote.type == AssetType.video) { - // it's very unlikely that a video of same length, filesize, name - // and date is wrong match. Cannot easily compare videos anyway - return true; - } - - // for images: make sure they are pixel-wise identical - // (skip first few KBs containing metadata) - final Uint64List localImage = _fakeDecodeImg(await file.readAsBytes()); - final res = await apiService.assetsApi.downloadAssetWithHttpInfo(remote.remoteId!); - final Uint64List remoteImage = _fakeDecodeImg(res.bodyBytes); - - final eq = const ListEquality().equals(remoteImage, localImage); - return eq; - } - } - } - - return false; - } - - static Uint64List _fakeDecodeImg(Uint8List bytes) { - const headerLength = 131072; // assume header is at most 128 KB - final start = bytes.length < headerLength * 2 ? (bytes.length ~/ (4 * 8)) * 8 : headerLength; - return bytes.buffer.asUint64List(start); - } - - static bool _sameExceptTimeZone(DateTime a, DateTime b) { - final ms = a.isAfter(b) - ? a.millisecondsSinceEpoch - b.millisecondsSinceEpoch - : b.millisecondsSinceEpoch - a.microsecondsSinceEpoch; - final x = ms / (1000 * 60 * 30); - final y = ms ~/ (1000 * 60 * 30); - return y.toDouble() == x && y < 24; - } -} - -final backupVerificationServiceProvider = Provider( - (ref) => BackupVerificationService( - ref.watch(userServiceProvider), - ref.watch(fileMediaRepositoryProvider), - ref.watch(assetRepositoryProvider), - ref.watch(exifRepositoryProvider), - ), -); diff --git a/mobile/lib/services/deep_link.service.dart b/mobile/lib/services/deep_link.service.dart index 9d2bdbe4a0..5ff0fa8a4d 100644 --- a/mobile/lib/services/deep_link.service.dart +++ b/mobile/lib/services/deep_link.service.dart @@ -7,10 +7,7 @@ import 'package:immich_mobile/domain/services/memory.service.dart'; import 'package:immich_mobile/domain/services/people.service.dart'; import 'package:immich_mobile/domain/services/remote_album.service.dart'; import 'package:immich_mobile/domain/services/timeline.service.dart'; -import 'package:immich_mobile/entities/store.entity.dart'; import 'package:immich_mobile/presentation/widgets/asset_viewer/asset_viewer.page.dart'; -import 'package:immich_mobile/providers/album/current_album.provider.dart'; -import 'package:immich_mobile/providers/asset_viewer/current_asset.provider.dart'; import 'package:immich_mobile/providers/infrastructure/album.provider.dart'; import 'package:immich_mobile/providers/infrastructure/asset.provider.dart' as beta_asset_provider; import 'package:immich_mobile/providers/infrastructure/memory.provider.dart'; @@ -18,19 +15,9 @@ import 'package:immich_mobile/providers/infrastructure/people.provider.dart'; import 'package:immich_mobile/providers/infrastructure/timeline.provider.dart'; import 'package:immich_mobile/providers/user.provider.dart'; import 'package:immich_mobile/routing/router.dart'; -import 'package:immich_mobile/services/album.service.dart'; -import 'package:immich_mobile/services/asset.service.dart'; -import 'package:immich_mobile/services/memory.service.dart'; -import 'package:immich_mobile/widgets/asset_grid/asset_grid_data_structure.dart'; final deepLinkServiceProvider = Provider( (ref) => DeepLinkService( - ref.watch(memoryServiceProvider), - ref.watch(assetServiceProvider), - ref.watch(albumServiceProvider), - ref.watch(currentAssetProvider.notifier), - ref.watch(currentAlbumProvider.notifier), - // Below is used for beta timeline ref.watch(timelineFactoryProvider), ref.watch(beta_asset_provider.assetServiceProvider), ref.watch(remoteAlbumServiceProvider), @@ -41,14 +28,6 @@ final deepLinkServiceProvider = Provider( ); class DeepLinkService { - /// TODO: Remove this when beta is default - final MemoryService _memoryService; - final AssetService _assetService; - final AlbumService _albumService; - final CurrentAsset _currentAsset; - final CurrentAlbum _currentAlbum; - - /// Used for beta timeline final TimelineFactory _betaTimelineFactory; final beta_asset_service.AssetService _betaAssetService; final RemoteAlbumService _betaRemoteAlbumService; @@ -58,11 +37,6 @@ class DeepLinkService { final UserDto? _currentUser; const DeepLinkService( - this._memoryService, - this._assetService, - this._albumService, - this._currentAsset, - this._currentAlbum, this._betaTimelineFactory, this._betaAssetService, this._betaRemoteAlbumService, @@ -75,7 +49,7 @@ class DeepLinkService { return DeepLink([ // we need something to segue back to if the app was cold started // TODO: use MainTimelineRoute this when beta is default - if (isColdStart) (Store.isBetaTimelineEnabled) ? const TabShellRoute() : const PhotosRoute(), + if (isColdStart) const TabShellRoute(), route, ]); } @@ -138,95 +112,52 @@ class DeepLinkService { } Future _buildMemoryDeepLink(String? memoryId) async { - if (Store.isBetaTimelineEnabled) { - List memories = []; + List memories = []; - if (memoryId == null) { - if (_currentUser == null) { - return null; - } - - memories = await _betaMemoryService.getMemoryLane(_currentUser.id); - } else { - final memory = await _betaMemoryService.get(memoryId); - if (memory != null) { - memories = [memory]; - } - } - - if (memories.isEmpty) { + if (memoryId == null) { + if (_currentUser == null) { return null; } - return DriftMemoryRoute(memories: memories, memoryIndex: 0); + memories = await _betaMemoryService.getMemoryLane(_currentUser.id); } else { - // TODO: Remove this when beta is default - if (memoryId == null) { - return null; + final memory = await _betaMemoryService.get(memoryId); + if (memory != null) { + memories = [memory]; } - final memory = await _memoryService.getMemoryById(memoryId); - - if (memory == null) { - return null; - } - - return MemoryRoute(memories: [memory], memoryIndex: 0); } - } - Future _buildAssetDeepLink(String assetId, WidgetRef ref) async { - if (Store.isBetaTimelineEnabled) { - final asset = await _betaAssetService.getRemoteAsset(assetId); - if (asset == null) { - return null; - } - - AssetViewer.setAsset(ref, asset); - return AssetViewerRoute( - initialIndex: 0, - timelineService: _betaTimelineFactory.fromAssets([asset], TimelineOrigin.deepLink), - ); - } else { - // TODO: Remove this when beta is default - final asset = await _assetService.getAssetByRemoteId(assetId); - if (asset == null) { - return null; - } - - _currentAsset.set(asset); - final renderList = await RenderList.fromAssets([asset], GroupAssetsBy.auto); - - return GalleryViewerRoute(renderList: renderList, initialIndex: 0, heroOffset: 0, showStack: true); - } - } - - Future _buildAlbumDeepLink(String albumId) async { - if (Store.isBetaTimelineEnabled) { - final album = await _betaRemoteAlbumService.get(albumId); - - if (album == null) { - return null; - } - - return RemoteAlbumRoute(album: album); - } else { - // TODO: Remove this when beta is default - final album = await _albumService.getAlbumByRemoteId(albumId); - - if (album == null) { - return null; - } - - _currentAlbum.set(album); - return AlbumViewerRoute(albumId: album.id); - } - } - - Future _buildActivityDeepLink(String albumId) async { - if (Store.isBetaTimelineEnabled == false) { + if (memories.isEmpty) { return null; } + return DriftMemoryRoute(memories: memories, memoryIndex: 0); + } + + Future _buildAssetDeepLink(String assetId, WidgetRef ref) async { + final asset = await _betaAssetService.getRemoteAsset(assetId); + if (asset == null) { + return null; + } + + AssetViewer.setAsset(ref, asset); + return AssetViewerRoute( + initialIndex: 0, + timelineService: _betaTimelineFactory.fromAssets([asset], TimelineOrigin.deepLink), + ); + } + + Future _buildAlbumDeepLink(String albumId) async { + final album = await _betaRemoteAlbumService.get(albumId); + + if (album == null) { + return null; + } + + return RemoteAlbumRoute(album: album); + } + + Future _buildActivityDeepLink(String albumId) async { final album = await _betaRemoteAlbumService.get(albumId); if (album == null || album.isActivityEnabled == false) { @@ -237,10 +168,6 @@ class DeepLinkService { } Future _buildPeopleDeepLink(String personId) async { - if (Store.isBetaTimelineEnabled == false) { - return null; - } - final person = await _betaPeopleService.get(personId); if (person == null) { diff --git a/mobile/lib/services/device.service.dart b/mobile/lib/services/device.service.dart deleted file mode 100644 index 50a0d93b24..0000000000 --- a/mobile/lib/services/device.service.dart +++ /dev/null @@ -1,25 +0,0 @@ -import 'package:flutter_udid/flutter_udid.dart'; -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/domain/models/store.model.dart'; -import 'package:immich_mobile/entities/store.entity.dart'; - -final deviceServiceProvider = Provider((ref) => const DeviceService()); - -class DeviceService { - const DeviceService(); - - createDeviceId() { - return FlutterUdid.consistentUdid; - } - - /// Returns the device ID from local storage or creates a new one if not found. - /// - /// This method first attempts to retrieve the device ID from the local store using - /// [StoreKey.deviceId]. If no device ID is found (returns null), it generates a - /// new device ID by calling [createDeviceId]. - /// - /// Returns a [String] representing the device's unique identifier. - String getDeviceId() { - return Store.tryGet(StoreKey.deviceId) ?? createDeviceId(); - } -} diff --git a/mobile/lib/services/download.service.dart b/mobile/lib/services/download.service.dart index 8e810ced2a..3f2c36fa7e 100644 --- a/mobile/lib/services/download.service.dart +++ b/mobile/lib/services/download.service.dart @@ -3,14 +3,9 @@ import 'dart:io'; import 'package:background_downloader/background_downloader.dart'; import 'package:flutter/services.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/constants/constants.dart'; -import 'package:immich_mobile/domain/models/store.model.dart'; -import 'package:immich_mobile/entities/asset.entity.dart'; -import 'package:immich_mobile/entities/store.entity.dart'; import 'package:immich_mobile/models/download/livephotos_medatada.model.dart'; import 'package:immich_mobile/repositories/download.repository.dart'; import 'package:immich_mobile/repositories/file_media.repository.dart'; -import 'package:immich_mobile/services/api.service.dart'; import 'package:logging/logging.dart'; final downloadServiceProvider = Provider( @@ -54,7 +49,7 @@ class DownloadService { final title = task.filename; final relativePath = Platform.isAndroid ? 'DCIM/Immich' : null; try { - final Asset? resultAsset = await _fileMediaRepository.saveImageWithFile( + final resultAsset = await _fileMediaRepository.saveImageWithFile( filePath, title: title, relativePath: relativePath, @@ -76,7 +71,7 @@ class DownloadService { final relativePath = Platform.isAndroid ? 'DCIM/Immich' : null; final file = File(filePath); try { - final Asset? resultAsset = await _fileMediaRepository.saveVideo(file, title: title, relativePath: relativePath); + final resultAsset = await _fileMediaRepository.saveVideo(file, title: title, relativePath: relativePath); return resultAsset != null; } catch (error, stack) { _log.severe("Error saving video", error, stack); @@ -136,62 +131,6 @@ class DownloadService { Future cancelDownload(String id) async { return await FileDownloader().cancelTaskWithId(id); } - - Future> downloadAll(List assets) async { - return await _downloadRepository.downloadAll(assets.expand(_createDownloadTasks).toList()); - } - - Future download(Asset asset) async { - final tasks = _createDownloadTasks(asset); - await _downloadRepository.downloadAll(tasks); - } - - List _createDownloadTasks(Asset asset) { - if (asset.isImage && asset.livePhotoVideoId != null && Platform.isIOS) { - return [ - _buildDownloadTask( - asset.remoteId!, - asset.fileName, - group: kDownloadGroupLivePhoto, - metadata: LivePhotosMetadata(part: LivePhotosPart.image, id: asset.remoteId!).toJson(), - ), - _buildDownloadTask( - asset.livePhotoVideoId!, - asset.fileName.toUpperCase().replaceAll(RegExp(r"\.(JPG|HEIC)$"), '.MOV'), - group: kDownloadGroupLivePhoto, - metadata: LivePhotosMetadata(part: LivePhotosPart.video, id: asset.remoteId!).toJson(), - ), - ]; - } - - if (asset.remoteId == null) { - return []; - } - - return [ - _buildDownloadTask( - asset.remoteId!, - asset.fileName, - group: asset.isImage ? kDownloadGroupImage : kDownloadGroupVideo, - ), - ]; - } - - DownloadTask _buildDownloadTask(String id, String filename, {String? group, String? metadata}) { - final path = r'/assets/{id}/original'.replaceAll('{id}', id); - final serverEndpoint = Store.get(StoreKey.serverEndpoint); - final headers = ApiService.getRequestHeaders(); - - return DownloadTask( - taskId: id, - url: serverEndpoint + path, - headers: headers, - filename: filename, - updates: Updates.statusAndProgress, - group: group ?? '', - metaData: metadata ?? '', - ); - } } TaskRecord _findTaskRecord(List records, String livePhotosId, LivePhotosPart part) { diff --git a/mobile/lib/services/entity.service.dart b/mobile/lib/services/entity.service.dart deleted file mode 100644 index fe7358fce6..0000000000 --- a/mobile/lib/services/entity.service.dart +++ /dev/null @@ -1,44 +0,0 @@ -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/entities/album.entity.dart'; -import 'package:immich_mobile/infrastructure/entities/user.entity.dart'; -import 'package:immich_mobile/infrastructure/repositories/user.repository.dart'; -import 'package:immich_mobile/providers/infrastructure/user.provider.dart'; -import 'package:immich_mobile/repositories/asset.repository.dart'; - -class EntityService { - final AssetRepository _assetRepository; - final IsarUserRepository _isarUserRepository; - const EntityService(this._assetRepository, this._isarUserRepository); - - Future fillAlbumWithDatabaseEntities(Album album) async { - final ownerId = album.ownerId; - if (ownerId != null) { - // replace owner with user from database - final user = await _isarUserRepository.getByUserId(ownerId); - album.owner.value = user == null ? null : User.fromDto(user); - } - final thumbnailAssetId = album.remoteThumbnailAssetId ?? album.thumbnail.value?.remoteId; - if (thumbnailAssetId != null) { - // set thumbnail with asset from database - album.thumbnail.value = await _assetRepository.getByRemoteId(thumbnailAssetId); - } - if (album.remoteUsers.isNotEmpty) { - // replace all users with users from database - final users = await _isarUserRepository.getByUserIds(album.remoteUsers.map((user) => user.id).toList()); - album.sharedUsers.clear(); - album.sharedUsers.addAll(users.nonNulls.map(User.fromDto)); - album.shared = true; - } - if (album.remoteAssets.isNotEmpty) { - // replace all assets with assets from database - final assets = await _assetRepository.getAllByRemoteId(album.remoteAssets.map((asset) => asset.remoteId!)); - album.assets.clear(); - album.assets.addAll(assets); - } - return album; - } -} - -final entityServiceProvider = Provider( - (ref) => EntityService(ref.watch(assetRepositoryProvider), ref.watch(userRepositoryProvider)), -); diff --git a/mobile/lib/services/etag.service.dart b/mobile/lib/services/etag.service.dart deleted file mode 100644 index 00eb83fcea..0000000000 --- a/mobile/lib/services/etag.service.dart +++ /dev/null @@ -1,14 +0,0 @@ -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/repositories/etag.repository.dart'; - -final etagServiceProvider = Provider((ref) => ETagService(ref.watch(etagRepositoryProvider))); - -class ETagService { - final ETagRepository _eTagRepository; - - const ETagService(this._eTagRepository); - - Future clearTable() { - return _eTagRepository.clearTable(); - } -} diff --git a/mobile/lib/services/exif.service.dart b/mobile/lib/services/exif.service.dart deleted file mode 100644 index 57f793b21e..0000000000 --- a/mobile/lib/services/exif.service.dart +++ /dev/null @@ -1,15 +0,0 @@ -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/infrastructure/repositories/exif.repository.dart'; -import 'package:immich_mobile/providers/infrastructure/exif.provider.dart'; - -final exifServiceProvider = Provider((ref) => ExifService(ref.watch(exifRepositoryProvider))); - -class ExifService { - final IsarExifRepository _exifInfoRepository; - - const ExifService(this._exifInfoRepository); - - Future clearTable() { - return _exifInfoRepository.deleteAll(); - } -} diff --git a/mobile/lib/services/hash.service.dart b/mobile/lib/services/hash.service.dart deleted file mode 100644 index 9d1f4e51e8..0000000000 --- a/mobile/lib/services/hash.service.dart +++ /dev/null @@ -1,191 +0,0 @@ -import 'dart:convert'; -import 'dart:io'; - -import 'package:flutter/foundation.dart'; -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/constants/constants.dart'; -import 'package:immich_mobile/domain/models/device_asset.model.dart'; -import 'package:immich_mobile/entities/asset.entity.dart'; -import 'package:immich_mobile/infrastructure/repositories/device_asset.repository.dart'; -import 'package:immich_mobile/providers/infrastructure/device_asset.provider.dart'; -import 'package:immich_mobile/services/background.service.dart'; -import 'package:logging/logging.dart'; - -class HashService { - HashService({ - required IsarDeviceAssetRepository deviceAssetRepository, - required BackgroundService backgroundService, - this.batchSizeLimit = kBatchHashSizeLimit, - int? batchFileLimit, - }) : _deviceAssetRepository = deviceAssetRepository, - _backgroundService = backgroundService, - batchFileLimit = batchFileLimit ?? kBatchHashFileLimit; - - final IsarDeviceAssetRepository _deviceAssetRepository; - final BackgroundService _backgroundService; - final int batchSizeLimit; - final int batchFileLimit; - final _log = Logger('HashService'); - - /// Processes a list of local [Asset]s, storing their hash and returning only those - /// that were successfully hashed. Hashes are looked up in a DB table - /// [DeviceAsset] by local id. Only missing entries are newly hashed and added to the DB table. - Future> hashAssets(List assets) async { - assets.sort(Asset.compareByLocalId); - - // Get and sort DB entries - guaranteed to be a subset of assets - final hashesInDB = await _deviceAssetRepository.getByIds(assets.map((a) => a.localId!).toList()); - hashesInDB.sort((a, b) => a.assetId.compareTo(b.assetId)); - - int dbIndex = 0; - int bytesProcessed = 0; - final hashedAssets = []; - final toBeHashed = <_AssetPath>[]; - final toBeDeleted = []; - - for (int assetIndex = 0; assetIndex < assets.length; assetIndex++) { - final asset = assets[assetIndex]; - DeviceAsset? matchingDbEntry; - - if (dbIndex < hashesInDB.length) { - final deviceAsset = hashesInDB[dbIndex]; - if (deviceAsset.assetId == asset.localId) { - matchingDbEntry = deviceAsset; - dbIndex++; - } - } - - if (matchingDbEntry != null && - matchingDbEntry.hash.isNotEmpty && - matchingDbEntry.modifiedTime.isAtSameMomentAs(asset.fileModifiedAt)) { - // Reuse the existing hash - hashedAssets.add(asset.copyWith(checksum: base64.encode(matchingDbEntry.hash))); - continue; - } - - final file = await _tryGetAssetFile(asset); - if (file == null) { - // Can't access file, delete any DB entry - if (matchingDbEntry != null) { - toBeDeleted.add(matchingDbEntry.assetId); - } - continue; - } - - bytesProcessed += await file.length(); - toBeHashed.add(_AssetPath(asset: asset, path: file.path)); - - if (_shouldProcessBatch(toBeHashed.length, bytesProcessed)) { - hashedAssets.addAll(await _processBatch(toBeHashed, toBeDeleted)); - toBeHashed.clear(); - toBeDeleted.clear(); - bytesProcessed = 0; - } - } - assert(dbIndex == hashesInDB.length, "All hashes should've been processed"); - - // Process any remaining files - if (toBeHashed.isNotEmpty) { - hashedAssets.addAll(await _processBatch(toBeHashed, toBeDeleted)); - } - - // Clean up deleted references - if (toBeDeleted.isNotEmpty) { - await _deviceAssetRepository.deleteIds(toBeDeleted); - } - - return hashedAssets; - } - - bool _shouldProcessBatch(int assetCount, int bytesProcessed) => - assetCount >= batchFileLimit || bytesProcessed >= batchSizeLimit; - - Future _tryGetAssetFile(Asset asset) async { - try { - final file = await asset.local!.originFile; - if (file == null) { - _log.warning( - "Failed to get file for asset ${asset.localId ?? ''}, name: ${asset.fileName}, created on: ${asset.fileCreatedAt}, skipping", - ); - return null; - } - return file; - } catch (error, stackTrace) { - _log.warning( - "Error getting file to hash for asset ${asset.localId ?? ''}, name: ${asset.fileName}, created on: ${asset.fileCreatedAt}, skipping", - error, - stackTrace, - ); - return null; - } - } - - /// Processes a batch of files and returns a list of successfully hashed assets after saving - /// them in [DeviceAssetToHash] for future retrieval - Future> _processBatch(List<_AssetPath> toBeHashed, List toBeDeleted) async { - _log.info("Hashing ${toBeHashed.length} files"); - final hashes = await _hashFiles(toBeHashed.map((e) => e.path).toList()); - assert( - hashes.length == toBeHashed.length, - "Number of Hashes returned from platform should be the same as the input", - ); - - final hashedAssets = []; - final toBeAdded = []; - - for (final (index, hash) in hashes.indexed) { - final asset = toBeHashed.elementAtOrNull(index)?.asset; - if (asset != null && hash?.length == 20) { - hashedAssets.add(asset.copyWith(checksum: base64.encode(hash!))); - toBeAdded.add(DeviceAsset(assetId: asset.localId!, hash: hash, modifiedTime: asset.fileModifiedAt)); - } else { - _log.warning("Failed to hash file ${asset?.localId ?? ''}"); - if (asset != null) { - toBeDeleted.add(asset.localId!); - } - } - } - - // Update the DB for future retrieval - await _deviceAssetRepository.transaction(() async { - await _deviceAssetRepository.updateAll(toBeAdded); - await _deviceAssetRepository.deleteIds(toBeDeleted); - }); - - _log.fine("Hashed ${hashedAssets.length}/${toBeHashed.length} assets"); - return hashedAssets; - } - - /// Hashes the given files and returns a list of the same length. - /// Files that could not be hashed will have a `null` value - Future> _hashFiles(List paths) async { - try { - final hashes = await _backgroundService.digestFiles(paths); - if (hashes != null) { - return hashes; - } - _log.severe("Hashing ${paths.length} files failed"); - } catch (e, s) { - _log.severe("Error occurred while hashing assets", e, s); - } - return List.filled(paths.length, null); - } -} - -class _AssetPath { - final Asset asset; - final String path; - - const _AssetPath({required this.asset, required this.path}); - - _AssetPath copyWith({Asset? asset, String? path}) { - return _AssetPath(asset: asset ?? this.asset, path: path ?? this.path); - } -} - -final hashServiceProvider = Provider( - (ref) => HashService( - deviceAssetRepository: ref.watch(deviceAssetRepositoryProvider), - backgroundService: ref.watch(backgroundServiceProvider), - ), -); diff --git a/mobile/lib/services/local_notification.service.dart b/mobile/lib/services/local_notification.service.dart deleted file mode 100644 index bf85f4a9a9..0000000000 --- a/mobile/lib/services/local_notification.service.dart +++ /dev/null @@ -1,118 +0,0 @@ -import 'package:flutter_local_notifications/flutter_local_notifications.dart'; -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/providers/backup/manual_upload.provider.dart'; -import 'package:immich_mobile/providers/notification_permission.provider.dart'; -import 'package:permission_handler/permission_handler.dart'; -import 'package:immich_mobile/utils/debug_print.dart'; - -final localNotificationService = Provider( - (ref) => LocalNotificationService(ref.watch(notificationPermissionProvider), ref), -); - -class LocalNotificationService { - final FlutterLocalNotificationsPlugin _localNotificationPlugin = FlutterLocalNotificationsPlugin(); - final PermissionStatus _permissionStatus; - final Ref ref; - - LocalNotificationService(this._permissionStatus, this.ref); - - static const manualUploadNotificationID = 4; - static const manualUploadDetailedNotificationID = 5; - static const manualUploadChannelName = 'Manual Asset Upload'; - static const manualUploadChannelID = 'immich/manualUpload'; - static const manualUploadChannelNameDetailed = 'Manual Asset Upload Detailed'; - static const manualUploadDetailedChannelID = 'immich/manualUploadDetailed'; - static const cancelUploadActionID = 'cancel_upload'; - - Future setup() async { - const androidSetting = AndroidInitializationSettings('@drawable/notification_icon'); - const iosSetting = DarwinInitializationSettings(); - - const initSettings = InitializationSettings(android: androidSetting, iOS: iosSetting); - - await _localNotificationPlugin.initialize( - initSettings, - onDidReceiveNotificationResponse: _onDidReceiveForegroundNotificationResponse, - ); - } - - Future _showOrUpdateNotification( - int id, - String title, - String body, - AndroidNotificationDetails androidNotificationDetails, - DarwinNotificationDetails iosNotificationDetails, - ) async { - final notificationDetails = NotificationDetails(android: androidNotificationDetails, iOS: iosNotificationDetails); - - if (_permissionStatus == PermissionStatus.granted) { - await _localNotificationPlugin.show(id, title, body, notificationDetails); - } - } - - Future closeNotification(int id) { - return _localNotificationPlugin.cancel(id); - } - - Future showOrUpdateManualUploadStatus( - String title, - String body, { - bool? isDetailed, - bool? presentBanner, - bool? showActions, - int? maxProgress, - int? progress, - }) { - var notificationlId = manualUploadNotificationID; - var androidChannelID = manualUploadChannelID; - var androidChannelName = manualUploadChannelName; - // Separate Notification for Info/Alerts and Progress - if (isDetailed != null && isDetailed) { - notificationlId = manualUploadDetailedNotificationID; - androidChannelID = manualUploadDetailedChannelID; - androidChannelName = manualUploadChannelNameDetailed; - } - // Progress notification - final androidNotificationDetails = (maxProgress != null && progress != null) - ? AndroidNotificationDetails( - androidChannelID, - androidChannelName, - ticker: title, - showProgress: true, - onlyAlertOnce: true, - maxProgress: maxProgress, - progress: progress, - indeterminate: false, - playSound: false, - priority: Priority.low, - importance: Importance.low, - ongoing: true, - actions: (showActions ?? false) - ? [ - const AndroidNotificationAction(cancelUploadActionID, 'Cancel', showsUserInterface: true), - ] - : null, - ) - // Non-progress notification - : AndroidNotificationDetails(androidChannelID, androidChannelName, playSound: false); - - final iosNotificationDetails = DarwinNotificationDetails( - presentBadge: true, - presentList: true, - presentBanner: presentBanner, - ); - - return _showOrUpdateNotification(notificationlId, title, body, androidNotificationDetails, iosNotificationDetails); - } - - void _onDidReceiveForegroundNotificationResponse(NotificationResponse notificationResponse) { - // Handle notification actions - switch (notificationResponse.actionId) { - case cancelUploadActionID: - { - dPrint(() => "User cancelled manual upload operation"); - ref.read(manualUploadProvider.notifier).cancelBackup(); - } - } - } -} diff --git a/mobile/lib/services/memory.service.dart b/mobile/lib/services/memory.service.dart deleted file mode 100644 index e485bb0957..0000000000 --- a/mobile/lib/services/memory.service.dart +++ /dev/null @@ -1,71 +0,0 @@ -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/extensions/translate_extensions.dart'; -import 'package:immich_mobile/models/memories/memory.model.dart'; -import 'package:immich_mobile/providers/api.provider.dart'; -import 'package:immich_mobile/repositories/asset.repository.dart'; -import 'package:immich_mobile/services/api.service.dart'; -import 'package:logging/logging.dart'; - -final memoryServiceProvider = StateProvider((ref) { - return MemoryService(ref.watch(apiServiceProvider), ref.watch(assetRepositoryProvider)); -}); - -class MemoryService { - final log = Logger("MemoryService"); - - final ApiService _apiService; - final AssetRepository _assetRepository; - - MemoryService(this._apiService, this._assetRepository); - - Future?> getMemoryLane() async { - try { - final now = DateTime.now(); - final data = await _apiService.memoriesApi.searchMemories( - for_: DateTime.utc(now.year, now.month, now.day, 0, 0, 0), - ); - - if (data == null) { - return null; - } - - List memories = []; - - for (final memory in data) { - final dbAssets = await _assetRepository.getAllByRemoteId(memory.assets.map((e) => e.id)); - final yearsAgo = now.year - memory.data.year; - if (dbAssets.isNotEmpty) { - final String title = 'years_ago'.t(args: {'years': yearsAgo.toString()}); - memories.add(Memory(title: title, assets: dbAssets)); - } - } - - return memories.isNotEmpty ? memories : null; - } catch (error, stack) { - log.severe("Cannot get memories", error, stack); - return null; - } - } - - Future getMemoryById(String id) async { - try { - final memoryResponse = await _apiService.memoriesApi.getMemory(id); - - if (memoryResponse == null) { - return null; - } - final dbAssets = await _assetRepository.getAllByRemoteId(memoryResponse.assets.map((e) => e.id)); - if (dbAssets.isEmpty) { - log.warning("No assets found for memory with ID: $id"); - return null; - } - final yearsAgo = DateTime.now().year - memoryResponse.data.year; - final String title = 'years_ago'.t(args: {'years': yearsAgo.toString()}); - - return Memory(title: title, assets: dbAssets); - } catch (error, stack) { - log.severe("Cannot get memory with ID: $id", error, stack); - return null; - } - } -} diff --git a/mobile/lib/services/partner.service.dart b/mobile/lib/services/partner.service.dart deleted file mode 100644 index b8e5ae9a4d..0000000000 --- a/mobile/lib/services/partner.service.dart +++ /dev/null @@ -1,73 +0,0 @@ -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/domain/models/user.model.dart'; -import 'package:immich_mobile/infrastructure/repositories/user.repository.dart'; -import 'package:immich_mobile/providers/infrastructure/user.provider.dart'; -import 'package:immich_mobile/repositories/partner.repository.dart'; -import 'package:immich_mobile/repositories/partner_api.repository.dart'; -import 'package:logging/logging.dart'; - -final partnerServiceProvider = Provider( - (ref) => PartnerService( - ref.watch(partnerApiRepositoryProvider), - ref.watch(userRepositoryProvider), - ref.watch(partnerRepositoryProvider), - ), -); - -class PartnerService { - final PartnerApiRepository _partnerApiRepository; - final PartnerRepository _partnerRepository; - final IsarUserRepository _isarUserRepository; - final Logger _log = Logger("PartnerService"); - - PartnerService(this._partnerApiRepository, this._isarUserRepository, this._partnerRepository); - - Future> getSharedWith() async { - return _partnerRepository.getSharedWith(); - } - - Future> getSharedBy() async { - return _partnerRepository.getSharedBy(); - } - - Stream> watchSharedWith() { - return _partnerRepository.watchSharedWith(); - } - - Stream> watchSharedBy() { - return _partnerRepository.watchSharedBy(); - } - - Future removePartner(UserDto partner) async { - try { - await _partnerApiRepository.delete(partner.id); - await _isarUserRepository.update(partner.copyWith(isPartnerSharedBy: false)); - } catch (e) { - _log.warning("Failed to remove partner ${partner.id}", e); - return false; - } - return true; - } - - Future addPartner(UserDto partner) async { - try { - await _partnerApiRepository.create(partner.id); - await _isarUserRepository.update(partner.copyWith(isPartnerSharedBy: true)); - return true; - } catch (e) { - _log.warning("Failed to add partner ${partner.id}", e); - } - return false; - } - - Future updatePartner(UserDto partner, {required bool inTimeline}) async { - try { - final dto = await _partnerApiRepository.update(partner.id, inTimeline: inTimeline); - await _isarUserRepository.update(partner.copyWith(inTimeline: dto.inTimeline)); - return true; - } catch (e) { - _log.warning("Failed to update partner ${partner.id}", e); - } - return false; - } -} diff --git a/mobile/lib/services/person.service.dart b/mobile/lib/services/person.service.dart index 37b16a8d29..023c62ed78 100644 --- a/mobile/lib/services/person.service.dart +++ b/mobile/lib/services/person.service.dart @@ -1,8 +1,5 @@ import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/domain/models/person.model.dart'; -import 'package:immich_mobile/entities/asset.entity.dart'; -import 'package:immich_mobile/repositories/asset.repository.dart'; -import 'package:immich_mobile/repositories/asset_api.repository.dart'; import 'package:immich_mobile/repositories/person_api.repository.dart'; import 'package:logging/logging.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart'; @@ -10,19 +7,12 @@ import 'package:riverpod_annotation/riverpod_annotation.dart'; part 'person.service.g.dart'; @riverpod -PersonService personService(Ref ref) => PersonService( - ref.watch(personApiRepositoryProvider), - ref.watch(assetApiRepositoryProvider), - ref.read(assetRepositoryProvider), -); +PersonService personService(Ref ref) => PersonService(ref.watch(personApiRepositoryProvider)); class PersonService { final Logger _log = Logger("PersonService"); final PersonApiRepository _personApiRepository; - final AssetApiRepository _assetApiRepository; - final AssetRepository _assetRepository; - - PersonService(this._personApiRepository, this._assetApiRepository, this._assetRepository); + PersonService(this._personApiRepository); Future> getAllPeople() async { try { @@ -33,16 +23,6 @@ class PersonService { } } - Future> getPersonAssets(String id) async { - try { - final assets = await _assetApiRepository.search(personIds: [id]); - return await _assetRepository.getAllByRemoteId(assets.map((a) => a.remoteId!)); - } catch (error, stack) { - _log.severe("Error while fetching person assets", error, stack); - } - return []; - } - Future updateName(String id, String name) async { try { return await _personApiRepository.update(id, name: name); diff --git a/mobile/lib/services/person.service.g.dart b/mobile/lib/services/person.service.g.dart index 8c2d46b3bd..4caf1ea434 100644 --- a/mobile/lib/services/person.service.g.dart +++ b/mobile/lib/services/person.service.g.dart @@ -6,7 +6,7 @@ part of 'person.service.dart'; // RiverpodGenerator // ************************************************************************** -String _$personServiceHash() => r'10883bccc6c402205e6785cf9ee6cd7142cd0983'; +String _$personServiceHash() => r'646e38d764c52e63d9fca86992e440f34196d519'; /// See also [personService]. @ProviderFor(personService) diff --git a/mobile/lib/services/search.service.dart b/mobile/lib/services/search.service.dart index f33adf80f9..0330c8485c 100644 --- a/mobile/lib/services/search.service.dart +++ b/mobile/lib/services/search.service.dart @@ -1,31 +1,22 @@ import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/extensions/string_extensions.dart'; import 'package:immich_mobile/infrastructure/repositories/search_api.repository.dart'; -import 'package:immich_mobile/models/search/search_filter.model.dart'; -import 'package:immich_mobile/models/search/search_result.model.dart'; import 'package:immich_mobile/providers/api.provider.dart'; import 'package:immich_mobile/providers/infrastructure/search.provider.dart'; -import 'package:immich_mobile/repositories/asset.repository.dart'; import 'package:immich_mobile/services/api.service.dart'; +import 'package:immich_mobile/utils/debug_print.dart'; import 'package:logging/logging.dart'; import 'package:openapi/api.dart'; -import 'package:immich_mobile/utils/debug_print.dart'; final searchServiceProvider = Provider( - (ref) => SearchService( - ref.watch(apiServiceProvider), - ref.watch(assetRepositoryProvider), - ref.watch(searchApiRepositoryProvider), - ), + (ref) => SearchService(ref.watch(apiServiceProvider), ref.watch(searchApiRepositoryProvider)), ); class SearchService { final ApiService _apiService; - final AssetRepository _assetRepository; final SearchApiRepository _searchApiRepository; final _log = Logger("SearchService"); - SearchService(this._apiService, this._assetRepository, this._searchApiRepository); + SearchService(this._apiService, this._searchApiRepository); Future?> getSearchSuggestions( SearchSuggestionType type, { @@ -48,24 +39,6 @@ class SearchService { } } - Future search(SearchFilter filter, int page) async { - try { - final response = await _searchApiRepository.search(filter, page); - - if (response == null || response.assets.items.isEmpty) { - return null; - } - - return SearchResult( - assets: await _assetRepository.getAllByRemoteId(response.assets.items.map((e) => e.id)), - nextPage: response.assets.nextPage?.toInt(), - ); - } catch (error, stackTrace) { - _log.severe("Failed to search for assets", error, stackTrace); - } - return null; - } - Future?> getExploreData() async { try { return await _apiService.searchApi.getExploreData(); diff --git a/mobile/lib/services/share.service.dart b/mobile/lib/services/share.service.dart deleted file mode 100644 index a0998d6d3d..0000000000 --- a/mobile/lib/services/share.service.dart +++ /dev/null @@ -1,74 +0,0 @@ -import 'dart:async'; -import 'dart:io'; - -import 'package:flutter/material.dart'; -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/entities/asset.entity.dart'; -import 'package:immich_mobile/extensions/response_extensions.dart'; -import 'package:immich_mobile/providers/api.provider.dart'; -import 'package:immich_mobile/services/api.service.dart'; -import 'package:logging/logging.dart'; -import 'package:path_provider/path_provider.dart'; -import 'package:share_plus/share_plus.dart'; - -final shareServiceProvider = Provider((ref) => ShareService(ref.watch(apiServiceProvider))); - -class ShareService { - final ApiService _apiService; - final Logger _log = Logger("ShareService"); - - ShareService(this._apiService); - - Future shareAsset(Asset asset, BuildContext context) async { - return await shareAssets([asset], context); - } - - Future shareAssets(List assets, BuildContext context) async { - try { - final downloadedXFiles = []; - - for (var asset in assets) { - if (asset.isLocal) { - // Prefer local assets to share - File? f = await asset.local!.originFile; - downloadedXFiles.add(XFile(f!.path)); - } else if (asset.isRemote) { - // Download remote asset otherwise - final tempDir = await getTemporaryDirectory(); - final fileName = asset.fileName; - final tempFile = await File('${tempDir.path}/$fileName').create(); - final res = await _apiService.assetsApi.downloadAssetWithHttpInfo(asset.remoteId!); - - if (res.statusCode != 200) { - _log.severe("Asset download for ${asset.fileName} failed", res.toLoggerString()); - continue; - } - - tempFile.writeAsBytesSync(res.bodyBytes); - downloadedXFiles.add(XFile(tempFile.path)); - } - } - - if (downloadedXFiles.isEmpty) { - _log.warning("No asset can be retrieved for share"); - return false; - } - - if (downloadedXFiles.length != assets.length) { - _log.warning("Partial share - Requested: ${assets.length}, Sharing: ${downloadedXFiles.length}"); - } - - final size = MediaQuery.of(context).size; - unawaited( - Share.shareXFiles( - downloadedXFiles, - sharePositionOrigin: Rect.fromPoints(Offset.zero, Offset(size.width / 3, size.height)), - ), - ); - return true; - } catch (error) { - _log.severe("Share failed", error); - } - return false; - } -} diff --git a/mobile/lib/services/stack.service.dart b/mobile/lib/services/stack.service.dart deleted file mode 100644 index 88189c6bcd..0000000000 --- a/mobile/lib/services/stack.service.dart +++ /dev/null @@ -1,64 +0,0 @@ -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/entities/asset.entity.dart'; -import 'package:immich_mobile/providers/api.provider.dart'; -import 'package:immich_mobile/repositories/asset.repository.dart'; -import 'package:immich_mobile/services/api.service.dart'; -import 'package:openapi/api.dart'; -import 'package:immich_mobile/utils/debug_print.dart'; - -class StackService { - const StackService(this._api, this._assetRepository); - - final ApiService _api; - final AssetRepository _assetRepository; - - Future getStack(String stackId) async { - try { - return _api.stacksApi.getStack(stackId); - } catch (error) { - dPrint(() => "Error while fetching stack: $error"); - } - return null; - } - - Future createStack(List assetIds) async { - try { - return _api.stacksApi.createStack(StackCreateDto(assetIds: assetIds)); - } catch (error) { - dPrint(() => "Error while creating stack: $error"); - } - return null; - } - - Future updateStack(String stackId, String primaryAssetId) async { - try { - return await _api.stacksApi.updateStack(stackId, StackUpdateDto(primaryAssetId: primaryAssetId)); - } catch (error) { - dPrint(() => "Error while updating stack children: $error"); - } - return null; - } - - Future deleteStack(String stackId, List assets) async { - try { - await _api.stacksApi.deleteStack(stackId); - - // Update local database to trigger rerendering - final List removeAssets = []; - for (final asset in assets) { - asset.stackId = null; - asset.stackPrimaryAssetId = null; - asset.stackCount = 0; - - removeAssets.add(asset); - } - await _assetRepository.transaction(() => _assetRepository.updateAll(removeAssets)); - } catch (error) { - dPrint(() => "Error while deleting stack: $error"); - } - } -} - -final stackServiceProvider = Provider( - (ref) => StackService(ref.watch(apiServiceProvider), ref.watch(assetRepositoryProvider)), -); diff --git a/mobile/lib/services/sync.service.dart b/mobile/lib/services/sync.service.dart deleted file mode 100644 index f5b55f36eb..0000000000 --- a/mobile/lib/services/sync.service.dart +++ /dev/null @@ -1,945 +0,0 @@ -import 'dart:async'; -import 'dart:io'; - -import 'package:collection/collection.dart'; -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/constants/enums.dart'; -import 'package:immich_mobile/domain/models/user.model.dart'; -import 'package:immich_mobile/domain/services/user.service.dart'; -import 'package:immich_mobile/entities/album.entity.dart'; -import 'package:immich_mobile/entities/asset.entity.dart'; -import 'package:immich_mobile/entities/etag.entity.dart'; -import 'package:immich_mobile/extensions/collection_extensions.dart'; -import 'package:immich_mobile/infrastructure/repositories/exif.repository.dart'; -import 'package:immich_mobile/infrastructure/repositories/user.repository.dart'; -import 'package:immich_mobile/infrastructure/repositories/user_api.repository.dart'; -import 'package:immich_mobile/providers/app_settings.provider.dart'; -import 'package:immich_mobile/providers/infrastructure/exif.provider.dart'; -import 'package:immich_mobile/providers/infrastructure/user.provider.dart'; -import 'package:immich_mobile/repositories/album.repository.dart'; -import 'package:immich_mobile/repositories/album_api.repository.dart'; -import 'package:immich_mobile/repositories/album_media.repository.dart'; -import 'package:immich_mobile/repositories/asset.repository.dart'; -import 'package:immich_mobile/repositories/etag.repository.dart'; -import 'package:immich_mobile/repositories/local_files_manager.repository.dart'; -import 'package:immich_mobile/repositories/partner.repository.dart'; -import 'package:immich_mobile/repositories/partner_api.repository.dart'; -import 'package:immich_mobile/services/app_settings.service.dart'; -import 'package:immich_mobile/services/entity.service.dart'; -import 'package:immich_mobile/services/hash.service.dart'; -import 'package:immich_mobile/utils/async_mutex.dart'; -import 'package:immich_mobile/utils/datetime_comparison.dart'; -import 'package:immich_mobile/utils/diff.dart'; -import 'package:immich_mobile/utils/hash.dart'; -import 'package:logging/logging.dart'; - -final syncServiceProvider = Provider( - (ref) => SyncService( - ref.watch(hashServiceProvider), - ref.watch(entityServiceProvider), - ref.watch(albumMediaRepositoryProvider), - ref.watch(albumApiRepositoryProvider), - ref.watch(albumRepositoryProvider), - ref.watch(assetRepositoryProvider), - ref.watch(exifRepositoryProvider), - ref.watch(partnerRepositoryProvider), - ref.watch(userRepositoryProvider), - ref.watch(userServiceProvider), - ref.watch(etagRepositoryProvider), - ref.watch(appSettingsServiceProvider), - ref.watch(localFilesManagerRepositoryProvider), - ref.watch(partnerApiRepositoryProvider), - ref.watch(userApiRepositoryProvider), - ), -); - -class SyncService { - final HashService _hashService; - final EntityService _entityService; - final AlbumMediaRepository _albumMediaRepository; - final AlbumApiRepository _albumApiRepository; - final AlbumRepository _albumRepository; - final AssetRepository _assetRepository; - final IsarExifRepository _exifInfoRepository; - final IsarUserRepository _isarUserRepository; - final UserService _userService; - final PartnerRepository _partnerRepository; - final ETagRepository _eTagRepository; - final PartnerApiRepository _partnerApiRepository; - final UserApiRepository _userApiRepository; - final AsyncMutex _lock = AsyncMutex(); - final Logger _log = Logger('SyncService'); - final AppSettingsService _appSettingsService; - final LocalFilesManagerRepository _localFilesManager; - - SyncService( - this._hashService, - this._entityService, - this._albumMediaRepository, - this._albumApiRepository, - this._albumRepository, - this._assetRepository, - this._exifInfoRepository, - this._partnerRepository, - this._isarUserRepository, - this._userService, - this._eTagRepository, - this._appSettingsService, - this._localFilesManager, - this._partnerApiRepository, - this._userApiRepository, - ); - - // public methods: - - /// Syncs users from the server to the local database - /// Returns `true`if there were any changes - Future syncUsersFromServer(List users) => _lock.run(() => _syncUsersFromServer(users)); - - /// Syncs remote assets owned by the logged-in user to the DB - /// Returns `true` if there were any changes - Future syncRemoteAssetsToDb({ - required List users, - required Future<(List? toUpsert, List? toDelete)> Function(List users, DateTime since) - getChangedAssets, - required FutureOr?> Function(UserDto user, DateTime until) loadAssets, - }) => _lock.run( - () async => - await _syncRemoteAssetChanges(users, getChangedAssets) ?? - await _syncRemoteAssetsFull(getUsersFromServer, loadAssets), - ); - - /// Syncs remote albums to the database - /// returns `true` if there were any changes - Future syncRemoteAlbumsToDb(List remote) => _lock.run(() => _syncRemoteAlbumsToDb(remote)); - - /// Syncs all device albums and their assets to the database - /// Returns `true` if there were any changes - Future syncLocalAlbumAssetsToDb(List onDevice, [Set? excludedAssets]) => - _lock.run(() => _syncLocalAlbumAssetsToDb(onDevice, excludedAssets)); - - /// returns all Asset IDs that are not contained in the existing list - List sharedAssetsToRemove(List deleteCandidates, List existing) { - if (deleteCandidates.isEmpty) { - return []; - } - deleteCandidates.sort(Asset.compareById); - existing.sort(Asset.compareById); - return _diffAssets(existing, deleteCandidates, compare: Asset.compareById).$3.map((e) => e.id).toList(); - } - - /// Syncs a new asset to the db. Returns `true` if successful - Future syncNewAssetToDb(Asset newAsset) => _lock.run(() => _syncNewAssetToDb(newAsset)); - - Future removeAllLocalAlbumsAndAssets() => _lock.run(_removeAllLocalAlbumsAndAssets); - - // private methods: - - /// Syncs users from the server to the local database - /// Returns `true`if there were any changes - Future _syncUsersFromServer(List users) async { - users.sortBy((u) => u.id); - final dbUsers = await _isarUserRepository.getAll(sortBy: SortUserBy.id); - final List toDelete = []; - final List toUpsert = []; - final changes = diffSortedListsSync( - users, - dbUsers, - compare: (UserDto a, UserDto b) => a.id.compareTo(b.id), - both: (UserDto a, UserDto b) { - if ((a.updatedAt == null && b.updatedAt != null) || - (a.updatedAt != null && b.updatedAt == null) || - (a.updatedAt != null && b.updatedAt != null && !a.updatedAt!.isAtSameMomentAs(b.updatedAt!)) || - a.isPartnerSharedBy != b.isPartnerSharedBy || - a.isPartnerSharedWith != b.isPartnerSharedWith || - a.inTimeline != b.inTimeline) { - toUpsert.add(a); - return true; - } - return false; - }, - onlyFirst: (UserDto a) => toUpsert.add(a), - onlySecond: (UserDto b) => toDelete.add(b.id), - ); - if (changes) { - await _isarUserRepository.transaction(() async { - await _isarUserRepository.delete(toDelete); - await _isarUserRepository.updateAll(toUpsert); - }); - } - return changes; - } - - /// Syncs a new asset to the db. Returns `true` if successful - Future _syncNewAssetToDb(Asset a) async { - final Asset? inDb = await _assetRepository.getByOwnerIdChecksum(a.ownerId, a.checksum); - if (inDb != null) { - // unify local/remote assets by replacing the - // local-only asset in the DB with a local&remote asset - a = inDb.updatedCopy(a); - } - try { - await _assetRepository.update(a); - } catch (e) { - _log.severe("Failed to put new asset into db", e); - return false; - } - return true; - } - - /// Efficiently syncs assets via changes. Returns `null` when a full sync is required. - Future _syncRemoteAssetChanges( - List users, - Future<(List? toUpsert, List? toDelete)> Function(List users, DateTime since) - getChangedAssets, - ) async { - final currentUser = _userService.getMyUser(); - final DateTime? since = (await _eTagRepository.get(currentUser.id))?.time?.toUtc(); - if (since == null) return null; - final DateTime now = DateTime.now(); - final (toUpsert, toDelete) = await getChangedAssets(users, since); - if (toUpsert == null || toDelete == null) { - await _clearUserAssetsETag(users); - return null; - } - try { - if (toDelete.isNotEmpty) { - await handleRemoteAssetRemoval(toDelete); - } - if (toUpsert.isNotEmpty) { - final (_, updated) = await _linkWithExistingFromDb(toUpsert); - await upsertAssetsWithExif(updated); - } - if (toUpsert.isNotEmpty || toDelete.isNotEmpty) { - await _updateUserAssetsETag(users, now); - return true; - } - return false; - } catch (e) { - _log.severe("Failed to sync remote assets to db", e); - } - return null; - } - - Future _moveToTrashMatchedAssets(Iterable idsToDelete) async { - final List localAssets = await _assetRepository.getAllLocal(); - final List matchedAssets = localAssets.where((asset) => idsToDelete.contains(asset.remoteId)).toList(); - - final mediaUrls = await Future.wait(matchedAssets.map((asset) => asset.local?.getMediaUrl() ?? Future.value(null))); - - await _localFilesManager.moveToTrash(mediaUrls.nonNulls.toList()); - } - - /// Deletes remote-only assets, updates merged assets to be local-only - Future handleRemoteAssetRemoval(List idsToDelete) async { - return _assetRepository.transaction(() async { - await _assetRepository.deleteAllByRemoteId(idsToDelete, state: AssetState.remote); - final merged = await _assetRepository.getAllByRemoteId(idsToDelete, state: AssetState.merged); - if (Platform.isAndroid && _appSettingsService.getSetting(AppSettingsEnum.manageLocalMediaAndroid)) { - await _moveToTrashMatchedAssets(idsToDelete); - } - if (merged.isEmpty) return; - for (final Asset asset in merged) { - asset.remoteId = null; - asset.isTrashed = false; - } - await _assetRepository.updateAll(merged); - }); - } - - Future> _getAllAccessibleUsers() async { - final sharedWith = (await _partnerRepository.getSharedWith()).toSet(); - sharedWith.add(_userService.getMyUser()); - return sharedWith.toList(); - } - - /// Syncs assets by loading and comparing all assets from the server. - Future _syncRemoteAssetsFull( - FutureOr?> Function() refreshUsers, - FutureOr?> Function(UserDto user, DateTime until) loadAssets, - ) async { - final serverUsers = await refreshUsers(); - if (serverUsers == null) { - _log.warning("_syncRemoteAssetsFull aborted because user refresh failed"); - return false; - } - await _syncUsersFromServer(serverUsers); - final List users = await _getAllAccessibleUsers(); - bool changes = false; - for (UserDto u in users) { - changes |= await _syncRemoteAssetsForUser(u, loadAssets); - } - return changes; - } - - Future _syncRemoteAssetsForUser( - UserDto user, - FutureOr?> Function(UserDto user, DateTime until) loadAssets, - ) async { - final DateTime now = DateTime.now().toUtc(); - final List? remote = await loadAssets(user, now); - if (remote == null) { - return false; - } - final List inDb = await _assetRepository.getAll(ownerId: user.id, sortBy: AssetSort.checksum); - assert(inDb.isSorted(Asset.compareByChecksum), "inDb not sorted!"); - - remote.sort(Asset.compareByChecksum); - - // filter our duplicates that might be introduced by the chunked retrieval - remote.uniqueConsecutive(compare: Asset.compareByChecksum); - - final (toAdd, toUpdate, toRemove) = _diffAssets(remote, inDb, remote: true); - if (toAdd.isEmpty && toUpdate.isEmpty && toRemove.isEmpty) { - await _updateUserAssetsETag([user], now); - return false; - } - final idsToDelete = toRemove.map((e) => e.id).toList(); - try { - await _assetRepository.deleteByIds(idsToDelete); - await upsertAssetsWithExif(toAdd + toUpdate); - } catch (e) { - _log.severe("Failed to sync remote assets to db", e); - } - await _updateUserAssetsETag([user], now); - return true; - } - - Future _updateUserAssetsETag(List users, DateTime time) { - final etags = users.map((u) => ETag(id: u.id, time: time)).toList(); - return _eTagRepository.upsertAll(etags); - } - - Future _clearUserAssetsETag(List users) { - final ids = users.map((u) => u.id).toList(); - return _eTagRepository.deleteByIds(ids); - } - - /// Syncs remote albums to the database - /// returns `true` if there were any changes - Future _syncRemoteAlbumsToDb(List remoteAlbums) async { - remoteAlbums.sortBy((e) => e.remoteId!); - - final List dbAlbums = await _albumRepository.getAll(remote: true, sortBy: AlbumSort.remoteId); - - final List toDelete = []; - final List existing = []; - - final bool changes = await diffSortedLists( - remoteAlbums, - dbAlbums, - compare: (remoteAlbum, dbAlbum) => remoteAlbum.remoteId!.compareTo(dbAlbum.remoteId!), - both: (remoteAlbum, dbAlbum) => _syncRemoteAlbum(remoteAlbum, dbAlbum, toDelete, existing), - onlyFirst: (remoteAlbum) => _addAlbumFromServer(remoteAlbum, existing), - onlySecond: (dbAlbum) => _removeAlbumFromDb(dbAlbum, toDelete), - ); - - if (toDelete.isNotEmpty) { - final List idsToRemove = sharedAssetsToRemove(toDelete, existing); - if (idsToRemove.isNotEmpty) { - await _assetRepository.deleteByIds(idsToRemove); - } - } else { - assert(toDelete.isEmpty); - } - return changes; - } - - /// syncs albums from the server to the local database (does not support - /// syncing changes from local back to server) - /// accumulates - Future _syncRemoteAlbum(Album dto, Album album, List deleteCandidates, List existing) async { - if (!_hasRemoteAlbumChanged(dto, album)) { - return false; - } - // loadDetails (/api/album/:id) will not include lastModifiedAssetTimestamp, - // i.e. it will always be null. Save it here. - final originalDto = dto; - dto = await _albumApiRepository.get(dto.remoteId!); - - final assetsInDb = await _assetRepository.getByAlbum(album, sortBy: AssetSort.ownerIdChecksum); - assert(assetsInDb.isSorted(Asset.compareByOwnerChecksum), "inDb unsorted!"); - final List assetsOnRemote = dto.remoteAssets.toList(); - assetsOnRemote.sort(Asset.compareByOwnerChecksum); - final (toAdd, toUpdate, toUnlink) = _diffAssets(assetsOnRemote, assetsInDb, compare: Asset.compareByOwnerChecksum); - - // update shared users - final List sharedUsers = album.sharedUsers.map((u) => u.toDto()).toList(growable: false); - sharedUsers.sort((a, b) => a.id.compareTo(b.id)); - final List users = dto.remoteUsers.map((u) => u.toDto()).toList()..sort((a, b) => a.id.compareTo(b.id)); - final List userIdsToAdd = []; - final List usersToUnlink = []; - diffSortedListsSync( - users, - sharedUsers, - compare: (UserDto a, UserDto b) => a.id.compareTo(b.id), - both: (a, b) => false, - onlyFirst: (UserDto a) => userIdsToAdd.add(a.id), - onlySecond: (UserDto a) => usersToUnlink.add(a), - ); - - // for shared album: put missing album assets into local DB - final (existingInDb, updated) = await _linkWithExistingFromDb(toAdd); - await upsertAssetsWithExif(updated); - final assetsToLink = existingInDb + updated; - final usersToLink = await _isarUserRepository.getByUserIds(userIdsToAdd); - - album.name = dto.name; - album.description = dto.description; - album.shared = dto.shared; - album.createdAt = dto.createdAt; - album.modifiedAt = dto.modifiedAt; - album.startDate = dto.startDate; - album.endDate = dto.endDate; - album.lastModifiedAssetTimestamp = originalDto.lastModifiedAssetTimestamp; - album.shared = dto.shared; - album.activityEnabled = dto.activityEnabled; - album.sortOrder = dto.sortOrder; - - final remoteThumbnailAssetId = dto.remoteThumbnailAssetId; - if (remoteThumbnailAssetId != null && album.thumbnail.value?.remoteId != remoteThumbnailAssetId) { - album.thumbnail.value = await _assetRepository.getByRemoteId(remoteThumbnailAssetId); - } - - // write & commit all changes to DB - try { - await _assetRepository.transaction(() async { - await _assetRepository.updateAll(toUpdate); - await _albumRepository.addUsers(album, usersToLink.nonNulls.toList()); - await _albumRepository.removeUsers(album, usersToUnlink); - await _albumRepository.addAssets(album, assetsToLink); - await _albumRepository.removeAssets(album, toUnlink); - await _albumRepository.recalculateMetadata(album); - await _albumRepository.update(album); - }); - _log.info("Synced changes of remote album ${album.name} to DB"); - } catch (e) { - _log.severe("Failed to sync remote album to database", e); - } - - if (album.shared || dto.shared) { - final userId = (_userService.getMyUser()).id; - final foreign = await _assetRepository.getByAlbum(album, notOwnedBy: [userId]); - existing.addAll(foreign); - - // delete assets in DB unless they belong to this user or part of some other shared album - final isarUserId = fastHash(userId); - deleteCandidates.addAll(toUnlink.where((a) => a.ownerId != isarUserId)); - } - - return true; - } - - /// Adds a remote album to the database while making sure to add any foreign - /// (shared) assets to the database beforehand - /// accumulates assets already existing in the database - Future _addAlbumFromServer(Album album, List existing) async { - if (album.remoteAssetCount != album.remoteAssets.length) { - album = await _albumApiRepository.get(album.remoteId!); - } - if (album.remoteAssetCount == album.remoteAssets.length) { - // in case an album contains assets not yet present in local DB: - // put missing album assets into local DB - final (existingInDb, updated) = await _linkWithExistingFromDb(album.remoteAssets.toList()); - existing.addAll(existingInDb); - await upsertAssetsWithExif(updated); - - await _entityService.fillAlbumWithDatabaseEntities(album); - await _albumRepository.create(album); - } else { - _log.warning( - "Failed to add album from server: assetCount ${album.remoteAssetCount} != " - "asset array length ${album.remoteAssets.length} for album ${album.name}", - ); - } - } - - /// Accumulates all suitable album assets to the `deleteCandidates` and - /// removes the album from the database. - Future _removeAlbumFromDb(Album album, List deleteCandidates) async { - if (album.isLocal) { - _log.info("Removing local album $album from DB"); - // delete assets in DB unless they are remote or part of some other album - deleteCandidates.addAll(await _assetRepository.getByAlbum(album, state: AssetState.local)); - } else if (album.shared) { - // delete assets in DB unless they belong to this user or are part of some other shared album or belong to a partner - final userIds = (await _getAllAccessibleUsers()).map((user) => user.id); - final orphanedAssets = await _assetRepository.getByAlbum(album, notOwnedBy: userIds); - deleteCandidates.addAll(orphanedAssets); - } - try { - await _albumRepository.delete(album.id); - _log.info("Removed local album $album from DB"); - } catch (e) { - _log.severe("Failed to remove local album $album from DB", e); - } - } - - /// Syncs all device albums and their assets to the database - /// Returns `true` if there were any changes - Future _syncLocalAlbumAssetsToDb(List onDevice, [Set? excludedAssets]) async { - onDevice.sort((a, b) => a.localId!.compareTo(b.localId!)); - final inDb = await _albumRepository.getAll(remote: false, sortBy: AlbumSort.localId); - final List deleteCandidates = []; - final List existing = []; - final bool anyChanges = await diffSortedLists( - onDevice, - inDb, - compare: (Album a, Album b) => a.localId!.compareTo(b.localId!), - both: (Album a, Album b) => _syncAlbumInDbAndOnDevice(a, b, deleteCandidates, existing, excludedAssets), - onlyFirst: (Album a) => _addAlbumFromDevice(a, existing, excludedAssets), - onlySecond: (Album a) => _removeAlbumFromDb(a, deleteCandidates), - ); - _log.fine("Syncing all local albums almost done. Collected ${deleteCandidates.length} asset candidates to delete"); - final (toDelete, toUpdate) = _handleAssetRemoval(deleteCandidates, existing, remote: false); - _log.fine("${toDelete.length} assets to delete, ${toUpdate.length} to update"); - if (toDelete.isNotEmpty || toUpdate.isNotEmpty) { - await _assetRepository.transaction(() async { - await _assetRepository.deleteByIds(toDelete); - await _assetRepository.updateAll(toUpdate); - }); - _log.info("Removed ${toDelete.length} and updated ${toUpdate.length} local assets from DB"); - } - return anyChanges; - } - - /// Syncs the device album to the album in the database - /// returns `true` if there were any changes - /// Accumulates asset candidates to delete and those already existing in DB - Future _syncAlbumInDbAndOnDevice( - Album deviceAlbum, - Album dbAlbum, - List deleteCandidates, - List existing, [ - Set? excludedAssets, - bool forceRefresh = false, - ]) async { - _log.info("Syncing a local album to DB: ${deviceAlbum.name}"); - if (!forceRefresh && !await _hasAlbumChangeOnDevice(deviceAlbum, dbAlbum)) { - _log.info("Local album ${deviceAlbum.name} has not changed. Skipping sync."); - return false; - } - _log.info("Local album ${deviceAlbum.name} has changed. Syncing..."); - if (!forceRefresh && excludedAssets == null && await _syncDeviceAlbumFast(deviceAlbum, dbAlbum)) { - _log.info("Fast synced local album ${deviceAlbum.name} to DB"); - return true; - } - // general case, e.g. some assets have been deleted or there are excluded albums on iOS - final inDb = await _assetRepository.getByAlbum( - dbAlbum, - ownerId: (_userService.getMyUser()).id, - sortBy: AssetSort.checksum, - ); - - assert(inDb.isSorted(Asset.compareByChecksum), "inDb not sorted!"); - final int assetCountOnDevice = await _albumMediaRepository.getAssetCount(deviceAlbum.localId!); - final List onDevice = await _getHashedAssets(deviceAlbum, excludedAssets: excludedAssets); - _removeDuplicates(onDevice); - // _removeDuplicates sorts `onDevice` by checksum - final (toAdd, toUpdate, toDelete) = _diffAssets(onDevice, inDb); - if (toAdd.isEmpty && - toUpdate.isEmpty && - toDelete.isEmpty && - dbAlbum.name == deviceAlbum.name && - dbAlbum.description == deviceAlbum.description && - dbAlbum.modifiedAt.isAtSameMomentAs(deviceAlbum.modifiedAt)) { - // changes only affeted excluded albums - _log.info("Only excluded assets in local album ${deviceAlbum.name} changed. Stopping sync."); - if (assetCountOnDevice != (await _eTagRepository.getById(deviceAlbum.eTagKeyAssetCount))?.assetCount) { - await _eTagRepository.upsertAll([ETag(id: deviceAlbum.eTagKeyAssetCount, assetCount: assetCountOnDevice)]); - } - return false; - } - _log.info( - "Syncing local album ${deviceAlbum.name}. ${toAdd.length} assets to add, ${toUpdate.length} to update, ${toDelete.length} to delete", - ); - final (existingInDb, updated) = await _linkWithExistingFromDb(toAdd); - _log.info( - "Linking assets to add with existing from db. ${existingInDb.length} existing, ${updated.length} to update", - ); - deleteCandidates.addAll(toDelete); - existing.addAll(existingInDb); - dbAlbum.name = deviceAlbum.name; - dbAlbum.description = deviceAlbum.description; - dbAlbum.modifiedAt = deviceAlbum.modifiedAt; - if (dbAlbum.thumbnail.value != null && toDelete.contains(dbAlbum.thumbnail.value)) { - dbAlbum.thumbnail.value = null; - } - try { - await _assetRepository.transaction(() async { - await _assetRepository.updateAll(updated + toUpdate); - await _albumRepository.addAssets(dbAlbum, existingInDb + updated); - await _albumRepository.removeAssets(dbAlbum, toDelete); - await _albumRepository.recalculateMetadata(dbAlbum); - await _albumRepository.update(dbAlbum); - await _eTagRepository.upsertAll([ETag(id: deviceAlbum.eTagKeyAssetCount, assetCount: assetCountOnDevice)]); - }); - _log.info("Synced changes of local album ${deviceAlbum.name} to DB"); - } catch (e) { - _log.severe("Failed to update synced album ${deviceAlbum.name} in DB", e); - } - - return true; - } - - /// fast path for common case: only new assets were added to device album - /// returns `true` if successful, else `false` - Future _syncDeviceAlbumFast(Album deviceAlbum, Album dbAlbum) async { - if (!deviceAlbum.modifiedAt.isAfter(dbAlbum.modifiedAt)) { - _log.info("Local album ${deviceAlbum.name} has not changed. Skipping sync."); - return false; - } - final int totalOnDevice = await _albumMediaRepository.getAssetCount(deviceAlbum.localId!); - final int lastKnownTotal = (await _eTagRepository.getById(deviceAlbum.eTagKeyAssetCount))?.assetCount ?? 0; - if (totalOnDevice <= lastKnownTotal) { - _log.info("Local album ${deviceAlbum.name} totalOnDevice is less than lastKnownTotal. Skipping sync."); - return false; - } - final List newAssets = await _getHashedAssets( - deviceAlbum, - modifiedFrom: dbAlbum.modifiedAt.add(const Duration(seconds: 1)), - modifiedUntil: deviceAlbum.modifiedAt, - ); - - if (totalOnDevice != lastKnownTotal + newAssets.length) { - _log.info( - "Local album ${deviceAlbum.name} totalOnDevice is not equal to lastKnownTotal + newAssets.length. Skipping sync.", - ); - return false; - } - dbAlbum.modifiedAt = deviceAlbum.modifiedAt; - _removeDuplicates(newAssets); - final (existingInDb, updated) = await _linkWithExistingFromDb(newAssets); - try { - await _assetRepository.transaction(() async { - await _assetRepository.updateAll(updated); - await _albumRepository.addAssets(dbAlbum, existingInDb + updated); - await _albumRepository.recalculateMetadata(dbAlbum); - await _albumRepository.update(dbAlbum); - await _eTagRepository.upsertAll([ETag(id: deviceAlbum.eTagKeyAssetCount, assetCount: totalOnDevice)]); - }); - _log.info("Fast synced local album ${deviceAlbum.name} to DB"); - } catch (e) { - _log.severe("Failed to fast sync local album ${deviceAlbum.name} to DB", e); - return false; - } - - return true; - } - - /// Adds a new album from the device to the database and Accumulates all - /// assets already existing in the database to the list of `existing` assets - Future _addAlbumFromDevice(Album album, List existing, [Set? excludedAssets]) async { - _log.info("Adding a new local album to DB: ${album.name}"); - final assets = await _getHashedAssets(album, excludedAssets: excludedAssets); - _removeDuplicates(assets); - final (existingInDb, updated) = await _linkWithExistingFromDb(assets); - _log.info("${existingInDb.length} assets already existed in DB, to upsert ${updated.length}"); - await upsertAssetsWithExif(updated); - existing.addAll(existingInDb); - album.assets.addAll(existingInDb); - album.assets.addAll(updated); - final thumb = existingInDb.firstOrNull ?? updated.firstOrNull; - album.thumbnail.value = thumb; - try { - await _albumRepository.create(album); - final int assetCount = await _albumMediaRepository.getAssetCount(album.localId!); - await _eTagRepository.upsertAll([ETag(id: album.eTagKeyAssetCount, assetCount: assetCount)]); - _log.info("Added a new local album to DB: ${album.name}"); - } catch (e) { - _log.severe("Failed to add new local album ${album.name} to DB", e); - } - } - - /// Returns a tuple (existing, updated) - Future<(List existing, List updated)> _linkWithExistingFromDb(List assets) async { - if (assets.isEmpty) return ([].cast(), [].cast()); - - final List inDb = await _assetRepository.getAllByOwnerIdChecksum( - assets.map((a) => a.ownerId).toInt64List(), - assets.map((a) => a.checksum).toList(growable: false), - ); - assert(inDb.length == assets.length); - final List existing = [], toUpsert = []; - for (int i = 0; i < assets.length; i++) { - final Asset? b = inDb[i]; - if (b == null) { - toUpsert.add(assets[i]); - continue; - } - if (b.canUpdate(assets[i])) { - final updated = b.updatedCopy(assets[i]); - assert(updated.isInDb); - toUpsert.add(updated); - } else { - existing.add(b); - } - } - assert(existing.length + toUpsert.length == assets.length); - return (existing, toUpsert); - } - - Future _toggleTrashStatusForAssets(List assetsList) async { - final trashMediaUrls = []; - - for (final asset in assetsList) { - if (asset.isTrashed) { - final mediaUrl = await asset.local?.getMediaUrl(); - if (mediaUrl == null) { - _log.warning("Failed to get media URL for asset ${asset.name} while moving to trash"); - continue; - } - trashMediaUrls.add(mediaUrl); - } else { - await _localFilesManager.restoreFromTrash(asset.fileName, asset.type.index); - } - } - - if (trashMediaUrls.isNotEmpty) { - await _localFilesManager.moveToTrash(trashMediaUrls); - } - } - - /// Inserts or updates the assets in the database with their ExifInfo (if any) - Future upsertAssetsWithExif(List assets) async { - if (assets.isEmpty) return; - - if (Platform.isAndroid && _appSettingsService.getSetting(AppSettingsEnum.manageLocalMediaAndroid)) { - await _toggleTrashStatusForAssets(assets); - } - - try { - await _assetRepository.transaction(() async { - await _assetRepository.updateAll(assets); - for (final Asset added in assets) { - added.exifInfo = added.exifInfo?.copyWith(assetId: added.id); - } - final exifInfos = assets.map((e) => e.exifInfo).nonNulls.toList(); - await _exifInfoRepository.updateAll(exifInfos); - }); - _log.info("Upserted ${assets.length} assets into the DB"); - } catch (e) { - _log.severe("Failed to upsert ${assets.length} assets into the DB", e); - // give details on the errors - assets.sort(Asset.compareByOwnerChecksum); - final inDb = await _assetRepository.getAllByOwnerIdChecksum( - assets.map((e) => e.ownerId).toInt64List(), - assets.map((e) => e.checksum).toList(growable: false), - ); - for (int i = 0; i < assets.length; i++) { - final Asset a = assets[i]; - final Asset? b = inDb[i]; - if (b == null) { - if (!a.isInDb) { - _log.warning("Trying to update an asset that does not exist in DB:\n$a"); - } - } else if (a.id != b.id) { - _log.warning("Trying to insert another asset with the same checksum+owner. In DB:\n$b\nTo insert:\n$a"); - } - } - for (int i = 1; i < assets.length; i++) { - if (Asset.compareByOwnerChecksum(assets[i - 1], assets[i]) == 0) { - _log.warning("Trying to insert duplicate assets:\n${assets[i - 1]}\n${assets[i]}"); - } - } - } - } - - /// Returns all assets that were successfully hashed - Future> _getHashedAssets( - Album album, { - int start = 0, - int end = 0x7fffffffffffffff, - DateTime? modifiedFrom, - DateTime? modifiedUntil, - Set? excludedAssets, - }) async { - final entities = await _albumMediaRepository.getAssets( - album.localId!, - start: start, - end: end, - modifiedFrom: modifiedFrom, - modifiedUntil: modifiedUntil, - ); - final filtered = excludedAssets == null - ? entities - : entities.where((e) => !excludedAssets.contains(e.localId!)).toList(); - return _hashService.hashAssets(filtered); - } - - List _removeDuplicates(List assets) { - final int before = assets.length; - assets.sort(Asset.compareByOwnerChecksumCreatedModified); - assets.uniqueConsecutive(compare: Asset.compareByOwnerChecksum, onDuplicate: (a, b) => {}); - final int duplicates = before - assets.length; - if (duplicates > 0) { - _log.warning("Ignored $duplicates duplicate assets on device"); - } - return assets; - } - - /// returns `true` if the albums differ on the surface - Future _hasAlbumChangeOnDevice(Album deviceAlbum, Album dbAlbum) async { - return deviceAlbum.name != dbAlbum.name || - deviceAlbum.description != dbAlbum.description || - !deviceAlbum.modifiedAt.isAtSameMomentAs(dbAlbum.modifiedAt) || - await _albumMediaRepository.getAssetCount(deviceAlbum.localId!) != - (await _eTagRepository.getById(deviceAlbum.eTagKeyAssetCount))?.assetCount; - } - - Future _removeAllLocalAlbumsAndAssets() async { - try { - final assets = await _assetRepository.getAllLocal(); - final (toDelete, toUpdate) = _handleAssetRemoval(assets, [], remote: false); - await _assetRepository.transaction(() async { - await _assetRepository.deleteByIds(toDelete); - await _assetRepository.updateAll(toUpdate); - await _albumRepository.deleteAllLocal(); - }); - return true; - } catch (e) { - _log.severe("Failed to remove all local albums and assets", e); - return false; - } - } - - Future?> getUsersFromServer() async { - List? users; - try { - users = await _userApiRepository.getAll(); - } catch (e) { - _log.warning("Failed to fetch users", e); - users = null; - } - final List sharedBy = await _partnerApiRepository.getAll(Direction.sharedByMe); - final List sharedWith = await _partnerApiRepository.getAll(Direction.sharedWithMe); - - if (users == null) { - _log.warning("Failed to refresh users"); - return null; - } - - users.sortBy((u) => u.id); - sharedBy.sortBy((u) => u.id); - sharedWith.sortBy((u) => u.id); - - final updatedSharedBy = []; - - diffSortedListsSync( - users, - sharedBy, - compare: (UserDto a, UserDto b) => a.id.compareTo(b.id), - both: (UserDto a, UserDto b) { - updatedSharedBy.add(a.copyWith(isPartnerSharedBy: true)); - return true; - }, - onlyFirst: (UserDto a) => updatedSharedBy.add(a), - onlySecond: (UserDto b) => updatedSharedBy.add(b), - ); - - final updatedSharedWith = []; - - diffSortedListsSync( - updatedSharedBy, - sharedWith, - compare: (UserDto a, UserDto b) => a.id.compareTo(b.id), - both: (UserDto a, UserDto b) { - updatedSharedWith.add(a.copyWith(inTimeline: b.inTimeline, isPartnerSharedWith: true)); - return true; - }, - onlyFirst: (UserDto a) => updatedSharedWith.add(a), - onlySecond: (UserDto b) => updatedSharedWith.add(b), - ); - - return updatedSharedWith; - } -} - -/// Returns a triple(toAdd, toUpdate, toRemove) -(List toAdd, List toUpdate, List toRemove) _diffAssets( - List assets, - List inDb, { - bool? remote, - int Function(Asset, Asset) compare = Asset.compareByChecksum, -}) { - // fast paths for trivial cases: reduces memory usage during initial sync etc. - if (assets.isEmpty && inDb.isEmpty) { - return const ([], [], []); - } else if (assets.isEmpty && remote == null) { - // remove all from database - return (const [], const [], inDb); - } else if (inDb.isEmpty) { - // add all assets - return (assets, const [], const []); - } - - final List toAdd = []; - final List toUpdate = []; - final List toRemove = []; - diffSortedListsSync( - inDb, - assets, - compare: compare, - both: (Asset a, Asset b) { - if (a.canUpdate(b)) { - toUpdate.add(a.updatedCopy(b)); - return true; - } - return false; - }, - onlyFirst: (Asset a) { - if (remote == true && a.isLocal) { - if (a.remoteId != null) { - a.remoteId = null; - toUpdate.add(a); - } - } else if (remote == false && a.isRemote) { - if (a.isLocal) { - a.localId = null; - toUpdate.add(a); - } - } else { - toRemove.add(a); - } - }, - onlySecond: (Asset b) => toAdd.add(b), - ); - return (toAdd, toUpdate, toRemove); -} - -/// returns a tuple (toDelete toUpdate) when assets are to be deleted -(List toDelete, List toUpdate) _handleAssetRemoval( - List deleteCandidates, - List existing, { - bool? remote, -}) { - if (deleteCandidates.isEmpty) { - return const ([], []); - } - deleteCandidates.sort(Asset.compareById); - deleteCandidates.uniqueConsecutive(compare: Asset.compareById); - existing.sort(Asset.compareById); - existing.uniqueConsecutive(compare: Asset.compareById); - final (tooAdd, toUpdate, toRemove) = _diffAssets( - existing, - deleteCandidates, - compare: Asset.compareById, - remote: remote, - ); - assert(tooAdd.isEmpty, "toAdd should be empty in _handleAssetRemoval"); - return (toRemove.map((e) => e.id).toList(), toUpdate); -} - -/// returns `true` if the albums differ on the surface -bool _hasRemoteAlbumChanged(Album remoteAlbum, Album dbAlbum) { - return remoteAlbum.remoteAssetCount != dbAlbum.assetCount || - remoteAlbum.name != dbAlbum.name || - remoteAlbum.description != dbAlbum.description || - remoteAlbum.remoteThumbnailAssetId != dbAlbum.thumbnail.value?.remoteId || - remoteAlbum.shared != dbAlbum.shared || - remoteAlbum.remoteUsers.length != dbAlbum.sharedUsers.length || - !remoteAlbum.modifiedAt.isAtSameMomentAs(dbAlbum.modifiedAt) || - !isAtSameMomentAs(remoteAlbum.startDate, dbAlbum.startDate) || - !isAtSameMomentAs(remoteAlbum.endDate, dbAlbum.endDate) || - !isAtSameMomentAs(remoteAlbum.lastModifiedAssetTimestamp, dbAlbum.lastModifiedAssetTimestamp); -} diff --git a/mobile/lib/services/timeline.service.dart b/mobile/lib/services/timeline.service.dart deleted file mode 100644 index eaff1027d8..0000000000 --- a/mobile/lib/services/timeline.service.dart +++ /dev/null @@ -1,98 +0,0 @@ -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/domain/services/user.service.dart'; -import 'package:immich_mobile/entities/album.entity.dart'; -import 'package:immich_mobile/entities/asset.entity.dart'; -import 'package:immich_mobile/providers/app_settings.provider.dart'; -import 'package:immich_mobile/providers/infrastructure/user.provider.dart'; -import 'package:immich_mobile/repositories/timeline.repository.dart'; -import 'package:immich_mobile/services/app_settings.service.dart'; -import 'package:immich_mobile/widgets/asset_grid/asset_grid_data_structure.dart'; - -final timelineServiceProvider = Provider((ref) { - return TimelineService( - ref.watch(timelineRepositoryProvider), - ref.watch(appSettingsServiceProvider), - ref.watch(userServiceProvider), - ); -}); - -class TimelineService { - final TimelineRepository _timelineRepository; - final AppSettingsService _appSettingsService; - final UserService _userService; - - const TimelineService(this._timelineRepository, this._appSettingsService, this._userService); - - Future> getTimelineUserIds() async { - final me = _userService.getMyUser(); - return _timelineRepository.getTimelineUserIds(me.id); - } - - Stream> watchTimelineUserIds() async* { - final me = _userService.getMyUser(); - yield* _timelineRepository.watchTimelineUsers(me.id); - } - - Stream watchHomeTimeline(String userId) { - return _timelineRepository.watchHomeTimeline(userId, _getGroupByOption()); - } - - Stream watchMultiUsersTimeline(List userIds) { - return _timelineRepository.watchMultiUsersTimeline(userIds, _getGroupByOption()); - } - - Stream watchArchiveTimeline() async* { - final user = _userService.getMyUser(); - - yield* _timelineRepository.watchArchiveTimeline(user.id); - } - - Stream watchFavoriteTimeline() async* { - final user = _userService.getMyUser(); - - yield* _timelineRepository.watchFavoriteTimeline(user.id); - } - - Stream watchAlbumTimeline(Album album) async* { - yield* _timelineRepository.watchAlbumTimeline(album, _getGroupByOption()); - } - - Stream watchTrashTimeline() async* { - final user = _userService.getMyUser(); - - yield* _timelineRepository.watchTrashTimeline(user.id); - } - - Stream watchAllVideosTimeline() { - final user = _userService.getMyUser(); - - return _timelineRepository.watchAllVideosTimeline(user.id); - } - - Future getTimelineFromAssets(List assets, GroupAssetsBy? groupBy) { - GroupAssetsBy groupOption = GroupAssetsBy.none; - if (groupBy == null) { - groupOption = _getGroupByOption(); - } else { - groupOption = groupBy; - } - - return _timelineRepository.getTimelineFromAssets(assets, groupOption); - } - - Stream watchAssetSelectionTimeline() async* { - final user = _userService.getMyUser(); - - yield* _timelineRepository.watchAssetSelectionTimeline(user.id); - } - - GroupAssetsBy _getGroupByOption() { - return GroupAssetsBy.values[_appSettingsService.getSetting(AppSettingsEnum.groupAssetsBy)]; - } - - Stream watchLockedTimelineProvider() async* { - final user = _userService.getMyUser(); - - yield* _timelineRepository.watchLockedTimeline(user.id, _getGroupByOption()); - } -} diff --git a/mobile/lib/services/trash.service.dart b/mobile/lib/services/trash.service.dart deleted file mode 100644 index 2c51a68c59..0000000000 --- a/mobile/lib/services/trash.service.dart +++ /dev/null @@ -1,75 +0,0 @@ -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/domain/services/user.service.dart'; -import 'package:immich_mobile/entities/asset.entity.dart'; -import 'package:immich_mobile/providers/api.provider.dart'; -import 'package:immich_mobile/providers/infrastructure/user.provider.dart'; -import 'package:immich_mobile/repositories/asset.repository.dart'; -import 'package:immich_mobile/services/api.service.dart'; -import 'package:openapi/api.dart'; - -final trashServiceProvider = Provider((ref) { - return TrashService( - ref.watch(apiServiceProvider), - ref.watch(assetRepositoryProvider), - ref.watch(userServiceProvider), - ); -}); - -class TrashService { - final ApiService _apiService; - final AssetRepository _assetRepository; - final UserService _userService; - - const TrashService(this._apiService, this._assetRepository, this._userService); - - Future restoreAssets(Iterable assetList) async { - final remoteAssets = assetList.where((a) => a.isRemote); - await _apiService.trashApi.restoreAssets(BulkIdsDto(ids: remoteAssets.map((e) => e.remoteId!).toList())); - - final updatedAssets = remoteAssets.map((asset) { - asset.isTrashed = false; - return asset; - }).toList(); - - await _assetRepository.updateAll(updatedAssets); - } - - Future emptyTrash() async { - final user = _userService.getMyUser(); - - await _apiService.trashApi.emptyTrash(); - - final trashedAssets = await _assetRepository.getTrashAssets(user.id); - final ids = trashedAssets.map((e) => e.remoteId!).toList(); - - await _assetRepository.transaction(() async { - await _assetRepository.deleteAllByRemoteId(ids, state: AssetState.remote); - - final merged = await _assetRepository.getAllByRemoteId(ids, state: AssetState.merged); - if (merged.isEmpty) { - return; - } - - for (final Asset asset in merged) { - asset.remoteId = null; - asset.isTrashed = false; - } - - await _assetRepository.updateAll(merged); - }); - } - - Future restoreTrash() async { - final user = _userService.getMyUser(); - - await _apiService.trashApi.restoreTrash(); - - final trashedAssets = await _assetRepository.getTrashAssets(user.id); - final updatedAssets = trashedAssets.map((asset) { - asset.isTrashed = false; - return asset; - }).toList(); - - await _assetRepository.updateAll(updatedAssets); - } -} diff --git a/mobile/lib/utils/backup_progress.dart b/mobile/lib/utils/backup_progress.dart deleted file mode 100644 index 36050f5e20..0000000000 --- a/mobile/lib/utils/backup_progress.dart +++ /dev/null @@ -1,83 +0,0 @@ -import 'dart:async'; -import 'dart:developer'; - -import 'package:easy_localization/easy_localization.dart'; - -final NumberFormat numberFormat = NumberFormat("###0.##"); - -String formatAssetBackupProgress(int uploadedAssets, int assetsToUpload) { - final int percent = (uploadedAssets * 100) ~/ assetsToUpload; - return "$percent% ($uploadedAssets/$assetsToUpload)"; -} - -/// prints progress in useful (kilo/mega/giga)bytes -String humanReadableFileBytesProgress(int bytes, int bytesTotal) { - String unit = "KB"; - - if (bytesTotal >= 0x40000000) { - unit = "GB"; - bytes >>= 20; - bytesTotal >>= 20; - } else if (bytesTotal >= 0x100000) { - unit = "MB"; - bytes >>= 10; - bytesTotal >>= 10; - } else if (bytesTotal < 0x400) { - return "${(bytes).toStringAsFixed(2)} B / ${(bytesTotal).toStringAsFixed(2)} B"; - } - - return "${(bytes / 1024.0).toStringAsFixed(2)} $unit / ${(bytesTotal / 1024.0).toStringAsFixed(2)} $unit"; -} - -/// prints percentage and absolute progress in useful (kilo/mega/giga)bytes -String humanReadableBytesProgress(int bytes, int bytesTotal) { - String unit = "KB"; // Kilobyte - if (bytesTotal >= 0x40000000) { - unit = "GB"; // Gigabyte - bytes >>= 20; - bytesTotal >>= 20; - } else if (bytesTotal >= 0x100000) { - unit = "MB"; // Megabyte - bytes >>= 10; - bytesTotal >>= 10; - } else if (bytesTotal < 0x400) { - return "$bytes / $bytesTotal B"; - } - final int percent = (bytes * 100) ~/ bytesTotal; - final String done = numberFormat.format(bytes / 1024.0); - final String total = numberFormat.format(bytesTotal / 1024.0); - return "$percent% ($done/$total$unit)"; -} - -class ThrottleProgressUpdate { - ThrottleProgressUpdate(this._fun, Duration interval) : _interval = interval.inMicroseconds; - final void Function(String?, int, int) _fun; - final int _interval; - int _invokedAt = 0; - Timer? _timer; - - String? title; - int progress = 0; - int total = 0; - - void call({final String? title, final int progress = 0, final int total = 0}) { - final time = Timeline.now; - this.title = title ?? this.title; - this.progress = progress; - this.total = total; - if (time > _invokedAt + _interval) { - _timer?.cancel(); - _onTimeElapsed(); - } else { - _timer ??= Timer(Duration(microseconds: _interval), _onTimeElapsed); - } - } - - void _onTimeElapsed() { - _invokedAt = Timeline.now; - _fun(title, progress, total); - _timer = null; - // clear title to not send/overwrite it next time if unchanged - title = null; - } -} diff --git a/mobile/lib/utils/bootstrap.dart b/mobile/lib/utils/bootstrap.dart index d63a92ba37..e79b06f53b 100644 --- a/mobile/lib/utils/bootstrap.dart +++ b/mobile/lib/utils/bootstrap.dart @@ -1,30 +1,14 @@ -import 'dart:io'; - import 'package:background_downloader/background_downloader.dart'; -import 'package:flutter/foundation.dart'; import 'package:immich_mobile/constants/constants.dart'; -import 'package:immich_mobile/domain/models/store.model.dart'; import 'package:immich_mobile/domain/services/log.service.dart'; import 'package:immich_mobile/domain/services/store.service.dart'; -import 'package:immich_mobile/entities/album.entity.dart'; -import 'package:immich_mobile/entities/android_device_asset.entity.dart'; -import 'package:immich_mobile/entities/asset.entity.dart'; -import 'package:immich_mobile/entities/backup_album.entity.dart'; -import 'package:immich_mobile/entities/duplicated_asset.entity.dart'; -import 'package:immich_mobile/entities/etag.entity.dart'; -import 'package:immich_mobile/entities/ios_device_asset.entity.dart'; import 'package:immich_mobile/extensions/translate_extensions.dart'; -import 'package:immich_mobile/infrastructure/entities/device_asset.entity.dart'; -import 'package:immich_mobile/infrastructure/entities/exif.entity.dart'; -import 'package:immich_mobile/infrastructure/entities/store.entity.dart'; -import 'package:immich_mobile/infrastructure/entities/user.entity.dart'; import 'package:immich_mobile/infrastructure/repositories/db.repository.dart'; import 'package:immich_mobile/infrastructure/repositories/log.repository.dart'; import 'package:immich_mobile/infrastructure/repositories/logger_db.repository.dart'; import 'package:immich_mobile/infrastructure/repositories/network.repository.dart'; import 'package:immich_mobile/infrastructure/repositories/store.repository.dart'; -import 'package:isar/isar.dart'; -import 'package:path_provider/path_provider.dart'; +import 'package:photo_manager/photo_manager.dart'; void configureFileDownloaderNotifications() { FileDownloader().configureNotificationForGroup( @@ -57,48 +41,10 @@ void configureFileDownloaderNotifications() { } abstract final class Bootstrap { - static Future<(Isar isar, Drift drift, DriftLogger logDb)> initDB() async { + static Future<(Drift, DriftLogger)> initDomain({bool listenStoreUpdates = true, bool shouldBufferLogs = true}) async { final drift = Drift(); final logDb = DriftLogger(); - - Isar? isar = Isar.getInstance(); - - if (isar != null) { - return (isar, drift, logDb); - } - - final dir = await getApplicationDocumentsDirectory(); - isar = await Isar.open( - [ - StoreValueSchema, - AssetSchema, - AlbumSchema, - ExifInfoSchema, - UserSchema, - BackupAlbumSchema, - DuplicatedAssetSchema, - ETagSchema, - if (Platform.isAndroid) AndroidDeviceAssetSchema, - if (Platform.isIOS) IOSDeviceAssetSchema, - DeviceAssetEntitySchema, - ], - directory: dir.path, - maxSizeMiB: 2048, - inspector: kDebugMode, - ); - - return (isar, drift, logDb); - } - - static Future initDomain( - Isar db, - Drift drift, - DriftLogger logDb, { - bool listenStoreUpdates = true, - bool shouldBufferLogs = true, - }) async { - final isBeta = await IsarStoreRepository(db).tryGet(StoreKey.betaTimeline) ?? true; - final IStoreRepository storeRepo = isBeta ? DriftStoreRepository(drift) : IsarStoreRepository(db); + final DriftStoreRepository storeRepo = DriftStoreRepository(drift); await StoreService.init(storeRepository: storeRepo, listenUpdates: listenStoreUpdates); @@ -109,5 +55,8 @@ abstract final class Bootstrap { ); await NetworkRepository.init(); + // Remove once all asset operations are migrated to Native APIs + await PhotoManager.setIgnorePermissionCheck(true); + return (drift, logDb); } } diff --git a/mobile/lib/utils/color_filter_generator.dart b/mobile/lib/utils/color_filter_generator.dart deleted file mode 100644 index 92aed4b1a0..0000000000 --- a/mobile/lib/utils/color_filter_generator.dart +++ /dev/null @@ -1,99 +0,0 @@ -import 'package:flutter/widgets.dart'; - -class InvertionFilter extends StatelessWidget { - final Widget? child; - const InvertionFilter({super.key, this.child}); - - @override - Widget build(BuildContext context) { - return ColorFiltered( - colorFilter: const ColorFilter.matrix([ - -1, 0, 0, 0, 255, // - 0, -1, 0, 0, 255, // - 0, 0, -1, 0, 255, // - 0, 0, 0, 1, 0, // - ]), - child: child, - ); - } -} - -// -1 - darkest, 1 - brightest, 0 - unchanged -class BrightnessFilter extends StatelessWidget { - final Widget? child; - final double brightness; - const BrightnessFilter({super.key, this.child, this.brightness = 0}); - - @override - Widget build(BuildContext context) { - return ColorFiltered( - colorFilter: ColorFilter.matrix(_ColorFilterGenerator.brightnessAdjustMatrix(brightness)), - child: child, - ); - } -} - -// -1 - greyscale, 1 - most saturated, 0 - unchanged -class SaturationFilter extends StatelessWidget { - final Widget? child; - final double saturation; - const SaturationFilter({super.key, this.child, this.saturation = 0}); - - @override - Widget build(BuildContext context) { - return ColorFiltered( - colorFilter: ColorFilter.matrix(_ColorFilterGenerator.saturationAdjustMatrix(saturation)), - child: child, - ); - } -} - -class _ColorFilterGenerator { - static List brightnessAdjustMatrix(double value) { - value = value * 10; - - if (value == 0) { - return [ - 1, 0, 0, 0, 0, // - 0, 1, 0, 0, 0, // - 0, 0, 1, 0, 0, // - 0, 0, 0, 1, 0, // - ]; - } - - return List.from([ - 1, 0, 0, 0, value, 0, 1, 0, 0, value, 0, 0, 1, 0, value, 0, 0, 0, 1, 0, // - ]).map((i) => i.toDouble()).toList(); - } - - static List saturationAdjustMatrix(double value) { - value = value * 100; - - if (value == 0) { - return [ - 1, 0, 0, 0, 0, // - 0, 1, 0, 0, 0, // - 0, 0, 1, 0, 0, // - 0, 0, 0, 1, 0, // - ]; - } - - double x = ((1 + ((value > 0) ? ((3 * value) / 100) : (value / 100)))).toDouble(); - double lumR = 0.3086; - double lumG = 0.6094; - double lumB = 0.082; - - return List.from([ - (lumR * (1 - x)) + x, lumG * (1 - x), lumB * (1 - x), // - 0, 0, // - lumR * (1 - x), // - (lumG * (1 - x)) + x, // - lumB * (1 - x), // - 0, 0, // - lumR * (1 - x), // - lumG * (1 - x), // - (lumB * (1 - x)) + x, // - 0, 0, 0, 0, 0, 1, 0, // - ]).map((i) => i.toDouble()).toList(); - } -} diff --git a/mobile/lib/utils/datetime_comparison.dart b/mobile/lib/utils/datetime_comparison.dart deleted file mode 100644 index f8ddcfea11..0000000000 --- a/mobile/lib/utils/datetime_comparison.dart +++ /dev/null @@ -1,2 +0,0 @@ -bool isAtSameMomentAs(DateTime? a, DateTime? b) => - (a == null && b == null) || ((a != null && b != null) && a.isAtSameMomentAs(b)); diff --git a/mobile/lib/utils/hooks/blurhash_hook.dart b/mobile/lib/utils/hooks/blurhash_hook.dart index ac5fd31724..534c0ad8fb 100644 --- a/mobile/lib/utils/hooks/blurhash_hook.dart +++ b/mobile/lib/utils/hooks/blurhash_hook.dart @@ -1,20 +1,10 @@ import 'dart:convert'; import 'dart:typed_data'; + import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:immich_mobile/domain/models/asset/base_asset.model.dart'; -import 'package:immich_mobile/entities/asset.entity.dart'; import 'package:thumbhash/thumbhash.dart' as thumbhash; -ObjectRef useBlurHashRef(Asset? asset) { - if (asset?.thumbhash == null) { - return useRef(null); - } - - final rbga = thumbhash.thumbHashToRGBA(base64Decode(asset!.thumbhash!)); - - return useRef(thumbhash.rgbaToBmp(rbga)); -} - ObjectRef useDriftBlurHashRef(RemoteAsset? asset) { if (asset?.thumbHash == null) { return useRef(null); diff --git a/mobile/lib/utils/image_url_builder.dart b/mobile/lib/utils/image_url_builder.dart index 079f0e51fa..c562049b1d 100644 --- a/mobile/lib/utils/image_url_builder.dart +++ b/mobile/lib/utils/image_url_builder.dart @@ -1,47 +1,7 @@ import 'package:immich_mobile/domain/models/store.model.dart'; -import 'package:immich_mobile/entities/album.entity.dart'; -import 'package:immich_mobile/entities/asset.entity.dart'; import 'package:immich_mobile/entities/store.entity.dart'; import 'package:openapi/api.dart'; -String getThumbnailUrl(final Asset asset, {AssetMediaSize type = AssetMediaSize.thumbnail}) { - return getThumbnailUrlForRemoteId(asset.remoteId!, type: type); -} - -String getThumbnailCacheKey(final Asset asset, {AssetMediaSize type = AssetMediaSize.thumbnail}) { - return getThumbnailCacheKeyForRemoteId(asset.remoteId!, asset.thumbhash!, type: type); -} - -String getThumbnailCacheKeyForRemoteId( - final String id, - final String thumbhash, { - AssetMediaSize type = AssetMediaSize.thumbnail, -}) { - if (type == AssetMediaSize.thumbnail) { - return 'thumbnail-image-$id-$thumbhash'; - } else { - return '${id}_${thumbhash}_previewStage'; - } -} - -String getAlbumThumbnailUrl(final Album album, {AssetMediaSize type = AssetMediaSize.thumbnail}) { - if (album.thumbnail.value?.remoteId == null) { - return ''; - } - return getThumbnailUrlForRemoteId(album.thumbnail.value!.remoteId!, type: type); -} - -String getAlbumThumbNailCacheKey(final Album album, {AssetMediaSize type = AssetMediaSize.thumbnail}) { - if (album.thumbnail.value?.remoteId == null) { - return ''; - } - return getThumbnailCacheKeyForRemoteId( - album.thumbnail.value!.remoteId!, - album.thumbnail.value!.thumbhash!, - type: type, - ); -} - String getOriginalUrlForRemoteId(final String id, {bool edited = true}) { return '${Store.get(StoreKey.serverEndpoint)}/assets/$id/original?edited=$edited'; } diff --git a/mobile/lib/utils/immich_loading_overlay.dart b/mobile/lib/utils/immich_loading_overlay.dart deleted file mode 100644 index be49c3bae9..0000000000 --- a/mobile/lib/utils/immich_loading_overlay.dart +++ /dev/null @@ -1,64 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:flutter_hooks/flutter_hooks.dart'; -import 'package:immich_mobile/extensions/build_context_extensions.dart'; -import 'package:immich_mobile/widgets/common/delayed_loading_indicator.dart'; - -final _loadingEntry = OverlayEntry( - builder: (context) => SizedBox.square( - dimension: double.infinity, - child: DecoratedBox( - decoration: BoxDecoration(color: context.colorScheme.surface.withAlpha(200)), - child: const Center( - child: DelayedLoadingIndicator(delay: Duration(seconds: 1), fadeInDuration: Duration(milliseconds: 400)), - ), - ), - ), -); - -ValueNotifier useProcessingOverlay() { - return use(const _LoadingOverlay()); -} - -class _LoadingOverlay extends Hook> { - const _LoadingOverlay(); - - @override - _LoadingOverlayState createState() => _LoadingOverlayState(); -} - -class _LoadingOverlayState extends HookState, _LoadingOverlay> { - late final _isLoading = ValueNotifier(false)..addListener(_listener); - OverlayEntry? _loadingOverlay; - - void _listener() { - setState(() { - WidgetsBinding.instance.addPostFrameCallback((_) { - if (_isLoading.value) { - _loadingOverlay?.remove(); - _loadingOverlay = _loadingEntry; - Overlay.of(context).insert(_loadingEntry); - } else { - _loadingOverlay?.remove(); - _loadingOverlay = null; - } - }); - }); - } - - @override - ValueNotifier build(BuildContext context) { - return _isLoading; - } - - @override - void dispose() { - _isLoading.dispose(); - super.dispose(); - } - - @override - Object? get debugValue => _isLoading.value; - - @override - String get debugLabel => 'useProcessingOverlay<>'; -} diff --git a/mobile/lib/utils/isolate.dart b/mobile/lib/utils/isolate.dart index c8224b9c55..20b56d4875 100644 --- a/mobile/lib/utils/isolate.dart +++ b/mobile/lib/utils/isolate.dart @@ -5,7 +5,6 @@ import 'package:flutter/services.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/domain/services/log.service.dart'; import 'package:immich_mobile/entities/store.entity.dart'; -import 'package:immich_mobile/providers/db.provider.dart'; import 'package:immich_mobile/providers/infrastructure/cancel.provider.dart'; import 'package:immich_mobile/providers/infrastructure/db.provider.dart'; import 'package:immich_mobile/utils/bootstrap.dart'; @@ -38,13 +37,9 @@ Cancelable runInIsolateGentle({ BackgroundIsolateBinaryMessenger.ensureInitialized(token); DartPluginRegistrant.ensureInitialized(); - final (isar, drift, logDb) = await Bootstrap.initDB(); - await Bootstrap.initDomain(isar, drift, logDb, shouldBufferLogs: false, listenStoreUpdates: false); + final (drift, logDb) = await Bootstrap.initDomain(shouldBufferLogs: false, listenStoreUpdates: false); final ref = ProviderContainer( overrides: [ - // TODO: Remove once isar is removed - dbProvider.overrideWithValue(isar), - isarProvider.overrideWithValue(isar), cancellationProvider.overrideWithValue(cancelledChecker), driftProvider.overrideWith(driftOverride(drift)), ], @@ -66,15 +61,6 @@ Cancelable runInIsolateGentle({ await LogService.I.dispose(); await logDb.close(); await drift.close(); - - // Close Isar safely - try { - if (isar.isOpen) { - await isar.close(); - } - } catch (e) { - dPrint(() => "Error closing Isar: $e"); - } } catch (error, stack) { dPrint(() => "Error closing resources in isolate: $error, $stack"); } finally { diff --git a/mobile/lib/utils/migration.dart b/mobile/lib/utils/migration.dart index 76916cee1e..9ac805af39 100644 --- a/mobile/lib/utils/migration.dart +++ b/mobile/lib/utils/migration.dart @@ -1,115 +1,14 @@ import 'dart:async'; -import 'dart:convert'; -import 'dart:io'; -import 'package:collection/collection.dart'; -import 'package:drift/drift.dart'; -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/store.model.dart'; -import 'package:immich_mobile/entities/album.entity.dart'; -import 'package:immich_mobile/entities/android_device_asset.entity.dart'; -import 'package:immich_mobile/entities/asset.entity.dart'; -import 'package:immich_mobile/entities/backup_album.entity.dart' as isar_backup_album; -import 'package:immich_mobile/entities/etag.entity.dart'; -import 'package:immich_mobile/entities/ios_device_asset.entity.dart'; import 'package:immich_mobile/entities/store.entity.dart'; -import 'package:immich_mobile/infrastructure/entities/device_asset.entity.dart'; -import 'package:immich_mobile/infrastructure/entities/exif.entity.dart'; -import 'package:immich_mobile/infrastructure/entities/local_album.entity.drift.dart'; -import 'package:immich_mobile/infrastructure/entities/local_asset.entity.drift.dart'; -import 'package:immich_mobile/infrastructure/entities/trashed_local_asset.entity.drift.dart'; -import 'package:immich_mobile/infrastructure/entities/store.entity.dart'; -import 'package:immich_mobile/infrastructure/entities/store.entity.drift.dart'; -import 'package:immich_mobile/infrastructure/entities/user.entity.dart'; -import 'package:immich_mobile/infrastructure/repositories/db.repository.dart'; -import 'package:immich_mobile/infrastructure/repositories/sync_stream.repository.dart'; -import 'package:immich_mobile/platform/native_sync_api.g.dart'; import 'package:immich_mobile/infrastructure/repositories/network.repository.dart'; -import 'package:immich_mobile/platform/network_api.g.dart'; -import 'package:immich_mobile/providers/infrastructure/platform.provider.dart'; import 'package:immich_mobile/services/api.service.dart'; -import 'package:immich_mobile/services/app_settings.service.dart'; -import 'package:immich_mobile/utils/datetime_helpers.dart'; -import 'package:immich_mobile/utils/debug_print.dart'; -import 'package:immich_mobile/utils/diff.dart'; -import 'package:isar/isar.dart'; -// ignore: import_rule_photo_manager -import 'package:photo_manager/photo_manager.dart'; const int targetVersion = 25; -Future migrateDatabaseIfNeeded(Isar db, Drift drift) async { - final hasVersion = Store.tryGet(StoreKey.version) != null; +Future migrateDatabaseIfNeeded() async { final int version = Store.get(StoreKey.version, targetVersion); - if (version < 9) { - await Store.put(StoreKey.version, targetVersion); - final value = await db.storeValues.get(StoreKey.currentUser.id); - if (value != null) { - final id = value.intValue; - if (id != null) { - await db.writeTxn(() async { - final user = await db.users.get(id); - await db.storeValues.put(StoreValue(StoreKey.currentUser.id, strValue: user?.id)); - }); - } - } - } - - if (version < 10) { - await Store.put(StoreKey.version, targetVersion); - await _migrateDeviceAsset(db); - } - - if (version < 13) { - await Store.put(StoreKey.photoManagerCustomFilter, true); - } - - // This means that the SQLite DB is just created and has no version - if (version < 14 || !hasVersion) { - await migrateStoreToSqlite(db, drift); - await Store.populateCache(); - } - - final syncStreamRepository = SyncStreamRepository(drift); - await handleBetaMigration(version, await _isNewInstallation(db, drift), syncStreamRepository); - - if (version < 17 && Store.isBetaTimelineEnabled) { - final delay = Store.get(StoreKey.backupTriggerDelay, AppSettingsEnum.backupTriggerDelay.defaultValue); - if (delay >= 1000) { - await Store.put(StoreKey.backupTriggerDelay, (delay / 1000).toInt()); - } - } - - if (version < 18 && Store.isBetaTimelineEnabled) { - await syncStreamRepository.reset(); - await Store.put(StoreKey.shouldResetSync, true); - } - - if (version < 19 && Store.isBetaTimelineEnabled) { - if (!await _populateLocalAssetTime(drift)) { - return; - } - } - - if (version < 20 && Store.isBetaTimelineEnabled) { - await _syncLocalAlbumIsIosSharedAlbum(drift); - } - - if (version < 21) { - final certData = SSLClientCertStoreVal.load(); - if (certData != null) { - await networkApi.addCertificate(ClientCertData(data: certData.data, password: certData.password ?? "")); - } - } - - if (version < 23 && Store.isBetaTimelineEnabled) { - await _populateLocalAssetPlaybackStyle(drift); - } - - if (version < 24 && Store.isBetaTimelineEnabled) { - await _applyLocalAssetOrientation(drift); - } if (version < 25) { final accessToken = Store.tryGet(StoreKey.accessToken); @@ -121,365 +20,6 @@ Future migrateDatabaseIfNeeded(Isar db, Drift drift) async { } } - if (version < 22 && !Store.isBetaTimelineEnabled) { - await Store.put(StoreKey.needBetaMigration, true); - } - - if (targetVersion >= 12) { - await Store.put(StoreKey.version, targetVersion); - return; - } - - final shouldTruncate = version < 8 || version < targetVersion; - - if (shouldTruncate) { - await _migrateTo(db, targetVersion); - } -} - -Future handleBetaMigration(int version, bool isNewInstallation, SyncStreamRepository syncStreamRepository) async { - // Handle migration only for this version - // TODO: remove when old timeline is removed - final isBeta = Store.tryGet(StoreKey.betaTimeline); - final needBetaMigration = Store.tryGet(StoreKey.needBetaMigration); - if (version <= 15 && needBetaMigration == null) { - // For new installations, no migration needed - // For existing installations, only migrate if beta timeline is not enabled (null or false) - if (isNewInstallation || isBeta == true) { - await Store.put(StoreKey.needBetaMigration, false); - await Store.put(StoreKey.betaTimeline, true); - } else { - await Store.put(StoreKey.needBetaMigration, true); - } - } - - if (version > 15) { - if (isBeta == null || isBeta) { - await Store.put(StoreKey.needBetaMigration, false); - await Store.put(StoreKey.betaTimeline, true); - } else { - await Store.put(StoreKey.needBetaMigration, false); - } - } - - if (version < 16) { - await syncStreamRepository.reset(); - await Store.put(StoreKey.shouldResetSync, true); - } -} - -Future _isNewInstallation(Isar db, Drift drift) async { - try { - final isarUserCount = await db.users.count(); - if (isarUserCount > 0) { - return false; - } - - final isarAssetCount = await db.assets.count(); - if (isarAssetCount > 0) { - return false; - } - - final driftStoreCount = await drift.storeEntity.select().get().then((list) => list.length); - if (driftStoreCount > 0) { - return false; - } - - final driftAssetCount = await drift.localAssetEntity.select().get().then((list) => list.length); - if (driftAssetCount > 0) { - return false; - } - - return true; - } catch (error) { - dPrint(() => "[MIGRATION] Error checking if new installation: $error"); - return false; - } -} - -Future _migrateTo(Isar db, int version) async { - await Store.delete(StoreKey.assetETag); - await db.writeTxn(() async { - await db.assets.clear(); - await db.exifInfos.clear(); - await db.albums.clear(); - await db.eTags.clear(); - await db.users.clear(); - }); - await Store.put(StoreKey.version, version); -} - -Future _migrateDeviceAsset(Isar db) async { - final ids = Platform.isAndroid - ? (await db.androidDeviceAssets.where().findAll()) - .map((a) => _DeviceAsset(assetId: a.id.toString(), hash: a.hash)) - .toList() - : (await db.iOSDeviceAssets.where().findAll()).map((i) => _DeviceAsset(assetId: i.id, hash: i.hash)).toList(); - - final PermissionState ps = await PhotoManager.requestPermissionExtend(); - if (!ps.hasAccess) { - dPrint(() => "[MIGRATION] Photo library permission not granted. Skipping device asset migration."); - return; - } - - List<_DeviceAsset> localAssets = []; - final List paths = await PhotoManager.getAssetPathList(onlyAll: true); - - if (paths.isEmpty) { - localAssets = (await db.assets.where().anyOf(ids, (query, id) => query.localIdEqualTo(id.assetId)).findAll()) - .map((a) => _DeviceAsset(assetId: a.localId!, dateTime: a.fileModifiedAt)) - .toList(); - } else { - final AssetPathEntity albumWithAll = paths.first; - final int assetCount = await albumWithAll.assetCountAsync; - - final List allDeviceAssets = await albumWithAll.getAssetListRange(start: 0, end: assetCount); - - localAssets = allDeviceAssets.map((a) => _DeviceAsset(assetId: a.id, dateTime: a.modifiedDateTime)).toList(); - } - - dPrint(() => "[MIGRATION] Device Asset Ids length - ${ids.length}"); - dPrint(() => "[MIGRATION] Local Asset Ids length - ${localAssets.length}"); - ids.sort((a, b) => a.assetId.compareTo(b.assetId)); - localAssets.sort((a, b) => a.assetId.compareTo(b.assetId)); - final List toAdd = []; - await diffSortedLists( - ids, - localAssets, - compare: (a, b) => a.assetId.compareTo(b.assetId), - both: (deviceAsset, asset) { - toAdd.add( - DeviceAssetEntity(assetId: deviceAsset.assetId, hash: deviceAsset.hash!, modifiedTime: asset.dateTime!), - ); - return false; - }, - onlyFirst: (deviceAsset) { - dPrint(() => '[MIGRATION] Local asset not found in DeviceAsset: ${deviceAsset.assetId}'); - }, - onlySecond: (asset) { - dPrint(() => '[MIGRATION] Local asset not found in DeviceAsset: ${asset.assetId}'); - }, - ); - - dPrint(() => "[MIGRATION] Total number of device assets migrated - ${toAdd.length}"); - - await db.writeTxn(() async { - await db.deviceAssetEntitys.putAll(toAdd); - }); -} - -Future _populateLocalAssetTime(Drift db) async { - try { - final nativeApi = NativeSyncApi(); - final albums = await nativeApi.getAlbums(); - for (final album in albums) { - final assets = await nativeApi.getAssetsForAlbum(album.id); - await db.batch((batch) async { - for (final asset in assets) { - batch.update( - db.localAssetEntity, - LocalAssetEntityCompanion( - longitude: Value(asset.longitude), - latitude: Value(asset.latitude), - adjustmentTime: Value(tryFromSecondsSinceEpoch(asset.adjustmentTime, isUtc: true)), - updatedAt: Value(tryFromSecondsSinceEpoch(asset.updatedAt, isUtc: true) ?? DateTime.timestamp()), - ), - where: (t) => t.id.equals(asset.id), - ); - } - }); - } - - return true; - } catch (error) { - dPrint(() => "[MIGRATION] Error while populating asset time: $error"); - return false; - } -} - -Future _syncLocalAlbumIsIosSharedAlbum(Drift db) async { - try { - final nativeApi = NativeSyncApi(); - final albums = await nativeApi.getAlbums(); - await db.batch((batch) { - for (final album in albums) { - batch.update( - db.localAlbumEntity, - LocalAlbumEntityCompanion(isIosSharedAlbum: Value(album.isCloud)), - where: (t) => t.id.equals(album.id), - ); - } - }); - dPrint(() => "[MIGRATION] Successfully updated isIosSharedAlbum for ${albums.length} albums"); - } catch (error) { - dPrint(() => "[MIGRATION] Error while syncing local album isIosSharedAlbum: $error"); - } -} - -Future migrateDeviceAssetToSqlite(Isar db, Drift drift) async { - try { - final isarDeviceAssets = await db.deviceAssetEntitys.where().findAll(); - await drift.batch((batch) { - for (final deviceAsset in isarDeviceAssets) { - batch.update( - drift.localAssetEntity, - LocalAssetEntityCompanion(checksum: Value(base64.encode(deviceAsset.hash))), - where: (t) => t.id.equals(deviceAsset.assetId), - ); - } - }); - } catch (error) { - dPrint(() => "[MIGRATION] Error while migrating device assets to SQLite: $error"); - } -} - -Future migrateBackupAlbumsToSqlite(Isar db, Drift drift) async { - try { - final isarBackupAlbums = await db.backupAlbums.where().findAll(); - // Recents is a virtual album on Android, and we don't have it with the new sync - // If recents is selected previously, select all albums during migration except the excluded ones - if (Platform.isAndroid) { - final recentAlbum = isarBackupAlbums.firstWhereOrNull((album) => album.id == 'isAll'); - if (recentAlbum != null) { - await drift.localAlbumEntity.update().write( - const LocalAlbumEntityCompanion(backupSelection: Value(BackupSelection.selected)), - ); - final excluded = isarBackupAlbums - .where((album) => album.selection == isar_backup_album.BackupSelection.exclude) - .map((album) => album.id) - .toList(); - await drift.batch((batch) async { - for (final id in excluded) { - batch.update( - drift.localAlbumEntity, - const LocalAlbumEntityCompanion(backupSelection: Value(BackupSelection.excluded)), - where: (t) => t.id.equals(id), - ); - } - }); - return; - } - } - - await drift.batch((batch) { - for (final album in isarBackupAlbums) { - batch.update( - drift.localAlbumEntity, - LocalAlbumEntityCompanion( - backupSelection: Value(switch (album.selection) { - isar_backup_album.BackupSelection.none => BackupSelection.none, - isar_backup_album.BackupSelection.select => BackupSelection.selected, - isar_backup_album.BackupSelection.exclude => BackupSelection.excluded, - }), - ), - where: (t) => t.id.equals(album.id), - ); - } - }); - } catch (error) { - dPrint(() => "[MIGRATION] Error while migrating backup albums to SQLite: $error"); - } -} - -Future migrateStoreToSqlite(Isar db, Drift drift) async { - try { - final isarStoreValues = await db.storeValues.where().findAll(); - await drift.batch((batch) { - for (final storeValue in isarStoreValues) { - final companion = StoreEntityCompanion( - id: Value(storeValue.id), - stringValue: Value(storeValue.strValue), - intValue: Value(storeValue.intValue), - ); - batch.insert(drift.storeEntity, companion, onConflict: DoUpdate((_) => companion)); - } - }); - } catch (error) { - dPrint(() => "[MIGRATION] Error while migrating store values to SQLite: $error"); - } -} - -Future migrateStoreToIsar(Isar db, Drift drift) async { - try { - final driftStoreValues = await drift.storeEntity - .select() - .map((entity) => StoreValue(entity.id, intValue: entity.intValue, strValue: entity.stringValue)) - .get(); - - await db.writeTxn(() async { - await db.storeValues.putAll(driftStoreValues); - }); - } catch (error) { - dPrint(() => "[MIGRATION] Error while migrating store values to Isar: $error"); - } -} - -Future _populateLocalAssetPlaybackStyle(Drift db) async { - try { - final nativeApi = NativeSyncApi(); - - final albums = await nativeApi.getAlbums(); - for (final album in albums) { - final assets = await nativeApi.getAssetsForAlbum(album.id); - await db.batch((batch) { - for (final asset in assets) { - batch.update( - db.localAssetEntity, - LocalAssetEntityCompanion(playbackStyle: Value(_toPlaybackStyle(asset.playbackStyle))), - where: (t) => t.id.equals(asset.id), - ); - } - }); - } - - if (Platform.isAndroid) { - final trashedAssetMap = await nativeApi.getTrashedAssets(); - for (final entry in trashedAssetMap.cast>().entries) { - final assets = entry.value.cast(); - await db.batch((batch) { - for (final asset in assets) { - batch.update( - db.trashedLocalAssetEntity, - TrashedLocalAssetEntityCompanion(playbackStyle: Value(_toPlaybackStyle(asset.playbackStyle))), - where: (t) => t.id.equals(asset.id), - ); - } - }); - } - dPrint(() => "[MIGRATION] Successfully populated playbackStyle for local and trashed assets"); - } else { - dPrint(() => "[MIGRATION] Successfully populated playbackStyle for local assets"); - } - } catch (error) { - dPrint(() => "[MIGRATION] Error while populating playbackStyle: $error"); - } -} - -Future _applyLocalAssetOrientation(Drift db) { - final query = db.localAssetEntity.update() - ..where((filter) => (filter.orientation.equals(90) | (filter.orientation.equals(270)))); - return query.write( - LocalAssetEntityCompanion.custom( - width: db.localAssetEntity.height, - height: db.localAssetEntity.width, - orientation: const Variable(0), - ), - ); -} - -AssetPlaybackStyle _toPlaybackStyle(PlatformAssetPlaybackStyle style) => switch (style) { - PlatformAssetPlaybackStyle.unknown => AssetPlaybackStyle.unknown, - PlatformAssetPlaybackStyle.image => AssetPlaybackStyle.image, - PlatformAssetPlaybackStyle.video => AssetPlaybackStyle.video, - PlatformAssetPlaybackStyle.imageAnimated => AssetPlaybackStyle.imageAnimated, - PlatformAssetPlaybackStyle.livePhoto => AssetPlaybackStyle.livePhoto, - PlatformAssetPlaybackStyle.videoLooping => AssetPlaybackStyle.videoLooping, -}; - -class _DeviceAsset { - final String assetId; - final List? hash; - final DateTime? dateTime; - - const _DeviceAsset({required this.assetId, this.hash, this.dateTime}); + await Store.put(StoreKey.version, targetVersion); + return; } diff --git a/mobile/lib/utils/provider_utils.dart b/mobile/lib/utils/provider_utils.dart index 6c2d6e0f11..9524433c05 100644 --- a/mobile/lib/utils/provider_utils.dart +++ b/mobile/lib/utils/provider_utils.dart @@ -2,21 +2,17 @@ import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/providers/infrastructure/search.provider.dart'; import 'package:immich_mobile/providers/infrastructure/user.provider.dart'; import 'package:immich_mobile/repositories/activity_api.repository.dart'; -import 'package:immich_mobile/repositories/album_api.repository.dart'; import 'package:immich_mobile/repositories/asset_api.repository.dart'; import 'package:immich_mobile/repositories/drift_album_api_repository.dart'; import 'package:immich_mobile/repositories/partner_api.repository.dart'; import 'package:immich_mobile/repositories/person_api.repository.dart'; -import 'package:immich_mobile/repositories/timeline.repository.dart'; void invalidateAllApiRepositoryProviders(WidgetRef ref) { ref.invalidate(userApiRepositoryProvider); ref.invalidate(activityApiRepositoryProvider); ref.invalidate(partnerApiRepositoryProvider); - ref.invalidate(albumApiRepositoryProvider); ref.invalidate(personApiRepositoryProvider); ref.invalidate(assetApiRepositoryProvider); - ref.invalidate(timelineRepositoryProvider); ref.invalidate(searchApiRepositoryProvider); // Drift diff --git a/mobile/lib/utils/selection_handlers.dart b/mobile/lib/utils/selection_handlers.dart deleted file mode 100644 index f0d333e262..0000000000 --- a/mobile/lib/utils/selection_handlers.dart +++ /dev/null @@ -1,143 +0,0 @@ -import 'package:easy_localization/easy_localization.dart'; -import 'package:flutter/material.dart'; -import 'package:fluttertoast/fluttertoast.dart'; -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/constants/enums.dart'; -import 'package:immich_mobile/entities/asset.entity.dart'; -import 'package:immich_mobile/extensions/asset_extensions.dart'; -import 'package:immich_mobile/extensions/build_context_extensions.dart'; -import 'package:immich_mobile/extensions/translate_extensions.dart'; -import 'package:immich_mobile/providers/asset.provider.dart'; -import 'package:immich_mobile/services/asset.service.dart'; -import 'package:immich_mobile/services/share.service.dart'; -import 'package:immich_mobile/widgets/common/date_time_picker.dart'; -import 'package:immich_mobile/widgets/common/immich_toast.dart'; -import 'package:immich_mobile/widgets/common/location_picker.dart'; -import 'package:immich_mobile/widgets/common/share_dialog.dart'; -import 'package:maplibre_gl/maplibre_gl.dart'; - -void handleShareAssets(WidgetRef ref, BuildContext context, Iterable selection) { - showDialog( - context: context, - builder: (BuildContext buildContext) { - ref.watch(shareServiceProvider).shareAssets(selection.toList(), context).then((bool status) { - if (!status) { - ImmichToast.show( - context: context, - msg: 'image_viewer_page_state_provider_share_error'.tr(), - toastType: ToastType.error, - gravity: ToastGravity.BOTTOM, - ); - } - buildContext.pop(); - }); - return const ShareDialog(); - }, - barrierDismissible: false, - useRootNavigator: false, - ); -} - -Future handleArchiveAssets( - WidgetRef ref, - BuildContext context, - List selection, { - bool? shouldArchive, - ToastGravity toastGravity = ToastGravity.BOTTOM, -}) async { - if (selection.isNotEmpty) { - shouldArchive ??= !selection.every((a) => a.isArchived); - await ref.read(assetProvider.notifier).toggleArchive(selection, shouldArchive); - final message = shouldArchive - ? 'moved_to_archive'.t(context: context, args: {'count': selection.length}) - : 'moved_to_library'.t(context: context, args: {'count': selection.length}); - if (context.mounted) { - ImmichToast.show(context: context, msg: message, gravity: toastGravity); - } - } -} - -Future handleFavoriteAssets( - WidgetRef ref, - BuildContext context, - List selection, { - bool? shouldFavorite, - ToastGravity toastGravity = ToastGravity.BOTTOM, -}) async { - if (selection.isNotEmpty) { - shouldFavorite ??= !selection.every((a) => a.isFavorite); - await ref.watch(assetProvider.notifier).toggleFavorite(selection, shouldFavorite); - - final assetOrAssets = selection.length > 1 ? 'assets' : 'asset'; - final toastMessage = shouldFavorite - ? 'Added ${selection.length} $assetOrAssets to favorites' - : 'Removed ${selection.length} $assetOrAssets from favorites'; - if (context.mounted) { - ImmichToast.show(context: context, msg: toastMessage, gravity: toastGravity); - } - } -} - -Future handleEditDateTime(WidgetRef ref, BuildContext context, List selection) async { - DateTime? initialDate; - String? timeZone; - Duration? offset; - if (selection.length == 1) { - final asset = selection.first; - final assetWithExif = await ref.watch(assetServiceProvider).loadExif(asset); - final (dt, oft) = assetWithExif.getTZAdjustedTimeAndOffset(); - initialDate = dt; - offset = oft; - timeZone = assetWithExif.exifInfo?.timeZone; - } - final dateTime = await showDateTimePicker( - context: context, - initialDateTime: initialDate, - initialTZ: timeZone, - initialTZOffset: offset, - ); - - if (dateTime == null) { - return; - } - - await ref.read(assetServiceProvider).changeDateTime(selection.toList(), dateTime); -} - -Future handleEditLocation(WidgetRef ref, BuildContext context, List selection) async { - LatLng? initialLatLng; - if (selection.length == 1) { - final asset = selection.first; - final assetWithExif = await ref.watch(assetServiceProvider).loadExif(asset); - if (assetWithExif.exifInfo?.latitude != null && assetWithExif.exifInfo?.longitude != null) { - initialLatLng = LatLng(assetWithExif.exifInfo!.latitude!, assetWithExif.exifInfo!.longitude!); - } - } - - final location = await showLocationPicker(context: context, initialLatLng: initialLatLng); - - if (location == null) { - return; - } - - await ref.read(assetServiceProvider).changeLocation(selection.toList(), location); -} - -Future handleSetAssetsVisibility( - WidgetRef ref, - BuildContext context, - AssetVisibilityEnum visibility, - List selection, -) async { - if (selection.isNotEmpty) { - await ref.watch(assetProvider.notifier).setLockedView(selection, visibility); - - final assetOrAssets = selection.length > 1 ? 'assets' : 'asset'; - final toastMessage = visibility == AssetVisibilityEnum.locked - ? 'Added ${selection.length} $assetOrAssets to locked folder' - : 'Removed ${selection.length} $assetOrAssets from locked folder'; - if (context.mounted) { - ImmichToast.show(context: context, msg: toastMessage, gravity: ToastGravity.BOTTOM); - } - } -} diff --git a/mobile/lib/utils/string_helper.dart b/mobile/lib/utils/string_helper.dart deleted file mode 100644 index 201d141531..0000000000 --- a/mobile/lib/utils/string_helper.dart +++ /dev/null @@ -1,7 +0,0 @@ -extension StringExtension on String { - String capitalizeFirstLetter() { - return "${this[0].toUpperCase()}${substring(1).toLowerCase()}"; - } -} - -String s(num count) => (count == 1 ? '' : 's'); diff --git a/mobile/lib/utils/throttle.dart b/mobile/lib/utils/throttle.dart deleted file mode 100644 index 8b41d92318..0000000000 --- a/mobile/lib/utils/throttle.dart +++ /dev/null @@ -1,51 +0,0 @@ -import 'package:flutter_hooks/flutter_hooks.dart'; - -/// Throttles function calls with the [interval] provided. -/// Also make sures to call the last Action after the elapsed interval -class Throttler { - final Duration interval; - DateTime? _lastActionTime; - - Throttler({required this.interval}); - - T? run(T Function() action) { - if (_lastActionTime == null || (DateTime.now().difference(_lastActionTime!) > interval)) { - final response = action(); - _lastActionTime = DateTime.now(); - return response; - } - - return null; - } - - void dispose() { - _lastActionTime = null; - } -} - -/// Creates a [Throttler] that will be disposed automatically. If no [interval] is provided, a -/// default interval of 300ms is used to throttle the function calls -Throttler useThrottler({Duration interval = const Duration(milliseconds: 300), List? keys}) => - use(_ThrottleHook(interval: interval, keys: keys)); - -class _ThrottleHook extends Hook { - const _ThrottleHook({required this.interval, super.keys}); - - final Duration interval; - - @override - HookState> createState() => _ThrottlerHookState(); -} - -class _ThrottlerHookState extends HookState { - late final throttler = Throttler(interval: hook.interval); - - @override - Throttler build(_) => throttler; - - @override - void dispose() => throttler.dispose(); - - @override - String get debugLabel => 'useThrottler'; -} diff --git a/mobile/lib/utils/thumbnail_utils.dart b/mobile/lib/utils/thumbnail_utils.dart deleted file mode 100644 index 685dc2b1c2..0000000000 --- a/mobile/lib/utils/thumbnail_utils.dart +++ /dev/null @@ -1,49 +0,0 @@ -import 'package:easy_localization/easy_localization.dart'; -import 'package:immich_mobile/domain/models/exif.model.dart'; -import 'package:immich_mobile/entities/asset.entity.dart'; -import 'package:immich_mobile/extensions/translate_extensions.dart'; - -String getAltText(ExifInfo? exifInfo, DateTime fileCreatedAt, AssetType type, List peopleNames) { - if (exifInfo?.description != null && exifInfo!.description!.isNotEmpty) { - return exifInfo.description!; - } - final (template, args) = getAltTextTemplate(exifInfo, fileCreatedAt, type, peopleNames); - return template.t(args: args); -} - -(String, Map) getAltTextTemplate( - ExifInfo? exifInfo, - DateTime fileCreatedAt, - AssetType type, - List peopleNames, -) { - final isVideo = type == AssetType.video; - final hasLocation = exifInfo?.city != null && exifInfo?.country != null; - final date = DateFormat.yMMMMd().format(fileCreatedAt); - final args = { - "isVideo": isVideo.toString(), - "date": date, - "city": exifInfo?.city ?? "", - "country": exifInfo?.country ?? "", - "person1": peopleNames.elementAtOrNull(0) ?? "", - "person2": peopleNames.elementAtOrNull(1) ?? "", - "person3": peopleNames.elementAtOrNull(2) ?? "", - "additionalCount": (peopleNames.length - 3).toString(), - }; - final template = hasLocation - ? (switch (peopleNames.length) { - 0 => "image_alt_text_date_place", - 1 => "image_alt_text_date_place_1_person", - 2 => "image_alt_text_date_place_2_people", - 3 => "image_alt_text_date_place_3_people", - _ => "image_alt_text_date_place_4_or_more_people", - }) - : (switch (peopleNames.length) { - 0 => "image_alt_text_date", - 1 => "image_alt_text_date_1_person", - 2 => "image_alt_text_date_2_people", - 3 => "image_alt_text_date_3_people", - _ => "image_alt_text_date_4_or_more_people", - }); - return (template, args); -} diff --git a/mobile/lib/widgets/activities/activity_text_field.dart b/mobile/lib/widgets/activities/activity_text_field.dart deleted file mode 100644 index d21cdfbc94..0000000000 --- a/mobile/lib/widgets/activities/activity_text_field.dart +++ /dev/null @@ -1,85 +0,0 @@ -import 'package:easy_localization/easy_localization.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_hooks/flutter_hooks.dart'; -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/extensions/build_context_extensions.dart'; -import 'package:immich_mobile/providers/activity.provider.dart'; -import 'package:immich_mobile/providers/album/current_album.provider.dart'; -import 'package:immich_mobile/providers/asset_viewer/current_asset.provider.dart'; -import 'package:immich_mobile/providers/user.provider.dart'; -import 'package:immich_mobile/widgets/common/user_circle_avatar.dart'; - -class ActivityTextField extends HookConsumerWidget { - final bool isEnabled; - final String? likeId; - final Function(String) onSubmit; - - const ActivityTextField({required this.onSubmit, this.isEnabled = true, this.likeId, super.key}); - - @override - Widget build(BuildContext context, WidgetRef ref) { - final album = ref.watch(currentAlbumProvider)!; - final asset = ref.watch(currentAssetProvider); - final activityNotifier = ref.read(albumActivityProvider(album.remoteId!, asset?.remoteId).notifier); - final user = ref.watch(currentUserProvider); - final inputController = useTextEditingController(); - final inputFocusNode = useFocusNode(); - final liked = likeId != null; - - // Show keyboard immediately on activities open - useEffect(() { - inputFocusNode.requestFocus(); - return null; - }, []); - - // Pass text to callback and reset controller - void onEditingComplete() { - onSubmit(inputController.text); - inputController.clear(); - inputFocusNode.unfocus(); - } - - Future addLike() async { - await activityNotifier.addLike(); - } - - Future removeLike() async { - if (liked) { - await activityNotifier.removeActivity(likeId!); - } - } - - return Padding( - padding: const EdgeInsets.only(bottom: 10), - child: TextField( - controller: inputController, - enabled: isEnabled, - focusNode: inputFocusNode, - textInputAction: TextInputAction.send, - autofocus: false, - decoration: InputDecoration( - border: InputBorder.none, - focusedBorder: InputBorder.none, - prefixIcon: user != null - ? Padding( - padding: const EdgeInsets.symmetric(horizontal: 15), - child: UserCircleAvatar(user: user, size: 30), - ) - : null, - suffixIcon: Padding( - padding: const EdgeInsets.only(right: 10), - child: IconButton( - icon: Icon(liked ? Icons.thumb_up : Icons.thumb_up_off_alt), - onPressed: liked ? removeLike : addLike, - ), - ), - suffixIconColor: liked ? context.primaryColor : null, - hintText: !isEnabled ? 'shared_album_activities_input_disable'.tr() : 'say_something'.tr(), - hintStyle: TextStyle(fontWeight: FontWeight.normal, fontSize: 14, color: Colors.grey[600]), - ), - onEditingComplete: onEditingComplete, - onTapOutside: (_) => inputFocusNode.unfocus(), - ), - ); - } -} diff --git a/mobile/lib/widgets/activities/activity_tile.dart b/mobile/lib/widgets/activities/activity_tile.dart deleted file mode 100644 index ac3b6c95a4..0000000000 --- a/mobile/lib/widgets/activities/activity_tile.dart +++ /dev/null @@ -1,113 +0,0 @@ -import 'package:auto_route/auto_route.dart'; -import 'package:flutter/material.dart'; -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/extensions/build_context_extensions.dart'; -import 'package:immich_mobile/extensions/datetime_extensions.dart'; -import 'package:immich_mobile/models/activities/activity.model.dart'; -import 'package:immich_mobile/presentation/widgets/images/remote_image_provider.dart'; -import 'package:immich_mobile/providers/activity_service.provider.dart'; -import 'package:immich_mobile/providers/asset_viewer/current_asset.provider.dart'; -import 'package:immich_mobile/widgets/common/user_circle_avatar.dart'; - -class ActivityTile extends HookConsumerWidget { - final Activity activity; - final bool isBottomSheet; - - const ActivityTile(this.activity, {super.key, this.isBottomSheet = false}); - - @override - Widget build(BuildContext context, WidgetRef ref) { - final asset = ref.watch(currentAssetProvider); - final isLike = activity.type == ActivityType.like; - // Asset thumbnail is displayed when we are accessing activities from the album page - // currentAssetProvider will not be set until we open the gallery viewer - final showAssetThumbnail = asset == null && activity.assetId != null && !isBottomSheet; - - onTap() async { - final activityService = ref.read(activityServiceProvider); - final route = await activityService.buildAssetViewerRoute(activity.assetId!, ref); - if (route != null) { - await context.pushRoute(route); - } - } - - return ListTile( - minVerticalPadding: 15, - leading: isLike - ? Container( - width: isBottomSheet ? 30 : 44, - alignment: Alignment.center, - child: Icon(Icons.thumb_up, color: context.primaryColor), - ) - : isBottomSheet - ? UserCircleAvatar(user: activity.user, size: 30) - : UserCircleAvatar(user: activity.user), - title: _ActivityTitle( - userName: activity.user.name, - createdAt: activity.createdAt.timeAgo(), - leftAlign: isBottomSheet ? false : (isLike || showAssetThumbnail), - ), - // No subtitle for like, so center title - titleAlignment: !isLike ? ListTileTitleAlignment.top : ListTileTitleAlignment.center, - trailing: showAssetThumbnail ? _ActivityAssetThumbnail(activity.assetId!, onTap) : null, - subtitle: !isLike ? Text(activity.comment!) : null, - ); - } -} - -class _ActivityTitle extends StatelessWidget { - final String userName; - final String createdAt; - final bool leftAlign; - - const _ActivityTitle({required this.userName, required this.createdAt, required this.leftAlign}); - - @override - Widget build(BuildContext context) { - final textColor = context.isDarkTheme ? Colors.white : Colors.black; - final textStyle = context.textTheme.bodyMedium?.copyWith(color: textColor.withValues(alpha: 0.6)); - - return Row( - mainAxisAlignment: leftAlign ? MainAxisAlignment.start : MainAxisAlignment.spaceBetween, - mainAxisSize: leftAlign ? MainAxisSize.min : MainAxisSize.max, - children: [ - Text(userName, style: textStyle, overflow: TextOverflow.ellipsis), - if (leftAlign) Text(" • ", style: textStyle), - Expanded( - child: Text( - createdAt, - style: textStyle, - overflow: TextOverflow.ellipsis, - textAlign: leftAlign ? TextAlign.left : TextAlign.right, - ), - ), - ], - ); - } -} - -class _ActivityAssetThumbnail extends StatelessWidget { - final String assetId; - final GestureTapCallback? onTap; - - const _ActivityAssetThumbnail(this.assetId, this.onTap); - - @override - Widget build(BuildContext context) { - return GestureDetector( - onTap: onTap, - child: Container( - width: 40, - height: 30, - decoration: BoxDecoration( - borderRadius: const BorderRadius.all(Radius.circular(4)), - image: DecorationImage( - image: RemoteImageProvider.thumbnail(assetId: assetId, thumbhash: ""), - fit: BoxFit.cover, - ), - ), - child: const SizedBox.shrink(), - ), - ); - } -} diff --git a/mobile/lib/widgets/activities/dismissible_activity.dart b/mobile/lib/widgets/activities/dismissible_activity.dart index 806181ecdc..c056f5ee35 100644 --- a/mobile/lib/widgets/activities/dismissible_activity.dart +++ b/mobile/lib/widgets/activities/dismissible_activity.dart @@ -1,8 +1,6 @@ import 'package:flutter/material.dart'; -import 'package:immich_mobile/widgets/activities/activity_tile.dart'; import 'package:immich_mobile/widgets/common/confirm_dialog.dart'; -/// Wraps an [ActivityTile] and makes it dismissible class DismissibleActivity extends StatelessWidget { final String activityId; final Widget body; diff --git a/mobile/lib/widgets/album/add_to_album_bottom_sheet.dart b/mobile/lib/widgets/album/add_to_album_bottom_sheet.dart deleted file mode 100644 index d8f6a8885a..0000000000 --- a/mobile/lib/widgets/album/add_to_album_bottom_sheet.dart +++ /dev/null @@ -1,98 +0,0 @@ -import 'package:auto_route/auto_route.dart'; -import 'package:easy_localization/easy_localization.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_hooks/flutter_hooks.dart'; -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/extensions/build_context_extensions.dart'; -import 'package:immich_mobile/providers/album/album.provider.dart'; -import 'package:immich_mobile/services/album.service.dart'; -import 'package:immich_mobile/widgets/album/add_to_album_sliverlist.dart'; -import 'package:immich_mobile/routing/router.dart'; -import 'package:immich_mobile/entities/album.entity.dart'; -import 'package:immich_mobile/entities/asset.entity.dart'; -import 'package:immich_mobile/widgets/common/drag_sheet.dart'; -import 'package:immich_mobile/widgets/common/immich_toast.dart'; - -class AddToAlbumBottomSheet extends HookConsumerWidget { - /// The asset to add to an album - final List assets; - - const AddToAlbumBottomSheet({super.key, required this.assets}); - - @override - Widget build(BuildContext context, WidgetRef ref) { - final albums = ref.watch(albumProvider).where((a) => a.isRemote).toList(); - final albumService = ref.watch(albumServiceProvider); - - useEffect(() { - // Fetch album updates, e.g., cover image - ref.read(albumProvider.notifier).refreshRemoteAlbums(); - - return null; - }, []); - - void addToAlbum(Album album) async { - final result = await albumService.addAssets(album, assets); - - if (result != null) { - if (result.alreadyInAlbum.isNotEmpty) { - ImmichToast.show( - context: context, - msg: 'add_to_album_bottom_sheet_already_exists'.tr(namedArgs: {"album": album.name}), - ); - } else { - ImmichToast.show( - context: context, - msg: 'add_to_album_bottom_sheet_added'.tr(namedArgs: {"album": album.name}), - ); - } - } - context.pop(); - } - - return Card( - elevation: 0, - shape: const RoundedRectangleBorder( - borderRadius: BorderRadius.only(topLeft: Radius.circular(15), topRight: Radius.circular(15)), - ), - child: CustomScrollView( - slivers: [ - SliverPadding( - padding: const EdgeInsets.symmetric(horizontal: 16), - sliver: SliverToBoxAdapter( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - const SizedBox(height: 12), - const Align(alignment: Alignment.center, child: CustomDraggingHandle()), - const SizedBox(height: 12), - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Text('add_to_album'.tr(), style: context.textTheme.displayMedium), - TextButton.icon( - icon: Icon(Icons.add, color: context.primaryColor), - label: Text('common_create_new_album'.tr(), style: TextStyle(color: context.primaryColor)), - onPressed: () { - context.pushRoute(CreateAlbumRoute(assets: assets)); - }, - ), - ], - ), - ], - ), - ), - ), - SliverPadding( - padding: const EdgeInsets.symmetric(horizontal: 16), - sliver: AddToAlbumSliverList( - albums: albums, - sharedAlbums: albums.where((a) => a.shared).toList(), - onAddToAlbum: addToAlbum, - ), - ), - ], - ), - ); - } -} diff --git a/mobile/lib/widgets/album/add_to_album_sliverlist.dart b/mobile/lib/widgets/album/add_to_album_sliverlist.dart deleted file mode 100644 index defbd90388..0000000000 --- a/mobile/lib/widgets/album/add_to_album_sliverlist.dart +++ /dev/null @@ -1,65 +0,0 @@ -import 'package:easy_localization/easy_localization.dart'; -import 'package:flutter/material.dart'; -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/providers/album/album_sort_by_options.provider.dart'; -import 'package:immich_mobile/widgets/album/album_thumbnail_listtile.dart'; -import 'package:immich_mobile/entities/album.entity.dart'; - -class AddToAlbumSliverList extends HookConsumerWidget { - /// The asset to add to an album - final List albums; - final List sharedAlbums; - final void Function(Album) onAddToAlbum; - final bool enabled; - - const AddToAlbumSliverList({ - super.key, - required this.onAddToAlbum, - required this.albums, - required this.sharedAlbums, - this.enabled = true, - }); - - @override - Widget build(BuildContext context, WidgetRef ref) { - final albumSortMode = ref.watch(albumSortByOptionsProvider); - final albumSortIsReverse = ref.watch(albumSortOrderProvider); - final sortedAlbums = albumSortMode.sortFn(albums, albumSortIsReverse); - final sortedSharedAlbums = albumSortMode.sortFn(sharedAlbums, albumSortIsReverse); - - return SliverList( - delegate: SliverChildBuilderDelegate(childCount: albums.length + (sharedAlbums.isEmpty ? 0 : 1), ( - context, - index, - ) { - // Build shared expander - if (index == 0 && sortedSharedAlbums.isNotEmpty) { - return Padding( - padding: const EdgeInsets.only(bottom: 8), - child: ExpansionTile( - title: Text('shared'.tr()), - tilePadding: const EdgeInsets.symmetric(horizontal: 10.0), - leading: const Icon(Icons.group), - children: [ - ListView.builder( - shrinkWrap: true, - physics: const ClampingScrollPhysics(), - itemCount: sortedSharedAlbums.length, - itemBuilder: (context, index) => AlbumThumbnailListTile( - album: sortedSharedAlbums[index], - onTap: enabled ? () => onAddToAlbum(sortedSharedAlbums[index]) : () {}, - ), - ), - ], - ), - ); - } - - // Build albums list - final offset = index - (sharedAlbums.isNotEmpty ? 1 : 0); - final album = sortedAlbums[offset]; - return AlbumThumbnailListTile(album: album, onTap: enabled ? () => onAddToAlbum(album) : () {}); - }), - ); - } -} diff --git a/mobile/lib/widgets/album/album_thumbnail_card.dart b/mobile/lib/widgets/album/album_thumbnail_card.dart deleted file mode 100644 index 6c56f5d843..0000000000 --- a/mobile/lib/widgets/album/album_thumbnail_card.dart +++ /dev/null @@ -1,111 +0,0 @@ -import 'package:easy_localization/easy_localization.dart'; -import 'package:flutter/material.dart'; -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/entities/album.entity.dart'; -import 'package:immich_mobile/extensions/build_context_extensions.dart'; -import 'package:immich_mobile/extensions/theme_extensions.dart'; -import 'package:immich_mobile/extensions/translate_extensions.dart'; -import 'package:immich_mobile/providers/user.provider.dart'; -import 'package:immich_mobile/widgets/common/immich_thumbnail.dart'; - -class AlbumThumbnailCard extends ConsumerWidget { - final Function()? onTap; - - /// Whether or not to show the owner of the album (or "Owned") - /// in the subtitle of the album - final bool showOwner; - final bool showTitle; - - const AlbumThumbnailCard({super.key, required this.album, this.onTap, this.showOwner = false, this.showTitle = true}); - - final Album album; - - @override - Widget build(BuildContext context, WidgetRef ref) { - return LayoutBuilder( - builder: (context, constraints) { - var cardSize = constraints.maxWidth; - - buildEmptyThumbnail() { - return Container( - height: cardSize, - width: cardSize, - decoration: BoxDecoration(color: context.colorScheme.surfaceContainerHigh), - child: Center( - child: Icon(Icons.no_photography, size: cardSize * .15, color: context.colorScheme.primary), - ), - ); - } - - buildAlbumThumbnail() => ImmichThumbnail(asset: album.thumbnail.value, width: cardSize, height: cardSize); - - buildAlbumTextRow() { - // Add the owner name to the subtitle - String? owner; - if (showOwner) { - if (album.ownerId == ref.read(currentUserProvider)?.id) { - owner = 'owned'.tr(); - } else if (album.ownerName != null) { - owner = 'shared_by_user'.t(context: context, args: {'user': album.ownerName!}); - } - } - - return Text.rich( - TextSpan( - children: [ - TextSpan( - text: 'items_count'.t(context: context, args: {'count': album.assetCount}), - ), - if (owner != null) const TextSpan(text: ' • '), - if (owner != null) TextSpan(text: owner), - ], - style: context.textTheme.bodyMedium?.copyWith(color: context.colorScheme.onSurfaceSecondary), - ), - overflow: TextOverflow.fade, - ); - } - - return GestureDetector( - onTap: onTap, - child: Flex( - direction: Axis.vertical, - children: [ - Flexible( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - SizedBox( - width: cardSize, - height: cardSize, - child: ClipRRect( - borderRadius: const BorderRadius.all(Radius.circular(20)), - child: album.thumbnail.value == null ? buildEmptyThumbnail() : buildAlbumThumbnail(), - ), - ), - if (showTitle) ...[ - Padding( - padding: const EdgeInsets.only(top: 8.0), - child: SizedBox( - width: cardSize, - child: Text( - album.name, - overflow: TextOverflow.ellipsis, - style: context.textTheme.titleSmall?.copyWith( - color: context.colorScheme.onSurface, - fontWeight: FontWeight.w500, - ), - ), - ), - ), - buildAlbumTextRow(), - ], - ], - ), - ), - ], - ), - ); - }, - ); - } -} diff --git a/mobile/lib/widgets/album/album_thumbnail_listtile.dart b/mobile/lib/widgets/album/album_thumbnail_listtile.dart deleted file mode 100644 index 386084b034..0000000000 --- a/mobile/lib/widgets/album/album_thumbnail_listtile.dart +++ /dev/null @@ -1,93 +0,0 @@ -import 'package:auto_route/auto_route.dart'; -import 'package:easy_localization/easy_localization.dart'; -import 'package:flutter/material.dart'; -import 'package:immich_mobile/entities/album.entity.dart'; -import 'package:immich_mobile/extensions/build_context_extensions.dart'; -import 'package:immich_mobile/extensions/translate_extensions.dart'; -import 'package:immich_mobile/presentation/widgets/images/remote_image_provider.dart'; -import 'package:immich_mobile/presentation/widgets/images/thumbnail.widget.dart'; -import 'package:immich_mobile/routing/router.dart'; -import 'package:immich_mobile/utils/image_url_builder.dart'; -import 'package:openapi/api.dart'; - -class AlbumThumbnailListTile extends StatelessWidget { - const AlbumThumbnailListTile({super.key, required this.album, this.onTap}); - - final Album album; - final void Function()? onTap; - - @override - Widget build(BuildContext context) { - var cardSize = 68.0; - - buildEmptyThumbnail() { - return Container( - decoration: BoxDecoration(color: context.isDarkTheme ? Colors.grey[800] : Colors.grey[200]), - child: SizedBox( - height: cardSize, - width: cardSize, - child: const Center(child: Icon(Icons.no_photography)), - ), - ); - } - - buildAlbumThumbnail() { - return SizedBox( - width: cardSize, - height: cardSize, - child: Thumbnail( - imageProvider: RemoteImageProvider(url: getAlbumThumbnailUrl(album, type: AssetMediaSize.thumbnail)), - ), - ); - } - - return GestureDetector( - behavior: HitTestBehavior.opaque, - onTap: - onTap ?? - () { - context.pushRoute(AlbumViewerRoute(albumId: album.id)); - }, - child: Padding( - padding: const EdgeInsets.only(bottom: 12.0), - child: Row( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - ClipRRect( - borderRadius: const BorderRadius.all(Radius.circular(8)), - child: album.thumbnail.value == null ? buildEmptyThumbnail() : buildAlbumThumbnail(), - ), - Expanded( - child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 8.0), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - album.name, - overflow: TextOverflow.ellipsis, - style: const TextStyle(fontWeight: FontWeight.bold), - ), - Row( - mainAxisSize: MainAxisSize.min, - children: [ - Text( - 'items_count'.t(context: context, args: {'count': album.assetCount}), - style: const TextStyle(fontSize: 12), - ), - if (album.shared) ...[ - const Text(' • ', style: TextStyle(fontSize: 12)), - Text('shared'.tr(), style: const TextStyle(fontSize: 12)), - ], - ], - ), - ], - ), - ), - ), - ], - ), - ), - ); - } -} diff --git a/mobile/lib/widgets/album/album_title_text_field.dart b/mobile/lib/widgets/album/album_title_text_field.dart deleted file mode 100644 index 0a7438b7ae..0000000000 --- a/mobile/lib/widgets/album/album_title_text_field.dart +++ /dev/null @@ -1,74 +0,0 @@ -import 'package:easy_localization/easy_localization.dart'; -import 'package:flutter/material.dart'; -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/extensions/build_context_extensions.dart'; -import 'package:immich_mobile/providers/album/album_title.provider.dart'; - -class AlbumTitleTextField extends ConsumerWidget { - const AlbumTitleTextField({ - super.key, - required this.isAlbumTitleEmpty, - required this.albumTitleTextFieldFocusNode, - required this.albumTitleController, - required this.isAlbumTitleTextFieldFocus, - }); - - final ValueNotifier isAlbumTitleEmpty; - final FocusNode albumTitleTextFieldFocusNode; - final TextEditingController albumTitleController; - final ValueNotifier isAlbumTitleTextFieldFocus; - - @override - Widget build(BuildContext context, WidgetRef ref) { - return TextField( - onChanged: (v) { - if (v.isEmpty) { - isAlbumTitleEmpty.value = true; - } else { - isAlbumTitleEmpty.value = false; - } - - ref.watch(albumTitleProvider.notifier).setAlbumTitle(v); - }, - focusNode: albumTitleTextFieldFocusNode, - style: TextStyle(fontSize: 28, color: context.colorScheme.onSurface, fontWeight: FontWeight.bold), - controller: albumTitleController, - onTap: () { - isAlbumTitleTextFieldFocus.value = true; - - if (albumTitleController.text == 'Untitled') { - albumTitleController.clear(); - } - }, - decoration: InputDecoration( - contentPadding: const EdgeInsets.symmetric(horizontal: 8, vertical: 8), - suffixIcon: !isAlbumTitleEmpty.value && isAlbumTitleTextFieldFocus.value - ? IconButton( - onPressed: () { - albumTitleController.clear(); - isAlbumTitleEmpty.value = true; - }, - icon: Icon(Icons.cancel_rounded, color: context.primaryColor), - splashRadius: 10, - ) - : null, - enabledBorder: const OutlineInputBorder( - borderSide: BorderSide(color: Colors.transparent), - borderRadius: BorderRadius.all(Radius.circular(10)), - ), - focusedBorder: const OutlineInputBorder( - borderSide: BorderSide(color: Colors.transparent), - borderRadius: BorderRadius.all(Radius.circular(10)), - ), - hintText: 'add_a_title'.tr(), - hintStyle: context.themeData.inputDecorationTheme.hintStyle?.copyWith( - fontSize: 28, - fontWeight: FontWeight.bold, - ), - focusColor: Colors.grey[300], - fillColor: context.colorScheme.surfaceContainerHigh, - filled: isAlbumTitleTextFieldFocus.value, - ), - ); - } -} diff --git a/mobile/lib/widgets/album/album_viewer_appbar.dart b/mobile/lib/widgets/album/album_viewer_appbar.dart deleted file mode 100644 index 4fd4b31013..0000000000 --- a/mobile/lib/widgets/album/album_viewer_appbar.dart +++ /dev/null @@ -1,307 +0,0 @@ -import 'dart:async'; - -import 'package:auto_route/auto_route.dart'; -import 'package:easy_localization/easy_localization.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_hooks/flutter_hooks.dart'; -import 'package:fluttertoast/fluttertoast.dart'; -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/extensions/build_context_extensions.dart'; -import 'package:immich_mobile/providers/activity_statistics.provider.dart'; -import 'package:immich_mobile/providers/album/album.provider.dart'; -import 'package:immich_mobile/providers/album/album_viewer.provider.dart'; -import 'package:immich_mobile/providers/album/current_album.provider.dart'; -import 'package:immich_mobile/routing/router.dart'; -import 'package:immich_mobile/widgets/common/immich_toast.dart'; - -class AlbumViewerAppbar extends HookConsumerWidget implements PreferredSizeWidget { - const AlbumViewerAppbar({ - super.key, - required this.userId, - required this.titleFocusNode, - required this.descriptionFocusNode, - this.onAddPhotos, - this.onAddUsers, - required this.onActivities, - }); - - final String userId; - final FocusNode titleFocusNode; - final FocusNode descriptionFocusNode; - final void Function()? onAddPhotos; - final void Function()? onAddUsers; - final void Function() onActivities; - - @override - Widget build(BuildContext context, WidgetRef ref) { - final albumState = useState(ref.read(currentAlbumProvider)); - final album = albumState.value; - ref.listen(currentAlbumProvider, (_, newAlbum) { - final oldAlbum = albumState.value; - if (oldAlbum != null && newAlbum != null && oldAlbum.id == newAlbum.id) { - return; - } - - albumState.value = newAlbum; - }); - - if (album == null) { - return const SizedBox(); - } - - final albumViewer = ref.watch(albumViewerProvider); - final newAlbumTitle = albumViewer.editTitleText; - final newAlbumDescription = albumViewer.editDescriptionText; - final isEditAlbum = albumViewer.isEditAlbum; - - final comments = album.shared ? ref.watch(activityStatisticsProvider(album.remoteId!)) : 0; - - deleteAlbum() async { - final bool success = await ref.watch(albumProvider.notifier).deleteAlbum(album); - - unawaited(context.navigateTo(const TabControllerRoute(children: [AlbumsRoute()]))); - - if (!success) { - ImmichToast.show( - context: context, - msg: "album_viewer_appbar_share_err_delete".tr(), - toastType: ToastType.error, - gravity: ToastGravity.BOTTOM, - ); - } - } - - Future onDeleteAlbumPressed() { - return showDialog( - context: context, - barrierDismissible: false, // user must tap button! - builder: (BuildContext context) { - return AlertDialog( - title: const Text('delete_album').tr(), - content: const Text('album_viewer_appbar_delete_confirm').tr(), - actions: [ - TextButton( - onPressed: () => context.pop('Cancel'), - child: Text( - 'cancel', - style: TextStyle(color: context.primaryColor, fontWeight: FontWeight.bold), - ).tr(), - ), - TextButton( - onPressed: () { - context.pop('Confirm'); - deleteAlbum(); - }, - child: Text( - 'confirm', - style: TextStyle(fontWeight: FontWeight.bold, color: context.colorScheme.error), - ).tr(), - ), - ], - ); - }, - ); - } - - void onLeaveAlbumPressed() async { - bool isSuccess = await ref.watch(albumProvider.notifier).leaveAlbum(album); - - if (isSuccess) { - unawaited(context.navigateTo(const TabControllerRoute(children: [AlbumsRoute()]))); - } else { - context.pop(); - ImmichToast.show( - context: context, - msg: "album_viewer_appbar_share_err_leave".tr(), - toastType: ToastType.error, - gravity: ToastGravity.BOTTOM, - ); - } - } - - buildBottomSheetActions() { - return [ - album.ownerId == userId - ? ListTile( - leading: const Icon(Icons.delete_forever_rounded), - title: const Text('delete_album', style: TextStyle(fontWeight: FontWeight.w500)).tr(), - onTap: onDeleteAlbumPressed, - ) - : ListTile( - leading: const Icon(Icons.person_remove_rounded), - title: const Text( - 'album_viewer_appbar_share_leave', - style: TextStyle(fontWeight: FontWeight.w500), - ).tr(), - onTap: onLeaveAlbumPressed, - ), - ]; - // } - } - - void onSortOrderToggled() async { - final updatedAlbum = await ref.read(albumProvider.notifier).toggleSortOrder(album); - - if (updatedAlbum == null) { - ImmichToast.show( - context: context, - msg: "error_change_sort_album".tr(), - toastType: ToastType.error, - gravity: ToastGravity.BOTTOM, - ); - } - - context.pop(); - } - - void buildBottomSheet() { - final ownerActions = [ - ListTile( - leading: const Icon(Icons.person_add_alt_rounded), - onTap: () { - context.pop(); - final onAddUsers = this.onAddUsers; - if (onAddUsers != null) { - onAddUsers(); - } - }, - title: const Text("album_viewer_page_share_add_users", style: TextStyle(fontWeight: FontWeight.w500)).tr(), - ), - ListTile( - leading: const Icon(Icons.swap_vert_rounded), - onTap: onSortOrderToggled, - title: const Text("change_display_order", style: TextStyle(fontWeight: FontWeight.w500)).tr(), - ), - ListTile( - leading: const Icon(Icons.link_rounded), - onTap: () { - context.pushRoute(SharedLinkEditRoute(albumId: album.remoteId)); - context.pop(); - }, - title: const Text("control_bottom_app_bar_share_link", style: TextStyle(fontWeight: FontWeight.w500)).tr(), - ), - ListTile( - leading: const Icon(Icons.settings_rounded), - onTap: () => context.navigateTo(const AlbumOptionsRoute()), - title: const Text("options", style: TextStyle(fontWeight: FontWeight.w500)).tr(), - ), - ]; - - final commonActions = [ - ListTile( - leading: const Icon(Icons.add_photo_alternate_outlined), - onTap: () { - context.pop(); - final onAddPhotos = this.onAddPhotos; - if (onAddPhotos != null) { - onAddPhotos(); - } - }, - title: const Text("add_photos", style: TextStyle(fontWeight: FontWeight.w500)).tr(), - ), - ]; - showModalBottomSheet( - backgroundColor: context.scaffoldBackgroundColor, - isScrollControlled: false, - context: context, - builder: (context) { - return SafeArea( - child: Padding( - padding: const EdgeInsets.only(top: 24.0), - child: ListView( - shrinkWrap: true, - children: [ - ...buildBottomSheetActions(), - if (onAddPhotos != null) ...commonActions, - if (onAddPhotos != null && userId == album.ownerId) ...ownerActions, - ], - ), - ), - ); - }, - ); - } - - Widget buildActivitiesButton() { - return IconButton( - onPressed: onActivities, - icon: Row( - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - const Icon(Icons.mode_comment_outlined), - if (comments != 0) - Padding( - padding: const EdgeInsets.only(left: 5), - child: Text( - comments.toString(), - style: TextStyle(fontWeight: FontWeight.bold, color: context.primaryColor), - ), - ), - ], - ), - ); - } - - buildLeadingButton() { - if (isEditAlbum) { - return IconButton( - onPressed: () async { - if (newAlbumTitle.isNotEmpty) { - bool isSuccess = await ref.watch(albumViewerProvider.notifier).changeAlbumTitle(album, newAlbumTitle); - if (!isSuccess) { - ImmichToast.show( - context: context, - msg: "album_viewer_appbar_share_err_title".tr(), - gravity: ToastGravity.BOTTOM, - toastType: ToastType.error, - ); - } - titleFocusNode.unfocus(); - } else if (newAlbumDescription.isNotEmpty) { - bool isSuccessDescription = await ref - .watch(albumViewerProvider.notifier) - .changeAlbumDescription(album, newAlbumDescription); - if (!isSuccessDescription) { - ImmichToast.show( - context: context, - msg: "album_viewer_appbar_share_err_description".tr(), - gravity: ToastGravity.BOTTOM, - toastType: ToastType.error, - ); - } - descriptionFocusNode.unfocus(); - } else { - titleFocusNode.unfocus(); - descriptionFocusNode.unfocus(); - ref.read(albumViewerProvider.notifier).disableEditAlbum(); - } - }, - icon: const Icon(Icons.check_rounded), - splashRadius: 25, - ); - } else { - return IconButton( - onPressed: context.maybePop, - icon: const Icon(Icons.arrow_back_ios_rounded), - splashRadius: 25, - ); - } - } - - return AppBar( - elevation: 0, - backgroundColor: context.scaffoldBackgroundColor, - leading: buildLeadingButton(), - centerTitle: false, - actions: [ - if (album.shared && (album.activityEnabled || comments != 0)) buildActivitiesButton(), - if (album.isRemote) ...[ - IconButton(splashRadius: 25, onPressed: buildBottomSheet, icon: const Icon(Icons.more_horiz_rounded)), - ], - ], - ); - } - - @override - Size get preferredSize => const Size.fromHeight(kToolbarHeight); -} diff --git a/mobile/lib/widgets/album/album_viewer_editable_description.dart b/mobile/lib/widgets/album/album_viewer_editable_description.dart deleted file mode 100644 index decd268ff3..0000000000 --- a/mobile/lib/widgets/album/album_viewer_editable_description.dart +++ /dev/null @@ -1,82 +0,0 @@ -import 'package:easy_localization/easy_localization.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_hooks/flutter_hooks.dart'; -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/extensions/build_context_extensions.dart'; -import 'package:immich_mobile/providers/album/album_viewer.provider.dart'; - -class AlbumViewerEditableDescription extends HookConsumerWidget { - final String albumDescription; - final FocusNode descriptionFocusNode; - const AlbumViewerEditableDescription({super.key, required this.albumDescription, required this.descriptionFocusNode}); - - @override - Widget build(BuildContext context, WidgetRef ref) { - final albumViewerState = ref.watch(albumViewerProvider); - - final descriptionTextEditController = useTextEditingController( - text: albumViewerState.isEditAlbum && albumViewerState.editDescriptionText.isNotEmpty - ? albumViewerState.editDescriptionText - : albumDescription, - ); - - void onFocusModeChange() { - if (!descriptionFocusNode.hasFocus && descriptionTextEditController.text.isEmpty) { - ref.watch(albumViewerProvider.notifier).setEditDescriptionText(""); - descriptionTextEditController.text = ""; - } - } - - useEffect(() { - descriptionFocusNode.addListener(onFocusModeChange); - return () { - descriptionFocusNode.removeListener(onFocusModeChange); - }; - }, []); - - return Material( - color: Colors.transparent, - child: TextField( - onChanged: (value) { - if (value.isEmpty) { - } else { - ref.watch(albumViewerProvider.notifier).setEditDescriptionText(value); - } - }, - focusNode: descriptionFocusNode, - style: context.textTheme.bodyLarge, - maxLines: 3, - minLines: 1, - controller: descriptionTextEditController, - onTap: () { - context.focusScope.requestFocus(descriptionFocusNode); - - ref.watch(albumViewerProvider.notifier).setEditDescriptionText(albumDescription); - ref.watch(albumViewerProvider.notifier).enableEditAlbum(); - - if (descriptionTextEditController.text == '') { - descriptionTextEditController.clear(); - } - }, - decoration: InputDecoration( - contentPadding: const EdgeInsets.all(8), - suffixIcon: descriptionFocusNode.hasFocus - ? IconButton( - onPressed: () { - descriptionTextEditController.clear(); - }, - icon: Icon(Icons.cancel_rounded, color: context.primaryColor), - splashRadius: 10, - ) - : null, - enabledBorder: const OutlineInputBorder(borderSide: BorderSide(color: Colors.transparent)), - focusedBorder: const OutlineInputBorder(borderSide: BorderSide(color: Colors.transparent)), - focusColor: Colors.grey[300], - fillColor: context.scaffoldBackgroundColor, - filled: descriptionFocusNode.hasFocus, - hintText: 'add_a_description'.tr(), - ), - ), - ); - } -} diff --git a/mobile/lib/widgets/album/album_viewer_editable_title.dart b/mobile/lib/widgets/album/album_viewer_editable_title.dart deleted file mode 100644 index c84e613017..0000000000 --- a/mobile/lib/widgets/album/album_viewer_editable_title.dart +++ /dev/null @@ -1,81 +0,0 @@ -import 'package:easy_localization/easy_localization.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_hooks/flutter_hooks.dart'; -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/extensions/build_context_extensions.dart'; -import 'package:immich_mobile/providers/album/album_viewer.provider.dart'; - -class AlbumViewerEditableTitle extends HookConsumerWidget { - final String albumName; - final FocusNode titleFocusNode; - const AlbumViewerEditableTitle({super.key, required this.albumName, required this.titleFocusNode}); - - @override - Widget build(BuildContext context, WidgetRef ref) { - final albumViewerState = ref.watch(albumViewerProvider); - - final titleTextEditController = useTextEditingController( - text: albumViewerState.isEditAlbum && albumViewerState.editTitleText.isNotEmpty - ? albumViewerState.editTitleText - : albumName, - ); - - void onFocusModeChange() { - if (!titleFocusNode.hasFocus && titleTextEditController.text.isEmpty) { - ref.watch(albumViewerProvider.notifier).setEditTitleText("Untitled"); - titleTextEditController.text = "Untitled"; - } - } - - useEffect(() { - titleFocusNode.addListener(onFocusModeChange); - return () { - titleFocusNode.removeListener(onFocusModeChange); - }; - }, []); - - return Material( - color: Colors.transparent, - child: TextField( - onChanged: (value) { - if (value.isEmpty) { - } else { - ref.watch(albumViewerProvider.notifier).setEditTitleText(value); - } - }, - focusNode: titleFocusNode, - style: context.textTheme.headlineLarge?.copyWith(fontWeight: FontWeight.w700), - controller: titleTextEditController, - onTap: () { - context.focusScope.requestFocus(titleFocusNode); - - ref.watch(albumViewerProvider.notifier).setEditTitleText(albumName); - ref.watch(albumViewerProvider.notifier).enableEditAlbum(); - - if (titleTextEditController.text == 'Untitled') { - titleTextEditController.clear(); - } - }, - decoration: InputDecoration( - contentPadding: const EdgeInsets.symmetric(horizontal: 8, vertical: 0), - suffixIcon: titleFocusNode.hasFocus - ? IconButton( - onPressed: () { - titleTextEditController.clear(); - }, - icon: Icon(Icons.cancel_rounded, color: context.primaryColor), - splashRadius: 10, - ) - : null, - enabledBorder: const OutlineInputBorder(borderSide: BorderSide(color: Colors.transparent)), - focusedBorder: const OutlineInputBorder(borderSide: BorderSide(color: Colors.transparent)), - focusColor: Colors.grey[300], - fillColor: context.scaffoldBackgroundColor, - filled: titleFocusNode.hasFocus, - hintText: 'add_a_title'.tr(), - hintStyle: context.themeData.inputDecorationTheme.hintStyle?.copyWith(fontSize: 28), - ), - ), - ); - } -} diff --git a/mobile/lib/widgets/album/shared_album_thumbnail_image.dart b/mobile/lib/widgets/album/shared_album_thumbnail_image.dart deleted file mode 100644 index b21e86d145..0000000000 --- a/mobile/lib/widgets/album/shared_album_thumbnail_image.dart +++ /dev/null @@ -1,20 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/entities/asset.entity.dart'; -import 'package:immich_mobile/widgets/common/immich_thumbnail.dart'; - -class SharedAlbumThumbnailImage extends HookConsumerWidget { - final Asset asset; - - const SharedAlbumThumbnailImage({super.key, required this.asset}); - - @override - Widget build(BuildContext context, WidgetRef ref) { - return GestureDetector( - onTap: () { - // debugPrint("View ${asset.id}"); - }, - child: Stack(children: [ImmichThumbnail(asset: asset, width: 500, height: 500)]), - ); - } -} diff --git a/mobile/lib/widgets/asset_grid/asset_drag_region.dart b/mobile/lib/widgets/asset_grid/asset_drag_region.dart deleted file mode 100644 index 71e55acbd6..0000000000 --- a/mobile/lib/widgets/asset_grid/asset_drag_region.dart +++ /dev/null @@ -1,207 +0,0 @@ -// Based on https://stackoverflow.com/a/52625182 - -import 'dart:async'; - -import 'package:collection/collection.dart'; -import 'package:flutter/gestures.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter/rendering.dart'; - -class AssetDragRegion extends StatefulWidget { - final Widget child; - - final void Function(AssetIndex valueKey)? onStart; - final void Function(AssetIndex valueKey)? onAssetEnter; - final void Function()? onEnd; - final void Function()? onScrollStart; - final void Function(ScrollDirection direction)? onScroll; - - const AssetDragRegion({ - super.key, - required this.child, - this.onStart, - this.onAssetEnter, - this.onEnd, - this.onScrollStart, - this.onScroll, - }); - @override - State createState() => _AssetDragRegionState(); -} - -class _AssetDragRegionState extends State { - late AssetIndex? assetUnderPointer; - late AssetIndex? anchorAsset; - - // Scroll related state - static const double scrollOffset = 0.10; - double? topScrollOffset; - double? bottomScrollOffset; - Timer? scrollTimer; - late bool scrollNotified; - - @override - void initState() { - super.initState(); - assetUnderPointer = null; - anchorAsset = null; - scrollNotified = false; - } - - @override - void didChangeDependencies() { - super.didChangeDependencies(); - topScrollOffset = null; - bottomScrollOffset = null; - } - - @override - void dispose() { - scrollTimer?.cancel(); - super.dispose(); - } - - @override - Widget build(BuildContext context) { - return RawGestureDetector( - gestures: { - _CustomLongPressGestureRecognizer: GestureRecognizerFactoryWithHandlers<_CustomLongPressGestureRecognizer>( - () => _CustomLongPressGestureRecognizer(), - _registerCallbacks, - ), - }, - child: widget.child, - ); - } - - void _registerCallbacks(_CustomLongPressGestureRecognizer recognizer) { - recognizer.onLongPressMoveUpdate = (details) => _onLongPressMove(details); - recognizer.onLongPressStart = (details) => _onLongPressStart(details); - recognizer.onLongPressUp = _onLongPressEnd; - } - - AssetIndex? _getValueKeyAtPositon(Offset position) { - final box = context.findAncestorRenderObjectOfType(); - if (box == null) return null; - - final hitTestResult = BoxHitTestResult(); - final local = box.globalToLocal(position); - if (!box.hitTest(hitTestResult, position: local)) return null; - - return (hitTestResult.path.firstWhereOrNull((hit) => hit.target is _AssetIndexProxy)?.target as _AssetIndexProxy?) - ?.index; - } - - void _onLongPressStart(LongPressStartDetails event) { - /// Calculate widget height and scroll offset when long press starting instead of in [initState] - /// or [didChangeDependencies] as the grid might still be rendering into view to get the actual size - final height = context.size?.height; - if (height != null && (topScrollOffset == null || bottomScrollOffset == null)) { - topScrollOffset = height * scrollOffset; - bottomScrollOffset = height - topScrollOffset!; - } - - final initialHit = _getValueKeyAtPositon(event.globalPosition); - anchorAsset = initialHit; - if (initialHit == null) return; - - if (anchorAsset != null) { - widget.onStart?.call(anchorAsset!); - } - } - - void _onLongPressEnd() { - scrollNotified = false; - scrollTimer?.cancel(); - widget.onEnd?.call(); - } - - void _onLongPressMove(LongPressMoveUpdateDetails event) { - if (anchorAsset == null) return; - if (topScrollOffset == null || bottomScrollOffset == null) return; - - final currentDy = event.localPosition.dy; - - if (currentDy > bottomScrollOffset!) { - scrollTimer ??= Timer.periodic( - const Duration(milliseconds: 50), - (_) => widget.onScroll?.call(ScrollDirection.forward), - ); - } else if (currentDy < topScrollOffset!) { - scrollTimer ??= Timer.periodic( - const Duration(milliseconds: 50), - (_) => widget.onScroll?.call(ScrollDirection.reverse), - ); - } else { - scrollTimer?.cancel(); - scrollTimer = null; - } - - final currentlyTouchingAsset = _getValueKeyAtPositon(event.globalPosition); - if (currentlyTouchingAsset == null) return; - - if (assetUnderPointer != currentlyTouchingAsset) { - if (!scrollNotified) { - scrollNotified = true; - widget.onScrollStart?.call(); - } - - widget.onAssetEnter?.call(currentlyTouchingAsset); - assetUnderPointer = currentlyTouchingAsset; - } - } -} - -class _CustomLongPressGestureRecognizer extends LongPressGestureRecognizer { - @override - void rejectGesture(int pointer) { - acceptGesture(pointer); - } -} - -class AssetIndexWrapper extends SingleChildRenderObjectWidget { - final int rowIndex; - final int sectionIndex; - - const AssetIndexWrapper({required Widget super.child, required this.rowIndex, required this.sectionIndex, super.key}); - - @override - // ignore: library_private_types_in_public_api - _AssetIndexProxy createRenderObject(BuildContext context) { - return _AssetIndexProxy( - index: AssetIndex(rowIndex: rowIndex, sectionIndex: sectionIndex), - ); - } - - @override - void updateRenderObject( - BuildContext context, - // ignore: library_private_types_in_public_api - _AssetIndexProxy renderObject, - ) { - renderObject.index = AssetIndex(rowIndex: rowIndex, sectionIndex: sectionIndex); - } -} - -class _AssetIndexProxy extends RenderProxyBox { - AssetIndex index; - - _AssetIndexProxy({required this.index}); -} - -class AssetIndex { - final int rowIndex; - final int sectionIndex; - - const AssetIndex({required this.rowIndex, required this.sectionIndex}); - - @override - bool operator ==(covariant AssetIndex other) { - if (identical(this, other)) return true; - - return other.rowIndex == rowIndex && other.sectionIndex == sectionIndex; - } - - @override - int get hashCode => rowIndex.hashCode ^ sectionIndex.hashCode; -} diff --git a/mobile/lib/widgets/asset_grid/asset_grid_data_structure.dart b/mobile/lib/widgets/asset_grid/asset_grid_data_structure.dart deleted file mode 100644 index d95d6efe2e..0000000000 --- a/mobile/lib/widgets/asset_grid/asset_grid_data_structure.dart +++ /dev/null @@ -1,307 +0,0 @@ -import 'dart:math'; - -import 'package:collection/collection.dart'; -import 'package:easy_localization/easy_localization.dart'; -import 'package:immich_mobile/entities/asset.entity.dart'; -import 'package:isar/isar.dart'; -import 'package:logging/logging.dart'; - -final log = Logger('AssetGridDataStructure'); - -enum RenderAssetGridElementType { assets, assetRow, groupDividerTitle, monthTitle } - -class RenderAssetGridElement { - final RenderAssetGridElementType type; - final String? title; - final DateTime date; - final int count; - final int offset; - final int totalCount; - - const RenderAssetGridElement( - this.type, { - this.title, - required this.date, - this.count = 0, - this.offset = 0, - this.totalCount = 0, - }); -} - -enum GroupAssetsBy { day, month, auto, none } - -class RenderList { - final List elements; - final List? allAssets; - final QueryBuilder? query; - final int totalAssets; - - /// reference to batch of assets loaded from DB with offset [_bufOffset] - List _buf = []; - - /// global offset of assets in [_buf] - int _bufOffset = 0; - - RenderList(this.elements, this.query, this.allAssets) : totalAssets = allAssets?.length ?? query!.countSync(); - - bool get isEmpty => totalAssets == 0; - - /// Loads the requested assets from the database to an internal buffer if not cached - /// and returns a slice of that buffer - List loadAssets(int offset, int count) { - assert(offset >= 0); - assert(count > 0); - assert(offset + count <= totalAssets); - if (allAssets != null) { - // if we already loaded all assets (e.g. from search result) - // simply return the requested slice of that array - return allAssets!.slice(offset, offset + count); - } else if (query != null) { - // general case: we have the query to load assets via offset from the DB on demand - if (offset < _bufOffset || offset + count > _bufOffset + _buf.length) { - // the requested slice (offset:offset+count) is not contained in the cache buffer `_buf` - // thus, fill the buffer with a new batch of assets that at least contains the requested - // assets and some more - - final bool forward = _bufOffset < offset; - // if the requested offset is greater than the cached offset, the user scrolls forward "down" - const batchSize = 256; - const oppositeSize = 64; - - // make sure to load a meaningful amount of data (and not only the requested slice) - // otherwise, each call to [loadAssets] would result in DB call trashing performance - // fills small requests to [batchSize], adds some legroom into the opposite scroll direction for large requests - final len = max(batchSize, count + oppositeSize); - // when scrolling forward, start shortly before the requested offset... - // when scrolling backward, end shortly after the requested offset... - // ... to guard against the user scrolling in the other direction - // a tiny bit resulting in a another required load from the DB - final start = max(0, forward ? offset - oppositeSize : (len > batchSize ? offset : offset + count - len)); - // load the calculated batch (start:start+len) from the DB and put it into the buffer - _buf = query!.offset(start).limit(len).findAllSync(); - _bufOffset = start; - } - assert(_bufOffset <= offset); - assert(_bufOffset + _buf.length >= offset + count); - // return the requested slice from the buffer (we made sure before that the assets are loaded!) - return _buf.slice(offset - _bufOffset, offset - _bufOffset + count); - } - throw Exception("RenderList has neither assets nor query"); - } - - /// Returns the requested asset either from cached buffer or directly from the database - Asset loadAsset(int index) { - if (allAssets != null) { - // all assets are already loaded (e.g. from search result) - return allAssets![index]; - } else if (query != null) { - // general case: we have the DB query to load asset(s) on demand - if (index >= _bufOffset && index < _bufOffset + _buf.length) { - // lucky case: the requested asset is already cached in the buffer! - return _buf[index - _bufOffset]; - } - // request the asset from the database (not changing the buffer!) - final asset = query!.offset(index).findFirstSync(); - if (asset == null) { - throw Exception("Asset at index $index does no longer exist in database"); - } - return asset; - } - throw Exception("RenderList has neither assets nor query"); - } - - static Future fromQuery(QueryBuilder query, GroupAssetsBy groupBy) => - _buildRenderList(null, query, groupBy); - - static Future _buildRenderList( - List? assets, - QueryBuilder? query, - GroupAssetsBy groupBy, - ) async { - final List elements = []; - - const pageSize = 50000; - const sectionSize = 60; // divides evenly by 2,3,4,5,6 - - if (groupBy == GroupAssetsBy.none) { - final int total = assets?.length ?? query!.countSync(); - - final dateLoader = query != null ? DateBatchLoader(query: query, batchSize: 1000 * sectionSize) : null; - - for (int i = 0; i < total; i += sectionSize) { - final date = assets != null ? assets[i].fileCreatedAt : await dateLoader?.getDate(i); - - final int count = i + sectionSize > total ? total - i : sectionSize; - if (date == null) break; - elements.add( - RenderAssetGridElement( - RenderAssetGridElementType.assets, - date: date, - count: count, - totalCount: total, - offset: i, - ), - ); - } - return RenderList(elements, query, assets); - } - - final formatSameYear = groupBy == GroupAssetsBy.month ? DateFormat.MMMM() : DateFormat.MMMEd(); - final formatOtherYear = groupBy == GroupAssetsBy.month ? DateFormat.yMMMM() : DateFormat.yMMMEd(); - final currentYear = DateTime.now().year; - final formatMergedSameYear = DateFormat.MMMd(); - final formatMergedOtherYear = DateFormat.yMMMd(); - - int offset = 0; - DateTime? last; - DateTime? current; - int lastOffset = 0; - int count = 0; - int monthCount = 0; - int lastMonthIndex = 0; - - String formatDateRange(DateTime from, DateTime to) { - final startDate = (from.year == currentYear ? formatMergedSameYear : formatMergedOtherYear).format(from); - final endDate = (to.year == currentYear ? formatMergedSameYear : formatMergedOtherYear).format(to); - if (DateTime(from.year, from.month, from.day) == DateTime(to.year, to.month, to.day)) { - // format range with time when both dates are on the same day - final startTime = DateFormat.Hm().format(from); - final endTime = DateFormat.Hm().format(to); - return "$startDate $startTime - $endTime"; - } - return "$startDate - $endDate"; - } - - void mergeMonth() { - if (last != null && groupBy == GroupAssetsBy.auto && monthCount <= 30 && elements.length > lastMonthIndex + 1) { - // merge all days into a single section - assert(elements[lastMonthIndex].date.month == last.month); - final e = elements[lastMonthIndex]; - - elements[lastMonthIndex] = RenderAssetGridElement( - RenderAssetGridElementType.monthTitle, - date: e.date, - count: monthCount, - totalCount: monthCount, - offset: e.offset, - title: formatDateRange(e.date, elements.last.date), - ); - elements.removeRange(lastMonthIndex + 1, elements.length); - } - } - - void addElems(DateTime d, DateTime? prevDate) { - final bool newMonth = last == null || last.year != d.year || last.month != d.month; - if (newMonth) { - mergeMonth(); - lastMonthIndex = elements.length; - monthCount = 0; - } - for (int j = 0; j < count; j += sectionSize) { - final type = j == 0 - ? (groupBy != GroupAssetsBy.month && newMonth - ? RenderAssetGridElementType.monthTitle - : RenderAssetGridElementType.groupDividerTitle) - : (groupBy == GroupAssetsBy.auto - ? RenderAssetGridElementType.groupDividerTitle - : RenderAssetGridElementType.assets); - final sectionCount = j + sectionSize > count ? count - j : sectionSize; - assert(sectionCount > 0 && sectionCount <= sectionSize); - elements.add( - RenderAssetGridElement( - type, - date: d, - count: sectionCount, - totalCount: groupBy == GroupAssetsBy.auto ? sectionCount : count, - offset: lastOffset + j, - title: j == 0 - ? (d.year == currentYear ? formatSameYear.format(d) : formatOtherYear.format(d)) - : (groupBy == GroupAssetsBy.auto ? formatDateRange(d, prevDate ?? d) : null), - ), - ); - } - monthCount += count; - } - - DateTime? prevDate; - while (true) { - // this iterates all assets (only their createdAt property) in batches - // memory usage is okay, however runtime is linear with number of assets - // TODO replace with groupBy once Isar supports such queries - final dates = assets != null - ? assets.map((a) => a.fileCreatedAt) - : await query!.offset(offset).limit(pageSize).fileCreatedAtProperty().findAll(); - int i = 0; - for (final date in dates) { - final d = DateTime(date.year, date.month, groupBy == GroupAssetsBy.month ? 1 : date.day); - current ??= d; - if (current != d) { - addElems(current, prevDate); - last = current; - current = d; - lastOffset = offset + i; - count = 0; - } - prevDate = date; - count++; - i++; - } - - if (assets != null || dates.length != pageSize) break; - offset += pageSize; - } - if (count > 0 && current != null) { - addElems(current, prevDate); - mergeMonth(); - } - assert(elements.every((e) => e.count <= sectionSize), "too large section"); - return RenderList(elements, query, assets); - } - - static RenderList empty() => RenderList([], null, []); - - static Future fromAssets(List assets, GroupAssetsBy groupBy) => - _buildRenderList(assets, null, groupBy); - - /// Deletes an asset from the render list and clears the buffer - /// This is only a workaround for deleted images still appearing in the gallery - void deleteAsset(Asset deleteAsset) { - allAssets?.remove(deleteAsset); - _buf.clear(); - _bufOffset = 0; - } -} - -class DateBatchLoader { - final QueryBuilder query; - final int batchSize; - - List _buffer = []; - int _bufferStart = 0; - - DateBatchLoader({required this.query, required this.batchSize}); - - Future getDate(int index) async { - if (!_isIndexInBuffer(index)) { - await _loadBatch(index); - } - - if (_isIndexInBuffer(index)) { - return _buffer[index - _bufferStart]; - } - - return null; - } - - Future _loadBatch(int targetIndex) async { - final batchStart = (targetIndex ~/ batchSize) * batchSize; - - _buffer = await query.offset(batchStart).limit(batchSize).fileCreatedAtProperty().findAll(); - - _bufferStart = batchStart; - } - - bool _isIndexInBuffer(int index) { - return index >= _bufferStart && index < _bufferStart + _buffer.length; - } -} diff --git a/mobile/lib/widgets/asset_grid/control_bottom_app_bar.dart b/mobile/lib/widgets/asset_grid/control_bottom_app_bar.dart deleted file mode 100644 index cd2dc70dae..0000000000 --- a/mobile/lib/widgets/asset_grid/control_bottom_app_bar.dart +++ /dev/null @@ -1,388 +0,0 @@ -import 'dart:io'; - -import 'package:easy_localization/easy_localization.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_hooks/flutter_hooks.dart'; -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/extensions/build_context_extensions.dart'; -import 'package:immich_mobile/providers/album/album.provider.dart'; -import 'package:immich_mobile/providers/routes.provider.dart'; -import 'package:immich_mobile/widgets/album/add_to_album_sliverlist.dart'; -import 'package:immich_mobile/widgets/album/add_to_album_bottom_sheet.dart'; -import 'package:immich_mobile/models/asset_selection_state.dart'; -import 'package:immich_mobile/widgets/asset_grid/delete_dialog.dart'; -import 'package:immich_mobile/widgets/asset_grid/upload_dialog.dart'; -import 'package:immich_mobile/providers/server_info.provider.dart'; -import 'package:immich_mobile/widgets/common/drag_sheet.dart'; -import 'package:immich_mobile/entities/album.entity.dart'; -import 'package:immich_mobile/entities/asset.entity.dart'; -import 'package:immich_mobile/utils/draggable_scroll_controller.dart'; - -final controlBottomAppBarNotifier = ControlBottomAppBarNotifier(); - -class ControlBottomAppBarNotifier with ChangeNotifier { - void minimize() { - notifyListeners(); - } -} - -class ControlBottomAppBar extends HookConsumerWidget { - final void Function(bool shareLocal) onShare; - final void Function()? onFavorite; - final void Function()? onArchive; - final void Function([bool force])? onDelete; - final void Function([bool force])? onDeleteServer; - final void Function(bool onlyBackedUp)? onDeleteLocal; - final Function(Album album) onAddToAlbum; - final void Function() onCreateNewAlbum; - final void Function() onUpload; - final void Function()? onStack; - final void Function()? onEditTime; - final void Function()? onEditLocation; - final void Function()? onRemoveFromAlbum; - final void Function()? onToggleLocked; - final void Function()? onDownload; - - final bool enabled; - final bool unfavorite; - final bool unarchive; - final AssetSelectionState selectionAssetState; - final List selectedAssets; - - const ControlBottomAppBar({ - super.key, - required this.onShare, - this.onFavorite, - this.onArchive, - this.onDelete, - this.onDeleteServer, - this.onDeleteLocal, - required this.onAddToAlbum, - required this.onCreateNewAlbum, - required this.onUpload, - this.onDownload, - this.onStack, - this.onEditTime, - this.onEditLocation, - this.onRemoveFromAlbum, - this.onToggleLocked, - this.selectionAssetState = const AssetSelectionState(), - this.selectedAssets = const [], - this.enabled = true, - this.unarchive = false, - this.unfavorite = false, - }); - - @override - Widget build(BuildContext context, WidgetRef ref) { - final hasRemote = selectionAssetState.hasRemote || selectionAssetState.hasMerged; - final hasLocal = selectionAssetState.hasLocal || selectionAssetState.hasMerged; - final trashEnabled = ref.watch(serverInfoProvider.select((v) => v.serverFeatures.trash)); - final albums = ref.watch(albumProvider).where((a) => a.isRemote).toList(); - final sharedAlbums = ref.watch(albumProvider).where((a) => a.shared).toList(); - const bottomPadding = 0.24; - final scrollController = useDraggableScrollController(); - final isInLockedView = ref.watch(inLockedViewProvider); - - void minimize() { - scrollController.animateTo(bottomPadding, duration: const Duration(milliseconds: 300), curve: Curves.easeOut); - } - - useEffect(() { - controlBottomAppBarNotifier.addListener(minimize); - return () { - controlBottomAppBarNotifier.removeListener(minimize); - }; - }, []); - - void showForceDeleteDialog(Function(bool) deleteCb, {String? alertMsg}) { - showDialog( - context: context, - builder: (BuildContext context) { - return DeleteDialog(alert: alertMsg, onDelete: () => deleteCb(true)); - }, - ); - } - - /// Show existing AddToAlbumBottomSheet - void showAddToAlbumBottomSheet() { - showModalBottomSheet( - elevation: 0, - shape: const RoundedRectangleBorder(borderRadius: BorderRadius.all(Radius.circular(15.0))), - context: context, - builder: (BuildContext _) { - return AddToAlbumBottomSheet(assets: selectedAssets); - }, - ); - } - - void handleRemoteDelete(bool force, Function(bool) deleteCb, {String? alertMsg}) { - if (!force) { - deleteCb(force); - return; - } - return showForceDeleteDialog(deleteCb, alertMsg: alertMsg); - } - - List renderActionButtons() { - return [ - ControlBoxButton( - iconData: Platform.isAndroid ? Icons.share_rounded : Icons.ios_share_rounded, - label: "share".tr(), - onPressed: enabled ? () => onShare(true) : null, - ), - if (!isInLockedView && hasRemote) - ControlBoxButton( - iconData: Icons.link_rounded, - label: "share_link".tr(), - onPressed: enabled ? () => onShare(false) : null, - ), - if (!isInLockedView && hasRemote && albums.isNotEmpty) - ConstrainedBox( - constraints: const BoxConstraints(maxWidth: 100), - child: ControlBoxButton( - iconData: Icons.photo_album, - label: "add_to_album".tr(), - onPressed: enabled ? showAddToAlbumBottomSheet : null, - ), - ), - if (hasRemote && onArchive != null) - ControlBoxButton( - iconData: unarchive ? Icons.unarchive_outlined : Icons.archive_outlined, - label: (unarchive ? "unarchive" : "archive").tr(), - onPressed: enabled ? onArchive : null, - ), - if (hasRemote && onFavorite != null) - ControlBoxButton( - iconData: unfavorite ? Icons.favorite_border_rounded : Icons.favorite_rounded, - label: (unfavorite ? "unfavorite" : "favorite").tr(), - onPressed: enabled ? onFavorite : null, - ), - if (hasRemote && onDownload != null) - ConstrainedBox( - constraints: const BoxConstraints(maxWidth: 90), - child: ControlBoxButton(iconData: Icons.download, label: "download".tr(), onPressed: onDownload), - ), - if (hasLocal && hasRemote && onDelete != null && !isInLockedView) - ConstrainedBox( - constraints: const BoxConstraints(maxWidth: 90), - child: ControlBoxButton( - iconData: Icons.delete_sweep_outlined, - label: "delete".tr(), - onPressed: enabled ? () => handleRemoteDelete(!trashEnabled, onDelete!) : null, - onLongPressed: enabled ? () => showForceDeleteDialog(onDelete!) : null, - ), - ), - if (hasRemote && onDeleteServer != null && !isInLockedView) - ConstrainedBox( - constraints: const BoxConstraints(maxWidth: 85), - child: ControlBoxButton( - iconData: Icons.cloud_off_outlined, - label: trashEnabled - ? "control_bottom_app_bar_trash_from_immich".tr() - : "control_bottom_app_bar_delete_from_immich".tr(), - onPressed: enabled - ? () => handleRemoteDelete(!trashEnabled, onDeleteServer!, alertMsg: "delete_dialog_alert_remote") - : null, - onLongPressed: enabled - ? () => showForceDeleteDialog(onDeleteServer!, alertMsg: "delete_dialog_alert_remote") - : null, - ), - ), - if (isInLockedView) - ConstrainedBox( - constraints: const BoxConstraints(maxWidth: 110), - child: ControlBoxButton( - iconData: Icons.delete_forever, - label: "delete_dialog_title".tr(), - onPressed: enabled - ? () => showForceDeleteDialog(onDeleteServer!, alertMsg: "delete_dialog_alert_remote") - : null, - ), - ), - if (hasLocal && onDeleteLocal != null && !isInLockedView) - ConstrainedBox( - constraints: const BoxConstraints(maxWidth: 95), - child: ControlBoxButton( - iconData: Icons.no_cell_outlined, - label: "control_bottom_app_bar_delete_from_local".tr(), - onPressed: enabled - ? () { - if (!selectionAssetState.hasLocal) { - return onDeleteLocal?.call(true); - } - - showDialog( - context: context, - builder: (BuildContext context) { - return DeleteLocalOnlyDialog(onDeleteLocal: onDeleteLocal!); - }, - ); - } - : null, - ), - ), - if (hasRemote && onEditTime != null) - ConstrainedBox( - constraints: const BoxConstraints(maxWidth: 95), - child: ControlBoxButton( - iconData: Icons.edit_calendar_outlined, - label: "control_bottom_app_bar_edit_time".tr(), - onPressed: enabled ? onEditTime : null, - ), - ), - if (hasRemote && onEditLocation != null) - ConstrainedBox( - constraints: const BoxConstraints(maxWidth: 90), - child: ControlBoxButton( - iconData: Icons.edit_location_alt_outlined, - label: "control_bottom_app_bar_edit_location".tr(), - onPressed: enabled ? onEditLocation : null, - ), - ), - if (hasRemote) - ConstrainedBox( - constraints: const BoxConstraints(maxWidth: 100), - child: ControlBoxButton( - iconData: isInLockedView ? Icons.lock_open_rounded : Icons.lock_outline_rounded, - label: isInLockedView ? "remove_from_locked_folder".tr() : "move_to_locked_folder".tr(), - onPressed: enabled ? onToggleLocked : null, - ), - ), - if (!selectionAssetState.hasLocal && selectionAssetState.selectedCount > 1 && onStack != null) - ConstrainedBox( - constraints: const BoxConstraints(maxWidth: 90), - child: ControlBoxButton( - iconData: Icons.filter_none_rounded, - label: "stack".tr(), - onPressed: enabled ? onStack : null, - ), - ), - if (onRemoveFromAlbum != null) - ConstrainedBox( - constraints: const BoxConstraints(maxWidth: 90), - child: ControlBoxButton( - iconData: Icons.remove_circle_outline, - label: 'remove_from_album'.tr(), - onPressed: enabled ? onRemoveFromAlbum : null, - ), - ), - if (selectionAssetState.hasLocal) - ControlBoxButton( - iconData: Icons.backup_outlined, - label: "upload".tr(), - onPressed: enabled - ? () => showDialog( - context: context, - builder: (BuildContext context) { - return UploadDialog(onUpload: onUpload); - }, - ) - : null, - ), - ]; - } - - getInitialSize() { - if (isInLockedView) { - return bottomPadding; - } - if (hasRemote) { - return 0.35; - } - return bottomPadding; - } - - getMaxChildSize() { - if (isInLockedView) { - return bottomPadding; - } - if (hasRemote) { - return 0.65; - } - return bottomPadding; - } - - return DraggableScrollableSheet( - initialChildSize: getInitialSize(), - minChildSize: bottomPadding, - maxChildSize: getMaxChildSize(), - snap: true, - controller: scrollController, - builder: (BuildContext context, ScrollController scrollController) { - return Card( - color: context.colorScheme.surfaceContainerHigh, - surfaceTintColor: context.colorScheme.surfaceContainerHigh, - elevation: 6.0, - shape: const RoundedRectangleBorder( - borderRadius: BorderRadius.only(topLeft: Radius.circular(12), topRight: Radius.circular(12)), - ), - margin: const EdgeInsets.all(0), - child: CustomScrollView( - controller: scrollController, - slivers: [ - SliverToBoxAdapter( - child: Column( - children: [ - const SizedBox(height: 12), - const CustomDraggingHandle(), - const SizedBox(height: 12), - SizedBox( - height: 120, - child: ListView( - shrinkWrap: true, - scrollDirection: Axis.horizontal, - children: renderActionButtons(), - ), - ), - if (hasRemote && !isInLockedView) ...[ - const Divider(indent: 16, endIndent: 16, thickness: 1), - _AddToAlbumTitleRow(onCreateNewAlbum: enabled ? onCreateNewAlbum : null), - ], - ], - ), - ), - if (hasRemote && !isInLockedView) - SliverPadding( - padding: const EdgeInsets.symmetric(horizontal: 16), - sliver: AddToAlbumSliverList( - albums: albums, - sharedAlbums: sharedAlbums, - onAddToAlbum: onAddToAlbum, - enabled: enabled, - ), - ), - ], - ), - ); - }, - ); - } -} - -class _AddToAlbumTitleRow extends StatelessWidget { - const _AddToAlbumTitleRow({required this.onCreateNewAlbum}); - - final VoidCallback? onCreateNewAlbum; - - @override - Widget build(BuildContext context) { - return Padding( - padding: const EdgeInsets.symmetric(horizontal: 16), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Text("add_to_album", style: context.textTheme.titleSmall).tr(), - TextButton.icon( - onPressed: onCreateNewAlbum, - icon: Icon(Icons.add, color: context.primaryColor), - label: Text( - "common_create_new_album", - style: TextStyle(color: context.primaryColor, fontWeight: FontWeight.bold, fontSize: 14), - ).tr(), - ), - ], - ), - ); - } -} diff --git a/mobile/lib/widgets/asset_grid/delete_dialog.dart b/mobile/lib/widgets/asset_grid/delete_dialog.dart index adb22889a8..ff5aac617a 100644 --- a/mobile/lib/widgets/asset_grid/delete_dialog.dart +++ b/mobile/lib/widgets/asset_grid/delete_dialog.dart @@ -1,18 +1,6 @@ import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:immich_mobile/extensions/build_context_extensions.dart'; -import 'package:immich_mobile/widgets/common/confirm_dialog.dart'; - -class DeleteDialog extends ConfirmDialog { - const DeleteDialog({super.key, String? alert, required Function onDelete}) - : super( - title: "delete_dialog_title", - content: alert ?? "delete_dialog_alert", - cancel: "cancel", - ok: "delete", - onOk: onDelete, - ); -} class DeleteLocalOnlyDialog extends StatelessWidget { final void Function(bool onlyMerged) onDeleteLocal; diff --git a/mobile/lib/widgets/asset_grid/disable_multi_select_button.dart b/mobile/lib/widgets/asset_grid/disable_multi_select_button.dart deleted file mode 100644 index 93a1d53f4e..0000000000 --- a/mobile/lib/widgets/asset_grid/disable_multi_select_button.dart +++ /dev/null @@ -1,31 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/extensions/build_context_extensions.dart'; - -class DisableMultiSelectButton extends ConsumerWidget { - const DisableMultiSelectButton({super.key, required this.onPressed, required this.selectedItemCount}); - - final Function onPressed; - final int selectedItemCount; - - @override - Widget build(BuildContext context, WidgetRef ref) { - return Align( - alignment: Alignment.topLeft, - child: Padding( - padding: const EdgeInsets.only(left: 16.0, top: 8.0), - child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 4.0), - child: ElevatedButton.icon( - onPressed: () => onPressed(), - icon: Icon(Icons.close_rounded, color: context.colorScheme.onPrimary), - label: Text( - '$selectedItemCount', - style: context.textTheme.titleMedium?.copyWith(height: 2.5, color: context.colorScheme.onPrimary), - ), - ), - ), - ), - ); - } -} diff --git a/mobile/lib/widgets/asset_grid/draggable_scrollbar.dart b/mobile/lib/widgets/asset_grid/draggable_scrollbar.dart deleted file mode 100644 index 3de52c2816..0000000000 --- a/mobile/lib/widgets/asset_grid/draggable_scrollbar.dart +++ /dev/null @@ -1,559 +0,0 @@ -import 'dart:async'; - -import 'package:flutter/material.dart'; - -/// Build the Scroll Thumb and label using the current configuration -typedef ScrollThumbBuilder = - Widget Function( - Color backgroundColor, - Animation thumbAnimation, - Animation labelAnimation, - double height, { - Text? labelText, - BoxConstraints? labelConstraints, - }); - -/// Build a Text widget using the current scroll offset -typedef LabelTextBuilder = Text Function(double offsetY); - -/// A widget that will display a BoxScrollView with a ScrollThumb that can be dragged -/// for quick navigation of the BoxScrollView. -class DraggableScrollbar extends StatefulWidget { - /// The view that will be scrolled with the scroll thumb - final CustomScrollView child; - - /// A function that builds a thumb using the current configuration - final ScrollThumbBuilder scrollThumbBuilder; - - /// The height of the scroll thumb - final double heightScrollThumb; - - /// The background color of the label and thumb - final Color backgroundColor; - - /// The amount of padding that should surround the thumb - final EdgeInsetsGeometry? padding; - - /// Determines how quickly the scrollbar will animate in and out - final Duration scrollbarAnimationDuration; - - /// How long should the thumb be visible before fading out - final Duration scrollbarTimeToFade; - - /// Build a Text widget from the current offset in the BoxScrollView - final LabelTextBuilder? labelTextBuilder; - - /// Determines box constraints for Container displaying label - final BoxConstraints? labelConstraints; - - /// The ScrollController for the BoxScrollView - final ScrollController controller; - - /// Determines scrollThumb displaying. If you draw own ScrollThumb and it is true you just don't need to use animation parameters in [scrollThumbBuilder] - final bool alwaysVisibleScrollThumb; - - DraggableScrollbar({ - super.key, - this.alwaysVisibleScrollThumb = false, - required this.heightScrollThumb, - required this.backgroundColor, - required this.scrollThumbBuilder, - required this.child, - required this.controller, - this.padding, - this.scrollbarAnimationDuration = const Duration(milliseconds: 300), - this.scrollbarTimeToFade = const Duration(milliseconds: 600), - this.labelTextBuilder, - this.labelConstraints, - }) : assert(child.scrollDirection == Axis.vertical); - - DraggableScrollbar.rrect({ - super.key, - Key? scrollThumbKey, - this.alwaysVisibleScrollThumb = false, - required this.child, - required this.controller, - this.heightScrollThumb = 48.0, - this.backgroundColor = Colors.white, - this.padding, - this.scrollbarAnimationDuration = const Duration(milliseconds: 300), - this.scrollbarTimeToFade = const Duration(milliseconds: 600), - this.labelTextBuilder, - this.labelConstraints, - }) : assert(child.scrollDirection == Axis.vertical), - scrollThumbBuilder = _thumbRRectBuilder(alwaysVisibleScrollThumb); - - DraggableScrollbar.arrows({ - super.key, - Key? scrollThumbKey, - this.alwaysVisibleScrollThumb = false, - required this.child, - required this.controller, - this.heightScrollThumb = 48.0, - this.backgroundColor = Colors.white, - this.padding, - this.scrollbarAnimationDuration = const Duration(milliseconds: 300), - this.scrollbarTimeToFade = const Duration(milliseconds: 600), - this.labelTextBuilder, - this.labelConstraints, - }) : assert(child.scrollDirection == Axis.vertical), - scrollThumbBuilder = _thumbArrowBuilder(alwaysVisibleScrollThumb); - - DraggableScrollbar.semicircle({ - super.key, - Key? scrollThumbKey, - this.alwaysVisibleScrollThumb = false, - required this.child, - required this.controller, - this.heightScrollThumb = 48.0, - this.backgroundColor = Colors.white, - this.padding, - this.scrollbarAnimationDuration = const Duration(milliseconds: 300), - this.scrollbarTimeToFade = const Duration(milliseconds: 600), - this.labelTextBuilder, - this.labelConstraints, - }) : assert(child.scrollDirection == Axis.vertical), - scrollThumbBuilder = _thumbSemicircleBuilder(heightScrollThumb * 0.6, scrollThumbKey, alwaysVisibleScrollThumb); - - @override - DraggableScrollbarState createState() => DraggableScrollbarState(); - - static buildScrollThumbAndLabel({ - required Widget scrollThumb, - required Color backgroundColor, - required Animation? thumbAnimation, - required Animation? labelAnimation, - required Text? labelText, - required BoxConstraints? labelConstraints, - required bool alwaysVisibleScrollThumb, - }) { - var scrollThumbAndLabel = labelText == null - ? scrollThumb - : Row( - mainAxisSize: MainAxisSize.min, - mainAxisAlignment: MainAxisAlignment.end, - children: [ - ScrollLabel( - animation: labelAnimation, - backgroundColor: backgroundColor, - constraints: labelConstraints, - child: labelText, - ), - scrollThumb, - ], - ); - - if (alwaysVisibleScrollThumb) { - return scrollThumbAndLabel; - } - return SlideFadeTransition(animation: thumbAnimation!, child: scrollThumbAndLabel); - } - - static ScrollThumbBuilder _thumbSemicircleBuilder(double width, Key? scrollThumbKey, bool alwaysVisibleScrollThumb) { - return ( - Color backgroundColor, - Animation thumbAnimation, - Animation labelAnimation, - double height, { - Text? labelText, - BoxConstraints? labelConstraints, - }) { - final scrollThumb = CustomPaint( - key: scrollThumbKey, - foregroundPainter: ArrowCustomPainter(Colors.white), - child: Material( - elevation: 4.0, - color: backgroundColor, - borderRadius: BorderRadius.only( - topLeft: Radius.circular(height), - bottomLeft: Radius.circular(height), - topRight: const Radius.circular(4.0), - bottomRight: const Radius.circular(4.0), - ), - child: Container(constraints: BoxConstraints.tight(Size(width, height))), - ), - ); - - return buildScrollThumbAndLabel( - scrollThumb: scrollThumb, - backgroundColor: backgroundColor, - thumbAnimation: thumbAnimation, - labelAnimation: labelAnimation, - labelText: labelText, - labelConstraints: labelConstraints, - alwaysVisibleScrollThumb: alwaysVisibleScrollThumb, - ); - }; - } - - static ScrollThumbBuilder _thumbArrowBuilder(bool alwaysVisibleScrollThumb) { - return ( - Color backgroundColor, - Animation thumbAnimation, - Animation labelAnimation, - double height, { - Text? labelText, - BoxConstraints? labelConstraints, - }) { - final scrollThumb = ClipPath( - clipper: const ArrowClipper(), - child: Container( - height: height, - width: 20.0, - decoration: BoxDecoration( - color: backgroundColor, - borderRadius: const BorderRadius.all(Radius.circular(12.0)), - ), - ), - ); - - return buildScrollThumbAndLabel( - scrollThumb: scrollThumb, - backgroundColor: backgroundColor, - thumbAnimation: thumbAnimation, - labelAnimation: labelAnimation, - labelText: labelText, - labelConstraints: labelConstraints, - alwaysVisibleScrollThumb: alwaysVisibleScrollThumb, - ); - }; - } - - static ScrollThumbBuilder _thumbRRectBuilder(bool alwaysVisibleScrollThumb) { - return ( - Color backgroundColor, - Animation thumbAnimation, - Animation labelAnimation, - double height, { - Text? labelText, - BoxConstraints? labelConstraints, - }) { - final scrollThumb = Material( - elevation: 4.0, - color: backgroundColor, - borderRadius: const BorderRadius.all(Radius.circular(7.0)), - child: Container(constraints: BoxConstraints.tight(Size(16.0, height))), - ); - - return buildScrollThumbAndLabel( - scrollThumb: scrollThumb, - backgroundColor: backgroundColor, - thumbAnimation: thumbAnimation, - labelAnimation: labelAnimation, - labelText: labelText, - labelConstraints: labelConstraints, - alwaysVisibleScrollThumb: alwaysVisibleScrollThumb, - ); - }; - } -} - -class ScrollLabel extends StatelessWidget { - final Animation? animation; - final Color backgroundColor; - final Text child; - - final BoxConstraints? constraints; - static const BoxConstraints _defaultConstraints = BoxConstraints.tightFor(width: 72.0, height: 28.0); - - const ScrollLabel({ - super.key, - required this.child, - required this.animation, - required this.backgroundColor, - this.constraints = _defaultConstraints, - }); - - @override - Widget build(BuildContext context) { - return FadeTransition( - opacity: animation!, - child: Container( - margin: const EdgeInsets.only(right: 12.0), - child: Material( - elevation: 4.0, - color: backgroundColor, - borderRadius: const BorderRadius.all(Radius.circular(16.0)), - child: Container(constraints: constraints ?? _defaultConstraints, alignment: Alignment.center, child: child), - ), - ), - ); - } -} - -class DraggableScrollbarState extends State with TickerProviderStateMixin { - late double _barOffset; - late double _viewOffset; - late bool _isDragInProcess; - - late AnimationController _thumbAnimationController; - late Animation _thumbAnimation; - late AnimationController _labelAnimationController; - late Animation _labelAnimation; - Timer? _fadeoutTimer; - - @override - void initState() { - super.initState(); - _barOffset = 0.0; - _viewOffset = 0.0; - _isDragInProcess = false; - - _thumbAnimationController = AnimationController(vsync: this, duration: widget.scrollbarAnimationDuration); - - _thumbAnimation = CurvedAnimation(parent: _thumbAnimationController, curve: Curves.fastOutSlowIn); - - _labelAnimationController = AnimationController(vsync: this, duration: widget.scrollbarAnimationDuration); - - _labelAnimation = CurvedAnimation(parent: _labelAnimationController, curve: Curves.fastOutSlowIn); - } - - @override - void dispose() { - _thumbAnimationController.dispose(); - _labelAnimationController.dispose(); - _fadeoutTimer?.cancel(); - super.dispose(); - } - - double get barMaxScrollExtent => context.size!.height - widget.heightScrollThumb; - - double get barMinScrollExtent => 0; - - double get viewMaxScrollExtent => widget.controller.position.maxScrollExtent; - - double get viewMinScrollExtent => widget.controller.position.minScrollExtent; - - @override - Widget build(BuildContext context) { - Text? labelText; - if (widget.labelTextBuilder != null && _isDragInProcess) { - labelText = widget.labelTextBuilder!(_viewOffset + _barOffset + widget.heightScrollThumb / 2); - } - - return LayoutBuilder( - builder: (BuildContext context, BoxConstraints constraints) { - //print("LayoutBuilder constraints=$constraints"); - - return NotificationListener( - onNotification: (ScrollNotification notification) { - changePosition(notification); - return false; - }, - child: Stack( - children: [ - RepaintBoundary(child: widget.child), - RepaintBoundary( - child: GestureDetector( - onVerticalDragStart: _onVerticalDragStart, - onVerticalDragUpdate: _onVerticalDragUpdate, - onVerticalDragEnd: _onVerticalDragEnd, - child: Container( - alignment: Alignment.topRight, - margin: EdgeInsets.only(top: _barOffset), - padding: widget.padding, - child: widget.scrollThumbBuilder( - widget.backgroundColor, - _thumbAnimation, - _labelAnimation, - widget.heightScrollThumb, - labelText: labelText, - labelConstraints: widget.labelConstraints, - ), - ), - ), - ), - ], - ), - ); - }, - ); - } - - //scroll bar has received notification that it's view was scrolled - //so it should also changes his position - //but only if it isn't dragged - changePosition(ScrollNotification notification) { - if (_isDragInProcess) { - return; - } - - setState(() { - if (notification is ScrollUpdateNotification) { - _barOffset += getBarDelta(notification.scrollDelta!, barMaxScrollExtent, viewMaxScrollExtent); - - if (_barOffset < barMinScrollExtent) { - _barOffset = barMinScrollExtent; - } - if (_barOffset > barMaxScrollExtent) { - _barOffset = barMaxScrollExtent; - } - - _viewOffset += notification.scrollDelta!; - if (_viewOffset < widget.controller.position.minScrollExtent) { - _viewOffset = widget.controller.position.minScrollExtent; - } - if (_viewOffset > viewMaxScrollExtent) { - _viewOffset = viewMaxScrollExtent; - } - } - - if (notification is ScrollUpdateNotification || notification is OverscrollNotification) { - if (_thumbAnimationController.status != AnimationStatus.forward) { - _thumbAnimationController.forward(); - } - - _fadeoutTimer?.cancel(); - _fadeoutTimer = Timer(widget.scrollbarTimeToFade, () { - _thumbAnimationController.reverse(); - _labelAnimationController.reverse(); - _fadeoutTimer = null; - }); - } - }); - } - - double getBarDelta(double scrollViewDelta, double barMaxScrollExtent, double viewMaxScrollExtent) { - return scrollViewDelta * barMaxScrollExtent / viewMaxScrollExtent; - } - - double getScrollViewDelta(double barDelta, double barMaxScrollExtent, double viewMaxScrollExtent) { - return barDelta * viewMaxScrollExtent / barMaxScrollExtent; - } - - void _onVerticalDragStart(DragStartDetails details) { - setState(() { - _isDragInProcess = true; - _labelAnimationController.forward(); - _fadeoutTimer?.cancel(); - }); - } - - void _onVerticalDragUpdate(DragUpdateDetails details) { - setState(() { - if (_thumbAnimationController.status != AnimationStatus.forward) { - _thumbAnimationController.forward(); - } - if (_isDragInProcess) { - _barOffset += details.delta.dy; - - if (_barOffset < barMinScrollExtent) { - _barOffset = barMinScrollExtent; - } - if (_barOffset > barMaxScrollExtent) { - _barOffset = barMaxScrollExtent; - } - - double viewDelta = getScrollViewDelta(details.delta.dy, barMaxScrollExtent, viewMaxScrollExtent); - - _viewOffset = widget.controller.position.pixels + viewDelta; - if (_viewOffset < widget.controller.position.minScrollExtent) { - _viewOffset = widget.controller.position.minScrollExtent; - } - if (_viewOffset > viewMaxScrollExtent) { - _viewOffset = viewMaxScrollExtent; - } - widget.controller.jumpTo(_viewOffset); - } - }); - } - - void _onVerticalDragEnd(DragEndDetails details) { - _fadeoutTimer = Timer(widget.scrollbarTimeToFade, () { - _thumbAnimationController.reverse(); - _labelAnimationController.reverse(); - _fadeoutTimer = null; - }); - setState(() { - _isDragInProcess = false; - }); - } -} - -/// Draws 2 triangles like arrow up and arrow down -class ArrowCustomPainter extends CustomPainter { - Color color; - - ArrowCustomPainter(this.color); - - @override - bool shouldRepaint(covariant CustomPainter oldDelegate) => false; - - @override - void paint(Canvas canvas, Size size) { - final paint = Paint()..color = color; - const width = 12.0; - const height = 8.0; - final baseX = size.width / 2; - final baseY = size.height / 2; - - canvas.drawPath(_trianglePath(Offset(baseX, baseY - 2.0), width, height, true), paint); - canvas.drawPath(_trianglePath(Offset(baseX, baseY + 2.0), width, height, false), paint); - } - - static Path _trianglePath(Offset o, double width, double height, bool isUp) { - return Path() - ..moveTo(o.dx, o.dy) - ..lineTo(o.dx + width, o.dy) - ..lineTo(o.dx + (width / 2), isUp ? o.dy - height : o.dy + height) - ..close(); - } -} - -///This cut 2 lines in arrow shape -class ArrowClipper extends CustomClipper { - const ArrowClipper(); - @override - Path getClip(Size size) { - Path path = Path(); - path.lineTo(0.0, size.height); - path.lineTo(size.width, size.height); - path.lineTo(size.width, 0.0); - path.lineTo(0.0, 0.0); - path.close(); - - double arrowWidth = 8.0; - double startPointX = (size.width - arrowWidth) / 2; - double startPointY = size.height / 2 - arrowWidth / 2; - path.moveTo(startPointX, startPointY); - path.lineTo(startPointX + arrowWidth / 2, startPointY - arrowWidth / 2); - path.lineTo(startPointX + arrowWidth, startPointY); - path.lineTo(startPointX + arrowWidth, startPointY + 1.0); - path.lineTo(startPointX + arrowWidth / 2, startPointY - arrowWidth / 2 + 1.0); - path.lineTo(startPointX, startPointY + 1.0); - path.close(); - - startPointY = size.height / 2 + arrowWidth / 2; - path.moveTo(startPointX + arrowWidth, startPointY); - path.lineTo(startPointX + arrowWidth / 2, startPointY + arrowWidth / 2); - path.lineTo(startPointX, startPointY); - path.lineTo(startPointX, startPointY - 1.0); - path.lineTo(startPointX + arrowWidth / 2, startPointY + arrowWidth / 2 - 1.0); - path.lineTo(startPointX + arrowWidth, startPointY - 1.0); - path.close(); - - return path; - } - - @override - bool shouldReclip(CustomClipper oldClipper) => false; -} - -class SlideFadeTransition extends StatelessWidget { - final Animation animation; - final Widget child; - - const SlideFadeTransition({super.key, required this.animation, required this.child}); - - @override - Widget build(BuildContext context) { - return AnimatedBuilder( - animation: animation, - builder: (context, child) => animation.value == 0.0 ? const SizedBox() : child!, - child: SlideTransition( - position: Tween(begin: const Offset(0.3, 0.0), end: const Offset(0.0, 0.0)).animate(animation), - child: FadeTransition(opacity: animation, child: child), - ), - ); - } -} diff --git a/mobile/lib/widgets/asset_grid/draggable_scrollbar_custom.dart b/mobile/lib/widgets/asset_grid/draggable_scrollbar_custom.dart deleted file mode 100644 index 17f35311f0..0000000000 --- a/mobile/lib/widgets/asset_grid/draggable_scrollbar_custom.dart +++ /dev/null @@ -1,490 +0,0 @@ -import 'dart:async'; - -import 'package:flutter/material.dart'; -import 'package:scrollable_positioned_list/scrollable_positioned_list.dart'; - -/// Build the Scroll Thumb and label using the current configuration -typedef ScrollThumbBuilder = - Widget Function( - Color backgroundColor, - Animation thumbAnimation, - Animation labelAnimation, - double height, { - Text? labelText, - BoxConstraints? labelConstraints, - }); - -/// Build a Text widget using the current scroll offset -typedef LabelTextBuilder = Text Function(int item); - -/// A widget that will display a BoxScrollView with a ScrollThumb that can be dragged -/// for quick navigation of the BoxScrollView. -class DraggableScrollbar extends StatefulWidget { - /// The view that will be scrolled with the scroll thumb - final ScrollablePositionedList child; - - final ItemPositionsListener itemPositionsListener; - - /// A function that builds a thumb using the current configuration - final ScrollThumbBuilder scrollThumbBuilder; - - /// The height of the scroll thumb - final double heightScrollThumb; - - /// The background color of the label and thumb - final Color backgroundColor; - - /// The amount of padding that should surround the thumb - final EdgeInsetsGeometry? padding; - - /// The height offset of the thumb/bar from the bottom of the page - final double? heightOffset; - - /// Determines how quickly the scrollbar will animate in and out - final Duration scrollbarAnimationDuration; - - /// How long should the thumb be visible before fading out - final Duration scrollbarTimeToFade; - - /// Build a Text widget from the current offset in the BoxScrollView - final LabelTextBuilder? labelTextBuilder; - - /// Determines box constraints for Container displaying label - final BoxConstraints? labelConstraints; - - /// The ScrollController for the BoxScrollView - final ItemScrollController controller; - - /// Determines scrollThumb displaying. If you draw own ScrollThumb and it is true you just don't need to use animation parameters in [scrollThumbBuilder] - final bool alwaysVisibleScrollThumb; - - final Function(bool scrolling) scrollStateListener; - - DraggableScrollbar.semicircle({ - super.key, - Key? scrollThumbKey, - this.alwaysVisibleScrollThumb = false, - required this.child, - required this.controller, - required this.itemPositionsListener, - required this.scrollStateListener, - this.heightScrollThumb = 48.0, - this.backgroundColor = Colors.white, - this.padding, - this.heightOffset, - this.scrollbarAnimationDuration = const Duration(milliseconds: 300), - this.scrollbarTimeToFade = const Duration(milliseconds: 600), - this.labelTextBuilder, - this.labelConstraints, - }) : assert(child.scrollDirection == Axis.vertical), - scrollThumbBuilder = _thumbSemicircleBuilder(heightScrollThumb * 0.6, scrollThumbKey, alwaysVisibleScrollThumb); - - @override - DraggableScrollbarState createState() => DraggableScrollbarState(); - - static buildScrollThumbAndLabel({ - required Widget scrollThumb, - required Color backgroundColor, - required Animation? thumbAnimation, - required Animation? labelAnimation, - required Text? labelText, - required BoxConstraints? labelConstraints, - required bool alwaysVisibleScrollThumb, - }) { - var scrollThumbAndLabel = labelText == null - ? scrollThumb - : Row( - mainAxisSize: MainAxisSize.min, - mainAxisAlignment: MainAxisAlignment.end, - children: [ - ScrollLabel( - animation: labelAnimation, - backgroundColor: backgroundColor, - constraints: labelConstraints, - child: labelText, - ), - scrollThumb, - ], - ); - - if (alwaysVisibleScrollThumb) { - return scrollThumbAndLabel; - } - return SlideFadeTransition(animation: thumbAnimation!, child: scrollThumbAndLabel); - } - - static ScrollThumbBuilder _thumbSemicircleBuilder(double width, Key? scrollThumbKey, bool alwaysVisibleScrollThumb) { - return ( - Color backgroundColor, - Animation thumbAnimation, - Animation labelAnimation, - double height, { - Text? labelText, - BoxConstraints? labelConstraints, - }) { - final scrollThumb = CustomPaint( - key: scrollThumbKey, - foregroundPainter: ArrowCustomPainter(Colors.white), - child: Material( - elevation: 4.0, - color: backgroundColor, - borderRadius: BorderRadius.only( - topLeft: Radius.circular(height), - bottomLeft: Radius.circular(height), - topRight: const Radius.circular(4.0), - bottomRight: const Radius.circular(4.0), - ), - child: Container(constraints: BoxConstraints.tight(Size(width, height))), - ), - ); - - return buildScrollThumbAndLabel( - scrollThumb: scrollThumb, - backgroundColor: backgroundColor, - thumbAnimation: thumbAnimation, - labelAnimation: labelAnimation, - labelText: labelText, - labelConstraints: labelConstraints, - alwaysVisibleScrollThumb: alwaysVisibleScrollThumb, - ); - }; - } -} - -class ScrollLabel extends StatelessWidget { - final Animation? animation; - final Color backgroundColor; - final Text child; - - final BoxConstraints? constraints; - static const BoxConstraints _defaultConstraints = BoxConstraints.tightFor(width: 72.0, height: 28.0); - - const ScrollLabel({ - super.key, - required this.child, - required this.animation, - required this.backgroundColor, - this.constraints = _defaultConstraints, - }); - - @override - Widget build(BuildContext context) { - return FadeTransition( - opacity: animation!, - child: Container( - margin: const EdgeInsets.only(right: 12.0), - child: Material( - elevation: 4.0, - color: backgroundColor, - borderRadius: const BorderRadius.all(Radius.circular(16.0)), - child: Container( - constraints: constraints ?? _defaultConstraints, - padding: const EdgeInsets.symmetric(horizontal: 10.0), - alignment: Alignment.center, - child: child, - ), - ), - ), - ); - } -} - -class DraggableScrollbarState extends State with TickerProviderStateMixin { - late double _barOffset; - late bool _isDragInProcess; - late int _currentItem; - - late AnimationController _thumbAnimationController; - late Animation _thumbAnimation; - late AnimationController _labelAnimationController; - late Animation _labelAnimation; - Timer? _fadeoutTimer; - - @override - void initState() { - super.initState(); - _barOffset = 0.0; - _isDragInProcess = false; - _currentItem = 0; - - _thumbAnimationController = AnimationController(vsync: this, duration: widget.scrollbarAnimationDuration); - - _thumbAnimation = CurvedAnimation(parent: _thumbAnimationController, curve: Curves.fastOutSlowIn); - - _labelAnimationController = AnimationController(vsync: this, duration: widget.scrollbarAnimationDuration); - - _labelAnimation = CurvedAnimation(parent: _labelAnimationController, curve: Curves.fastOutSlowIn); - } - - @override - void dispose() { - _thumbAnimationController.dispose(); - _labelAnimationController.dispose(); - _fadeoutTimer?.cancel(); - super.dispose(); - } - - double get barMaxScrollExtent => (context.size?.height ?? 0) - widget.heightScrollThumb - (widget.heightOffset ?? 0); - - double get barMinScrollExtent => 0; - - int get maxItemCount => widget.child.itemCount; - - @override - Widget build(BuildContext context) { - Text? labelText; - if (widget.labelTextBuilder != null && _isDragInProcess) { - labelText = widget.labelTextBuilder!(_currentItem); - } - - return LayoutBuilder( - builder: (BuildContext context, BoxConstraints constraints) { - //print("LayoutBuilder constraints=$constraints"); - - return NotificationListener( - onNotification: (ScrollNotification notification) { - changePosition(notification); - return false; - }, - child: Stack( - children: [ - RepaintBoundary(child: widget.child), - RepaintBoundary( - child: GestureDetector( - onVerticalDragStart: _onVerticalDragStart, - onVerticalDragUpdate: _onVerticalDragUpdate, - onVerticalDragEnd: _onVerticalDragEnd, - child: Container( - alignment: Alignment.topRight, - margin: EdgeInsets.only(top: _barOffset), - padding: widget.padding, - child: widget.scrollThumbBuilder( - widget.backgroundColor, - _thumbAnimation, - _labelAnimation, - widget.heightScrollThumb, - labelText: labelText, - labelConstraints: widget.labelConstraints, - ), - ), - ), - ), - ], - ), - ); - }, - ); - } - - // scroll bar has received notification that it's view was scrolled - // so it should also changes his position - // but only if it isn't dragged - changePosition(ScrollNotification notification) { - if (_isDragInProcess) { - return; - } - - setState(() { - try { - int firstItemIndex = widget.itemPositionsListener.itemPositions.value.first.index; - - if (notification is ScrollUpdateNotification) { - _barOffset = (firstItemIndex / maxItemCount) * barMaxScrollExtent; - - if (_barOffset < barMinScrollExtent) { - _barOffset = barMinScrollExtent; - } - if (_barOffset > barMaxScrollExtent) { - _barOffset = barMaxScrollExtent; - } - } - - if (notification is ScrollUpdateNotification || notification is OverscrollNotification) { - if (_thumbAnimationController.status != AnimationStatus.forward) { - _thumbAnimationController.forward(); - } - - if (itemPosition < maxItemCount) { - _currentItem = itemPosition; - } - - _fadeoutTimer?.cancel(); - _fadeoutTimer = Timer(widget.scrollbarTimeToFade, () { - _thumbAnimationController.reverse(); - _labelAnimationController.reverse(); - _fadeoutTimer = null; - }); - } - } catch (_) {} - }); - } - - void _onVerticalDragStart(DragStartDetails details) { - setState(() { - _isDragInProcess = true; - _labelAnimationController.forward(); - _fadeoutTimer?.cancel(); - }); - - widget.scrollStateListener(true); - } - - int get itemPosition { - int numberOfItems = widget.child.itemCount; - return ((_barOffset / barMaxScrollExtent) * numberOfItems).toInt(); - } - - void _jumpToBarPosition() { - if (itemPosition > maxItemCount - 1) { - return; - } - - _currentItem = itemPosition; - - /// If the bar is at the bottom but the item position is still smaller than the max item count (due to rounding error) - /// jump to the end of the list - if (barMaxScrollExtent - _barOffset < 10 && itemPosition < maxItemCount) { - widget.controller.jumpTo(index: maxItemCount); - - return; - } - - widget.controller.jumpTo(index: itemPosition); - } - - Timer? dragHaltTimer; - int lastTimerPosition = 0; - - void _onVerticalDragUpdate(DragUpdateDetails details) { - setState(() { - if (_thumbAnimationController.status != AnimationStatus.forward) { - _thumbAnimationController.forward(); - } - if (_isDragInProcess) { - _barOffset += details.delta.dy; - - if (_barOffset < barMinScrollExtent) { - _barOffset = barMinScrollExtent; - } - if (_barOffset > barMaxScrollExtent) { - _barOffset = barMaxScrollExtent; - } - - if (itemPosition != lastTimerPosition) { - lastTimerPosition = itemPosition; - dragHaltTimer?.cancel(); - widget.scrollStateListener(true); - - dragHaltTimer = Timer(const Duration(milliseconds: 500), () { - widget.scrollStateListener(false); - }); - } - - _jumpToBarPosition(); - } - }); - } - - void _onVerticalDragEnd(DragEndDetails details) { - _fadeoutTimer = Timer(widget.scrollbarTimeToFade, () { - _thumbAnimationController.reverse(); - _labelAnimationController.reverse(); - _fadeoutTimer = null; - }); - - setState(() { - _jumpToBarPosition(); - _isDragInProcess = false; - }); - - widget.scrollStateListener(false); - } -} - -/// Draws 2 triangles like arrow up and arrow down -class ArrowCustomPainter extends CustomPainter { - Color color; - - ArrowCustomPainter(this.color); - - @override - bool shouldRepaint(covariant CustomPainter oldDelegate) => false; - - @override - void paint(Canvas canvas, Size size) { - final paint = Paint()..color = color; - const width = 12.0; - const height = 8.0; - final baseX = size.width / 2; - final baseY = size.height / 2; - - canvas.drawPath(_trianglePath(Offset(baseX, baseY - 2.0), width, height, true), paint); - canvas.drawPath(_trianglePath(Offset(baseX, baseY + 2.0), width, height, false), paint); - } - - static Path _trianglePath(Offset o, double width, double height, bool isUp) { - return Path() - ..moveTo(o.dx, o.dy) - ..lineTo(o.dx + width, o.dy) - ..lineTo(o.dx + (width / 2), isUp ? o.dy - height : o.dy + height) - ..close(); - } -} - -///This cut 2 lines in arrow shape -class ArrowClipper extends CustomClipper { - const ArrowClipper(); - @override - Path getClip(Size size) { - Path path = Path(); - path.lineTo(0.0, size.height); - path.lineTo(size.width, size.height); - path.lineTo(size.width, 0.0); - path.lineTo(0.0, 0.0); - path.close(); - - double arrowWidth = 8.0; - double startPointX = (size.width - arrowWidth) / 2; - double startPointY = size.height / 2 - arrowWidth / 2; - path.moveTo(startPointX, startPointY); - path.lineTo(startPointX + arrowWidth / 2, startPointY - arrowWidth / 2); - path.lineTo(startPointX + arrowWidth, startPointY); - path.lineTo(startPointX + arrowWidth, startPointY + 1.0); - path.lineTo(startPointX + arrowWidth / 2, startPointY - arrowWidth / 2 + 1.0); - path.lineTo(startPointX, startPointY + 1.0); - path.close(); - - startPointY = size.height / 2 + arrowWidth / 2; - path.moveTo(startPointX + arrowWidth, startPointY); - path.lineTo(startPointX + arrowWidth / 2, startPointY + arrowWidth / 2); - path.lineTo(startPointX, startPointY); - path.lineTo(startPointX, startPointY - 1.0); - path.lineTo(startPointX + arrowWidth / 2, startPointY + arrowWidth / 2 - 1.0); - path.lineTo(startPointX + arrowWidth, startPointY - 1.0); - path.close(); - - return path; - } - - @override - bool shouldReclip(CustomClipper oldClipper) => false; -} - -class SlideFadeTransition extends StatelessWidget { - final Animation animation; - final Widget child; - - const SlideFadeTransition({super.key, required this.animation, required this.child}); - - @override - Widget build(BuildContext context) { - return AnimatedBuilder( - animation: animation, - builder: (context, child) => animation.value == 0.0 ? const SizedBox() : child!, - child: SlideTransition( - position: Tween(begin: const Offset(0.3, 0.0), end: const Offset(0.0, 0.0)).animate(animation), - child: FadeTransition(opacity: animation, child: child), - ), - ); - } -} diff --git a/mobile/lib/widgets/asset_grid/group_divider_title.dart b/mobile/lib/widgets/asset_grid/group_divider_title.dart deleted file mode 100644 index 1464c941f0..0000000000 --- a/mobile/lib/widgets/asset_grid/group_divider_title.dart +++ /dev/null @@ -1,84 +0,0 @@ -import 'package:easy_localization/easy_localization.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_hooks/flutter_hooks.dart'; -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/extensions/build_context_extensions.dart'; -import 'package:immich_mobile/extensions/theme_extensions.dart'; -import 'package:immich_mobile/widgets/asset_grid/asset_grid_data_structure.dart'; -import 'package:immich_mobile/providers/app_settings.provider.dart'; -import 'package:immich_mobile/services/app_settings.service.dart'; -import 'package:immich_mobile/providers/haptic_feedback.provider.dart'; - -class GroupDividerTitle extends HookConsumerWidget { - const GroupDividerTitle({ - super.key, - required this.text, - required this.multiselectEnabled, - required this.onSelect, - required this.onDeselect, - required this.selected, - }); - - final String text; - final bool multiselectEnabled; - final Function onSelect; - final Function onDeselect; - final bool selected; - - @override - Widget build(BuildContext context, WidgetRef ref) { - final appSettingService = ref.watch(appSettingsServiceProvider); - final groupBy = useState(GroupAssetsBy.day); - - useEffect(() { - groupBy.value = GroupAssetsBy.values[appSettingService.getSetting(AppSettingsEnum.groupAssetsBy)]; - return null; - }, []); - - void handleTitleIconClick() { - ref.read(hapticFeedbackProvider.notifier).heavyImpact(); - if (selected) { - onDeselect(); - } else { - onSelect(); - } - } - - return Padding( - padding: EdgeInsets.only( - top: groupBy.value == GroupAssetsBy.month ? 32.0 : 16.0, - bottom: 16.0, - left: 12.0, - right: 12.0, - ), - child: Row( - children: [ - Text( - text, - style: groupBy.value == GroupAssetsBy.month - ? context.textTheme.bodyLarge?.copyWith(fontSize: 24.0) - : context.textTheme.labelLarge?.copyWith( - color: context.textTheme.labelLarge?.color?.withAlpha(250), - fontWeight: FontWeight.w500, - ), - ), - const Spacer(), - GestureDetector( - onTap: handleTitleIconClick, - child: multiselectEnabled && selected - ? Icon( - Icons.check_circle_rounded, - color: context.primaryColor, - semanticLabel: "unselect_all_in".tr(namedArgs: {"group": text}), - ) - : Icon( - Icons.check_circle_outline_rounded, - color: context.colorScheme.onSurfaceSecondary, - semanticLabel: "select_all_in".tr(namedArgs: {"group": text}), - ), - ), - ], - ), - ); - } -} diff --git a/mobile/lib/widgets/asset_grid/immich_asset_grid.dart b/mobile/lib/widgets/asset_grid/immich_asset_grid.dart deleted file mode 100644 index ab6b350a7b..0000000000 --- a/mobile/lib/widgets/asset_grid/immich_asset_grid.dart +++ /dev/null @@ -1,135 +0,0 @@ -import 'dart:math'; - -import 'package:auto_route/auto_route.dart'; -import 'package:flutter/gestures.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_hooks/flutter_hooks.dart'; -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/extensions/asyncvalue_extensions.dart'; -import 'package:immich_mobile/providers/timeline.provider.dart'; -import 'package:immich_mobile/widgets/asset_grid/asset_grid_data_structure.dart'; -import 'package:immich_mobile/widgets/asset_grid/immich_asset_grid_view.dart'; -import 'package:immich_mobile/providers/app_settings.provider.dart'; -import 'package:immich_mobile/services/app_settings.service.dart'; -import 'package:immich_mobile/entities/asset.entity.dart'; -import 'package:scrollable_positioned_list/scrollable_positioned_list.dart'; - -class ImmichAssetGrid extends HookConsumerWidget { - final int? assetsPerRow; - final double margin; - final bool? showStorageIndicator; - final ImmichAssetGridSelectionListener? listener; - final bool selectionActive; - final List? assets; - final RenderList? renderList; - final Future Function()? onRefresh; - final Set? preselectedAssets; - final bool canDeselect; - final bool? dynamicLayout; - final bool showMultiSelectIndicator; - final void Function(Iterable itemPositions)? visibleItemsListener; - final Widget? topWidget; - final bool shrinkWrap; - final bool showDragScroll; - final bool showDragScrollLabel; - final bool showStack; - - const ImmichAssetGrid({ - super.key, - this.assets, - this.onRefresh, - this.renderList, - this.assetsPerRow, - this.showStorageIndicator, - this.listener, - this.margin = 2.0, - this.selectionActive = false, - this.preselectedAssets, - this.canDeselect = true, - this.dynamicLayout, - this.showMultiSelectIndicator = true, - this.visibleItemsListener, - this.topWidget, - this.shrinkWrap = false, - this.showDragScroll = true, - this.showDragScrollLabel = true, - this.showStack = false, - }); - - @override - Widget build(BuildContext context, WidgetRef ref) { - var settings = ref.watch(appSettingsServiceProvider); - - final perRow = useState(assetsPerRow ?? settings.getSetting(AppSettingsEnum.tilesPerRow)!); - final scaleFactor = useState(7.0 - perRow.value); - final baseScaleFactor = useState(7.0 - perRow.value); - - /// assets need different hero tags across tabs / modals - /// otherwise, hero animations are performed across tabs (looks buggy!) - int heroOffset() { - const int range = 1152921504606846976; // 2^60 - final tabScope = TabsRouterScope.of(context); - if (tabScope != null) { - final int tabIndex = tabScope.controller.activeIndex; - return tabIndex * range; - } - return range * 7; - } - - Widget buildAssetGridView(RenderList renderList) { - return RawGestureDetector( - gestures: { - CustomScaleGestureRecognizer: GestureRecognizerFactoryWithHandlers( - () => CustomScaleGestureRecognizer(), - (CustomScaleGestureRecognizer scale) { - scale.onStart = (details) { - baseScaleFactor.value = scaleFactor.value; - }; - - scale.onUpdate = (details) { - scaleFactor.value = max(min(5.0, baseScaleFactor.value * details.scale), 1.0); - if (7 - scaleFactor.value.toInt() != perRow.value) { - perRow.value = 7 - scaleFactor.value.toInt(); - settings.setSetting(AppSettingsEnum.tilesPerRow, perRow.value); - } - }; - }, - ), - }, - child: ImmichAssetGridView( - onRefresh: onRefresh, - assetsPerRow: perRow.value, - listener: listener, - showStorageIndicator: showStorageIndicator ?? settings.getSetting(AppSettingsEnum.storageIndicator), - renderList: renderList, - margin: margin, - selectionActive: selectionActive, - preselectedAssets: preselectedAssets, - canDeselect: canDeselect, - dynamicLayout: dynamicLayout ?? settings.getSetting(AppSettingsEnum.dynamicLayout), - showMultiSelectIndicator: showMultiSelectIndicator, - visibleItemsListener: visibleItemsListener, - topWidget: topWidget, - heroOffset: heroOffset(), - shrinkWrap: shrinkWrap, - showDragScroll: showDragScroll, - showStack: showStack, - showLabel: showDragScrollLabel, - ), - ); - } - - if (renderList != null) return buildAssetGridView(renderList!); - - final renderListFuture = ref.watch(assetsTimelineProvider(assets!)); - return renderListFuture.widgetWhen(onData: (renderList) => buildAssetGridView(renderList)); - } -} - -/// accepts a gesture even though it should reject it (because child won) -class CustomScaleGestureRecognizer extends ScaleGestureRecognizer { - @override - void rejectGesture(int pointer) { - acceptGesture(pointer); - } -} diff --git a/mobile/lib/widgets/asset_grid/immich_asset_grid_view.dart b/mobile/lib/widgets/asset_grid/immich_asset_grid_view.dart deleted file mode 100644 index c323c573b4..0000000000 --- a/mobile/lib/widgets/asset_grid/immich_asset_grid_view.dart +++ /dev/null @@ -1,828 +0,0 @@ -import 'dart:collection'; -import 'dart:developer'; -import 'dart:math'; - -import 'package:auto_route/auto_route.dart'; -import 'package:collection/collection.dart'; -import 'package:easy_localization/easy_localization.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter/rendering.dart'; -import 'package:flutter/services.dart'; -import 'package:fluttertoast/fluttertoast.dart'; -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/entities/asset.entity.dart'; -import 'package:immich_mobile/extensions/build_context_extensions.dart'; -import 'package:immich_mobile/extensions/collection_extensions.dart'; -import 'package:immich_mobile/extensions/theme_extensions.dart'; -import 'package:immich_mobile/providers/asset_viewer/current_asset.provider.dart'; -import 'package:immich_mobile/providers/asset_viewer/is_motion_video_playing.provider.dart'; -import 'package:immich_mobile/providers/asset_viewer/scroll_notifier.provider.dart'; -import 'package:immich_mobile/providers/asset_viewer/scroll_to_date_notifier.provider.dart'; -import 'package:immich_mobile/providers/asset_viewer/show_controls.provider.dart'; -import 'package:immich_mobile/providers/haptic_feedback.provider.dart'; -import 'package:immich_mobile/providers/tab.provider.dart'; -import 'package:immich_mobile/routing/router.dart'; -import 'package:immich_mobile/widgets/asset_grid/asset_drag_region.dart'; -import 'package:immich_mobile/widgets/asset_grid/asset_grid_data_structure.dart'; -import 'package:immich_mobile/widgets/asset_grid/control_bottom_app_bar.dart'; -import 'package:immich_mobile/widgets/asset_grid/disable_multi_select_button.dart'; -import 'package:immich_mobile/widgets/asset_grid/draggable_scrollbar_custom.dart'; -import 'package:immich_mobile/widgets/asset_grid/group_divider_title.dart'; -import 'package:immich_mobile/widgets/asset_grid/thumbnail_image.dart'; -import 'package:immich_mobile/widgets/asset_grid/thumbnail_placeholder.dart'; -import 'package:immich_mobile/widgets/common/immich_toast.dart'; -import 'package:scrollable_positioned_list/scrollable_positioned_list.dart'; - -typedef ImmichAssetGridSelectionListener = void Function(bool, Set); - -class ImmichAssetGridView extends ConsumerStatefulWidget { - final RenderList renderList; - final int assetsPerRow; - final double margin; - final bool showStorageIndicator; - final ImmichAssetGridSelectionListener? listener; - final bool selectionActive; - final Future Function()? onRefresh; - final Set? preselectedAssets; - final bool canDeselect; - final bool dynamicLayout; - final bool showMultiSelectIndicator; - final void Function(Iterable itemPositions)? visibleItemsListener; - final Widget? topWidget; - final int heroOffset; - final bool shrinkWrap; - final bool showDragScroll; - final bool showStack; - final bool showLabel; - - const ImmichAssetGridView({ - super.key, - required this.renderList, - required this.assetsPerRow, - required this.showStorageIndicator, - this.listener, - this.margin = 5.0, - this.selectionActive = false, - this.onRefresh, - this.preselectedAssets, - this.canDeselect = true, - this.dynamicLayout = true, - this.showMultiSelectIndicator = true, - this.visibleItemsListener, - this.topWidget, - this.heroOffset = 0, - this.shrinkWrap = false, - this.showDragScroll = true, - this.showStack = false, - this.showLabel = true, - }); - - @override - createState() { - return ImmichAssetGridViewState(); - } -} - -class ImmichAssetGridViewState extends ConsumerState { - final ItemScrollController _itemScrollController = ItemScrollController(); - final ScrollOffsetController _scrollOffsetController = ScrollOffsetController(); - final ItemPositionsListener _itemPositionsListener = ItemPositionsListener.create(); - late final KeepAliveLink currentAssetLink; - - /// The timestamp when the haptic feedback was last invoked - int _hapticFeedbackTS = 0; - DateTime? _prevItemTime; - bool _scrolling = false; - final Set _selectedAssets = LinkedHashSet(equals: (a, b) => a.id == b.id, hashCode: (a) => a.id); - - bool _dragging = false; - int? _dragAnchorAssetIndex; - int? _dragAnchorSectionIndex; - final Set _draggedAssets = HashSet(equals: (a, b) => a.id == b.id, hashCode: (a) => a.id); - - ScrollPhysics? _scrollPhysics; - - Set _getSelectedAssets() { - return Set.from(_selectedAssets); - } - - void _callSelectionListener(bool selectionActive) { - widget.listener?.call(selectionActive, _getSelectedAssets()); - } - - void _selectAssets(List assets) { - setState(() { - if (_dragging) { - _draggedAssets.addAll(assets); - } - _selectedAssets.addAll(assets); - _callSelectionListener(true); - }); - } - - void _deselectAssets(List assets) { - final assetsToDeselect = assets.where( - (a) => widget.canDeselect || !(widget.preselectedAssets?.contains(a) ?? false), - ); - - setState(() { - _selectedAssets.removeAll(assetsToDeselect); - if (_dragging) { - _draggedAssets.removeAll(assetsToDeselect); - } - _callSelectionListener(_selectedAssets.isNotEmpty); - }); - } - - void _deselectAll() { - setState(() { - _selectedAssets.clear(); - _dragAnchorAssetIndex = null; - _dragAnchorSectionIndex = null; - _draggedAssets.clear(); - _dragging = false; - if (!widget.canDeselect && widget.preselectedAssets != null && widget.preselectedAssets!.isNotEmpty) { - _selectedAssets.addAll(widget.preselectedAssets!); - } - _callSelectionListener(false); - }); - } - - bool _allAssetsSelected(List assets) { - return widget.selectionActive && assets.firstWhereOrNull((e) => !_selectedAssets.contains(e)) == null; - } - - Future _scrollToIndex(int index) async { - // if the index is so far down, that the end of the list is reached on the screen - // the scroll_position widget crashes. This is a workaround to prevent this. - // If the index is within the last 10 elements, we jump instead of scrolling. - if (widget.renderList.elements.length <= index + 10) { - _itemScrollController.jumpTo(index: index); - return; - } - await _itemScrollController.scrollTo(index: index, alignment: 0, duration: const Duration(milliseconds: 500)); - } - - Widget _itemBuilder(BuildContext c, int position) { - int index = position; - if (widget.topWidget != null) { - if (index == 0) { - return widget.topWidget!; - } - index--; - } - - final section = widget.renderList.elements[index]; - return _Section( - showStorageIndicator: widget.showStorageIndicator, - selectedAssets: _selectedAssets, - selectionActive: widget.selectionActive, - sectionIndex: index, - section: section, - margin: widget.margin, - renderList: widget.renderList, - assetsPerRow: widget.assetsPerRow, - scrolling: _scrolling, - dynamicLayout: widget.dynamicLayout, - selectAssets: _selectAssets, - deselectAssets: _deselectAssets, - allAssetsSelected: _allAssetsSelected, - showStack: widget.showStack, - heroOffset: widget.heroOffset, - onAssetTap: (asset) { - ref.read(currentAssetProvider.notifier).set(asset); - ref.read(isPlayingMotionVideoProvider.notifier).playing = false; - if (asset.isVideo) { - ref.read(showControlsProvider.notifier).show = false; - } - }, - ); - } - - Text _labelBuilder(int pos) { - final maxLength = widget.renderList.elements.length; - if (pos < 0 || pos >= maxLength) { - return const Text(""); - } - - final date = widget.renderList.elements[pos % maxLength].date; - - return Text( - DateFormat.yMMMM().format(date), - style: const TextStyle(color: Colors.white, fontWeight: FontWeight.bold), - ); - } - - Widget _buildMultiSelectIndicator() { - return DisableMultiSelectButton(onPressed: () => _deselectAll(), selectedItemCount: _selectedAssets.length); - } - - Widget _buildAssetGrid() { - final useDragScrolling = widget.showDragScroll && widget.renderList.totalAssets >= 20; - - void dragScrolling(bool active) { - if (active != _scrolling) { - setState(() { - _scrolling = active; - }); - } - } - - bool appBarOffset() { - return (ref.watch(tabProvider).index == 0 && ModalRoute.of(context)?.settings.name == TabControllerRoute.name) || - (ModalRoute.of(context)?.settings.name == AlbumViewerRoute.name); - } - - final listWidget = ScrollablePositionedList.builder( - padding: EdgeInsets.only(top: appBarOffset() ? 60 : 0, bottom: 220), - itemBuilder: _itemBuilder, - itemPositionsListener: _itemPositionsListener, - physics: _scrollPhysics, - itemScrollController: _itemScrollController, - scrollOffsetController: _scrollOffsetController, - itemCount: widget.renderList.elements.length + (widget.topWidget != null ? 1 : 0), - addRepaintBoundaries: true, - shrinkWrap: widget.shrinkWrap, - ); - - final child = (useDragScrolling && ModalRoute.of(context) != null) - ? DraggableScrollbar.semicircle( - scrollStateListener: dragScrolling, - itemPositionsListener: _itemPositionsListener, - controller: _itemScrollController, - backgroundColor: context.isDarkTheme - ? context.colorScheme.primary.darken(amount: .5) - : context.colorScheme.primary, - labelTextBuilder: widget.showLabel ? _labelBuilder : null, - padding: appBarOffset() ? const EdgeInsets.only(top: 60) : const EdgeInsets.only(), - heightOffset: appBarOffset() ? 60 : 0, - labelConstraints: const BoxConstraints(maxHeight: 28), - scrollbarAnimationDuration: const Duration(milliseconds: 300), - scrollbarTimeToFade: const Duration(milliseconds: 1000), - child: listWidget, - ) - : listWidget; - - return widget.onRefresh == null - ? child - : appBarOffset() - ? RefreshIndicator(onRefresh: widget.onRefresh!, edgeOffset: 30, child: child) - : RefreshIndicator(onRefresh: widget.onRefresh!, child: child); - } - - void _scrollToDate() { - final date = scrollToDateNotifierProvider.value; - if (date == null) { - ImmichToast.show( - context: context, - msg: "Scroll To Date failed, date is null.", - gravity: ToastGravity.BOTTOM, - toastType: ToastType.error, - ); - return; - } - - // Search for the index of the exact date in the list - var index = widget.renderList.elements.indexWhere( - (e) => e.date.year == date.year && e.date.month == date.month && e.date.day == date.day, - ); - - // If the exact date is not found, the timeline is grouped by month, - // thus we search for the month - if (index == -1) { - index = widget.renderList.elements.indexWhere((e) => e.date.year == date.year && e.date.month == date.month); - } - - if (index < widget.renderList.elements.length) { - // Not sure why the index is shifted, but it works. :3 - _scrollToIndex(index + 1); - } else { - ImmichToast.show( - context: context, - msg: "The date (${DateFormat.yMd().format(date)}) could not be found in the timeline.", - gravity: ToastGravity.BOTTOM, - toastType: ToastType.error, - ); - } - } - - @override - void didUpdateWidget(ImmichAssetGridView oldWidget) { - super.didUpdateWidget(oldWidget); - if (!widget.selectionActive) { - setState(() { - _selectedAssets.clear(); - }); - } - } - - @override - void initState() { - super.initState(); - currentAssetLink = ref.read(currentAssetProvider.notifier).ref.keepAlive(); - scrollToTopNotifierProvider.addListener(_scrollToTop); - scrollToDateNotifierProvider.addListener(_scrollToDate); - - if (widget.visibleItemsListener != null) { - _itemPositionsListener.itemPositions.addListener(_positionListener); - } - if (widget.preselectedAssets != null) { - _selectedAssets.addAll(widget.preselectedAssets!); - } - - _itemPositionsListener.itemPositions.addListener(_hapticsListener); - } - - @override - void dispose() { - scrollToTopNotifierProvider.removeListener(_scrollToTop); - scrollToDateNotifierProvider.removeListener(_scrollToDate); - if (widget.visibleItemsListener != null) { - _itemPositionsListener.itemPositions.removeListener(_positionListener); - } - _itemPositionsListener.itemPositions.removeListener(_hapticsListener); - currentAssetLink.close(); - super.dispose(); - } - - void _positionListener() { - final values = _itemPositionsListener.itemPositions.value; - widget.visibleItemsListener?.call(values); - } - - void _hapticsListener() { - /// throttle interval for the haptic feedback in microseconds. - /// Currently set to 100ms. - const feedbackInterval = 100000; - - final values = _itemPositionsListener.itemPositions.value; - final start = values.firstOrNull; - - if (start != null) { - final pos = start.index; - final maxLength = widget.renderList.elements.length; - if (pos < 0 || pos >= maxLength) { - return; - } - - final date = widget.renderList.elements[pos].date; - - // only provide the feedback if the prev. date is known. - // Otherwise the app would provide the haptic feedback - // on startup. - if (_prevItemTime == null) { - _prevItemTime = date; - } else if (_prevItemTime?.year != date.year || _prevItemTime?.month != date.month) { - _prevItemTime = date; - - final now = Timeline.now; - if (now > (_hapticFeedbackTS + feedbackInterval)) { - _hapticFeedbackTS = now; - ref.read(hapticFeedbackProvider.notifier).mediumImpact(); - } - } - } - } - - void _scrollToTop() { - // for some reason, this is necessary as well in order - // to correctly reposition the drag thumb scroll bar - _itemScrollController.jumpTo(index: 0); - _itemScrollController.scrollTo(index: 0, duration: const Duration(milliseconds: 200)); - } - - void _setDragStartIndex(AssetIndex index) { - setState(() { - _scrollPhysics = const ClampingScrollPhysics(); - _dragAnchorAssetIndex = index.rowIndex; - _dragAnchorSectionIndex = index.sectionIndex; - _dragging = true; - }); - } - - void _stopDrag() { - WidgetsBinding.instance.addPostFrameCallback((_) { - // Update the physics post frame to prevent sudden change in physics on iOS. - setState(() { - _scrollPhysics = null; - }); - }); - setState(() { - _dragging = false; - _draggedAssets.clear(); - }); - } - - void _dragDragScroll(ScrollDirection direction) { - _scrollOffsetController.animateScroll( - offset: direction == ScrollDirection.forward ? 175 : -175, - duration: const Duration(milliseconds: 125), - ); - } - - void _handleDragAssetEnter(AssetIndex index) { - if (_dragAnchorSectionIndex == null || _dragAnchorAssetIndex == null) { - return; - } - - final dragAnchorSectionIndex = _dragAnchorSectionIndex!; - final dragAnchorAssetIndex = _dragAnchorAssetIndex!; - - late final int startSectionIndex; - late final int startSectionAssetIndex; - late final int endSectionIndex; - late final int endSectionAssetIndex; - - if (index.sectionIndex < dragAnchorSectionIndex) { - startSectionIndex = index.sectionIndex; - startSectionAssetIndex = index.rowIndex; - endSectionIndex = dragAnchorSectionIndex; - endSectionAssetIndex = dragAnchorAssetIndex; - } else if (index.sectionIndex > dragAnchorSectionIndex) { - startSectionIndex = dragAnchorSectionIndex; - startSectionAssetIndex = dragAnchorAssetIndex; - endSectionIndex = index.sectionIndex; - endSectionAssetIndex = index.rowIndex; - } else { - startSectionIndex = dragAnchorSectionIndex; - endSectionIndex = dragAnchorSectionIndex; - - // If same section, assign proper start / end asset Index - if (dragAnchorAssetIndex < index.rowIndex) { - startSectionAssetIndex = dragAnchorAssetIndex; - endSectionAssetIndex = index.rowIndex; - } else { - startSectionAssetIndex = index.rowIndex; - endSectionAssetIndex = dragAnchorAssetIndex; - } - } - - final selectedAssets = {}; - var currentSectionIndex = startSectionIndex; - while (currentSectionIndex < endSectionIndex) { - final section = widget.renderList.elements.elementAtOrNull(currentSectionIndex); - if (section == null) continue; - - final sectionAssets = widget.renderList.loadAssets(section.offset, section.count); - - if (currentSectionIndex == startSectionIndex) { - selectedAssets.addAll(sectionAssets.slice(startSectionAssetIndex, sectionAssets.length)); - } else { - selectedAssets.addAll(sectionAssets); - } - - currentSectionIndex += 1; - } - - final section = widget.renderList.elements.elementAtOrNull(endSectionIndex); - if (section != null) { - final sectionAssets = widget.renderList.loadAssets(section.offset, section.count); - if (startSectionIndex == endSectionIndex) { - selectedAssets.addAll(sectionAssets.slice(startSectionAssetIndex, endSectionAssetIndex + 1)); - } else { - selectedAssets.addAll(sectionAssets.slice(0, endSectionAssetIndex + 1)); - } - } - - _deselectAssets(_draggedAssets.toList()); - _draggedAssets.clear(); - _draggedAssets.addAll(selectedAssets); - _selectAssets(_draggedAssets.toList()); - } - - @override - Widget build(BuildContext context) { - return PopScope( - canPop: !(widget.selectionActive && _selectedAssets.isNotEmpty), - onPopInvokedWithResult: (didPop, _) { - if (didPop) { - return; - } else { - /// `preselectedAssets` is only present when opening the asset grid from the - /// "add to album" button. - /// - /// `_selectedAssets` includes `preselectedAssets` on initialization. - if (_selectedAssets.length > (widget.preselectedAssets?.length ?? 0)) { - /// `_deselectAll` only deselects the selected assets, - /// doesn't affect the preselected ones. - _deselectAll(); - return; - } else { - Navigator.of(context).canPop() ? Navigator.of(context).pop() : null; - } - } - }, - child: Stack( - children: [ - AssetDragRegion( - onStart: _setDragStartIndex, - onAssetEnter: _handleDragAssetEnter, - onEnd: _stopDrag, - onScroll: _dragDragScroll, - onScrollStart: () => - WidgetsBinding.instance.addPostFrameCallback((_) => controlBottomAppBarNotifier.minimize()), - child: _buildAssetGrid(), - ), - if (widget.showMultiSelectIndicator && widget.selectionActive) _buildMultiSelectIndicator(), - ], - ), - ); - } -} - -/// A single row of all placeholder widgets -class _PlaceholderRow extends StatelessWidget { - final int number; - final double width; - final double height; - final double margin; - - const _PlaceholderRow({ - super.key, - required this.number, - required this.width, - required this.height, - required this.margin, - }); - - @override - Widget build(BuildContext context) { - return Row( - children: [ - for (int i = 0; i < number; i++) - ThumbnailPlaceholder( - key: ValueKey(i), - width: width, - height: height, - margin: EdgeInsets.only(bottom: margin, right: i + 1 == number ? 0.0 : margin), - ), - ], - ); - } -} - -/// A section for the render grid -class _Section extends StatelessWidget { - final RenderAssetGridElement section; - final int sectionIndex; - final Set selectedAssets; - final bool scrolling; - final double margin; - final int assetsPerRow; - final RenderList renderList; - final bool selectionActive; - final bool dynamicLayout; - final void Function(List) selectAssets; - final void Function(List) deselectAssets; - final bool Function(List) allAssetsSelected; - final bool showStack; - final int heroOffset; - final bool showStorageIndicator; - final void Function(Asset) onAssetTap; - - const _Section({ - required this.section, - required this.sectionIndex, - required this.scrolling, - required this.margin, - required this.assetsPerRow, - required this.renderList, - required this.selectionActive, - required this.dynamicLayout, - required this.selectAssets, - required this.deselectAssets, - required this.allAssetsSelected, - required this.selectedAssets, - required this.showStack, - required this.heroOffset, - required this.showStorageIndicator, - required this.onAssetTap, - }); - - @override - Widget build(BuildContext context) { - return LayoutBuilder( - builder: (context, constraints) { - final width = constraints.maxWidth / assetsPerRow - margin * (assetsPerRow - 1) / assetsPerRow; - final rows = (section.count + assetsPerRow - 1) ~/ assetsPerRow; - final List assetsToRender = scrolling ? [] : renderList.loadAssets(section.offset, section.count); - return Column( - key: ValueKey(section.offset), - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - if (section.type == RenderAssetGridElementType.monthTitle) _MonthTitle(date: section.date), - if (section.type == RenderAssetGridElementType.groupDividerTitle || - section.type == RenderAssetGridElementType.monthTitle) - _Title( - selectionActive: selectionActive, - title: section.title!, - assets: scrolling ? [] : renderList.loadAssets(section.offset, section.totalCount), - allAssetsSelected: allAssetsSelected, - selectAssets: selectAssets, - deselectAssets: deselectAssets, - ), - for (int i = 0; i < rows; i++) - scrolling - ? _PlaceholderRow( - key: ValueKey(i), - number: i + 1 == rows ? section.count - i * assetsPerRow : assetsPerRow, - width: width, - height: width, - margin: margin, - ) - : _AssetRow( - key: ValueKey(i), - rowStartIndex: i * assetsPerRow, - sectionIndex: sectionIndex, - assets: assetsToRender.nestedSlice(i * assetsPerRow, min((i + 1) * assetsPerRow, section.count)), - absoluteOffset: section.offset + i * assetsPerRow, - width: width, - assetsPerRow: assetsPerRow, - margin: margin, - dynamicLayout: dynamicLayout, - renderList: renderList, - selectedAssets: selectedAssets, - isSelectionActive: selectionActive, - showStack: showStack, - heroOffset: heroOffset, - showStorageIndicator: showStorageIndicator, - selectionActive: selectionActive, - onSelect: (asset) => selectAssets([asset]), - onDeselect: (asset) => deselectAssets([asset]), - onAssetTap: onAssetTap, - ), - ], - ); - }, - ); - } -} - -/// The month title row for a section -class _MonthTitle extends StatelessWidget { - final DateTime date; - - const _MonthTitle({required this.date}); - - @override - Widget build(BuildContext context) { - final monthFormat = DateTime.now().year == date.year ? DateFormat.MMMM() : DateFormat.yMMMM(); - final String title = monthFormat.format(date); - return Padding( - key: Key("month-$title"), - padding: const EdgeInsets.only(left: 12.0, top: 24.0), - child: Text( - toBeginningOfSentenceCase(title, context.locale.languageCode), - style: const TextStyle(fontSize: 26, fontWeight: FontWeight.w500), - ), - ); - } -} - -/// A title row -class _Title extends StatelessWidget { - final String title; - final List assets; - final bool selectionActive; - final void Function(List) selectAssets; - final void Function(List) deselectAssets; - final bool Function(List) allAssetsSelected; - - const _Title({ - required this.title, - required this.assets, - required this.selectionActive, - required this.selectAssets, - required this.deselectAssets, - required this.allAssetsSelected, - }); - - @override - Widget build(BuildContext context) { - return GroupDividerTitle( - text: toBeginningOfSentenceCase(title, context.locale.languageCode), - multiselectEnabled: selectionActive, - onSelect: () => selectAssets(assets), - onDeselect: () => deselectAssets(assets), - selected: allAssetsSelected(assets), - ); - } -} - -/// The row of assets -class _AssetRow extends StatelessWidget { - final List assets; - final int rowStartIndex; - final int sectionIndex; - final Set selectedAssets; - final int absoluteOffset; - final double width; - final bool dynamicLayout; - final double margin; - final int assetsPerRow; - final RenderList renderList; - final bool selectionActive; - final bool showStorageIndicator; - final int heroOffset; - final bool showStack; - final void Function(Asset) onAssetTap; - final void Function(Asset)? onSelect; - final void Function(Asset)? onDeselect; - final bool isSelectionActive; - - const _AssetRow({ - super.key, - required this.rowStartIndex, - required this.sectionIndex, - required this.assets, - required this.absoluteOffset, - required this.width, - required this.dynamicLayout, - required this.margin, - required this.assetsPerRow, - required this.renderList, - required this.selectionActive, - required this.showStorageIndicator, - required this.heroOffset, - required this.showStack, - required this.isSelectionActive, - required this.selectedAssets, - required this.onAssetTap, - this.onSelect, - this.onDeselect, - }); - - @override - Widget build(BuildContext context) { - // Default: All assets have the same width - final widthDistribution = List.filled(assets.length, 1.0); - - if (dynamicLayout) { - final aspectRatios = assets.map((e) => (e.width ?? 1) / (e.height ?? 1)).toList(); - final meanAspectRatio = aspectRatios.sum / assets.length; - - // 1: mean width - // 0.5: width < mean - threshold - // 1.5: width > mean + threshold - final arConfiguration = aspectRatios.map((e) { - if (e - meanAspectRatio > 0.3) return 1.5; - if (e - meanAspectRatio < -0.3) return 0.5; - return 1.0; - }); - - // Normalize: - final sum = arConfiguration.sum; - widthDistribution.setRange(0, widthDistribution.length, arConfiguration.map((e) => (e * assets.length) / sum)); - } - return Row( - key: key, - children: assets.mapIndexed((int index, Asset asset) { - final bool last = index + 1 == assetsPerRow; - final isSelected = isSelectionActive && selectedAssets.contains(asset); - return Container( - width: width * widthDistribution[index], - height: width, - margin: EdgeInsets.only(bottom: margin, right: last ? 0.0 : margin), - child: GestureDetector( - onTap: () { - if (selectionActive) { - if (isSelected) { - onDeselect?.call(asset); - } else { - onSelect?.call(asset); - } - } else { - final asset = renderList.loadAsset(absoluteOffset + index); - onAssetTap(asset); - context.pushRoute( - GalleryViewerRoute( - renderList: renderList, - initialIndex: absoluteOffset + index, - heroOffset: heroOffset, - showStack: showStack, - ), - ); - } - }, - onLongPress: () { - onSelect?.call(asset); - HapticFeedback.heavyImpact(); - }, - child: AssetIndexWrapper( - rowIndex: rowStartIndex + index, - sectionIndex: sectionIndex, - child: ThumbnailImage( - asset: asset, - multiselectEnabled: selectionActive, - isSelected: isSelectionActive && selectedAssets.contains(asset), - showStorageIndicator: showStorageIndicator, - heroOffset: heroOffset, - showStack: showStack, - ), - ), - ), - ); - }).toList(), - ); - } -} diff --git a/mobile/lib/widgets/asset_grid/multiselect_grid.dart b/mobile/lib/widgets/asset_grid/multiselect_grid.dart deleted file mode 100644 index c0d8a6bea2..0000000000 --- a/mobile/lib/widgets/asset_grid/multiselect_grid.dart +++ /dev/null @@ -1,458 +0,0 @@ -import 'dart:async'; - -import 'package:auto_route/auto_route.dart'; -import 'package:easy_localization/easy_localization.dart'; -import 'package:flutter/foundation.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_hooks/flutter_hooks.dart'; -import 'package:fluttertoast/fluttertoast.dart'; -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/constants/enums.dart'; -import 'package:immich_mobile/entities/album.entity.dart'; -import 'package:immich_mobile/entities/asset.entity.dart'; -import 'package:immich_mobile/extensions/collection_extensions.dart'; -import 'package:immich_mobile/extensions/translate_extensions.dart'; -import 'package:immich_mobile/models/asset_selection_state.dart'; -import 'package:immich_mobile/providers/album/album.provider.dart'; -import 'package:immich_mobile/providers/asset.provider.dart'; -import 'package:immich_mobile/providers/asset_viewer/download.provider.dart'; -import 'package:immich_mobile/providers/backup/manual_upload.provider.dart'; -import 'package:immich_mobile/providers/multiselect.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/services/album.service.dart'; -import 'package:immich_mobile/services/stack.service.dart'; -import 'package:immich_mobile/utils/immich_loading_overlay.dart'; -import 'package:immich_mobile/utils/selection_handlers.dart'; -import 'package:immich_mobile/widgets/asset_grid/asset_grid_data_structure.dart'; -import 'package:immich_mobile/widgets/asset_grid/control_bottom_app_bar.dart'; -import 'package:immich_mobile/widgets/asset_grid/immich_asset_grid.dart'; -import 'package:immich_mobile/widgets/common/immich_toast.dart'; - -class MultiselectGrid extends HookConsumerWidget { - const MultiselectGrid({ - super.key, - required this.renderListProvider, - this.onRefresh, - this.buildLoadingIndicator, - this.onRemoveFromAlbum, - this.topWidget, - this.stackEnabled = false, - this.dragScrollLabelEnabled = true, - this.archiveEnabled = false, - this.deleteEnabled = true, - this.favoriteEnabled = true, - this.editEnabled = false, - this.unarchive = false, - this.unfavorite = false, - this.downloadEnabled = true, - this.emptyIndicator, - }); - - final ProviderListenable> renderListProvider; - final Future Function()? onRefresh; - final Widget Function()? buildLoadingIndicator; - final Future Function(Iterable)? onRemoveFromAlbum; - final Widget? topWidget; - final bool stackEnabled; - final bool dragScrollLabelEnabled; - final bool archiveEnabled; - final bool unarchive; - final bool deleteEnabled; - final bool downloadEnabled; - final bool favoriteEnabled; - final bool unfavorite; - final bool editEnabled; - final Widget? emptyIndicator; - Widget buildDefaultLoadingIndicator() => const Center(child: CircularProgressIndicator()); - - Widget buildEmptyIndicator() => emptyIndicator ?? Center(child: const Text("no_assets_to_show").tr()); - - @override - Widget build(BuildContext context, WidgetRef ref) { - final multiselectEnabled = ref.watch(multiselectProvider.notifier); - final selectionEnabledHook = useState(false); - final selectionAssetState = useState(const AssetSelectionState()); - - final selection = useState({}); - final currentUser = ref.watch(currentUserProvider); - final processing = useProcessingOverlay(); - - useEffect(() { - selectionEnabledHook.addListener(() { - multiselectEnabled.state = selectionEnabledHook.value; - }); - - return () { - // This does not work in tests - if (kReleaseMode) { - selectionEnabledHook.dispose(); - } - }; - }, []); - - void selectionListener(bool multiselect, Set selectedAssets) { - selectionEnabledHook.value = multiselect; - selection.value = selectedAssets; - selectionAssetState.value = AssetSelectionState.fromSelection(selectedAssets); - } - - errorBuilder(String? msg) => msg != null && msg.isNotEmpty - ? () => ImmichToast.show(context: context, msg: msg, gravity: ToastGravity.BOTTOM) - : null; - - Iterable ownedRemoteSelection({String? localErrorMessage, String? ownerErrorMessage}) { - final assets = selection.value; - return assets - .remoteOnly(errorCallback: errorBuilder(localErrorMessage)) - .ownedOnly(currentUser, errorCallback: errorBuilder(ownerErrorMessage)); - } - - Iterable remoteSelection({String? errorMessage}) => - selection.value.remoteOnly(errorCallback: errorBuilder(errorMessage)); - - void onShareAssets(bool shareLocal) { - processing.value = true; - if (shareLocal) { - // Share = Download + Send to OS specific share sheet - handleShareAssets(ref, context, selection.value); - } else { - final ids = remoteSelection(errorMessage: "home_page_share_err_local".tr()).map((e) => e.remoteId!); - context.pushRoute(SharedLinkEditRoute(assetsList: ids.toList())); - } - processing.value = false; - selectionEnabledHook.value = false; - } - - void onFavoriteAssets() async { - processing.value = true; - try { - final remoteAssets = ownedRemoteSelection( - localErrorMessage: 'home_page_favorite_err_local'.tr(), - ownerErrorMessage: 'home_page_favorite_err_partner'.tr(), - ); - if (remoteAssets.isNotEmpty) { - await handleFavoriteAssets(ref, context, remoteAssets.toList()); - } - } finally { - processing.value = false; - selectionEnabledHook.value = false; - } - } - - void onArchiveAsset() async { - processing.value = true; - try { - final remoteAssets = ownedRemoteSelection( - localErrorMessage: 'home_page_archive_err_local'.tr(), - ownerErrorMessage: 'home_page_archive_err_partner'.tr(), - ); - await handleArchiveAssets(ref, context, remoteAssets.toList()); - } finally { - processing.value = false; - selectionEnabledHook.value = false; - } - } - - void onDelete([bool force = false]) async { - processing.value = true; - try { - final toDelete = selection.value - .ownedOnly(currentUser, errorCallback: errorBuilder('home_page_delete_err_partner'.tr())) - .toList(); - final isDeleted = await ref.read(assetProvider.notifier).deleteAssets(toDelete, force: force); - - if (isDeleted) { - ImmichToast.show( - context: context, - msg: force - ? 'assets_deleted_permanently'.tr(namedArgs: {'count': "${selection.value.length}"}) - : 'assets_trashed'.tr(namedArgs: {'count': "${selection.value.length}"}), - gravity: ToastGravity.BOTTOM, - ); - selectionEnabledHook.value = false; - } - } finally { - processing.value = false; - } - } - - void onDeleteLocal(bool isMergedAsset) async { - processing.value = true; - try { - final localAssets = selection.value.where((a) => a.isLocal).toList(); - - final toDelete = isMergedAsset ? localAssets.where((e) => e.storage == AssetState.merged) : localAssets; - - if (toDelete.isEmpty) { - return; - } - - final isDeleted = await ref.read(assetProvider.notifier).deleteLocalAssets(toDelete.toList()); - - if (isDeleted) { - final deletedCount = localAssets.where((e) => !isMergedAsset || e.isRemote).length; - - ImmichToast.show( - context: context, - msg: 'assets_removed_permanently_from_device'.tr(namedArgs: {'count': "$deletedCount"}), - gravity: ToastGravity.BOTTOM, - ); - - selectionEnabledHook.value = false; - } - } finally { - processing.value = false; - } - } - - void onDownload() async { - processing.value = true; - try { - final toDownload = selection.value.toList(); - - final results = await ref.read(downloadStateProvider.notifier).downloadAllAsset(toDownload); - - final totalCount = toDownload.length; - final successCount = results.where((e) => e).length; - final failedCount = totalCount - successCount; - - final msg = failedCount > 0 - ? 'assets_downloaded_failed'.t(context: context, args: {'count': successCount, 'error': failedCount}) - : 'assets_downloaded_successfully'.t(context: context, args: {'count': successCount}); - - ImmichToast.show(context: context, msg: msg, gravity: ToastGravity.BOTTOM); - } finally { - processing.value = false; - selectionEnabledHook.value = false; - } - } - - void onDeleteRemote([bool shouldDeletePermanently = false]) async { - processing.value = true; - try { - final toDelete = ownedRemoteSelection( - localErrorMessage: 'home_page_delete_remote_err_local'.tr(), - ownerErrorMessage: 'home_page_delete_err_partner'.tr(), - ).toList(); - - final isDeleted = await ref - .read(assetProvider.notifier) - .deleteRemoteAssets(toDelete, shouldDeletePermanently: shouldDeletePermanently); - if (isDeleted) { - ImmichToast.show( - context: context, - msg: shouldDeletePermanently - ? 'assets_deleted_permanently_from_server'.tr(namedArgs: {'count': "${toDelete.length}"}) - : 'assets_trashed_from_server'.tr(namedArgs: {'count': "${toDelete.length}"}), - gravity: ToastGravity.BOTTOM, - ); - } - } finally { - selectionEnabledHook.value = false; - processing.value = false; - } - } - - void onUpload() { - processing.value = true; - selectionEnabledHook.value = false; - try { - ref - .read(manualUploadProvider.notifier) - .uploadAssets(context, selection.value.where((a) => a.storage == AssetState.local)); - } finally { - processing.value = false; - } - } - - void onAddToAlbum(Album album) async { - processing.value = true; - try { - final Iterable assets = remoteSelection(errorMessage: "home_page_add_to_album_err_local".tr()); - if (assets.isEmpty) { - return; - } - final result = await ref.read(albumServiceProvider).addAssets(album, assets); - - if (result != null) { - if (result.alreadyInAlbum.isNotEmpty) { - ImmichToast.show( - context: context, - msg: "home_page_add_to_album_conflicts".tr( - namedArgs: { - "album": album.name, - "added": result.successfullyAdded.toString(), - "failed": result.alreadyInAlbum.length.toString(), - }, - ), - ); - } else { - ImmichToast.show( - context: context, - msg: "home_page_add_to_album_success".tr( - namedArgs: {"album": album.name, "added": result.successfullyAdded.toString()}, - ), - toastType: ToastType.success, - ); - } - } - } finally { - processing.value = false; - selectionEnabledHook.value = false; - } - } - - void onCreateNewAlbum() async { - processing.value = true; - try { - final Iterable assets = remoteSelection(errorMessage: "home_page_add_to_album_err_local".tr()); - if (assets.isEmpty) { - return; - } - final result = await ref.read(albumServiceProvider).createAlbumWithGeneratedName(assets); - - if (result != null) { - unawaited(ref.watch(albumProvider.notifier).refreshRemoteAlbums()); - selectionEnabledHook.value = false; - - unawaited(context.pushRoute(AlbumViewerRoute(albumId: result.id))); - } - } finally { - processing.value = false; - } - } - - void onStack() async { - try { - processing.value = true; - if (!selectionEnabledHook.value || selection.value.length < 2) { - return; - } - - await ref.read(stackServiceProvider).createStack(selection.value.map((e) => e.remoteId!).toList()); - } finally { - processing.value = false; - selectionEnabledHook.value = false; - } - } - - void onEditTime() async { - try { - final remoteAssets = ownedRemoteSelection( - localErrorMessage: 'home_page_favorite_err_local'.tr(), - ownerErrorMessage: 'home_page_favorite_err_partner'.tr(), - ); - - if (remoteAssets.isNotEmpty) { - unawaited(handleEditDateTime(ref, context, remoteAssets.toList())); - } - } finally { - selectionEnabledHook.value = false; - } - } - - void onEditLocation() async { - try { - final remoteAssets = ownedRemoteSelection( - localErrorMessage: 'home_page_favorite_err_local'.tr(), - ownerErrorMessage: 'home_page_favorite_err_partner'.tr(), - ); - - if (remoteAssets.isNotEmpty) { - unawaited(handleEditLocation(ref, context, remoteAssets.toList())); - } - } finally { - selectionEnabledHook.value = false; - } - } - - void onToggleLockedVisibility() async { - processing.value = true; - try { - final remoteAssets = ownedRemoteSelection( - localErrorMessage: 'home_page_locked_error_local'.tr(), - ownerErrorMessage: 'home_page_locked_error_partner'.tr(), - ); - if (remoteAssets.isNotEmpty) { - final isInLockedView = ref.read(inLockedViewProvider); - final visibility = isInLockedView ? AssetVisibilityEnum.timeline : AssetVisibilityEnum.locked; - - await handleSetAssetsVisibility(ref, context, visibility, remoteAssets.toList()); - } - } finally { - processing.value = false; - selectionEnabledHook.value = false; - } - } - - Future Function() wrapLongRunningFun(Future Function() fun, {bool showOverlay = true}) => () async { - if (showOverlay) processing.value = true; - try { - final result = await fun(); - if (result.runtimeType != bool || result == true) { - selectionEnabledHook.value = false; - } - return result; - } finally { - if (showOverlay) processing.value = false; - } - }; - - return SafeArea( - top: true, - bottom: false, - child: Stack( - children: [ - ref - .watch(renderListProvider) - .when( - data: (data) => data.isEmpty && (buildLoadingIndicator != null || topWidget == null) - ? (buildLoadingIndicator ?? buildEmptyIndicator)() - : ImmichAssetGrid( - renderList: data, - listener: selectionListener, - selectionActive: selectionEnabledHook.value, - onRefresh: onRefresh == null ? null : wrapLongRunningFun(onRefresh!, showOverlay: false), - topWidget: topWidget, - showStack: stackEnabled, - showDragScrollLabel: dragScrollLabelEnabled, - ), - error: (error, _) => Center(child: Text(error.toString())), - loading: buildLoadingIndicator ?? buildDefaultLoadingIndicator, - ), - if (selectionEnabledHook.value) - ControlBottomAppBar( - key: const ValueKey("controlBottomAppBar"), - onShare: onShareAssets, - onFavorite: favoriteEnabled ? onFavoriteAssets : null, - onArchive: archiveEnabled ? onArchiveAsset : null, - onDelete: deleteEnabled ? onDelete : null, - onDeleteServer: deleteEnabled ? onDeleteRemote : null, - onDownload: downloadEnabled ? onDownload : null, - - /// local file deletion is allowed irrespective of [deleteEnabled] since it has - /// nothing to do with the state of the asset in the Immich server - onDeleteLocal: onDeleteLocal, - onAddToAlbum: onAddToAlbum, - onCreateNewAlbum: onCreateNewAlbum, - onUpload: onUpload, - enabled: !processing.value, - selectionAssetState: selectionAssetState.value, - selectedAssets: selection.value.toList(), - onStack: stackEnabled ? onStack : null, - onEditTime: editEnabled ? onEditTime : null, - onEditLocation: editEnabled ? onEditLocation : null, - unfavorite: unfavorite, - unarchive: unarchive, - onToggleLocked: onToggleLockedVisibility, - onRemoveFromAlbum: onRemoveFromAlbum != null - ? wrapLongRunningFun(() => onRemoveFromAlbum!(selection.value)) - : null, - ), - ], - ), - ); - } -} diff --git a/mobile/lib/widgets/asset_grid/multiselect_grid_status_indicator.dart b/mobile/lib/widgets/asset_grid/multiselect_grid_status_indicator.dart deleted file mode 100644 index 3a1fa82a28..0000000000 --- a/mobile/lib/widgets/asset_grid/multiselect_grid_status_indicator.dart +++ /dev/null @@ -1,26 +0,0 @@ -import 'package:easy_localization/easy_localization.dart'; -import 'package:flutter/material.dart'; -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/providers/asset_viewer/render_list_status_provider.dart'; -import 'package:immich_mobile/widgets/common/delayed_loading_indicator.dart'; - -class MultiselectGridStatusIndicator extends HookConsumerWidget { - const MultiselectGridStatusIndicator({super.key, this.buildLoadingIndicator, this.emptyIndicator}); - - final Widget Function()? buildLoadingIndicator; - final Widget? emptyIndicator; - - @override - Widget build(BuildContext context, WidgetRef ref) { - final renderListStatus = ref.watch(renderListStatusProvider); - return switch (renderListStatus) { - RenderListStatusEnum.loading => - buildLoadingIndicator == null - ? const Center(child: DelayedLoadingIndicator(delay: Duration(milliseconds: 500))) - : buildLoadingIndicator!(), - RenderListStatusEnum.empty => emptyIndicator ?? Center(child: const Text("no_assets_to_show").tr()), - RenderListStatusEnum.error => Center(child: const Text("error_loading_assets").tr()), - RenderListStatusEnum.complete => const SizedBox(), - }; - } -} diff --git a/mobile/lib/widgets/asset_grid/thumbnail_image.dart b/mobile/lib/widgets/asset_grid/thumbnail_image.dart deleted file mode 100644 index 93385b88b3..0000000000 --- a/mobile/lib/widgets/asset_grid/thumbnail_image.dart +++ /dev/null @@ -1,259 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:immich_mobile/constants/constants.dart'; -import 'package:immich_mobile/extensions/build_context_extensions.dart'; -import 'package:immich_mobile/entities/asset.entity.dart'; -import 'package:immich_mobile/extensions/duration_extensions.dart'; -import 'package:immich_mobile/extensions/theme_extensions.dart'; -import 'package:immich_mobile/widgets/common/immich_thumbnail.dart'; - -class ThumbnailImage extends StatelessWidget { - /// The asset to show the thumbnail image for - final Asset asset; - - /// Whether to show the storage indicator icont over the image or not - final bool showStorageIndicator; - - /// Whether to show the show stack icon over the image or not - final bool showStack; - - /// Whether to show the checkmark indicating that this image is selected - final bool isSelected; - - /// Can override [isSelected] and never show the selection indicator - final bool multiselectEnabled; - - /// If we are allowed to deselect this image - final bool canDeselect; - - /// The offset index to apply to this hero tag for animation - final int heroOffset; - - const ThumbnailImage({ - super.key, - required this.asset, - this.showStorageIndicator = true, - this.showStack = false, - this.isSelected = false, - this.multiselectEnabled = false, - this.heroOffset = 0, - this.canDeselect = true, - }); - - @override - Widget build(BuildContext context) { - final assetContainerColor = context.isDarkTheme - ? context.primaryColor.darken(amount: 0.6) - : context.primaryColor.lighten(amount: 0.8); - - return Stack( - children: [ - AnimatedContainer( - duration: const Duration(milliseconds: 300), - curve: Curves.decelerate, - decoration: BoxDecoration( - border: multiselectEnabled && isSelected - ? canDeselect - ? Border.all(color: assetContainerColor, width: 8) - : const Border( - top: BorderSide(color: Colors.grey, width: 8), - right: BorderSide(color: Colors.grey, width: 8), - bottom: BorderSide(color: Colors.grey, width: 8), - left: BorderSide(color: Colors.grey, width: 8), - ) - : const Border(), - ), - child: Stack( - children: [ - _ImageIcon( - heroOffset: heroOffset, - asset: asset, - assetContainerColor: assetContainerColor, - multiselectEnabled: multiselectEnabled, - canDeselect: canDeselect, - isSelected: isSelected, - ), - if (showStorageIndicator) _StorageIcon(storage: asset.storage), - if (asset.isFavorite) - const Positioned(left: 8, bottom: 5, child: Icon(Icons.favorite, color: Colors.white, size: 16)), - if (asset.isVideo) _VideoIcon(duration: asset.duration), - if (asset.stackCount > 0) _StackIcon(isVideo: asset.isVideo, stackCount: asset.stackCount), - ], - ), - ), - if (multiselectEnabled) - isSelected - ? const Padding( - padding: EdgeInsets.all(3.0), - child: Align(alignment: Alignment.topLeft, child: _SelectedIcon()), - ) - : const Icon(Icons.circle_outlined, color: Colors.white), - ], - ); - } -} - -class _SelectedIcon extends StatelessWidget { - const _SelectedIcon(); - - @override - Widget build(BuildContext context) { - final assetContainerColor = context.isDarkTheme - ? context.primaryColor.darken(amount: 0.6) - : context.primaryColor.lighten(amount: 0.8); - - return DecoratedBox( - decoration: BoxDecoration(shape: BoxShape.circle, color: assetContainerColor), - child: Icon(Icons.check_circle_rounded, color: context.primaryColor), - ); - } -} - -class _VideoIcon extends StatelessWidget { - final Duration duration; - - const _VideoIcon({required this.duration}); - - @override - Widget build(BuildContext context) { - return Positioned( - top: 5, - right: 8, - child: Row( - children: [ - Text( - duration.format(), - style: const TextStyle(color: Colors.white, fontSize: 10, fontWeight: FontWeight.bold), - ), - const SizedBox(width: 3), - const Icon(Icons.play_circle_fill_rounded, color: Colors.white, size: 18), - ], - ), - ); - } -} - -class _StackIcon extends StatelessWidget { - final bool isVideo; - final int stackCount; - - const _StackIcon({required this.isVideo, required this.stackCount}); - - @override - Widget build(BuildContext context) { - return Positioned( - top: isVideo ? 28 : 5, - right: 8, - child: Row( - children: [ - if (stackCount > 1) - Text( - "$stackCount", - style: const TextStyle(color: Colors.white, fontSize: 10, fontWeight: FontWeight.bold), - ), - if (stackCount > 1) const SizedBox(width: 3), - const Icon(Icons.burst_mode_rounded, color: Colors.white, size: 18), - ], - ), - ); - } -} - -class _StorageIcon extends StatelessWidget { - final AssetState storage; - - const _StorageIcon({required this.storage}); - - @override - Widget build(BuildContext context) { - return switch (storage) { - AssetState.local => const Positioned( - right: 8, - bottom: 5, - child: Icon( - Icons.cloud_off_outlined, - color: Color.fromRGBO(255, 255, 255, 0.8), - size: 16, - shadows: [Shadow(blurRadius: 5.0, color: Color.fromRGBO(0, 0, 0, 0.6), offset: Offset(0.0, 0.0))], - ), - ), - AssetState.remote => const Positioned( - right: 8, - bottom: 5, - child: Icon( - Icons.cloud_outlined, - color: Color.fromRGBO(255, 255, 255, 0.8), - size: 16, - shadows: [Shadow(blurRadius: 5.0, color: Color.fromRGBO(0, 0, 0, 0.6), offset: Offset(0.0, 0.0))], - ), - ), - AssetState.merged => const Positioned( - right: 8, - bottom: 5, - child: Icon( - Icons.cloud_done_outlined, - color: Color.fromRGBO(255, 255, 255, 0.8), - size: 16, - shadows: [Shadow(blurRadius: 5.0, color: Color.fromRGBO(0, 0, 0, 0.6), offset: Offset(0.0, 0.0))], - ), - ), - }; - } -} - -class _ImageIcon extends StatelessWidget { - final int heroOffset; - final Asset asset; - final Color assetContainerColor; - final bool multiselectEnabled; - final bool canDeselect; - final bool isSelected; - - const _ImageIcon({ - required this.heroOffset, - required this.asset, - required this.assetContainerColor, - required this.multiselectEnabled, - required this.canDeselect, - required this.isSelected, - }); - - @override - Widget build(BuildContext context) { - // Assets from response DTOs do not have an isar id, querying which would give us the default autoIncrement id - final isDto = asset.id == noDbId; - final image = SizedBox.expand( - child: Hero( - tag: isDto ? '${asset.remoteId}-$heroOffset' : asset.id + heroOffset, - child: Stack( - children: [ - SizedBox.expand(child: ImmichThumbnail(asset: asset, height: 250, width: 250)), - const DecoratedBox( - decoration: BoxDecoration( - gradient: LinearGradient( - colors: [ - Color.fromRGBO(0, 0, 0, 0.1), - Colors.transparent, - Colors.transparent, - Color.fromRGBO(0, 0, 0, 0.1), - ], - begin: Alignment.topCenter, - end: Alignment.bottomCenter, - stops: [0, 0.3, 0.6, 1], - ), - ), - ), - ], - ), - ), - ); - - if (!multiselectEnabled || !isSelected) { - return image; - } - - return DecoratedBox( - decoration: canDeselect ? BoxDecoration(color: assetContainerColor) : const BoxDecoration(color: Colors.grey), - child: ClipRRect(borderRadius: const BorderRadius.all(Radius.circular(15.0)), child: image), - ); - } -} diff --git a/mobile/lib/widgets/asset_grid/upload_dialog.dart b/mobile/lib/widgets/asset_grid/upload_dialog.dart deleted file mode 100644 index 86e2759566..0000000000 --- a/mobile/lib/widgets/asset_grid/upload_dialog.dart +++ /dev/null @@ -1,14 +0,0 @@ -import 'package:immich_mobile/widgets/common/confirm_dialog.dart'; - -class UploadDialog extends ConfirmDialog { - final Function onUpload; - - const UploadDialog({super.key, required this.onUpload}) - : super( - title: 'upload_dialog_title', - content: 'upload_dialog_info', - cancel: 'cancel', - ok: 'upload', - onOk: onUpload, - ); -} diff --git a/mobile/lib/widgets/asset_viewer/advanced_bottom_sheet.dart b/mobile/lib/widgets/asset_viewer/advanced_bottom_sheet.dart deleted file mode 100644 index 1a3ef3eac3..0000000000 --- a/mobile/lib/widgets/asset_viewer/advanced_bottom_sheet.dart +++ /dev/null @@ -1,77 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:flutter/services.dart'; -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/extensions/build_context_extensions.dart'; -import 'package:immich_mobile/entities/asset.entity.dart'; - -class AdvancedBottomSheet extends HookConsumerWidget { - final Asset assetDetail; - final ScrollController? scrollController; - - const AdvancedBottomSheet({super.key, required this.assetDetail, this.scrollController}); - - @override - Widget build(BuildContext context, WidgetRef ref) { - return SingleChildScrollView( - controller: scrollController, - child: Container( - margin: const EdgeInsets.symmetric(horizontal: 8.0), - child: LayoutBuilder( - builder: (context, constraints) { - // One column - return Column( - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - const Align(child: Text("ADVANCED INFO", style: TextStyle(fontSize: 12.0))), - const SizedBox(height: 32.0), - Container( - decoration: BoxDecoration( - color: context.isDarkTheme ? Colors.grey[900] : Colors.grey[200], - borderRadius: const BorderRadius.all(Radius.circular(15.0)), - ), - child: Padding( - padding: const EdgeInsets.only(right: 16.0, left: 16, top: 8, bottom: 16), - child: ListView( - shrinkWrap: true, - physics: const NeverScrollableScrollPhysics(), - children: [ - Align( - alignment: Alignment.centerRight, - child: IconButton( - onPressed: () { - Clipboard.setData(ClipboardData(text: assetDetail.toString())).then((_) { - context.scaffoldMessenger.showSnackBar( - SnackBar( - content: Text( - "Copied to clipboard", - style: context.textTheme.bodyLarge?.copyWith(color: context.primaryColor), - ), - ), - ); - }); - }, - icon: Icon(Icons.copy, size: 16.0, color: context.primaryColor), - ), - ), - SelectableText( - assetDetail.toString(), - style: const TextStyle( - fontSize: 12.0, - fontWeight: FontWeight.bold, - fontFamily: "GoogleSansCode", - ), - showCursor: true, - ), - ], - ), - ), - ), - const SizedBox(height: 32.0), - ], - ); - }, - ), - ), - ); - } -} diff --git a/mobile/lib/widgets/asset_viewer/bottom_gallery_bar.dart b/mobile/lib/widgets/asset_viewer/bottom_gallery_bar.dart deleted file mode 100644 index 22a7deffff..0000000000 --- a/mobile/lib/widgets/asset_viewer/bottom_gallery_bar.dart +++ /dev/null @@ -1,362 +0,0 @@ -import 'dart:async'; -import 'dart:io'; - -import 'package:auto_route/auto_route.dart'; -import 'package:easy_localization/easy_localization.dart'; -import 'package:flutter/material.dart'; -import 'package:fluttertoast/fluttertoast.dart'; -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/entities/asset.entity.dart'; -import 'package:immich_mobile/extensions/build_context_extensions.dart'; -import 'package:immich_mobile/pages/editing/edit.page.dart'; -import 'package:immich_mobile/providers/album/album.provider.dart'; -import 'package:immich_mobile/providers/album/current_album.provider.dart'; -import 'package:immich_mobile/providers/asset.provider.dart'; -import 'package:immich_mobile/providers/asset_viewer/asset_stack.provider.dart'; -import 'package:immich_mobile/providers/asset_viewer/current_asset.provider.dart'; -import 'package:immich_mobile/providers/asset_viewer/download.provider.dart'; -import 'package:immich_mobile/providers/asset_viewer/show_controls.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/routing/router.dart'; -import 'package:immich_mobile/services/stack.service.dart'; -import 'package:immich_mobile/utils/hash.dart'; -import 'package:immich_mobile/widgets/asset_grid/asset_grid_data_structure.dart'; -import 'package:immich_mobile/widgets/asset_grid/delete_dialog.dart'; -import 'package:immich_mobile/widgets/asset_viewer/video_controls.dart'; -import 'package:immich_mobile/widgets/common/immich_image.dart'; -import 'package:immich_mobile/widgets/common/immich_toast.dart'; - -class BottomGalleryBar extends ConsumerWidget { - final ValueNotifier assetIndex; - final bool showStack; - final ValueNotifier stackIndex; - final ValueNotifier totalAssets; - final PageController controller; - final RenderList renderList; - - const BottomGalleryBar({ - super.key, - required this.showStack, - required this.stackIndex, - required this.assetIndex, - required this.controller, - required this.totalAssets, - required this.renderList, - }); - - @override - Widget build(BuildContext context, WidgetRef ref) { - final isInLockedView = ref.watch(inLockedViewProvider); - final asset = ref.watch(currentAssetProvider); - if (asset == null) { - return const SizedBox(); - } - final isOwner = asset.ownerId == fastHash(ref.watch(currentUserProvider)?.id ?? ''); - final showControls = ref.watch(showControlsProvider); - final stackId = asset.stackId; - - final stackItems = showStack && stackId != null ? ref.watch(assetStackStateProvider(stackId)) : []; - bool isStackPrimaryAsset = asset.stackPrimaryAssetId == null; - final navStack = AutoRouter.of(context).stackData; - final isTrashEnabled = ref.watch(serverInfoProvider.select((v) => v.serverFeatures.trash)); - final isFromTrash = - isTrashEnabled && navStack.length > 2 && navStack.elementAt(navStack.length - 2).name == TrashRoute.name; - final isInAlbum = ref.watch(currentAlbumProvider)?.isRemote ?? false; - - void removeAssetFromStack() { - if (stackIndex.value > 0 && showStack && stackId != null) { - ref.read(assetStackStateProvider(stackId).notifier).removeChild(stackIndex.value - 1); - } - } - - void handleDelete() async { - Future onDelete(bool force) async { - final isDeleted = await ref.read(assetProvider.notifier).deleteAssets({asset}, force: force); - if (isDeleted && isStackPrimaryAsset) { - // Workaround for asset remaining in the gallery - renderList.deleteAsset(asset); - - // `assetIndex == totalAssets.value - 1` handle the case of removing the last asset - // to not throw the error when the next preCache index is called - if (totalAssets.value == 1 || assetIndex.value == totalAssets.value - 1) { - // Handle only one asset - await context.maybePop(); - } - - totalAssets.value -= 1; - } - if (isDeleted) { - ref.read(currentAssetProvider.notifier).set(renderList.loadAsset(assetIndex.value)); - } - return isDeleted; - } - - // Asset is trashed - if (isTrashEnabled && !isFromTrash) { - final isDeleted = await onDelete(false); - if (isDeleted) { - // Can only trash assets stored in server. Local assets are always permanently removed for now - if (context.mounted && asset.isRemote && isStackPrimaryAsset) { - ImmichToast.show( - durationInSecond: 1, - context: context, - msg: 'asset_trashed'.tr(), - gravity: ToastGravity.BOTTOM, - ); - } - removeAssetFromStack(); - } - return; - } - - // Asset is permanently removed - unawaited( - showDialog( - context: context, - builder: (BuildContext _) { - return DeleteDialog( - onDelete: () async { - final isDeleted = await onDelete(true); - if (isDeleted) { - removeAssetFromStack(); - } - }, - ); - }, - ), - ); - } - - unStack() async { - if (asset.stackId == null) { - return; - } - - await ref.read(stackServiceProvider).deleteStack(asset.stackId!, stackItems); - } - - void showStackActionItems() { - showModalBottomSheet( - context: context, - enableDrag: false, - builder: (BuildContext ctx) { - return SafeArea( - child: Padding( - padding: const EdgeInsets.only(top: 24.0), - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - ListTile( - leading: const Icon(Icons.filter_none_outlined, size: 18), - onTap: () async { - await unStack(); - ctx.pop(); - await context.maybePop(); - }, - title: const Text("viewer_unstack", style: TextStyle(fontWeight: FontWeight.bold)).tr(), - ), - ], - ), - ), - ); - }, - ); - } - - shareAsset() { - if (asset.isOffline) { - ImmichToast.show( - durationInSecond: 1, - context: context, - msg: 'asset_action_share_err_offline'.tr(), - gravity: ToastGravity.BOTTOM, - ); - return; - } - ref.read(downloadStateProvider.notifier).shareAsset(asset, context); - } - - void handleEdit() async { - final image = Image(image: ImmichImage.imageProvider(asset: asset)); - - unawaited( - context.navigator.push( - MaterialPageRoute( - builder: (context) => EditImagePage(asset: asset, image: image, isEdited: false), - ), - ), - ); - } - - handleArchive() { - ref.read(assetProvider.notifier).toggleArchive([asset]); - if (isStackPrimaryAsset) { - context.maybePop(); - return; - } - removeAssetFromStack(); - } - - handleDownload() { - if (asset.isLocal) { - return; - } - if (asset.isOffline) { - ImmichToast.show( - durationInSecond: 1, - context: context, - msg: 'asset_action_share_err_offline'.tr(), - gravity: ToastGravity.BOTTOM, - ); - return; - } - - ref.read(downloadStateProvider.notifier).downloadAsset(asset); - } - - handleRemoveFromAlbum() async { - final album = ref.read(currentAlbumProvider); - final bool isSuccess = album != null && await ref.read(albumProvider.notifier).removeAsset(album, [asset]); - - if (isSuccess) { - // Workaround for asset remaining in the gallery - renderList.deleteAsset(asset); - - if (totalAssets.value == 1) { - // Handle empty viewer - await context.maybePop(); - } else { - // changing this also for the last asset causes the parent to rebuild with an error - totalAssets.value -= 1; - } - if (assetIndex.value == totalAssets.value && assetIndex.value > 0) { - // handle the case of removing the last asset in the list - assetIndex.value -= 1; - } - } else { - ImmichToast.show( - context: context, - msg: "album_viewer_appbar_share_err_remove".tr(), - toastType: ToastType.error, - gravity: ToastGravity.BOTTOM, - ); - } - } - - final List> albumActions = [ - { - BottomNavigationBarItem( - icon: Icon(Platform.isAndroid ? Icons.share_rounded : Icons.ios_share_rounded), - label: 'share'.tr(), - tooltip: 'share'.tr(), - ): (_) => - shareAsset(), - }, - if (asset.isImage && !isInLockedView) - { - BottomNavigationBarItem( - icon: const Icon(Icons.tune_outlined), - label: 'edit'.tr(), - tooltip: 'edit'.tr(), - ): (_) => - handleEdit(), - }, - if (isOwner && !isInLockedView) - { - asset.isArchived - ? BottomNavigationBarItem( - icon: const Icon(Icons.unarchive_rounded), - label: 'unarchive'.tr(), - tooltip: 'unarchive'.tr(), - ) - : BottomNavigationBarItem( - icon: const Icon(Icons.archive_outlined), - label: 'archive'.tr(), - tooltip: 'archive'.tr(), - ): (_) => - handleArchive(), - }, - if (isOwner && asset.stackCount > 0 && !isInLockedView) - { - BottomNavigationBarItem( - icon: const Icon(Icons.burst_mode_outlined), - label: 'stack'.tr(), - tooltip: 'stack'.tr(), - ): (_) => - showStackActionItems(), - }, - if (isOwner && !isInAlbum) - { - BottomNavigationBarItem( - icon: const Icon(Icons.delete_outline), - label: 'delete'.tr(), - tooltip: 'delete'.tr(), - ): (_) => - handleDelete(), - }, - if (!isOwner) - { - BottomNavigationBarItem( - icon: const Icon(Icons.download_outlined), - label: 'download'.tr(), - tooltip: 'download'.tr(), - ): (_) => - handleDownload(), - }, - if (isInAlbum) - { - BottomNavigationBarItem( - icon: const Icon(Icons.remove_circle_outline), - label: 'remove_from_album'.tr(), - tooltip: 'remove_from_album'.tr(), - ): (_) => - handleRemoveFromAlbum(), - }, - ]; - return IgnorePointer( - ignoring: !showControls, - child: AnimatedOpacity( - duration: const Duration(milliseconds: 100), - opacity: showControls ? 1.0 : 0.0, - child: DecoratedBox( - decoration: const BoxDecoration( - gradient: LinearGradient( - begin: Alignment.bottomCenter, - end: Alignment.topCenter, - colors: [Colors.black, Colors.transparent], - ), - ), - position: DecorationPosition.background, - child: Padding( - padding: const EdgeInsets.only(top: 40.0), - child: Column( - children: [ - if (asset.isVideo) VideoControls(videoPlayerName: asset.id.toString()), - BottomNavigationBar( - elevation: 0.0, - backgroundColor: Colors.transparent, - unselectedIconTheme: const IconThemeData(color: Colors.white), - selectedIconTheme: const IconThemeData(color: Colors.white), - unselectedLabelStyle: const TextStyle(color: Colors.white, fontWeight: FontWeight.w500, height: 2.3), - selectedLabelStyle: const TextStyle(color: Colors.white, fontWeight: FontWeight.w500, height: 2.3), - unselectedFontSize: 14, - selectedFontSize: 14, - selectedItemColor: Colors.white, - unselectedItemColor: Colors.white, - showSelectedLabels: true, - showUnselectedLabels: true, - items: albumActions.map((e) => e.keys.first).toList(growable: false), - onTap: (index) { - albumActions[index].values.first.call(index); - }, - ), - ], - ), - ), - ), - ), - ); - } -} diff --git a/mobile/lib/widgets/asset_viewer/center_play_button.dart b/mobile/lib/widgets/asset_viewer/center_play_button.dart deleted file mode 100644 index 55d8be8095..0000000000 --- a/mobile/lib/widgets/asset_viewer/center_play_button.dart +++ /dev/null @@ -1,44 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:immich_mobile/widgets/asset_viewer/animated_play_pause.dart'; - -class CenterPlayButton extends StatelessWidget { - const CenterPlayButton({ - super.key, - required this.backgroundColor, - this.iconColor, - required this.show, - required this.isPlaying, - required this.isFinished, - this.onPressed, - }); - - final Color backgroundColor; - final Color? iconColor; - final bool show; - final bool isPlaying; - final bool isFinished; - final VoidCallback? onPressed; - - @override - Widget build(BuildContext context) { - return Center( - child: UnconstrainedBox( - child: AnimatedOpacity( - opacity: show ? 1.0 : 0.0, - duration: const Duration(milliseconds: 100), - child: DecoratedBox( - decoration: BoxDecoration(color: backgroundColor, shape: BoxShape.circle), - child: IconButton( - iconSize: 32, - padding: const EdgeInsets.all(12.0), - icon: isFinished - ? Icon(Icons.replay, color: iconColor) - : AnimatedPlayPause(color: iconColor, playing: isPlaying), - onPressed: onPressed, - ), - ), - ), - ), - ); - } -} diff --git a/mobile/lib/widgets/asset_viewer/custom_video_player_controls.dart b/mobile/lib/widgets/asset_viewer/custom_video_player_controls.dart deleted file mode 100644 index 09c0e9d091..0000000000 --- a/mobile/lib/widgets/asset_viewer/custom_video_player_controls.dart +++ /dev/null @@ -1,114 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/models/cast/cast_manager_state.dart'; -import 'package:immich_mobile/providers/asset_viewer/current_asset.provider.dart'; -import 'package:immich_mobile/providers/asset_viewer/show_controls.provider.dart'; -import 'package:immich_mobile/providers/asset_viewer/video_player_provider.dart'; -import 'package:immich_mobile/providers/cast.provider.dart'; -import 'package:immich_mobile/utils/hooks/timer_hook.dart'; -import 'package:immich_mobile/widgets/asset_viewer/center_play_button.dart'; -import 'package:immich_mobile/widgets/common/delayed_loading_indicator.dart'; - -class CustomVideoPlayerControls extends HookConsumerWidget { - final String videoId; - final Duration hideTimerDuration; - - const CustomVideoPlayerControls({ - super.key, - required this.videoId, - this.hideTimerDuration = const Duration(seconds: 5), - }); - - @override - Widget build(BuildContext context, WidgetRef ref) { - final assetIsVideo = ref.watch(currentAssetProvider.select((asset) => asset != null && asset.isVideo)); - final showControls = ref.watch(showControlsProvider); - final status = ref.watch(videoPlayerProvider(videoId).select((value) => value.status)); - - final cast = ref.watch(castProvider); - - // A timer to hide the controls - final hideTimer = useTimer(hideTimerDuration, () { - if (!context.mounted) { - return; - } - final s = ref.read(videoPlayerProvider(videoId)).status; - - // Do not hide on paused - if (s != VideoPlaybackStatus.paused && s != VideoPlaybackStatus.completed && assetIsVideo) { - ref.read(showControlsProvider.notifier).show = false; - } - }); - final showBuffering = status == VideoPlaybackStatus.buffering && !cast.isCasting; - - /// Shows the controls and starts the timer to hide them - void showControlsAndStartHideTimer() { - hideTimer.reset(); - ref.read(showControlsProvider.notifier).show = true; - } - - // When playback starts, reset the hide timer - ref.listen(videoPlayerProvider(videoId).select((v) => v.status), (previous, next) { - if (next == VideoPlaybackStatus.playing) { - hideTimer.reset(); - } - }); - - /// Toggles between playing and pausing depending on the state of the video - void togglePlay() { - showControlsAndStartHideTimer(); - - if (cast.isCasting) { - if (cast.castState == CastState.playing) { - ref.read(castProvider.notifier).pause(); - } else if (cast.castState == CastState.paused) { - ref.read(castProvider.notifier).play(); - } else if (cast.castState == CastState.idle) { - // resend the play command since its finished - final asset = ref.read(currentAssetProvider); - if (asset == null) { - return; - } - ref.read(castProvider.notifier).loadMediaOld(asset, true); - } - return; - } - - final notifier = ref.read(videoPlayerProvider(videoId).notifier); - if (status == VideoPlaybackStatus.playing) { - notifier.pause(); - } else if (status == VideoPlaybackStatus.completed) { - notifier.restart(); - } else { - notifier.play(); - } - } - - return GestureDetector( - behavior: HitTestBehavior.opaque, - onTap: showControlsAndStartHideTimer, - child: AbsorbPointer( - absorbing: !showControls, - child: Stack( - children: [ - if (showBuffering) - const Center(child: DelayedLoadingIndicator(fadeInDuration: Duration(milliseconds: 400))) - else - GestureDetector( - onTap: () => ref.read(showControlsProvider.notifier).show = false, - child: CenterPlayButton( - backgroundColor: Colors.black54, - iconColor: Colors.white, - isFinished: status == VideoPlaybackStatus.completed, - isPlaying: - status == VideoPlaybackStatus.playing || (cast.isCasting && cast.castState == CastState.playing), - show: assetIsVideo && showControls, - onPressed: togglePlay, - ), - ), - ], - ), - ), - ); - } -} diff --git a/mobile/lib/widgets/asset_viewer/description_input.dart b/mobile/lib/widgets/asset_viewer/description_input.dart deleted file mode 100644 index b0cefd63fa..0000000000 --- a/mobile/lib/widgets/asset_viewer/description_input.dart +++ /dev/null @@ -1,106 +0,0 @@ -import 'package:easy_localization/easy_localization.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_hooks/flutter_hooks.dart'; -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/domain/models/exif.model.dart'; -import 'package:immich_mobile/entities/asset.entity.dart'; -import 'package:immich_mobile/extensions/build_context_extensions.dart'; -import 'package:immich_mobile/extensions/theme_extensions.dart'; -import 'package:immich_mobile/providers/asset.provider.dart'; -import 'package:immich_mobile/providers/user.provider.dart'; -import 'package:immich_mobile/services/asset.service.dart'; -import 'package:immich_mobile/utils/hash.dart'; -import 'package:immich_mobile/widgets/common/immich_toast.dart'; -import 'package:logging/logging.dart'; - -class DescriptionInput extends HookConsumerWidget { - DescriptionInput({super.key, required this.asset, this.exifInfo}); - - final Asset asset; - final ExifInfo? exifInfo; - final Logger _log = Logger('DescriptionInput'); - - @override - Widget build(BuildContext context, WidgetRef ref) { - final controller = useTextEditingController(); - final focusNode = useFocusNode(); - final isFocus = useState(false); - final isTextEmpty = useState(controller.text.isEmpty); - final assetService = ref.watch(assetServiceProvider); - final owner = ref.watch(currentUserProvider); - final hasError = useState(false); - final assetWithExif = ref.watch(assetDetailProvider(asset)); - final hasDescription = useState(false); - final isOwner = fastHash(owner?.id ?? '') == asset.ownerId; - - useEffect(() { - assetService.getDescription(asset).then((value) { - controller.text = value; - hasDescription.value = value.isNotEmpty; - }); - return null; - }, [assetWithExif.value]); - - if (!isOwner && !hasDescription.value) { - return const SizedBox.shrink(); - } - - submitDescription(String description) async { - hasError.value = false; - try { - await assetService.setDescription(asset, description); - controller.text = description; - } catch (error, stack) { - hasError.value = true; - _log.severe("Error updating description", error, stack); - ImmichToast.show(context: context, msg: "description_input_submit_error".tr(), toastType: ToastType.error); - } - } - - Widget? suffixIcon; - if (hasError.value) { - suffixIcon = const Icon(Icons.warning_outlined); - } else if (!isTextEmpty.value && isFocus.value) { - suffixIcon = IconButton( - onPressed: () { - controller.clear(); - isTextEmpty.value = true; - }, - icon: Icon(Icons.cancel_rounded, color: context.colorScheme.onSurfaceSecondary), - splashRadius: 10, - ); - } - - return TextField( - enabled: isOwner, - focusNode: focusNode, - onTap: () => isFocus.value = true, - onChanged: (value) { - isTextEmpty.value = false; - }, - onTapOutside: (a) async { - isFocus.value = false; - focusNode.unfocus(); - - if (exifInfo?.description != controller.text) { - await submitDescription(controller.text); - } - }, - autofocus: false, - maxLines: null, - keyboardType: TextInputType.multiline, - controller: controller, - style: context.textTheme.labelLarge, - decoration: InputDecoration( - hintText: 'description_input_hint_text'.tr(), - border: InputBorder.none, - suffixIcon: suffixIcon, - enabledBorder: InputBorder.none, - focusedBorder: InputBorder.none, - disabledBorder: InputBorder.none, - errorBorder: InputBorder.none, - focusedErrorBorder: InputBorder.none, - ), - ); - } -} diff --git a/mobile/lib/widgets/asset_viewer/detail_panel/asset_date_time.dart b/mobile/lib/widgets/asset_viewer/detail_panel/asset_date_time.dart deleted file mode 100644 index df8f6593df..0000000000 --- a/mobile/lib/widgets/asset_viewer/detail_panel/asset_date_time.dart +++ /dev/null @@ -1,44 +0,0 @@ -import 'package:easy_localization/easy_localization.dart'; -import 'package:flutter/material.dart'; -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/entities/asset.entity.dart'; -import 'package:immich_mobile/extensions/asset_extensions.dart'; -import 'package:immich_mobile/extensions/build_context_extensions.dart'; -import 'package:immich_mobile/extensions/duration_extensions.dart'; -import 'package:immich_mobile/providers/asset.provider.dart'; -import 'package:immich_mobile/utils/selection_handlers.dart'; - -class AssetDateTime extends ConsumerWidget { - final Asset asset; - - const AssetDateTime({super.key, required this.asset}); - - String getDateTimeString(Asset a) { - final (deltaTime, timeZone) = a.getTZAdjustedTimeAndOffset(); - final date = DateFormat.yMMMEd().format(deltaTime); - final time = DateFormat.jm().format(deltaTime); - return '$date • $time GMT${timeZone.formatAsOffset()}'; - } - - @override - Widget build(BuildContext context, WidgetRef ref) { - final watchedAsset = ref.watch(assetDetailProvider(asset)); - String formattedDateTime = getDateTimeString(asset); - - void editDateTime() async { - await handleEditDateTime(ref, context, [asset]); - - if (watchedAsset.value != null) { - formattedDateTime = getDateTimeString(watchedAsset.value!); - } - } - - return Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Text(formattedDateTime, style: context.textTheme.bodyMedium?.copyWith(fontWeight: FontWeight.w600)), - if (asset.isRemote) IconButton(onPressed: editDateTime, icon: const Icon(Icons.edit_outlined), iconSize: 20), - ], - ); - } -} diff --git a/mobile/lib/widgets/asset_viewer/detail_panel/asset_details.dart b/mobile/lib/widgets/asset_viewer/detail_panel/asset_details.dart deleted file mode 100644 index f0f9a2efcb..0000000000 --- a/mobile/lib/widgets/asset_viewer/detail_panel/asset_details.dart +++ /dev/null @@ -1,40 +0,0 @@ -import 'package:easy_localization/easy_localization.dart'; -import 'package:flutter/material.dart'; -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/domain/models/exif.model.dart'; -import 'package:immich_mobile/entities/asset.entity.dart'; -import 'package:immich_mobile/extensions/build_context_extensions.dart'; -import 'package:immich_mobile/providers/asset.provider.dart'; -import 'package:immich_mobile/widgets/asset_viewer/detail_panel/camera_info.dart'; -import 'package:immich_mobile/widgets/asset_viewer/detail_panel/file_info.dart'; - -class AssetDetails extends ConsumerWidget { - final Asset asset; - final ExifInfo? exifInfo; - - const AssetDetails({super.key, required this.asset, this.exifInfo}); - - @override - Widget build(BuildContext context, WidgetRef ref) { - final assetWithExif = ref.watch(assetDetailProvider(asset)); - final ExifInfo? exifInfo = (assetWithExif.value ?? asset).exifInfo; - - return Padding( - padding: const EdgeInsets.only(top: 24.0), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - "exif_bottom_sheet_details", - style: context.textTheme.labelMedium?.copyWith( - color: context.textTheme.labelMedium?.color?.withAlpha(200), - fontWeight: FontWeight.w600, - ), - ).tr(), - FileInfo(asset: asset), - if (exifInfo?.make != null) CameraInfo(exifInfo: exifInfo!), - ], - ), - ); - } -} diff --git a/mobile/lib/widgets/asset_viewer/detail_panel/asset_location.dart b/mobile/lib/widgets/asset_viewer/detail_panel/asset_location.dart deleted file mode 100644 index 6edf226e8b..0000000000 --- a/mobile/lib/widgets/asset_viewer/detail_panel/asset_location.dart +++ /dev/null @@ -1,88 +0,0 @@ -import 'package:easy_localization/easy_localization.dart'; -import 'package:flutter/material.dart'; -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/domain/models/exif.model.dart'; -import 'package:immich_mobile/entities/asset.entity.dart'; -import 'package:immich_mobile/extensions/build_context_extensions.dart'; -import 'package:immich_mobile/providers/asset.provider.dart'; -import 'package:immich_mobile/utils/selection_handlers.dart'; -import 'package:immich_mobile/widgets/asset_viewer/detail_panel/exif_map.dart'; - -class AssetLocation extends HookConsumerWidget { - final Asset asset; - - const AssetLocation({super.key, required this.asset}); - - @override - Widget build(BuildContext context, WidgetRef ref) { - final assetWithExif = ref.watch(assetDetailProvider(asset)); - final ExifInfo? exifInfo = (assetWithExif.value ?? asset).exifInfo; - final hasCoordinates = exifInfo?.hasCoordinates ?? false; - - void editLocation() { - handleEditLocation(ref, context, [assetWithExif.value ?? asset]); - } - - // Guard no lat/lng - if (!hasCoordinates) { - return asset.isRemote - ? ListTile( - minLeadingWidth: 0, - contentPadding: const EdgeInsets.all(0), - leading: const Icon(Icons.location_on), - title: Text( - "add_a_location", - style: context.textTheme.bodyMedium?.copyWith(fontWeight: FontWeight.w600, color: context.primaryColor), - ).tr(), - onTap: editLocation, - ) - : const SizedBox.shrink(); - } - - Widget getLocationName() { - if (exifInfo == null) { - return const SizedBox.shrink(); - } - - final cityName = exifInfo.city; - final stateName = exifInfo.state; - - bool hasLocationName = (cityName != null && stateName != null); - - return hasLocationName - ? Text("$cityName, $stateName", style: context.textTheme.labelLarge) - : const SizedBox.shrink(); - } - - return Padding( - padding: EdgeInsets.only(top: asset.isRemote ? 0 : 16), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Text( - "exif_bottom_sheet_location", - style: context.textTheme.labelMedium?.copyWith( - color: context.textTheme.labelMedium?.color?.withAlpha(200), - fontWeight: FontWeight.w600, - ), - ).tr(), - if (asset.isRemote) - IconButton(onPressed: editLocation, icon: const Icon(Icons.edit_outlined), iconSize: 20), - ], - ), - asset.isRemote ? const SizedBox.shrink() : const SizedBox(height: 16), - ExifMap(exifInfo: exifInfo!, markerId: asset.remoteId, markerAssetThumbhash: asset.thumbhash), - const SizedBox(height: 16), - getLocationName(), - Text( - "${exifInfo.latitude!.toStringAsFixed(4)}, ${exifInfo.longitude!.toStringAsFixed(4)}", - style: context.textTheme.labelMedium?.copyWith(color: context.textTheme.labelMedium?.color?.withAlpha(150)), - ), - ], - ), - ); - } -} diff --git a/mobile/lib/widgets/asset_viewer/detail_panel/camera_info.dart b/mobile/lib/widgets/asset_viewer/detail_panel/camera_info.dart deleted file mode 100644 index 5ae29d32c7..0000000000 --- a/mobile/lib/widgets/asset_viewer/detail_panel/camera_info.dart +++ /dev/null @@ -1,26 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:immich_mobile/domain/models/exif.model.dart'; -import 'package:immich_mobile/extensions/build_context_extensions.dart'; - -class CameraInfo extends StatelessWidget { - final ExifInfo exifInfo; - - const CameraInfo({super.key, required this.exifInfo}); - - @override - Widget build(BuildContext context) { - final textColor = context.isDarkTheme ? Colors.white : Colors.black; - return ListTile( - contentPadding: const EdgeInsets.all(0), - dense: true, - leading: Icon(Icons.camera, color: textColor.withAlpha(200)), - title: Text("${exifInfo.make} ${exifInfo.model}", style: context.textTheme.labelLarge), - subtitle: exifInfo.f != null || exifInfo.exposureSeconds != null || exifInfo.mm != null || exifInfo.iso != null - ? Text( - "ƒ/${exifInfo.fNumber} ${exifInfo.exposureTime} ${exifInfo.focalLength} mm ISO ${exifInfo.iso ?? ''} ", - style: context.textTheme.bodySmall, - ) - : null, - ); - } -} diff --git a/mobile/lib/widgets/asset_viewer/detail_panel/detail_panel.dart b/mobile/lib/widgets/asset_viewer/detail_panel/detail_panel.dart deleted file mode 100644 index 97c9477c97..0000000000 --- a/mobile/lib/widgets/asset_viewer/detail_panel/detail_panel.dart +++ /dev/null @@ -1,37 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/widgets/asset_viewer/description_input.dart'; -import 'package:immich_mobile/widgets/asset_viewer/detail_panel/asset_date_time.dart'; -import 'package:immich_mobile/widgets/asset_viewer/detail_panel/asset_details.dart'; -import 'package:immich_mobile/widgets/asset_viewer/detail_panel/asset_location.dart'; -import 'package:immich_mobile/widgets/asset_viewer/detail_panel/people_info.dart'; -import 'package:immich_mobile/entities/asset.entity.dart'; - -class DetailPanel extends HookConsumerWidget { - final Asset asset; - final ScrollController? scrollController; - - const DetailPanel({super.key, required this.asset, this.scrollController}); - - @override - Widget build(BuildContext context, WidgetRef ref) { - return ListView( - controller: scrollController, - shrinkWrap: true, - children: [ - Padding( - padding: const EdgeInsets.symmetric(horizontal: 16.0), - child: Column( - children: [ - AssetDateTime(asset: asset), - asset.isRemote ? DescriptionInput(asset: asset) : const SizedBox.shrink(), - PeopleInfo(asset: asset), - AssetLocation(asset: asset), - AssetDetails(asset: asset), - ], - ), - ), - ], - ); - } -} diff --git a/mobile/lib/widgets/asset_viewer/detail_panel/file_info.dart b/mobile/lib/widgets/asset_viewer/detail_panel/file_info.dart deleted file mode 100644 index 78d9ac1776..0000000000 --- a/mobile/lib/widgets/asset_viewer/detail_panel/file_info.dart +++ /dev/null @@ -1,47 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:immich_mobile/entities/asset.entity.dart'; -import 'package:immich_mobile/extensions/build_context_extensions.dart'; -import 'package:immich_mobile/utils/bytes_units.dart'; - -class FileInfo extends StatelessWidget { - final Asset asset; - - const FileInfo({super.key, required this.asset}); - - @override - Widget build(BuildContext context) { - final textColor = context.isDarkTheme ? Colors.white : Colors.black; - - final height = asset.orientatedHeight ?? asset.height; - final width = asset.orientatedWidth ?? asset.width; - String resolution = height != null && width != null ? "$width x $height " : ""; - String fileSize = asset.exifInfo?.fileSize != null ? formatBytes(asset.exifInfo!.fileSize!) : ""; - String text = resolution + fileSize; - final imgSizeString = text.isNotEmpty ? text : null; - - String? title; - String? subtitle; - - if (imgSizeString == null && asset.fileName.isNotEmpty) { - // There is only filename - title = asset.fileName; - } else if (imgSizeString != null && asset.fileName.isNotEmpty) { - // There is both filename and size information - title = asset.fileName; - subtitle = imgSizeString; - } else if (imgSizeString != null && asset.fileName.isEmpty) { - title = imgSizeString; - } else { - return const SizedBox.shrink(); - } - - return ListTile( - contentPadding: const EdgeInsets.all(0), - dense: true, - leading: Icon(Icons.image, color: textColor.withAlpha(200)), - titleAlignment: ListTileTitleAlignment.center, - title: Text(title, style: context.textTheme.labelLarge), - subtitle: subtitle == null ? null : Text(subtitle), - ); - } -} diff --git a/mobile/lib/widgets/asset_viewer/detail_panel/people_info.dart b/mobile/lib/widgets/asset_viewer/detail_panel/people_info.dart deleted file mode 100644 index b96cbc777d..0000000000 --- a/mobile/lib/widgets/asset_viewer/detail_panel/people_info.dart +++ /dev/null @@ -1,91 +0,0 @@ -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/entities/asset.entity.dart'; -import 'package:immich_mobile/extensions/build_context_extensions.dart'; -import 'package:immich_mobile/models/search/search_curated_content.model.dart'; -import 'package:immich_mobile/providers/asset_viewer/asset_people.provider.dart'; -import 'package:immich_mobile/routing/router.dart'; -import 'package:immich_mobile/utils/people.utils.dart'; -import 'package:immich_mobile/widgets/search/curated_people_row.dart'; -import 'package:immich_mobile/widgets/search/person_name_edit_form.dart'; - -class PeopleInfo extends ConsumerWidget { - final Asset asset; - final EdgeInsets? padding; - - const PeopleInfo({super.key, required this.asset, this.padding}); - - @override - Widget build(BuildContext context, WidgetRef ref) { - final peopleProvider = ref.watch(assetPeopleNotifierProvider(asset).notifier); - final people = ref.watch(assetPeopleNotifierProvider(asset)).value?.where((p) => !p.isHidden); - - showPersonNameEditModel(String personId, String personName) { - return showDialog( - context: context, - useRootNavigator: false, - builder: (BuildContext context) { - return PersonNameEditForm(personId: personId, personName: personName); - }, - ).then((_) { - // ensure the people list is up-to-date. - peopleProvider.refresh(); - }); - } - - final curatedPeople = - people - ?.map( - (p) => SearchCuratedContent( - id: p.id, - label: p.name, - subtitle: p.birthDate != null && p.birthDate!.isBefore(asset.fileCreatedAt) - ? formatAge(p.birthDate!, asset.fileCreatedAt) - : null, - ), - ) - .toList() ?? - []; - - return AnimatedCrossFade( - crossFadeState: (people?.isEmpty ?? true) ? CrossFadeState.showFirst : CrossFadeState.showSecond, - duration: const Duration(milliseconds: 200), - firstChild: Container(), - secondChild: Padding( - padding: const EdgeInsets.only(top: 8.0), - child: Column( - children: [ - Padding( - padding: padding ?? EdgeInsets.zero, - child: Align( - alignment: Alignment.topLeft, - child: Text( - "exif_bottom_sheet_people", - style: context.textTheme.labelMedium?.copyWith( - color: context.textTheme.labelMedium?.color?.withAlpha(200), - fontWeight: FontWeight.w600, - ), - ).tr(), - ), - ), - Padding( - padding: const EdgeInsets.only(top: 16.0), - child: CuratedPeopleRow( - padding: padding, - content: curatedPeople, - onTap: (content, index) { - context - .pushRoute(PersonResultRoute(personId: content.id, personName: content.label)) - .then((_) => peopleProvider.refresh()); - }, - onNameTap: (person, index) => {showPersonNameEditModel(person.id, person.label)}, - ), - ), - ], - ), - ), - ); - } -} diff --git a/mobile/lib/widgets/asset_viewer/gallery_app_bar.dart b/mobile/lib/widgets/asset_viewer/gallery_app_bar.dart deleted file mode 100644 index dcb0334801..0000000000 --- a/mobile/lib/widgets/asset_viewer/gallery_app_bar.dart +++ /dev/null @@ -1,119 +0,0 @@ -import 'package:auto_route/auto_route.dart'; -import 'package:easy_localization/easy_localization.dart'; -import 'package:flutter/material.dart'; -import 'package:fluttertoast/fluttertoast.dart'; -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/entities/asset.entity.dart'; -import 'package:immich_mobile/providers/album/current_album.provider.dart'; -import 'package:immich_mobile/providers/asset.provider.dart'; -import 'package:immich_mobile/providers/asset_viewer/current_asset.provider.dart'; -import 'package:immich_mobile/providers/asset_viewer/download.provider.dart'; -import 'package:immich_mobile/providers/asset_viewer/scroll_to_date_notifier.provider.dart'; -import 'package:immich_mobile/providers/asset_viewer/show_controls.provider.dart'; -import 'package:immich_mobile/providers/backup/manual_upload.provider.dart'; -import 'package:immich_mobile/providers/partner.provider.dart'; -import 'package:immich_mobile/providers/tab.provider.dart'; -import 'package:immich_mobile/providers/trash.provider.dart'; -import 'package:immich_mobile/providers/user.provider.dart'; -import 'package:immich_mobile/routing/router.dart'; -import 'package:immich_mobile/utils/hash.dart'; -import 'package:immich_mobile/widgets/album/add_to_album_bottom_sheet.dart'; -import 'package:immich_mobile/widgets/asset_grid/upload_dialog.dart'; -import 'package:immich_mobile/widgets/asset_viewer/top_control_app_bar.dart'; -import 'package:immich_mobile/widgets/common/immich_toast.dart'; - -class GalleryAppBar extends ConsumerWidget { - final void Function() showInfo; - - const GalleryAppBar({super.key, required this.showInfo}); - - @override - Widget build(BuildContext context, WidgetRef ref) { - final asset = ref.watch(currentAssetProvider); - if (asset == null) { - return const SizedBox(); - } - final album = ref.watch(currentAlbumProvider); - final isOwner = asset.ownerId == fastHash(ref.watch(currentUserProvider)?.id ?? ''); - final showControls = ref.watch(showControlsProvider); - - final isPartner = ref.watch(partnerSharedWithProvider).map((e) => fastHash(e.id)).contains(asset.ownerId); - - toggleFavorite(Asset asset) => ref.read(assetProvider.notifier).toggleFavorite([asset]); - - handleActivities() { - if (album != null && album.shared && album.remoteId != null) { - context.pushRoute(const ActivitiesRoute()); - } - } - - handleRestore(Asset asset) async { - final result = await ref.read(trashProvider.notifier).restoreAssets([asset]); - - if (result && context.mounted) { - ImmichToast.show(context: context, msg: 'asset_restored_successfully'.tr(), gravity: ToastGravity.BOTTOM); - } - } - - handleUpload(Asset asset) { - showDialog( - context: context, - builder: (BuildContext _) { - return UploadDialog( - onUpload: () { - ref.read(manualUploadProvider.notifier).uploadAssets(context, [asset]); - }, - ); - }, - ); - } - - addToAlbum(Asset addToAlbumAsset) { - showModalBottomSheet( - elevation: 0, - shape: const RoundedRectangleBorder(borderRadius: BorderRadius.all(Radius.circular(15.0))), - context: context, - builder: (BuildContext _) { - return AddToAlbumBottomSheet(assets: [addToAlbumAsset]); - }, - ); - } - - handleDownloadAsset() { - ref.read(downloadStateProvider.notifier).downloadAsset(asset); - } - - handleLocateAsset() async { - // Go back to the gallery - await context.maybePop(); - await context.navigateTo(const TabControllerRoute(children: [PhotosRoute()])); - ref.read(tabProvider.notifier).update((state) => state = TabEnum.home); - // Scroll to the asset's date - scrollToDateNotifierProvider.scrollToDate(asset.fileCreatedAt); - } - - return IgnorePointer( - ignoring: !showControls, - child: AnimatedOpacity( - duration: const Duration(milliseconds: 100), - opacity: showControls ? 1.0 : 0.0, - child: Container( - color: Colors.black.withValues(alpha: 0.4), - child: TopControlAppBar( - isOwner: isOwner, - isPartner: isPartner, - asset: asset, - onMoreInfoPressed: showInfo, - onLocatePressed: handleLocateAsset, - onFavorite: toggleFavorite, - onRestorePressed: () => handleRestore(asset), - onUploadPressed: asset.isLocal ? () => handleUpload(asset) : null, - onDownloadPressed: asset.isLocal ? null : handleDownloadAsset, - onAddToAlbumPressed: () => addToAlbum(asset), - onActivitiesPressed: handleActivities, - ), - ), - ), - ); - } -} diff --git a/mobile/lib/widgets/asset_viewer/motion_photo_button.dart b/mobile/lib/widgets/asset_viewer/motion_photo_button.dart deleted file mode 100644 index f5479ab86e..0000000000 --- a/mobile/lib/widgets/asset_viewer/motion_photo_button.dart +++ /dev/null @@ -1,22 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/constants/colors.dart'; -import 'package:immich_mobile/providers/asset_viewer/is_motion_video_playing.provider.dart'; - -class MotionPhotoButton extends ConsumerWidget { - const MotionPhotoButton({super.key}); - - @override - Widget build(BuildContext context, WidgetRef ref) { - final isPlaying = ref.watch(isPlayingMotionVideoProvider); - - return IconButton( - onPressed: () { - ref.read(isPlayingMotionVideoProvider.notifier).toggle(); - }, - icon: isPlaying - ? const Icon(Icons.motion_photos_pause_outlined, color: grey200) - : const Icon(Icons.play_circle_outline_rounded, color: grey200), - ); - } -} diff --git a/mobile/lib/widgets/asset_viewer/top_control_app_bar.dart b/mobile/lib/widgets/asset_viewer/top_control_app_bar.dart deleted file mode 100644 index 35f3840797..0000000000 --- a/mobile/lib/widgets/asset_viewer/top_control_app_bar.dart +++ /dev/null @@ -1,182 +0,0 @@ -import 'package:auto_route/auto_route.dart'; -import 'package:flutter/material.dart'; -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/extensions/build_context_extensions.dart'; -import 'package:immich_mobile/providers/activity_statistics.provider.dart'; -import 'package:immich_mobile/providers/album/current_album.provider.dart'; -import 'package:immich_mobile/entities/asset.entity.dart'; -import 'package:immich_mobile/providers/asset.provider.dart'; -import 'package:immich_mobile/providers/routes.provider.dart'; -import 'package:immich_mobile/providers/cast.provider.dart'; -import 'package:immich_mobile/providers/tab.provider.dart'; -import 'package:immich_mobile/providers/websocket.provider.dart'; -import 'package:immich_mobile/widgets/asset_viewer/cast_dialog.dart'; -import 'package:immich_mobile/widgets/asset_viewer/motion_photo_button.dart'; -import 'package:immich_mobile/providers/asset_viewer/current_asset.provider.dart'; - -class TopControlAppBar extends HookConsumerWidget { - const TopControlAppBar({ - super.key, - required this.asset, - required this.onMoreInfoPressed, - required this.onDownloadPressed, - required this.onLocatePressed, - required this.onAddToAlbumPressed, - required this.onRestorePressed, - required this.onFavorite, - required this.onUploadPressed, - required this.isOwner, - required this.onActivitiesPressed, - required this.isPartner, - }); - - final Asset asset; - final Function onMoreInfoPressed; - final VoidCallback? onUploadPressed; - final VoidCallback? onDownloadPressed; - final VoidCallback onLocatePressed; - final VoidCallback onAddToAlbumPressed; - final VoidCallback onRestorePressed; - final VoidCallback onActivitiesPressed; - final Function(Asset) onFavorite; - final bool isOwner; - final bool isPartner; - - @override - Widget build(BuildContext context, WidgetRef ref) { - final isInLockedView = ref.watch(inLockedViewProvider); - const double iconSize = 22.0; - final a = ref.watch(assetWatcher(asset)).value ?? asset; - final album = ref.watch(currentAlbumProvider); - final isCasting = ref.watch(castProvider.select((c) => c.isCasting)); - final websocketConnected = ref.watch(websocketProvider.select((c) => c.isConnected)); - - final comments = album != null && album.remoteId != null && asset.remoteId != null - ? ref.watch(activityStatisticsProvider(album.remoteId!, asset.remoteId)) - : 0; - - Widget buildFavoriteButton(a) { - return IconButton( - onPressed: () => onFavorite(a), - icon: Icon(a.isFavorite ? Icons.favorite : Icons.favorite_border, color: Colors.grey[200]), - ); - } - - Widget buildLocateButton() { - return IconButton( - onPressed: () { - onLocatePressed(); - }, - icon: Icon(Icons.image_search, color: Colors.grey[200]), - ); - } - - Widget buildMoreInfoButton() { - return IconButton( - onPressed: () { - onMoreInfoPressed(); - }, - icon: Icon(Icons.info_outline_rounded, color: Colors.grey[200]), - ); - } - - Widget buildDownloadButton() { - return IconButton( - onPressed: onDownloadPressed, - icon: Icon(Icons.cloud_download_outlined, color: Colors.grey[200]), - ); - } - - Widget buildAddToAlbumButton() { - return IconButton( - onPressed: () { - onAddToAlbumPressed(); - }, - icon: Icon(Icons.add, color: Colors.grey[200]), - ); - } - - Widget buildRestoreButton() { - return IconButton( - onPressed: () { - onRestorePressed(); - }, - icon: Icon(Icons.history_rounded, color: Colors.grey[200]), - ); - } - - Widget buildActivitiesButton() { - return IconButton( - onPressed: () { - onActivitiesPressed(); - }, - icon: Row( - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - Icon(Icons.mode_comment_outlined, color: Colors.grey[200]), - if (comments != 0) - Padding( - padding: const EdgeInsets.only(left: 5), - child: Text( - comments.toString(), - style: TextStyle(fontWeight: FontWeight.bold, color: Colors.grey[200]), - ), - ), - ], - ), - ); - } - - Widget buildUploadButton() { - return IconButton( - onPressed: onUploadPressed, - icon: Icon(Icons.backup_outlined, color: Colors.grey[200]), - ); - } - - Widget buildBackButton() { - return IconButton( - onPressed: () { - context.maybePop(); - }, - icon: Icon(Icons.arrow_back_ios_new_rounded, size: 20.0, color: Colors.grey[200]), - ); - } - - Widget buildCastButton() { - return IconButton( - onPressed: () { - showDialog(context: context, builder: (context) => const CastDialog()); - }, - icon: Icon( - isCasting ? Icons.cast_connected_rounded : Icons.cast_rounded, - size: 20.0, - color: isCasting ? context.primaryColor : Colors.grey[200], - ), - ); - } - - bool isInHomePage = ref.read(tabProvider.notifier).state == TabEnum.home; - bool? isInTrash = ref.read(currentAssetProvider)?.isTrashed; - - return AppBar( - foregroundColor: Colors.grey[100], - backgroundColor: Colors.transparent, - leading: buildBackButton(), - actionsIconTheme: const IconThemeData(size: iconSize), - shape: const Border(), - actions: [ - if (asset.isRemote && isOwner) buildFavoriteButton(a), - if (isOwner && !isInHomePage && !(isInTrash ?? false) && !isInLockedView) buildLocateButton(), - if (asset.livePhotoVideoId != null) const MotionPhotoButton(), - if (asset.isLocal && !asset.isRemote) buildUploadButton(), - if (asset.isRemote && !asset.isLocal && isOwner) buildDownloadButton(), - if (asset.isRemote && (isOwner || isPartner) && !asset.isTrashed && !isInLockedView) buildAddToAlbumButton(), - if (isCasting || (asset.isRemote && websocketConnected)) buildCastButton(), - if (asset.isTrashed) buildRestoreButton(), - if (album != null && album.shared && !isInLockedView) buildActivitiesButton(), - buildMoreInfoButton(), - ], - ); - } -} diff --git a/mobile/lib/widgets/backup/album_info_card.dart b/mobile/lib/widgets/backup/album_info_card.dart deleted file mode 100644 index d635e136bc..0000000000 --- a/mobile/lib/widgets/backup/album_info_card.dart +++ /dev/null @@ -1,185 +0,0 @@ -import 'package:auto_route/auto_route.dart'; -import 'package:easy_localization/easy_localization.dart'; -import 'package:flutter/material.dart'; -import 'package:fluttertoast/fluttertoast.dart'; -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/extensions/build_context_extensions.dart'; -import 'package:immich_mobile/models/backup/available_album.model.dart'; -import 'package:immich_mobile/providers/album/album.provider.dart'; -import 'package:immich_mobile/providers/app_settings.provider.dart'; -import 'package:immich_mobile/providers/backup/backup.provider.dart'; -import 'package:immich_mobile/providers/haptic_feedback.provider.dart'; -import 'package:immich_mobile/routing/router.dart'; -import 'package:immich_mobile/services/app_settings.service.dart'; -import 'package:immich_mobile/widgets/common/immich_toast.dart'; - -class AlbumInfoCard extends HookConsumerWidget { - final AvailableAlbum album; - - const AlbumInfoCard({super.key, required this.album}); - - @override - Widget build(BuildContext context, WidgetRef ref) { - final bool isSelected = ref.watch(backupProvider).selectedBackupAlbums.contains(album); - final bool isExcluded = ref.watch(backupProvider).excludedBackupAlbums.contains(album); - final syncAlbum = ref.watch(appSettingsServiceProvider).getSetting(AppSettingsEnum.syncAlbums); - - final isDarkTheme = context.isDarkTheme; - - ColorFilter selectedFilter = ColorFilter.mode(context.primaryColor.withAlpha(100), BlendMode.darken); - ColorFilter excludedFilter = ColorFilter.mode(Colors.red.withAlpha(75), BlendMode.darken); - ColorFilter unselectedFilter = const ColorFilter.mode(Colors.black, BlendMode.color); - - buildSelectedTextBox() { - if (isSelected) { - return Chip( - visualDensity: VisualDensity.compact, - shape: const RoundedRectangleBorder(borderRadius: BorderRadius.all(Radius.circular(5))), - label: Text( - "album_info_card_backup_album_included", - style: TextStyle( - fontSize: 10, - color: isDarkTheme ? Colors.black : Colors.white, - fontWeight: FontWeight.bold, - ), - ).tr(), - backgroundColor: context.primaryColor, - ); - } else if (isExcluded) { - return Chip( - visualDensity: VisualDensity.compact, - shape: const RoundedRectangleBorder(borderRadius: BorderRadius.all(Radius.circular(5))), - label: Text( - "album_info_card_backup_album_excluded", - style: TextStyle( - fontSize: 10, - color: isDarkTheme ? Colors.black : Colors.white, - fontWeight: FontWeight.bold, - ), - ).tr(), - backgroundColor: Colors.red[300], - ); - } - - return const SizedBox(); - } - - buildImageFilter() { - if (isSelected) { - return selectedFilter; - } else if (isExcluded) { - return excludedFilter; - } else { - return unselectedFilter; - } - } - - return GestureDetector( - onTap: () { - ref.read(hapticFeedbackProvider.notifier).selectionClick(); - - if (isSelected) { - ref.read(backupProvider.notifier).removeAlbumForBackup(album); - } else { - ref.read(backupProvider.notifier).addAlbumForBackup(album); - if (syncAlbum) { - ref.read(albumProvider.notifier).createSyncAlbum(album.name); - } - } - }, - onDoubleTap: () { - ref.read(hapticFeedbackProvider.notifier).selectionClick(); - - if (isExcluded) { - // Remove from exclude album list - ref.read(backupProvider.notifier).removeExcludedAlbumForBackup(album); - } else { - // Add to exclude album list - - if (album.id == 'isAll' || album.name == 'Recents') { - ImmichToast.show( - context: context, - msg: 'Cannot exclude album contains all assets', - toastType: ToastType.error, - gravity: ToastGravity.BOTTOM, - ); - return; - } - - ref.read(backupProvider.notifier).addExcludedAlbumForBackup(album); - } - }, - child: Card( - clipBehavior: Clip.hardEdge, - margin: const EdgeInsets.all(1), - shape: RoundedRectangleBorder( - borderRadius: const BorderRadius.all( - Radius.circular(12), // if you need this - ), - side: BorderSide( - color: isDarkTheme ? const Color.fromARGB(255, 37, 35, 35) : const Color(0xFFC9C9C9), - width: 1, - ), - ), - elevation: 0, - borderOnForeground: false, - child: Column( - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - Expanded( - child: Stack( - clipBehavior: Clip.hardEdge, - children: [ - ColorFiltered( - colorFilter: buildImageFilter(), - child: const Image( - width: double.infinity, - height: double.infinity, - image: AssetImage('assets/immich-logo.png'), - fit: BoxFit.cover, - ), - ), - Positioned(bottom: 10, right: 25, child: buildSelectedTextBox()), - ], - ), - ), - Padding( - padding: const EdgeInsets.only(left: 25), - child: Row( - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Text( - album.name, - style: TextStyle(fontSize: 14, color: context.primaryColor, fontWeight: FontWeight.bold), - ), - Padding( - padding: const EdgeInsets.only(top: 2.0), - child: Text( - album.assetCount.toString() + (album.isAll ? " (${'all'.tr()})" : ""), - style: TextStyle(fontSize: 12, color: Colors.grey[600]), - ), - ), - ], - ), - ), - IconButton( - onPressed: () { - context.pushRoute(AlbumPreviewRoute(album: album.album)); - }, - icon: Icon(Icons.image_outlined, color: context.primaryColor, size: 24), - splashRadius: 25, - ), - ], - ), - ), - ], - ), - ), - ); - } -} diff --git a/mobile/lib/widgets/backup/album_info_list_tile.dart b/mobile/lib/widgets/backup/album_info_list_tile.dart deleted file mode 100644 index 9796f45e8b..0000000000 --- a/mobile/lib/widgets/backup/album_info_list_tile.dart +++ /dev/null @@ -1,98 +0,0 @@ -import 'package:auto_route/auto_route.dart'; -import 'package:flutter/material.dart'; -import 'package:fluttertoast/fluttertoast.dart'; -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/extensions/build_context_extensions.dart'; -import 'package:immich_mobile/models/backup/available_album.model.dart'; -import 'package:immich_mobile/providers/album/album.provider.dart'; -import 'package:immich_mobile/providers/app_settings.provider.dart'; -import 'package:immich_mobile/providers/backup/backup.provider.dart'; -import 'package:immich_mobile/routing/router.dart'; -import 'package:immich_mobile/providers/haptic_feedback.provider.dart'; -import 'package:immich_mobile/services/app_settings.service.dart'; -import 'package:immich_mobile/widgets/common/immich_toast.dart'; - -class AlbumInfoListTile extends HookConsumerWidget { - final AvailableAlbum album; - - const AlbumInfoListTile({super.key, required this.album}); - - @override - Widget build(BuildContext context, WidgetRef ref) { - final bool isSelected = ref.watch(backupProvider).selectedBackupAlbums.contains(album); - final bool isExcluded = ref.watch(backupProvider).excludedBackupAlbums.contains(album); - final syncAlbum = ref.watch(appSettingsServiceProvider).getSetting(AppSettingsEnum.syncAlbums); - - buildTileColor() { - if (isSelected) { - return context.isDarkTheme ? context.primaryColor.withAlpha(100) : context.primaryColor.withAlpha(25); - } else if (isExcluded) { - return context.isDarkTheme ? Colors.red[300]?.withAlpha(150) : Colors.red[100]?.withAlpha(150); - } else { - return Colors.transparent; - } - } - - buildIcon() { - if (isSelected) { - return Icon(Icons.check_circle_rounded, color: context.colorScheme.primary); - } - - if (isExcluded) { - return Icon(Icons.remove_circle_rounded, color: context.colorScheme.error); - } - - return Icon(Icons.circle, color: context.colorScheme.surfaceContainerHighest); - } - - return GestureDetector( - onDoubleTap: () { - ref.watch(hapticFeedbackProvider.notifier).selectionClick(); - - if (isExcluded) { - // Remove from exclude album list - ref.read(backupProvider.notifier).removeExcludedAlbumForBackup(album); - } else { - // Add to exclude album list - - if (album.id == 'isAll' || album.name == 'Recents') { - ImmichToast.show( - context: context, - msg: 'Cannot exclude album contains all assets', - toastType: ToastType.error, - gravity: ToastGravity.BOTTOM, - ); - return; - } - - ref.read(backupProvider.notifier).addExcludedAlbumForBackup(album); - } - }, - child: ListTile( - tileColor: buildTileColor(), - contentPadding: const EdgeInsets.symmetric(vertical: 8, horizontal: 16), - onTap: () { - ref.read(hapticFeedbackProvider.notifier).selectionClick(); - if (isSelected) { - ref.read(backupProvider.notifier).removeAlbumForBackup(album); - } else { - ref.read(backupProvider.notifier).addAlbumForBackup(album); - if (syncAlbum) { - ref.read(albumProvider.notifier).createSyncAlbum(album.name); - } - } - }, - leading: buildIcon(), - title: Text(album.name, style: const TextStyle(fontSize: 14, fontWeight: FontWeight.bold)), - subtitle: Text(album.assetCount.toString()), - trailing: IconButton( - onPressed: () { - context.pushRoute(AlbumPreviewRoute(album: album.album)); - }, - icon: Icon(Icons.image_outlined, color: context.primaryColor, size: 24), - splashRadius: 25, - ), - ), - ); - } -} diff --git a/mobile/lib/widgets/backup/asset_info_table.dart b/mobile/lib/widgets/backup/asset_info_table.dart deleted file mode 100644 index 2cccded2bb..0000000000 --- a/mobile/lib/widgets/backup/asset_info_table.dart +++ /dev/null @@ -1,105 +0,0 @@ -import 'package:easy_localization/easy_localization.dart'; -import 'package:flutter/material.dart'; -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/extensions/build_context_extensions.dart'; -import 'package:immich_mobile/extensions/theme_extensions.dart'; -import 'package:immich_mobile/models/backup/backup_state.model.dart'; -import 'package:immich_mobile/models/backup/current_upload_asset.model.dart'; -import 'package:immich_mobile/providers/backup/backup.provider.dart'; -import 'package:immich_mobile/providers/backup/manual_upload.provider.dart'; - -class BackupAssetInfoTable extends ConsumerWidget { - const BackupAssetInfoTable({super.key}); - - @override - Widget build(BuildContext context, WidgetRef ref) { - final isManualUpload = ref.watch( - backupProvider.select((value) => value.backupProgress == BackUpProgressEnum.manualInProgress), - ); - - final isUploadInProgress = ref.watch( - backupProvider.select( - (value) => - value.backupProgress == BackUpProgressEnum.inProgress || - value.backupProgress == BackUpProgressEnum.inBackground || - value.backupProgress == BackUpProgressEnum.manualInProgress, - ), - ); - - final asset = isManualUpload - ? ref.watch(manualUploadProvider.select((value) => value.currentUploadAsset)) - : ref.watch(backupProvider.select((value) => value.currentUploadAsset)); - - return Padding( - padding: const EdgeInsets.only(top: 8.0), - child: Table( - border: TableBorder.all(color: context.colorScheme.outlineVariant, width: 1), - children: [ - TableRow( - children: [ - TableCell( - verticalAlignment: TableCellVerticalAlignment.middle, - child: Padding( - padding: const EdgeInsets.all(6.0), - child: - Text( - 'backup_controller_page_filename', - style: TextStyle( - color: context.colorScheme.onSurfaceSecondary, - fontWeight: FontWeight.bold, - fontSize: 10.0, - ), - ).tr( - namedArgs: isUploadInProgress - ? {'filename': asset.fileName, 'size': asset.fileType.toLowerCase()} - : {'filename': "-", 'size': "-"}, - ), - ), - ), - ], - ), - TableRow( - children: [ - TableCell( - verticalAlignment: TableCellVerticalAlignment.middle, - child: Padding( - padding: const EdgeInsets.all(6.0), - child: Text( - "backup_controller_page_created", - style: TextStyle( - color: context.colorScheme.onSurfaceSecondary, - fontWeight: FontWeight.bold, - fontSize: 10.0, - ), - ).tr(namedArgs: {'date': isUploadInProgress ? _getAssetCreationDate(asset) : "-"}), - ), - ), - ], - ), - TableRow( - children: [ - TableCell( - child: Padding( - padding: const EdgeInsets.all(6.0), - child: Text( - "backup_controller_page_id", - style: TextStyle( - color: context.colorScheme.onSurfaceSecondary, - fontWeight: FontWeight.bold, - fontSize: 10.0, - ), - ).tr(namedArgs: {'id': isUploadInProgress ? asset.id : "-"}), - ), - ), - ], - ), - ], - ), - ); - } - - @pragma('vm:prefer-inline') - String _getAssetCreationDate(CurrentUploadAsset asset) { - return DateFormat.yMMMMd().format(asset.fileCreatedAt.toLocal()); - } -} diff --git a/mobile/lib/widgets/backup/current_backup_asset_info_box.dart b/mobile/lib/widgets/backup/current_backup_asset_info_box.dart deleted file mode 100644 index c2f94e706a..0000000000 --- a/mobile/lib/widgets/backup/current_backup_asset_info_box.dart +++ /dev/null @@ -1,37 +0,0 @@ -import 'dart:io'; - -import 'package:easy_localization/easy_localization.dart'; -import 'package:flutter/material.dart'; -import 'package:immich_mobile/extensions/build_context_extensions.dart'; -import 'package:immich_mobile/widgets/backup/asset_info_table.dart'; -import 'package:immich_mobile/widgets/backup/error_chip.dart'; -import 'package:immich_mobile/widgets/backup/icloud_download_progress_bar.dart'; -import 'package:immich_mobile/widgets/backup/upload_progress_bar.dart'; -import 'package:immich_mobile/widgets/backup/upload_stats.dart'; - -class CurrentUploadingAssetInfoBox extends StatelessWidget { - const CurrentUploadingAssetInfoBox({super.key}); - - @override - Widget build(BuildContext context) { - return ListTile( - isThreeLine: true, - leading: Icon(Icons.image_outlined, color: context.primaryColor, size: 30), - title: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Text("backup_controller_page_uploading_file_info", style: context.textTheme.titleSmall).tr(), - const BackupErrorChip(), - ], - ), - subtitle: Column( - children: [ - if (Platform.isIOS) const IcloudDownloadProgressBar(), - const BackupUploadProgressBar(), - const BackupUploadStats(), - const BackupAssetInfoTable(), - ], - ), - ); - } -} diff --git a/mobile/lib/widgets/backup/error_chip.dart b/mobile/lib/widgets/backup/error_chip.dart deleted file mode 100644 index 191049cd75..0000000000 --- a/mobile/lib/widgets/backup/error_chip.dart +++ /dev/null @@ -1,28 +0,0 @@ -import 'package:auto_route/auto_route.dart'; -import 'package:flutter/material.dart'; -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/constants/colors.dart'; -import 'package:immich_mobile/providers/backup/error_backup_list.provider.dart'; -import 'package:immich_mobile/routing/router.dart'; -import 'package:immich_mobile/widgets/backup/error_chip_text.dart'; - -class BackupErrorChip extends ConsumerWidget { - const BackupErrorChip({super.key}); - - @override - Widget build(BuildContext context, WidgetRef ref) { - final hasErrors = ref.watch(errorBackupListProvider.select((value) => value.isNotEmpty)); - if (!hasErrors) { - return const SizedBox(); - } - - return ActionChip( - avatar: const Icon(Icons.info, color: red400), - elevation: 1, - visualDensity: VisualDensity.compact, - label: const BackupErrorChipText(), - backgroundColor: Colors.white, - onPressed: () => context.pushRoute(const FailedBackupStatusRoute()), - ); - } -} diff --git a/mobile/lib/widgets/backup/error_chip_text.dart b/mobile/lib/widgets/backup/error_chip_text.dart deleted file mode 100644 index c987dfd331..0000000000 --- a/mobile/lib/widgets/backup/error_chip_text.dart +++ /dev/null @@ -1,22 +0,0 @@ -import 'package:easy_localization/easy_localization.dart'; -import 'package:flutter/material.dart'; -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/constants/colors.dart'; -import 'package:immich_mobile/providers/backup/error_backup_list.provider.dart'; - -class BackupErrorChipText extends ConsumerWidget { - const BackupErrorChipText({super.key}); - - @override - Widget build(BuildContext context, WidgetRef ref) { - final count = ref.watch(errorBackupListProvider).length; - if (count == 0) { - return const SizedBox(); - } - - return const Text( - "backup_controller_page_failed", - style: TextStyle(color: red400, fontWeight: FontWeight.bold, fontSize: 11), - ).tr(namedArgs: {'count': count.toString()}); - } -} diff --git a/mobile/lib/widgets/backup/icloud_download_progress_bar.dart b/mobile/lib/widgets/backup/icloud_download_progress_bar.dart deleted file mode 100644 index 9f0f7ec3eb..0000000000 --- a/mobile/lib/widgets/backup/icloud_download_progress_bar.dart +++ /dev/null @@ -1,43 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/extensions/build_context_extensions.dart'; -import 'package:immich_mobile/models/backup/backup_state.model.dart'; -import 'package:immich_mobile/providers/backup/backup.provider.dart'; -import 'package:immich_mobile/providers/backup/manual_upload.provider.dart'; - -class IcloudDownloadProgressBar extends ConsumerWidget { - const IcloudDownloadProgressBar({super.key}); - @override - Widget build(BuildContext context, WidgetRef ref) { - final isManualUpload = ref.watch( - backupProvider.select((value) => value.backupProgress == BackUpProgressEnum.manualInProgress), - ); - - final isIcloudAsset = isManualUpload - ? ref.watch(manualUploadProvider.select((value) => value.currentUploadAsset.isIcloudAsset)) - : ref.watch(backupProvider.select((value) => value.currentUploadAsset.isIcloudAsset)); - - if (!isIcloudAsset) { - return const SizedBox(); - } - - final iCloudDownloadProgress = ref.watch(backupProvider.select((value) => value.iCloudDownloadProgress)); - - return Padding( - padding: const EdgeInsets.only(top: 8.0), - child: Row( - children: [ - SizedBox(width: 110, child: Text("iCloud Download", style: context.textTheme.labelSmall)), - Expanded( - child: LinearProgressIndicator( - minHeight: 10.0, - value: iCloudDownloadProgress / 100.0, - borderRadius: const BorderRadius.all(Radius.circular(10.0)), - ), - ), - Text(" ${iCloudDownloadProgress ~/ 1}%", style: const TextStyle(fontSize: 12)), - ], - ), - ); - } -} diff --git a/mobile/lib/widgets/backup/ios_debug_info_tile.dart b/mobile/lib/widgets/backup/ios_debug_info_tile.dart deleted file mode 100644 index be333c6460..0000000000 --- a/mobile/lib/widgets/backup/ios_debug_info_tile.dart +++ /dev/null @@ -1,49 +0,0 @@ -import 'package:intl/intl.dart'; -import 'package:flutter/material.dart'; -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/extensions/build_context_extensions.dart'; -import 'package:immich_mobile/extensions/translate_extensions.dart'; -import 'package:immich_mobile/providers/backup/ios_background_settings.provider.dart'; - -/// This is a simple debug widget which should be removed later on when we are -/// more confident about background sync -class IosDebugInfoTile extends HookConsumerWidget { - final IOSBackgroundSettings settings; - const IosDebugInfoTile({super.key, required this.settings}); - - @override - Widget build(BuildContext context, WidgetRef ref) { - final fetch = settings.timeOfLastFetch; - final processing = settings.timeOfLastProcessing; - final processes = settings.numberOfBackgroundTasksQueued; - - final String title; - if (processes == 0) { - title = 'ios_debug_info_no_processes_queued'.t(context: context); - } else { - title = 'ios_debug_info_processes_queued'.t(context: context, args: {'count': processes}); - } - - final df = DateFormat.yMd().add_jm(); - final String subtitle; - if (fetch == null && processing == null) { - subtitle = 'ios_debug_info_no_sync_yet'.t(context: context); - } else if (fetch != null && processing == null) { - subtitle = 'ios_debug_info_fetch_ran_at'.t(context: context, args: {'dateTime': df.format(fetch)}); - } else if (processing != null && fetch == null) { - subtitle = 'ios_debug_info_processing_ran_at'.t(context: context, args: {'dateTime': df.format(processing)}); - } else { - final fetchOrProcessing = fetch!.isAfter(processing!) ? fetch : processing; - subtitle = 'ios_debug_info_last_sync_at'.t(context: context, args: {'dateTime': df.format(fetchOrProcessing)}); - } - - return ListTile( - title: Text( - title, - style: TextStyle(fontWeight: FontWeight.bold, fontSize: 14, color: context.primaryColor), - ), - subtitle: Text(subtitle, style: const TextStyle(fontSize: 14)), - leading: Icon(Icons.bug_report, color: context.primaryColor), - ); - } -} diff --git a/mobile/lib/widgets/backup/upload_progress_bar.dart b/mobile/lib/widgets/backup/upload_progress_bar.dart deleted file mode 100644 index 641ed14878..0000000000 --- a/mobile/lib/widgets/backup/upload_progress_bar.dart +++ /dev/null @@ -1,45 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/extensions/build_context_extensions.dart'; -import 'package:immich_mobile/models/backup/backup_state.model.dart'; -import 'package:immich_mobile/providers/backup/backup.provider.dart'; -import 'package:immich_mobile/providers/backup/manual_upload.provider.dart'; - -class BackupUploadProgressBar extends ConsumerWidget { - const BackupUploadProgressBar({super.key}); - - @override - Widget build(BuildContext context, WidgetRef ref) { - final isManualUpload = ref.watch( - backupProvider.select((value) => value.backupProgress == BackUpProgressEnum.manualInProgress), - ); - - final isIcloudAsset = isManualUpload - ? ref.watch(manualUploadProvider.select((value) => value.currentUploadAsset.isIcloudAsset)) - : ref.watch(backupProvider.select((value) => value.currentUploadAsset.isIcloudAsset)); - - final uploadProgress = isManualUpload - ? ref.watch(manualUploadProvider.select((value) => value.progressInPercentage)) - : ref.watch(backupProvider.select((value) => value.progressInPercentage)); - - return Padding( - padding: const EdgeInsets.only(top: 8.0), - child: Row( - children: [ - if (isIcloudAsset) SizedBox(width: 110, child: Text("Immich Upload", style: context.textTheme.labelSmall)), - Expanded( - child: LinearProgressIndicator( - minHeight: 10.0, - value: uploadProgress / 100.0, - borderRadius: const BorderRadius.all(Radius.circular(10.0)), - ), - ), - Text( - " ${uploadProgress.toStringAsFixed(0)}%", - style: const TextStyle(fontSize: 12, fontFamily: "GoogleSansCode"), - ), - ], - ), - ); - } -} diff --git a/mobile/lib/widgets/backup/upload_stats.dart b/mobile/lib/widgets/backup/upload_stats.dart deleted file mode 100644 index 38f99e53fc..0000000000 --- a/mobile/lib/widgets/backup/upload_stats.dart +++ /dev/null @@ -1,51 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/models/backup/backup_state.model.dart'; -import 'package:immich_mobile/providers/backup/backup.provider.dart'; -import 'package:immich_mobile/providers/backup/manual_upload.provider.dart'; - -class BackupUploadStats extends ConsumerWidget { - const BackupUploadStats({super.key}); - - @override - Widget build(BuildContext context, WidgetRef ref) { - final isManualUpload = ref.watch( - backupProvider.select((value) => value.backupProgress == BackUpProgressEnum.manualInProgress), - ); - - final uploadFileProgress = isManualUpload - ? ref.watch(manualUploadProvider.select((value) => value.progressInFileSize)) - : ref.watch(backupProvider.select((value) => value.progressInFileSize)); - - final uploadFileSpeed = isManualUpload - ? ref.watch(manualUploadProvider.select((value) => value.progressInFileSpeed)) - : ref.watch(backupProvider.select((value) => value.progressInFileSpeed)); - - return Padding( - padding: const EdgeInsets.only(top: 2.0, bottom: 2.0), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Text(uploadFileProgress, style: const TextStyle(fontSize: 10, fontFamily: "GoogleSansCode")), - Text( - _formatUploadFileSpeed(uploadFileSpeed), - style: const TextStyle(fontSize: 10, fontFamily: "GoogleSansCode"), - ), - ], - ), - ); - } - - @pragma('vm:prefer-inline') - String _formatUploadFileSpeed(double uploadFileSpeed) { - if (uploadFileSpeed < 1024) { - return '${uploadFileSpeed.toStringAsFixed(2)} B/s'; - } else if (uploadFileSpeed < 1024 * 1024) { - return '${(uploadFileSpeed / 1024).toStringAsFixed(2)} KB/s'; - } else if (uploadFileSpeed < 1024 * 1024 * 1024) { - return '${(uploadFileSpeed / (1024 * 1024)).toStringAsFixed(2)} MB/s'; - } else { - return '${(uploadFileSpeed / (1024 * 1024 * 1024)).toStringAsFixed(2)} GB/s'; - } - } -} diff --git a/mobile/lib/widgets/common/app_bar_dialog/app_bar_dialog.dart b/mobile/lib/widgets/common/app_bar_dialog/app_bar_dialog.dart index c330fb4649..c6c6b2cff1 100644 --- a/mobile/lib/widgets/common/app_bar_dialog/app_bar_dialog.dart +++ b/mobile/lib/widgets/common/app_bar_dialog/app_bar_dialog.dart @@ -5,18 +5,15 @@ import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:flutter_hooks/flutter_hooks.dart' hide Store; import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/entities/store.entity.dart'; import 'package:immich_mobile/extensions/build_context_extensions.dart'; -import 'package:immich_mobile/models/backup/backup_state.model.dart'; -import 'package:immich_mobile/providers/asset.provider.dart'; +import 'package:immich_mobile/models/server_info/server_disk_info.model.dart'; +import 'package:immich_mobile/pages/common/settings.page.dart'; import 'package:immich_mobile/providers/auth.provider.dart'; import 'package:immich_mobile/providers/backup/backup.provider.dart'; -import 'package:immich_mobile/providers/backup/manual_upload.provider.dart'; import 'package:immich_mobile/providers/infrastructure/readonly_mode.provider.dart'; import 'package:immich_mobile/providers/locale_provider.dart'; import 'package:immich_mobile/providers/user.provider.dart'; import 'package:immich_mobile/providers/websocket.provider.dart'; -import 'package:immich_mobile/pages/common/settings.page.dart'; import 'package:immich_mobile/routing/router.dart'; import 'package:immich_mobile/utils/bytes_units.dart'; import 'package:immich_mobile/widgets/common/app_bar_dialog/app_bar_profile_info.dart'; @@ -32,7 +29,7 @@ class ImmichAppBarDialog extends HookConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { ref.watch(localeProvider); - BackUpState backupState = ref.watch(backupProvider); + ServerDiskInfo backupState = ref.watch(backupProvider); final theme = context.themeData; bool isHorizontal = !context.isMobile; final horizontalPadding = isHorizontal ? 100.0 : 20.0; @@ -128,9 +125,6 @@ class ImmichAppBarDialog extends HookConsumerWidget { isLoggingOut.value = true; await ref.read(authProvider.notifier).logout().whenComplete(() => isLoggingOut.value = false); - ref.read(manualUploadProvider.notifier).cancelBackup(); - ref.read(backupProvider.notifier).cancelBackup(); - unawaited(ref.read(assetProvider.notifier).clearAllAssets()); ref.read(websocketProvider.notifier).disconnect(); unawaited(context.replaceRoute(const LoginRoute())); }, @@ -146,9 +140,9 @@ class ImmichAppBarDialog extends HookConsumerWidget { } Widget buildStorageInformation() { - var percentage = backupState.serverInfo.diskUsagePercentage / 100; - var usedDiskSpace = backupState.serverInfo.diskUse; - var totalDiskSpace = backupState.serverInfo.diskSize; + var percentage = backupState.diskUsagePercentage / 100; + var usedDiskSpace = backupState.diskUse; + var totalDiskSpace = backupState.diskSize; if (user != null && user.hasQuota) { usedDiskSpace = formatBytes(user.quotaUsageInBytes); @@ -275,7 +269,7 @@ class ImmichAppBarDialog extends HookConsumerWidget { ], ), ), - if (Store.isBetaTimelineEnabled && isReadonlyModeEnabled) buildReadonlyMessage(), + if (isReadonlyModeEnabled) buildReadonlyMessage(), buildAppLogButton(), buildFreeUpSpaceButton(), buildSettingButton(), diff --git a/mobile/lib/widgets/common/app_bar_dialog/app_bar_profile_info.dart b/mobile/lib/widgets/common/app_bar_dialog/app_bar_profile_info.dart index a9fdb9a43f..d6881f519a 100644 --- a/mobile/lib/widgets/common/app_bar_dialog/app_bar_profile_info.dart +++ b/mobile/lib/widgets/common/app_bar_dialog/app_bar_profile_info.dart @@ -4,7 +4,6 @@ import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:image_picker/image_picker.dart'; -import 'package:immich_mobile/entities/store.entity.dart'; import 'package:immich_mobile/extensions/build_context_extensions.dart'; import 'package:immich_mobile/extensions/theme_extensions.dart'; import 'package:immich_mobile/providers/auth.provider.dart'; @@ -62,10 +61,6 @@ class AppBarProfileInfoBox extends HookConsumerWidget { } void toggleReadonlyMode() { - // read only mode is only supported int he beta experience - // TODO: remove this check when the beta UI goes stable - if (!Store.isBetaTimelineEnabled) return; - final isReadonlyModeEnabled = ref.watch(readonlyModeProvider); ref.read(readonlyModeProvider.notifier).toggleReadonlyMode(); diff --git a/mobile/lib/widgets/common/drag_sheet.dart b/mobile/lib/widgets/common/drag_sheet.dart deleted file mode 100644 index 5d1fda1beb..0000000000 --- a/mobile/lib/widgets/common/drag_sheet.dart +++ /dev/null @@ -1,54 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:immich_mobile/extensions/build_context_extensions.dart'; - -class CustomDraggingHandle extends StatelessWidget { - const CustomDraggingHandle({super.key}); - - @override - Widget build(BuildContext context) { - return Container( - height: 4, - width: 30, - decoration: BoxDecoration( - color: context.themeData.dividerColor, - borderRadius: const BorderRadius.all(Radius.circular(20)), - ), - ); - } -} - -class ControlBoxButton extends StatelessWidget { - const ControlBoxButton({super.key, required this.label, required this.iconData, this.onPressed, this.onLongPressed}); - - final String label; - final IconData iconData; - final void Function()? onPressed; - final void Function()? onLongPressed; - - @override - Widget build(BuildContext context) { - final minWidth = context.isMobile ? MediaQuery.sizeOf(context).width / 4.5 : 75.0; - - return MaterialButton( - padding: const EdgeInsets.all(10), - shape: const RoundedRectangleBorder(borderRadius: BorderRadius.all(Radius.circular(20))), - onPressed: onPressed, - onLongPress: onLongPressed, - minWidth: minWidth, - child: Column( - mainAxisAlignment: MainAxisAlignment.start, - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - Icon(iconData, size: 24), - const SizedBox(height: 8), - Text( - label, - style: const TextStyle(fontSize: 14.0, fontWeight: FontWeight.w400), - maxLines: 3, - textAlign: TextAlign.center, - ), - ], - ), - ); - } -} diff --git a/mobile/lib/widgets/common/immich_app_bar.dart b/mobile/lib/widgets/common/immich_app_bar.dart deleted file mode 100644 index 56b7e91eec..0000000000 --- a/mobile/lib/widgets/common/immich_app_bar.dart +++ /dev/null @@ -1,170 +0,0 @@ -import 'package:auto_route/auto_route.dart'; -import 'package:easy_localization/easy_localization.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_svg/svg.dart'; -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/extensions/build_context_extensions.dart'; -import 'package:immich_mobile/models/backup/backup_state.model.dart'; -import 'package:immich_mobile/providers/backup/backup.provider.dart'; -import 'package:immich_mobile/providers/cast.provider.dart'; -import 'package:immich_mobile/providers/server_info.provider.dart'; -import 'package:immich_mobile/providers/user.provider.dart'; -import 'package:immich_mobile/routing/router.dart'; -import 'package:immich_mobile/widgets/asset_viewer/cast_dialog.dart'; -import 'package:immich_mobile/widgets/common/app_bar_dialog/app_bar_dialog.dart'; -import 'package:immich_mobile/widgets/common/user_circle_avatar.dart'; - -class ImmichAppBar extends ConsumerWidget implements PreferredSizeWidget { - @override - Size get preferredSize => const Size.fromHeight(kToolbarHeight); - final List? actions; - final bool showUploadButton; - - const ImmichAppBar({super.key, this.actions, this.showUploadButton = true}); - - @override - Widget build(BuildContext context, WidgetRef ref) { - final BackUpState backupState = ref.watch(backupProvider); - final bool isEnableAutoBackup = backupState.backgroundBackup || backupState.autoBackup; - final user = ref.watch(currentUserProvider); - final bool versionWarningPresent = ref.watch(versionWarningPresentProvider(user)); - final isDarkTheme = context.isDarkTheme; - const widgetSize = 30.0; - final isCasting = ref.watch(castProvider.select((c) => c.isCasting)); - - buildProfileIndicator() { - return InkWell( - onTap: () => - showDialog(context: context, useRootNavigator: false, builder: (ctx) => const ImmichAppBarDialog()), - borderRadius: const BorderRadius.all(Radius.circular(12)), - child: Badge( - label: Container( - decoration: BoxDecoration(color: Colors.black, borderRadius: BorderRadius.circular(widgetSize / 2)), - child: const Icon(Icons.info, color: Color.fromARGB(255, 243, 188, 106), size: widgetSize / 2), - ), - backgroundColor: Colors.transparent, - alignment: Alignment.bottomRight, - isLabelVisible: versionWarningPresent, - offset: const Offset(-2, -12), - child: user == null - ? const Icon(Icons.face_outlined, size: widgetSize) - : Semantics( - label: "logged_in_as".tr(namedArgs: {"user": user.name}), - child: UserCircleAvatar(size: 32, user: user), - ), - ), - ); - } - - getBackupBadgeIcon() { - final iconColor = isDarkTheme ? Colors.white : Colors.black; - - if (isEnableAutoBackup) { - if (backupState.backupProgress == BackUpProgressEnum.inProgress) { - return Container( - padding: const EdgeInsets.all(3.5), - child: CircularProgressIndicator( - strokeWidth: 2, - strokeCap: StrokeCap.round, - valueColor: AlwaysStoppedAnimation(iconColor), - semanticsLabel: 'backup_controller_page_backup'.tr(), - ), - ); - } else if (backupState.backupProgress != BackUpProgressEnum.inBackground && - backupState.backupProgress != BackUpProgressEnum.manualInProgress) { - return Icon( - Icons.check_outlined, - size: 9, - color: iconColor, - semanticLabel: 'backup_controller_page_backup'.tr(), - ); - } - } - - if (!isEnableAutoBackup) { - return Icon( - Icons.cloud_off_rounded, - size: 9, - color: iconColor, - semanticLabel: 'backup_controller_page_backup'.tr(), - ); - } - } - - buildBackupIndicator() { - final indicatorIcon = getBackupBadgeIcon(); - final badgeBackground = context.colorScheme.surfaceContainer; - - return InkWell( - onTap: () => context.pushRoute(const BackupControllerRoute()), - borderRadius: const BorderRadius.all(Radius.circular(12)), - child: Badge( - label: Container( - width: widgetSize / 2, - height: widgetSize / 2, - decoration: BoxDecoration( - color: badgeBackground, - border: Border.all(color: context.colorScheme.outline.withValues(alpha: .3)), - borderRadius: BorderRadius.circular(widgetSize / 2), - ), - child: indicatorIcon, - ), - backgroundColor: Colors.transparent, - alignment: Alignment.bottomRight, - isLabelVisible: indicatorIcon != null, - offset: const Offset(-2, -12), - child: Icon(Icons.backup_rounded, size: widgetSize, color: context.primaryColor), - ), - ); - } - - return AppBar( - backgroundColor: context.themeData.appBarTheme.backgroundColor, - shape: const RoundedRectangleBorder(borderRadius: BorderRadius.all(Radius.circular(5))), - automaticallyImplyLeading: false, - centerTitle: false, - title: Builder( - builder: (BuildContext context) { - return Row( - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - Padding( - padding: const EdgeInsets.only(top: 3.0), - child: SvgPicture.asset( - context.isDarkTheme ? 'assets/immich-logo-inline-dark.svg' : 'assets/immich-logo-inline-light.svg', - height: 40, - ), - ), - const Tooltip( - triggerMode: TooltipTriggerMode.tap, - showDuration: Duration(seconds: 4), - message: - "The old timeline is deprecated and will be removed in a future release. Kindly switch to the new timeline under Advanced Settings.", - child: Padding( - padding: EdgeInsets.only(top: 3.0), - child: Icon(Icons.error_rounded, fill: 1, color: Colors.amber, size: 20), - ), - ), - ], - ); - }, - ), - actions: [ - if (actions != null) - ...actions!.map((action) => Padding(padding: const EdgeInsets.only(right: 16), child: action)), - if (isCasting) - Padding( - padding: const EdgeInsets.only(right: 12), - child: IconButton( - onPressed: () { - showDialog(context: context, builder: (context) => const CastDialog()); - }, - icon: Icon(isCasting ? Icons.cast_connected_rounded : Icons.cast_rounded), - ), - ), - if (showUploadButton) Padding(padding: const EdgeInsets.only(right: 20), child: buildBackupIndicator()), - Padding(padding: const EdgeInsets.only(right: 20), child: buildProfileIndicator()), - ], - ); - } -} diff --git a/mobile/lib/widgets/common/immich_image.dart b/mobile/lib/widgets/common/immich_image.dart deleted file mode 100644 index 57978e83ff..0000000000 --- a/mobile/lib/widgets/common/immich_image.dart +++ /dev/null @@ -1,100 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:immich_mobile/domain/models/asset/base_asset.model.dart' as base_asset; -import 'package:immich_mobile/domain/models/store.model.dart'; -import 'package:immich_mobile/entities/asset.entity.dart'; -import 'package:immich_mobile/entities/store.entity.dart'; -import 'package:immich_mobile/extensions/build_context_extensions.dart'; -import 'package:immich_mobile/presentation/widgets/images/local_image_provider.dart'; -import 'package:immich_mobile/presentation/widgets/images/remote_image_provider.dart'; -import 'package:immich_mobile/widgets/asset_grid/thumbnail_placeholder.dart'; -import 'package:octo_image/octo_image.dart'; - -class ImmichImage extends StatelessWidget { - const ImmichImage( - this.asset, { - this.width, - this.height, - this.fit = BoxFit.cover, - this.placeholder = const ThumbnailPlaceholder(), - super.key, - }); - - final Asset? asset; - final Widget? placeholder; - final double? width; - final double? height; - final BoxFit fit; - - // Helper function to return the image provider for the asset - // either by using the asset ID or the asset itself - /// [asset] is the Asset to request, or else use [assetId] to get a remote - /// image provider - static ImageProvider imageProvider({Asset? asset, String? assetId, double width = 1080, double height = 1920}) { - if (asset == null && assetId == null) { - throw Exception('Must supply either asset or assetId'); - } - - if (asset == null) { - return RemoteFullImageProvider( - assetId: assetId!, - thumbhash: '', - assetType: base_asset.AssetType.video, - isAnimated: false, - ); - } - - if (useLocal(asset)) { - return LocalFullImageProvider( - id: asset.localId!, - assetType: base_asset.AssetType.video, - size: Size(width, height), - isAnimated: false, - ); - } else { - return RemoteFullImageProvider( - assetId: asset.remoteId!, - thumbhash: asset.thumbhash ?? '', - assetType: base_asset.AssetType.video, - isAnimated: false, - ); - } - } - - // Whether to use the local asset image provider or a remote one - static bool useLocal(Asset asset) => - !asset.isRemote || asset.isLocal && !Store.get(StoreKey.preferRemoteImage, false); - - @override - Widget build(BuildContext context) { - if (asset == null) { - return Container( - color: Colors.grey, - width: width, - height: height, - child: const Center(child: Icon(Icons.no_photography)), - ); - } - - final imageProviderInstance = ImmichImage.imageProvider(asset: asset, width: context.width, height: context.height); - - return OctoImage( - fadeInDuration: const Duration(milliseconds: 0), - fadeOutDuration: const Duration(milliseconds: 100), - placeholderBuilder: (context) { - if (placeholder != null) { - return placeholder!; - } - return const SizedBox(); - }, - image: imageProviderInstance, - width: width, - height: height, - fit: fit, - errorBuilder: (context, error, stackTrace) { - imageProviderInstance.evict(); - - return Icon(Icons.image_not_supported_outlined, size: 32, color: Colors.red[200]); - }, - ); - } -} diff --git a/mobile/lib/widgets/common/immich_thumbnail.dart b/mobile/lib/widgets/common/immich_thumbnail.dart deleted file mode 100644 index f17353c3aa..0000000000 --- a/mobile/lib/widgets/common/immich_thumbnail.dart +++ /dev/null @@ -1,88 +0,0 @@ -import 'dart:typed_data'; - -import 'package:flutter/material.dart'; -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/presentation/widgets/images/local_image_provider.dart'; -import 'package:immich_mobile/presentation/widgets/images/remote_image_provider.dart'; -import 'package:immich_mobile/entities/asset.entity.dart'; -import 'package:immich_mobile/utils/hooks/blurhash_hook.dart'; -import 'package:immich_mobile/utils/thumbnail_utils.dart'; -import 'package:immich_mobile/widgets/common/immich_image.dart'; -import 'package:immich_mobile/widgets/common/thumbhash_placeholder.dart'; -import 'package:octo_image/octo_image.dart'; -import 'package:immich_mobile/domain/models/asset/base_asset.model.dart' as base_asset; - -class ImmichThumbnail extends HookConsumerWidget { - const ImmichThumbnail({this.asset, this.width = 250, this.height = 250, this.fit = BoxFit.cover, super.key}); - - final Asset? asset; - final double width; - final double height; - final BoxFit fit; - - /// Helper function to return the image provider for the asset thumbnail - /// either by using the asset ID or the asset itself - /// [asset] is the Asset to request, or else use [assetId] to get a remote - /// image provider - static ImageProvider imageProvider({Asset? asset, String? assetId, int thumbnailSize = 256}) { - if (asset == null && assetId == null) { - throw Exception('Must supply either asset or assetId'); - } - - if (asset == null) { - return RemoteImageProvider.thumbnail(assetId: assetId!, thumbhash: ""); - } - - if (ImmichImage.useLocal(asset)) { - return LocalThumbProvider( - id: asset.localId!, - assetType: base_asset.AssetType.video, - size: Size(thumbnailSize.toDouble(), thumbnailSize.toDouble()), - ); - } else { - return RemoteImageProvider.thumbnail(assetId: asset.remoteId!, thumbhash: asset.thumbhash ?? ""); - } - } - - @override - Widget build(BuildContext context, WidgetRef ref) { - Uint8List? blurhash = useBlurHashRef(asset).value; - - if (asset == null) { - return Container( - color: Colors.grey, - width: width, - height: height, - child: const Center(child: Icon(Icons.no_photography)), - ); - } - - final assetAltText = getAltText(asset!.exifInfo, asset!.fileCreatedAt, asset!.type, []); - - final thumbnailProviderInstance = ImmichThumbnail.imageProvider(asset: asset); - - customErrorBuilder(BuildContext ctx, Object error, StackTrace? stackTrace) { - thumbnailProviderInstance.evict(); - - final originalErrorWidgetBuilder = blurHashErrorBuilder(blurhash, fit: fit); - return originalErrorWidgetBuilder(ctx, error, stackTrace); - } - - return Semantics( - label: assetAltText, - child: OctoImage.fromSet( - placeholderFadeInDuration: Duration.zero, - fadeInDuration: Duration.zero, - fadeOutDuration: const Duration(milliseconds: 100), - octoSet: OctoSet( - placeholderBuilder: blurHashPlaceholderBuilder(blurhash, fit: fit), - errorBuilder: customErrorBuilder, - ), - image: thumbnailProviderInstance, - width: width, - height: height, - fit: fit, - ), - ); - } -} diff --git a/mobile/lib/widgets/common/share_dialog.dart b/mobile/lib/widgets/common/share_dialog.dart deleted file mode 100644 index 625390c4b7..0000000000 --- a/mobile/lib/widgets/common/share_dialog.dart +++ /dev/null @@ -1,19 +0,0 @@ -import 'package:easy_localization/easy_localization.dart'; -import 'package:flutter/material.dart'; - -class ShareDialog extends StatelessWidget { - const ShareDialog({super.key}); - - @override - Widget build(BuildContext context) { - return AlertDialog( - content: Column( - mainAxisSize: MainAxisSize.min, - children: [ - const CircularProgressIndicator(), - Container(margin: const EdgeInsets.only(top: 12), child: const Text('share_dialog_preparing').tr()), - ], - ), - ); - } -} diff --git a/mobile/lib/widgets/common/thumbhash_placeholder.dart b/mobile/lib/widgets/common/thumbhash_placeholder.dart index 0cb1222989..8a9c2eb928 100644 --- a/mobile/lib/widgets/common/thumbhash_placeholder.dart +++ b/mobile/lib/widgets/common/thumbhash_placeholder.dart @@ -4,15 +4,6 @@ import 'package:immich_mobile/widgets/asset_grid/thumbnail_placeholder.dart'; import 'package:immich_mobile/widgets/common/fade_in_placeholder_image.dart'; import 'package:octo_image/octo_image.dart'; -/// Simple set to show [OctoPlaceholder.circularProgressIndicator] as -/// placeholder and [OctoError.icon] as error. -OctoSet blurHashOrPlaceholder(Uint8List? blurhash, {BoxFit? fit, Text? errorMessage}) { - return OctoSet( - placeholderBuilder: blurHashPlaceholderBuilder(blurhash, fit: fit), - errorBuilder: blurHashErrorBuilder(blurhash, fit: fit, message: errorMessage), - ); -} - OctoPlaceholderBuilder blurHashPlaceholderBuilder(Uint8List? blurhash, {BoxFit? fit}) { return (context) => blurhash == null ? const ThumbnailPlaceholder() diff --git a/mobile/lib/widgets/forms/change_password_form.dart b/mobile/lib/widgets/forms/change_password_form.dart index 179b05a712..7ed9fa5f1c 100644 --- a/mobile/lib/widgets/forms/change_password_form.dart +++ b/mobile/lib/widgets/forms/change_password_form.dart @@ -1,14 +1,11 @@ -import 'package:easy_localization/easy_localization.dart'; import 'package:auto_route/auto_route.dart'; +import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:fluttertoast/fluttertoast.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/extensions/build_context_extensions.dart'; -import 'package:immich_mobile/providers/backup/backup.provider.dart'; -import 'package:immich_mobile/providers/backup/manual_upload.provider.dart'; import 'package:immich_mobile/providers/auth.provider.dart'; -import 'package:immich_mobile/providers/asset.provider.dart'; import 'package:immich_mobile/providers/websocket.provider.dart'; import 'package:immich_mobile/widgets/common/immich_toast.dart'; @@ -64,10 +61,6 @@ class ChangePasswordForm extends HookConsumerWidget { if (isSuccess) { await ref.read(authProvider.notifier).logout(); - - ref.read(manualUploadProvider.notifier).cancelBackup(); - ref.read(backupProvider.notifier).cancelBackup(); - await ref.read(assetProvider.notifier).clearAllAssets(); ref.read(websocketProvider.notifier).disconnect(); AutoRouter.of(context).back(); diff --git a/mobile/lib/widgets/forms/login/email_input.dart b/mobile/lib/widgets/forms/login/email_input.dart deleted file mode 100644 index 4d90d918ac..0000000000 --- a/mobile/lib/widgets/forms/login/email_input.dart +++ /dev/null @@ -1,41 +0,0 @@ -import 'package:easy_localization/easy_localization.dart'; -import 'package:flutter/material.dart'; - -class EmailInput extends StatelessWidget { - final TextEditingController controller; - final FocusNode? focusNode; - final Function()? onSubmit; - - const EmailInput({super.key, required this.controller, this.focusNode, this.onSubmit}); - - String? _validateInput(String? email) { - if (email == null || email == '') return null; - if (email.endsWith(' ')) return 'login_form_err_trailing_whitespace'.tr(); - if (email.startsWith(' ')) return 'login_form_err_leading_whitespace'.tr(); - if (email.contains(' ') || !email.contains('@')) { - return 'login_form_err_invalid_email'.tr(); - } - return null; - } - - @override - Widget build(BuildContext context) { - return TextFormField( - autofocus: true, - controller: controller, - decoration: InputDecoration( - labelText: 'email'.tr(), - border: const OutlineInputBorder(), - hintText: 'login_form_email_hint'.tr(), - hintStyle: const TextStyle(fontWeight: FontWeight.normal, fontSize: 14), - ), - validator: _validateInput, - autovalidateMode: AutovalidateMode.always, - autofillHints: const [AutofillHints.email], - keyboardType: TextInputType.emailAddress, - onFieldSubmitted: (_) => onSubmit?.call(), - focusNode: focusNode, - textInputAction: TextInputAction.next, - ); - } -} diff --git a/mobile/lib/widgets/forms/login/loading_icon.dart b/mobile/lib/widgets/forms/login/loading_icon.dart deleted file mode 100644 index 052ce43ac7..0000000000 --- a/mobile/lib/widgets/forms/login/loading_icon.dart +++ /dev/null @@ -1,13 +0,0 @@ -import 'package:flutter/material.dart'; - -class LoadingIcon extends StatelessWidget { - const LoadingIcon({super.key}); - - @override - Widget build(BuildContext context) { - return const Padding( - padding: EdgeInsets.only(top: 18.0), - child: SizedBox(width: 24, height: 24, child: FittedBox(child: CircularProgressIndicator(strokeWidth: 2))), - ); - } -} diff --git a/mobile/lib/widgets/forms/login/login_button.dart b/mobile/lib/widgets/forms/login/login_button.dart deleted file mode 100644 index 0f9fb21d8f..0000000000 --- a/mobile/lib/widgets/forms/login/login_button.dart +++ /dev/null @@ -1,19 +0,0 @@ -import 'package:easy_localization/easy_localization.dart'; -import 'package:flutter/material.dart'; -import 'package:hooks_riverpod/hooks_riverpod.dart'; - -class LoginButton extends ConsumerWidget { - final Function() onPressed; - - const LoginButton({super.key, required this.onPressed}); - - @override - Widget build(BuildContext context, WidgetRef ref) { - return ElevatedButton.icon( - style: ElevatedButton.styleFrom(padding: const EdgeInsets.symmetric(vertical: 12)), - onPressed: onPressed, - icon: const Icon(Icons.login_rounded), - label: const Text("login", style: TextStyle(fontSize: 14, fontWeight: FontWeight.bold)).tr(), - ); - } -} diff --git a/mobile/lib/widgets/forms/login/login_form.dart b/mobile/lib/widgets/forms/login/login_form.dart index 2aa770f104..fb3b9c5977 100644 --- a/mobile/lib/widgets/forms/login/login_form.dart +++ b/mobile/lib/widgets/forms/login/login_form.dart @@ -17,7 +17,6 @@ import 'package:immich_mobile/extensions/build_context_extensions.dart'; import 'package:immich_mobile/extensions/translate_extensions.dart'; import 'package:immich_mobile/providers/auth.provider.dart'; import 'package:immich_mobile/providers/background_sync.provider.dart'; -import 'package:immich_mobile/providers/backup/backup.provider.dart'; import 'package:immich_mobile/providers/gallery_permission.provider.dart'; import 'package:immich_mobile/providers/oauth.provider.dart'; import 'package:immich_mobile/providers/server_info.provider.dart'; @@ -34,7 +33,6 @@ import 'package:immich_ui/immich_ui.dart'; import 'package:logging/logging.dart'; import 'package:openapi/api.dart'; import 'package:package_info_plus/package_info_plus.dart'; -import 'package:permission_handler/permission_handler.dart'; class LoginForm extends HookConsumerWidget { LoginForm({super.key}); @@ -246,18 +244,14 @@ class LoginForm extends HookConsumerWidget { if (result.shouldChangePassword && !result.isAdmin) { unawaited(context.pushRoute(const ChangePasswordRoute())); } else { - final isBeta = Store.isBetaTimelineEnabled; - if (isBeta) { - await ref.read(galleryPermissionNotifier.notifier).requestGalleryPermission(); - if (isSyncRemoteDeletionsMode()) { - await getManageMediaPermission(); - } - unawaited(handleSyncFlow()); - ref.read(websocketProvider.notifier).connect(); - unawaited(context.replaceRoute(const TabShellRoute())); - return; + await ref.read(galleryPermissionNotifier.notifier).requestGalleryPermission(); + if (isSyncRemoteDeletionsMode()) { + await getManageMediaPermission(); } - unawaited(context.replaceRoute(const TabControllerRoute())); + unawaited(handleSyncFlow()); + ref.read(websocketProvider.notifier).connect(); + unawaited(context.replaceRoute(const TabShellRoute())); + return; } } catch (error) { ImmichToast.show( @@ -338,21 +332,13 @@ class LoginForm extends HookConsumerWidget { .saveAuthInfo(accessToken: loginResponseDto.accessToken); if (isSuccess) { - final permission = ref.watch(galleryPermissionNotifier); - final isBeta = Store.isBetaTimelineEnabled; - if (!isBeta && (permission.isGranted || permission.isLimited)) { - unawaited(ref.watch(backupProvider.notifier).resumeBackup()); + await ref.read(galleryPermissionNotifier.notifier).requestGalleryPermission(); + if (isSyncRemoteDeletionsMode()) { + await getManageMediaPermission(); } - if (isBeta) { - await ref.read(galleryPermissionNotifier.notifier).requestGalleryPermission(); - if (isSyncRemoteDeletionsMode()) { - await getManageMediaPermission(); - } - unawaited(handleSyncFlow()); - unawaited(context.replaceRoute(const TabShellRoute())); - return; - } - unawaited(context.replaceRoute(const TabControllerRoute())); + unawaited(handleSyncFlow()); + unawaited(context.replaceRoute(const TabShellRoute())); + return; } } catch (error, stack) { log.severe('Error logging in with OAuth: $error', stack); diff --git a/mobile/lib/widgets/map/map_app_bar.dart b/mobile/lib/widgets/map/map_app_bar.dart deleted file mode 100644 index 73706c7661..0000000000 --- a/mobile/lib/widgets/map/map_app_bar.dart +++ /dev/null @@ -1,128 +0,0 @@ -import 'dart:async'; - -import 'package:auto_route/auto_route.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_hooks/flutter_hooks.dart'; -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/entities/asset.entity.dart'; -import 'package:immich_mobile/extensions/build_context_extensions.dart'; -import 'package:immich_mobile/providers/map/map_state.provider.dart'; -import 'package:immich_mobile/utils/immich_loading_overlay.dart'; -import 'package:immich_mobile/utils/selection_handlers.dart'; -import 'package:immich_mobile/widgets/map/map_settings_sheet.dart'; - -class MapAppBar extends HookWidget implements PreferredSizeWidget { - final ValueNotifier> selectedAssets; - - const MapAppBar({super.key, required this.selectedAssets}); - - @override - Widget build(BuildContext context) { - return Padding( - padding: EdgeInsets.only(top: context.padding.top + 25), - child: ValueListenableBuilder( - valueListenable: selectedAssets, - builder: (ctx, value, child) => - value.isNotEmpty ? _SelectionRow(selectedAssets: selectedAssets) : const _NonSelectionRow(), - ), - ); - } - - @override - Size get preferredSize => const Size.fromHeight(100); -} - -class _NonSelectionRow extends StatelessWidget { - const _NonSelectionRow(); - - @override - Widget build(BuildContext context) { - void onSettingsPressed() { - showModalBottomSheet( - elevation: 0.0, - showDragHandle: true, - isScrollControlled: true, - context: context, - builder: (_) => const MapSettingsSheet(), - ); - } - - return Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - ElevatedButton( - onPressed: () => context.maybePop(), - style: ElevatedButton.styleFrom(shape: const CircleBorder()), - child: const Icon(Icons.arrow_back_ios_new_rounded), - ), - ElevatedButton( - onPressed: onSettingsPressed, - style: ElevatedButton.styleFrom(shape: const CircleBorder()), - child: const Icon(Icons.more_vert_rounded), - ), - ], - ); - } -} - -class _SelectionRow extends HookConsumerWidget { - final ValueNotifier> selectedAssets; - - const _SelectionRow({required this.selectedAssets}); - - @override - Widget build(BuildContext context, WidgetRef ref) { - final isProcessing = useProcessingOverlay(); - - Future handleProcessing(FutureOr Function() action, [bool reloadMarkers = false]) async { - isProcessing.value = true; - await action(); - // Reset state - selectedAssets.value = {}; - isProcessing.value = false; - if (reloadMarkers) { - ref.read(mapStateNotifierProvider.notifier).setRefetchMarkers(true); - } - } - - return Row( - children: [ - Padding( - padding: const EdgeInsets.only(left: 20), - child: ElevatedButton.icon( - onPressed: () => selectedAssets.value = {}, - icon: const Icon(Icons.close_rounded), - label: Text( - '${selectedAssets.value.length}', - style: context.textTheme.titleMedium?.copyWith(color: context.colorScheme.onPrimary), - ), - ), - ), - Expanded( - child: Row( - mainAxisAlignment: MainAxisAlignment.end, - children: [ - ElevatedButton( - onPressed: () => handleProcessing(() => handleShareAssets(ref, context, selectedAssets.value.toList())), - style: ElevatedButton.styleFrom(shape: const CircleBorder()), - child: const Icon(Icons.ios_share_rounded), - ), - ElevatedButton( - onPressed: () => - handleProcessing(() => handleFavoriteAssets(ref, context, selectedAssets.value.toList())), - style: ElevatedButton.styleFrom(shape: const CircleBorder()), - child: const Icon(Icons.favorite), - ), - ElevatedButton( - onPressed: () => - handleProcessing(() => handleArchiveAssets(ref, context, selectedAssets.value.toList()), true), - style: ElevatedButton.styleFrom(shape: const CircleBorder()), - child: const Icon(Icons.archive), - ), - ], - ), - ), - ], - ); - } -} diff --git a/mobile/lib/widgets/map/map_asset_grid.dart b/mobile/lib/widgets/map/map_asset_grid.dart deleted file mode 100644 index b6c1e708a7..0000000000 --- a/mobile/lib/widgets/map/map_asset_grid.dart +++ /dev/null @@ -1,289 +0,0 @@ -import 'dart:math' as math; - -import 'package:collection/collection.dart'; -import 'package:easy_localization/easy_localization.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_hooks/flutter_hooks.dart'; -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/entities/asset.entity.dart'; -import 'package:immich_mobile/extensions/build_context_extensions.dart'; -import 'package:immich_mobile/extensions/collection_extensions.dart'; -import 'package:immich_mobile/extensions/translate_extensions.dart'; -import 'package:immich_mobile/models/map/map_event.model.dart'; -import 'package:immich_mobile/providers/db.provider.dart'; -import 'package:immich_mobile/providers/timeline.provider.dart'; -import 'package:immich_mobile/utils/color_filter_generator.dart'; -import 'package:immich_mobile/utils/throttle.dart'; -import 'package:immich_mobile/widgets/asset_grid/asset_grid_data_structure.dart'; -import 'package:immich_mobile/widgets/asset_grid/immich_asset_grid.dart'; -import 'package:immich_mobile/widgets/common/drag_sheet.dart'; -import 'package:logging/logging.dart'; -import 'package:scrollable_positioned_list/scrollable_positioned_list.dart'; - -class MapAssetGrid extends HookConsumerWidget { - final Stream mapEventStream; - final Function(String)? onGridAssetChanged; - final Function(String)? onZoomToAsset; - final Function(bool, Set)? onAssetsSelected; - final ValueNotifier> selectedAssets; - final ScrollController controller; - - const MapAssetGrid({ - required this.mapEventStream, - this.onGridAssetChanged, - this.onZoomToAsset, - this.onAssetsSelected, - required this.selectedAssets, - required this.controller, - super.key, - }); - - @override - Widget build(BuildContext context, WidgetRef ref) { - final log = Logger("MapAssetGrid"); - final assetsInBounds = useState>([]); - final cachedRenderList = useRef(null); - final lastRenderElementIndex = useRef(null); - final assetInSheet = useValueNotifier(null); - final gridScrollThrottler = useThrottler(interval: const Duration(milliseconds: 300)); - - // Add a cache for assets we've already loaded - final assetCache = useRef>({}); - - void handleMapEvents(MapEvent event) async { - if (event is MapAssetsInBoundsUpdated) { - final assetIds = event.assetRemoteIds; - final missingIds = []; - final currentAssets = []; - - for (final id in assetIds) { - final asset = assetCache.value[id]; - if (asset != null) { - currentAssets.add(asset); - } else { - missingIds.add(id); - } - } - - // Only fetch missing assets - if (missingIds.isNotEmpty) { - final newAssets = await ref.read(dbProvider).assets.getAllByRemoteId(missingIds); - - // Add new assets to cache and current list - for (final asset in newAssets) { - if (asset.remoteId != null) { - assetCache.value[asset.remoteId!] = asset; - currentAssets.add(asset); - } - } - } - - assetsInBounds.value = currentAssets; - return; - } - } - - useOnStreamChange(mapEventStream, onData: handleMapEvents); - - // Hard-restrict to 4 assets / row in portrait mode - const assetsPerRow = 4; - - void handleVisibleItems(Iterable positions) { - final orderedPos = positions.sortedByField((p) => p.index); - // Index of row where the items are mostly visible - const partialOffset = 0.20; - final item = orderedPos.firstWhereOrNull((p) => p.itemTrailingEdge > partialOffset); - - // Guard no elements, reset state - // Also fail fast when the sheet is just opened and the user is yet to scroll (i.e leading = 0) - if (item == null || item.itemLeadingEdge == 0) { - lastRenderElementIndex.value = null; - return; - } - - final renderElement = cachedRenderList.value?.elements.elementAtOrNull(item.index); - // Guard no render list or render element - if (renderElement == null) { - return; - } - // Reset index - lastRenderElementIndex.value == item.index; - - // - // | 1 | 2 | 3 | 4 | 5 | 6 | - // - // | 7 | 8 | 9 | - // - // | 10 | - - // Skip through the assets from the previous row - final rowOffset = renderElement.offset; - // Column offset = (total trailingEdge - trailingEdge crossed) / offset for each asset - final totalOffset = item.itemTrailingEdge - item.itemLeadingEdge; - final edgeOffset = - (totalOffset - partialOffset) / - // Round the total count to the next multiple of [assetsPerRow] - ((renderElement.totalCount / assetsPerRow) * assetsPerRow).floor(); - - // trailing should never be above the totalOffset - final columnOffset = (totalOffset - math.min(item.itemTrailingEdge, totalOffset)) ~/ edgeOffset; - final assetOffset = rowOffset + columnOffset; - final selectedAsset = cachedRenderList.value?.allAssets?.elementAtOrNull(assetOffset)?.remoteId; - - if (selectedAsset != null) { - onGridAssetChanged?.call(selectedAsset); - assetInSheet.value = selectedAsset; - } - } - - return Card( - margin: EdgeInsets.zero, - child: Stack( - children: [ - /// The Align and FractionallySizedBox are to prevent the Asset Grid from going behind the - /// _MapSheetDragRegion and thereby displaying content behind the top right and top left curves - Align( - alignment: Alignment.bottomCenter, - child: FractionallySizedBox( - // Place it just below the drag handle - heightFactor: 0.87, - child: assetsInBounds.value.isNotEmpty - ? ref - .watch(assetsTimelineProvider(assetsInBounds.value)) - .when( - data: (renderList) { - // Cache render list here to use it back during visibleItemsListener - cachedRenderList.value = renderList; - return ValueListenableBuilder( - valueListenable: selectedAssets, - builder: (_, value, __) => ImmichAssetGrid( - shrinkWrap: true, - renderList: renderList, - showDragScroll: false, - assetsPerRow: assetsPerRow, - showMultiSelectIndicator: false, - selectionActive: value.isNotEmpty, - listener: onAssetsSelected, - visibleItemsListener: (pos) => gridScrollThrottler.run(() => handleVisibleItems(pos)), - ), - ); - }, - error: (error, stackTrace) { - log.warning("Cannot get assets in the current map bounds", error, stackTrace); - return const SizedBox.shrink(); - }, - loading: () => const SizedBox.shrink(), - ) - : const _MapNoAssetsInSheet(), - ), - ), - _MapSheetDragRegion( - controller: controller, - assetsInBoundCount: assetsInBounds.value.length, - assetInSheet: assetInSheet, - onZoomToAsset: onZoomToAsset, - ), - ], - ), - ); - } -} - -class _MapNoAssetsInSheet extends StatelessWidget { - const _MapNoAssetsInSheet(); - - @override - Widget build(BuildContext context) { - const image = Image(height: 150, width: 150, image: AssetImage('assets/lighthouse.png')); - - return Center( - child: ListView( - shrinkWrap: true, - children: [ - context.isDarkTheme - ? const InvertionFilter( - child: SaturationFilter(saturation: -1, child: BrightnessFilter(brightness: -5, child: image)), - ) - : image, - const SizedBox(height: 20), - Center( - child: Text("map_zoom_to_see_photos".tr(), style: context.textTheme.displayLarge?.copyWith(fontSize: 18)), - ), - ], - ), - ); - } -} - -class _MapSheetDragRegion extends StatelessWidget { - final ScrollController controller; - final int assetsInBoundCount; - final ValueNotifier assetInSheet; - final Function(String)? onZoomToAsset; - - const _MapSheetDragRegion({ - required this.controller, - required this.assetsInBoundCount, - required this.assetInSheet, - this.onZoomToAsset, - }); - - @override - Widget build(BuildContext context) { - final assetsInBoundsText = "map_assets_in_bounds".t(context: context, args: {'count': assetsInBoundCount}); - - return SingleChildScrollView( - controller: controller, - physics: const ClampingScrollPhysics(), - child: Card( - margin: EdgeInsets.zero, - shape: context.isMobile - ? const RoundedRectangleBorder( - borderRadius: BorderRadius.only(topRight: Radius.circular(20), topLeft: Radius.circular(20)), - ) - : const BeveledRectangleBorder(), - elevation: 0.0, - child: Stack( - children: [ - Column( - crossAxisAlignment: CrossAxisAlignment.center, - mainAxisAlignment: MainAxisAlignment.center, - children: [ - const SizedBox(height: 15), - const CustomDraggingHandle(), - const SizedBox(height: 15), - Center( - child: Text( - assetsInBoundsText, - style: TextStyle( - fontSize: 20, - color: context.textTheme.displayLarge?.color?.withValues(alpha: 0.75), - fontWeight: FontWeight.w500, - ), - ), - ), - const SizedBox(height: 8), - ], - ), - ValueListenableBuilder( - valueListenable: assetInSheet, - builder: (_, value, __) => Visibility( - visible: value != null, - child: Positioned( - right: 18, - top: 24, - child: IconButton( - icon: Icon(Icons.map_outlined, color: context.textTheme.displayLarge?.color), - iconSize: 24, - tooltip: 'zoom_to_bounds'.tr(), - onPressed: () => onZoomToAsset?.call(value!), - ), - ), - ), - ), - ], - ), - ), - ); - } -} diff --git a/mobile/lib/widgets/map/map_bottom_sheet.dart b/mobile/lib/widgets/map/map_bottom_sheet.dart deleted file mode 100644 index fba9e9a041..0000000000 --- a/mobile/lib/widgets/map/map_bottom_sheet.dart +++ /dev/null @@ -1,98 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:flutter_hooks/flutter_hooks.dart'; -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/entities/asset.entity.dart'; -import 'package:immich_mobile/extensions/build_context_extensions.dart'; -import 'package:immich_mobile/models/map/map_event.model.dart'; -import 'package:immich_mobile/utils/draggable_scroll_controller.dart'; -import 'package:immich_mobile/widgets/map/map_asset_grid.dart'; - -class MapBottomSheet extends HookConsumerWidget { - final Stream mapEventStream; - final Function(String)? onGridAssetChanged; - final Function(String)? onZoomToAsset; - final Function()? onZoomToLocation; - final Function(bool, Set)? onAssetsSelected; - final ValueNotifier> selectedAssets; - - const MapBottomSheet({ - required this.mapEventStream, - this.onGridAssetChanged, - this.onZoomToAsset, - this.onAssetsSelected, - this.onZoomToLocation, - required this.selectedAssets, - super.key, - }); - - @override - Widget build(BuildContext context, WidgetRef ref) { - const sheetMinExtent = 0.1; - final sheetController = useDraggableScrollController(); - final bottomSheetOffset = useValueNotifier(sheetMinExtent); - final isBottomSheetOpened = useRef(false); - - void handleMapEvents(MapEvent event) async { - if (event is MapCloseBottomSheet) { - await sheetController.animateTo( - 0.1, - duration: const Duration(milliseconds: 200), - curve: Curves.linearToEaseOut, - ); - } - } - - useOnStreamChange(mapEventStream, onData: handleMapEvents); - - bool onScrollNotification(DraggableScrollableNotification notification) { - isBottomSheetOpened.value = notification.extent > (notification.maxExtent * 0.9); - bottomSheetOffset.value = notification.extent; - // do not bubble - return true; - } - - return Stack( - children: [ - NotificationListener( - onNotification: onScrollNotification, - child: DraggableScrollableSheet( - controller: sheetController, - minChildSize: sheetMinExtent, - maxChildSize: 0.8, - initialChildSize: sheetMinExtent, - snap: true, - snapSizes: [sheetMinExtent, 0.5, 0.8], - shouldCloseOnMinExtent: false, - builder: (ctx, scrollController) => MapAssetGrid( - controller: scrollController, - mapEventStream: mapEventStream, - selectedAssets: selectedAssets, - onAssetsSelected: onAssetsSelected, - // Do not bother with the event if the bottom sheet is not user scrolled - onGridAssetChanged: (assetId) => isBottomSheetOpened.value ? onGridAssetChanged?.call(assetId) : null, - onZoomToAsset: onZoomToAsset, - ), - ), - ), - ValueListenableBuilder( - valueListenable: bottomSheetOffset, - builder: (context, value, child) { - return Positioned( - right: 0, - bottom: context.height * (value + 0.02), - child: AnimatedOpacity( - opacity: value < 0.8 ? 1 : 0, - duration: const Duration(milliseconds: 150), - child: ElevatedButton( - onPressed: onZoomToLocation, - style: ElevatedButton.styleFrom(shape: const CircleBorder()), - child: const Icon(Icons.my_location), - ), - ), - ); - }, - ), - ], - ); - } -} diff --git a/mobile/lib/widgets/map/map_settings_sheet.dart b/mobile/lib/widgets/map/map_settings_sheet.dart deleted file mode 100644 index 644056d153..0000000000 --- a/mobile/lib/widgets/map/map_settings_sheet.dart +++ /dev/null @@ -1,61 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/providers/map/map_state.provider.dart'; -import 'package:immich_mobile/widgets/map/map_settings/map_settings_list_tile.dart'; -import 'package:immich_mobile/widgets/map/map_settings/map_settings_time_dropdown.dart'; -import 'package:immich_mobile/widgets/map/map_settings/map_theme_picker.dart'; - -class MapSettingsSheet extends HookConsumerWidget { - const MapSettingsSheet({super.key}); - - @override - Widget build(BuildContext context, WidgetRef ref) { - final mapState = ref.watch(mapStateNotifierProvider); - - return DraggableScrollableSheet( - expand: false, - initialChildSize: 0.6, - builder: (ctx, scrollController) => SingleChildScrollView( - controller: scrollController, - child: Card( - elevation: 0.0, - shadowColor: Colors.transparent, - margin: EdgeInsets.zero, - child: Column( - mainAxisSize: MainAxisSize.max, - children: [ - MapThemePicker( - themeMode: mapState.themeMode, - onThemeChange: (mode) => ref.read(mapStateNotifierProvider.notifier).switchTheme(mode), - ), - const Divider(height: 30, thickness: 2), - MapSettingsListTile( - title: "map_settings_only_show_favorites", - selected: mapState.showFavoriteOnly, - onChanged: (favoriteOnly) => - ref.read(mapStateNotifierProvider.notifier).switchFavoriteOnly(favoriteOnly), - ), - MapSettingsListTile( - title: "map_settings_include_show_archived", - selected: mapState.includeArchived, - onChanged: (includeArchive) => - ref.read(mapStateNotifierProvider.notifier).switchIncludeArchived(includeArchive), - ), - MapSettingsListTile( - title: "map_settings_include_show_partners", - selected: mapState.withPartners, - onChanged: (withPartners) => - ref.read(mapStateNotifierProvider.notifier).switchWithPartners(withPartners), - ), - MapTimeDropDown( - relativeTime: mapState.relativeTime, - onTimeChange: (time) => ref.read(mapStateNotifierProvider.notifier).setRelativeTime(time), - ), - const SizedBox(height: 20), - ], - ), - ), - ), - ); - } -} diff --git a/mobile/lib/widgets/map/positioned_asset_marker_icon.dart b/mobile/lib/widgets/map/positioned_asset_marker_icon.dart deleted file mode 100644 index b6d7241cf4..0000000000 --- a/mobile/lib/widgets/map/positioned_asset_marker_icon.dart +++ /dev/null @@ -1,43 +0,0 @@ -import 'dart:io'; -import 'dart:math'; - -import 'package:flutter/material.dart'; -import 'package:immich_mobile/extensions/build_context_extensions.dart'; -import 'package:immich_mobile/widgets/map/asset_marker_icon.dart'; - -class PositionedAssetMarkerIcon extends StatelessWidget { - final Point point; - final String assetRemoteId; - final String assetThumbhash; - final double size; - final int durationInMilliseconds; - - final Function()? onTap; - - const PositionedAssetMarkerIcon({ - required this.point, - required this.assetRemoteId, - required this.assetThumbhash, - this.size = 100, - this.durationInMilliseconds = 100, - this.onTap, - super.key, - }); - - @override - Widget build(BuildContext context) { - final ratio = Platform.isIOS ? 1.0 : context.devicePixelRatio; - return AnimatedPositioned( - left: point.x / ratio - size / 2, - top: point.y / ratio - size, - duration: Duration(milliseconds: durationInMilliseconds), - child: GestureDetector( - onTap: () => onTap?.call(), - child: SizedBox.square( - dimension: size, - child: AssetMarkerIcon(id: assetRemoteId, thumbhash: assetThumbhash, key: Key(assetRemoteId)), - ), - ), - ); - } -} diff --git a/mobile/lib/widgets/memories/memory_bottom_info.dart b/mobile/lib/widgets/memories/memory_bottom_info.dart deleted file mode 100644 index 4b43821782..0000000000 --- a/mobile/lib/widgets/memories/memory_bottom_info.dart +++ /dev/null @@ -1,50 +0,0 @@ -// ignore_for_file: require_trailing_commas - -import 'package:auto_route/auto_route.dart'; -import 'package:easy_localization/easy_localization.dart'; -import 'package:flutter/material.dart'; -import 'package:immich_mobile/models/memories/memory.model.dart'; -import 'package:immich_mobile/providers/asset_viewer/scroll_to_date_notifier.provider.dart'; - -class MemoryBottomInfo extends StatelessWidget { - final Memory memory; - - const MemoryBottomInfo({super.key, required this.memory}); - - @override - Widget build(BuildContext context) { - final df = DateFormat.yMMMMd(); - return Padding( - padding: const EdgeInsets.all(16.0), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - memory.title, - style: TextStyle(color: Colors.grey[400], fontSize: 13.0, fontWeight: FontWeight.w500), - ), - Text( - df.format(memory.assets[0].fileCreatedAt), - style: const TextStyle(color: Colors.white, fontSize: 15.0, fontWeight: FontWeight.w500), - ), - ], - ), - MaterialButton( - minWidth: 0, - onPressed: () { - context.maybePop(); - scrollToDateNotifierProvider.scrollToDate(memory.assets[0].fileCreatedAt); - }, - shape: const CircleBorder(), - color: Colors.white.withValues(alpha: 0.2), - elevation: 0, - child: const Icon(Icons.open_in_new, color: Colors.white), - ), - ], - ), - ); - } -} diff --git a/mobile/lib/widgets/memories/memory_card.dart b/mobile/lib/widgets/memories/memory_card.dart deleted file mode 100644 index 189cc67428..0000000000 --- a/mobile/lib/widgets/memories/memory_card.dart +++ /dev/null @@ -1,117 +0,0 @@ -import 'dart:ui'; - -import 'package:flutter/material.dart'; -import 'package:flutter_hooks/flutter_hooks.dart'; -import 'package:immich_mobile/entities/asset.entity.dart'; -import 'package:immich_mobile/extensions/build_context_extensions.dart'; -import 'package:immich_mobile/pages/common/native_video_viewer.page.dart'; -import 'package:immich_mobile/utils/hooks/blurhash_hook.dart'; -import 'package:immich_mobile/widgets/common/immich_image.dart'; - -class MemoryCard extends StatelessWidget { - final Asset asset; - final String title; - final bool showTitle; - final Function()? onVideoEnded; - - const MemoryCard({required this.asset, required this.title, required this.showTitle, this.onVideoEnded, super.key}); - - @override - Widget build(BuildContext context) { - return Card( - color: Colors.black, - shape: const RoundedRectangleBorder( - borderRadius: BorderRadius.all(Radius.circular(25.0)), - side: BorderSide(color: Colors.black, width: 1.0), - ), - clipBehavior: Clip.hardEdge, - child: Stack( - children: [ - SizedBox.expand(child: _BlurredBackdrop(asset: asset)), - LayoutBuilder( - builder: (context, constraints) { - // Determine the fit using the aspect ratio - BoxFit fit = BoxFit.contain; - if (asset.width != null && asset.height != null) { - final aspectRatio = asset.width! / asset.height!; - final phoneAspectRatio = constraints.maxWidth / constraints.maxHeight; - // Look for a 25% difference in either direction - if (phoneAspectRatio * .75 < aspectRatio && phoneAspectRatio * 1.25 > aspectRatio) { - // Cover to look nice if we have nearly the same aspect ratio - fit = BoxFit.cover; - } - } - - if (asset.isImage) { - return Hero( - tag: 'memory-${asset.id}', - child: ImmichImage(asset, fit: fit, height: double.infinity, width: double.infinity), - ); - } else { - return Hero( - tag: 'memory-${asset.id}', - child: SizedBox( - width: context.width, - height: context.height, - child: NativeVideoViewerPage( - key: ValueKey(asset.id), - asset: asset, - showControls: false, - playbackDelayFactor: 2, - image: ImmichImage(asset, width: context.width, height: context.height, fit: BoxFit.contain), - ), - ), - ); - } - }, - ), - if (showTitle) - Positioned( - left: 18.0, - bottom: 18.0, - child: Text( - title, - style: context.textTheme.headlineMedium?.copyWith(color: Colors.white, fontWeight: FontWeight.w500), - ), - ), - ], - ), - ); - } -} - -class _BlurredBackdrop extends HookWidget { - final Asset asset; - - const _BlurredBackdrop({required this.asset}); - - @override - Widget build(BuildContext context) { - final blurhash = useBlurHashRef(asset).value; - if (blurhash != null) { - // Use a nice cheap blur hash image decoration - return Container( - decoration: BoxDecoration( - image: DecorationImage(image: MemoryImage(blurhash), fit: BoxFit.cover), - ), - child: Container(color: Colors.black.withValues(alpha: 0.2)), - ); - } else { - // Fall back to using a more expensive image filtered - // Since the ImmichImage is already precached, we can - // safely use that as the image provider - return ImageFiltered( - imageFilter: ImageFilter.blur(sigmaX: 30, sigmaY: 30), - child: Container( - decoration: BoxDecoration( - image: DecorationImage( - image: ImmichImage.imageProvider(asset: asset, height: context.height, width: context.width), - fit: BoxFit.cover, - ), - ), - child: Container(color: Colors.black.withValues(alpha: 0.2)), - ), - ); - } - } -} diff --git a/mobile/lib/widgets/memories/memory_lane.dart b/mobile/lib/widgets/memories/memory_lane.dart deleted file mode 100644 index 4cba83bea7..0000000000 --- a/mobile/lib/widgets/memories/memory_lane.dart +++ /dev/null @@ -1,91 +0,0 @@ -import 'package:auto_route/auto_route.dart'; -import 'package:collection/collection.dart'; -import 'package:flutter/material.dart'; -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/models/memories/memory.model.dart'; -import 'package:immich_mobile/widgets/asset_grid/thumbnail_placeholder.dart'; -import 'package:immich_mobile/providers/asset_viewer/current_asset.provider.dart'; -import 'package:immich_mobile/providers/memory.provider.dart'; -import 'package:immich_mobile/routing/router.dart'; -import 'package:immich_mobile/providers/haptic_feedback.provider.dart'; -import 'package:immich_mobile/widgets/common/immich_image.dart'; - -class MemoryLane extends HookConsumerWidget { - const MemoryLane({super.key}); - - @override - Widget build(BuildContext context, WidgetRef ref) { - final memoryLaneFutureProvider = ref.watch(memoryFutureProvider); - - final memoryLane = memoryLaneFutureProvider - .whenData( - (memories) => memories != null - ? ConstrainedBox( - constraints: const BoxConstraints(maxHeight: 200), - child: CarouselView( - itemExtent: 145.0, - shrinkExtent: 1.0, - elevation: 2, - backgroundColor: Colors.black, - overlayColor: WidgetStateProperty.all(Colors.white.withValues(alpha: 0.1)), - onTap: (memoryIndex) { - ref.read(hapticFeedbackProvider.notifier).heavyImpact(); - if (memories[memoryIndex].assets.isNotEmpty) { - final asset = memories[memoryIndex].assets[0]; - ref.read(currentAssetProvider.notifier).set(asset); - } - context.pushRoute(MemoryRoute(memories: memories, memoryIndex: memoryIndex)); - }, - children: memories - .mapIndexed((index, memory) => MemoryCard(index: index, memory: memory)) - .toList(), - ), - ) - : const SizedBox(), - ) - .value; - - return memoryLane ?? const SizedBox(); - } -} - -class MemoryCard extends ConsumerWidget { - const MemoryCard({super.key, required this.index, required this.memory}); - - final int index; - final Memory memory; - - @override - Widget build(BuildContext context, WidgetRef ref) { - return Center( - child: Stack( - children: [ - ColorFiltered( - colorFilter: ColorFilter.mode(Colors.black.withValues(alpha: 0.2), BlendMode.darken), - child: Hero( - tag: 'memory-${memory.assets[0].id}', - child: ImmichImage( - memory.assets[0], - fit: BoxFit.cover, - width: 205, - height: 200, - placeholder: const ThumbnailPlaceholder(width: 105, height: 200), - ), - ), - ), - Positioned( - bottom: 16, - left: 16, - child: ConstrainedBox( - constraints: const BoxConstraints(maxWidth: 114), - child: Text( - memory.title, - style: const TextStyle(fontWeight: FontWeight.w600, color: Colors.white, fontSize: 15), - ), - ), - ), - ], - ), - ); - } -} diff --git a/mobile/lib/widgets/search/curated_people_row.dart b/mobile/lib/widgets/search/curated_people_row.dart deleted file mode 100644 index 9155de2131..0000000000 --- a/mobile/lib/widgets/search/curated_people_row.dart +++ /dev/null @@ -1,89 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:easy_localization/easy_localization.dart'; -import 'package:immich_mobile/extensions/build_context_extensions.dart'; -import 'package:immich_mobile/models/search/search_curated_content.model.dart'; -import 'package:immich_mobile/presentation/widgets/images/remote_image_provider.dart'; -import 'package:immich_mobile/utils/image_url_builder.dart'; - -class CuratedPeopleRow extends StatelessWidget { - static const double imageSize = 60.0; - - final List content; - final EdgeInsets? padding; - - /// Callback with the content and the index when tapped - final Function(SearchCuratedContent, int)? onTap; - final Function(SearchCuratedContent, int)? onNameTap; - - const CuratedPeopleRow({super.key, required this.content, this.onTap, this.padding, required this.onNameTap}); - - @override - Widget build(BuildContext context) { - return SizedBox( - width: double.infinity, - child: SingleChildScrollView( - padding: padding, - scrollDirection: Axis.horizontal, - child: Row( - mainAxisAlignment: MainAxisAlignment.start, - crossAxisAlignment: CrossAxisAlignment.start, - children: List.generate(content.length, (index) { - final person = content[index]; - return Padding( - padding: const EdgeInsets.only(right: 16.0), - child: Column( - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - GestureDetector( - onTap: () => onTap?.call(person, index), - child: SizedBox( - height: imageSize, - child: Material( - shape: const CircleBorder(side: BorderSide.none), - elevation: 3, - child: CircleAvatar( - maxRadius: imageSize / 2, - backgroundImage: RemoteImageProvider(url: getFaceThumbnailUrl(person.id)), - ), - ), - ), - ), - const SizedBox(height: 8), - SizedBox(width: imageSize, child: _buildPersonLabel(context, person, index)), - ], - ), - ); - }), - ), - ), - ); - } - - Widget _buildPersonLabel(BuildContext context, SearchCuratedContent person, int index) { - if (person.label.isEmpty) { - return GestureDetector( - onTap: () => onNameTap?.call(person, index), - child: Text( - "exif_bottom_sheet_person_add_person", - style: context.textTheme.labelLarge?.copyWith(color: context.primaryColor), - maxLines: 2, - overflow: TextOverflow.ellipsis, - textAlign: TextAlign.center, - ).tr(), - ); - } - return Column( - mainAxisSize: MainAxisSize.min, - children: [ - Text( - person.label, - textAlign: TextAlign.center, - overflow: TextOverflow.ellipsis, - style: context.textTheme.labelLarge, - maxLines: 2, - ), - if (person.subtitle != null) Text(person.subtitle!, textAlign: TextAlign.center), - ], - ); - } -} diff --git a/mobile/lib/widgets/search/curated_places_row.dart b/mobile/lib/widgets/search/curated_places_row.dart deleted file mode 100644 index 9d21292bde..0000000000 --- a/mobile/lib/widgets/search/curated_places_row.dart +++ /dev/null @@ -1,60 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:immich_mobile/domain/models/store.model.dart'; -import 'package:immich_mobile/entities/store.entity.dart'; -import 'package:immich_mobile/models/search/search_curated_content.model.dart'; -import 'package:immich_mobile/widgets/search/search_map_thumbnail.dart'; -import 'package:immich_mobile/widgets/search/thumbnail_with_info.dart'; - -class CuratedPlacesRow extends StatelessWidget { - const CuratedPlacesRow({ - super.key, - required this.content, - required this.imageSize, - this.isMapEnabled = true, - this.onTap, - }); - - final bool isMapEnabled; - final List content; - final double imageSize; - - /// Callback with the content and the index when tapped - final Function(SearchCuratedContent, int)? onTap; - - @override - Widget build(BuildContext context) { - // Calculating the actual index of the content based on the whether map is enabled or not. - // If enabled, inject map as the first item in the list (index 0) and so the actual content will start from index 1 - final int actualContentIndex = isMapEnabled ? 1 : 0; - - return SizedBox( - height: imageSize, - child: ListView.separated( - scrollDirection: Axis.horizontal, - padding: const EdgeInsets.symmetric(horizontal: 16), - separatorBuilder: (context, index) => const SizedBox(width: 10), - itemBuilder: (context, index) { - // Injecting Map thumbnail as the first element - if (isMapEnabled && index == 0) { - return SizedBox.square( - dimension: imageSize, - child: SearchMapThumbnail(size: imageSize), - ); - } - final actualIndex = index - actualContentIndex; - final object = content[actualIndex]; - final thumbnailRequestUrl = '${Store.get(StoreKey.serverEndpoint)}/assets/${object.id}/thumbnail'; - return SizedBox.square( - dimension: imageSize, - child: ThumbnailWithInfo( - imageUrl: thumbnailRequestUrl, - textInfo: object.label, - onTap: () => onTap?.call(object, actualIndex), - ), - ); - }, - itemCount: content.length + actualContentIndex, - ), - ); - } -} diff --git a/mobile/lib/widgets/search/explore_grid.dart b/mobile/lib/widgets/search/explore_grid.dart deleted file mode 100644 index 6af20df029..0000000000 --- a/mobile/lib/widgets/search/explore_grid.dart +++ /dev/null @@ -1,69 +0,0 @@ -import 'package:auto_route/auto_route.dart'; -import 'package:flutter/material.dart'; -import 'package:immich_mobile/domain/models/store.model.dart'; -import 'package:immich_mobile/entities/asset.entity.dart'; -import 'package:immich_mobile/entities/store.entity.dart'; -import 'package:immich_mobile/models/search/search_curated_content.model.dart'; -import 'package:immich_mobile/models/search/search_filter.model.dart'; -import 'package:immich_mobile/routing/router.dart'; -import 'package:immich_mobile/utils/image_url_builder.dart'; -import 'package:immich_mobile/widgets/search/thumbnail_with_info.dart'; - -class ExploreGrid extends StatelessWidget { - final List curatedContent; - final bool isPeople; - - const ExploreGrid({super.key, required this.curatedContent, this.isPeople = false}); - - @override - Widget build(BuildContext context) { - if (curatedContent.isEmpty) { - return Padding( - padding: const EdgeInsets.symmetric(horizontal: 16.0), - child: SizedBox( - height: 100, - width: 100, - child: ThumbnailWithInfo(textInfo: '', onTap: () {}), - ), - ); - } - - return GridView.builder( - gridDelegate: const SliverGridDelegateWithMaxCrossAxisExtent( - maxCrossAxisExtent: 140, - mainAxisSpacing: 4, - crossAxisSpacing: 4, - ), - itemBuilder: (context, index) { - final content = curatedContent[index]; - final thumbnailRequestUrl = isPeople - ? getFaceThumbnailUrl(content.id) - : '${Store.get(StoreKey.serverEndpoint)}/assets/${content.id}/thumbnail'; - - return ThumbnailWithInfo( - imageUrl: thumbnailRequestUrl, - textInfo: content.label, - borderRadius: 0, - onTap: () { - isPeople - ? context.pushRoute(PersonResultRoute(personId: content.id, personName: content.label)) - : context.pushRoute( - SearchRoute( - prefilter: SearchFilter( - people: {}, - location: SearchLocationFilter(city: content.label), - camera: SearchCameraFilter(), - date: SearchDateFilter(), - display: SearchDisplayFilters(isNotInAlbum: false, isArchive: false, isFavorite: false), - rating: SearchRatingFilter(), - mediaType: AssetType.other, - ), - ), - ); - }, - ); - }, - itemCount: curatedContent.length, - ); - } -} diff --git a/mobile/lib/widgets/search/person_name_edit_form.dart b/mobile/lib/widgets/search/person_name_edit_form.dart deleted file mode 100644 index 3fa443121a..0000000000 --- a/mobile/lib/widgets/search/person_name_edit_form.dart +++ /dev/null @@ -1,65 +0,0 @@ -import 'package:easy_localization/easy_localization.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_hooks/flutter_hooks.dart'; -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/extensions/build_context_extensions.dart'; -import 'package:immich_mobile/providers/search/people.provider.dart'; - -class PersonNameEditFormResult { - final bool success; - final String updatedName; - - const PersonNameEditFormResult(this.success, this.updatedName); -} - -class PersonNameEditForm extends HookConsumerWidget { - final String personId; - final String personName; - - const PersonNameEditForm({super.key, required this.personId, required this.personName}); - - @override - Widget build(BuildContext context, WidgetRef ref) { - final controller = useTextEditingController(text: personName); - final isError = useState(false); - - return AlertDialog( - title: const Text("add_a_name", style: TextStyle(fontWeight: FontWeight.bold)).tr(), - content: SingleChildScrollView( - child: TextFormField( - controller: controller, - textCapitalization: TextCapitalization.words, - autofocus: true, - decoration: InputDecoration( - hintText: 'name'.tr(), - border: const OutlineInputBorder(), - errorText: isError.value ? 'Error occurred' : null, - ), - ), - ), - actions: [ - TextButton( - onPressed: () => context.pop(const PersonNameEditFormResult(false, '')), - child: Text( - "cancel", - style: TextStyle(color: Colors.red[300], fontWeight: FontWeight.bold), - ).tr(), - ), - TextButton( - onPressed: () async { - isError.value = false; - final result = await ref.read(updatePersonNameProvider(personId, controller.text).future); - isError.value = !result; - if (result) { - context.pop(PersonNameEditFormResult(true, controller.text)); - } - }, - child: Text( - "save", - style: TextStyle(color: context.primaryColor, fontWeight: FontWeight.bold), - ).tr(), - ), - ], - ); - } -} diff --git a/mobile/lib/widgets/search/search_filter/media_type_picker.dart b/mobile/lib/widgets/search/search_filter/media_type_picker.dart index e0e34b654e..ac89de8190 100644 --- a/mobile/lib/widgets/search/search_filter/media_type_picker.dart +++ b/mobile/lib/widgets/search/search_filter/media_type_picker.dart @@ -1,7 +1,7 @@ import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; -import 'package:immich_mobile/entities/asset.entity.dart'; +import 'package:immich_mobile/domain/models/asset/base_asset.model.dart'; class MediaTypePicker extends HookWidget { const MediaTypePicker({super.key, required this.onSelect, this.filter}); diff --git a/mobile/lib/widgets/search/search_map_thumbnail.dart b/mobile/lib/widgets/search/search_map_thumbnail.dart deleted file mode 100644 index 7533e46f1a..0000000000 --- a/mobile/lib/widgets/search/search_map_thumbnail.dart +++ /dev/null @@ -1,27 +0,0 @@ -import 'package:auto_route/auto_route.dart'; -import 'package:easy_localization/easy_localization.dart'; -import 'package:flutter/material.dart'; -import 'package:immich_mobile/routing/router.dart'; -import 'package:immich_mobile/widgets/map/map_thumbnail.dart'; -import 'package:immich_mobile/widgets/search/thumbnail_with_info_container.dart'; -import 'package:maplibre_gl/maplibre_gl.dart'; - -class SearchMapThumbnail extends StatelessWidget { - const SearchMapThumbnail({super.key, this.size = 60.0}); - - final double size; - final bool showTitle = true; - - @override - Widget build(BuildContext context) { - return ThumbnailWithInfoContainer( - label: 'search_page_your_map'.tr(), - onTap: () { - context.pushRoute(MapRoute()); - }, - child: IgnorePointer( - child: MapThumbnail(zoom: 2, centre: const LatLng(47, 5), height: size, width: size, showAttribution: false), - ), - ); - } -} diff --git a/mobile/lib/widgets/search/search_row_section.dart b/mobile/lib/widgets/search/search_row_section.dart deleted file mode 100644 index b8584fefef..0000000000 --- a/mobile/lib/widgets/search/search_row_section.dart +++ /dev/null @@ -1,34 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:immich_mobile/widgets/search/search_row_title.dart'; - -class SearchRowSection extends StatelessWidget { - const SearchRowSection({ - super.key, - required this.onViewAllPressed, - required this.title, - this.isEmpty = false, - required this.child, - }); - - final Function() onViewAllPressed; - final String title; - final bool isEmpty; - final Widget child; - - @override - Widget build(BuildContext context) { - if (isEmpty) { - return const SizedBox.shrink(); - } - - return Column( - children: [ - Padding( - padding: const EdgeInsets.symmetric(horizontal: 16), - child: SearchRowTitle(onViewAllPressed: onViewAllPressed, title: title), - ), - child, - ], - ); - } -} diff --git a/mobile/lib/widgets/settings/advanced_settings.dart b/mobile/lib/widgets/settings/advanced_settings.dart index d5905a246c..a38ccd3556 100644 --- a/mobile/lib/widgets/settings/advanced_settings.dart +++ b/mobile/lib/widgets/settings/advanced_settings.dart @@ -6,7 +6,6 @@ import 'package:flutter/material.dart'; import 'package:flutter_hooks/flutter_hooks.dart' hide Store; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/domain/services/log.service.dart'; -import 'package:immich_mobile/entities/store.entity.dart'; import 'package:immich_mobile/extensions/build_context_extensions.dart'; import 'package:immich_mobile/providers/infrastructure/platform.provider.dart'; import 'package:immich_mobile/providers/infrastructure/readonly_mode.provider.dart'; @@ -14,9 +13,7 @@ import 'package:immich_mobile/repositories/local_files_manager.repository.dart'; import 'package:immich_mobile/services/app_settings.service.dart'; import 'package:immich_mobile/utils/bytes_units.dart'; import 'package:immich_mobile/utils/hooks/app_settings_update_hook.dart'; -import 'package:immich_mobile/widgets/settings/beta_timeline_list_tile.dart'; import 'package:immich_mobile/widgets/settings/custom_proxy_headers_settings/custom_proxy_headers_settings.dart'; -import 'package:immich_mobile/widgets/settings/local_storage_settings.dart'; import 'package:immich_mobile/widgets/settings/settings_action_tile.dart'; import 'package:immich_mobile/widgets/settings/settings_slider_list_tile.dart'; import 'package:immich_mobile/widgets/settings/settings_sub_page_scaffold.dart'; @@ -35,7 +32,6 @@ class AdvancedSettings extends HookConsumerWidget { final manageMediaAndroidPermission = useState(false); final levelId = useAppSettingsState(AppSettingsEnum.logLevel); final preferRemote = useAppSettingsState(AppSettingsEnum.preferRemoteImage); - final useAlternatePMFilter = useAppSettingsState(AppSettingsEnum.photoManagerCustomFilter); final readonlyModeEnabled = useAppSettingsState(AppSettingsEnum.readonlyModeEnabled); final logLevel = Level.LEVELS[levelId.value].name; @@ -114,35 +110,26 @@ class AdvancedSettings extends HookConsumerWidget { title: "advanced_settings_prefer_remote_title".tr(), subtitle: "advanced_settings_prefer_remote_subtitle".tr(), ), - if (!Store.isBetaTimelineEnabled) const LocalStorageSettings(), const CustomProxyHeaderSettings(), const SslClientCertSettings(), - if (!Store.isBetaTimelineEnabled) - SettingsSwitchListTile( - valueNotifier: useAlternatePMFilter, - title: "advanced_settings_enable_alternate_media_filter_title".tr(), - subtitle: "advanced_settings_enable_alternate_media_filter_subtitle".tr(), - ), - if (!Store.isBetaTimelineEnabled) const BetaTimelineListTile(), - if (Store.isBetaTimelineEnabled) - SettingsSwitchListTile( - valueNotifier: readonlyModeEnabled, - title: "advanced_settings_readonly_mode_title".tr(), - subtitle: "advanced_settings_readonly_mode_subtitle".tr(), - onChanged: (value) { - readonlyModeEnabled.value = value; - ref.read(readonlyModeProvider.notifier).setReadonlyMode(value); - context.scaffoldMessenger.showSnackBar( - SnackBar( - duration: const Duration(seconds: 2), - content: Text( - (value ? "readonly_mode_enabled" : "readonly_mode_disabled").tr(), - style: context.textTheme.bodyLarge?.copyWith(color: context.primaryColor), - ), + SettingsSwitchListTile( + valueNotifier: readonlyModeEnabled, + title: "advanced_settings_readonly_mode_title".tr(), + subtitle: "advanced_settings_readonly_mode_subtitle".tr(), + onChanged: (value) { + readonlyModeEnabled.value = value; + ref.read(readonlyModeProvider.notifier).setReadonlyMode(value); + context.scaffoldMessenger.showSnackBar( + SnackBar( + duration: const Duration(seconds: 2), + content: Text( + (value ? "readonly_mode_enabled" : "readonly_mode_disabled").tr(), + style: context.textTheme.bodyLarge?.copyWith(color: context.primaryColor), ), - ); - }, - ), + ), + ); + }, + ), ListTile( title: Text("advanced_settings_clear_image_cache".tr(), style: const TextStyle(fontWeight: FontWeight.w500)), leading: const Icon(Icons.playlist_remove_rounded), diff --git a/mobile/lib/widgets/settings/asset_list_settings/asset_list_group_settings.dart b/mobile/lib/widgets/settings/asset_list_settings/asset_list_group_settings.dart index 08e66df48d..42ea3acfc0 100644 --- a/mobile/lib/widgets/settings/asset_list_settings/asset_list_group_settings.dart +++ b/mobile/lib/widgets/settings/asset_list_settings/asset_list_group_settings.dart @@ -2,11 +2,11 @@ import 'dart:async'; import 'package:flutter/material.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:immich_mobile/domain/models/timeline.model.dart'; import 'package:immich_mobile/extensions/translate_extensions.dart'; import 'package:immich_mobile/providers/app_settings.provider.dart'; import 'package:immich_mobile/services/app_settings.service.dart'; import 'package:immich_mobile/utils/hooks/app_settings_update_hook.dart'; -import 'package:immich_mobile/widgets/asset_grid/asset_grid_data_structure.dart'; import 'package:immich_mobile/widgets/settings/setting_group_title.dart'; import 'package:immich_mobile/widgets/settings/settings_radio_list_tile.dart'; diff --git a/mobile/lib/widgets/settings/asset_list_settings/asset_list_layout_settings.dart b/mobile/lib/widgets/settings/asset_list_settings/asset_list_layout_settings.dart index 2d5c9f06eb..55c8195947 100644 --- a/mobile/lib/widgets/settings/asset_list_settings/asset_list_layout_settings.dart +++ b/mobile/lib/widgets/settings/asset_list_settings/asset_list_layout_settings.dart @@ -1,21 +1,18 @@ import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/entities/store.entity.dart'; import 'package:immich_mobile/extensions/translate_extensions.dart'; import 'package:immich_mobile/providers/app_settings.provider.dart'; import 'package:immich_mobile/services/app_settings.service.dart'; import 'package:immich_mobile/utils/hooks/app_settings_update_hook.dart'; import 'package:immich_mobile/widgets/settings/setting_group_title.dart'; import 'package:immich_mobile/widgets/settings/settings_slider_list_tile.dart'; -import 'package:immich_mobile/widgets/settings/settings_switch_list_tile.dart'; class LayoutSettings extends HookConsumerWidget { const LayoutSettings({super.key}); @override Widget build(BuildContext context, WidgetRef ref) { - final useDynamicLayout = useAppSettingsState(AppSettingsEnum.dynamicLayout); final tilesPerRow = useAppSettingsState(AppSettingsEnum.tilesPerRow); return Column( @@ -25,12 +22,6 @@ class LayoutSettings extends HookConsumerWidget { title: "asset_list_layout_sub_title".t(context: context), icon: Icons.view_module_outlined, ), - if (!Store.isBetaTimelineEnabled) - SettingsSwitchListTile( - valueNotifier: useDynamicLayout, - title: "asset_list_layout_settings_dynamic_layout_title".t(context: context), - onChanged: (_) => ref.invalidate(appSettingsServiceProvider), - ), SettingsSliderListTile( valueNotifier: tilesPerRow, text: 'theme_setting_asset_list_tiles_per_row_title'.tr(namedArgs: {'count': "${tilesPerRow.value}"}), diff --git a/mobile/lib/widgets/settings/backup_settings/background_settings.dart b/mobile/lib/widgets/settings/backup_settings/background_settings.dart deleted file mode 100644 index 038a567dc2..0000000000 --- a/mobile/lib/widgets/settings/backup_settings/background_settings.dart +++ /dev/null @@ -1,204 +0,0 @@ -import 'dart:io'; - -import 'package:easy_localization/easy_localization.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_hooks/flutter_hooks.dart'; -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/extensions/build_context_extensions.dart'; -import 'package:immich_mobile/providers/backup/backup.provider.dart'; -import 'package:immich_mobile/providers/backup/ios_background_settings.provider.dart'; -import 'package:immich_mobile/widgets/backup/ios_debug_info_tile.dart'; -import 'package:immich_mobile/widgets/settings/settings_button_list_tile.dart'; -import 'package:immich_mobile/widgets/settings/settings_slider_list_tile.dart'; -import 'package:immich_mobile/widgets/settings/settings_switch_list_tile.dart'; -import 'package:permission_handler/permission_handler.dart'; -import 'package:url_launcher/url_launcher.dart'; - -class BackgroundBackupSettings extends ConsumerWidget { - const BackgroundBackupSettings({super.key}); - - @override - Widget build(BuildContext context, WidgetRef ref) { - final isBackgroundEnabled = ref.watch(backupProvider.select((s) => s.backgroundBackup)); - final iosSettings = ref.watch(iOSBackgroundSettingsProvider); - - void showErrorToUser(String msg) { - final snackBar = SnackBar( - content: Text(msg.tr(), style: context.textTheme.bodyLarge?.copyWith(color: context.primaryColor)), - backgroundColor: Colors.red, - ); - context.scaffoldMessenger.showSnackBar(snackBar); - } - - void showBatteryOptimizationInfoToUser() { - showDialog( - context: context, - barrierDismissible: false, - builder: (BuildContext ctx) { - return AlertDialog( - title: const Text('backup_controller_page_background_battery_info_title').tr(), - content: SingleChildScrollView( - child: const Text('backup_controller_page_background_battery_info_message').tr(), - ), - actions: [ - ElevatedButton( - onPressed: () => - launchUrl(Uri.parse('https://dontkillmyapp.com'), mode: LaunchMode.externalApplication), - child: const Text( - "backup_controller_page_background_battery_info_link", - style: TextStyle(fontWeight: FontWeight.bold, fontSize: 12), - ).tr(), - ), - ElevatedButton( - child: const Text( - 'backup_controller_page_background_battery_info_ok', - style: TextStyle(fontWeight: FontWeight.bold, fontSize: 12), - ).tr(), - onPressed: () => ctx.pop(), - ), - ], - ); - }, - ); - } - - if (!isBackgroundEnabled) { - return SettingsButtonListTile( - icon: Icons.cloud_sync_outlined, - title: 'backup_controller_page_background_is_off'.tr(), - subtileText: 'backup_controller_page_background_description'.tr(), - buttonText: 'backup_controller_page_background_turn_on'.tr(), - onButtonTap: () => ref - .read(backupProvider.notifier) - .configureBackgroundBackup( - enabled: true, - onError: showErrorToUser, - onBatteryInfo: showBatteryOptimizationInfoToUser, - ), - ); - } - - return Column( - children: [ - if (!Platform.isIOS || iosSettings?.appRefreshEnabled == true) - _BackgroundSettingsEnabled(onError: showErrorToUser, onBatteryInfo: showBatteryOptimizationInfoToUser), - if (Platform.isIOS && iosSettings?.appRefreshEnabled != true) const _IOSBackgroundRefreshDisabled(), - if (Platform.isIOS && iosSettings != null) IosDebugInfoTile(settings: iosSettings), - ], - ); - } -} - -class _IOSBackgroundRefreshDisabled extends StatelessWidget { - const _IOSBackgroundRefreshDisabled(); - - @override - Widget build(BuildContext context) { - return SettingsButtonListTile( - icon: Icons.task_outlined, - title: 'backup_controller_page_background_app_refresh_disabled_title'.tr(), - subtileText: 'backup_controller_page_background_app_refresh_disabled_content'.tr(), - buttonText: 'backup_controller_page_background_app_refresh_enable_button_text'.tr(), - onButtonTap: () => openAppSettings(), - ); - } -} - -class _BackgroundSettingsEnabled extends HookConsumerWidget { - final void Function(String msg) onError; - final void Function() onBatteryInfo; - - const _BackgroundSettingsEnabled({required this.onError, required this.onBatteryInfo}); - - @override - Widget build(BuildContext context, WidgetRef ref) { - final isWifiRequired = ref.watch(backupProvider.select((s) => s.backupRequireWifi)); - final isWifiRequiredNotifier = useValueNotifier(isWifiRequired); - useValueChanged( - isWifiRequired, - (_, __) => WidgetsBinding.instance.addPostFrameCallback((_) => isWifiRequiredNotifier.value = isWifiRequired), - ); - - final isChargingRequired = ref.watch(backupProvider.select((s) => s.backupRequireCharging)); - final isChargingRequiredNotifier = useValueNotifier(isChargingRequired); - useValueChanged( - isChargingRequired, - (_, __) => - WidgetsBinding.instance.addPostFrameCallback((_) => isChargingRequiredNotifier.value = isChargingRequired), - ); - - int backupDelayToSliderValue(int ms) => switch (ms) { - 5000 => 0, - 30000 => 1, - 120000 => 2, - _ => 3, - }; - - int backupDelayToMilliseconds(int v) => switch (v) { - 0 => 5000, - 1 => 30000, - 2 => 120000, - _ => 600000, - }; - - String formatBackupDelaySliderValue(int v) => switch (v) { - 0 => 'setting_notifications_notify_seconds'.tr(namedArgs: {'count': '5'}), - 1 => 'setting_notifications_notify_seconds'.tr(namedArgs: {'count': '30'}), - 2 => 'setting_notifications_notify_minutes'.tr(namedArgs: {'count': '2'}), - _ => 'setting_notifications_notify_minutes'.tr(namedArgs: {'count': '10'}), - }; - - final backupTriggerDelay = ref.watch(backupProvider.select((s) => s.backupTriggerDelay)); - final triggerDelay = useState(backupDelayToSliderValue(backupTriggerDelay)); - useValueChanged( - triggerDelay.value, - (_, __) => ref - .read(backupProvider.notifier) - .configureBackgroundBackup( - triggerDelay: backupDelayToMilliseconds(triggerDelay.value), - onError: onError, - onBatteryInfo: onBatteryInfo, - ), - ); - - return SettingsButtonListTile( - icon: Icons.cloud_sync_rounded, - iconColor: context.primaryColor, - title: 'backup_controller_page_background_is_on'.tr(), - buttonText: 'backup_controller_page_background_turn_off'.tr(), - onButtonTap: () => ref - .read(backupProvider.notifier) - .configureBackgroundBackup(enabled: false, onError: onError, onBatteryInfo: onBatteryInfo), - subtitle: Column( - children: [ - SettingsSwitchListTile( - valueNotifier: isWifiRequiredNotifier, - title: 'backup_controller_page_background_wifi'.tr(), - icon: Icons.wifi, - onChanged: (enabled) => ref - .read(backupProvider.notifier) - .configureBackgroundBackup(requireWifi: enabled, onError: onError, onBatteryInfo: onBatteryInfo), - ), - SettingsSwitchListTile( - valueNotifier: isChargingRequiredNotifier, - title: 'backup_controller_page_background_charging'.tr(), - icon: Icons.charging_station, - onChanged: (enabled) => ref - .read(backupProvider.notifier) - .configureBackgroundBackup(requireCharging: enabled, onError: onError, onBatteryInfo: onBatteryInfo), - ), - if (Platform.isAndroid) - SettingsSliderListTile( - valueNotifier: triggerDelay, - text: 'backup_controller_page_background_delay'.tr( - namedArgs: {'duration': formatBackupDelaySliderValue(triggerDelay.value)}, - ), - maxValue: 3.0, - noDivisons: 3, - label: formatBackupDelaySliderValue(triggerDelay.value), - ), - ], - ), - ); - } -} diff --git a/mobile/lib/widgets/settings/backup_settings/backup_settings.dart b/mobile/lib/widgets/settings/backup_settings/backup_settings.dart deleted file mode 100644 index 50aa57da9f..0000000000 --- a/mobile/lib/widgets/settings/backup_settings/backup_settings.dart +++ /dev/null @@ -1,82 +0,0 @@ -import 'dart:io'; - -import 'package:easy_localization/easy_localization.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_hooks/flutter_hooks.dart'; -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/providers/backup/backup_verification.provider.dart'; -import 'package:immich_mobile/services/app_settings.service.dart'; -import 'package:immich_mobile/services/asset.service.dart'; -import 'package:immich_mobile/widgets/settings/backup_settings/background_settings.dart'; -import 'package:immich_mobile/widgets/settings/backup_settings/foreground_settings.dart'; -import 'package:immich_mobile/widgets/settings/settings_button_list_tile.dart'; -import 'package:immich_mobile/widgets/settings/settings_sub_page_scaffold.dart'; -import 'package:immich_mobile/widgets/settings/settings_switch_list_tile.dart'; -import 'package:immich_mobile/utils/hooks/app_settings_update_hook.dart'; - -class BackupSettings extends HookConsumerWidget { - const BackupSettings({super.key}); - - @override - Widget build(BuildContext context, WidgetRef ref) { - final ignoreIcloudAssets = useAppSettingsState(AppSettingsEnum.ignoreIcloudAssets); - final isAdvancedTroubleshooting = useAppSettingsState(AppSettingsEnum.advancedTroubleshooting); - final albumSync = useAppSettingsState(AppSettingsEnum.syncAlbums); - final isCorruptCheckInProgress = ref.watch(backupVerificationProvider); - final isAlbumSyncInProgress = useState(false); - - syncAlbums() async { - isAlbumSyncInProgress.value = true; - try { - await ref.read(assetServiceProvider).syncUploadedAssetToAlbums(); - } catch (_) { - } finally { - Future.delayed(const Duration(seconds: 1), () { - isAlbumSyncInProgress.value = false; - }); - } - } - - final backupSettings = [ - const ForegroundBackupSettings(), - const BackgroundBackupSettings(), - if (Platform.isIOS) - SettingsSwitchListTile( - valueNotifier: ignoreIcloudAssets, - title: 'ignore_icloud_photos'.tr(), - subtitle: 'ignore_icloud_photos_description'.tr(), - ), - if (Platform.isAndroid && isAdvancedTroubleshooting.value) - SettingsButtonListTile( - icon: Icons.warning_rounded, - title: 'check_corrupt_asset_backup'.tr(), - subtitle: isCorruptCheckInProgress - ? const Column( - children: [ - SizedBox(height: 20), - Center(child: CircularProgressIndicator()), - SizedBox(height: 20), - ], - ) - : null, - subtileText: !isCorruptCheckInProgress ? 'check_corrupt_asset_backup_description'.tr() : null, - buttonText: 'check_corrupt_asset_backup_button'.tr(), - onButtonTap: !isCorruptCheckInProgress - ? () => ref.read(backupVerificationProvider.notifier).performBackupCheck(context) - : null, - ), - if (albumSync.value) - SettingsButtonListTile( - icon: Icons.photo_album_outlined, - title: 'sync_albums'.tr(), - subtitle: Text("sync_albums_manual_subtitle".tr()), - buttonText: 'sync_albums'.tr(), - child: isAlbumSyncInProgress.value - ? const CircularProgressIndicator() - : ElevatedButton(onPressed: syncAlbums, child: Text('sync'.tr())), - ), - ]; - - return SettingsSubPageScaffold(settings: backupSettings, showDivider: true); - } -} diff --git a/mobile/lib/widgets/settings/backup_settings/foreground_settings.dart b/mobile/lib/widgets/settings/backup_settings/foreground_settings.dart deleted file mode 100644 index a2ff00fe45..0000000000 --- a/mobile/lib/widgets/settings/backup_settings/foreground_settings.dart +++ /dev/null @@ -1,35 +0,0 @@ -import 'package:easy_localization/easy_localization.dart'; -import 'package:flutter/material.dart'; -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/extensions/build_context_extensions.dart'; -import 'package:immich_mobile/providers/backup/backup.provider.dart'; -import 'package:immich_mobile/widgets/settings/settings_button_list_tile.dart'; - -class ForegroundBackupSettings extends ConsumerWidget { - const ForegroundBackupSettings({super.key}); - - @override - Widget build(BuildContext context, WidgetRef ref) { - final isAutoBackup = ref.watch(backupProvider.select((s) => s.autoBackup)); - - void onButtonTap() => ref.read(backupProvider.notifier).setAutoBackup(!isAutoBackup); - - if (isAutoBackup) { - return SettingsButtonListTile( - icon: Icons.cloud_done_rounded, - iconColor: context.primaryColor, - title: 'backup_controller_page_status_on'.tr(), - buttonText: 'backup_controller_page_turn_off'.tr(), - onButtonTap: onButtonTap, - ); - } - - return SettingsButtonListTile( - icon: Icons.cloud_off_rounded, - title: 'backup_controller_page_status_off'.tr(), - subtileText: 'backup_controller_page_desc_backup'.tr(), - buttonText: 'backup_controller_page_turn_on'.tr(), - onButtonTap: onButtonTap, - ); - } -} diff --git a/mobile/lib/widgets/settings/beta_timeline_list_tile.dart b/mobile/lib/widgets/settings/beta_timeline_list_tile.dart deleted file mode 100644 index 21e0edb34c..0000000000 --- a/mobile/lib/widgets/settings/beta_timeline_list_tile.dart +++ /dev/null @@ -1,71 +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/extensions/build_context_extensions.dart'; -import 'package:immich_mobile/extensions/translate_extensions.dart'; -import 'package:immich_mobile/providers/app_settings.provider.dart'; -import 'package:immich_mobile/providers/auth.provider.dart'; -import 'package:immich_mobile/routing/router.dart'; -import 'package:immich_mobile/services/app_settings.service.dart'; -import 'package:immich_mobile/widgets/settings/setting_list_tile.dart'; - -class BetaTimelineListTile extends ConsumerWidget { - const BetaTimelineListTile({super.key}); - - @override - Widget build(BuildContext context, WidgetRef ref) { - final betaTimelineValue = ref.watch(appSettingsServiceProvider).getSetting(AppSettingsEnum.betaTimeline); - final auth = ref.watch(authProvider); - - if (!auth.isAuthenticated) { - return const SizedBox.shrink(); - } - - void onSwitchChanged(bool value) { - showDialog( - context: context, - builder: (context) { - return AlertDialog( - title: value ? const Text("Enable New Timeline") : const Text("Disable New Timeline"), - content: value - ? const Text("Are you sure you want to enable the new timeline?") - : const Text("Are you sure you want to disable the new timeline?"), - actions: [ - TextButton( - onPressed: () { - context.pop(); - }, - child: Text( - "cancel".t(context: context), - style: TextStyle(fontSize: 16, fontWeight: FontWeight.w500, color: context.colorScheme.outline), - ), - ), - ElevatedButton( - onPressed: () async { - Navigator.of(context).pop(); - unawaited(context.router.replaceAll([ChangeExperienceRoute(switchingToBeta: value)])); - }, - child: Text("ok".t(context: context)), - ), - ], - ); - }, - ); - } - - return Padding( - padding: const EdgeInsets.only(left: 4.0), - child: SettingListTile( - title: "new_timeline".t(context: context), - trailing: Switch.adaptive( - value: betaTimelineValue, - onChanged: onSwitchChanged, - activeThumbColor: context.primaryColor, - ), - onTap: () => onSwitchChanged(!betaTimelineValue), - ), - ); - } -} diff --git a/mobile/lib/widgets/settings/local_storage_settings.dart b/mobile/lib/widgets/settings/local_storage_settings.dart deleted file mode 100644 index af9e4079bb..0000000000 --- a/mobile/lib/widgets/settings/local_storage_settings.dart +++ /dev/null @@ -1,51 +0,0 @@ -import 'package:easy_localization/easy_localization.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_hooks/flutter_hooks.dart' show useEffect, useState; -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/entities/duplicated_asset.entity.dart'; -import 'package:immich_mobile/extensions/build_context_extensions.dart'; -import 'package:immich_mobile/extensions/theme_extensions.dart'; -import 'package:immich_mobile/providers/db.provider.dart'; - -class LocalStorageSettings extends HookConsumerWidget { - const LocalStorageSettings({super.key}); - @override - Widget build(BuildContext context, WidgetRef ref) { - final isarDb = ref.watch(dbProvider); - final cacheItemCount = useState(0); - - useEffect(() { - cacheItemCount.value = isarDb.duplicatedAssets.countSync(); - return null; - }, []); - - void clearCache() async { - await isarDb.writeTxn(() => isarDb.duplicatedAssets.clear()); - cacheItemCount.value = await isarDb.duplicatedAssets.count(); - } - - return ListTile( - contentPadding: const EdgeInsets.symmetric(horizontal: 20), - dense: true, - title: Text( - "cache_settings_duplicated_assets_title", - style: context.textTheme.bodyLarge?.copyWith(fontWeight: FontWeight.w500), - ).tr(namedArgs: {'count': "${cacheItemCount.value}"}), - subtitle: Text( - "cache_settings_duplicated_assets_subtitle", - style: context.textTheme.bodyMedium?.copyWith(color: context.colorScheme.onSurfaceSecondary), - ).tr(), - trailing: TextButton( - onPressed: cacheItemCount.value > 0 ? clearCache : null, - child: Text( - "cache_settings_duplicated_assets_clear_button", - style: TextStyle( - fontSize: 12, - color: cacheItemCount.value > 0 ? Colors.red : Colors.grey, - fontWeight: FontWeight.bold, - ), - ).tr(), - ), - ); - } -} diff --git a/mobile/pubspec.lock b/mobile/pubspec.lock index 9d5f431792..f2caf05be8 100644 --- a/mobile/pubspec.lock +++ b/mobile/pubspec.lock @@ -378,14 +378,6 @@ packages: url: "https://pub.dev" source: hosted version: "3.1.0" - dartx: - dependency: transitive - description: - name: dartx - sha256: "8b25435617027257d43e6508b5fe061012880ddfdaa75a71d607c3de2a13d244" - url: "https://pub.dev" - source: hosted - version: "1.2.0" dbus: dependency: transitive description: @@ -1012,40 +1004,6 @@ packages: url: "https://pub.dev" source: hosted version: "1.0.5" - isar: - dependency: "direct main" - description: - path: "packages/isar" - ref: bb1dca40fe87a001122e5d43bc6254718cb49f3a - resolved-ref: bb1dca40fe87a001122e5d43bc6254718cb49f3a - url: "https://github.com/immich-app/isar" - source: git - version: "3.1.8" - isar_community: - dependency: transitive - description: - name: isar_community - sha256: "28f59e54636c45ba0bb1b3b7f2656f1c50325f740cea6efcd101900be3fba546" - url: "https://pub.dev" - source: hosted - version: "3.3.0-dev.3" - isar_community_flutter_libs: - dependency: "direct main" - description: - name: isar_community_flutter_libs - sha256: c2934fe755bb3181cb67602fd5df0d080b3d3eb52799f98623aa4fc5acbea010 - url: "https://pub.dev" - source: hosted - version: "3.3.0-dev.3" - isar_generator: - dependency: "direct dev" - description: - path: "packages/isar_generator" - ref: bb1dca40fe87a001122e5d43bc6254718cb49f3a - resolved-ref: bb1dca40fe87a001122e5d43bc6254718cb49f3a - url: "https://github.com/immich-app/isar" - source: git - version: "3.1.8" jni: dependency: transitive description: @@ -1909,14 +1867,6 @@ packages: url: "https://pub.dev" source: hosted version: "0.1.0+1" - time: - dependency: transitive - description: - name: time - sha256: "370572cf5d1e58adcb3e354c47515da3f7469dac3a95b447117e728e7be6f461" - url: "https://pub.dev" - source: hosted - version: "2.1.5" timezone: dependency: "direct main" description: @@ -2173,14 +2123,6 @@ packages: url: "https://pub.dev" source: hosted version: "6.6.1" - xxh3: - dependency: transitive - description: - name: xxh3 - sha256: "399a0438f5d426785723c99da6b16e136f4953fb1e9db0bf270bd41dd4619916" - url: "https://pub.dev" - source: hosted - version: "1.2.0" yaml: dependency: transitive description: diff --git a/mobile/pubspec.yaml b/mobile/pubspec.yaml index 1e03bd6e7f..5eb4deb924 100644 --- a/mobile/pubspec.yaml +++ b/mobile/pubspec.yaml @@ -43,16 +43,9 @@ dependencies: immich_ui: path: './packages/ui' intl: ^0.20.2 - isar: - git: - url: https://github.com/immich-app/isar - ref: 'bb1dca40fe87a001122e5d43bc6254718cb49f3a' - path: packages/isar/ - isar_community_flutter_libs: 3.3.0-dev.3 local_auth: ^2.3.0 logging: ^1.3.0 maplibre_gl: ^0.22.0 - native_video_player: git: url: https://github.com/immich-app/native_video_player @@ -115,11 +108,6 @@ dev_dependencies: path: './immich_lint' integration_test: sdk: flutter - isar_generator: - git: - url: https://github.com/immich-app/isar - ref: 'bb1dca40fe87a001122e5d43bc6254718cb49f3a' - path: packages/isar_generator/ mocktail: ^1.0.4 # Type safe platform code pigeon: ^26.0.2 diff --git a/mobile/test/api.mocks.dart b/mobile/test/api.mocks.dart index c6a3a90582..e1c32eaaee 100644 --- a/mobile/test/api.mocks.dart +++ b/mobile/test/api.mocks.dart @@ -1,8 +1,6 @@ import 'package:mocktail/mocktail.dart'; import 'package:openapi/api.dart'; -class MockAssetsApi extends Mock implements AssetsApi {} - class MockSyncApi extends Mock implements SyncApi {} class MockServerApi extends Mock implements ServerApi {} diff --git a/mobile/test/domain/service.mock.dart b/mobile/test/domain/service.mock.dart index 56b4802f88..89e85a3794 100644 --- a/mobile/test/domain/service.mock.dart +++ b/mobile/test/domain/service.mock.dart @@ -1,20 +1,13 @@ import 'package:immich_mobile/domain/services/store.service.dart'; -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/background_upload.service.dart'; import 'package:mocktail/mocktail.dart'; class MockStoreService extends Mock implements StoreService {} -class MockUserService extends Mock implements UserService {} - class MockBackgroundSyncManager extends Mock implements BackgroundSyncManager {} class MockNativeSyncApi extends Mock implements NativeSyncApi {} class MockAppSettingsService extends Mock implements AppSettingsService {} - -class MockBackgroundUploadService extends Mock implements BackgroundUploadService {} - diff --git a/mobile/test/domain/services/album.service_test.dart b/mobile/test/domain/services/album.service_test.dart deleted file mode 100644 index 9110a09471..0000000000 --- a/mobile/test/domain/services/album.service_test.dart +++ /dev/null @@ -1,115 +0,0 @@ -import 'package:flutter_test/flutter_test.dart'; -import 'package:immich_mobile/constants/enums.dart'; -import 'package:immich_mobile/domain/models/album/album.model.dart'; -import 'package:immich_mobile/domain/services/remote_album.service.dart'; -import 'package:immich_mobile/infrastructure/repositories/remote_album.repository.dart'; -import 'package:immich_mobile/providers/album/album_sort_by_options.provider.dart'; -import 'package:immich_mobile/repositories/drift_album_api_repository.dart'; -import 'package:mocktail/mocktail.dart'; - -import '../../infrastructure/repository.mock.dart'; - -void main() { - late RemoteAlbumService sut; - late DriftRemoteAlbumRepository mockRemoteAlbumRepo; - late DriftAlbumApiRepository mockAlbumApiRepo; - - final albumA = RemoteAlbum( - id: '1', - name: 'Album A', - description: "", - isActivityEnabled: false, - order: AlbumAssetOrder.asc, - assetCount: 1, - createdAt: DateTime(2023, 1, 1), - updatedAt: DateTime(2023, 1, 2), - ownerId: 'owner1', - ownerName: "Test User", - isShared: false, - ); - - final albumB = RemoteAlbum( - id: '2', - name: 'Album B', - description: "", - isActivityEnabled: false, - order: AlbumAssetOrder.desc, - assetCount: 2, - createdAt: DateTime(2023, 2, 1), - updatedAt: DateTime(2023, 2, 2), - ownerId: 'owner2', - ownerName: "Test User", - isShared: false, - ); - - setUp(() { - mockRemoteAlbumRepo = MockRemoteAlbumRepository(); - mockAlbumApiRepo = MockDriftAlbumApiRepository(); - - when( - () => mockRemoteAlbumRepo.getSortedAlbumIds(any(), aggregation: AssetDateAggregation.end), - ).thenAnswer((_) async => ['1', '2']); - - when( - () => mockRemoteAlbumRepo.getSortedAlbumIds(any(), aggregation: AssetDateAggregation.start), - ).thenAnswer((_) async => ['1', '2']); - - sut = RemoteAlbumService(mockRemoteAlbumRepo, mockAlbumApiRepo); - }); - - group('sortAlbums', () { - test('should sort correctly based on name', () async { - final albums = [albumB, albumA]; - - final result = await sut.sortAlbums(albums, AlbumSortMode.title); - expect(result, [albumA, albumB]); - }); - - test('should sort correctly based on createdAt', () async { - final albums = [albumB, albumA]; - - final result = await sut.sortAlbums(albums, AlbumSortMode.created); - expect(result, [albumB, albumA]); - }); - - test('should sort correctly based on updatedAt', () async { - final albums = [albumB, albumA]; - - final result = await sut.sortAlbums(albums, AlbumSortMode.lastModified); - expect(result, [albumB, albumA]); - }); - - test('should sort correctly based on assetCount', () async { - final albums = [albumB, albumA]; - - final result = await sut.sortAlbums(albums, AlbumSortMode.assetCount); - expect(result, [albumB, albumA]); - }); - - test('should sort correctly based on newestAssetTimestamp', () async { - final albums = [albumB, albumA]; - - final result = await sut.sortAlbums(albums, AlbumSortMode.mostRecent); - expect(result, [albumB, albumA]); - }); - - test('should sort correctly based on oldestAssetTimestamp', () async { - final albums = [albumB, albumA]; - - final result = await sut.sortAlbums(albums, AlbumSortMode.mostOldest); - expect(result, [albumA, albumB]); - }); - - test('should flip order when isReverse is true for all modes', () async { - final albums = [albumB, albumA]; - - for (final mode in AlbumSortMode.values) { - final normal = await sut.sortAlbums(albums, mode, isReverse: false); - final reversed = await sut.sortAlbums(albums, mode, isReverse: true); - - // reversed should be the exact inverse of normal - expect(reversed, normal.reversed.toList(), reason: 'Mode: $mode'); - } - }); - }); -} diff --git a/mobile/test/domain/services/asset.service_test.dart b/mobile/test/domain/services/asset.service_test.dart deleted file mode 100644 index 04e49f89f9..0000000000 --- a/mobile/test/domain/services/asset.service_test.dart +++ /dev/null @@ -1,185 +0,0 @@ -import 'package:flutter/foundation.dart'; -import 'package:flutter_test/flutter_test.dart'; -import 'package:immich_mobile/domain/models/exif.model.dart'; -import 'package:immich_mobile/domain/services/asset.service.dart'; -import 'package:mocktail/mocktail.dart'; - -import '../../infrastructure/repository.mock.dart'; -import '../../test_utils.dart'; - -void main() { - late AssetService sut; - late MockRemoteAssetRepository mockRemoteAssetRepository; - late MockDriftLocalAssetRepository mockLocalAssetRepository; - - setUp(() { - mockRemoteAssetRepository = MockRemoteAssetRepository(); - mockLocalAssetRepository = MockDriftLocalAssetRepository(); - sut = AssetService( - remoteAssetRepository: mockRemoteAssetRepository, - localAssetRepository: mockLocalAssetRepository, - ); - }); - - group('getAspectRatio', () { - test('flips dimensions on Android for 90° and 270° orientations', () async { - debugDefaultTargetPlatformOverride = TargetPlatform.android; - addTearDown(() => debugDefaultTargetPlatformOverride = null); - - for (final orientation in [90, 270]) { - final localAsset = TestUtils.createLocalAsset( - id: 'local-$orientation', - width: 1920, - height: 1080, - orientation: orientation, - ); - - final result = await sut.getAspectRatio(localAsset); - - expect(result, 1080 / 1920, reason: 'Orientation $orientation should flip on Android'); - } - }); - - test('does not flip dimensions on iOS regardless of orientation', () async { - debugDefaultTargetPlatformOverride = TargetPlatform.iOS; - addTearDown(() => debugDefaultTargetPlatformOverride = null); - - for (final orientation in [0, 90, 270]) { - final localAsset = TestUtils.createLocalAsset( - id: 'local-$orientation', - width: 1920, - height: 1080, - orientation: orientation, - ); - - final result = await sut.getAspectRatio(localAsset); - - expect(result, 1920 / 1080, reason: 'iOS should never flip dimensions'); - } - }); - - test('fetches dimensions from remote repository when missing from asset', () async { - final remoteAsset = TestUtils.createRemoteAsset(id: 'remote-1', width: null, height: null); - - final exif = const ExifInfo(orientation: '1'); - - final fetchedAsset = TestUtils.createRemoteAsset(id: 'remote-1', width: 1920, height: 1080); - - when(() => mockRemoteAssetRepository.getExif('remote-1')).thenAnswer((_) async => exif); - when(() => mockRemoteAssetRepository.get('remote-1')).thenAnswer((_) async => fetchedAsset); - - final result = await sut.getAspectRatio(remoteAsset); - - expect(result, 1920 / 1080); - verify(() => mockRemoteAssetRepository.get('remote-1')).called(1); - }); - - test('fetches dimensions from local repository when missing from local asset', () async { - final localAsset = TestUtils.createLocalAsset(id: 'local-1', width: null, height: null, orientation: 0); - - final fetchedAsset = TestUtils.createLocalAsset(id: 'local-1', width: 1920, height: 1080, orientation: 0); - - when(() => mockLocalAssetRepository.get('local-1')).thenAnswer((_) async => fetchedAsset); - - final result = await sut.getAspectRatio(localAsset); - - expect(result, 1920 / 1080); - verify(() => mockLocalAssetRepository.get('local-1')).called(1); - }); - - test('uses fetched asset orientation when dimensions are missing on Android', () async { - debugDefaultTargetPlatformOverride = TargetPlatform.android; - addTearDown(() => debugDefaultTargetPlatformOverride = null); - - // Original asset has default orientation 0, but dimensions are missing - final localAsset = TestUtils.createLocalAsset(id: 'local-1', width: null, height: null, orientation: 0); - - // Fetched asset has 90° orientation and proper dimensions - final fetchedAsset = TestUtils.createLocalAsset(id: 'local-1', width: 1920, height: 1080, orientation: 90); - - when(() => mockLocalAssetRepository.get('local-1')).thenAnswer((_) async => fetchedAsset); - - final result = await sut.getAspectRatio(localAsset); - - // Should flip dimensions since fetched asset has 90° orientation - expect(result, 1080 / 1920); - verify(() => mockLocalAssetRepository.get('local-1')).called(1); - }); - - test('returns 1.0 when dimensions are still unavailable after fetching', () async { - final remoteAsset = TestUtils.createRemoteAsset(id: 'remote-1', width: null, height: null); - - final exif = const ExifInfo(orientation: '1'); - - when(() => mockRemoteAssetRepository.getExif('remote-1')).thenAnswer((_) async => exif); - when(() => mockRemoteAssetRepository.get('remote-1')).thenAnswer((_) async => null); - - final result = await sut.getAspectRatio(remoteAsset); - - expect(result, 1.0); - }); - - test('returns 1.0 when height is zero', () async { - final remoteAsset = TestUtils.createRemoteAsset(id: 'remote-1', width: 1920, height: 0); - - final exif = const ExifInfo(orientation: '1'); - - when(() => mockRemoteAssetRepository.getExif('remote-1')).thenAnswer((_) async => exif); - - final result = await sut.getAspectRatio(remoteAsset); - - expect(result, 1.0); - }); - - test('handles local asset with remoteId using local orientation not remote exif', () async { - // When a LocalAsset has a remoteId (merged), we should use local orientation - // because the width/height come from the local asset (pre-corrected on iOS) - final localAsset = TestUtils.createLocalAsset( - id: 'local-1', - remoteId: 'remote-1', - width: 1920, - height: 1080, - orientation: 0, - ); - - final result = await sut.getAspectRatio(localAsset); - - expect(result, 1920 / 1080); - // Should not call remote exif for LocalAsset - verifyNever(() => mockRemoteAssetRepository.getExif(any())); - }); - - test('handles local asset with remoteId and 90 degree rotation on Android', () async { - debugDefaultTargetPlatformOverride = TargetPlatform.android; - addTearDown(() => debugDefaultTargetPlatformOverride = null); - - final localAsset = TestUtils.createLocalAsset( - id: 'local-1', - remoteId: 'remote-1', - width: 1920, - height: 1080, - orientation: 90, - ); - - final result = await sut.getAspectRatio(localAsset); - - expect(result, 1080 / 1920); - }); - - test('should not flip remote asset dimensions', () async { - final flippedOrientations = ['1', '2', '3', '4', '5', '6', '7', '8', '90', '-90']; - - for (final orientation in flippedOrientations) { - final remoteAsset = TestUtils.createRemoteAsset(id: 'remote-$orientation', width: 1920, height: 1080); - - final exif = ExifInfo(orientation: orientation); - - when(() => mockRemoteAssetRepository.getExif('remote-$orientation')).thenAnswer((_) async => exif); - - final result = await sut.getAspectRatio(remoteAsset); - - expect(result, 1920 / 1080, reason: 'Should not flipped remote asset dimensions for orientation $orientation'); - } - }); - }); -} diff --git a/mobile/test/domain/services/log_service_test.dart b/mobile/test/domain/services/log_service_test.dart index 95f677ba98..0ccef393ab 100644 --- a/mobile/test/domain/services/log_service_test.dart +++ b/mobile/test/domain/services/log_service_test.dart @@ -29,11 +29,11 @@ final _kWarnLog = LogMessage( void main() { late LogService sut; late LogRepository mockLogRepo; - late IsarStoreRepository mockStoreRepo; + late DriftStoreRepository mockStoreRepo; setUp(() async { mockLogRepo = MockLogRepository(); - mockStoreRepo = MockStoreRepository(); + mockStoreRepo = MockDriftStoreRepository(); registerFallbackValue(_kInfoLog); diff --git a/mobile/test/domain/services/store_service_test.dart b/mobile/test/domain/services/store_service_test.dart index 996170b518..8ceb1e3c9c 100644 --- a/mobile/test/domain/services/store_service_test.dart +++ b/mobile/test/domain/services/store_service_test.dart @@ -15,13 +15,11 @@ final _kBackupFailedSince = DateTime.utc(2023); void main() { late StoreService sut; - late IsarStoreRepository mockStoreRepo; late DriftStoreRepository mockDriftStoreRepo; late StreamController>> controller; setUp(() async { controller = StreamController>>.broadcast(); - mockStoreRepo = MockStoreRepository(); mockDriftStoreRepo = MockDriftStoreRepository(); // For generics, we need to provide fallback to each concrete type to avoid runtime errors registerFallbackValue(StoreKey.accessToken); @@ -29,16 +27,6 @@ void main() { registerFallbackValue(StoreKey.backgroundBackup); registerFallbackValue(StoreKey.backupFailedSince); - when(() => mockStoreRepo.getAll()).thenAnswer( - (_) async => [ - const StoreDto(StoreKey.accessToken, _kAccessToken), - const StoreDto(StoreKey.backgroundBackup, _kBackgroundBackup), - const StoreDto(StoreKey.groupAssetsBy, _kGroupAssetsBy), - StoreDto(StoreKey.backupFailedSince, _kBackupFailedSince), - ], - ); - when(() => mockStoreRepo.watchAll()).thenAnswer((_) => controller.stream); - when(() => mockDriftStoreRepo.getAll()).thenAnswer( (_) async => [ const StoreDto(StoreKey.accessToken, _kAccessToken), @@ -49,7 +37,7 @@ void main() { ); when(() => mockDriftStoreRepo.watchAll()).thenAnswer((_) => controller.stream); - sut = await StoreService.create(storeRepository: mockStoreRepo); + sut = await StoreService.create(storeRepository: mockDriftStoreRepo); }); tearDown(() async { @@ -59,7 +47,7 @@ void main() { group("Store Service Init:", () { test('Populates the internal cache on init', () { - verify(() => mockStoreRepo.getAll()).called(1); + verify(() => mockDriftStoreRepo.getAll()).called(1); expect(sut.tryGet(StoreKey.accessToken), _kAccessToken); expect(sut.tryGet(StoreKey.backgroundBackup), _kBackgroundBackup); expect(sut.tryGet(StoreKey.groupAssetsBy), _kGroupAssetsBy); @@ -74,7 +62,7 @@ void main() { await pumpEventQueue(); - verify(() => mockStoreRepo.watchAll()).called(1); + verify(() => mockDriftStoreRepo.watchAll()).called(1); expect(sut.tryGet(StoreKey.accessToken), _kAccessToken.toUpperCase()); }); }); @@ -95,19 +83,18 @@ void main() { group('Store Service put:', () { setUp(() { - when(() => mockStoreRepo.upsert(any>(), any())).thenAnswer((_) async => true); when(() => mockDriftStoreRepo.upsert(any>(), any())).thenAnswer((_) async => true); }); test('Skip insert when value is not modified', () async { await sut.put(StoreKey.accessToken, _kAccessToken); - verifyNever(() => mockStoreRepo.upsert(StoreKey.accessToken, any())); + verifyNever(() => mockDriftStoreRepo.upsert(StoreKey.accessToken, any())); }); test('Insert value when modified', () async { final newAccessToken = _kAccessToken.toUpperCase(); await sut.put(StoreKey.accessToken, newAccessToken); - verify(() => mockStoreRepo.upsert(StoreKey.accessToken, newAccessToken)).called(1); + verify(() => mockDriftStoreRepo.upsert(StoreKey.accessToken, newAccessToken)).called(1); expect(sut.tryGet(StoreKey.accessToken), newAccessToken); }); }); @@ -117,7 +104,6 @@ void main() { setUp(() { valueController = StreamController.broadcast(); - when(() => mockStoreRepo.watch(any>())).thenAnswer((_) => valueController.stream); when(() => mockDriftStoreRepo.watch(any>())).thenAnswer((_) => valueController.stream); }); @@ -136,19 +122,18 @@ void main() { } await pumpEventQueue(); - verify(() => mockStoreRepo.watch(StoreKey.accessToken)).called(1); + verify(() => mockDriftStoreRepo.watch(StoreKey.accessToken)).called(1); }); }); group('Store Service delete:', () { setUp(() { - when(() => mockStoreRepo.delete(any>())).thenAnswer((_) async => true); when(() => mockDriftStoreRepo.delete(any>())).thenAnswer((_) async => true); }); test('Removes the value from the DB', () async { await sut.delete(StoreKey.accessToken); - verify(() => mockStoreRepo.delete(StoreKey.accessToken)).called(1); + verify(() => mockDriftStoreRepo.delete(StoreKey.accessToken)).called(1); }); test('Removes the value from the cache', () async { @@ -159,13 +144,12 @@ void main() { group('Store Service clear:', () { setUp(() { - when(() => mockStoreRepo.deleteAll()).thenAnswer((_) async => true); when(() => mockDriftStoreRepo.deleteAll()).thenAnswer((_) async => true); }); test('Clears all values from the store', () async { await sut.clear(); - verify(() => mockStoreRepo.deleteAll()).called(1); + verify(() => mockDriftStoreRepo.deleteAll()).called(1); expect(sut.tryGet(StoreKey.accessToken), isNull); expect(sut.tryGet(StoreKey.backgroundBackup), isNull); expect(sut.tryGet(StoreKey.groupAssetsBy), isNull); diff --git a/mobile/test/domain/services/user_service_test.dart b/mobile/test/domain/services/user_service_test.dart index 395f38a207..80b6d80457 100644 --- a/mobile/test/domain/services/user_service_test.dart +++ b/mobile/test/domain/services/user_service_test.dart @@ -4,7 +4,6 @@ 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/domain/services/user.service.dart'; -import 'package:immich_mobile/infrastructure/repositories/user.repository.dart'; import 'package:immich_mobile/infrastructure/repositories/user_api.repository.dart'; import 'package:mocktail/mocktail.dart'; @@ -14,19 +13,13 @@ import '../service.mock.dart'; void main() { late UserService sut; - late IsarUserRepository mockUserRepo; late UserApiRepository mockUserApiRepo; late StoreService mockStoreService; setUp(() { - mockUserRepo = MockIsarUserRepository(); mockUserApiRepo = MockUserApiRepository(); mockStoreService = MockStoreService(); - sut = UserService( - isarUserRepository: mockUserRepo, - userApiRepository: mockUserApiRepo, - storeService: mockStoreService, - ); + sut = UserService(userApiRepository: mockUserApiRepo, storeService: mockStoreService); registerFallbackValue(UserStub.admin); when(() => mockStoreService.get(StoreKey.currentUser)).thenReturn(UserStub.admin); @@ -77,11 +70,9 @@ void main() { test('should return user from api and store it', () async { when(() => mockUserApiRepo.getMyUser()).thenAnswer((_) async => UserStub.admin); when(() => mockStoreService.put(StoreKey.currentUser, UserStub.admin)).thenAnswer((_) async => true); - when(() => mockUserRepo.update(UserStub.admin)).thenAnswer((_) async => UserStub.admin); final result = await sut.refreshMyUser(); verify(() => mockStoreService.put(StoreKey.currentUser, UserStub.admin)).called(1); - verify(() => mockUserRepo.update(UserStub.admin)).called(1); expect(result, UserStub.admin); }); @@ -90,7 +81,6 @@ void main() { final result = await sut.refreshMyUser(); verifyNever(() => mockStoreService.put(StoreKey.currentUser, UserStub.admin)); - verifyNever(() => mockUserRepo.update(UserStub.admin)); expect(result, isNull); }); }); @@ -104,12 +94,10 @@ void main() { () => mockUserApiRepo.createProfileImage(name: profileImagePath, data: Uint8List(0)), ).thenAnswer((_) async => profileImagePath); when(() => mockStoreService.put(StoreKey.currentUser, updatedUser)).thenAnswer((_) async => true); - when(() => mockUserRepo.update(updatedUser)).thenAnswer((_) async => UserStub.admin); final result = await sut.createProfileImage(profileImagePath, Uint8List(0)); verify(() => mockStoreService.put(StoreKey.currentUser, updatedUser)).called(1); - verify(() => mockUserRepo.update(updatedUser)).called(1); expect(result, profileImagePath); }); @@ -123,7 +111,6 @@ void main() { final result = await sut.createProfileImage(profileImagePath, Uint8List(0)); verifyNever(() => mockStoreService.put(StoreKey.currentUser, updatedUser)); - verifyNever(() => mockUserRepo.update(updatedUser)); expect(result, isNull); }); }); diff --git a/mobile/test/dto.mocks.dart b/mobile/test/dto.mocks.dart deleted file mode 100644 index ed53fcdc90..0000000000 --- a/mobile/test/dto.mocks.dart +++ /dev/null @@ -1,6 +0,0 @@ -import 'package:mocktail/mocktail.dart'; -import 'package:openapi/api.dart'; - -class MockSmartSearchDto extends Mock implements SmartSearchDto {} - -class MockMetadataSearchDto extends Mock implements MetadataSearchDto {} diff --git a/mobile/test/fixtures/album.stub.dart b/mobile/test/fixtures/album.stub.dart index a22a4b72ab..5141540a25 100644 --- a/mobile/test/fixtures/album.stub.dart +++ b/mobile/test/fixtures/album.stub.dart @@ -1,108 +1,4 @@ import 'package:immich_mobile/domain/models/album/local_album.model.dart'; -import 'package:immich_mobile/entities/album.entity.dart'; -import 'package:immich_mobile/infrastructure/entities/user.entity.dart'; - -import 'asset.stub.dart'; -import 'user.stub.dart'; - -final class AlbumStub { - const AlbumStub._(); - - static final emptyAlbum = Album( - name: "empty-album", - localId: "empty-album-local", - remoteId: "empty-album-remote", - createdAt: DateTime(2000), - modifiedAt: DateTime(2023), - shared: false, - activityEnabled: false, - startDate: DateTime(2020), - ); - - static final sharedWithUser = Album( - name: "empty-album-shared-with-user", - localId: "empty-album-shared-with-user-local", - remoteId: "empty-album-shared-with-user-remote", - createdAt: DateTime(2023), - modifiedAt: DateTime(2023), - shared: true, - activityEnabled: false, - endDate: DateTime(2020), - )..sharedUsers.addAll([User.fromDto(UserStub.admin)]); - - static final oneAsset = Album( - name: "album-with-single-asset", - localId: "album-with-single-asset-local", - remoteId: "album-with-single-asset-remote", - createdAt: DateTime(2022), - modifiedAt: DateTime(2023), - shared: false, - activityEnabled: false, - startDate: DateTime(2020), - endDate: DateTime(2023), - )..assets.addAll([AssetStub.image1]); - - static final twoAsset = - Album( - name: "album-with-two-assets", - localId: "album-with-two-assets-local", - remoteId: "album-with-two-assets-remote", - createdAt: DateTime(2001), - modifiedAt: DateTime(2010), - shared: false, - activityEnabled: false, - startDate: DateTime(2019), - endDate: DateTime(2020), - ) - ..assets.addAll([AssetStub.image1, AssetStub.image2]) - ..activityEnabled = true - ..owner.value = User.fromDto(UserStub.admin); - - static final create2020end2020Album = Album( - name: "create2020update2020Album", - localId: "create2020update2020Album-local", - remoteId: "create2020update2020Album-remote", - createdAt: DateTime(2020), - modifiedAt: DateTime(2020), - shared: false, - activityEnabled: false, - startDate: DateTime(2020), - endDate: DateTime(2020), - ); - static final create2020end2022Album = Album( - name: "create2020update2021Album", - localId: "create2020update2021Album-local", - remoteId: "create2020update2021Album-remote", - createdAt: DateTime(2020), - modifiedAt: DateTime(2022), - shared: false, - activityEnabled: false, - startDate: DateTime(2020), - endDate: DateTime(2022), - ); - static final create2020end2024Album = Album( - name: "create2020update2022Album", - localId: "create2020update2022Album-local", - remoteId: "create2020update2022Album-remote", - createdAt: DateTime(2020), - modifiedAt: DateTime(2024), - shared: false, - activityEnabled: false, - startDate: DateTime(2020), - endDate: DateTime(2024), - ); - static final create2020end2026Album = Album( - name: "create2020update2023Album", - localId: "create2020update2023Album-local", - remoteId: "create2020update2023Album-remote", - createdAt: DateTime(2020), - modifiedAt: DateTime(2026), - shared: false, - activityEnabled: false, - startDate: DateTime(2020), - endDate: DateTime(2026), - ); -} abstract final class LocalAlbumStub { const LocalAlbumStub._(); diff --git a/mobile/test/fixtures/asset.stub.dart b/mobile/test/fixtures/asset.stub.dart index 90a7f11737..473b900271 100644 --- a/mobile/test/fixtures/asset.stub.dart +++ b/mobile/test/fixtures/asset.stub.dart @@ -1,59 +1,4 @@ import 'package:immich_mobile/domain/models/asset/base_asset.model.dart'; -import 'package:immich_mobile/domain/models/exif.model.dart'; -import 'package:immich_mobile/entities/asset.entity.dart' as old; - -final class AssetStub { - const AssetStub._(); - - static final image1 = old.Asset( - checksum: "image1-checksum", - localId: "image1", - remoteId: 'image1-remote', - ownerId: 1, - fileCreatedAt: DateTime(2019), - fileModifiedAt: DateTime(2020), - updatedAt: DateTime.now(), - durationInSeconds: 0, - type: old.AssetType.image, - fileName: "image1.jpg", - isFavorite: true, - isArchived: false, - isTrashed: false, - exifInfo: const ExifInfo(isFlipped: false), - ); - - static final image2 = old.Asset( - checksum: "image2-checksum", - localId: "image2", - remoteId: 'image2-remote', - ownerId: 1, - fileCreatedAt: DateTime(2000), - fileModifiedAt: DateTime(2010), - updatedAt: DateTime.now(), - durationInSeconds: 60, - type: old.AssetType.video, - fileName: "image2.jpg", - isFavorite: false, - isArchived: false, - isTrashed: false, - exifInfo: const ExifInfo(isFlipped: true), - ); - - static final image3 = old.Asset( - checksum: "image3-checksum", - localId: "image3", - ownerId: 1, - fileCreatedAt: DateTime(2025), - fileModifiedAt: DateTime(2025), - updatedAt: DateTime.now(), - durationInSeconds: 60, - type: old.AssetType.image, - fileName: "image3.jpg", - isFavorite: true, - isArchived: false, - isTrashed: false, - ); -} abstract final class LocalAssetStub { const LocalAssetStub._(); diff --git a/mobile/test/fixtures/exif.stub.dart b/mobile/test/fixtures/exif.stub.dart deleted file mode 100644 index 5ad9a41761..0000000000 --- a/mobile/test/fixtures/exif.stub.dart +++ /dev/null @@ -1,18 +0,0 @@ -import 'package:immich_mobile/domain/models/exif.model.dart'; - -abstract final class ExifStub { - static final size = const ExifInfo(assetId: 1, fileSize: 1000); - - static final gps = const ExifInfo( - assetId: 2, - latitude: 20, - longitude: 20, - city: 'city', - state: 'state', - country: 'country', - ); - - static final rotated90CW = const ExifInfo(assetId: 3, orientation: "90"); - - static final rotated270CW = const ExifInfo(assetId: 4, orientation: "-90"); -} diff --git a/mobile/test/fixtures/user.stub.dart b/mobile/test/fixtures/user.stub.dart index 2ba7177f89..b92ba71e5b 100644 --- a/mobile/test/fixtures/user.stub.dart +++ b/mobile/test/fixtures/user.stub.dart @@ -12,24 +12,4 @@ abstract final class UserStub { profileChangedAt: DateTime(2021), avatarColor: AvatarColor.green, ); - - static final user1 = UserDto( - id: "user1", - email: "user1@test.com", - name: "user1", - isAdmin: false, - updatedAt: DateTime(2022), - profileChangedAt: DateTime(2022), - avatarColor: AvatarColor.red, - ); - - static final user2 = UserDto( - id: "user2", - email: "user2@test.com", - name: "user2", - isAdmin: false, - updatedAt: DateTime(2023), - profileChangedAt: DateTime(2023), - avatarColor: AvatarColor.primary, - ); } diff --git a/mobile/test/infrastructure/repositories/exif_repository_test.dart b/mobile/test/infrastructure/repositories/exif_repository_test.dart deleted file mode 100644 index 4e7ee4d79d..0000000000 --- a/mobile/test/infrastructure/repositories/exif_repository_test.dart +++ /dev/null @@ -1,49 +0,0 @@ -import 'package:flutter_test/flutter_test.dart'; -import 'package:immich_mobile/infrastructure/entities/exif.entity.dart'; -import 'package:immich_mobile/infrastructure/repositories/exif.repository.dart'; -import 'package:isar/isar.dart'; - -import '../../fixtures/exif.stub.dart'; -import '../../test_utils.dart'; - -Future _populateExifTable(Isar db) async { - await db.writeTxn(() async { - await db.exifInfos.putAll([ - ExifInfo.fromDto(ExifStub.size), - ExifInfo.fromDto(ExifStub.gps), - ExifInfo.fromDto(ExifStub.rotated90CW), - ExifInfo.fromDto(ExifStub.rotated270CW), - ]); - }); -} - -void main() { - late Isar db; - late IsarExifRepository sut; - - setUp(() async { - db = await TestUtils.initIsar(); - sut = IsarExifRepository(db); - }); - - group("Return with proper orientation", () { - setUp(() async { - await _populateExifTable(db); - }); - - test("isFlipped true for 90CW", () async { - final exif = await sut.get(ExifStub.rotated90CW.assetId!); - expect(exif!.isFlipped, true); - }); - - test("isFlipped true for 270CW", () async { - final exif = await sut.get(ExifStub.rotated270CW.assetId!); - expect(exif!.isFlipped, true); - }); - - test("isFlipped false for the original non-rotated image", () async { - final exif = await sut.get(ExifStub.size.assetId!); - expect(exif!.isFlipped, false); - }); - }); -} diff --git a/mobile/test/infrastructure/repositories/store_repository_test.dart b/mobile/test/infrastructure/repositories/store_repository_test.dart index 18d41e32e0..4cf1adc6b1 100644 --- a/mobile/test/infrastructure/repositories/store_repository_test.dart +++ b/mobile/test/infrastructure/repositories/store_repository_test.dart @@ -1,14 +1,15 @@ import 'dart:async'; +import 'package:drift/drift.dart' hide isNull; +import 'package:drift/native.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:immich_mobile/domain/models/store.model.dart'; import 'package:immich_mobile/domain/models/user.model.dart'; -import 'package:immich_mobile/infrastructure/entities/store.entity.dart'; +import 'package:immich_mobile/infrastructure/entities/store.entity.drift.dart'; +import 'package:immich_mobile/infrastructure/repositories/db.repository.dart'; import 'package:immich_mobile/infrastructure/repositories/store.repository.dart'; -import 'package:isar/isar.dart'; import '../../fixtures/user.stub.dart'; -import '../../test_utils.dart'; const _kTestAccessToken = "#TestToken"; final _kTestBackupFailed = DateTime(2025, 2, 20, 11, 45); @@ -16,30 +17,54 @@ const _kTestVersion = 10; const _kTestColorfulInterface = false; final _kTestUser = UserStub.admin; -Future _addIntStoreValue(Isar db, StoreKey key, int? value) async { - await db.storeValues.put(StoreValue(key.id, intValue: value, strValue: null)); -} - -Future _addStrStoreValue(Isar db, StoreKey key, String? value) async { - await db.storeValues.put(StoreValue(key.id, intValue: null, strValue: value)); -} - -Future _populateStore(Isar db) async { - await db.writeTxn(() async { - await _addIntStoreValue(db, StoreKey.colorfulInterface, _kTestColorfulInterface ? 1 : 0); - await _addIntStoreValue(db, StoreKey.backupFailedSince, _kTestBackupFailed.millisecondsSinceEpoch); - await _addStrStoreValue(db, StoreKey.accessToken, _kTestAccessToken); - await _addIntStoreValue(db, StoreKey.version, _kTestVersion); +Future _populateStore(Drift db) async { + await db.batch((batch) async { + batch.insert( + db.storeEntity, + StoreEntityCompanion( + id: Value(StoreKey.colorfulInterface.id), + intValue: const Value(_kTestColorfulInterface ? 1 : 0), + stringValue: const Value(null), + ), + ); + batch.insert( + db.storeEntity, + StoreEntityCompanion( + id: Value(StoreKey.backupFailedSince.id), + intValue: Value(_kTestBackupFailed.millisecondsSinceEpoch), + stringValue: const Value(null), + ), + ); + batch.insert( + db.storeEntity, + StoreEntityCompanion( + id: Value(StoreKey.accessToken.id), + intValue: const Value(null), + stringValue: const Value(_kTestAccessToken), + ), + ); + batch.insert( + db.storeEntity, + StoreEntityCompanion( + id: Value(StoreKey.version.id), + intValue: const Value(_kTestVersion), + stringValue: const Value(null), + ), + ); }); } void main() { - late Isar db; - late IsarStoreRepository sut; + late Drift db; + late DriftStoreRepository sut; setUp(() async { - db = await TestUtils.initIsar(); - sut = IsarStoreRepository(db); + db = Drift(DatabaseConnection(NativeDatabase.memory(), closeStreamsSynchronously: true)); + sut = DriftStoreRepository(db); + }); + + tearDown(() async { + await db.close(); }); group('Store Repository converters:', () { @@ -98,10 +123,10 @@ void main() { }); test('deleteAll()', () async { - final count = await db.storeValues.count(); + final count = await db.storeEntity.count().getSingle(); expect(count, isNot(isZero)); await sut.deleteAll(); - unawaited(expectLater(await db.storeValues.count(), isZero)); + unawaited(expectLater(await db.storeEntity.count().getSingle(), isZero)); }); }); diff --git a/mobile/test/infrastructure/repositories/sync_api_repository_test.dart b/mobile/test/infrastructure/repositories/sync_api_repository_test.dart index 85eebacb14..d538b567bd 100644 --- a/mobile/test/infrastructure/repositories/sync_api_repository_test.dart +++ b/mobile/test/infrastructure/repositories/sync_api_repository_test.dart @@ -1,10 +1,13 @@ import 'dart:async'; import 'dart:convert'; +import 'package:drift/drift.dart'; +import 'package:drift/native.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:http/http.dart' as http; import 'package:immich_mobile/domain/models/sync_event.model.dart'; import 'package:immich_mobile/domain/services/store.service.dart'; +import 'package:immich_mobile/infrastructure/repositories/db.repository.dart'; import 'package:immich_mobile/infrastructure/repositories/store.repository.dart'; import 'package:immich_mobile/infrastructure/repositories/sync_api.repository.dart'; import 'package:immich_mobile/utils/semver.dart'; @@ -13,7 +16,6 @@ import 'package:openapi/api.dart'; import '../../api.mocks.dart'; import '../../service.mocks.dart'; -import '../../test_utils.dart'; class MockHttpClient extends Mock implements http.Client {} @@ -38,7 +40,8 @@ void main() { late int testBatchSize = 3; setUpAll(() async { - await StoreService.init(storeRepository: IsarStoreRepository(await TestUtils.initIsar())); + final db = Drift(DatabaseConnection(NativeDatabase.memory(), closeStreamsSynchronously: true)); + await StoreService.init(storeRepository: DriftStoreRepository(db)); }); setUp(() { @@ -137,7 +140,7 @@ void main() { bool abortWasCalledInCallback = false; final Completer firstBatchReceived = Completer(); - Future onDataCallback(List events, Function() abort, Function() _) async { + Future onDataCallback(List _, Function() abort, Function() _) async { onDataCallCount++; if (onDataCallCount == 1) { abort(); @@ -241,7 +244,7 @@ void main() { final streamError = Exception("Network Error"); int onDataCallCount = 0; - Future onDataCallback(List events, Function() _, Function() __) async { + Future onDataCallback(List _, Function() _, Function() __) async { onDataCallCount++; } @@ -267,7 +270,7 @@ void main() { when(() => mockStreamedResponse.stream).thenAnswer((_) => http.ByteStream(errorBodyController.stream)); int onDataCallCount = 0; - Future onDataCallback(List events, Function() _, Function() __) async { + Future onDataCallback(List _, Function() _, Function() __) async { onDataCallCount++; } diff --git a/mobile/test/infrastructure/repository.mock.dart b/mobile/test/infrastructure/repository.mock.dart index 2d4af5b308..b7992c1822 100644 --- a/mobile/test/infrastructure/repository.mock.dart +++ b/mobile/test/infrastructure/repository.mock.dart @@ -1,5 +1,4 @@ import 'package:immich_mobile/infrastructure/repositories/backup.repository.dart'; -import 'package:immich_mobile/infrastructure/repositories/device_asset.repository.dart'; import 'package:immich_mobile/infrastructure/repositories/local_album.repository.dart'; import 'package:immich_mobile/infrastructure/repositories/local_asset.repository.dart'; import 'package:immich_mobile/infrastructure/repositories/log.repository.dart'; @@ -11,22 +10,15 @@ import 'package:immich_mobile/infrastructure/repositories/sync_api.repository.da import 'package:immich_mobile/infrastructure/repositories/sync_migration.repository.dart'; import 'package:immich_mobile/infrastructure/repositories/sync_stream.repository.dart'; import 'package:immich_mobile/infrastructure/repositories/trashed_local_asset.repository.dart'; -import 'package:immich_mobile/infrastructure/repositories/user.repository.dart'; import 'package:immich_mobile/infrastructure/repositories/user_api.repository.dart'; import 'package:immich_mobile/repositories/drift_album_api_repository.dart'; import 'package:immich_mobile/repositories/upload.repository.dart'; import 'package:mocktail/mocktail.dart'; -class MockStoreRepository extends Mock implements IsarStoreRepository {} - class MockDriftStoreRepository extends Mock implements DriftStoreRepository {} class MockLogRepository extends Mock implements LogRepository {} -class MockIsarUserRepository extends Mock implements IsarUserRepository {} - -class MockDeviceAssetRepository extends Mock implements IsarDeviceAssetRepository {} - class MockSyncStreamRepository extends Mock implements SyncStreamRepository {} class MockLocalAlbumRepository extends Mock implements DriftLocalAlbumRepository {} diff --git a/mobile/test/modules/activity/activities_page_test.dart b/mobile/test/modules/activity/activities_page_test.dart deleted file mode 100644 index 39350530ea..0000000000 --- a/mobile/test/modules/activity/activities_page_test.dart +++ /dev/null @@ -1,175 +0,0 @@ -@Skip('currently failing due to mock HTTP client to download ISAR binaries') -@Tags(['widget']) -library; - -import 'package:flutter/material.dart'; -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/entities/album.entity.dart'; -import 'package:immich_mobile/entities/asset.entity.dart'; -import 'package:immich_mobile/entities/store.entity.dart'; -import 'package:immich_mobile/infrastructure/entities/user.entity.dart'; -import 'package:immich_mobile/infrastructure/repositories/store.repository.dart'; -import 'package:immich_mobile/models/activities/activity.model.dart'; -import 'package:immich_mobile/pages/common/activities.page.dart'; -import 'package:immich_mobile/providers/activity.provider.dart'; -import 'package:immich_mobile/providers/album/current_album.provider.dart'; -import 'package:immich_mobile/providers/asset_viewer/current_asset.provider.dart'; -import 'package:immich_mobile/providers/user.provider.dart'; -import 'package:immich_mobile/widgets/activities/activity_text_field.dart'; -import 'package:immich_mobile/widgets/activities/dismissible_activity.dart'; -import 'package:isar/isar.dart'; -import 'package:mocktail/mocktail.dart'; -import 'package:riverpod_annotation/riverpod_annotation.dart'; - -import '../../fixtures/album.stub.dart'; -import '../../fixtures/asset.stub.dart'; -import '../../fixtures/user.stub.dart'; -import '../../test_utils.dart'; -import '../../widget_tester_extensions.dart'; -import '../album/album_mocks.dart'; -import '../asset_viewer/asset_viewer_mocks.dart'; -import '../shared/shared_mocks.dart'; -import 'activity_mocks.dart'; - -final _activities = [ - Activity( - id: '1', - createdAt: DateTime(100), - type: ActivityType.comment, - comment: 'First Activity', - assetId: 'asset-2', - user: UserStub.admin, - ), - Activity( - id: '2', - createdAt: DateTime(200), - type: ActivityType.comment, - comment: 'Second Activity', - user: UserStub.user1, - ), - Activity(id: '3', createdAt: DateTime(300), type: ActivityType.like, assetId: 'asset-1', user: UserStub.user2), - Activity(id: '4', createdAt: DateTime(400), type: ActivityType.like, user: UserStub.user1), -]; - -void main() { - late MockAlbumActivity activityMock; - late MockCurrentAlbumProvider mockCurrentAlbumProvider; - late MockCurrentAssetProvider mockCurrentAssetProvider; - late List overrides; - late Isar db; - - setUpAll(() async { - TestUtils.init(); - db = await TestUtils.initIsar(); - await StoreService.init(storeRepository: IsarStoreRepository(db)); - await Store.put(StoreKey.currentUser, UserStub.admin); - await Store.put(StoreKey.serverEndpoint, ''); - await Store.put(StoreKey.accessToken, ''); - }); - - setUp(() async { - mockCurrentAlbumProvider = MockCurrentAlbumProvider(AlbumStub.twoAsset); - mockCurrentAssetProvider = MockCurrentAssetProvider(AssetStub.image1); - activityMock = MockAlbumActivity(_activities); - overrides = [ - albumActivityProvider(AlbumStub.twoAsset.remoteId!, AssetStub.image1.remoteId!).overrideWith(() => activityMock), - currentAlbumProvider.overrideWith(() => mockCurrentAlbumProvider), - currentAssetProvider.overrideWith(() => mockCurrentAssetProvider), - ]; - - await db.writeTxn(() async { - await db.clear(); - // Save all assets - await db.users.put(User.fromDto(UserStub.admin)); - await db.assets.putAll([AssetStub.image1, AssetStub.image2]); - await db.albums.put(AlbumStub.twoAsset); - await AlbumStub.twoAsset.owner.save(); - await AlbumStub.twoAsset.assets.save(); - }); - expect(db.albums.countSync(), 1); - expect(db.assets.countSync(), 2); - expect(db.users.countSync(), 1); - }); - - group("App bar", () { - testWidgets("No title when currentAsset != null", (tester) async { - await tester.pumpConsumerWidget(const ActivitiesPage(), overrides: overrides); - - final listTile = tester.widget(find.byType(AppBar)); - expect(listTile.title, isNull); - }); - - testWidgets("Album name as title when currentAsset == null", (tester) async { - await tester.pumpConsumerWidget(const ActivitiesPage(), overrides: overrides); - await tester.pumpAndSettle(); - - mockCurrentAssetProvider.state = null; - await tester.pumpAndSettle(); - - expect(find.text(AlbumStub.twoAsset.name), findsOneWidget); - final listTile = tester.widget(find.byType(AppBar)); - expect(listTile.title, isNotNull); - }); - }); - - group("Body", () { - testWidgets("Contains a stack with Activity List and Activity Input", (tester) async { - await tester.pumpConsumerWidget(const ActivitiesPage(), overrides: overrides); - await tester.pumpAndSettle(); - - expect(find.descendant(of: find.byType(Stack), matching: find.byType(ActivityTextField)), findsOneWidget); - - expect(find.descendant(of: find.byType(Stack), matching: find.byType(ListView)), findsOneWidget); - }); - - testWidgets("List Contains all dismissible activities", (tester) async { - await tester.pumpConsumerWidget(const ActivitiesPage(), overrides: overrides); - await tester.pumpAndSettle(); - - final listFinder = find.descendant(of: find.byType(Stack), matching: find.byType(ListView)); - final listChildren = find.descendant(of: listFinder, matching: find.byType(DismissibleActivity)); - expect(listChildren, findsNWidgets(_activities.length)); - }); - - testWidgets("Submitting text input adds a comment with the text", (tester) async { - await tester.pumpConsumerWidget(const ActivitiesPage(), overrides: overrides); - await tester.pumpAndSettle(); - - when(() => activityMock.addComment(any())).thenAnswer((_) => Future.value()); - - final textField = find.byType(TextField); - await tester.enterText(textField, 'Test comment'); - await tester.testTextInput.receiveAction(TextInputAction.done); - - verify(() => activityMock.addComment('Test comment')); - }); - - testWidgets("Owner can remove all activities", (tester) async { - await tester.pumpConsumerWidget(const ActivitiesPage(), overrides: overrides); - await tester.pumpAndSettle(); - - final deletableActivityFinder = find.byWidgetPredicate( - (widget) => widget is DismissibleActivity && widget.onDismiss != null, - ); - expect(deletableActivityFinder, findsNWidgets(_activities.length)); - }); - - testWidgets("Non-Owner can remove only their activities", (tester) async { - final mockCurrentUser = MockCurrentUserProvider(); - - await tester.pumpConsumerWidget( - const ActivitiesPage(), - overrides: [...overrides, currentUserProvider.overrideWith((ref) => mockCurrentUser)], - ); - mockCurrentUser.state = UserStub.user1; - await tester.pumpAndSettle(); - - final deletableActivityFinder = find.byWidgetPredicate( - (widget) => widget is DismissibleActivity && widget.onDismiss != null, - ); - expect(deletableActivityFinder, findsNWidgets(_activities.where((a) => a.user == UserStub.user1).length)); - }); - }); -} diff --git a/mobile/test/modules/activity/activity_mocks.dart b/mobile/test/modules/activity/activity_mocks.dart deleted file mode 100644 index c50810795e..0000000000 --- a/mobile/test/modules/activity/activity_mocks.dart +++ /dev/null @@ -1,19 +0,0 @@ -import 'package:immich_mobile/models/activities/activity.model.dart'; -import 'package:immich_mobile/providers/activity.provider.dart'; -import 'package:immich_mobile/providers/activity_statistics.provider.dart'; -import 'package:immich_mobile/services/activity.service.dart'; -import 'package:mocktail/mocktail.dart'; - -class ActivityServiceMock extends Mock implements ActivityService {} - -class MockAlbumActivity extends AlbumActivityInternal with Mock implements AlbumActivity { - List? initActivities; - MockAlbumActivity([this.initActivities]); - - @override - Future> build(String albumId, [String? assetId]) async { - return initActivities ?? []; - } -} - -class ActivityStatisticsMock extends ActivityStatisticsInternal with Mock implements ActivityStatistics {} diff --git a/mobile/test/modules/activity/activity_provider_test.dart b/mobile/test/modules/activity/activity_provider_test.dart deleted file mode 100644 index 84eba62b70..0000000000 --- a/mobile/test/modules/activity/activity_provider_test.dart +++ /dev/null @@ -1,331 +0,0 @@ -import 'package:flutter_test/flutter_test.dart'; -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/models/activities/activity.model.dart'; -import 'package:immich_mobile/providers/activity.provider.dart'; -import 'package:immich_mobile/providers/activity_service.provider.dart'; -import 'package:immich_mobile/providers/activity_statistics.provider.dart'; -import 'package:mocktail/mocktail.dart'; - -import '../../fixtures/user.stub.dart'; -import '../../test_utils.dart'; -import 'activity_mocks.dart'; - -final _activities = [ - Activity( - id: '1', - createdAt: DateTime(100), - type: ActivityType.comment, - comment: 'First Activity', - assetId: 'asset-2', - user: UserStub.admin, - ), - Activity( - id: '2', - createdAt: DateTime(200), - type: ActivityType.comment, - comment: 'Second Activity', - user: UserStub.user1, - ), - Activity(id: '3', createdAt: DateTime(300), type: ActivityType.like, assetId: 'asset-1', user: UserStub.admin), - Activity(id: '4', createdAt: DateTime(400), type: ActivityType.like, user: UserStub.user1), -]; - -void main() { - late ActivityServiceMock activityMock; - late ActivityStatisticsMock activityStatisticsMock; - late ActivityStatisticsMock albumActivityStatisticsMock; - late ProviderContainer container; - late AlbumActivityProvider provider; - late ListenerMock>> listener; - - setUpAll(() { - registerFallbackValue(AsyncData>([..._activities])); - }); - - setUp(() async { - activityMock = ActivityServiceMock(); - activityStatisticsMock = ActivityStatisticsMock(); - albumActivityStatisticsMock = ActivityStatisticsMock(); - - container = TestUtils.createContainer( - overrides: [ - activityServiceProvider.overrideWith((ref) => activityMock), - activityStatisticsProvider('test-album', 'test-asset').overrideWith(() => activityStatisticsMock), - activityStatisticsProvider('test-album').overrideWith(() => albumActivityStatisticsMock), - ], - ); - - // Mock values - when(() => activityStatisticsMock.build(any(), any())).thenReturn(0); - when(() => albumActivityStatisticsMock.build(any())).thenReturn(0); - when( - () => activityMock.getAllActivities('test-album', assetId: 'test-asset'), - ).thenAnswer((_) async => [..._activities]); - when(() => activityMock.getAllActivities('test-album')).thenAnswer((_) async => [..._activities]); - - // Init and wait for providers future to complete - provider = albumActivityProvider('test-album', 'test-asset'); - listener = ListenerMock(); - container.listen(provider, listener.call, fireImmediately: true); - - await container.read(provider.future); - }); - - test('Returns a list of activity', () async { - verifyInOrder([ - () => listener.call(null, const AsyncLoading()), - () => listener.call( - const AsyncLoading(), - any( - that: allOf([ - isA>>(), - predicate((AsyncData> ad) => ad.requireValue.every((e) => _activities.contains(e))), - ]), - ), - ), - ]); - - verifyNoMoreInteractions(listener); - }); - - group('addLike()', () { - test('Like successfully added', () async { - final like = Activity(id: '5', createdAt: DateTime(2023), type: ActivityType.like, user: UserStub.admin); - - when( - () => activityMock.addActivity('test-album', ActivityType.like, assetId: 'test-asset'), - ).thenAnswer((_) async => AsyncData(like)); - - final albumProvider = albumActivityProvider('test-album'); - container.read(albumProvider.notifier); - await container.read(albumProvider.future); - - await container.read(provider.notifier).addLike(); - - verify(() => activityMock.addActivity('test-album', ActivityType.like, assetId: 'test-asset')); - - final activities = await container.read(provider.future); - expect(activities, hasLength(5)); - expect(activities, contains(like)); - - // Never bump activity count for new likes - verifyNever(() => activityStatisticsMock.addActivity()); - verifyNever(() => albumActivityStatisticsMock.addActivity()); - - final albumActivities = container.read(albumProvider).requireValue; - expect(albumActivities, hasLength(5)); - expect(albumActivities, contains(like)); - }); - - test('Like failed', () async { - final like = Activity(id: '5', createdAt: DateTime(2023), type: ActivityType.like, user: UserStub.admin); - when( - () => activityMock.addActivity('test-album', ActivityType.like, assetId: 'test-asset'), - ).thenAnswer((_) async => AsyncError(Exception('Mock'), StackTrace.current)); - - final albumProvider = albumActivityProvider('test-album'); - container.read(albumProvider.notifier); - await container.read(albumProvider.future); - - await container.read(provider.notifier).addLike(); - - verify(() => activityMock.addActivity('test-album', ActivityType.like, assetId: 'test-asset')); - - final activities = await container.read(provider.future); - expect(activities, hasLength(4)); - expect(activities, isNot(contains(like))); - - verifyNever(() => albumActivityStatisticsMock.addActivity()); - - final albumActivities = container.read(albumProvider).requireValue; - expect(albumActivities, hasLength(4)); - expect(albumActivities, isNot(contains(like))); - }); - }); - - group('removeActivity()', () { - test('Like successfully removed', () async { - when(() => activityMock.removeActivity('3')).thenAnswer((_) async => true); - - await container.read(provider.notifier).removeActivity('3'); - - verify(() => activityMock.removeActivity('3')); - - final activities = await container.read(provider.future); - expect(activities, hasLength(3)); - expect(activities, isNot(anyElement(predicate((Activity a) => a.id == '3')))); - - verifyNever(() => activityStatisticsMock.removeActivity()); - verifyNever(() => albumActivityStatisticsMock.removeActivity()); - }); - - test('Remove Like failed', () async { - when(() => activityMock.removeActivity('3')).thenAnswer((_) async => false); - - await container.read(provider.notifier).removeActivity('3'); - - final activities = await container.read(provider.future); - expect(activities, hasLength(4)); - expect(activities, anyElement(predicate((Activity a) => a.id == '3'))); - - verifyNever(() => activityStatisticsMock.removeActivity()); - verifyNever(() => albumActivityStatisticsMock.removeActivity()); - }); - - test('Comment successfully removed', () async { - when(() => activityMock.removeActivity('1')).thenAnswer((_) async => true); - - await container.read(provider.notifier).removeActivity('1'); - - final activities = await container.read(provider.future); - expect(activities, isNot(anyElement(predicate((Activity a) => a.id == '1')))); - - verify(() => activityStatisticsMock.removeActivity()); - verify(() => albumActivityStatisticsMock.removeActivity()); - }); - - test('Removes activity from album state when asset scoped', () async { - when(() => activityMock.removeActivity('3')).thenAnswer((_) async => true); - when(() => activityMock.getAllActivities('test-album')).thenAnswer((_) async => [..._activities]); - - final albumProvider = albumActivityProvider('test-album'); - container.read(albumProvider.notifier); - await container.read(albumProvider.future); - - await container.read(provider.notifier).removeActivity('3'); - - final assetActivities = container.read(provider).requireValue; - final albumActivities = container.read(albumProvider).requireValue; - - expect(assetActivities, hasLength(3)); - expect(assetActivities, isNot(anyElement(predicate((Activity a) => a.id == '3')))); - - expect(albumActivities, hasLength(3)); - expect(albumActivities, isNot(anyElement(predicate((Activity a) => a.id == '3')))); - - verify(() => activityMock.removeActivity('3')); - verifyNever(() => activityStatisticsMock.removeActivity()); - verifyNever(() => albumActivityStatisticsMock.removeActivity()); - }); - }); - - group('addComment()', () { - test('Comment successfully added', () async { - final comment = Activity( - id: '5', - createdAt: DateTime(2023), - type: ActivityType.comment, - user: UserStub.admin, - comment: 'Test-Comment', - assetId: 'test-asset', - ); - - final albumProvider = albumActivityProvider('test-album'); - container.read(albumProvider.notifier); - await container.read(albumProvider.future); - - when( - () => activityMock.addActivity( - 'test-album', - ActivityType.comment, - assetId: 'test-asset', - comment: 'Test-Comment', - ), - ).thenAnswer((_) async => AsyncData(comment)); - when(() => activityStatisticsMock.build('test-album', 'test-asset')).thenReturn(4); - when(() => albumActivityStatisticsMock.build('test-album')).thenReturn(2); - - await container.read(provider.notifier).addComment('Test-Comment'); - - verify( - () => activityMock.addActivity( - 'test-album', - ActivityType.comment, - assetId: 'test-asset', - comment: 'Test-Comment', - ), - ); - - final activities = await container.read(provider.future); - expect(activities, hasLength(5)); - expect(activities, contains(comment)); - - verify(() => activityStatisticsMock.addActivity()); - verify(() => albumActivityStatisticsMock.addActivity()); - - final albumActivities = container.read(albumProvider).requireValue; - expect(albumActivities, hasLength(5)); - expect(albumActivities, contains(comment)); - }); - - test('Comment successfully added without assetId', () async { - final comment = Activity( - id: '5', - createdAt: DateTime(2023), - type: ActivityType.comment, - user: UserStub.admin, - assetId: 'test-asset', - comment: 'Test-Comment', - ); - - when( - () => activityMock.addActivity('test-album', ActivityType.comment, comment: 'Test-Comment'), - ).thenAnswer((_) async => AsyncData(comment)); - when(() => albumActivityStatisticsMock.build('test-album')).thenReturn(2); - when(() => activityMock.getAllActivities('test-album')).thenAnswer((_) async => [..._activities]); - - final albumProvider = albumActivityProvider('test-album'); - container.read(albumProvider.notifier); - await container.read(albumProvider.future); - await container.read(albumProvider.notifier).addComment('Test-Comment'); - - verify( - () => activityMock.addActivity('test-album', ActivityType.comment, assetId: null, comment: 'Test-Comment'), - ); - - final activities = await container.read(albumProvider.future); - expect(activities, hasLength(5)); - expect(activities, contains(comment)); - - verifyNever(() => activityStatisticsMock.addActivity()); - verify(() => albumActivityStatisticsMock.addActivity()); - }); - - test('Comment failed', () async { - final comment = Activity( - id: '5', - createdAt: DateTime(2023), - type: ActivityType.comment, - user: UserStub.admin, - comment: 'Test-Comment', - assetId: 'test-asset', - ); - - when( - () => activityMock.addActivity( - 'test-album', - ActivityType.comment, - assetId: 'test-asset', - comment: 'Test-Comment', - ), - ).thenAnswer((_) async => AsyncError(Exception('Error'), StackTrace.current)); - - final albumProvider = albumActivityProvider('test-album'); - container.read(albumProvider.notifier); - await container.read(albumProvider.future); - - await container.read(provider.notifier).addComment('Test-Comment'); - - final activities = await container.read(provider.future); - expect(activities, hasLength(4)); - expect(activities, isNot(contains(comment))); - - verifyNever(() => activityStatisticsMock.addActivity()); - verifyNever(() => albumActivityStatisticsMock.addActivity()); - - final albumActivities = container.read(albumProvider).requireValue; - expect(albumActivities, hasLength(4)); - expect(albumActivities, isNot(contains(comment))); - }); - }); -} diff --git a/mobile/test/modules/activity/activity_statistics_provider_test.dart b/mobile/test/modules/activity/activity_statistics_provider_test.dart deleted file mode 100644 index 7fe73868f5..0000000000 --- a/mobile/test/modules/activity/activity_statistics_provider_test.dart +++ /dev/null @@ -1,71 +0,0 @@ -import 'package:flutter_test/flutter_test.dart'; -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/models/activities/activity.model.dart'; -import 'package:immich_mobile/providers/activity_service.provider.dart'; -import 'package:immich_mobile/providers/activity_statistics.provider.dart'; -import 'package:mocktail/mocktail.dart'; - -import '../../test_utils.dart'; -import 'activity_mocks.dart'; - -void main() { - late ActivityServiceMock activityMock; - late ProviderContainer container; - late ListenerMock listener; - - setUp(() async { - activityMock = ActivityServiceMock(); - container = TestUtils.createContainer(overrides: [activityServiceProvider.overrideWith((ref) => activityMock)]); - listener = ListenerMock(); - }); - - test('Returns the proper count family', () async { - when( - () => activityMock.getStatistics('test-album', assetId: 'test-asset'), - ).thenAnswer((_) async => const ActivityStats(comments: 5)); - - // Read here to make the getStatistics call - container.read(activityStatisticsProvider('test-album', 'test-asset')); - - container.listen(activityStatisticsProvider('test-album', 'test-asset'), listener.call, fireImmediately: true); - - // Sleep for the getStatistics future to resolve - await Future.delayed(const Duration(milliseconds: 1)); - - verifyInOrder([() => listener.call(null, 0), () => listener.call(0, 5)]); - - verifyNoMoreInteractions(listener); - }); - - test('Adds activity', () async { - when(() => activityMock.getStatistics('test-album')).thenAnswer((_) async => const ActivityStats(comments: 10)); - - final provider = activityStatisticsProvider('test-album'); - container.listen(provider, listener.call, fireImmediately: true); - - // Sleep for the getStatistics future to resolve - await Future.delayed(const Duration(milliseconds: 1)); - - container.read(provider.notifier).addActivity(); - container.read(provider.notifier).addActivity(); - - expect(container.read(provider), 12); - }); - - test('Removes activity', () async { - when( - () => activityMock.getStatistics('new-album', assetId: 'test-asset'), - ).thenAnswer((_) async => const ActivityStats(comments: 10)); - - final provider = activityStatisticsProvider('new-album', 'test-asset'); - container.listen(provider, listener.call, fireImmediately: true); - - // Sleep for the getStatistics future to resolve - await Future.delayed(const Duration(milliseconds: 1)); - - container.read(provider.notifier).removeActivity(); - container.read(provider.notifier).removeActivity(); - - expect(container.read(provider), 8); - }); -} diff --git a/mobile/test/modules/activity/activity_text_field_test.dart b/mobile/test/modules/activity/activity_text_field_test.dart deleted file mode 100644 index 4f4a2c7068..0000000000 --- a/mobile/test/modules/activity/activity_text_field_test.dart +++ /dev/null @@ -1,149 +0,0 @@ -@Skip('currently failing due to mock HTTP client to download ISAR binaries') -@Tags(['widget']) -library; - -import 'package:flutter/material.dart'; -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/entities/store.entity.dart'; -import 'package:immich_mobile/infrastructure/repositories/store.repository.dart'; -import 'package:immich_mobile/providers/activity.provider.dart'; -import 'package:immich_mobile/providers/album/current_album.provider.dart'; -import 'package:immich_mobile/providers/user.provider.dart'; -import 'package:immich_mobile/widgets/activities/activity_text_field.dart'; -import 'package:immich_mobile/widgets/common/user_circle_avatar.dart'; -import 'package:isar/isar.dart'; -import 'package:mocktail/mocktail.dart'; -import 'package:riverpod_annotation/riverpod_annotation.dart'; - -import '../../fixtures/album.stub.dart'; -import '../../fixtures/user.stub.dart'; -import '../../test_utils.dart'; -import '../../widget_tester_extensions.dart'; -import '../album/album_mocks.dart'; -import '../shared/shared_mocks.dart'; -import 'activity_mocks.dart'; - -void main() { - late Isar db; - late MockCurrentAlbumProvider mockCurrentAlbumProvider; - late MockAlbumActivity activityMock; - late List overrides; - - setUpAll(() async { - TestUtils.init(); - db = await TestUtils.initIsar(); - await StoreService.init(storeRepository: IsarStoreRepository(db)); - await Store.put(StoreKey.currentUser, UserStub.admin); - await Store.put(StoreKey.serverEndpoint, ''); - }); - - setUp(() { - mockCurrentAlbumProvider = MockCurrentAlbumProvider(AlbumStub.twoAsset); - activityMock = MockAlbumActivity(); - overrides = [ - currentAlbumProvider.overrideWith(() => mockCurrentAlbumProvider), - albumActivityProvider(AlbumStub.twoAsset.remoteId!).overrideWith(() => activityMock), - ]; - }); - - testWidgets('Returns an Input text field', (tester) async { - await tester.pumpConsumerWidget(ActivityTextField(onSubmit: (_) {}), overrides: overrides); - - expect(find.byType(TextField), findsOneWidget); - }); - - testWidgets('No UserCircleAvatar when user == null', (tester) async { - final userProvider = MockCurrentUserProvider(); - - await tester.pumpConsumerWidget( - ActivityTextField(onSubmit: (_) {}), - overrides: [currentUserProvider.overrideWith((ref) => userProvider), ...overrides], - ); - - expect(find.byType(UserCircleAvatar), findsNothing); - }); - - testWidgets('UserCircleAvatar displayed when user != null', (tester) async { - await tester.pumpConsumerWidget(ActivityTextField(onSubmit: (_) {}), overrides: overrides); - - expect(find.byType(UserCircleAvatar), findsOneWidget); - }); - - testWidgets('Filled icon if likedId != null', (tester) async { - await tester.pumpConsumerWidget( - ActivityTextField(onSubmit: (_) {}, likeId: '1'), - overrides: overrides, - ); - - expect(find.widgetWithIcon(IconButton, Icons.thumb_up), findsOneWidget); - expect(find.widgetWithIcon(IconButton, Icons.thumb_up_off_alt), findsNothing); - }); - - testWidgets('Bordered icon if likedId == null', (tester) async { - await tester.pumpConsumerWidget(ActivityTextField(onSubmit: (_) {}), overrides: overrides); - - expect(find.widgetWithIcon(IconButton, Icons.thumb_up_off_alt), findsOneWidget); - expect(find.widgetWithIcon(IconButton, Icons.thumb_up), findsNothing); - }); - - testWidgets('Adds new like', (tester) async { - await tester.pumpConsumerWidget(ActivityTextField(onSubmit: (_) {}), overrides: overrides); - - when(() => activityMock.addLike()).thenAnswer((_) => Future.value()); - - final suffixIcon = find.byType(IconButton); - await tester.tap(suffixIcon); - - verify(() => activityMock.addLike()); - }); - - testWidgets('Removes like if already liked', (tester) async { - await tester.pumpConsumerWidget( - ActivityTextField(onSubmit: (_) {}, likeId: 'test-suffix'), - overrides: overrides, - ); - - when(() => activityMock.removeActivity(any())).thenAnswer((_) => Future.value()); - - final suffixIcon = find.byType(IconButton); - await tester.tap(suffixIcon); - - verify(() => activityMock.removeActivity('test-suffix')); - }); - - testWidgets('Passes text entered to onSubmit on submit', (tester) async { - String? receivedText; - - await tester.pumpConsumerWidget( - ActivityTextField(onSubmit: (text) => receivedText = text, likeId: 'test-suffix'), - overrides: overrides, - ); - - final textField = find.byType(TextField); - await tester.enterText(textField, 'This is a test comment'); - await tester.testTextInput.receiveAction(TextInputAction.done); - expect(receivedText, 'This is a test comment'); - }); - - testWidgets('Input disabled when isEnabled false', (tester) async { - String? receviedText; - - await tester.pumpConsumerWidget( - ActivityTextField(onSubmit: (text) => receviedText = text, isEnabled: false, likeId: 'test-suffix'), - overrides: overrides, - ); - - final suffixIcon = find.byType(IconButton); - await tester.tap(suffixIcon, warnIfMissed: false); - - final textField = find.byType(TextField); - await tester.enterText(textField, 'This is a test comment'); - await tester.testTextInput.receiveAction(TextInputAction.done); - - expect(receviedText, isNull); - verifyNever(() => activityMock.addLike()); - verifyNever(() => activityMock.removeActivity(any())); - }); -} diff --git a/mobile/test/modules/activity/activity_tile_test.dart b/mobile/test/modules/activity/activity_tile_test.dart deleted file mode 100644 index 538e3c0911..0000000000 --- a/mobile/test/modules/activity/activity_tile_test.dart +++ /dev/null @@ -1,165 +0,0 @@ -@Skip('currently failing due to mock HTTP client to download ISAR binaries') -@Tags(['widget']) -library; - -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/store.model.dart'; -import 'package:immich_mobile/domain/services/store.service.dart'; -import 'package:immich_mobile/entities/store.entity.dart'; -import 'package:immich_mobile/infrastructure/repositories/store.repository.dart'; -import 'package:immich_mobile/models/activities/activity.model.dart'; -import 'package:immich_mobile/providers/asset_viewer/current_asset.provider.dart'; -import 'package:immich_mobile/widgets/activities/activity_tile.dart'; -import 'package:immich_mobile/widgets/common/user_circle_avatar.dart'; -import 'package:isar/isar.dart'; - -import '../../fixtures/asset.stub.dart'; -import '../../fixtures/user.stub.dart'; -import '../../test_utils.dart'; -import '../../widget_tester_extensions.dart'; -import '../asset_viewer/asset_viewer_mocks.dart'; - -void main() { - late MockCurrentAssetProvider assetProvider; - late List overrides; - late Isar db; - - setUpAll(() async { - TestUtils.init(); - db = await TestUtils.initIsar(); - // For UserCircleAvatar - await StoreService.init(storeRepository: IsarStoreRepository(db)); - await Store.put(StoreKey.currentUser, UserStub.admin); - await Store.put(StoreKey.serverEndpoint, ''); - await Store.put(StoreKey.accessToken, ''); - }); - - setUp(() { - assetProvider = MockCurrentAssetProvider(); - overrides = [currentAssetProvider.overrideWith(() => assetProvider)]; - }); - - testWidgets('Returns a ListTile', (tester) async { - await tester.pumpConsumerWidget( - ActivityTile(Activity(id: '1', createdAt: DateTime(100), type: ActivityType.like, user: UserStub.admin)), - overrides: overrides, - ); - - expect(find.byType(ListTile), findsOneWidget); - }); - - testWidgets('No trailing widget when activity assetId == null', (tester) async { - await tester.pumpConsumerWidget( - ActivityTile(Activity(id: '1', createdAt: DateTime(100), type: ActivityType.like, user: UserStub.admin)), - overrides: overrides, - ); - - final listTile = tester.widget(find.byType(ListTile)); - expect(listTile.trailing, isNull); - }); - - testWidgets('Asset Thumbanil as trailing widget when activity assetId != null', (tester) async { - await tester.pumpConsumerWidget( - ActivityTile( - Activity(id: '1', createdAt: DateTime(100), type: ActivityType.like, user: UserStub.admin, assetId: '1'), - ), - overrides: overrides, - ); - - final listTile = tester.widget(find.byType(ListTile)); - expect(listTile.trailing, isNotNull); - // TODO: Validate this to be the common class after migrating ActivityTile#_ActivityAssetThumbnail to a common class - }); - - testWidgets('No trailing widget when current asset != null', (tester) async { - await tester.pumpConsumerWidget( - ActivityTile( - Activity(id: '1', createdAt: DateTime(100), type: ActivityType.like, user: UserStub.admin, assetId: '1'), - ), - overrides: overrides, - ); - - assetProvider.state = AssetStub.image1; - await tester.pumpAndSettle(); - - final listTile = tester.widget(find.byType(ListTile)); - expect(listTile.trailing, isNull); - }); - - group('Like Activity', () { - final activity = Activity(id: '1', createdAt: DateTime(100), type: ActivityType.like, user: UserStub.admin); - - testWidgets('Like contains filled thumbs-up as leading', (tester) async { - await tester.pumpConsumerWidget(ActivityTile(activity), overrides: overrides); - - // Leading widget should not be null - final listTile = tester.widget(find.byType(ListTile)); - expect(listTile.leading, isNotNull); - - // And should have a thumb_up icon - final thumbUpIconFinder = find.widgetWithIcon(listTile.leading!.runtimeType, Icons.thumb_up); - - expect(thumbUpIconFinder, findsOneWidget); - }); - - testWidgets('Like title is center aligned', (tester) async { - await tester.pumpConsumerWidget(ActivityTile(activity), overrides: overrides); - - final listTile = tester.widget(find.byType(ListTile)); - - expect(listTile.titleAlignment, ListTileTitleAlignment.center); - }); - - testWidgets('No subtitle for likes', (tester) async { - await tester.pumpConsumerWidget(ActivityTile(activity), overrides: overrides); - - final listTile = tester.widget(find.byType(ListTile)); - - expect(listTile.subtitle, isNull); - }); - }); - - group('Comment Activity', () { - final activity = Activity( - id: '1', - createdAt: DateTime(100), - type: ActivityType.comment, - comment: 'This is a test comment', - user: UserStub.admin, - ); - - testWidgets('Comment contains User Circle Avatar as leading', (tester) async { - await tester.pumpConsumerWidget(ActivityTile(activity), overrides: overrides); - - final userAvatarFinder = find.byType(UserCircleAvatar); - expect(userAvatarFinder, findsOneWidget); - - // Leading widget should not be null - final listTile = tester.widget(find.byType(ListTile)); - expect(listTile.leading, isNotNull); - - // Make sure that the leading widget is the UserCircleAvatar - final userAvatar = tester.widget(userAvatarFinder); - expect(listTile.leading, userAvatar); - }); - - testWidgets('Comment title is top aligned', (tester) async { - await tester.pumpConsumerWidget(ActivityTile(activity), overrides: overrides); - - final listTile = tester.widget(find.byType(ListTile)); - - expect(listTile.titleAlignment, ListTileTitleAlignment.top); - }); - - testWidgets('Contains comment text as subtitle', (tester) async { - await tester.pumpConsumerWidget(ActivityTile(activity), overrides: overrides); - - final listTile = tester.widget(find.byType(ListTile)); - - expect(listTile.subtitle, isNotNull); - expect(find.descendant(of: find.byType(ListTile), matching: find.text(activity.comment!)), findsOneWidget); - }); - }); -} diff --git a/mobile/test/modules/activity/dismissible_activity_test.dart b/mobile/test/modules/activity/dismissible_activity_test.dart deleted file mode 100644 index 32516e73ea..0000000000 --- a/mobile/test/modules/activity/dismissible_activity_test.dart +++ /dev/null @@ -1,99 +0,0 @@ -@Tags(['widget']) -library; - -import 'package:flutter/material.dart'; -import 'package:flutter_test/flutter_test.dart'; -import 'package:immich_mobile/models/activities/activity.model.dart'; -import 'package:immich_mobile/widgets/activities/activity_tile.dart'; -import 'package:immich_mobile/widgets/activities/dismissible_activity.dart'; -import 'package:immich_mobile/providers/asset_viewer/current_asset.provider.dart'; -import 'package:immich_mobile/widgets/common/confirm_dialog.dart'; -import 'package:riverpod_annotation/riverpod_annotation.dart'; - -import '../../fixtures/user.stub.dart'; -import '../../test_utils.dart'; -import '../../widget_tester_extensions.dart'; -import '../asset_viewer/asset_viewer_mocks.dart'; - -final activity = Activity(id: '1', createdAt: DateTime(100), type: ActivityType.like, user: UserStub.admin); - -void main() { - late MockCurrentAssetProvider assetProvider; - late List overrides; - - setUpAll(() => TestUtils.init()); - - setUp(() { - assetProvider = MockCurrentAssetProvider(); - overrides = [currentAssetProvider.overrideWith(() => assetProvider)]; - }); - - testWidgets('Returns a Dismissible', (tester) async { - await tester.pumpConsumerWidget( - DismissibleActivity('1', ActivityTile(activity), onDismiss: (_) {}), - overrides: overrides, - ); - - expect(find.byType(Dismissible), findsOneWidget); - }); - - testWidgets('Dialog displayed when onDismiss is set', (tester) async { - await tester.pumpConsumerWidget( - DismissibleActivity('1', ActivityTile(activity), onDismiss: (_) {}), - overrides: overrides, - ); - - final dismissible = find.byType(Dismissible); - await tester.drag(dismissible, const Offset(500, 0)); - await tester.pumpAndSettle(); - - expect(find.byType(ConfirmDialog), findsOneWidget); - }); - - testWidgets('Ok action in ConfirmDialog should call onDismiss with activityId', (tester) async { - String? receivedActivityId; - await tester.pumpConsumerWidget( - DismissibleActivity('1', ActivityTile(activity), onDismiss: (id) => receivedActivityId = id), - overrides: overrides, - ); - - final dismissible = find.byType(Dismissible); - await tester.drag(dismissible, const Offset(-500, 0)); - await tester.pumpAndSettle(); - - final okButton = find.text('delete'); - await tester.tap(okButton); - await tester.pumpAndSettle(); - - expect(receivedActivityId, '1'); - }); - - testWidgets('Delete icon for background if onDismiss is set', (tester) async { - await tester.pumpConsumerWidget( - DismissibleActivity('1', ActivityTile(activity), onDismiss: (_) {}), - overrides: overrides, - ); - - final dismissible = find.byType(Dismissible); - await tester.drag(dismissible, const Offset(500, 0)); - await tester.pumpAndSettle(); - - expect(find.byIcon(Icons.delete_sweep_rounded), findsOneWidget); - }); - - testWidgets('No delete dialog if onDismiss is not set', (tester) async { - await tester.pumpConsumerWidget(DismissibleActivity('1', ActivityTile(activity)), overrides: overrides); - - // When onDismiss is not set, the widget should not be wrapped by a Dismissible - expect(find.byType(Dismissible), findsNothing); - expect(find.byType(ConfirmDialog), findsNothing); - }); - - testWidgets('No icon for background if onDismiss is not set', (tester) async { - await tester.pumpConsumerWidget(DismissibleActivity('1', ActivityTile(activity)), overrides: overrides); - - // No Dismissible should exist when onDismiss is not provided, so no delete icon either - expect(find.byType(Dismissible), findsNothing); - expect(find.byIcon(Icons.delete_sweep_rounded), findsNothing); - }); -} diff --git a/mobile/test/modules/album/album_mocks.dart b/mobile/test/modules/album/album_mocks.dart deleted file mode 100644 index 7a1b76e0c7..0000000000 --- a/mobile/test/modules/album/album_mocks.dart +++ /dev/null @@ -1,13 +0,0 @@ -import 'package:immich_mobile/providers/album/current_album.provider.dart'; -import 'package:immich_mobile/entities/album.entity.dart'; -import 'package:mocktail/mocktail.dart'; - -class MockCurrentAlbumProvider extends CurrentAlbum with Mock implements CurrentAlbumInternal { - Album? initAlbum; - MockCurrentAlbumProvider([this.initAlbum]); - - @override - Album? build() { - return initAlbum; - } -} diff --git a/mobile/test/modules/album/album_sort_by_options_provider_test.dart b/mobile/test/modules/album/album_sort_by_options_provider_test.dart deleted file mode 100644 index a35255bc21..0000000000 --- a/mobile/test/modules/album/album_sort_by_options_provider_test.dart +++ /dev/null @@ -1,270 +0,0 @@ -import 'package:flutter_test/flutter_test.dart'; -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/entities/album.entity.dart'; -import 'package:immich_mobile/entities/asset.entity.dart'; -import 'package:immich_mobile/providers/album/album_sort_by_options.provider.dart'; -import 'package:immich_mobile/providers/app_settings.provider.dart'; -import 'package:immich_mobile/services/app_settings.service.dart'; -import 'package:isar/isar.dart'; -import 'package:mocktail/mocktail.dart'; - -import '../../fixtures/album.stub.dart'; -import '../../fixtures/asset.stub.dart'; -import '../../test_utils.dart'; -import '../settings/settings_mocks.dart'; - -void main() { - /// Verify the sort modes - group("AlbumSortMode", () { - late final Isar db; - - setUpAll(() async { - db = await TestUtils.initIsar(); - }); - - final albums = [AlbumStub.emptyAlbum, AlbumStub.sharedWithUser, AlbumStub.oneAsset, AlbumStub.twoAsset]; - - setUp(() { - db.writeTxnSync(() { - db.clearSync(); - // Save all assets - db.assets.putAllSync([AssetStub.image1, AssetStub.image2]); - db.albums.putAllSync(albums); - for (final album in albums) { - album.sharedUsers.saveSync(); - album.assets.saveSync(); - } - }); - expect(db.albums.countSync(), 4); - expect(db.assets.countSync(), 2); - }); - - group("Album sort - Created Time", () { - const created = AlbumSortMode.created; - test("Created time - ASC", () { - final sorted = created.sortFn(albums, false); - final sortedList = [AlbumStub.emptyAlbum, AlbumStub.twoAsset, AlbumStub.oneAsset, AlbumStub.sharedWithUser]; - expect(sorted, orderedEquals(sortedList)); - }); - - test("Created time - DESC", () { - final sorted = created.sortFn(albums, true); - final sortedList = [AlbumStub.sharedWithUser, AlbumStub.oneAsset, AlbumStub.twoAsset, AlbumStub.emptyAlbum]; - expect(sorted, orderedEquals(sortedList)); - }); - }); - - group("Album sort - Asset count", () { - const assetCount = AlbumSortMode.assetCount; - test("Asset Count - ASC", () { - final sorted = assetCount.sortFn(albums, false); - final sortedList = [AlbumStub.emptyAlbum, AlbumStub.sharedWithUser, AlbumStub.oneAsset, AlbumStub.twoAsset]; - expect(sorted, orderedEquals(sortedList)); - }); - - test("Asset Count - DESC", () { - final sorted = assetCount.sortFn(albums, true); - final sortedList = [AlbumStub.twoAsset, AlbumStub.oneAsset, AlbumStub.sharedWithUser, AlbumStub.emptyAlbum]; - expect(sorted, orderedEquals(sortedList)); - }); - }); - - group("Album sort - Last modified", () { - const lastModified = AlbumSortMode.lastModified; - test("Last modified - ASC", () { - final sorted = lastModified.sortFn(albums, false); - final sortedList = [AlbumStub.twoAsset, AlbumStub.emptyAlbum, AlbumStub.sharedWithUser, AlbumStub.oneAsset]; - expect(sorted, orderedEquals(sortedList)); - }); - - test("Last modified - DESC", () { - final sorted = lastModified.sortFn(albums, true); - final sortedList = [AlbumStub.oneAsset, AlbumStub.sharedWithUser, AlbumStub.emptyAlbum, AlbumStub.twoAsset]; - expect(sorted, orderedEquals(sortedList)); - }); - }); - - group("Album sort - Created", () { - const created = AlbumSortMode.created; - test("Created - ASC", () { - final sorted = created.sortFn(albums, false); - final sortedList = [AlbumStub.emptyAlbum, AlbumStub.twoAsset, AlbumStub.oneAsset, AlbumStub.sharedWithUser]; - expect(sorted, orderedEquals(sortedList)); - }); - - test("Created - DESC", () { - final sorted = created.sortFn(albums, true); - final sortedList = [AlbumStub.sharedWithUser, AlbumStub.oneAsset, AlbumStub.twoAsset, AlbumStub.emptyAlbum]; - expect(sorted, orderedEquals(sortedList)); - }); - }); - - group("Album sort - Most Recent", () { - const mostRecent = AlbumSortMode.mostRecent; - - test("Most Recent - DESC", () { - final sorted = mostRecent.sortFn([ - AlbumStub.create2020end2020Album, - AlbumStub.create2020end2022Album, - AlbumStub.create2020end2024Album, - AlbumStub.create2020end2026Album, - ], false); - final sortedList = [ - AlbumStub.create2020end2026Album, - AlbumStub.create2020end2024Album, - AlbumStub.create2020end2022Album, - AlbumStub.create2020end2020Album, - ]; - expect(sorted, orderedEquals(sortedList)); - }); - - test("Most Recent - ASC", () { - final sorted = mostRecent.sortFn([ - AlbumStub.create2020end2020Album, - AlbumStub.create2020end2022Album, - AlbumStub.create2020end2024Album, - AlbumStub.create2020end2026Album, - ], true); - final sortedList = [ - AlbumStub.create2020end2020Album, - AlbumStub.create2020end2022Album, - AlbumStub.create2020end2024Album, - AlbumStub.create2020end2026Album, - ]; - expect(sorted, orderedEquals(sortedList)); - }); - }); - - group("Album sort - Most Oldest", () { - const mostOldest = AlbumSortMode.mostOldest; - - test("Most Oldest - ASC", () { - final sorted = mostOldest.sortFn(albums, false); - final sortedList = [AlbumStub.twoAsset, AlbumStub.emptyAlbum, AlbumStub.oneAsset, AlbumStub.sharedWithUser]; - expect(sorted, orderedEquals(sortedList)); - }); - - test("Most Oldest - DESC", () { - final sorted = mostOldest.sortFn(albums, true); - final sortedList = [AlbumStub.sharedWithUser, AlbumStub.oneAsset, AlbumStub.emptyAlbum, AlbumStub.twoAsset]; - expect(sorted, orderedEquals(sortedList)); - }); - }); - }); - - /// Verify the sort mode provider - group('AlbumSortByOptions', () { - late AppSettingsService settingsMock; - late ProviderContainer container; - - setUp(() async { - settingsMock = MockAppSettingsService(); - container = TestUtils.createContainer( - overrides: [appSettingsServiceProvider.overrideWith((ref) => settingsMock)], - ); - when( - () => settingsMock.setSetting(AppSettingsEnum.selectedAlbumSortReverse, any()), - ).thenAnswer((_) async => {}); - when( - () => settingsMock.setSetting(AppSettingsEnum.selectedAlbumSortOrder, any()), - ).thenAnswer((_) async => {}); - }); - - test('Returns the default sort mode when none set', () { - // Returns the default value when nothing is set - when(() => settingsMock.getSetting(AppSettingsEnum.selectedAlbumSortOrder)).thenReturn(0); - - expect(container.read(albumSortByOptionsProvider), AlbumSortMode.created); - }); - - test('Returns the correct sort mode with index from Store', () { - // Returns the default value when nothing is set - when(() => settingsMock.getSetting(AppSettingsEnum.selectedAlbumSortOrder)).thenReturn(3); - - expect(container.read(albumSortByOptionsProvider), AlbumSortMode.lastModified); - }); - - test('Properly saves the correct store index of sort mode', () { - container.read(albumSortByOptionsProvider.notifier).changeSortMode(AlbumSortMode.mostOldest); - - verify( - () => settingsMock.setSetting(AppSettingsEnum.selectedAlbumSortOrder, AlbumSortMode.mostOldest.storeIndex), - ); - }); - - test('Notifies listeners on state change', () { - when(() => settingsMock.getSetting(AppSettingsEnum.selectedAlbumSortOrder)).thenReturn(0); - - final listener = ListenerMock(); - container.listen(albumSortByOptionsProvider, listener.call, fireImmediately: true); - - // Created -> Most Oldest - container.read(albumSortByOptionsProvider.notifier).changeSortMode(AlbumSortMode.mostOldest); - - // Most Oldest -> Title - container.read(albumSortByOptionsProvider.notifier).changeSortMode(AlbumSortMode.title); - - verifyInOrder([ - () => listener.call(null, AlbumSortMode.created), - () => listener.call(AlbumSortMode.created, AlbumSortMode.mostOldest), - () => listener.call(AlbumSortMode.mostOldest, AlbumSortMode.title), - ]); - - verifyNoMoreInteractions(listener); - }); - }); - - /// Verify the sort order provider - group('AlbumSortOrder', () { - late AppSettingsService settingsMock; - late ProviderContainer container; - - registerFallbackValue(AppSettingsEnum.selectedAlbumSortReverse); - - setUp(() async { - settingsMock = MockAppSettingsService(); - container = TestUtils.createContainer( - overrides: [appSettingsServiceProvider.overrideWith((ref) => settingsMock)], - ); - when( - () => settingsMock.setSetting(AppSettingsEnum.selectedAlbumSortReverse, any()), - ).thenAnswer((_) async => {}); - when( - () => settingsMock.setSetting(AppSettingsEnum.selectedAlbumSortOrder, any()), - ).thenAnswer((_) async => {}); - }); - - test('Returns the default sort order when none set - false', () { - when(() => settingsMock.getSetting(AppSettingsEnum.selectedAlbumSortReverse)).thenReturn(false); - - expect(container.read(albumSortOrderProvider), isFalse); - }); - - test('Properly saves the correct order', () { - container.read(albumSortOrderProvider.notifier).changeSortDirection(true); - - verify(() => settingsMock.setSetting(AppSettingsEnum.selectedAlbumSortReverse, true)); - }); - - test('Notifies listeners on state change', () { - when(() => settingsMock.getSetting(AppSettingsEnum.selectedAlbumSortReverse)).thenReturn(false); - - final listener = ListenerMock(); - container.listen(albumSortOrderProvider, listener.call, fireImmediately: true); - - // false -> true - container.read(albumSortOrderProvider.notifier).changeSortDirection(true); - - // true -> false - container.read(albumSortOrderProvider.notifier).changeSortDirection(false); - - verifyInOrder([ - () => listener.call(null, false), - () => listener.call(false, true), - () => listener.call(true, false), - ]); - - verifyNoMoreInteractions(listener); - }); - }); -} diff --git a/mobile/test/modules/asset_viewer/asset_viewer_mocks.dart b/mobile/test/modules/asset_viewer/asset_viewer_mocks.dart deleted file mode 100644 index 89b06d3c09..0000000000 --- a/mobile/test/modules/asset_viewer/asset_viewer_mocks.dart +++ /dev/null @@ -1,13 +0,0 @@ -import 'package:immich_mobile/providers/asset_viewer/current_asset.provider.dart'; -import 'package:immich_mobile/entities/asset.entity.dart'; -import 'package:mocktail/mocktail.dart'; - -class MockCurrentAssetProvider extends CurrentAssetInternal with Mock implements CurrentAsset { - Asset? initAsset; - MockCurrentAssetProvider([this.initAsset]); - - @override - Asset? build() { - return initAsset; - } -} diff --git a/mobile/test/modules/extensions/asset_extensions_test.dart b/mobile/test/modules/extensions/asset_extensions_test.dart deleted file mode 100644 index 2b9b740ca7..0000000000 --- a/mobile/test/modules/extensions/asset_extensions_test.dart +++ /dev/null @@ -1,113 +0,0 @@ -import 'package:flutter_test/flutter_test.dart'; -import 'package:immich_mobile/domain/models/exif.model.dart'; -import 'package:immich_mobile/entities/asset.entity.dart'; -import 'package:immich_mobile/extensions/asset_extensions.dart'; -import 'package:timezone/data/latest.dart'; -import 'package:timezone/timezone.dart'; - -ExifInfo makeExif({DateTime? dateTimeOriginal, String? timeZone}) { - return ExifInfo(dateTimeOriginal: dateTimeOriginal, timeZone: timeZone); -} - -Asset makeAsset({required String id, required DateTime createdAt, ExifInfo? exifInfo}) { - return Asset( - checksum: '', - localId: id, - remoteId: id, - ownerId: 1, - fileCreatedAt: createdAt, - fileModifiedAt: DateTime.now(), - updatedAt: DateTime.now(), - durationInSeconds: 0, - type: AssetType.image, - fileName: id, - isFavorite: false, - isArchived: false, - isTrashed: false, - exifInfo: exifInfo, - ); -} - -void main() { - // Init Timezone DB - initializeTimeZones(); - - group("Returns local time and offset if no exifInfo", () { - test('returns createdAt directly if in local', () { - final createdAt = DateTime(2023, 12, 12, 12, 12, 12); - final a = makeAsset(id: '1', createdAt: createdAt); - final (dt, tz) = a.getTZAdjustedTimeAndOffset(); - - expect(createdAt, dt); - expect(createdAt.timeZoneOffset, tz); - }); - - test('returns createdAt in local if in utc', () { - final createdAt = DateTime.utc(2023, 12, 12, 12, 12, 12); - final a = makeAsset(id: '1', createdAt: createdAt); - final (dt, tz) = a.getTZAdjustedTimeAndOffset(); - - final localCreatedAt = createdAt.toLocal(); - expect(localCreatedAt, dt); - expect(localCreatedAt.timeZoneOffset, tz); - }); - }); - - group("Returns dateTimeOriginal", () { - test('Returns dateTimeOriginal in UTC from exifInfo without timezone', () { - final createdAt = DateTime.parse("2023-01-27T14:00:00-0500"); - final dateTimeOriginal = DateTime.parse("2022-01-27T14:00:00+0530"); - final e = makeExif(dateTimeOriginal: dateTimeOriginal); - final a = makeAsset(id: '1', createdAt: createdAt, exifInfo: e); - final (dt, tz) = a.getTZAdjustedTimeAndOffset(); - - final dateTimeInUTC = dateTimeOriginal.toUtc(); - expect(dateTimeInUTC, dt); - expect(dateTimeInUTC.timeZoneOffset, tz); - }); - - test('Returns dateTimeOriginal in UTC from exifInfo with invalid timezone', () { - final createdAt = DateTime.parse("2023-01-27T14:00:00-0500"); - final dateTimeOriginal = DateTime.parse("2022-01-27T14:00:00+0530"); - final e = makeExif(dateTimeOriginal: dateTimeOriginal, timeZone: "#_#"); // Invalid timezone - final a = makeAsset(id: '1', createdAt: createdAt, exifInfo: e); - final (dt, tz) = a.getTZAdjustedTimeAndOffset(); - - final dateTimeInUTC = dateTimeOriginal.toUtc(); - expect(dateTimeInUTC, dt); - expect(dateTimeInUTC.timeZoneOffset, tz); - }); - }); - - group("Returns adjusted time if timezone available", () { - test('With timezone as location', () { - final createdAt = DateTime.parse("2023-01-27T14:00:00-0500"); - final dateTimeOriginal = DateTime.parse("2022-01-27T14:00:00+0530"); - const location = "Asia/Hong_Kong"; - final e = makeExif(dateTimeOriginal: dateTimeOriginal, timeZone: location); - final a = makeAsset(id: '1', createdAt: createdAt, exifInfo: e); - final (dt, tz) = a.getTZAdjustedTimeAndOffset(); - - final adjustedTime = TZDateTime.from(dateTimeOriginal.toUtc(), getLocation(location)); - expect(adjustedTime, dt); - expect(adjustedTime.timeZoneOffset, tz); - }); - - test('With timezone as offset', () { - final createdAt = DateTime.parse("2023-01-27T14:00:00-0500"); - final dateTimeOriginal = DateTime.parse("2022-01-27T14:00:00+0530"); - const offset = "utc+08:00"; - final e = makeExif(dateTimeOriginal: dateTimeOriginal, timeZone: offset); - final a = makeAsset(id: '1', createdAt: createdAt, exifInfo: e); - final (dt, tz) = a.getTZAdjustedTimeAndOffset(); - - final location = getLocation("Asia/Hong_Kong"); - final offsetFromLocation = Duration(milliseconds: location.currentTimeZone.offset); - final adjustedTime = dateTimeOriginal.toUtc().add(offsetFromLocation); - - // Adds the offset to the actual time and returns the offset separately - expect(adjustedTime, dt); - expect(offsetFromLocation, tz); - }); - }); -} diff --git a/mobile/test/modules/home/asset_grid_data_structure_test.dart b/mobile/test/modules/home/asset_grid_data_structure_test.dart deleted file mode 100644 index 3e1fe06c68..0000000000 --- a/mobile/test/modules/home/asset_grid_data_structure_test.dart +++ /dev/null @@ -1,113 +0,0 @@ -import 'package:flutter_test/flutter_test.dart'; -import 'package:immich_mobile/widgets/asset_grid/asset_grid_data_structure.dart'; -import 'package:immich_mobile/entities/asset.entity.dart'; - -void main() { - final List testAssets = []; - - for (int i = 0; i < 150; i++) { - int month = i ~/ 31; - int day = (i % 31).toInt(); - - DateTime date = DateTime(2022, month, day); - - testAssets.add( - Asset( - checksum: "", - localId: '$i', - ownerId: 1, - fileCreatedAt: date, - fileModifiedAt: date, - updatedAt: date, - durationInSeconds: 0, - type: AssetType.image, - fileName: '', - isFavorite: false, - isArchived: false, - isTrashed: false, - ), - ); - } - - final List assets = []; - - assets.addAll( - testAssets.sublist(0, 5).map((e) { - e.fileCreatedAt = DateTime(2022, 1, 5); - return e; - }).toList(), - ); - assets.addAll( - testAssets.sublist(5, 10).map((e) { - e.fileCreatedAt = DateTime(2022, 1, 10); - return e; - }).toList(), - ); - assets.addAll( - testAssets.sublist(10, 15).map((e) { - e.fileCreatedAt = DateTime(2022, 2, 17); - return e; - }).toList(), - ); - assets.addAll( - testAssets.sublist(15, 30).map((e) { - e.fileCreatedAt = DateTime(2022, 10, 15); - return e; - }).toList(), - ); - - group('Test grouped', () { - test('test grouped check months', () async { - final renderList = await RenderList.fromAssets(assets, GroupAssetsBy.day); - - // Oct - // Day 1 - // 15 Assets => 5 Rows - // Feb - // Day 1 - // 5 Assets => 2 Rows - // Jan - // Day 2 - // 5 Assets => 2 Rows - // Day 1 - // 5 Assets => 2 Rows - expect(renderList.elements, hasLength(4)); - expect(renderList.elements[0].type, RenderAssetGridElementType.monthTitle); - expect(renderList.elements[0].date.month, 1); - expect(renderList.elements[1].type, RenderAssetGridElementType.groupDividerTitle); - expect(renderList.elements[1].date.month, 1); - expect(renderList.elements[2].type, RenderAssetGridElementType.monthTitle); - expect(renderList.elements[2].date.month, 2); - expect(renderList.elements[3].type, RenderAssetGridElementType.monthTitle); - expect(renderList.elements[3].date.month, 10); - }); - - test('test grouped check types', () async { - final renderList = await RenderList.fromAssets(assets, GroupAssetsBy.day); - - // Oct - // Day 1 - // 15 Assets => 3 Rows - // Feb - // Day 1 - // 5 Assets => 1 Row - // Jan - // Day 2 - // 5 Assets => 1 Row - // Day 1 - // 5 Assets => 1 Row - final types = [ - RenderAssetGridElementType.monthTitle, - RenderAssetGridElementType.groupDividerTitle, - RenderAssetGridElementType.monthTitle, - RenderAssetGridElementType.monthTitle, - ]; - - expect(renderList.elements, hasLength(types.length)); - - for (int i = 0; i < renderList.elements.length; i++) { - expect(renderList.elements[i].type, types[i]); - } - }); - }); -} diff --git a/mobile/test/modules/map/map_theme_override_test.dart b/mobile/test/modules/map/map_theme_override_test.dart index de16b7f24f..56efde98dd 100644 --- a/mobile/test/modules/map/map_theme_override_test.dart +++ b/mobile/test/modules/map/map_theme_override_test.dart @@ -2,16 +2,18 @@ @Tags(['widget']) library; +import 'package:drift/drift.dart'; +import 'package:drift/native.dart'; import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/domain/services/store.service.dart'; +import 'package:immich_mobile/infrastructure/repositories/db.repository.dart'; import 'package:immich_mobile/infrastructure/repositories/store.repository.dart'; import 'package:immich_mobile/models/map/map_state.model.dart'; import 'package:immich_mobile/providers/locale_provider.dart'; import 'package:immich_mobile/providers/map/map_state.provider.dart'; import 'package:immich_mobile/widgets/map/map_theme_override.dart'; -import 'package:isar/isar.dart'; import '../../test_utils.dart'; import '../../widget_tester_extensions.dart'; @@ -21,17 +23,17 @@ void main() { late MockMapStateNotifier mapStateNotifier; late List overrides; late MapState mapState; - late Isar db; + late Drift db; setUpAll(() async { - db = await TestUtils.initIsar(); + db = Drift(DatabaseConnection(NativeDatabase.memory(), closeStreamsSynchronously: true)); TestUtils.init(); }); setUp(() async { mapState = const MapState(themeMode: ThemeMode.dark); mapStateNotifier = MockMapStateNotifier(mapState); - await StoreService.init(storeRepository: IsarStoreRepository(db)); + await StoreService.init(storeRepository: DriftStoreRepository(db)); overrides = [ mapStateNotifierProvider.overrideWith(() => mapStateNotifier), localeProvider.overrideWithValue(const Locale("en")), diff --git a/mobile/test/modules/settings/settings_mocks.dart b/mobile/test/modules/settings/settings_mocks.dart deleted file mode 100644 index 63fd9312b7..0000000000 --- a/mobile/test/modules/settings/settings_mocks.dart +++ /dev/null @@ -1,4 +0,0 @@ -import 'package:immich_mobile/services/app_settings.service.dart'; -import 'package:mocktail/mocktail.dart'; - -class MockAppSettingsService extends Mock implements AppSettingsService {} diff --git a/mobile/test/modules/shared/shared_mocks.dart b/mobile/test/modules/shared/shared_mocks.dart deleted file mode 100644 index 790bbbd815..0000000000 --- a/mobile/test/modules/shared/shared_mocks.dart +++ /dev/null @@ -1,11 +0,0 @@ -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/domain/models/user.model.dart'; -import 'package:immich_mobile/providers/user.provider.dart'; -import 'package:mocktail/mocktail.dart'; - -class MockCurrentUserProvider extends StateNotifier with Mock implements CurrentUserProvider { - MockCurrentUserProvider() : super(null); - - @override - set state(UserDto? user) => super.state = user; -} diff --git a/mobile/test/modules/shared/sync_service_test.dart b/mobile/test/modules/shared/sync_service_test.dart deleted file mode 100644 index 767a52b8d8..0000000000 --- a/mobile/test/modules/shared/sync_service_test.dart +++ /dev/null @@ -1,285 +0,0 @@ -import 'package:drift/drift.dart'; -import 'package:drift/native.dart'; -import 'package:flutter/widgets.dart'; -import 'package:flutter_test/flutter_test.dart'; -import 'package:immich_mobile/constants/enums.dart'; -import 'package:immich_mobile/domain/models/store.model.dart'; -import 'package:immich_mobile/domain/models/user.model.dart'; -import 'package:immich_mobile/domain/services/log.service.dart'; -import 'package:immich_mobile/domain/services/store.service.dart'; -import 'package:immich_mobile/entities/asset.entity.dart'; -import 'package:immich_mobile/entities/etag.entity.dart'; -import 'package:immich_mobile/entities/store.entity.dart'; -import 'package:immich_mobile/infrastructure/repositories/log.repository.dart'; -import 'package:immich_mobile/infrastructure/repositories/logger_db.repository.dart'; -import 'package:immich_mobile/infrastructure/repositories/store.repository.dart'; -import 'package:immich_mobile/repositories/asset.repository.dart'; -import 'package:immich_mobile/repositories/partner_api.repository.dart'; -import 'package:immich_mobile/services/sync.service.dart'; -import 'package:mocktail/mocktail.dart'; - -import '../../domain/service.mock.dart'; -import '../../fixtures/asset.stub.dart'; -import '../../infrastructure/repository.mock.dart'; -import '../../repository.mocks.dart'; -import '../../service.mocks.dart'; -import '../../test_utils.dart'; - -void main() { - int assetIdCounter = 0; - Asset makeAsset({ - required String checksum, - String? localId, - String? remoteId, - int ownerId = 590700560494856554, // hash of "1" - }) { - final DateTime date = DateTime(2000); - return Asset( - id: assetIdCounter++, - checksum: checksum, - localId: localId, - remoteId: remoteId, - ownerId: ownerId, - fileCreatedAt: date, - fileModifiedAt: date, - updatedAt: date, - durationInSeconds: 0, - type: AssetType.image, - fileName: localId ?? remoteId ?? "", - isFavorite: false, - isArchived: false, - isTrashed: false, - ); - } - - final owner = UserDto( - id: "1", - updatedAt: DateTime.now(), - email: "a@b.c", - name: "first last", - isAdmin: false, - profileChangedAt: DateTime.now(), - ); - - setUpAll(() async { - final loggerDb = DriftLogger(DatabaseConnection(NativeDatabase.memory(), closeStreamsSynchronously: true)); - final LogRepository logRepository = LogRepository(loggerDb); - - WidgetsFlutterBinding.ensureInitialized(); - final db = await TestUtils.initIsar(); - - db.writeTxnSync(() => db.clearSync()); - await StoreService.init(storeRepository: IsarStoreRepository(db)); - await Store.put(StoreKey.currentUser, owner); - await LogService.init(logRepository: logRepository, storeRepository: IsarStoreRepository(db)); - }); - - group('Test SyncService grouped', () { - final MockHashService hs = MockHashService(); - final MockEntityService entityService = MockEntityService(); - final MockAlbumRepository albumRepository = MockAlbumRepository(); - final MockAssetRepository assetRepository = MockAssetRepository(); - final MockExifInfoRepository exifInfoRepository = MockExifInfoRepository(); - final MockIsarUserRepository userRepository = MockIsarUserRepository(); - final MockETagRepository eTagRepository = MockETagRepository(); - final MockAlbumMediaRepository albumMediaRepository = MockAlbumMediaRepository(); - final MockAlbumApiRepository albumApiRepository = MockAlbumApiRepository(); - final MockAppSettingService appSettingService = MockAppSettingService(); - final MockLocalFilesManagerRepository localFilesManagerRepository = MockLocalFilesManagerRepository(); - final MockPartnerApiRepository partnerApiRepository = MockPartnerApiRepository(); - final MockUserApiRepository userApiRepository = MockUserApiRepository(); - final MockPartnerRepository partnerRepository = MockPartnerRepository(); - final MockUserService userService = MockUserService(); - - final owner = UserDto( - id: "1", - updatedAt: DateTime.now(), - email: "a@b.c", - name: "first last", - isAdmin: false, - profileChangedAt: DateTime(2021), - ); - - late SyncService s; - - final List initialAssets = [ - makeAsset(checksum: "a", remoteId: "0-1"), - makeAsset(checksum: "b", remoteId: "2-1"), - makeAsset(checksum: "c", localId: "1", remoteId: "1-1"), - makeAsset(checksum: "d", localId: "2"), - makeAsset(checksum: "e", localId: "3"), - ]; - setUp(() { - s = SyncService( - hs, - entityService, - albumMediaRepository, - albumApiRepository, - albumRepository, - assetRepository, - exifInfoRepository, - partnerRepository, - userRepository, - userService, - eTagRepository, - appSettingService, - localFilesManagerRepository, - partnerApiRepository, - userApiRepository, - ); - when(() => userService.getMyUser()).thenReturn(owner); - when(() => eTagRepository.get(owner.id)).thenAnswer((_) async => ETag(id: owner.id, time: DateTime.now())); - when(() => eTagRepository.deleteByIds(["1"])).thenAnswer((_) async {}); - when(() => eTagRepository.upsertAll(any())).thenAnswer((_) async {}); - when(() => partnerRepository.getSharedWith()).thenAnswer((_) async => []); - when(() => userRepository.getAll(sortBy: SortUserBy.id)).thenAnswer((_) async => [owner]); - when(() => userRepository.getAll()).thenAnswer((_) async => [owner]); - when( - () => assetRepository.getAll(ownerId: owner.id, sortBy: AssetSort.checksum), - ).thenAnswer((_) async => initialAssets); - when( - () => assetRepository.getAllByOwnerIdChecksum(any(), any()), - ).thenAnswer((_) async => [initialAssets[3], null, null]); - when(() => assetRepository.updateAll(any())).thenAnswer((_) async => []); - when(() => assetRepository.deleteByIds(any())).thenAnswer((_) async {}); - when(() => exifInfoRepository.updateAll(any())).thenAnswer((_) async => []); - when( - () => assetRepository.transaction(any()), - ).thenAnswer((call) => (call.positionalArguments.first as Function).call()); - when( - () => assetRepository.transaction(any()), - ).thenAnswer((call) => (call.positionalArguments.first as Function).call()); - when(() => userApiRepository.getAll()).thenAnswer((_) async => [owner]); - registerFallbackValue(Direction.sharedByMe); - when(() => partnerApiRepository.getAll(any())).thenAnswer((_) async => []); - }); - test('test inserting existing assets', () async { - final List remoteAssets = [ - makeAsset(checksum: "a", remoteId: "0-1"), - makeAsset(checksum: "b", remoteId: "2-1"), - makeAsset(checksum: "c", remoteId: "1-1"), - ]; - final bool c1 = await s.syncRemoteAssetsToDb( - users: [owner], - getChangedAssets: _failDiff, - loadAssets: (u, d) => remoteAssets, - ); - expect(c1, isFalse); - verifyNever(() => assetRepository.updateAll(any())); - }); - - test('test inserting new assets', () async { - final List remoteAssets = [ - makeAsset(checksum: "a", remoteId: "0-1"), - makeAsset(checksum: "b", remoteId: "2-1"), - makeAsset(checksum: "c", remoteId: "1-1"), - makeAsset(checksum: "d", remoteId: "1-2"), - makeAsset(checksum: "f", remoteId: "1-4"), - makeAsset(checksum: "g", remoteId: "3-1"), - ]; - final bool c1 = await s.syncRemoteAssetsToDb( - users: [owner], - getChangedAssets: _failDiff, - loadAssets: (u, d) => remoteAssets, - ); - expect(c1, isTrue); - final updatedAsset = initialAssets[3].updatedCopy(remoteAssets[3]); - verify(() => assetRepository.updateAll([remoteAssets[4], remoteAssets[5], updatedAsset])); - }); - - test('test syncing duplicate assets', () async { - final List remoteAssets = [ - makeAsset(checksum: "a", remoteId: "0-1"), - makeAsset(checksum: "b", remoteId: "1-1"), - makeAsset(checksum: "c", remoteId: "2-1"), - makeAsset(checksum: "h", remoteId: "2-1b"), - makeAsset(checksum: "i", remoteId: "2-1c"), - makeAsset(checksum: "j", remoteId: "2-1d"), - ]; - final bool c1 = await s.syncRemoteAssetsToDb( - users: [owner], - getChangedAssets: _failDiff, - loadAssets: (u, d) => remoteAssets, - ); - expect(c1, isTrue); - when( - () => assetRepository.getAll(ownerId: owner.id, sortBy: AssetSort.checksum), - ).thenAnswer((_) async => remoteAssets); - final bool c2 = await s.syncRemoteAssetsToDb( - users: [owner], - getChangedAssets: _failDiff, - loadAssets: (u, d) => remoteAssets, - ); - expect(c2, isFalse); - final currentState = [...remoteAssets]; - when( - () => assetRepository.getAll(ownerId: owner.id, sortBy: AssetSort.checksum), - ).thenAnswer((_) async => currentState); - remoteAssets.removeAt(4); - final bool c3 = await s.syncRemoteAssetsToDb( - users: [owner], - getChangedAssets: _failDiff, - loadAssets: (u, d) => remoteAssets, - ); - expect(c3, isTrue); - remoteAssets.add(makeAsset(checksum: "k", remoteId: "2-1e")); - remoteAssets.add(makeAsset(checksum: "l", remoteId: "2-2")); - final bool c4 = await s.syncRemoteAssetsToDb( - users: [owner], - getChangedAssets: _failDiff, - loadAssets: (u, d) => remoteAssets, - ); - expect(c4, isTrue); - }); - - test('test efficient sync', () async { - when( - () => assetRepository.deleteAllByRemoteId([ - initialAssets[1].remoteId!, - initialAssets[2].remoteId!, - ], state: AssetState.remote), - ).thenAnswer((_) async { - return; - }); - when( - () => assetRepository.getAllByRemoteId(["2-1", "1-1"], state: AssetState.merged), - ).thenAnswer((_) async => [initialAssets[2]]); - when( - () => assetRepository.getAllByOwnerIdChecksum(any(), any()), - ).thenAnswer((_) async => [initialAssets[0], null, null]); //afg - final List toUpsert = [ - makeAsset(checksum: "a", remoteId: "0-1"), // changed - makeAsset(checksum: "f", remoteId: "0-2"), // new - makeAsset(checksum: "g", remoteId: "0-3"), // new - ]; - toUpsert[0].isFavorite = true; - final List toDelete = ["2-1", "1-1"]; - final expected = [...toUpsert]; - expected[0].id = initialAssets[0].id; - final bool c = await s.syncRemoteAssetsToDb( - users: [owner], - getChangedAssets: (user, since) async => (toUpsert, toDelete), - loadAssets: (user, date) => throw Exception(), - ); - expect(c, isTrue); - verify(() => assetRepository.updateAll(expected)); - }); - - group("upsertAssetsWithExif", () { - test('test upsert with EXIF data', () async { - final assets = [AssetStub.image1, AssetStub.image2]; - - expect(assets.map((a) => a.exifInfo?.assetId), List.filled(assets.length, null)); - await s.upsertAssetsWithExif(assets); - verify( - () => exifInfoRepository.updateAll( - any(that: containsAll(assets.map((a) => a.exifInfo!.copyWith(assetId: a.id)))), - ), - ); - expect(assets.map((a) => a.exifInfo?.assetId), assets.map((a) => a.id)); - }); - }); - }); -} - -Future<(List?, List?)> _failDiff(List user, DateTime time) => Future.value((null, null)); diff --git a/mobile/test/modules/utils/migration_test.dart b/mobile/test/modules/utils/migration_test.dart deleted file mode 100644 index 08ab1204a6..0000000000 --- a/mobile/test/modules/utils/migration_test.dart +++ /dev/null @@ -1,131 +0,0 @@ -import 'package:drift/drift.dart' hide isNull; -import 'package:drift/native.dart'; -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/entities/store.entity.dart'; -import 'package:immich_mobile/infrastructure/repositories/db.repository.dart'; -import 'package:immich_mobile/infrastructure/repositories/store.repository.dart'; -import 'package:immich_mobile/infrastructure/repositories/sync_stream.repository.dart'; -import 'package:immich_mobile/utils/migration.dart'; -import 'package:mocktail/mocktail.dart'; - -import '../../infrastructure/repository.mock.dart'; - -void main() { - late Drift db; - late SyncStreamRepository mockSyncStreamRepository; - - setUpAll(() async { - db = Drift(DatabaseConnection(NativeDatabase.memory(), closeStreamsSynchronously: true)); - await StoreService.init(storeRepository: DriftStoreRepository(db)); - mockSyncStreamRepository = MockSyncStreamRepository(); - when(() => mockSyncStreamRepository.reset()).thenAnswer((_) async => {}); - }); - - tearDown(() async { - await Store.clear(); - }); - - group('handleBetaMigration Tests', () { - group("version < 15", () { - test('already on new timeline', () async { - await Store.put(StoreKey.betaTimeline, true); - - await handleBetaMigration(14, false, mockSyncStreamRepository); - - expect(Store.tryGet(StoreKey.betaTimeline), true); - expect(Store.tryGet(StoreKey.needBetaMigration), false); - }); - - test('already on old timeline', () async { - await Store.put(StoreKey.betaTimeline, false); - - await handleBetaMigration(14, false, mockSyncStreamRepository); - - expect(Store.tryGet(StoreKey.needBetaMigration), true); - }); - - test('fresh install', () async { - await Store.delete(StoreKey.betaTimeline); - await handleBetaMigration(14, true, mockSyncStreamRepository); - - expect(Store.tryGet(StoreKey.betaTimeline), true); - expect(Store.tryGet(StoreKey.needBetaMigration), false); - }); - }); - - group("version == 15", () { - test('already on new timeline', () async { - await Store.put(StoreKey.betaTimeline, true); - - await handleBetaMigration(15, false, mockSyncStreamRepository); - - expect(Store.tryGet(StoreKey.betaTimeline), true); - expect(Store.tryGet(StoreKey.needBetaMigration), false); - }); - - test('already on old timeline', () async { - await Store.put(StoreKey.betaTimeline, false); - - await handleBetaMigration(15, false, mockSyncStreamRepository); - - expect(Store.tryGet(StoreKey.needBetaMigration), true); - }); - - test('fresh install', () async { - await Store.delete(StoreKey.betaTimeline); - await handleBetaMigration(15, true, mockSyncStreamRepository); - - expect(Store.tryGet(StoreKey.betaTimeline), true); - expect(Store.tryGet(StoreKey.needBetaMigration), false); - }); - }); - - group("version > 15", () { - test('already on new timeline', () async { - await Store.put(StoreKey.betaTimeline, true); - - await handleBetaMigration(16, false, mockSyncStreamRepository); - - expect(Store.tryGet(StoreKey.betaTimeline), true); - expect(Store.tryGet(StoreKey.needBetaMigration), false); - }); - - test('already on old timeline', () async { - await Store.put(StoreKey.betaTimeline, false); - - await handleBetaMigration(16, false, mockSyncStreamRepository); - - expect(Store.tryGet(StoreKey.betaTimeline), false); - expect(Store.tryGet(StoreKey.needBetaMigration), false); - }); - - test('fresh install', () async { - await Store.delete(StoreKey.betaTimeline); - await handleBetaMigration(16, true, mockSyncStreamRepository); - - expect(Store.tryGet(StoreKey.betaTimeline), true); - expect(Store.tryGet(StoreKey.needBetaMigration), false); - }); - }); - }); - - group('sync reset tests', () { - test('version < 16', () async { - await Store.put(StoreKey.shouldResetSync, false); - - await handleBetaMigration(15, false, mockSyncStreamRepository); - - expect(Store.tryGet(StoreKey.shouldResetSync), true); - }); - - test('version >= 16', () async { - await Store.put(StoreKey.shouldResetSync, false); - - await handleBetaMigration(16, false, mockSyncStreamRepository); - - expect(Store.tryGet(StoreKey.shouldResetSync), false); - }); - }); -} diff --git a/mobile/test/modules/utils/throttler_test.dart b/mobile/test/modules/utils/throttler_test.dart deleted file mode 100644 index 1757826daf..0000000000 --- a/mobile/test/modules/utils/throttler_test.dart +++ /dev/null @@ -1,46 +0,0 @@ -import 'package:flutter_test/flutter_test.dart'; -import 'package:immich_mobile/utils/throttle.dart'; -import 'package:immich_mobile/utils/debug_print.dart'; - -class _Counter { - int _count = 0; - _Counter(); - - int get count => _count; - void increment() { - dPrint(() => "Counter inside increment: $count"); - _count = _count + 1; - } -} - -void main() { - test('Executes the method immediately if no calls received previously', () async { - var counter = _Counter(); - final throttler = Throttler(interval: const Duration(milliseconds: 300)); - throttler.run(() => counter.increment()); - expect(counter.count, 1); - }); - - test('Does not execute calls before throttle interval', () async { - var counter = _Counter(); - final throttler = Throttler(interval: const Duration(milliseconds: 100)); - throttler.run(() => counter.increment()); - throttler.run(() => counter.increment()); - throttler.run(() => counter.increment()); - throttler.run(() => counter.increment()); - throttler.run(() => counter.increment()); - await Future.delayed(const Duration(seconds: 1)); - expect(counter.count, 1); - }); - - test('Executes the method if received in intervals', () async { - var counter = _Counter(); - final throttler = Throttler(interval: const Duration(milliseconds: 100)); - for (final _ in Iterable.generate(10)) { - throttler.run(() => counter.increment()); - await Future.delayed(const Duration(milliseconds: 50)); - } - await Future.delayed(const Duration(seconds: 1)); - expect(counter.count, 5); - }); -} diff --git a/mobile/test/modules/utils/thumbnail_utils_test.dart b/mobile/test/modules/utils/thumbnail_utils_test.dart deleted file mode 100644 index dd4588fc80..0000000000 --- a/mobile/test/modules/utils/thumbnail_utils_test.dart +++ /dev/null @@ -1,63 +0,0 @@ -import 'package:easy_localization/easy_localization.dart'; -import 'package:flutter_test/flutter_test.dart'; -import 'package:immich_mobile/domain/models/exif.model.dart'; -import 'package:immich_mobile/entities/asset.entity.dart'; -import 'package:immich_mobile/utils/thumbnail_utils.dart'; - -void main() { - final dateTime = DateTime(2025, 04, 25, 12, 13, 14); - final dateTimeString = DateFormat.yMMMMd().format(dateTime); - - test('returns description if it has one', () { - final result = getAltText(const ExifInfo(description: 'description'), dateTime, AssetType.image, []); - expect(result, 'description'); - }); - - test('returns image alt text with date if no location', () { - final (template, args) = getAltTextTemplate(const ExifInfo(), dateTime, AssetType.image, []); - expect(template, "image_alt_text_date"); - expect(args["isVideo"], "false"); - expect(args["date"], dateTimeString); - }); - - test('returns image alt text with date and place', () { - final (template, args) = getAltTextTemplate( - const ExifInfo(city: 'city', country: 'country'), - dateTime, - AssetType.video, - [], - ); - expect(template, "image_alt_text_date_place"); - expect(args["isVideo"], "true"); - expect(args["date"], dateTimeString); - expect(args["city"], "city"); - expect(args["country"], "country"); - }); - - test('returns image alt text with date and some people', () { - final (template, args) = getAltTextTemplate(const ExifInfo(), dateTime, AssetType.image, ["Alice", "Bob"]); - expect(template, "image_alt_text_date_2_people"); - expect(args["isVideo"], "false"); - expect(args["date"], dateTimeString); - expect(args["person1"], "Alice"); - expect(args["person2"], "Bob"); - }); - - test('returns image alt text with date and location and many people', () { - final (template, args) = getAltTextTemplate( - const ExifInfo(city: "city", country: 'country'), - dateTime, - AssetType.video, - ["Alice", "Bob", "Carol", "David", "Eve"], - ); - expect(template, "image_alt_text_date_place_4_or_more_people"); - expect(args["isVideo"], "true"); - expect(args["date"], dateTimeString); - expect(args["city"], "city"); - expect(args["country"], "country"); - expect(args["person1"], "Alice"); - expect(args["person2"], "Bob"); - expect(args["person3"], "Carol"); - expect(args["additionalCount"], "2"); - }); -} diff --git a/mobile/test/pages/search/search.page_test.dart b/mobile/test/pages/search/search.page_test.dart deleted file mode 100644 index 9592623a28..0000000000 --- a/mobile/test/pages/search/search.page_test.dart +++ /dev/null @@ -1,98 +0,0 @@ -@Skip('currently failing due to mock HTTP client to download ISAR binaries') -@Tags(['pages']) -library; - -import 'package:flutter/material.dart'; -import 'package:flutter_test/flutter_test.dart'; -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/domain/services/store.service.dart'; -import 'package:immich_mobile/infrastructure/repositories/store.repository.dart'; -import 'package:immich_mobile/pages/search/search.page.dart'; -import 'package:immich_mobile/providers/api.provider.dart'; -import 'package:immich_mobile/providers/db.provider.dart'; -import 'package:immich_mobile/providers/infrastructure/db.provider.dart'; -import 'package:isar/isar.dart'; -import 'package:mocktail/mocktail.dart'; -import 'package:openapi/api.dart'; - -import '../../dto.mocks.dart'; -import '../../service.mocks.dart'; -import '../../test_utils.dart'; -import '../../widget_tester_extensions.dart'; - -void main() { - late List overrides; - late Isar db; - late MockApiService mockApiService; - late MockSearchApi mockSearchApi; - - setUpAll(() async { - TestUtils.init(); - db = await TestUtils.initIsar(); - await StoreService.init(storeRepository: IsarStoreRepository(db)); - mockApiService = MockApiService(); - mockSearchApi = MockSearchApi(); - when(() => mockApiService.searchApi).thenReturn(mockSearchApi); - registerFallbackValue(MockSmartSearchDto()); - registerFallbackValue(MockMetadataSearchDto()); - overrides = [ - dbProvider.overrideWithValue(db), - isarProvider.overrideWithValue(db), - apiServiceProvider.overrideWithValue(mockApiService), - ]; - }); - - final emptyTextSearch = isA().having((s) => s.originalFileName, 'originalFileName', null); - - testWidgets('contextual search with/without text', (tester) async { - await tester.pumpConsumerWidget(const SearchPage(), overrides: overrides); - - await tester.pumpAndSettle(); - - expect(find.byIcon(Icons.abc_rounded), findsOneWidget, reason: 'Should have contextual search icon'); - - final searchField = find.byKey(const Key('search_text_field')); - expect(searchField, findsOneWidget); - - await tester.enterText(searchField, 'test'); - await tester.testTextInput.receiveAction(TextInputAction.search); - - var captured = verify(() => mockSearchApi.searchSmart(captureAny())).captured; - - expect(captured.first, isA().having((s) => s.query, 'query', 'test')); - - await tester.enterText(searchField, ''); - await tester.testTextInput.receiveAction(TextInputAction.search); - - captured = verify(() => mockSearchApi.searchAssets(captureAny())).captured; - expect(captured.first, emptyTextSearch); - }); - - testWidgets('not contextual search with/without text', (tester) async { - await tester.pumpConsumerWidget(const SearchPage(), overrides: overrides); - - await tester.pumpAndSettle(); - - await tester.tap(find.byKey(const Key('contextual_search_button'))); - - await tester.pumpAndSettle(); - - expect(find.byIcon(Icons.image_search_rounded), findsOneWidget, reason: 'Should not have contextual search icon'); - - final searchField = find.byKey(const Key('search_text_field')); - expect(searchField, findsOneWidget); - - await tester.enterText(searchField, 'test'); - await tester.testTextInput.receiveAction(TextInputAction.search); - - var captured = verify(() => mockSearchApi.searchAssets(captureAny())).captured; - - expect(captured.first, isA().having((s) => s.originalFileName, 'originalFileName', 'test')); - - await tester.enterText(searchField, ''); - await tester.testTextInput.receiveAction(TextInputAction.search); - - captured = verify(() => mockSearchApi.searchAssets(captureAny())).captured; - expect(captured.first, emptyTextSearch); - }); -} diff --git a/mobile/test/repository.mocks.dart b/mobile/test/repository.mocks.dart index 4b54ec4055..d049626f1d 100644 --- a/mobile/test/repository.mocks.dart +++ b/mobile/test/repository.mocks.dart @@ -1,48 +1,16 @@ -import 'package:immich_mobile/infrastructure/repositories/exif.repository.dart'; -import 'package:immich_mobile/repositories/partner_api.repository.dart'; -import 'package:immich_mobile/repositories/album_media.repository.dart'; -import 'package:immich_mobile/repositories/album_api.repository.dart'; -import 'package:immich_mobile/repositories/partner.repository.dart'; -import 'package:immich_mobile/repositories/etag.repository.dart'; -import 'package:immich_mobile/repositories/local_files_manager.repository.dart'; -import 'package:immich_mobile/repositories/file_media.repository.dart'; -import 'package:immich_mobile/repositories/backup.repository.dart'; +import 'package:immich_mobile/repositories/asset_api.repository.dart'; +import 'package:immich_mobile/repositories/asset_media.repository.dart'; import 'package:immich_mobile/repositories/auth.repository.dart'; import 'package:immich_mobile/repositories/auth_api.repository.dart'; -import 'package:immich_mobile/repositories/asset.repository.dart'; -import 'package:immich_mobile/repositories/asset_media.repository.dart'; -import 'package:immich_mobile/repositories/album.repository.dart'; -import 'package:immich_mobile/repositories/asset_api.repository.dart'; +import 'package:immich_mobile/repositories/local_files_manager.repository.dart'; import 'package:mocktail/mocktail.dart'; -class MockAlbumRepository extends Mock implements AlbumRepository {} - -class MockAssetRepository extends Mock implements AssetRepository {} - -class MockBackupRepository extends Mock implements BackupAlbumRepository {} - -class MockExifInfoRepository extends Mock implements IsarExifRepository {} - -class MockETagRepository extends Mock implements ETagRepository {} - -class MockAlbumMediaRepository extends Mock implements AlbumMediaRepository {} - -class MockBackupAlbumRepository extends Mock implements BackupAlbumRepository {} - class MockAssetApiRepository extends Mock implements AssetApiRepository {} class MockAssetMediaRepository extends Mock implements AssetMediaRepository {} -class MockFileMediaRepository extends Mock implements FileMediaRepository {} - -class MockAlbumApiRepository extends Mock implements AlbumApiRepository {} - class MockAuthApiRepository extends Mock implements AuthApiRepository {} class MockAuthRepository extends Mock implements AuthRepository {} -class MockPartnerRepository extends Mock implements PartnerRepository {} - -class MockPartnerApiRepository extends Mock implements PartnerApiRepository {} - class MockLocalFilesManagerRepository extends Mock implements LocalFilesManagerRepository {} diff --git a/mobile/test/service.mocks.dart b/mobile/test/service.mocks.dart index 87a8c01cf0..4591dd845d 100644 --- a/mobile/test/service.mocks.dart +++ b/mobile/test/service.mocks.dart @@ -1,31 +1,10 @@ -import 'package:immich_mobile/services/album.service.dart'; import 'package:immich_mobile/services/api.service.dart'; import 'package:immich_mobile/services/app_settings.service.dart'; -import 'package:immich_mobile/services/background.service.dart'; -import 'package:immich_mobile/services/backup.service.dart'; -import 'package:immich_mobile/services/entity.service.dart'; -import 'package:immich_mobile/services/hash.service.dart'; import 'package:immich_mobile/services/network.service.dart'; -import 'package:immich_mobile/services/sync.service.dart'; import 'package:mocktail/mocktail.dart'; -import 'package:openapi/api.dart'; class MockApiService extends Mock implements ApiService {} -class MockAlbumService extends Mock implements AlbumService {} - -class MockBackupService extends Mock implements BackupService {} - -class MockSyncService extends Mock implements SyncService {} - -class MockHashService extends Mock implements HashService {} - -class MockEntityService extends Mock implements EntityService {} - class MockNetworkService extends Mock implements NetworkService {} -class MockSearchApi extends Mock implements SearchApi {} - class MockAppSettingService extends Mock implements AppSettingsService {} - -class MockBackgroundService extends Mock implements BackgroundService {} diff --git a/mobile/test/services/album.service_test.dart b/mobile/test/services/album.service_test.dart deleted file mode 100644 index 97683cdab1..0000000000 --- a/mobile/test/services/album.service_test.dart +++ /dev/null @@ -1,177 +0,0 @@ -import 'package:flutter_test/flutter_test.dart'; -import 'package:immich_mobile/entities/backup_album.entity.dart'; -import 'package:immich_mobile/services/album.service.dart'; -import 'package:mocktail/mocktail.dart'; - -import '../domain/service.mock.dart'; -import '../fixtures/album.stub.dart'; -import '../fixtures/asset.stub.dart'; -import '../fixtures/user.stub.dart'; -import '../repository.mocks.dart'; -import '../service.mocks.dart'; - -void main() { - late AlbumService sut; - late MockUserService userService; - late MockSyncService syncService; - late MockEntityService entityService; - late MockAlbumRepository albumRepository; - late MockAssetRepository assetRepository; - late MockBackupRepository backupRepository; - late MockAlbumMediaRepository albumMediaRepository; - late MockAlbumApiRepository albumApiRepository; - - setUp(() { - userService = MockUserService(); - syncService = MockSyncService(); - entityService = MockEntityService(); - albumRepository = MockAlbumRepository(); - assetRepository = MockAssetRepository(); - backupRepository = MockBackupRepository(); - albumMediaRepository = MockAlbumMediaRepository(); - albumApiRepository = MockAlbumApiRepository(); - - when(() => userService.getMyUser()).thenReturn(UserStub.user1); - - when( - () => albumRepository.transaction(any()), - ).thenAnswer((call) => (call.positionalArguments.first as Function).call()); - when( - () => assetRepository.transaction(any()), - ).thenAnswer((call) => (call.positionalArguments.first as Function).call()); - - sut = AlbumService( - syncService, - userService, - entityService, - albumRepository, - assetRepository, - backupRepository, - albumMediaRepository, - albumApiRepository, - ); - }); - - group('refreshDeviceAlbums', () { - test('empty selection with one album in db', () async { - when(() => backupRepository.getIdsBySelection(BackupSelection.exclude)).thenAnswer((_) async => []); - when(() => backupRepository.getIdsBySelection(BackupSelection.select)).thenAnswer((_) async => []); - when(() => albumMediaRepository.getAll()).thenAnswer((_) async => []); - when(() => albumRepository.count(local: true)).thenAnswer((_) async => 1); - when(() => syncService.removeAllLocalAlbumsAndAssets()).thenAnswer((_) async => true); - final result = await sut.refreshDeviceAlbums(); - expect(result, false); - verify(() => syncService.removeAllLocalAlbumsAndAssets()); - }); - - test('one selected albums, two on device', () async { - when(() => backupRepository.getIdsBySelection(BackupSelection.exclude)).thenAnswer((_) async => []); - when( - () => backupRepository.getIdsBySelection(BackupSelection.select), - ).thenAnswer((_) async => [AlbumStub.oneAsset.localId!]); - when(() => albumMediaRepository.getAll()).thenAnswer((_) async => [AlbumStub.oneAsset, AlbumStub.twoAsset]); - when(() => syncService.syncLocalAlbumAssetsToDb(any(), any())).thenAnswer((_) async => true); - final result = await sut.refreshDeviceAlbums(); - expect(result, true); - verify(() => syncService.syncLocalAlbumAssetsToDb([AlbumStub.oneAsset], null)).called(1); - verifyNoMoreInteractions(syncService); - }); - }); - - group('refreshRemoteAlbums', () { - test('is working', () async { - when(() => syncService.getUsersFromServer()).thenAnswer((_) async => []); - when(() => syncService.syncUsersFromServer(any())).thenAnswer((_) async => true); - when(() => albumApiRepository.getAll(shared: true)).thenAnswer((_) async => [AlbumStub.sharedWithUser]); - - when( - () => albumApiRepository.getAll(shared: null), - ).thenAnswer((_) async => [AlbumStub.oneAsset, AlbumStub.twoAsset]); - - when( - () => syncService.syncRemoteAlbumsToDb([AlbumStub.twoAsset, AlbumStub.oneAsset, AlbumStub.sharedWithUser]), - ).thenAnswer((_) async => true); - final result = await sut.refreshRemoteAlbums(); - expect(result, true); - verify(() => syncService.getUsersFromServer()).called(1); - verify(() => syncService.syncUsersFromServer([])).called(1); - verify(() => albumApiRepository.getAll(shared: true)).called(1); - verify(() => albumApiRepository.getAll(shared: null)).called(1); - verify( - () => syncService.syncRemoteAlbumsToDb([AlbumStub.twoAsset, AlbumStub.oneAsset, AlbumStub.sharedWithUser]), - ).called(1); - verifyNoMoreInteractions(userService); - verifyNoMoreInteractions(albumApiRepository); - verifyNoMoreInteractions(syncService); - }); - }); - - group('createAlbum', () { - test('shared with assets', () async { - when( - () => albumApiRepository.create( - "name", - assetIds: any(named: "assetIds"), - sharedUserIds: any(named: "sharedUserIds"), - ), - ).thenAnswer((_) async => AlbumStub.oneAsset); - - when( - () => entityService.fillAlbumWithDatabaseEntities(AlbumStub.oneAsset), - ).thenAnswer((_) async => AlbumStub.oneAsset); - - when(() => albumRepository.create(AlbumStub.oneAsset)).thenAnswer((_) async => AlbumStub.twoAsset); - - final result = await sut.createAlbum("name", [AssetStub.image1], [UserStub.user1]); - expect(result, AlbumStub.twoAsset); - verify( - () => albumApiRepository.create( - "name", - assetIds: [AssetStub.image1.remoteId!], - sharedUserIds: [UserStub.user1.id], - ), - ).called(1); - verify(() => entityService.fillAlbumWithDatabaseEntities(AlbumStub.oneAsset)).called(1); - }); - }); - - group('addAdditionalAssetToAlbum', () { - test('one added, one duplicate', () async { - when( - () => albumApiRepository.addAssets(AlbumStub.oneAsset.remoteId!, any()), - ).thenAnswer((_) async => (added: [AssetStub.image2.remoteId!], duplicates: [AssetStub.image1.remoteId!])); - when(() => albumRepository.get(AlbumStub.oneAsset.id)).thenAnswer((_) async => AlbumStub.oneAsset); - when(() => albumRepository.addAssets(AlbumStub.oneAsset, [AssetStub.image2])).thenAnswer((_) async {}); - when(() => albumRepository.removeAssets(AlbumStub.oneAsset, [])).thenAnswer((_) async {}); - when(() => albumRepository.recalculateMetadata(AlbumStub.oneAsset)).thenAnswer((_) async => AlbumStub.oneAsset); - when(() => albumRepository.update(AlbumStub.oneAsset)).thenAnswer((_) async => AlbumStub.oneAsset); - - final result = await sut.addAssets(AlbumStub.oneAsset, [AssetStub.image1, AssetStub.image2]); - - expect(result != null, true); - expect(result!.alreadyInAlbum, [AssetStub.image1.remoteId!]); - expect(result.successfullyAdded, 1); - }); - }); - - group('addAdditionalUserToAlbum', () { - test('one added', () async { - when( - () => albumApiRepository.addUsers(AlbumStub.emptyAlbum.remoteId!, any()), - ).thenAnswer((_) async => AlbumStub.sharedWithUser); - - when( - () => albumRepository.addUsers( - AlbumStub.emptyAlbum, - AlbumStub.emptyAlbum.sharedUsers.map((u) => u.toDto()).toList(), - ), - ).thenAnswer((_) async => AlbumStub.emptyAlbum); - - when(() => albumRepository.update(AlbumStub.emptyAlbum)).thenAnswer((_) async => AlbumStub.emptyAlbum); - - final result = await sut.addUsers(AlbumStub.emptyAlbum, [UserStub.user2.id]); - - expect(result, true); - }); - }); -} diff --git a/mobile/test/services/asset.service_test.dart b/mobile/test/services/asset.service_test.dart deleted file mode 100644 index b741150165..0000000000 --- a/mobile/test/services/asset.service_test.dart +++ /dev/null @@ -1,103 +0,0 @@ -import 'package:flutter_test/flutter_test.dart'; -import 'package:immich_mobile/entities/asset.entity.dart'; -import 'package:immich_mobile/services/asset.service.dart'; -import 'package:maplibre_gl/maplibre_gl.dart'; -import 'package:mocktail/mocktail.dart'; -import 'package:openapi/api.dart'; - -import '../api.mocks.dart'; -import '../domain/service.mock.dart'; -import '../fixtures/asset.stub.dart'; -import '../infrastructure/repository.mock.dart'; -import '../repository.mocks.dart'; -import '../service.mocks.dart'; - -class FakeAssetBulkUpdateDto extends Fake implements AssetBulkUpdateDto {} - -void main() { - late AssetService sut; - - late MockAssetRepository assetRepository; - late MockAssetApiRepository assetApiRepository; - late MockExifInfoRepository exifInfoRepository; - late MockETagRepository eTagRepository; - late MockBackupAlbumRepository backupAlbumRepository; - late MockIsarUserRepository userRepository; - late MockAssetMediaRepository assetMediaRepository; - late MockApiService apiService; - - late MockSyncService syncService; - late MockAlbumService albumService; - late MockBackupService backupService; - late MockUserService userService; - - setUp(() { - assetRepository = MockAssetRepository(); - assetApiRepository = MockAssetApiRepository(); - exifInfoRepository = MockExifInfoRepository(); - userRepository = MockIsarUserRepository(); - eTagRepository = MockETagRepository(); - backupAlbumRepository = MockBackupAlbumRepository(); - apiService = MockApiService(); - assetMediaRepository = MockAssetMediaRepository(); - - syncService = MockSyncService(); - userService = MockUserService(); - albumService = MockAlbumService(); - backupService = MockBackupService(); - - sut = AssetService( - assetApiRepository, - assetRepository, - exifInfoRepository, - userRepository, - eTagRepository, - backupAlbumRepository, - apiService, - syncService, - backupService, - albumService, - userService, - assetMediaRepository, - ); - - registerFallbackValue(FakeAssetBulkUpdateDto()); - }); - - group("Edit ExifInfo", () { - late AssetsApi assetsApi; - setUp(() { - assetsApi = MockAssetsApi(); - when(() => apiService.assetsApi).thenReturn(assetsApi); - when(() => assetsApi.updateAssets(any())).thenAnswer((_) async => Future.value()); - }); - - test("asset is updated with DateTime", () async { - final assets = [AssetStub.image1, AssetStub.image2]; - final dateTime = DateTime.utc(2025, 6, 4, 2, 57); - await sut.changeDateTime(assets, dateTime.toIso8601String()); - - verify(() => assetsApi.updateAssets(any())).called(1); - final upsertExifCallback = verify(() => syncService.upsertAssetsWithExif(captureAny())); - upsertExifCallback.called(1); - final receivedAssets = upsertExifCallback.captured.firstOrNull as List? ?? []; - final receivedDatetime = receivedAssets.cast().map((a) => a.exifInfo?.dateTimeOriginal ?? DateTime(0)); - expect(receivedDatetime.every((d) => d == dateTime), isTrue); - }); - - test("asset is updated with LatLng", () async { - final assets = [AssetStub.image1, AssetStub.image2]; - final latLng = const LatLng(37.7749, -122.4194); - await sut.changeLocation(assets, latLng); - - verify(() => assetsApi.updateAssets(any())).called(1); - final upsertExifCallback = verify(() => syncService.upsertAssetsWithExif(captureAny())); - upsertExifCallback.called(1); - final receivedAssets = upsertExifCallback.captured.firstOrNull as List? ?? []; - final receivedCoords = receivedAssets.cast().map( - (a) => LatLng(a.exifInfo?.latitude ?? 0, a.exifInfo?.longitude ?? 0), - ); - expect(receivedCoords.every((l) => l == latLng), isTrue); - }); - }); -} diff --git a/mobile/test/services/auth.service_test.dart b/mobile/test/services/auth.service_test.dart index 7c7de3cd0e..f9a6d5e282 100644 --- a/mobile/test/services/auth.service_test.dart +++ b/mobile/test/services/auth.service_test.dart @@ -1,18 +1,19 @@ +import 'package:drift/drift.dart' hide isNull; +import 'package:drift/native.dart'; import 'package:flutter/widgets.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:immich_mobile/domain/services/store.service.dart'; +import 'package:immich_mobile/infrastructure/repositories/db.repository.dart'; import 'package:immich_mobile/infrastructure/repositories/store.repository.dart'; import 'package:immich_mobile/models/auth/auxilary_endpoint.model.dart'; import 'package:immich_mobile/services/app_settings.service.dart'; import 'package:immich_mobile/services/auth.service.dart'; -import 'package:isar/isar.dart'; import 'package:mocktail/mocktail.dart'; import 'package:openapi/api.dart'; import '../domain/service.mock.dart'; import '../repository.mocks.dart'; import '../service.mocks.dart'; -import '../test_utils.dart'; void main() { late AuthService sut; @@ -22,7 +23,7 @@ void main() { late MockNetworkService networkService; late MockBackgroundSyncManager backgroundSyncManager; late MockAppSettingService appSettingsService; - late Isar db; + late Drift db; setUp(() async { authApiRepository = MockAuthApiRepository(); @@ -45,19 +46,16 @@ void main() { }); setUpAll(() async { - db = await TestUtils.initIsar(); - db.writeTxnSync(() => db.clearSync()); - await StoreService.init(storeRepository: IsarStoreRepository(db)); + WidgetsFlutterBinding.ensureInitialized(); + db = Drift(DatabaseConnection(NativeDatabase.memory(), closeStreamsSynchronously: true)); + await StoreService.init(storeRepository: DriftStoreRepository(db)); + }); + + tearDownAll(() async { + await db.close(); }); group('validateServerUrl', () { - setUpAll(() async { - WidgetsFlutterBinding.ensureInitialized(); - final db = await TestUtils.initIsar(); - db.writeTxnSync(() => db.clearSync()); - await StoreService.init(storeRepository: IsarStoreRepository(db)); - }); - test('Should resolve HTTP endpoint', () async { const testUrl = 'http://ip:2283'; const resolvedUrl = 'http://ip:2283/api'; diff --git a/mobile/test/services/entity.service_test.dart b/mobile/test/services/entity.service_test.dart deleted file mode 100644 index 64b9fc604b..0000000000 --- a/mobile/test/services/entity.service_test.dart +++ /dev/null @@ -1,76 +0,0 @@ -import 'package:flutter_test/flutter_test.dart'; -import 'package:immich_mobile/entities/album.entity.dart'; -import 'package:immich_mobile/infrastructure/entities/user.entity.dart'; -import 'package:immich_mobile/services/entity.service.dart'; -import 'package:mocktail/mocktail.dart'; - -import '../fixtures/asset.stub.dart'; -import '../fixtures/user.stub.dart'; -import '../infrastructure/repository.mock.dart'; -import '../repository.mocks.dart'; - -void main() { - late EntityService sut; - late MockAssetRepository assetRepository; - late MockIsarUserRepository userRepository; - - setUp(() { - assetRepository = MockAssetRepository(); - userRepository = MockIsarUserRepository(); - sut = EntityService(assetRepository, userRepository); - }); - - group('fillAlbumWithDatabaseEntities', () { - test('remote album with owner, thumbnail, sharedUsers and assets', () async { - final Album album = - Album( - name: "album-with-two-assets-and-two-users", - localId: "album-with-two-assets-and-two-users-local", - remoteId: "album-with-two-assets-and-two-users-remote", - createdAt: DateTime(2001), - modifiedAt: DateTime(2010), - shared: true, - activityEnabled: true, - startDate: DateTime(2019), - endDate: DateTime(2020), - ) - ..remoteThumbnailAssetId = AssetStub.image1.remoteId - ..assets.addAll([AssetStub.image1, AssetStub.image1]) - ..owner.value = User.fromDto(UserStub.user1) - ..sharedUsers.addAll([User.fromDto(UserStub.admin), User.fromDto(UserStub.admin)]); - - when(() => userRepository.getByUserId(any())).thenAnswer((_) async => UserStub.admin); - when(() => userRepository.getByUserId(any())).thenAnswer((_) async => UserStub.admin); - - when(() => assetRepository.getByRemoteId(AssetStub.image1.remoteId!)).thenAnswer((_) async => AssetStub.image1); - - when(() => userRepository.getByUserIds(any())).thenAnswer((_) async => [UserStub.user1, UserStub.user2]); - - when(() => assetRepository.getAllByRemoteId(any())).thenAnswer((_) async => [AssetStub.image1, AssetStub.image2]); - - await sut.fillAlbumWithDatabaseEntities(album); - expect(album.owner.value?.toDto(), UserStub.admin); - expect(album.thumbnail.value, AssetStub.image1); - expect(album.remoteUsers.map((u) => u.toDto()).toSet(), {UserStub.user1, UserStub.user2}); - expect(album.remoteAssets.toSet(), {AssetStub.image1, AssetStub.image2}); - }); - - test('remote album without any info', () async { - makeEmptyAlbum() => Album( - name: "album-without-info", - localId: "album-without-info-local", - remoteId: "album-without-info-remote", - createdAt: DateTime(2001), - modifiedAt: DateTime(2010), - shared: false, - activityEnabled: false, - ); - - final album = makeEmptyAlbum(); - await sut.fillAlbumWithDatabaseEntities(album); - verifyNoMoreInteractions(assetRepository); - verifyNoMoreInteractions(userRepository); - expect(album, makeEmptyAlbum()); - }); - }); -} diff --git a/mobile/test/services/hash_service_test.dart b/mobile/test/services/hash_service_test.dart deleted file mode 100644 index 9429d434b0..0000000000 --- a/mobile/test/services/hash_service_test.dart +++ /dev/null @@ -1,349 +0,0 @@ -import 'dart:convert'; -import 'dart:io'; -import 'dart:math'; - -import 'package:collection/collection.dart'; -import 'package:file/memory.dart'; -import 'package:flutter/foundation.dart'; -import 'package:flutter_test/flutter_test.dart'; -import 'package:immich_mobile/domain/models/device_asset.model.dart'; -import 'package:immich_mobile/entities/asset.entity.dart'; -import 'package:immich_mobile/infrastructure/repositories/device_asset.repository.dart'; -import 'package:immich_mobile/services/background.service.dart'; -import 'package:immich_mobile/services/hash.service.dart'; -import 'package:mocktail/mocktail.dart'; - -import '../fixtures/asset.stub.dart'; -import '../infrastructure/repository.mock.dart'; -import '../service.mocks.dart'; -import '../mocks/asset_entity.mock.dart'; - -class MockAsset extends Mock implements Asset {} - -void main() { - late HashService sut; - late BackgroundService mockBackgroundService; - late IsarDeviceAssetRepository mockDeviceAssetRepository; - - setUp(() { - mockBackgroundService = MockBackgroundService(); - mockDeviceAssetRepository = MockDeviceAssetRepository(); - - sut = HashService(deviceAssetRepository: mockDeviceAssetRepository, backgroundService: mockBackgroundService); - - when(() => mockDeviceAssetRepository.transaction(any())).thenAnswer((_) async { - final capturedCallback = verify(() => mockDeviceAssetRepository.transaction(captureAny())).captured; - // Invoke the transaction callback - await (capturedCallback.firstOrNull as Future Function()?)?.call(); - }); - when(() => mockDeviceAssetRepository.updateAll(any())).thenAnswer((_) async => true); - when(() => mockDeviceAssetRepository.deleteIds(any())).thenAnswer((_) async => true); - }); - - group("HashService: No DeviceAsset entry", () { - test("hash successfully", () async { - final (mockAsset, file, deviceAsset, hash) = await _createAssetMock(AssetStub.image1); - - when(() => mockBackgroundService.digestFiles([file.path])).thenAnswer((_) async => [hash]); - // No DB entries for this asset - when(() => mockDeviceAssetRepository.getByIds([AssetStub.image1.localId!])).thenAnswer((_) async => []); - - final result = await sut.hashAssets([mockAsset]); - - // Verify we stored the new hash in DB - when(() => mockDeviceAssetRepository.transaction(any())).thenAnswer((_) async { - final capturedCallback = verify(() => mockDeviceAssetRepository.transaction(captureAny())).captured; - // Invoke the transaction callback - await (capturedCallback.firstOrNull as Future Function()?)?.call(); - verify( - () => mockDeviceAssetRepository.updateAll([ - deviceAsset.copyWith(modifiedTime: AssetStub.image1.fileModifiedAt), - ]), - ).called(1); - verify(() => mockDeviceAssetRepository.deleteIds([])).called(1); - }); - expect(result, [AssetStub.image1.copyWith(checksum: base64.encode(hash))]); - }); - }); - - group("HashService: Has DeviceAsset entry", () { - test("when the asset is not modified", () async { - final hash = utf8.encode("image1-hash"); - - when(() => mockDeviceAssetRepository.getByIds([AssetStub.image1.localId!])).thenAnswer( - (_) async => [ - DeviceAsset(assetId: AssetStub.image1.localId!, hash: hash, modifiedTime: AssetStub.image1.fileModifiedAt), - ], - ); - final result = await sut.hashAssets([AssetStub.image1]); - - verifyNever(() => mockBackgroundService.digestFiles(any())); - verifyNever(() => mockBackgroundService.digestFile(any())); - verifyNever(() => mockDeviceAssetRepository.updateAll(any())); - verifyNever(() => mockDeviceAssetRepository.deleteIds(any())); - - expect(result, [AssetStub.image1.copyWith(checksum: base64.encode(hash))]); - }); - - test("hashed successful when asset is modified", () async { - final (mockAsset, file, deviceAsset, hash) = await _createAssetMock(AssetStub.image1); - - when(() => mockBackgroundService.digestFiles([file.path])).thenAnswer((_) async => [hash]); - when( - () => mockDeviceAssetRepository.getByIds([AssetStub.image1.localId!]), - ).thenAnswer((_) async => [deviceAsset]); - - final result = await sut.hashAssets([mockAsset]); - - when(() => mockDeviceAssetRepository.transaction(any())).thenAnswer((_) async { - final capturedCallback = verify(() => mockDeviceAssetRepository.transaction(captureAny())).captured; - // Invoke the transaction callback - await (capturedCallback.firstOrNull as Future Function()?)?.call(); - verify( - () => mockDeviceAssetRepository.updateAll([ - deviceAsset.copyWith(modifiedTime: AssetStub.image1.fileModifiedAt), - ]), - ).called(1); - verify(() => mockDeviceAssetRepository.deleteIds([])).called(1); - }); - - verify(() => mockBackgroundService.digestFiles([file.path])).called(1); - - expect(result, [AssetStub.image1.copyWith(checksum: base64.encode(hash))]); - }); - }); - - group("HashService: Cleanup", () { - late Asset mockAsset; - late Uint8List hash; - late DeviceAsset deviceAsset; - late File file; - - setUp(() async { - (mockAsset, file, deviceAsset, hash) = await _createAssetMock(AssetStub.image1); - - when(() => mockBackgroundService.digestFiles([file.path])).thenAnswer((_) async => [hash]); - when( - () => mockDeviceAssetRepository.getByIds([AssetStub.image1.localId!]), - ).thenAnswer((_) async => [deviceAsset]); - }); - - test("cleanups DeviceAsset when local file cannot be obtained", () async { - when(() => mockAsset.local).thenThrow(Exception("File not found")); - final result = await sut.hashAssets([mockAsset]); - - verifyNever(() => mockBackgroundService.digestFiles(any())); - verifyNever(() => mockBackgroundService.digestFile(any())); - verifyNever(() => mockDeviceAssetRepository.updateAll(any())); - verify(() => mockDeviceAssetRepository.deleteIds([AssetStub.image1.localId!])).called(1); - - expect(result, isEmpty); - }); - - test("cleanups DeviceAsset when hashing failed", () async { - when(() => mockDeviceAssetRepository.transaction(any())).thenAnswer((_) async { - final capturedCallback = verify(() => mockDeviceAssetRepository.transaction(captureAny())).captured; - // Invoke the transaction callback - await (capturedCallback.firstOrNull as Future Function()?)?.call(); - - // Verify the callback inside the transaction because, doing it outside results - // in a small delay before the callback is invoked, resulting in other LOCs getting executed - // resulting in an incorrect state - // - // i.e, consider the following piece of code - // await _deviceAssetRepository.transaction(() async { - // await _deviceAssetRepository.updateAll(toBeAdded); - // await _deviceAssetRepository.deleteIds(toBeDeleted); - // }); - // toBeDeleted.clear(); - // since the transaction method is mocked, the callback is not invoked until it is captured - // and executed manually in the next event loop. However, the toBeDeleted.clear() is executed - // immediately once the transaction stub is executed, resulting in the deleteIds method being - // called with an empty list. - // - // To avoid this, we capture the callback and execute it within the transaction stub itself - // and verify the results inside the transaction stub - verify(() => mockDeviceAssetRepository.updateAll([])).called(1); - verify(() => mockDeviceAssetRepository.deleteIds([AssetStub.image1.localId!])).called(1); - }); - - when(() => mockBackgroundService.digestFiles([file.path])).thenAnswer( - // Invalid hash, length != 20 - (_) async => [Uint8List.fromList(hash.slice(2).toList())], - ); - - final result = await sut.hashAssets([mockAsset]); - - verify(() => mockBackgroundService.digestFiles([file.path])).called(1); - expect(result, isEmpty); - }); - }); - - group("HashService: Batch processing", () { - test("processes assets in batches when size limit is reached", () async { - // Setup multiple assets with large file sizes - final (mock1, mock2, mock3) = await ( - _createAssetMock(AssetStub.image1), - _createAssetMock(AssetStub.image2), - _createAssetMock(AssetStub.image3), - ).wait; - - final (asset1, file1, deviceAsset1, hash1) = mock1; - final (asset2, file2, deviceAsset2, hash2) = mock2; - final (asset3, file3, deviceAsset3, hash3) = mock3; - - when(() => mockDeviceAssetRepository.getByIds(any())).thenAnswer((_) async => []); - - // Setup for multiple batch processing calls - when(() => mockBackgroundService.digestFiles([file1.path, file2.path])).thenAnswer((_) async => [hash1, hash2]); - when(() => mockBackgroundService.digestFiles([file3.path])).thenAnswer((_) async => [hash3]); - - final size = await file1.length() + await file2.length(); - - sut = HashService( - deviceAssetRepository: mockDeviceAssetRepository, - backgroundService: mockBackgroundService, - batchSizeLimit: size, - ); - final result = await sut.hashAssets([asset1, asset2, asset3]); - - // Verify multiple batch process calls - verify(() => mockBackgroundService.digestFiles([file1.path, file2.path])).called(1); - verify(() => mockBackgroundService.digestFiles([file3.path])).called(1); - - expect(result, [ - AssetStub.image1.copyWith(checksum: base64.encode(hash1)), - AssetStub.image2.copyWith(checksum: base64.encode(hash2)), - AssetStub.image3.copyWith(checksum: base64.encode(hash3)), - ]); - }); - - test("processes assets in batches when file limit is reached", () async { - // Setup multiple assets with large file sizes - final (mock1, mock2, mock3) = await ( - _createAssetMock(AssetStub.image1), - _createAssetMock(AssetStub.image2), - _createAssetMock(AssetStub.image3), - ).wait; - - final (asset1, file1, deviceAsset1, hash1) = mock1; - final (asset2, file2, deviceAsset2, hash2) = mock2; - final (asset3, file3, deviceAsset3, hash3) = mock3; - - when(() => mockDeviceAssetRepository.getByIds(any())).thenAnswer((_) async => []); - - when(() => mockBackgroundService.digestFiles([file1.path])).thenAnswer((_) async => [hash1]); - when(() => mockBackgroundService.digestFiles([file2.path])).thenAnswer((_) async => [hash2]); - when(() => mockBackgroundService.digestFiles([file3.path])).thenAnswer((_) async => [hash3]); - - sut = HashService( - deviceAssetRepository: mockDeviceAssetRepository, - backgroundService: mockBackgroundService, - batchFileLimit: 1, - ); - final result = await sut.hashAssets([asset1, asset2, asset3]); - - // Verify multiple batch process calls - verify(() => mockBackgroundService.digestFiles([file1.path])).called(1); - verify(() => mockBackgroundService.digestFiles([file2.path])).called(1); - verify(() => mockBackgroundService.digestFiles([file3.path])).called(1); - - expect(result, [ - AssetStub.image1.copyWith(checksum: base64.encode(hash1)), - AssetStub.image2.copyWith(checksum: base64.encode(hash2)), - AssetStub.image3.copyWith(checksum: base64.encode(hash3)), - ]); - }); - - test("HashService: Sort & Process different states", () async { - final (asset1, file1, deviceAsset1, hash1) = await _createAssetMock(AssetStub.image1); // Will need rehashing - final (asset2, file2, deviceAsset2, hash2) = await _createAssetMock(AssetStub.image2); // Will have matching hash - final (asset3, file3, deviceAsset3, hash3) = await _createAssetMock(AssetStub.image3); // No DB entry - final asset4 = AssetStub.image3.copyWith(localId: "image4"); // Cannot be hashed - - when(() => mockBackgroundService.digestFiles([file1.path, file3.path])).thenAnswer((_) async => [hash1, hash3]); - // DB entries are not sorted and a dummy entry added - when( - () => mockDeviceAssetRepository.getByIds([ - AssetStub.image1.localId!, - AssetStub.image2.localId!, - AssetStub.image3.localId!, - asset4.localId!, - ]), - ).thenAnswer( - (_) async => [ - // Same timestamp to reuse deviceAsset - deviceAsset2.copyWith(modifiedTime: asset2.fileModifiedAt), - deviceAsset1, - deviceAsset3.copyWith(assetId: asset4.localId!), - ], - ); - - final result = await sut.hashAssets([asset1, asset2, asset3, asset4]); - - // Verify correct processing of all assets - verify(() => mockBackgroundService.digestFiles([file1.path, file3.path])).called(1); - expect(result.length, 3); - expect(result, [ - AssetStub.image2.copyWith(checksum: base64.encode(hash2)), - AssetStub.image1.copyWith(checksum: base64.encode(hash1)), - AssetStub.image3.copyWith(checksum: base64.encode(hash3)), - ]); - }); - - group("HashService: Edge cases", () { - test("handles empty list of assets", () async { - when(() => mockDeviceAssetRepository.getByIds(any())).thenAnswer((_) async => []); - - final result = await sut.hashAssets([]); - - verifyNever(() => mockBackgroundService.digestFiles(any())); - verifyNever(() => mockDeviceAssetRepository.updateAll(any())); - verifyNever(() => mockDeviceAssetRepository.deleteIds(any())); - - expect(result, isEmpty); - }); - - test("handles all file access failures", () async { - // No DB entries - when( - () => mockDeviceAssetRepository.getByIds([AssetStub.image1.localId!, AssetStub.image2.localId!]), - ).thenAnswer((_) async => []); - - final result = await sut.hashAssets([AssetStub.image1, AssetStub.image2]); - - verifyNever(() => mockBackgroundService.digestFiles(any())); - verifyNever(() => mockDeviceAssetRepository.updateAll(any())); - expect(result, isEmpty); - }); - }); - }); -} - -Future<(Asset, File, DeviceAsset, Uint8List)> _createAssetMock(Asset asset) async { - final random = Random(); - final hash = Uint8List.fromList(List.generate(20, (i) => random.nextInt(255))); - final mockAsset = MockAsset(); - final mockAssetEntity = MockAssetEntity(); - final fs = MemoryFileSystem(); - final deviceAsset = DeviceAsset( - assetId: asset.localId!, - hash: Uint8List.fromList(hash), - modifiedTime: DateTime.now(), - ); - final tmp = await fs.systemTempDirectory.createTemp(); - final file = tmp.childFile("${asset.fileName}-path"); - await file.writeAsString("${asset.fileName}-content"); - - when(() => mockAsset.localId).thenReturn(asset.localId); - when(() => mockAsset.fileName).thenReturn(asset.fileName); - when(() => mockAsset.fileCreatedAt).thenReturn(asset.fileCreatedAt); - when(() => mockAsset.fileModifiedAt).thenReturn(asset.fileModifiedAt); - when( - () => mockAsset.copyWith(checksum: any(named: "checksum")), - ).thenReturn(asset.copyWith(checksum: base64.encode(hash))); - when(() => mockAsset.local).thenAnswer((_) => mockAssetEntity); - when(() => mockAssetEntity.originFile).thenAnswer((_) async => file); - - return (mockAsset, file, deviceAsset, hash); -} diff --git a/mobile/test/test_utils.dart b/mobile/test/test_utils.dart index 30d4e2e6d4..75a41b46fb 100644 --- a/mobile/test/test_utils.dart +++ b/mobile/test/test_utils.dart @@ -4,82 +4,13 @@ import 'dart:io'; import 'package:easy_localization/easy_localization.dart'; import 'package:fake_async/fake_async.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' as domain; -import 'package:immich_mobile/entities/album.entity.dart'; -import 'package:immich_mobile/entities/android_device_asset.entity.dart'; -import 'package:immich_mobile/entities/asset.entity.dart'; -import 'package:immich_mobile/entities/backup_album.entity.dart'; -import 'package:immich_mobile/entities/duplicated_asset.entity.dart'; -import 'package:immich_mobile/entities/etag.entity.dart'; -import 'package:immich_mobile/entities/ios_device_asset.entity.dart'; -import 'package:immich_mobile/infrastructure/entities/device_asset.entity.dart'; -import 'package:immich_mobile/infrastructure/entities/exif.entity.dart'; -import 'package:immich_mobile/infrastructure/entities/store.entity.dart'; -import 'package:immich_mobile/infrastructure/entities/user.entity.dart'; -import 'package:isar/isar.dart'; -import 'package:mocktail/mocktail.dart'; import 'mock_http_override.dart'; -// Listener Mock to test when a provider notifies its listeners -class ListenerMock extends Mock { - void call(T? previous, T next); -} - abstract final class TestUtils { const TestUtils._(); - /// Downloads Isar binaries (if required) and initializes a new Isar db - static Future initIsar() async { - await Isar.initializeIsarCore(download: true); - - final instance = Isar.getInstance(); - if (instance != null) { - return instance; - } - - final db = await Isar.open( - [ - StoreValueSchema, - ExifInfoSchema, - AssetSchema, - AlbumSchema, - UserSchema, - BackupAlbumSchema, - DuplicatedAssetSchema, - ETagSchema, - AndroidDeviceAssetSchema, - IOSDeviceAssetSchema, - DeviceAssetEntitySchema, - ], - directory: "test/", - maxSizeMiB: 1024, - inspector: false, - ); - - // Clear and close db on test end - addTearDown(() async { - await db.writeTxn(() async => await db.clear()); - await db.close(); - }); - return db; - } - - /// Creates a new ProviderContainer to test Riverpod providers - static ProviderContainer createContainer({ - ProviderContainer? parent, - List overrides = const [], - List? observers, - }) { - final container = ProviderContainer(parent: parent, overrides: overrides, observers: observers); - - // Dispose on test end - addTearDown(container.dispose); - - return container; - } - static void init() { // Turn off easy localization logging EasyLocalization.logger.enableBuildModes = []; diff --git a/mobile/test/test_utils/medium_factory.dart b/mobile/test/test_utils/medium_factory.dart index 50e73e5b5e..c8c41bbf0f 100644 --- a/mobile/test/test_utils/medium_factory.dart +++ b/mobile/test/test_utils/medium_factory.dart @@ -1,7 +1,6 @@ import 'dart:math'; 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/infrastructure/repositories/db.repository.dart'; import 'package:immich_mobile/infrastructure/repositories/local_album.repository.dart'; @@ -10,28 +9,6 @@ class MediumFactory { const MediumFactory(Drift db) : _db = db; - LocalAsset localAsset({ - String? id, - String? name, - AssetType? type, - DateTime? createdAt, - DateTime? updatedAt, - String? checksum, - }) { - final random = Random(); - - return LocalAsset( - id: id ?? '${random.nextInt(1000000)}', - name: name ?? 'Asset ${random.nextInt(1000000)}', - checksum: checksum ?? '${random.nextInt(1000000)}', - type: type ?? AssetType.image, - createdAt: createdAt ?? DateTime.fromMillisecondsSinceEpoch(random.nextInt(1000000000)), - updatedAt: updatedAt ?? DateTime.fromMillisecondsSinceEpoch(random.nextInt(1000000000)), - playbackStyle: AssetPlaybackStyle.image, - isEdited: false, - ); - } - LocalAlbum localAlbum({ String? id, String? name, From 5334a6254a31fa8e6948e749d704407d0b918a07 Mon Sep 17 00:00:00 2001 From: bo0tzz Date: Wed, 15 Apr 2026 19:31:23 +0200 Subject: [PATCH 03/16] fix: make web build stage deterministic (#27823) --- server/Dockerfile | 3 +++ web/svelte.config.js | 3 +++ 2 files changed, 6 insertions(+) diff --git a/server/Dockerfile b/server/Dockerfile index 476d58b983..5951baa96c 100644 --- a/server/Dockerfile +++ b/server/Dockerfile @@ -23,6 +23,9 @@ RUN --mount=type=cache,id=pnpm-server,target=/buildcache/pnpm-store \ FROM builder AS web +ARG BUILD_ID +ENV IMMICH_BUILD=${BUILD_ID} + WORKDIR /usr/src/app COPY ./web ./web/ COPY ./i18n ./i18n/ diff --git a/web/svelte.config.js b/web/svelte.config.js index e2a5fb5c46..15223655e6 100644 --- a/web/svelte.config.js +++ b/web/svelte.config.js @@ -15,6 +15,9 @@ const config = { }, preprocess: vitePreprocess(), kit: { + version: { + name: process.env.IMMICH_BUILD || Date.now().toString(), + }, paths: { relative: false, }, From d41013131283ee75f098fdb1423e18203ade5e55 Mon Sep 17 00:00:00 2001 From: Jason Rasmussen Date: Wed, 15 Apr 2026 13:58:48 -0400 Subject: [PATCH 04/16] chore!: remove old timeline sync endpoints (#27804) --- mobile/openapi/README.md | 7 - mobile/openapi/lib/api.dart | 3 - mobile/openapi/lib/api/deprecated_api.dart | 115 ---------- mobile/openapi/lib/api/sync_api.dart | 115 ---------- mobile/openapi/lib/api_client.dart | 6 - .../lib/model/asset_delta_sync_dto.dart | 113 ---------- .../model/asset_delta_sync_response_dto.dart | 119 ---------- .../lib/model/asset_full_sync_dto.dart | 150 ------------- mobile/openapi/lib/model/job_name.dart | 3 - open-api/immich-openapi-specs.json | 205 ------------------ open-api/typescript-sdk/src/fetch-client.ts | 54 ----- server/src/controllers/sync.controller.ts | 35 +-- server/src/dtos/sync.dto.ts | 31 --- server/src/enum.ts | 16 -- server/src/queries/asset.repository.sql | 57 ----- server/src/queries/audit.repository.sql | 16 -- server/src/repositories/asset.repository.ts | 77 ------- server/src/repositories/audit.repository.ts | 44 ---- server/src/repositories/index.ts | 2 - server/src/schema/index.ts | 4 - .../1776217577402-DropAuditTable.ts | 18 ++ server/src/schema/tables/audit.table.ts | 24 -- server/src/services/audit.service.spec.ts | 26 --- server/src/services/audit.service.ts | 15 -- server/src/services/base.service.ts | 3 - server/src/services/index.ts | 2 - server/src/services/queue.service.spec.ts | 1 - server/src/services/queue.service.ts | 1 - server/src/services/sync.service.spec.ts | 98 --------- server/src/services/sync.service.ts | 83 +------ server/src/types.ts | 1 - .../repositories/asset.repository.mock.ts | 2 - server/test/utils.ts | 4 - 33 files changed, 20 insertions(+), 1430 deletions(-) delete mode 100644 mobile/openapi/lib/model/asset_delta_sync_dto.dart delete mode 100644 mobile/openapi/lib/model/asset_delta_sync_response_dto.dart delete mode 100644 mobile/openapi/lib/model/asset_full_sync_dto.dart delete mode 100644 server/src/queries/audit.repository.sql delete mode 100644 server/src/repositories/audit.repository.ts create mode 100644 server/src/schema/migrations/1776217577402-DropAuditTable.ts delete mode 100644 server/src/schema/tables/audit.table.ts delete mode 100644 server/src/services/audit.service.spec.ts delete mode 100644 server/src/services/audit.service.ts delete mode 100644 server/src/services/sync.service.spec.ts diff --git a/mobile/openapi/README.md b/mobile/openapi/README.md index 612a65ae4e..c893ce33a5 100644 --- a/mobile/openapi/README.md +++ b/mobile/openapi/README.md @@ -143,8 +143,6 @@ Class | Method | HTTP request | Description *DatabaseBackupsAdminApi* | [**uploadDatabaseBackup**](doc//DatabaseBackupsAdminApi.md#uploaddatabasebackup) | **POST** /admin/database-backups/upload | Upload database backup *DeprecatedApi* | [**createPartnerDeprecated**](doc//DeprecatedApi.md#createpartnerdeprecated) | **POST** /partners/{id} | Create a partner *DeprecatedApi* | [**getAllUserAssetsByDeviceId**](doc//DeprecatedApi.md#getalluserassetsbydeviceid) | **GET** /assets/device/{deviceId} | Retrieve assets by device ID -*DeprecatedApi* | [**getDeltaSync**](doc//DeprecatedApi.md#getdeltasync) | **POST** /sync/delta-sync | Get delta sync for user -*DeprecatedApi* | [**getFullSyncForUser**](doc//DeprecatedApi.md#getfullsyncforuser) | **POST** /sync/full-sync | Get full sync for user *DeprecatedApi* | [**getQueuesLegacy**](doc//DeprecatedApi.md#getqueueslegacy) | **GET** /jobs | Retrieve queue counts and status *DeprecatedApi* | [**runQueueCommandLegacy**](doc//DeprecatedApi.md#runqueuecommandlegacy) | **PUT** /jobs/{name} | Run jobs *DownloadApi* | [**downloadArchive**](doc//DownloadApi.md#downloadarchive) | **POST** /download/archive | Download asset archive @@ -263,8 +261,6 @@ Class | Method | HTTP request | Description *StacksApi* | [**searchStacks**](doc//StacksApi.md#searchstacks) | **GET** /stacks | Retrieve stacks *StacksApi* | [**updateStack**](doc//StacksApi.md#updatestack) | **PUT** /stacks/{id} | Update a stack *SyncApi* | [**deleteSyncAck**](doc//SyncApi.md#deletesyncack) | **DELETE** /sync/ack | Delete acknowledgements -*SyncApi* | [**getDeltaSync**](doc//SyncApi.md#getdeltasync) | **POST** /sync/delta-sync | Get delta sync for user -*SyncApi* | [**getFullSyncForUser**](doc//SyncApi.md#getfullsyncforuser) | **POST** /sync/full-sync | Get full sync for user *SyncApi* | [**getSyncAck**](doc//SyncApi.md#getsyncack) | **GET** /sync/ack | Retrieve acknowledgements *SyncApi* | [**getSyncStream**](doc//SyncApi.md#getsyncstream) | **POST** /sync/stream | Stream sync changes *SyncApi* | [**sendSyncAck**](doc//SyncApi.md#sendsyncack) | **POST** /sync/ack | Acknowledge changes @@ -352,8 +348,6 @@ Class | Method | HTTP request | Description - [AssetBulkUploadCheckResponseDto](doc//AssetBulkUploadCheckResponseDto.md) - [AssetBulkUploadCheckResult](doc//AssetBulkUploadCheckResult.md) - [AssetCopyDto](doc//AssetCopyDto.md) - - [AssetDeltaSyncDto](doc//AssetDeltaSyncDto.md) - - [AssetDeltaSyncResponseDto](doc//AssetDeltaSyncResponseDto.md) - [AssetEditAction](doc//AssetEditAction.md) - [AssetEditActionItemDto](doc//AssetEditActionItemDto.md) - [AssetEditActionItemDtoParameters](doc//AssetEditActionItemDtoParameters.md) @@ -366,7 +360,6 @@ Class | Method | HTTP request | Description - [AssetFaceUpdateDto](doc//AssetFaceUpdateDto.md) - [AssetFaceUpdateItem](doc//AssetFaceUpdateItem.md) - [AssetFaceWithoutPersonResponseDto](doc//AssetFaceWithoutPersonResponseDto.md) - - [AssetFullSyncDto](doc//AssetFullSyncDto.md) - [AssetIdErrorReason](doc//AssetIdErrorReason.md) - [AssetIdsDto](doc//AssetIdsDto.md) - [AssetIdsResponseDto](doc//AssetIdsResponseDto.md) diff --git a/mobile/openapi/lib/api.dart b/mobile/openapi/lib/api.dart index 9403852ef0..b64f222791 100644 --- a/mobile/openapi/lib/api.dart +++ b/mobile/openapi/lib/api.dart @@ -94,8 +94,6 @@ part 'model/asset_bulk_upload_check_item.dart'; part 'model/asset_bulk_upload_check_response_dto.dart'; part 'model/asset_bulk_upload_check_result.dart'; part 'model/asset_copy_dto.dart'; -part 'model/asset_delta_sync_dto.dart'; -part 'model/asset_delta_sync_response_dto.dart'; part 'model/asset_edit_action.dart'; part 'model/asset_edit_action_item_dto.dart'; part 'model/asset_edit_action_item_dto_parameters.dart'; @@ -108,7 +106,6 @@ part 'model/asset_face_response_dto.dart'; part 'model/asset_face_update_dto.dart'; part 'model/asset_face_update_item.dart'; part 'model/asset_face_without_person_response_dto.dart'; -part 'model/asset_full_sync_dto.dart'; part 'model/asset_id_error_reason.dart'; part 'model/asset_ids_dto.dart'; part 'model/asset_ids_response_dto.dart'; diff --git a/mobile/openapi/lib/api/deprecated_api.dart b/mobile/openapi/lib/api/deprecated_api.dart index a292c176fd..5fa5df88b6 100644 --- a/mobile/openapi/lib/api/deprecated_api.dart +++ b/mobile/openapi/lib/api/deprecated_api.dart @@ -135,121 +135,6 @@ class DeprecatedApi { return null; } - /// Get delta sync for user - /// - /// Retrieve changed assets since the last sync for the authenticated user. - /// - /// Note: This method returns the HTTP [Response]. - /// - /// Parameters: - /// - /// * [AssetDeltaSyncDto] assetDeltaSyncDto (required): - Future getDeltaSyncWithHttpInfo(AssetDeltaSyncDto assetDeltaSyncDto,) async { - // ignore: prefer_const_declarations - final apiPath = r'/sync/delta-sync'; - - // ignore: prefer_final_locals - Object? postBody = assetDeltaSyncDto; - - final queryParams = []; - final headerParams = {}; - final formParams = {}; - - const contentTypes = ['application/json']; - - - return apiClient.invokeAPI( - apiPath, - 'POST', - queryParams, - postBody, - headerParams, - formParams, - contentTypes.isEmpty ? null : contentTypes.first, - ); - } - - /// Get delta sync for user - /// - /// Retrieve changed assets since the last sync for the authenticated user. - /// - /// Parameters: - /// - /// * [AssetDeltaSyncDto] assetDeltaSyncDto (required): - Future getDeltaSync(AssetDeltaSyncDto assetDeltaSyncDto,) async { - final response = await getDeltaSyncWithHttpInfo(assetDeltaSyncDto,); - 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), 'AssetDeltaSyncResponseDto',) as AssetDeltaSyncResponseDto; - - } - return null; - } - - /// Get full sync for user - /// - /// Retrieve all assets for a full synchronization for the authenticated user. - /// - /// Note: This method returns the HTTP [Response]. - /// - /// Parameters: - /// - /// * [AssetFullSyncDto] assetFullSyncDto (required): - Future getFullSyncForUserWithHttpInfo(AssetFullSyncDto assetFullSyncDto,) async { - // ignore: prefer_const_declarations - final apiPath = r'/sync/full-sync'; - - // ignore: prefer_final_locals - Object? postBody = assetFullSyncDto; - - final queryParams = []; - final headerParams = {}; - final formParams = {}; - - const contentTypes = ['application/json']; - - - return apiClient.invokeAPI( - apiPath, - 'POST', - queryParams, - postBody, - headerParams, - formParams, - contentTypes.isEmpty ? null : contentTypes.first, - ); - } - - /// Get full sync for user - /// - /// Retrieve all assets for a full synchronization for the authenticated user. - /// - /// Parameters: - /// - /// * [AssetFullSyncDto] assetFullSyncDto (required): - Future?> getFullSyncForUser(AssetFullSyncDto assetFullSyncDto,) async { - final response = await getFullSyncForUserWithHttpInfo(assetFullSyncDto,); - 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) { - final responseBody = await _decodeBodyBytes(response); - return (await apiClient.deserializeAsync(responseBody, 'List') as List) - .cast() - .toList(growable: false); - - } - return null; - } - /// Retrieve queue counts and status /// /// Retrieve the counts of the current queue, as well as the current status. diff --git a/mobile/openapi/lib/api/sync_api.dart b/mobile/openapi/lib/api/sync_api.dart index 6194fd0f89..e7bc822ace 100644 --- a/mobile/openapi/lib/api/sync_api.dart +++ b/mobile/openapi/lib/api/sync_api.dart @@ -64,121 +64,6 @@ class SyncApi { } } - /// Get delta sync for user - /// - /// Retrieve changed assets since the last sync for the authenticated user. - /// - /// Note: This method returns the HTTP [Response]. - /// - /// Parameters: - /// - /// * [AssetDeltaSyncDto] assetDeltaSyncDto (required): - Future getDeltaSyncWithHttpInfo(AssetDeltaSyncDto assetDeltaSyncDto,) async { - // ignore: prefer_const_declarations - final apiPath = r'/sync/delta-sync'; - - // ignore: prefer_final_locals - Object? postBody = assetDeltaSyncDto; - - final queryParams = []; - final headerParams = {}; - final formParams = {}; - - const contentTypes = ['application/json']; - - - return apiClient.invokeAPI( - apiPath, - 'POST', - queryParams, - postBody, - headerParams, - formParams, - contentTypes.isEmpty ? null : contentTypes.first, - ); - } - - /// Get delta sync for user - /// - /// Retrieve changed assets since the last sync for the authenticated user. - /// - /// Parameters: - /// - /// * [AssetDeltaSyncDto] assetDeltaSyncDto (required): - Future getDeltaSync(AssetDeltaSyncDto assetDeltaSyncDto,) async { - final response = await getDeltaSyncWithHttpInfo(assetDeltaSyncDto,); - 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), 'AssetDeltaSyncResponseDto',) as AssetDeltaSyncResponseDto; - - } - return null; - } - - /// Get full sync for user - /// - /// Retrieve all assets for a full synchronization for the authenticated user. - /// - /// Note: This method returns the HTTP [Response]. - /// - /// Parameters: - /// - /// * [AssetFullSyncDto] assetFullSyncDto (required): - Future getFullSyncForUserWithHttpInfo(AssetFullSyncDto assetFullSyncDto,) async { - // ignore: prefer_const_declarations - final apiPath = r'/sync/full-sync'; - - // ignore: prefer_final_locals - Object? postBody = assetFullSyncDto; - - final queryParams = []; - final headerParams = {}; - final formParams = {}; - - const contentTypes = ['application/json']; - - - return apiClient.invokeAPI( - apiPath, - 'POST', - queryParams, - postBody, - headerParams, - formParams, - contentTypes.isEmpty ? null : contentTypes.first, - ); - } - - /// Get full sync for user - /// - /// Retrieve all assets for a full synchronization for the authenticated user. - /// - /// Parameters: - /// - /// * [AssetFullSyncDto] assetFullSyncDto (required): - Future?> getFullSyncForUser(AssetFullSyncDto assetFullSyncDto,) async { - final response = await getFullSyncForUserWithHttpInfo(assetFullSyncDto,); - 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) { - final responseBody = await _decodeBodyBytes(response); - return (await apiClient.deserializeAsync(responseBody, 'List') as List) - .cast() - .toList(growable: false); - - } - return null; - } - /// Retrieve acknowledgements /// /// Retrieve the synchronization acknowledgments for the current session. diff --git a/mobile/openapi/lib/api_client.dart b/mobile/openapi/lib/api_client.dart index 3ed1f7529f..c54a7bafa8 100644 --- a/mobile/openapi/lib/api_client.dart +++ b/mobile/openapi/lib/api_client.dart @@ -234,10 +234,6 @@ class ApiClient { return AssetBulkUploadCheckResult.fromJson(value); case 'AssetCopyDto': return AssetCopyDto.fromJson(value); - case 'AssetDeltaSyncDto': - return AssetDeltaSyncDto.fromJson(value); - case 'AssetDeltaSyncResponseDto': - return AssetDeltaSyncResponseDto.fromJson(value); case 'AssetEditAction': return AssetEditActionTypeTransformer().decode(value); case 'AssetEditActionItemDto': @@ -262,8 +258,6 @@ class ApiClient { return AssetFaceUpdateItem.fromJson(value); case 'AssetFaceWithoutPersonResponseDto': return AssetFaceWithoutPersonResponseDto.fromJson(value); - case 'AssetFullSyncDto': - return AssetFullSyncDto.fromJson(value); case 'AssetIdErrorReason': return AssetIdErrorReasonTypeTransformer().decode(value); case 'AssetIdsDto': diff --git a/mobile/openapi/lib/model/asset_delta_sync_dto.dart b/mobile/openapi/lib/model/asset_delta_sync_dto.dart deleted file mode 100644 index f59cdc1a67..0000000000 --- a/mobile/openapi/lib/model/asset_delta_sync_dto.dart +++ /dev/null @@ -1,113 +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 AssetDeltaSyncDto { - /// Returns a new [AssetDeltaSyncDto] instance. - AssetDeltaSyncDto({ - required this.updatedAfter, - this.userIds = const [], - }); - - /// Sync assets updated after this date - DateTime updatedAfter; - - /// User IDs to sync - List userIds; - - @override - bool operator ==(Object other) => identical(this, other) || other is AssetDeltaSyncDto && - other.updatedAfter == updatedAfter && - _deepEquality.equals(other.userIds, userIds); - - @override - int get hashCode => - // ignore: unnecessary_parenthesis - (updatedAfter.hashCode) + - (userIds.hashCode); - - @override - String toString() => 'AssetDeltaSyncDto[updatedAfter=$updatedAfter, userIds=$userIds]'; - - Map toJson() { - final json = {}; - json[r'updatedAfter'] = _isEpochMarker(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))$/') - ? this.updatedAfter.millisecondsSinceEpoch - : this.updatedAfter.toUtc().toIso8601String(); - json[r'userIds'] = this.userIds; - return json; - } - - /// Returns a new [AssetDeltaSyncDto] instance and imports its values from - /// [value] if it's a [Map], null otherwise. - // ignore: prefer_constructors_over_static_methods - static AssetDeltaSyncDto? fromJson(dynamic value) { - upgradeDto(value, "AssetDeltaSyncDto"); - if (value is Map) { - final json = value.cast(); - - return AssetDeltaSyncDto( - updatedAfter: mapDateTime(json, r'updatedAfter', 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))$/')!, - userIds: json[r'userIds'] is Iterable - ? (json[r'userIds'] as Iterable).cast().toList(growable: false) - : const [], - ); - } - return null; - } - - static List listFromJson(dynamic json, {bool growable = false,}) { - final result = []; - if (json is List && json.isNotEmpty) { - for (final row in json) { - final value = AssetDeltaSyncDto.fromJson(row); - if (value != null) { - result.add(value); - } - } - } - return result.toList(growable: growable); - } - - static Map mapFromJson(dynamic json) { - final map = {}; - if (json is Map && json.isNotEmpty) { - json = json.cast(); // ignore: parameter_assignments - for (final entry in json.entries) { - final value = AssetDeltaSyncDto.fromJson(entry.value); - if (value != null) { - map[entry.key] = value; - } - } - } - return map; - } - - // maps a json object with a list of AssetDeltaSyncDto-objects as value to a dart map - static Map> mapListFromJson(dynamic json, {bool growable = false,}) { - final map = >{}; - if (json is Map && json.isNotEmpty) { - // ignore: parameter_assignments - json = json.cast(); - for (final entry in json.entries) { - map[entry.key] = AssetDeltaSyncDto.listFromJson(entry.value, growable: growable,); - } - } - return map; - } - - /// The list of required keys that must be present in a JSON. - static const requiredKeys = { - 'updatedAfter', - 'userIds', - }; -} - diff --git a/mobile/openapi/lib/model/asset_delta_sync_response_dto.dart b/mobile/openapi/lib/model/asset_delta_sync_response_dto.dart deleted file mode 100644 index 348bae370b..0000000000 --- a/mobile/openapi/lib/model/asset_delta_sync_response_dto.dart +++ /dev/null @@ -1,119 +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 AssetDeltaSyncResponseDto { - /// Returns a new [AssetDeltaSyncResponseDto] instance. - AssetDeltaSyncResponseDto({ - this.deleted = const [], - required this.needsFullSync, - this.upserted = const [], - }); - - /// Deleted asset IDs - List deleted; - - /// Whether full sync is needed - bool needsFullSync; - - List upserted; - - @override - bool operator ==(Object other) => identical(this, other) || other is AssetDeltaSyncResponseDto && - _deepEquality.equals(other.deleted, deleted) && - other.needsFullSync == needsFullSync && - _deepEquality.equals(other.upserted, upserted); - - @override - int get hashCode => - // ignore: unnecessary_parenthesis - (deleted.hashCode) + - (needsFullSync.hashCode) + - (upserted.hashCode); - - @override - String toString() => 'AssetDeltaSyncResponseDto[deleted=$deleted, needsFullSync=$needsFullSync, upserted=$upserted]'; - - Map toJson() { - final json = {}; - json[r'deleted'] = this.deleted; - json[r'needsFullSync'] = this.needsFullSync; - json[r'upserted'] = this.upserted; - return json; - } - - /// Returns a new [AssetDeltaSyncResponseDto] instance and imports its values from - /// [value] if it's a [Map], null otherwise. - // ignore: prefer_constructors_over_static_methods - static AssetDeltaSyncResponseDto? fromJson(dynamic value) { - upgradeDto(value, "AssetDeltaSyncResponseDto"); - if (value is Map) { - final json = value.cast(); - - return AssetDeltaSyncResponseDto( - deleted: json[r'deleted'] is Iterable - ? (json[r'deleted'] as Iterable).cast().toList(growable: false) - : const [], - needsFullSync: mapValueOfType(json, r'needsFullSync')!, - upserted: AssetResponseDto.listFromJson(json[r'upserted']), - ); - } - return null; - } - - static List listFromJson(dynamic json, {bool growable = false,}) { - final result = []; - if (json is List && json.isNotEmpty) { - for (final row in json) { - final value = AssetDeltaSyncResponseDto.fromJson(row); - if (value != null) { - result.add(value); - } - } - } - return result.toList(growable: growable); - } - - static Map mapFromJson(dynamic json) { - final map = {}; - if (json is Map && json.isNotEmpty) { - json = json.cast(); // ignore: parameter_assignments - for (final entry in json.entries) { - final value = AssetDeltaSyncResponseDto.fromJson(entry.value); - if (value != null) { - map[entry.key] = value; - } - } - } - return map; - } - - // maps a json object with a list of AssetDeltaSyncResponseDto-objects as value to a dart map - static Map> mapListFromJson(dynamic json, {bool growable = false,}) { - final map = >{}; - if (json is Map && json.isNotEmpty) { - // ignore: parameter_assignments - json = json.cast(); - for (final entry in json.entries) { - map[entry.key] = AssetDeltaSyncResponseDto.listFromJson(entry.value, growable: growable,); - } - } - return map; - } - - /// The list of required keys that must be present in a JSON. - static const requiredKeys = { - 'deleted', - 'needsFullSync', - 'upserted', - }; -} - diff --git a/mobile/openapi/lib/model/asset_full_sync_dto.dart b/mobile/openapi/lib/model/asset_full_sync_dto.dart deleted file mode 100644 index 835e063e92..0000000000 --- a/mobile/openapi/lib/model/asset_full_sync_dto.dart +++ /dev/null @@ -1,150 +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 AssetFullSyncDto { - /// Returns a new [AssetFullSyncDto] instance. - AssetFullSyncDto({ - this.lastId, - required this.limit, - required this.updatedUntil, - this.userId, - }); - - /// Last asset ID (pagination) - /// - /// Please note: This property should have been non-nullable! Since the specification file - /// does not include a default value (using the "default:" property), however, the generated - /// source code must fall back to having a nullable type. - /// Consider adding a "default:" property in the specification file to hide this note. - /// - String? lastId; - - /// Maximum number of assets to return - /// - /// Minimum value: 1 - /// Maximum value: 9007199254740991 - int limit; - - /// Sync assets updated until this date - DateTime updatedUntil; - - /// Filter by user ID - /// - /// Please note: This property should have been non-nullable! Since the specification file - /// does not include a default value (using the "default:" property), however, the generated - /// source code must fall back to having a nullable type. - /// Consider adding a "default:" property in the specification file to hide this note. - /// - String? userId; - - @override - bool operator ==(Object other) => identical(this, other) || other is AssetFullSyncDto && - other.lastId == lastId && - other.limit == limit && - other.updatedUntil == updatedUntil && - other.userId == userId; - - @override - int get hashCode => - // ignore: unnecessary_parenthesis - (lastId == null ? 0 : lastId!.hashCode) + - (limit.hashCode) + - (updatedUntil.hashCode) + - (userId == null ? 0 : userId!.hashCode); - - @override - String toString() => 'AssetFullSyncDto[lastId=$lastId, limit=$limit, updatedUntil=$updatedUntil, userId=$userId]'; - - Map toJson() { - final json = {}; - if (this.lastId != null) { - json[r'lastId'] = this.lastId; - } else { - // json[r'lastId'] = null; - } - json[r'limit'] = this.limit; - json[r'updatedUntil'] = _isEpochMarker(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))$/') - ? this.updatedUntil.millisecondsSinceEpoch - : this.updatedUntil.toUtc().toIso8601String(); - if (this.userId != null) { - json[r'userId'] = this.userId; - } else { - // json[r'userId'] = null; - } - return json; - } - - /// Returns a new [AssetFullSyncDto] instance and imports its values from - /// [value] if it's a [Map], null otherwise. - // ignore: prefer_constructors_over_static_methods - static AssetFullSyncDto? fromJson(dynamic value) { - upgradeDto(value, "AssetFullSyncDto"); - if (value is Map) { - final json = value.cast(); - - return AssetFullSyncDto( - lastId: mapValueOfType(json, r'lastId'), - limit: mapValueOfType(json, r'limit')!, - updatedUntil: mapDateTime(json, r'updatedUntil', 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))$/')!, - userId: mapValueOfType(json, r'userId'), - ); - } - return null; - } - - static List listFromJson(dynamic json, {bool growable = false,}) { - final result = []; - if (json is List && json.isNotEmpty) { - for (final row in json) { - final value = AssetFullSyncDto.fromJson(row); - if (value != null) { - result.add(value); - } - } - } - return result.toList(growable: growable); - } - - static Map mapFromJson(dynamic json) { - final map = {}; - if (json is Map && json.isNotEmpty) { - json = json.cast(); // ignore: parameter_assignments - for (final entry in json.entries) { - final value = AssetFullSyncDto.fromJson(entry.value); - if (value != null) { - map[entry.key] = value; - } - } - } - return map; - } - - // maps a json object with a list of AssetFullSyncDto-objects as value to a dart map - static Map> mapListFromJson(dynamic json, {bool growable = false,}) { - final map = >{}; - if (json is Map && json.isNotEmpty) { - // ignore: parameter_assignments - json = json.cast(); - for (final entry in json.entries) { - map[entry.key] = AssetFullSyncDto.listFromJson(entry.value, growable: growable,); - } - } - return map; - } - - /// The list of required keys that must be present in a JSON. - static const requiredKeys = { - 'limit', - 'updatedUntil', - }; -} - diff --git a/mobile/openapi/lib/model/job_name.dart b/mobile/openapi/lib/model/job_name.dart index 96b9339b7d..08f70569f8 100644 --- a/mobile/openapi/lib/model/job_name.dart +++ b/mobile/openapi/lib/model/job_name.dart @@ -38,7 +38,6 @@ class JobName { static const assetFileMigration = JobName._(r'AssetFileMigration'); static const assetGenerateThumbnailsQueueAll = JobName._(r'AssetGenerateThumbnailsQueueAll'); static const assetGenerateThumbnails = JobName._(r'AssetGenerateThumbnails'); - static const auditLogCleanup = JobName._(r'AuditLogCleanup'); static const auditTableCleanup = JobName._(r'AuditTableCleanup'); static const databaseBackup = JobName._(r'DatabaseBackup'); static const facialRecognitionQueueAll = JobName._(r'FacialRecognitionQueueAll'); @@ -97,7 +96,6 @@ class JobName { assetFileMigration, assetGenerateThumbnailsQueueAll, assetGenerateThumbnails, - auditLogCleanup, auditTableCleanup, databaseBackup, facialRecognitionQueueAll, @@ -191,7 +189,6 @@ class JobNameTypeTransformer { case r'AssetFileMigration': return JobName.assetFileMigration; case r'AssetGenerateThumbnailsQueueAll': return JobName.assetGenerateThumbnailsQueueAll; case r'AssetGenerateThumbnails': return JobName.assetGenerateThumbnails; - case r'AuditLogCleanup': return JobName.auditLogCleanup; case r'AuditTableCleanup': return JobName.auditTableCleanup; case r'DatabaseBackup': return JobName.databaseBackup; case r'FacialRecognitionQueueAll': return JobName.facialRecognitionQueueAll; diff --git a/open-api/immich-openapi-specs.json b/open-api/immich-openapi-specs.json index 8eb172c1fb..1b2e6af587 100644 --- a/open-api/immich-openapi-specs.json +++ b/open-api/immich-openapi-specs.json @@ -12294,123 +12294,6 @@ "x-immich-state": "Stable" } }, - "/sync/delta-sync": { - "post": { - "deprecated": true, - "description": "Retrieve changed assets since the last sync for the authenticated user.", - "operationId": "getDeltaSync", - "parameters": [], - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/AssetDeltaSyncDto" - } - } - }, - "required": true - }, - "responses": { - "200": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/AssetDeltaSyncResponseDto" - } - } - }, - "description": "" - } - }, - "security": [ - { - "bearer": [] - }, - { - "cookie": [] - }, - { - "api_key": [] - } - ], - "summary": "Get delta sync for user", - "tags": [ - "Sync", - "Deprecated" - ], - "x-immich-history": [ - { - "version": "v1", - "state": "Added" - }, - { - "version": "v2", - "state": "Deprecated" - } - ], - "x-immich-state": "Deprecated" - } - }, - "/sync/full-sync": { - "post": { - "deprecated": true, - "description": "Retrieve all assets for a full synchronization for the authenticated user.", - "operationId": "getFullSyncForUser", - "parameters": [], - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/AssetFullSyncDto" - } - } - }, - "required": true - }, - "responses": { - "200": { - "content": { - "application/json": { - "schema": { - "items": { - "$ref": "#/components/schemas/AssetResponseDto" - }, - "type": "array" - } - } - }, - "description": "" - } - }, - "security": [ - { - "bearer": [] - }, - { - "cookie": [] - }, - { - "api_key": [] - } - ], - "summary": "Get full sync for user", - "tags": [ - "Sync", - "Deprecated" - ], - "x-immich-history": [ - { - "version": "v1", - "state": "Added" - }, - { - "version": "v2", - "state": "Deprecated" - } - ], - "x-immich-state": "Deprecated" - } - }, "/sync/stream": { "post": { "description": "Retrieve a JSON lines streamed response of changes for synchronization. This endpoint is used by the mobile app to efficiently stay up to date with changes.", @@ -16031,59 +15914,6 @@ ], "type": "object" }, - "AssetDeltaSyncDto": { - "properties": { - "updatedAfter": { - "description": "Sync assets updated after this date", - "example": "2024-01-01T00:00:00.000Z", - "format": "date-time", - "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))$", - "type": "string" - }, - "userIds": { - "description": "User IDs to sync", - "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})$", - "type": "string" - }, - "type": "array" - } - }, - "required": [ - "updatedAfter", - "userIds" - ], - "type": "object" - }, - "AssetDeltaSyncResponseDto": { - "description": "Asset delta sync response", - "properties": { - "deleted": { - "description": "Deleted asset IDs", - "items": { - "type": "string" - }, - "type": "array" - }, - "needsFullSync": { - "description": "Whether full sync is needed", - "type": "boolean" - }, - "upserted": { - "items": { - "$ref": "#/components/schemas/AssetResponseDto" - }, - "type": "array" - } - }, - "required": [ - "deleted", - "needsFullSync", - "upserted" - ], - "type": "object" - }, "AssetEditAction": { "description": "Type of edit action to perform", "enum": [ @@ -16429,40 +16259,6 @@ ], "type": "object" }, - "AssetFullSyncDto": { - "properties": { - "lastId": { - "description": "Last asset ID (pagination)", - "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" - }, - "limit": { - "description": "Maximum number of assets to return", - "maximum": 9007199254740991, - "minimum": 1, - "type": "integer" - }, - "updatedUntil": { - "description": "Sync assets updated until this date", - "example": "2024-01-01T00:00:00.000Z", - "format": "date-time", - "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))$", - "type": "string" - }, - "userId": { - "description": "Filter by user ID", - "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" - } - }, - "required": [ - "limit", - "updatedUntil" - ], - "type": "object" - }, "AssetIdErrorReason": { "description": "Error reason if failed", "enum": [ @@ -18207,7 +18003,6 @@ "AssetFileMigration", "AssetGenerateThumbnailsQueueAll", "AssetGenerateThumbnails", - "AuditLogCleanup", "AuditTableCleanup", "DatabaseBackup", "FacialRecognitionQueueAll", diff --git a/open-api/typescript-sdk/src/fetch-client.ts b/open-api/typescript-sdk/src/fetch-client.ts index de6e2383e9..a452d86b8c 100644 --- a/open-api/typescript-sdk/src/fetch-client.ts +++ b/open-api/typescript-sdk/src/fetch-client.ts @@ -2321,29 +2321,6 @@ export type SyncAckSetDto = { /** Acknowledgment IDs (max 1000) */ acks: string[]; }; -export type AssetDeltaSyncDto = { - /** Sync assets updated after this date */ - updatedAfter: string; - /** User IDs to sync */ - userIds: string[]; -}; -export type AssetDeltaSyncResponseDto = { - /** Deleted asset IDs */ - deleted: string[]; - /** Whether full sync is needed */ - needsFullSync: boolean; - upserted: AssetResponseDto[]; -}; -export type AssetFullSyncDto = { - /** Last asset ID (pagination) */ - lastId?: string; - /** Maximum number of assets to return */ - limit: number; - /** Sync assets updated until this date */ - updatedUntil: string; - /** Filter by user ID */ - userId?: string; -}; export type SyncStreamDto = { /** Reset sync state */ reset?: boolean; @@ -6094,36 +6071,6 @@ export function sendSyncAck({ syncAckSetDto }: { body: syncAckSetDto }))); } -/** - * Get delta sync for user - */ -export function getDeltaSync({ assetDeltaSyncDto }: { - assetDeltaSyncDto: AssetDeltaSyncDto; -}, opts?: Oazapfts.RequestOpts) { - return oazapfts.ok(oazapfts.fetchJson<{ - status: 200; - data: AssetDeltaSyncResponseDto; - }>("/sync/delta-sync", oazapfts.json({ - ...opts, - method: "POST", - body: assetDeltaSyncDto - }))); -} -/** - * Get full sync for user - */ -export function getFullSyncForUser({ assetFullSyncDto }: { - assetFullSyncDto: AssetFullSyncDto; -}, opts?: Oazapfts.RequestOpts) { - return oazapfts.ok(oazapfts.fetchJson<{ - status: 200; - data: AssetResponseDto[]; - }>("/sync/full-sync", oazapfts.json({ - ...opts, - method: "POST", - body: assetFullSyncDto - }))); -} /** * Stream sync changes */ @@ -7120,7 +7067,6 @@ export enum JobName { AssetFileMigration = "AssetFileMigration", AssetGenerateThumbnailsQueueAll = "AssetGenerateThumbnailsQueueAll", AssetGenerateThumbnails = "AssetGenerateThumbnails", - AuditLogCleanup = "AuditLogCleanup", AuditTableCleanup = "AuditTableCleanup", DatabaseBackup = "DatabaseBackup", FacialRecognitionQueueAll = "FacialRecognitionQueueAll", diff --git a/server/src/controllers/sync.controller.ts b/server/src/controllers/sync.controller.ts index de94738f73..c9f3fa7825 100644 --- a/server/src/controllers/sync.controller.ts +++ b/server/src/controllers/sync.controller.ts @@ -2,17 +2,8 @@ import { Body, Controller, Delete, Get, Header, HttpCode, HttpStatus, Post, Res import { ApiTags } from '@nestjs/swagger'; import { Response } from 'express'; import { Endpoint, HistoryBuilder } from 'src/decorators'; -import { AssetResponseDto } from 'src/dtos/asset-response.dto'; import { AuthDto } from 'src/dtos/auth.dto'; -import { - AssetDeltaSyncDto, - AssetDeltaSyncResponseDto, - AssetFullSyncDto, - SyncAckDeleteDto, - SyncAckDto, - SyncAckSetDto, - SyncStreamDto, -} from 'src/dtos/sync.dto'; +import { SyncAckDeleteDto, SyncAckDto, SyncAckSetDto, SyncStreamDto } from 'src/dtos/sync.dto'; import { ApiTag, Permission } from 'src/enum'; import { Auth, Authenticated } from 'src/middleware/auth.guard'; import { GlobalExceptionFilter } from 'src/middleware/global-exception.filter'; @@ -26,30 +17,6 @@ export class SyncController { private errorService: GlobalExceptionFilter, ) {} - @Post('full-sync') - @Authenticated() - @HttpCode(HttpStatus.OK) - @Endpoint({ - summary: 'Get full sync for user', - description: 'Retrieve all assets for a full synchronization for the authenticated user.', - history: new HistoryBuilder().added('v1').deprecated('v2'), - }) - getFullSyncForUser(@Auth() auth: AuthDto, @Body() dto: AssetFullSyncDto): Promise { - return this.service.getFullSync(auth, dto); - } - - @Post('delta-sync') - @Authenticated() - @HttpCode(HttpStatus.OK) - @Endpoint({ - summary: 'Get delta sync for user', - description: 'Retrieve changed assets since the last sync for the authenticated user.', - history: new HistoryBuilder().added('v1').deprecated('v2'), - }) - getDeltaSync(@Auth() auth: AuthDto, @Body() dto: AssetDeltaSyncDto): Promise { - return this.service.getDeltaSync(auth, dto); - } - @Post('stream') @Authenticated({ permission: Permission.SyncStream }) @Header('Content-Type', 'application/jsonlines+json') diff --git a/server/src/dtos/sync.dto.ts b/server/src/dtos/sync.dto.ts index d7903ebb0c..f6383d0a5d 100644 --- a/server/src/dtos/sync.dto.ts +++ b/server/src/dtos/sync.dto.ts @@ -1,6 +1,5 @@ /* eslint-disable @typescript-eslint/no-unsafe-function-type */ import { createZodDto } from 'nestjs-zod'; -import { AssetResponseSchema } from 'src/dtos/asset-response.dto'; import { AssetEditActionSchema } from 'src/dtos/editing.dto'; import { AlbumUserRoleSchema, @@ -17,36 +16,6 @@ import { import { isoDatetimeToDate } from 'src/validation'; import z from 'zod'; -const AssetFullSyncSchema = z - .object({ - lastId: z.uuidv4().optional().describe('Last asset ID (pagination)'), - updatedUntil: isoDatetimeToDate.describe('Sync assets updated until this date'), - limit: z.int().min(1).describe('Maximum number of assets to return'), - userId: z.uuidv4().optional().describe('Filter by user ID'), - }) - .meta({ id: 'AssetFullSyncDto' }); - -const AssetDeltaSyncSchema = z - .object({ - updatedAfter: isoDatetimeToDate.describe('Sync assets updated after this date'), - userIds: z.array(z.uuidv4()).describe('User IDs to sync'), - }) - .meta({ id: 'AssetDeltaSyncDto' }); - -export class AssetFullSyncDto extends createZodDto(AssetFullSyncSchema) {} -export class AssetDeltaSyncDto extends createZodDto(AssetDeltaSyncSchema) {} - -const AssetDeltaSyncResponseSchema = z - .object({ - needsFullSync: z.boolean().describe('Whether full sync is needed'), - upserted: z.array(AssetResponseSchema), - deleted: z.array(z.string()).describe('Deleted asset IDs'), - }) - .describe('Asset delta sync response') - .meta({ id: 'AssetDeltaSyncResponseDto' }); - -export class AssetDeltaSyncResponseDto extends createZodDto(AssetDeltaSyncResponseSchema) {} - export const extraSyncModels: Function[] = []; const ExtraModel = (): ClassDecorator => { diff --git a/server/src/enum.ts b/server/src/enum.ts index 067b5435e4..08d710e08c 100644 --- a/server/src/enum.ts +++ b/server/src/enum.ts @@ -88,21 +88,6 @@ export enum AssetOrder { export const AssetOrderSchema = z.enum(AssetOrder).describe('Asset sort order').meta({ id: 'AssetOrder' }); -export enum DatabaseAction { - Create = 'CREATE', - Update = 'UPDATE', - Delete = 'DELETE', -} - -export const DatabaseActionSchema = z.enum(DatabaseAction).describe('Database action').meta({ id: 'DatabaseAction' }); - -export enum EntityType { - Asset = 'ASSET', - Album = 'ALBUM', -} - -export const EntityTypeSchema = z.enum(EntityType).describe('Entity type').meta({ id: 'EntityType' }); - export enum MemoryType { /** pictures taken on this day X years ago */ OnThisDay = 'on_this_day', @@ -761,7 +746,6 @@ export enum JobName { AssetGenerateThumbnailsQueueAll = 'AssetGenerateThumbnailsQueueAll', AssetGenerateThumbnails = 'AssetGenerateThumbnails', - AuditLogCleanup = 'AuditLogCleanup', AuditTableCleanup = 'AuditTableCleanup', DatabaseBackup = 'DatabaseBackup', diff --git a/server/src/queries/asset.repository.sql b/server/src/queries/asset.repository.sql index a68fde2b93..88cd9c08d5 100644 --- a/server/src/queries/asset.repository.sql +++ b/server/src/queries/asset.repository.sql @@ -497,63 +497,6 @@ where limit $5 --- AssetRepository.getAllForUserFullSync -select - "asset".*, - to_json("asset_exif") as "exifInfo", - to_json("stacked_assets") as "stack" -from - "asset" - left join "asset_exif" on "asset"."id" = "asset_exif"."assetId" - left join "stack" on "stack"."id" = "asset"."stackId" - left join lateral ( - select - "stack".*, - count("stacked") as "assetCount" - from - "asset" as "stacked" - where - "stacked"."stackId" = "stack"."id" - group by - "stack"."id" - ) as "stacked_assets" on "stack"."id" is not null -where - "asset"."ownerId" = $1::uuid - and "asset"."visibility" != $2 - and "asset"."updatedAt" <= $3 - and "asset"."id" > $4 -order by - "asset"."id" -limit - $5 - --- AssetRepository.getChangedDeltaSync -select - "asset".*, - to_json("asset_exif") as "exifInfo", - to_json("stacked_assets") as "stack" -from - "asset" - left join "asset_exif" on "asset"."id" = "asset_exif"."assetId" - left join "stack" on "stack"."id" = "asset"."stackId" - left join lateral ( - select - "stack".*, - count("stacked") as "assetCount" - from - "asset" as "stacked" - where - "stacked"."stackId" = "stack"."id" - group by - "stack"."id" - ) as "stacked_assets" on "stack"."id" is not null -where - "asset"."ownerId" = any ($1::uuid[]) - and "asset"."visibility" != $2 - and "asset"."updatedAt" > $3 -limit - $4 - -- AssetRepository.detectOfflineExternalAssets update "asset" set diff --git a/server/src/queries/audit.repository.sql b/server/src/queries/audit.repository.sql deleted file mode 100644 index b1a10abf48..0000000000 --- a/server/src/queries/audit.repository.sql +++ /dev/null @@ -1,16 +0,0 @@ --- NOTE: This file is auto generated by ./sql-generator - --- AuditRepository.getAfter -select distinct - on ("audit"."entityId", "audit"."entityType") "audit"."entityId" -from - "audit" -where - "audit"."createdAt" > $1 - and "audit"."action" = $2 - and "audit"."entityType" = $3 - and "audit"."ownerId" in ($4) -order by - "audit"."entityId" desc, - "audit"."entityType" desc, - "audit"."createdAt" desc diff --git a/server/src/repositories/asset.repository.ts b/server/src/repositories/asset.repository.ts index 932f72daae..686a0345a4 100644 --- a/server/src/repositories/asset.repository.ts +++ b/server/src/repositories/asset.repository.ts @@ -106,19 +106,6 @@ interface AssetExploreFieldOptions { minAssetsPerField: number; } -interface AssetFullSyncOptions { - ownerId: string; - lastId?: string; - updatedUntil: Date; - limit: number; -} - -interface AssetDeltaSyncOptions { - userIds: string[]; - updatedAfter: Date; - limit: number; -} - interface AssetGetByChecksumOptions { ownerId: string; checksum: Buffer; @@ -905,70 +892,6 @@ export class AssetRepository { return { fieldName: 'exifInfo.city', items }; } - @GenerateSql({ - params: [ - { - ownerId: DummyValue.UUID, - lastId: DummyValue.UUID, - updatedUntil: DummyValue.DATE, - limit: 10, - }, - ], - }) - getAllForUserFullSync(options: AssetFullSyncOptions) { - const { ownerId, lastId, updatedUntil, limit } = options; - return this.db - .selectFrom('asset') - .selectAll('asset') - .$call(withExif) - .leftJoin('stack', 'stack.id', 'asset.stackId') - .leftJoinLateral( - (eb) => - eb - .selectFrom('asset as stacked') - .selectAll('stack') - .select((eb) => eb.fn.count(eb.table('stacked')).as('assetCount')) - .whereRef('stacked.stackId', '=', 'stack.id') - .groupBy('stack.id') - .as('stacked_assets'), - (join) => join.on('stack.id', 'is not', null), - ) - .select((eb) => eb.fn.toJson(eb.table('stacked_assets')).$castTo().as('stack')) - .where('asset.ownerId', '=', asUuid(ownerId)) - .where('asset.visibility', '!=', AssetVisibility.Hidden) - .where('asset.updatedAt', '<=', updatedUntil) - .$if(!!lastId, (qb) => qb.where('asset.id', '>', lastId!)) - .orderBy('asset.id') - .limit(limit) - .execute(); - } - - @GenerateSql({ params: [{ userIds: [DummyValue.UUID], updatedAfter: DummyValue.DATE, limit: 100 }] }) - async getChangedDeltaSync(options: AssetDeltaSyncOptions) { - return this.db - .selectFrom('asset') - .selectAll('asset') - .$call(withExif) - .leftJoin('stack', 'stack.id', 'asset.stackId') - .leftJoinLateral( - (eb) => - eb - .selectFrom('asset as stacked') - .selectAll('stack') - .select((eb) => eb.fn.count(eb.table('stacked')).as('assetCount')) - .whereRef('stacked.stackId', '=', 'stack.id') - .groupBy('stack.id') - .as('stacked_assets'), - (join) => join.on('stack.id', 'is not', null), - ) - .select((eb) => eb.fn.toJson(eb.table('stacked_assets').$castTo()).as('stack')) - .where('asset.ownerId', '=', anyUuid(options.userIds)) - .where('asset.visibility', '!=', AssetVisibility.Hidden) - .where('asset.updatedAt', '>', options.updatedAfter) - .limit(options.limit) - .execute(); - } - async upsertFile( file: Pick< Insertable, diff --git a/server/src/repositories/audit.repository.ts b/server/src/repositories/audit.repository.ts deleted file mode 100644 index 2d56eddc9a..0000000000 --- a/server/src/repositories/audit.repository.ts +++ /dev/null @@ -1,44 +0,0 @@ -import { Injectable } from '@nestjs/common'; -import { Kysely } from 'kysely'; -import { InjectKysely } from 'nestjs-kysely'; -import { DummyValue, GenerateSql } from 'src/decorators'; -import { DatabaseAction, EntityType } from 'src/enum'; -import { DB } from 'src/schema'; - -export interface AuditSearch { - action?: DatabaseAction; - entityType?: EntityType; - userIds: string[]; -} - -@Injectable() -export class AuditRepository { - constructor(@InjectKysely() private db: Kysely) {} - - @GenerateSql({ - params: [ - DummyValue.DATE, - { action: DatabaseAction.Create, entityType: EntityType.Asset, userIds: [DummyValue.UUID] }, - ], - }) - async getAfter(since: Date, options: AuditSearch): Promise { - const records = await this.db - .selectFrom('audit') - .where('audit.createdAt', '>', since) - .$if(!!options.action, (qb) => qb.where('audit.action', '=', options.action!)) - .$if(!!options.entityType, (qb) => qb.where('audit.entityType', '=', options.entityType!)) - .where('audit.ownerId', 'in', options.userIds) - .distinctOn(['audit.entityId', 'audit.entityType']) - .orderBy('audit.entityId', 'desc') - .orderBy('audit.entityType', 'desc') - .orderBy('audit.createdAt', 'desc') - .select('audit.entityId') - .execute(); - - return records.map(({ entityId }) => entityId); - } - - async removeBefore(before: Date): Promise { - await this.db.deleteFrom('audit').where('createdAt', '<', before).execute(); - } -} diff --git a/server/src/repositories/index.ts b/server/src/repositories/index.ts index 361a2e7179..fcff171a5e 100644 --- a/server/src/repositories/index.ts +++ b/server/src/repositories/index.ts @@ -7,7 +7,6 @@ import { AppRepository } from 'src/repositories/app.repository'; import { AssetEditRepository } from 'src/repositories/asset-edit.repository'; import { AssetJobRepository } from 'src/repositories/asset-job.repository'; import { AssetRepository } from 'src/repositories/asset.repository'; -import { AuditRepository } from 'src/repositories/audit.repository'; import { ConfigRepository } from 'src/repositories/config.repository'; import { CronRepository } from 'src/repositories/cron.repository'; import { CryptoRepository } from 'src/repositories/crypto.repository'; @@ -56,7 +55,6 @@ export const repositories = [ ActivityRepository, AlbumRepository, AlbumUserRepository, - AuditRepository, ApiKeyRepository, AppRepository, AssetRepository, diff --git a/server/src/schema/index.ts b/server/src/schema/index.ts index 2426c2aab7..e3db3d01c7 100644 --- a/server/src/schema/index.ts +++ b/server/src/schema/index.ts @@ -40,7 +40,6 @@ import { AssetMetadataAuditTable } from 'src/schema/tables/asset-metadata-audit. import { AssetMetadataTable } from 'src/schema/tables/asset-metadata.table'; import { AssetOcrTable } from 'src/schema/tables/asset-ocr.table'; import { AssetTable } from 'src/schema/tables/asset.table'; -import { AuditTable } from 'src/schema/tables/audit.table'; import { FaceSearchTable } from 'src/schema/tables/face-search.table'; import { GeodataPlacesTable } from 'src/schema/tables/geodata-places.table'; import { LibraryTable } from 'src/schema/tables/library.table'; @@ -98,7 +97,6 @@ export class ImmichDatabase { AssetOcrTable, AssetTable, AssetFileTable, - AuditTable, AssetExifTable, FaceSearchTable, GeodataPlacesTable, @@ -197,8 +195,6 @@ export interface DB { asset_ocr: AssetOcrTable; ocr_search: OcrSearchTable; - audit: AuditTable; - face_search: FaceSearchTable; geodata_places: GeodataPlacesTable; diff --git a/server/src/schema/migrations/1776217577402-DropAuditTable.ts b/server/src/schema/migrations/1776217577402-DropAuditTable.ts new file mode 100644 index 0000000000..f7e968a3ed --- /dev/null +++ b/server/src/schema/migrations/1776217577402-DropAuditTable.ts @@ -0,0 +1,18 @@ +import { Kysely, sql } from 'kysely'; + +export async function up(db: Kysely): Promise { + await sql`DROP TABLE "audit";`.execute(db); +} + +export async function down(db: Kysely): Promise { + await sql`CREATE TABLE "audit" ( + "id" serial NOT NULL, + "entityType" character varying NOT NULL, + "entityId" uuid NOT NULL, + "action" character varying NOT NULL, + "ownerId" uuid NOT NULL, + "createdAt" timestamp with time zone NOT NULL DEFAULT now(), + CONSTRAINT "audit_pkey" PRIMARY KEY ("id") +);`.execute(db); + await sql`CREATE INDEX "audit_ownerId_createdAt_idx" ON "audit" ("ownerId", "createdAt");`.execute(db); +} diff --git a/server/src/schema/tables/audit.table.ts b/server/src/schema/tables/audit.table.ts deleted file mode 100644 index 78c9a57c09..0000000000 --- a/server/src/schema/tables/audit.table.ts +++ /dev/null @@ -1,24 +0,0 @@ -import { Column, CreateDateColumn, Generated, Index, PrimaryColumn, Table, Timestamp } from '@immich/sql-tools'; -import { DatabaseAction, EntityType } from 'src/enum'; - -@Table('audit') -@Index({ columns: ['ownerId', 'createdAt'] }) -export class AuditTable { - @PrimaryColumn({ type: 'serial', synchronize: false }) - id!: Generated; - - @Column() - entityType!: EntityType; - - @Column({ type: 'uuid' }) - entityId!: string; - - @Column() - action!: DatabaseAction; - - @Column({ type: 'uuid' }) - ownerId!: string; - - @CreateDateColumn() - createdAt!: Generated; -} diff --git a/server/src/services/audit.service.spec.ts b/server/src/services/audit.service.spec.ts deleted file mode 100644 index 7363ea74e1..0000000000 --- a/server/src/services/audit.service.spec.ts +++ /dev/null @@ -1,26 +0,0 @@ -import { JobStatus } from 'src/enum'; -import { AuditService } from 'src/services/audit.service'; -import { newTestService, ServiceMocks } from 'test/utils'; - -describe(AuditService.name, () => { - let sut: AuditService; - let mocks: ServiceMocks; - - beforeEach(() => { - ({ sut, mocks } = newTestService(AuditService)); - }); - - it('should work', () => { - expect(sut).toBeDefined(); - }); - - describe('handleCleanup', () => { - it('should delete old audit entries', async () => { - mocks.audit.removeBefore.mockResolvedValue(); - - await expect(sut.handleCleanup()).resolves.toBe(JobStatus.Success); - - expect(mocks.audit.removeBefore).toHaveBeenCalledWith(expect.any(Date)); - }); - }); -}); diff --git a/server/src/services/audit.service.ts b/server/src/services/audit.service.ts deleted file mode 100644 index 498d99b82c..0000000000 --- a/server/src/services/audit.service.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { Injectable } from '@nestjs/common'; -import { DateTime } from 'luxon'; -import { AUDIT_LOG_MAX_DURATION } from 'src/constants'; -import { OnJob } from 'src/decorators'; -import { JobName, JobStatus, QueueName } from 'src/enum'; -import { BaseService } from 'src/services/base.service'; - -@Injectable() -export class AuditService extends BaseService { - @OnJob({ name: JobName.AuditLogCleanup, queue: QueueName.BackgroundTask }) - async handleCleanup(): Promise { - await this.auditRepository.removeBefore(DateTime.now().minus(AUDIT_LOG_MAX_DURATION).toJSDate()); - return JobStatus.Success; - } -} diff --git a/server/src/services/base.service.ts b/server/src/services/base.service.ts index b3a50a07ae..4b02d6e944 100644 --- a/server/src/services/base.service.ts +++ b/server/src/services/base.service.ts @@ -14,7 +14,6 @@ import { AppRepository } from 'src/repositories/app.repository'; import { AssetEditRepository } from 'src/repositories/asset-edit.repository'; import { AssetJobRepository } from 'src/repositories/asset-job.repository'; import { AssetRepository } from 'src/repositories/asset.repository'; -import { AuditRepository } from 'src/repositories/audit.repository'; import { ConfigRepository } from 'src/repositories/config.repository'; import { CronRepository } from 'src/repositories/cron.repository'; import { CryptoRepository } from 'src/repositories/crypto.repository'; @@ -72,7 +71,6 @@ export const BASE_SERVICE_DEPENDENCIES = [ AssetRepository, AssetEditRepository, AssetJobRepository, - AuditRepository, ConfigRepository, CronRepository, CryptoRepository, @@ -131,7 +129,6 @@ export class BaseService { protected assetRepository: AssetRepository, protected assetEditRepository: AssetEditRepository, protected assetJobRepository: AssetJobRepository, - protected auditRepository: AuditRepository, protected configRepository: ConfigRepository, protected cronRepository: CronRepository, protected cryptoRepository: CryptoRepository, diff --git a/server/src/services/index.ts b/server/src/services/index.ts index ba54474b71..f4e82b13a4 100644 --- a/server/src/services/index.ts +++ b/server/src/services/index.ts @@ -4,7 +4,6 @@ import { ApiKeyService } from 'src/services/api-key.service'; import { ApiService } from 'src/services/api.service'; import { AssetMediaService } from 'src/services/asset-media.service'; import { AssetService } from 'src/services/asset.service'; -import { AuditService } from 'src/services/audit.service'; import { AuthAdminService } from 'src/services/auth-admin.service'; import { AuthService } from 'src/services/auth.service'; import { CliService } from 'src/services/cli.service'; @@ -54,7 +53,6 @@ export const services = [ ApiService, AssetMediaService, AssetService, - AuditService, AuthService, AuthAdminService, CliService, diff --git a/server/src/services/queue.service.spec.ts b/server/src/services/queue.service.spec.ts index 2c76fee877..d4c425e8bd 100644 --- a/server/src/services/queue.service.spec.ts +++ b/server/src/services/queue.service.spec.ts @@ -42,7 +42,6 @@ describe(QueueService.name, () => { { name: JobName.MemoryCleanup }, { name: JobName.SessionCleanup }, { name: JobName.AuditTableCleanup }, - { name: JobName.AuditLogCleanup }, { name: JobName.MemoryGenerate }, { name: JobName.UserSyncUsage }, { name: JobName.AssetGenerateThumbnailsQueueAll, data: { force: false } }, diff --git a/server/src/services/queue.service.ts b/server/src/services/queue.service.ts index 662ccbe618..ba6f4c5f3b 100644 --- a/server/src/services/queue.service.ts +++ b/server/src/services/queue.service.ts @@ -270,7 +270,6 @@ export class QueueService extends BaseService { { name: JobName.MemoryCleanup }, { name: JobName.SessionCleanup }, { name: JobName.AuditTableCleanup }, - { name: JobName.AuditLogCleanup }, ); } diff --git a/server/src/services/sync.service.spec.ts b/server/src/services/sync.service.spec.ts deleted file mode 100644 index 234e3ac223..0000000000 --- a/server/src/services/sync.service.spec.ts +++ /dev/null @@ -1,98 +0,0 @@ -import { mapAsset } from 'src/dtos/asset-response.dto'; -import { SyncService } from 'src/services/sync.service'; -import { AssetFactory } from 'test/factories/asset.factory'; -import { PartnerFactory } from 'test/factories/partner.factory'; -import { authStub } from 'test/fixtures/auth.stub'; -import { getForAsset, getForPartner } from 'test/mappers'; -import { factory } from 'test/small.factory'; -import { newTestService, ServiceMocks } from 'test/utils'; - -const untilDate = new Date(2024); -const mapAssetOpts = { auth: authStub.user1, stripMetadata: false, withStack: true }; - -describe(SyncService.name, () => { - let sut: SyncService; - let mocks: ServiceMocks; - - beforeEach(() => { - ({ sut, mocks } = newTestService(SyncService)); - }); - - it('should exist', () => { - expect(sut).toBeDefined(); - }); - - describe('getAllAssetsForUserFullSync', () => { - it('should return a list of all assets owned by the user', async () => { - const [asset1, asset2] = [ - AssetFactory.from({ libraryId: 'library-id', isExternal: true }).owner(authStub.user1.user).build(), - AssetFactory.from().owner(authStub.user1.user).build(), - ]; - mocks.asset.getAllForUserFullSync.mockResolvedValue([getForAsset(asset1), getForAsset(asset2)]); - await expect(sut.getFullSync(authStub.user1, { limit: 2, updatedUntil: untilDate })).resolves.toEqual([ - mapAsset(getForAsset(asset1), mapAssetOpts), - mapAsset(getForAsset(asset2), mapAssetOpts), - ]); - expect(mocks.asset.getAllForUserFullSync).toHaveBeenCalledWith({ - ownerId: authStub.user1.user.id, - updatedUntil: untilDate, - limit: 2, - }); - }); - }); - - describe('getChangesForDeltaSync', () => { - it('should return a response requiring a full sync when partners are out of sync', async () => { - const partner = PartnerFactory.create(); - const auth = factory.auth({ user: { id: partner.sharedWithId } }); - - mocks.partner.getAll.mockResolvedValue([getForPartner(partner)]); - - await expect( - sut.getDeltaSync(authStub.user1, { updatedAfter: new Date(), userIds: [auth.user.id] }), - ).resolves.toEqual({ needsFullSync: true, upserted: [], deleted: [] }); - - expect(mocks.asset.getChangedDeltaSync).toHaveBeenCalledTimes(0); - expect(mocks.audit.getAfter).toHaveBeenCalledTimes(0); - }); - - it('should return a response requiring a full sync when last sync was too long ago', async () => { - mocks.partner.getAll.mockResolvedValue([]); - await expect( - sut.getDeltaSync(authStub.user1, { updatedAfter: new Date(2000), userIds: [authStub.user1.user.id] }), - ).resolves.toEqual({ needsFullSync: true, upserted: [], deleted: [] }); - expect(mocks.asset.getChangedDeltaSync).toHaveBeenCalledTimes(0); - expect(mocks.audit.getAfter).toHaveBeenCalledTimes(0); - }); - - it('should return a response requiring a full sync when there are too many changes', async () => { - const asset = AssetFactory.create(); - mocks.partner.getAll.mockResolvedValue([]); - mocks.asset.getChangedDeltaSync.mockResolvedValue( - Array.from>({ length: 10_000 }).fill(getForAsset(asset)), - ); - await expect( - sut.getDeltaSync(authStub.user1, { updatedAfter: new Date(), userIds: [authStub.user1.user.id] }), - ).resolves.toEqual({ needsFullSync: true, upserted: [], deleted: [] }); - expect(mocks.asset.getChangedDeltaSync).toHaveBeenCalledTimes(1); - expect(mocks.audit.getAfter).toHaveBeenCalledTimes(0); - }); - - it('should return a response with changes and deletions', async () => { - const asset = AssetFactory.create({ ownerId: authStub.user1.user.id }); - const deletedAsset = AssetFactory.create({ libraryId: 'library-id', isExternal: true }); - mocks.partner.getAll.mockResolvedValue([]); - mocks.asset.getChangedDeltaSync.mockResolvedValue([getForAsset(asset)]); - mocks.audit.getAfter.mockResolvedValue([deletedAsset.id]); - await expect( - sut.getDeltaSync(authStub.user1, { updatedAfter: new Date(), userIds: [authStub.user1.user.id] }), - ).resolves.toEqual({ - needsFullSync: false, - upserted: [mapAsset(getForAsset(asset), mapAssetOpts)], - deleted: [deletedAsset.id], - }); - expect(mocks.asset.getChangedDeltaSync).toHaveBeenCalledTimes(1); - expect(mocks.audit.getAfter).toHaveBeenCalledTimes(1); - }); - }); -}); diff --git a/server/src/services/sync.service.ts b/server/src/services/sync.service.ts index 9bdeca14d7..50bf9368c8 100644 --- a/server/src/services/sync.service.ts +++ b/server/src/services/sync.service.ts @@ -2,14 +2,9 @@ import { BadRequestException, ForbiddenException, Injectable } from '@nestjs/com import { Insertable } from 'kysely'; import { DateTime, Duration } from 'luxon'; import { Writable } from 'node:stream'; -import { AUDIT_LOG_MAX_DURATION } from 'src/constants'; import { OnJob } from 'src/decorators'; -import { AssetResponseDto, mapAsset } from 'src/dtos/asset-response.dto'; import { AuthDto } from 'src/dtos/auth.dto'; import { - AssetDeltaSyncDto, - AssetDeltaSyncResponseDto, - AssetFullSyncDto, SyncAckDeleteDto, SyncAckSetDto, syncAssetFaceV2ToV1, @@ -17,23 +12,12 @@ import { SyncItem, SyncStreamDto, } from 'src/dtos/sync.dto'; -import { - AssetVisibility, - DatabaseAction, - EntityType, - JobName, - Permission, - QueueName, - SyncEntityType, - SyncRequestType, -} from 'src/enum'; +import { JobName, QueueName, SyncEntityType, SyncRequestType } from 'src/enum'; import { SyncQueryOptions } from 'src/repositories/sync.repository'; import { SessionSyncCheckpointTable } from 'src/schema/tables/sync-checkpoint.table'; import { BaseService } from 'src/services/base.service'; import { SyncAck } from 'src/types'; -import { getMyPartnerIds } from 'src/utils/asset.util'; import { hexOrBufferToBase64 } from 'src/utils/bytes'; -import { setIsEqual } from 'src/utils/set'; import { fromAck, serialize, SerializeOptions, toAck } from 'src/utils/sync'; type CheckpointMap = Partial>; @@ -66,7 +50,6 @@ const sendEntityBackfillCompleteAck = (response: Writable, ackType: SyncEntityTy send(response, { type: SyncEntityType.SyncAckV1, data: {}, ackType, ids: [id, COMPLETE_ID] }); }; -const FULL_SYNC = { needsFullSync: true, deleted: [], upserted: [] }; export const SYNC_TYPES_ORDER = [ SyncRequestType.AuthUsersV1, SyncRequestType.UsersV1, @@ -887,68 +870,4 @@ export class SyncService extends BaseService { }, ]); } - - async getFullSync(auth: AuthDto, dto: AssetFullSyncDto): Promise { - // mobile implementation is faster if this is a single id - const userId = dto.userId || auth.user.id; - await this.requireAccess({ auth, permission: Permission.TimelineRead, ids: [userId] }); - const assets = await this.assetRepository.getAllForUserFullSync({ - ownerId: userId, - updatedUntil: dto.updatedUntil, - lastId: dto.lastId, - limit: dto.limit, - }); - return assets.map((a) => mapAsset(a, { auth, stripMetadata: false, withStack: true })); - } - - async getDeltaSync(auth: AuthDto, dto: AssetDeltaSyncDto): Promise { - // app has not synced in the last 100 days - const duration = DateTime.now().diff(DateTime.fromJSDate(dto.updatedAfter)); - if (duration > AUDIT_LOG_MAX_DURATION) { - return FULL_SYNC; - } - - // app does not have the correct partners synced - const partnerIds = await getMyPartnerIds({ userId: auth.user.id, repository: this.partnerRepository }); - const userIds = [auth.user.id, ...partnerIds]; - if (!setIsEqual(new Set(userIds), new Set(dto.userIds))) { - return FULL_SYNC; - } - - await this.requireAccess({ auth, permission: Permission.TimelineRead, ids: dto.userIds }); - - const limit = 10_000; - const upserted = await this.assetRepository.getChangedDeltaSync({ limit, updatedAfter: dto.updatedAfter, userIds }); - - // too many changes, need to do a full sync - if (upserted.length === limit) { - return FULL_SYNC; - } - - const deleted = await this.auditRepository.getAfter(dto.updatedAfter, { - userIds, - entityType: EntityType.Asset, - action: DatabaseAction.Delete, - }); - - const result = { - needsFullSync: false, - upserted: upserted - // do not return archived assets for partner users - .filter( - (a) => - a.ownerId === auth.user.id || (a.ownerId !== auth.user.id && a.visibility === AssetVisibility.Timeline), - ) - .map((a) => - mapAsset(a, { - auth, - stripMetadata: false, - // ignore stacks for non partner users - withStack: a.ownerId === auth.user.id, - }), - ), - deleted, - }; - return result; - } } diff --git a/server/src/types.ts b/server/src/types.ts index 33174e187e..f9bde47aae 100644 --- a/server/src/types.ts +++ b/server/src/types.ts @@ -351,7 +351,6 @@ export type JobItem = | { name: JobName.FileDelete; data: IDeleteFilesJob } // Cleanup - | { name: JobName.AuditLogCleanup; data?: IBaseJob } | { name: JobName.SessionCleanup; data?: IBaseJob } // Tags diff --git a/server/test/repositories/asset.repository.mock.ts b/server/test/repositories/asset.repository.mock.ts index f66de5371e..f3d6693578 100644 --- a/server/test/repositories/asset.repository.mock.ts +++ b/server/test/repositories/asset.repository.mock.ts @@ -33,8 +33,6 @@ export const newAssetRepositoryMock = (): Mocked { cron: automock(CronRepository, { args: [, loggerMock] }), crypto: newCryptoRepositoryMock(), activity: automock(ActivityRepository), - audit: automock(AuditRepository), album: automock(AlbumRepository, { strict: false }), albumUser: automock(AlbumUserRepository), asset: newAssetRepositoryMock(), @@ -373,7 +370,6 @@ export const newTestService = ( overrides.asset || (mocks.asset as As), overrides.assetEdit || (mocks.assetEdit as As), overrides.assetJob || (mocks.assetJob as As), - overrides.audit || (mocks.audit as As), overrides.config || (mocks.config as As as ConfigRepository), overrides.cron || (mocks.cron as As), overrides.crypto || (mocks.crypto as As), From 8ee5d3039ada5b9f60cea204d98e49833478ab3a Mon Sep 17 00:00:00 2001 From: Daniel Dietzler <36593685+danieldietzler@users.noreply.github.com> Date: Wed, 15 Apr 2026 21:00:33 +0200 Subject: [PATCH 05/16] chore!: remove deviceId and deviceAssetId (#27818) chore: remove deviceId and deviceAssetId --- cli/src/commands/asset.ts | 2 - docs/docs/guides/python-file-upload.md | 2 - e2e/src/specs/server/api/asset.e2e-spec.ts | 29 --- e2e/src/specs/server/api/search.e2e-spec.ts | 1 - e2e/src/specs/web/duplicates.e2e-spec.ts | 4 +- .../ui/generators/timeline/rest-response.ts | 2 - .../ui/mock-network/broken-asset-network.ts | 2 - e2e/src/utils.ts | 7 - mobile/openapi/README.md | 5 - mobile/openapi/lib/api.dart | 2 - mobile/openapi/lib/api/assets_api.dart | 144 +----------- mobile/openapi/lib/api/deprecated_api.dart | 62 ------ mobile/openapi/lib/api/search_api.dart | 15 +- mobile/openapi/lib/api_client.dart | 4 - .../openapi/lib/model/asset_response_dto.dart | 20 +- .../lib/model/check_existing_assets_dto.dart | 111 ---------- .../check_existing_assets_response_dto.dart | 102 --------- .../lib/model/metadata_search_dto.dart | 38 +--- .../openapi/lib/model/random_search_dto.dart | 20 +- .../openapi/lib/model/smart_search_dto.dart | 20 +- .../lib/model/statistics_search_dto.dart | 20 +- open-api/immich-openapi-specs.json | 205 ------------------ open-api/typescript-sdk/src/fetch-client.ts | 60 +---- .../asset-media.controller.spec.ts | 24 -- .../src/controllers/asset-media.controller.ts | 17 -- server/src/controllers/asset.controller.ts | 12 - server/src/database.ts | 4 - server/src/dtos/asset-media-response.dto.ts | 7 - server/src/dtos/asset-media.dto.ts | 10 - server/src/dtos/asset-response.dto.ts | 6 - server/src/dtos/asset.dto.ts | 7 - server/src/dtos/search.dto.ts | 2 - server/src/queries/asset.job.repository.sql | 2 - server/src/queries/asset.repository.sql | 14 +- server/src/repositories/asset.repository.ts | 35 +-- server/src/repositories/search.repository.ts | 2 - ...6263790468-DropDeviceIdAndDeviceAssetId.ts | 11 + server/src/schema/tables/asset.table.ts | 6 - .../src/services/asset-media.service.spec.ts | 15 -- server/src/services/asset-media.service.ts | 17 -- server/src/services/asset.service.spec.ts | 14 -- server/src/services/asset.service.ts | 4 - server/src/services/library.service.spec.ts | 1 - server/src/services/library.service.ts | 5 +- server/src/services/metadata.service.spec.ts | 6 - server/src/services/metadata.service.ts | 2 - server/src/utils/database.ts | 2 - server/src/utils/duplicate.spec.ts | 2 - server/test/factories/asset.factory.ts | 2 - server/test/mappers.ts | 2 - server/test/medium.factory.ts | 2 - .../services/asset-media.service.spec.ts | 14 +- .../repositories/asset.repository.mock.ts | 2 - web/src/lib/utils/file-uploader.ts | 5 +- web/src/test-data/factories/asset-factory.ts | 2 - 55 files changed, 31 insertions(+), 1102 deletions(-) delete mode 100644 mobile/openapi/lib/model/check_existing_assets_dto.dart delete mode 100644 mobile/openapi/lib/model/check_existing_assets_response_dto.dart create mode 100644 server/src/schema/migrations/1776263790468-DropDeviceIdAndDeviceAssetId.ts diff --git a/cli/src/commands/asset.ts b/cli/src/commands/asset.ts index c3ad820547..2c6430c83a 100644 --- a/cli/src/commands/asset.ts +++ b/cli/src/commands/asset.ts @@ -404,8 +404,6 @@ const uploadFile = async (input: string, stats: Stats): Promise { const { body, status } = await request(app) .post('/assets') .set('Authorization', `Bearer ${quotaUser.accessToken}`) - .field('deviceAssetId', 'example-image') - .field('deviceId', 'e2e') .field('fileCreatedAt', new Date().toISOString()) .field('fileModifiedAt', new Date().toISOString()) .attach('assetData', makeRandomImage(), 'example.jpg'); @@ -1109,8 +1107,6 @@ describe('/asset', () => { const { body, status } = await request(app) .post('/assets') .set('Authorization', `Bearer ${quotaUser.accessToken}`) - .field('deviceAssetId', 'example-image') - .field('deviceId', 'e2e') .field('fileCreatedAt', new Date().toISOString()) .field('fileModifiedAt', new Date().toISOString()) .attach('assetData', randomBytes(2014), 'example.jpg'); @@ -1164,29 +1160,4 @@ describe('/asset', () => { expect(video.checksum).toStrictEqual(checksum); }); }); - - describe('POST /assets/exist', () => { - it('ignores invalid deviceAssetIds', async () => { - const response = await utils.checkExistingAssets(user1.accessToken, { - deviceId: 'test-assets-exist', - deviceAssetIds: ['invalid', 'INVALID'], - }); - - expect(response.existingIds).toHaveLength(0); - }); - - it('returns the IDs of existing assets', async () => { - await utils.createAsset(user1.accessToken, { - deviceId: 'test-assets-exist', - deviceAssetId: 'test-asset-0', - }); - - const response = await utils.checkExistingAssets(user1.accessToken, { - deviceId: 'test-assets-exist', - deviceAssetIds: ['test-asset-0'], - }); - - expect(response.existingIds).toEqual(['test-asset-0']); - }); - }); }); diff --git a/e2e/src/specs/server/api/search.e2e-spec.ts b/e2e/src/specs/server/api/search.e2e-spec.ts index 2f6ea75f77..4ee021b1e4 100644 --- a/e2e/src/specs/server/api/search.e2e-spec.ts +++ b/e2e/src/specs/server/api/search.e2e-spec.ts @@ -74,7 +74,6 @@ describe('/search', () => { const bytes = await readFile(join(testAssetDir, filename)); assets.push( await utils.createAsset(admin.accessToken, { - deviceAssetId: `test-${filename}`, assetData: { bytes, filename }, ...dto, }), diff --git a/e2e/src/specs/web/duplicates.e2e-spec.ts b/e2e/src/specs/web/duplicates.e2e-spec.ts index 34f11cdf78..c39e9019d3 100644 --- a/e2e/src/specs/web/duplicates.e2e-spec.ts +++ b/e2e/src/specs/web/duplicates.e2e-spec.ts @@ -16,8 +16,8 @@ test.describe('Duplicates Utility', () => { test.beforeEach(async ({ context }) => { [firstAsset, secondAsset] = await Promise.all([ - utils.createAsset(admin.accessToken, { deviceAssetId: 'duplicate-a' }), - utils.createAsset(admin.accessToken, { deviceAssetId: 'duplicate-b' }), + utils.createAsset(admin.accessToken, {}), + utils.createAsset(admin.accessToken, {}), ]); await updateAssets( diff --git a/e2e/src/ui/generators/timeline/rest-response.ts b/e2e/src/ui/generators/timeline/rest-response.ts index 0c4bd06dc3..de8aa3ee05 100644 --- a/e2e/src/ui/generators/timeline/rest-response.ts +++ b/e2e/src/ui/generators/timeline/rest-response.ts @@ -315,11 +315,9 @@ export function toAssetResponseDto(asset: MockTimelineAsset, owner?: UserRespons return { id: asset.id, - deviceAssetId: `device-${asset.id}`, ownerId: asset.ownerId, owner: owner || defaultOwner, libraryId: `library-${asset.ownerId}`, - deviceId: `device-${asset.ownerId}`, type: asset.isVideo ? AssetTypeEnum.Video : AssetTypeEnum.Image, originalPath: `/original/${asset.id}.${asset.isVideo ? 'mp4' : 'jpg'}`, originalFileName: `${asset.id}.${asset.isVideo ? 'mp4' : 'jpg'}`, diff --git a/e2e/src/ui/mock-network/broken-asset-network.ts b/e2e/src/ui/mock-network/broken-asset-network.ts index eff93a8d72..75d579e1ef 100644 --- a/e2e/src/ui/mock-network/broken-asset-network.ts +++ b/e2e/src/ui/mock-network/broken-asset-network.ts @@ -16,7 +16,6 @@ export const createMockStackAsset = (ownerId: string): AssetResponseDto => { const now = new Date().toISOString(); return { id: assetId, - deviceAssetId: `device-${assetId}`, ownerId, owner: { id: ownerId, @@ -27,7 +26,6 @@ export const createMockStackAsset = (ownerId: string): AssetResponseDto => { avatarColor: 'blue' as never, }, libraryId: `library-${ownerId}`, - deviceId: `device-${ownerId}`, type: AssetTypeEnum.Image, originalPath: `/original/${assetId}.jpg`, originalFileName: `${assetId}.jpg`, diff --git a/e2e/src/utils.ts b/e2e/src/utils.ts index f9dd11a1ec..aa4c3b8499 100644 --- a/e2e/src/utils.ts +++ b/e2e/src/utils.ts @@ -3,7 +3,6 @@ import { AssetMediaResponseDto, AssetResponseDto, AssetVisibility, - CheckExistingAssetsDto, CreateAlbumDto, CreateLibraryDto, JobCreateDto, @@ -20,7 +19,6 @@ import { UserAdminCreateDto, UserPreferencesUpdateDto, ValidateLibraryDto, - checkExistingAssets, createAlbum, createApiKey, createJob, @@ -343,8 +341,6 @@ export const utils = { }, ) => { const _dto = { - deviceAssetId: 'test-1', - deviceId: 'test', fileCreatedAt: new Date().toISOString(), fileModifiedAt: new Date().toISOString(), ...dto, @@ -416,9 +412,6 @@ export const utils = { getAssetInfo: (accessToken: string, id: string) => getAssetInfo({ id }, { headers: asBearerAuth(accessToken) }), - checkExistingAssets: (accessToken: string, checkExistingAssetsDto: CheckExistingAssetsDto) => - checkExistingAssets({ checkExistingAssetsDto }, { headers: asBearerAuth(accessToken) }), - searchAssets: async (accessToken: string, dto: MetadataSearchDto) => { return searchAssets({ metadataSearchDto: dto }, { headers: asBearerAuth(accessToken) }); }, diff --git a/mobile/openapi/README.md b/mobile/openapi/README.md index c893ce33a5..e95790b020 100644 --- a/mobile/openapi/README.md +++ b/mobile/openapi/README.md @@ -96,14 +96,12 @@ Class | Method | HTTP request | Description *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 *AssetsApi* | [**checkBulkUpload**](doc//AssetsApi.md#checkbulkupload) | **POST** /assets/bulk-upload-check | Check bulk upload -*AssetsApi* | [**checkExistingAssets**](doc//AssetsApi.md#checkexistingassets) | **POST** /assets/exist | Check existing assets *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 *AssetsApi* | [**deleteAssets**](doc//AssetsApi.md#deleteassets) | **DELETE** /assets | Delete assets *AssetsApi* | [**deleteBulkAssetMetadata**](doc//AssetsApi.md#deletebulkassetmetadata) | **DELETE** /assets/metadata | Delete asset metadata *AssetsApi* | [**downloadAsset**](doc//AssetsApi.md#downloadasset) | **GET** /assets/{id}/original | Download original asset *AssetsApi* | [**editAsset**](doc//AssetsApi.md#editasset) | **PUT** /assets/{id}/edits | Apply edits to an existing asset -*AssetsApi* | [**getAllUserAssetsByDeviceId**](doc//AssetsApi.md#getalluserassetsbydeviceid) | **GET** /assets/device/{deviceId} | Retrieve assets by device ID *AssetsApi* | [**getAssetEdits**](doc//AssetsApi.md#getassetedits) | **GET** /assets/{id}/edits | Retrieve edits for an existing asset *AssetsApi* | [**getAssetInfo**](doc//AssetsApi.md#getassetinfo) | **GET** /assets/{id} | Retrieve an asset *AssetsApi* | [**getAssetMetadata**](doc//AssetsApi.md#getassetmetadata) | **GET** /assets/{id}/metadata | Get asset metadata @@ -142,7 +140,6 @@ Class | Method | HTTP request | Description *DatabaseBackupsAdminApi* | [**startDatabaseRestoreFlow**](doc//DatabaseBackupsAdminApi.md#startdatabaserestoreflow) | **POST** /admin/database-backups/start-restore | Start database backup restore flow *DatabaseBackupsAdminApi* | [**uploadDatabaseBackup**](doc//DatabaseBackupsAdminApi.md#uploaddatabasebackup) | **POST** /admin/database-backups/upload | Upload database backup *DeprecatedApi* | [**createPartnerDeprecated**](doc//DeprecatedApi.md#createpartnerdeprecated) | **POST** /partners/{id} | Create a partner -*DeprecatedApi* | [**getAllUserAssetsByDeviceId**](doc//DeprecatedApi.md#getalluserassetsbydeviceid) | **GET** /assets/device/{deviceId} | Retrieve assets by device ID *DeprecatedApi* | [**getQueuesLegacy**](doc//DeprecatedApi.md#getqueueslegacy) | **GET** /jobs | Retrieve queue counts and status *DeprecatedApi* | [**runQueueCommandLegacy**](doc//DeprecatedApi.md#runqueuecommandlegacy) | **PUT** /jobs/{name} | Run jobs *DownloadApi* | [**downloadArchive**](doc//DownloadApi.md#downloadarchive) | **POST** /download/archive | Download asset archive @@ -396,8 +393,6 @@ Class | Method | HTTP request | Description - [CastResponse](doc//CastResponse.md) - [CastUpdate](doc//CastUpdate.md) - [ChangePasswordDto](doc//ChangePasswordDto.md) - - [CheckExistingAssetsDto](doc//CheckExistingAssetsDto.md) - - [CheckExistingAssetsResponseDto](doc//CheckExistingAssetsResponseDto.md) - [Colorspace](doc//Colorspace.md) - [ContributorCountResponseDto](doc//ContributorCountResponseDto.md) - [CreateAlbumDto](doc//CreateAlbumDto.md) diff --git a/mobile/openapi/lib/api.dart b/mobile/openapi/lib/api.dart index b64f222791..15bc8190dc 100644 --- a/mobile/openapi/lib/api.dart +++ b/mobile/openapi/lib/api.dart @@ -142,8 +142,6 @@ part 'model/cq_mode.dart'; part 'model/cast_response.dart'; part 'model/cast_update.dart'; part 'model/change_password_dto.dart'; -part 'model/check_existing_assets_dto.dart'; -part 'model/check_existing_assets_response_dto.dart'; part 'model/colorspace.dart'; part 'model/contributor_count_response_dto.dart'; part 'model/create_album_dto.dart'; diff --git a/mobile/openapi/lib/api/assets_api.dart b/mobile/openapi/lib/api/assets_api.dart index 9e0183536e..5046376168 100644 --- a/mobile/openapi/lib/api/assets_api.dart +++ b/mobile/openapi/lib/api/assets_api.dart @@ -72,62 +72,6 @@ class AssetsApi { return null; } - /// Check existing assets - /// - /// Checks if multiple assets exist on the server and returns all existing - used by background backup - /// - /// Note: This method returns the HTTP [Response]. - /// - /// Parameters: - /// - /// * [CheckExistingAssetsDto] checkExistingAssetsDto (required): - Future checkExistingAssetsWithHttpInfo(CheckExistingAssetsDto checkExistingAssetsDto,) async { - // ignore: prefer_const_declarations - final apiPath = r'/assets/exist'; - - // ignore: prefer_final_locals - Object? postBody = checkExistingAssetsDto; - - final queryParams = []; - final headerParams = {}; - final formParams = {}; - - const contentTypes = ['application/json']; - - - return apiClient.invokeAPI( - apiPath, - 'POST', - queryParams, - postBody, - headerParams, - formParams, - contentTypes.isEmpty ? null : contentTypes.first, - ); - } - - /// Check existing assets - /// - /// Checks if multiple assets exist on the server and returns all existing - used by background backup - /// - /// Parameters: - /// - /// * [CheckExistingAssetsDto] checkExistingAssetsDto (required): - Future checkExistingAssets(CheckExistingAssetsDto checkExistingAssetsDto,) async { - final response = await checkExistingAssetsWithHttpInfo(checkExistingAssetsDto,); - 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), 'CheckExistingAssetsResponseDto',) as CheckExistingAssetsResponseDto; - - } - return null; - } - /// Copy asset /// /// Copy asset information like albums, tags, etc. from one asset to another. @@ -472,68 +416,6 @@ class AssetsApi { return null; } - /// Retrieve assets by device ID - /// - /// Get all asset of a device that are in the database, ID only. - /// - /// Note: This method returns the HTTP [Response]. - /// - /// Parameters: - /// - /// * [String] deviceId (required): - /// Device ID - Future getAllUserAssetsByDeviceIdWithHttpInfo(String deviceId,) async { - // ignore: prefer_const_declarations - final apiPath = r'/assets/device/{deviceId}' - .replaceAll('{deviceId}', deviceId); - - // ignore: prefer_final_locals - Object? postBody; - - final queryParams = []; - final headerParams = {}; - final formParams = {}; - - const contentTypes = []; - - - return apiClient.invokeAPI( - apiPath, - 'GET', - queryParams, - postBody, - headerParams, - formParams, - contentTypes.isEmpty ? null : contentTypes.first, - ); - } - - /// Retrieve assets by device ID - /// - /// Get all asset of a device that are in the database, ID only. - /// - /// Parameters: - /// - /// * [String] deviceId (required): - /// Device ID - Future?> getAllUserAssetsByDeviceId(String deviceId,) async { - final response = await getAllUserAssetsByDeviceIdWithHttpInfo(deviceId,); - 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) { - final responseBody = await _decodeBodyBytes(response); - return (await apiClient.deserializeAsync(responseBody, 'List') as List) - .cast() - .toList(growable: false); - - } - return null; - } - /// Retrieve edits for an existing asset /// /// Retrieve a series of edit actions (crop, rotate, mirror) associated with the specified asset. @@ -1339,12 +1221,6 @@ class AssetsApi { /// * [MultipartFile] assetData (required): /// Asset file data /// - /// * [String] deviceAssetId (required): - /// Device asset ID - /// - /// * [String] deviceId (required): - /// Device ID - /// /// * [DateTime] fileCreatedAt (required): /// File creation date /// @@ -1377,7 +1253,7 @@ class AssetsApi { /// Sidecar file data /// /// * [AssetVisibility] visibility: - Future uploadAssetWithHttpInfo(MultipartFile assetData, String deviceAssetId, String deviceId, DateTime fileCreatedAt, DateTime fileModifiedAt, { String? key, String? slug, String? xImmichChecksum, String? duration, String? filename, bool? isFavorite, String? livePhotoVideoId, List? metadata, MultipartFile? sidecarData, AssetVisibility? visibility, }) async { + Future uploadAssetWithHttpInfo(MultipartFile assetData, DateTime fileCreatedAt, DateTime fileModifiedAt, { String? key, String? slug, String? xImmichChecksum, String? duration, String? filename, bool? isFavorite, String? livePhotoVideoId, List? metadata, MultipartFile? sidecarData, AssetVisibility? visibility, }) async { // ignore: prefer_const_declarations final apiPath = r'/assets'; @@ -1408,14 +1284,6 @@ class AssetsApi { mp.fields[r'assetData'] = assetData.field; mp.files.add(assetData); } - if (deviceAssetId != null) { - hasFields = true; - mp.fields[r'deviceAssetId'] = parameterToString(deviceAssetId); - } - if (deviceId != null) { - hasFields = true; - mp.fields[r'deviceId'] = parameterToString(deviceId); - } if (duration != null) { hasFields = true; mp.fields[r'duration'] = parameterToString(duration); @@ -1477,12 +1345,6 @@ class AssetsApi { /// * [MultipartFile] assetData (required): /// Asset file data /// - /// * [String] deviceAssetId (required): - /// Device asset ID - /// - /// * [String] deviceId (required): - /// Device ID - /// /// * [DateTime] fileCreatedAt (required): /// File creation date /// @@ -1515,8 +1377,8 @@ class AssetsApi { /// Sidecar file data /// /// * [AssetVisibility] visibility: - Future uploadAsset(MultipartFile assetData, String deviceAssetId, String deviceId, DateTime fileCreatedAt, DateTime fileModifiedAt, { String? key, String? slug, String? xImmichChecksum, String? duration, String? filename, bool? isFavorite, String? livePhotoVideoId, List? metadata, MultipartFile? sidecarData, AssetVisibility? visibility, }) async { - final response = await uploadAssetWithHttpInfo(assetData, deviceAssetId, deviceId, fileCreatedAt, fileModifiedAt, key: key, slug: slug, xImmichChecksum: xImmichChecksum, duration: duration, filename: filename, isFavorite: isFavorite, livePhotoVideoId: livePhotoVideoId, metadata: metadata, sidecarData: sidecarData, visibility: visibility, ); + Future uploadAsset(MultipartFile assetData, DateTime fileCreatedAt, DateTime fileModifiedAt, { String? key, String? slug, String? xImmichChecksum, String? duration, String? filename, bool? isFavorite, String? livePhotoVideoId, List? metadata, MultipartFile? sidecarData, AssetVisibility? visibility, }) async { + final response = await uploadAssetWithHttpInfo(assetData, fileCreatedAt, fileModifiedAt, key: key, slug: slug, xImmichChecksum: xImmichChecksum, duration: duration, filename: filename, isFavorite: isFavorite, livePhotoVideoId: livePhotoVideoId, metadata: metadata, sidecarData: sidecarData, visibility: visibility, ); if (response.statusCode >= HttpStatus.badRequest) { throw ApiException(response.statusCode, await _decodeBodyBytes(response)); } diff --git a/mobile/openapi/lib/api/deprecated_api.dart b/mobile/openapi/lib/api/deprecated_api.dart index 5fa5df88b6..a437cd5837 100644 --- a/mobile/openapi/lib/api/deprecated_api.dart +++ b/mobile/openapi/lib/api/deprecated_api.dart @@ -73,68 +73,6 @@ class DeprecatedApi { return null; } - /// Retrieve assets by device ID - /// - /// Get all asset of a device that are in the database, ID only. - /// - /// Note: This method returns the HTTP [Response]. - /// - /// Parameters: - /// - /// * [String] deviceId (required): - /// Device ID - Future getAllUserAssetsByDeviceIdWithHttpInfo(String deviceId,) async { - // ignore: prefer_const_declarations - final apiPath = r'/assets/device/{deviceId}' - .replaceAll('{deviceId}', deviceId); - - // ignore: prefer_final_locals - Object? postBody; - - final queryParams = []; - final headerParams = {}; - final formParams = {}; - - const contentTypes = []; - - - return apiClient.invokeAPI( - apiPath, - 'GET', - queryParams, - postBody, - headerParams, - formParams, - contentTypes.isEmpty ? null : contentTypes.first, - ); - } - - /// Retrieve assets by device ID - /// - /// Get all asset of a device that are in the database, ID only. - /// - /// Parameters: - /// - /// * [String] deviceId (required): - /// Device ID - Future?> getAllUserAssetsByDeviceId(String deviceId,) async { - final response = await getAllUserAssetsByDeviceIdWithHttpInfo(deviceId,); - 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) { - final responseBody = await _decodeBodyBytes(response); - return (await apiClient.deserializeAsync(responseBody, 'List') as List) - .cast() - .toList(growable: false); - - } - return null; - } - /// Retrieve queue counts and status /// /// Retrieve the counts of the current queue, as well as the current status. diff --git a/mobile/openapi/lib/api/search_api.dart b/mobile/openapi/lib/api/search_api.dart index 46fc8594a8..730627d4a1 100644 --- a/mobile/openapi/lib/api/search_api.dart +++ b/mobile/openapi/lib/api/search_api.dart @@ -368,9 +368,6 @@ class SearchApi { /// * [DateTime] createdBefore: /// Filter by creation date (before) /// - /// * [String] deviceId: - /// Device ID to filter by - /// /// * [bool] isEncoded: /// Filter by encoded status /// @@ -446,7 +443,7 @@ class SearchApi { /// /// * [bool] withExif: /// Include EXIF data in response - Future searchLargeAssetsWithHttpInfo({ List? albumIds, String? city, String? country, DateTime? createdAfter, DateTime? createdBefore, String? deviceId, bool? isEncoded, bool? isFavorite, bool? isMotion, bool? isNotInAlbum, bool? isOffline, String? lensModel, String? libraryId, String? make, int? minFileSize, String? model, String? ocr, List? personIds, num? rating, num? size, String? state, List? tagIds, DateTime? takenAfter, DateTime? takenBefore, DateTime? trashedAfter, DateTime? trashedBefore, AssetTypeEnum? type, DateTime? updatedAfter, DateTime? updatedBefore, AssetVisibility? visibility, bool? withDeleted, bool? withExif, }) async { + Future searchLargeAssetsWithHttpInfo({ List? albumIds, String? city, String? country, DateTime? createdAfter, DateTime? createdBefore, bool? isEncoded, bool? isFavorite, bool? isMotion, bool? isNotInAlbum, bool? isOffline, String? lensModel, String? libraryId, String? make, int? minFileSize, String? model, String? ocr, List? personIds, num? rating, num? size, String? state, List? tagIds, DateTime? takenAfter, DateTime? takenBefore, DateTime? trashedAfter, DateTime? trashedBefore, AssetTypeEnum? type, DateTime? updatedAfter, DateTime? updatedBefore, AssetVisibility? visibility, bool? withDeleted, bool? withExif, }) async { // ignore: prefer_const_declarations final apiPath = r'/search/large-assets'; @@ -472,9 +469,6 @@ class SearchApi { if (createdBefore != null) { queryParams.addAll(_queryParams('', 'createdBefore', createdBefore)); } - if (deviceId != null) { - queryParams.addAll(_queryParams('', 'deviceId', deviceId)); - } if (isEncoded != null) { queryParams.addAll(_queryParams('', 'isEncoded', isEncoded)); } @@ -589,9 +583,6 @@ class SearchApi { /// * [DateTime] createdBefore: /// Filter by creation date (before) /// - /// * [String] deviceId: - /// Device ID to filter by - /// /// * [bool] isEncoded: /// Filter by encoded status /// @@ -667,8 +658,8 @@ class SearchApi { /// /// * [bool] withExif: /// Include EXIF data in response - Future?> searchLargeAssets({ List? albumIds, String? city, String? country, DateTime? createdAfter, DateTime? createdBefore, String? deviceId, bool? isEncoded, bool? isFavorite, bool? isMotion, bool? isNotInAlbum, bool? isOffline, String? lensModel, String? libraryId, String? make, int? minFileSize, String? model, String? ocr, List? personIds, num? rating, num? size, String? state, List? tagIds, DateTime? takenAfter, DateTime? takenBefore, DateTime? trashedAfter, DateTime? trashedBefore, AssetTypeEnum? type, DateTime? updatedAfter, DateTime? updatedBefore, AssetVisibility? visibility, bool? withDeleted, bool? withExif, }) async { - final response = await searchLargeAssetsWithHttpInfo( albumIds: albumIds, city: city, country: country, createdAfter: createdAfter, createdBefore: createdBefore, deviceId: deviceId, isEncoded: isEncoded, isFavorite: isFavorite, isMotion: isMotion, isNotInAlbum: isNotInAlbum, isOffline: isOffline, lensModel: lensModel, libraryId: libraryId, make: make, minFileSize: minFileSize, model: model, ocr: ocr, personIds: personIds, rating: rating, size: size, state: state, tagIds: tagIds, takenAfter: takenAfter, takenBefore: takenBefore, trashedAfter: trashedAfter, trashedBefore: trashedBefore, type: type, updatedAfter: updatedAfter, updatedBefore: updatedBefore, visibility: visibility, withDeleted: withDeleted, withExif: withExif, ); + Future?> searchLargeAssets({ List? albumIds, String? city, String? country, DateTime? createdAfter, DateTime? createdBefore, bool? isEncoded, bool? isFavorite, bool? isMotion, bool? isNotInAlbum, bool? isOffline, String? lensModel, String? libraryId, String? make, int? minFileSize, String? model, String? ocr, List? personIds, num? rating, num? size, String? state, List? tagIds, DateTime? takenAfter, DateTime? takenBefore, DateTime? trashedAfter, DateTime? trashedBefore, AssetTypeEnum? type, DateTime? updatedAfter, DateTime? updatedBefore, AssetVisibility? visibility, bool? withDeleted, bool? withExif, }) async { + final response = await searchLargeAssetsWithHttpInfo( albumIds: albumIds, city: city, country: country, createdAfter: createdAfter, createdBefore: createdBefore, isEncoded: isEncoded, isFavorite: isFavorite, isMotion: isMotion, isNotInAlbum: isNotInAlbum, isOffline: isOffline, lensModel: lensModel, libraryId: libraryId, make: make, minFileSize: minFileSize, model: model, ocr: ocr, personIds: personIds, rating: rating, size: size, state: state, tagIds: tagIds, takenAfter: takenAfter, takenBefore: takenBefore, trashedAfter: trashedAfter, trashedBefore: trashedBefore, type: type, updatedAfter: updatedAfter, updatedBefore: updatedBefore, visibility: visibility, withDeleted: withDeleted, withExif: withExif, ); if (response.statusCode >= HttpStatus.badRequest) { throw ApiException(response.statusCode, await _decodeBodyBytes(response)); } diff --git a/mobile/openapi/lib/api_client.dart b/mobile/openapi/lib/api_client.dart index c54a7bafa8..c4782a2340 100644 --- a/mobile/openapi/lib/api_client.dart +++ b/mobile/openapi/lib/api_client.dart @@ -330,10 +330,6 @@ class ApiClient { return CastUpdate.fromJson(value); case 'ChangePasswordDto': return ChangePasswordDto.fromJson(value); - case 'CheckExistingAssetsDto': - return CheckExistingAssetsDto.fromJson(value); - case 'CheckExistingAssetsResponseDto': - return CheckExistingAssetsResponseDto.fromJson(value); case 'Colorspace': return ColorspaceTypeTransformer().decode(value); case 'ContributorCountResponseDto': diff --git a/mobile/openapi/lib/model/asset_response_dto.dart b/mobile/openapi/lib/model/asset_response_dto.dart index d185761f54..a9d346b155 100644 --- a/mobile/openapi/lib/model/asset_response_dto.dart +++ b/mobile/openapi/lib/model/asset_response_dto.dart @@ -15,8 +15,6 @@ class AssetResponseDto { AssetResponseDto({ required this.checksum, required this.createdAt, - required this.deviceAssetId, - required this.deviceId, this.duplicateId, required this.duration, this.exifInfo, @@ -56,12 +54,6 @@ class AssetResponseDto { /// The UTC timestamp when the asset was originally uploaded to Immich. DateTime createdAt; - /// Device asset ID - String deviceAssetId; - - /// Device ID - String deviceId; - /// Duplicate group ID String? duplicateId; @@ -179,8 +171,6 @@ class AssetResponseDto { bool operator ==(Object other) => identical(this, other) || other is AssetResponseDto && other.checksum == checksum && other.createdAt == createdAt && - other.deviceAssetId == deviceAssetId && - other.deviceId == deviceId && other.duplicateId == duplicateId && other.duration == duration && other.exifInfo == exifInfo && @@ -218,8 +208,6 @@ class AssetResponseDto { // ignore: unnecessary_parenthesis (checksum.hashCode) + (createdAt.hashCode) + - (deviceAssetId.hashCode) + - (deviceId.hashCode) + (duplicateId == null ? 0 : duplicateId!.hashCode) + (duration.hashCode) + (exifInfo == null ? 0 : exifInfo!.hashCode) + @@ -253,14 +241,12 @@ class AssetResponseDto { (width == null ? 0 : width!.hashCode); @override - String toString() => 'AssetResponseDto[checksum=$checksum, createdAt=$createdAt, deviceAssetId=$deviceAssetId, deviceId=$deviceId, 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, unassignedFaces=$unassignedFaces, 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, unassignedFaces=$unassignedFaces, updatedAt=$updatedAt, visibility=$visibility, width=$width]'; Map toJson() { final json = {}; json[r'checksum'] = this.checksum; json[r'createdAt'] = this.createdAt.toUtc().toIso8601String(); - json[r'deviceAssetId'] = this.deviceAssetId; - json[r'deviceId'] = this.deviceId; if (this.duplicateId != null) { json[r'duplicateId'] = this.duplicateId; } else { @@ -350,8 +336,6 @@ class AssetResponseDto { return AssetResponseDto( checksum: mapValueOfType(json, r'checksum')!, createdAt: mapDateTime(json, r'createdAt', r'')!, - deviceAssetId: mapValueOfType(json, r'deviceAssetId')!, - deviceId: mapValueOfType(json, r'deviceId')!, duplicateId: mapValueOfType(json, r'duplicateId'), duration: mapValueOfType(json, r'duration')!, exifInfo: ExifResponseDto.fromJson(json[r'exifInfo']), @@ -436,8 +420,6 @@ class AssetResponseDto { static const requiredKeys = { 'checksum', 'createdAt', - 'deviceAssetId', - 'deviceId', 'duration', 'fileCreatedAt', 'fileModifiedAt', diff --git a/mobile/openapi/lib/model/check_existing_assets_dto.dart b/mobile/openapi/lib/model/check_existing_assets_dto.dart deleted file mode 100644 index 6e4a471092..0000000000 --- a/mobile/openapi/lib/model/check_existing_assets_dto.dart +++ /dev/null @@ -1,111 +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 CheckExistingAssetsDto { - /// Returns a new [CheckExistingAssetsDto] instance. - CheckExistingAssetsDto({ - this.deviceAssetIds = const [], - required this.deviceId, - }); - - /// Device asset IDs to check - List deviceAssetIds; - - /// Device ID - String deviceId; - - @override - bool operator ==(Object other) => identical(this, other) || other is CheckExistingAssetsDto && - _deepEquality.equals(other.deviceAssetIds, deviceAssetIds) && - other.deviceId == deviceId; - - @override - int get hashCode => - // ignore: unnecessary_parenthesis - (deviceAssetIds.hashCode) + - (deviceId.hashCode); - - @override - String toString() => 'CheckExistingAssetsDto[deviceAssetIds=$deviceAssetIds, deviceId=$deviceId]'; - - Map toJson() { - final json = {}; - json[r'deviceAssetIds'] = this.deviceAssetIds; - json[r'deviceId'] = this.deviceId; - return json; - } - - /// Returns a new [CheckExistingAssetsDto] instance and imports its values from - /// [value] if it's a [Map], null otherwise. - // ignore: prefer_constructors_over_static_methods - static CheckExistingAssetsDto? fromJson(dynamic value) { - upgradeDto(value, "CheckExistingAssetsDto"); - if (value is Map) { - final json = value.cast(); - - return CheckExistingAssetsDto( - deviceAssetIds: json[r'deviceAssetIds'] is Iterable - ? (json[r'deviceAssetIds'] as Iterable).cast().toList(growable: false) - : const [], - deviceId: mapValueOfType(json, r'deviceId')!, - ); - } - return null; - } - - static List listFromJson(dynamic json, {bool growable = false,}) { - final result = []; - if (json is List && json.isNotEmpty) { - for (final row in json) { - final value = CheckExistingAssetsDto.fromJson(row); - if (value != null) { - result.add(value); - } - } - } - return result.toList(growable: growable); - } - - static Map mapFromJson(dynamic json) { - final map = {}; - if (json is Map && json.isNotEmpty) { - json = json.cast(); // ignore: parameter_assignments - for (final entry in json.entries) { - final value = CheckExistingAssetsDto.fromJson(entry.value); - if (value != null) { - map[entry.key] = value; - } - } - } - return map; - } - - // maps a json object with a list of CheckExistingAssetsDto-objects as value to a dart map - static Map> mapListFromJson(dynamic json, {bool growable = false,}) { - final map = >{}; - if (json is Map && json.isNotEmpty) { - // ignore: parameter_assignments - json = json.cast(); - for (final entry in json.entries) { - map[entry.key] = CheckExistingAssetsDto.listFromJson(entry.value, growable: growable,); - } - } - return map; - } - - /// The list of required keys that must be present in a JSON. - static const requiredKeys = { - 'deviceAssetIds', - 'deviceId', - }; -} - diff --git a/mobile/openapi/lib/model/check_existing_assets_response_dto.dart b/mobile/openapi/lib/model/check_existing_assets_response_dto.dart deleted file mode 100644 index 9fb13f100f..0000000000 --- a/mobile/openapi/lib/model/check_existing_assets_response_dto.dart +++ /dev/null @@ -1,102 +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 CheckExistingAssetsResponseDto { - /// Returns a new [CheckExistingAssetsResponseDto] instance. - CheckExistingAssetsResponseDto({ - this.existingIds = const [], - }); - - /// Existing asset IDs - List existingIds; - - @override - bool operator ==(Object other) => identical(this, other) || other is CheckExistingAssetsResponseDto && - _deepEquality.equals(other.existingIds, existingIds); - - @override - int get hashCode => - // ignore: unnecessary_parenthesis - (existingIds.hashCode); - - @override - String toString() => 'CheckExistingAssetsResponseDto[existingIds=$existingIds]'; - - Map toJson() { - final json = {}; - json[r'existingIds'] = this.existingIds; - return json; - } - - /// Returns a new [CheckExistingAssetsResponseDto] instance and imports its values from - /// [value] if it's a [Map], null otherwise. - // ignore: prefer_constructors_over_static_methods - static CheckExistingAssetsResponseDto? fromJson(dynamic value) { - upgradeDto(value, "CheckExistingAssetsResponseDto"); - if (value is Map) { - final json = value.cast(); - - return CheckExistingAssetsResponseDto( - existingIds: json[r'existingIds'] is Iterable - ? (json[r'existingIds'] as Iterable).cast().toList(growable: false) - : const [], - ); - } - return null; - } - - static List listFromJson(dynamic json, {bool growable = false,}) { - final result = []; - if (json is List && json.isNotEmpty) { - for (final row in json) { - final value = CheckExistingAssetsResponseDto.fromJson(row); - if (value != null) { - result.add(value); - } - } - } - return result.toList(growable: growable); - } - - static Map mapFromJson(dynamic json) { - final map = {}; - if (json is Map && json.isNotEmpty) { - json = json.cast(); // ignore: parameter_assignments - for (final entry in json.entries) { - final value = CheckExistingAssetsResponseDto.fromJson(entry.value); - if (value != null) { - map[entry.key] = value; - } - } - } - return map; - } - - // maps a json object with a list of CheckExistingAssetsResponseDto-objects as value to a dart map - static Map> mapListFromJson(dynamic json, {bool growable = false,}) { - final map = >{}; - if (json is Map && json.isNotEmpty) { - // ignore: parameter_assignments - json = json.cast(); - for (final entry in json.entries) { - map[entry.key] = CheckExistingAssetsResponseDto.listFromJson(entry.value, growable: growable,); - } - } - return map; - } - - /// The list of required keys that must be present in a JSON. - static const requiredKeys = { - 'existingIds', - }; -} - diff --git a/mobile/openapi/lib/model/metadata_search_dto.dart b/mobile/openapi/lib/model/metadata_search_dto.dart index 0e8d509a16..d49ea7a4e5 100644 --- a/mobile/openapi/lib/model/metadata_search_dto.dart +++ b/mobile/openapi/lib/model/metadata_search_dto.dart @@ -20,8 +20,6 @@ class MetadataSearchDto { this.createdAfter, this.createdBefore, this.description, - this.deviceAssetId, - this.deviceId, this.encodedVideoPath, this.id, this.isEncoded, @@ -104,24 +102,6 @@ class MetadataSearchDto { /// String? description; - /// Filter by device asset ID - /// - /// Please note: This property should have been non-nullable! Since the specification file - /// does not include a default value (using the "default:" property), however, the generated - /// source code must fall back to having a nullable type. - /// Consider adding a "default:" property in the specification file to hide this note. - /// - String? deviceAssetId; - - /// Device ID to filter by - /// - /// Please note: This property should have been non-nullable! Since the specification file - /// does not include a default value (using the "default:" property), however, the generated - /// source code must fall back to having a nullable type. - /// Consider adding a "default:" property in the specification file to hide this note. - /// - String? deviceId; - /// Filter by encoded video file path /// /// Please note: This property should have been non-nullable! Since the specification file @@ -403,8 +383,6 @@ class MetadataSearchDto { other.createdAfter == createdAfter && other.createdBefore == createdBefore && other.description == description && - other.deviceAssetId == deviceAssetId && - other.deviceId == deviceId && other.encodedVideoPath == encodedVideoPath && other.id == id && other.isEncoded == isEncoded && @@ -451,8 +429,6 @@ class MetadataSearchDto { (createdAfter == null ? 0 : createdAfter!.hashCode) + (createdBefore == null ? 0 : createdBefore!.hashCode) + (description == null ? 0 : description!.hashCode) + - (deviceAssetId == null ? 0 : deviceAssetId!.hashCode) + - (deviceId == null ? 0 : deviceId!.hashCode) + (encodedVideoPath == null ? 0 : encodedVideoPath!.hashCode) + (id == null ? 0 : id!.hashCode) + (isEncoded == null ? 0 : isEncoded!.hashCode) + @@ -490,7 +466,7 @@ class MetadataSearchDto { (withStacked == null ? 0 : withStacked!.hashCode); @override - String toString() => 'MetadataSearchDto[albumIds=$albumIds, checksum=$checksum, city=$city, country=$country, createdAfter=$createdAfter, createdBefore=$createdBefore, description=$description, deviceAssetId=$deviceAssetId, deviceId=$deviceId, encodedVideoPath=$encodedVideoPath, id=$id, isEncoded=$isEncoded, isFavorite=$isFavorite, isMotion=$isMotion, isNotInAlbum=$isNotInAlbum, isOffline=$isOffline, lensModel=$lensModel, libraryId=$libraryId, make=$make, model=$model, ocr=$ocr, order=$order, originalFileName=$originalFileName, originalPath=$originalPath, page=$page, personIds=$personIds, previewPath=$previewPath, rating=$rating, size=$size, state=$state, tagIds=$tagIds, takenAfter=$takenAfter, takenBefore=$takenBefore, thumbnailPath=$thumbnailPath, trashedAfter=$trashedAfter, trashedBefore=$trashedBefore, type=$type, updatedAfter=$updatedAfter, updatedBefore=$updatedBefore, visibility=$visibility, withDeleted=$withDeleted, withExif=$withExif, withPeople=$withPeople, withStacked=$withStacked]'; + String toString() => 'MetadataSearchDto[albumIds=$albumIds, checksum=$checksum, city=$city, country=$country, createdAfter=$createdAfter, createdBefore=$createdBefore, description=$description, encodedVideoPath=$encodedVideoPath, id=$id, isEncoded=$isEncoded, isFavorite=$isFavorite, isMotion=$isMotion, isNotInAlbum=$isNotInAlbum, isOffline=$isOffline, lensModel=$lensModel, libraryId=$libraryId, make=$make, model=$model, ocr=$ocr, order=$order, originalFileName=$originalFileName, originalPath=$originalPath, page=$page, personIds=$personIds, previewPath=$previewPath, rating=$rating, size=$size, state=$state, tagIds=$tagIds, takenAfter=$takenAfter, takenBefore=$takenBefore, thumbnailPath=$thumbnailPath, trashedAfter=$trashedAfter, trashedBefore=$trashedBefore, type=$type, updatedAfter=$updatedAfter, updatedBefore=$updatedBefore, visibility=$visibility, withDeleted=$withDeleted, withExif=$withExif, withPeople=$withPeople, withStacked=$withStacked]'; Map toJson() { final json = {}; @@ -529,16 +505,6 @@ class MetadataSearchDto { } else { // json[r'description'] = null; } - if (this.deviceAssetId != null) { - json[r'deviceAssetId'] = this.deviceAssetId; - } else { - // json[r'deviceAssetId'] = null; - } - if (this.deviceId != null) { - json[r'deviceId'] = this.deviceId; - } else { - // json[r'deviceId'] = null; - } if (this.encodedVideoPath != null) { json[r'encodedVideoPath'] = this.encodedVideoPath; } else { @@ -743,8 +709,6 @@ class MetadataSearchDto { createdAfter: mapDateTime(json, r'createdAfter', 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))$/'), createdBefore: mapDateTime(json, r'createdBefore', 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))$/'), description: mapValueOfType(json, r'description'), - deviceAssetId: mapValueOfType(json, r'deviceAssetId'), - deviceId: mapValueOfType(json, r'deviceId'), encodedVideoPath: mapValueOfType(json, r'encodedVideoPath'), id: mapValueOfType(json, r'id'), isEncoded: mapValueOfType(json, r'isEncoded'), diff --git a/mobile/openapi/lib/model/random_search_dto.dart b/mobile/openapi/lib/model/random_search_dto.dart index 904561a033..3f33d8f850 100644 --- a/mobile/openapi/lib/model/random_search_dto.dart +++ b/mobile/openapi/lib/model/random_search_dto.dart @@ -18,7 +18,6 @@ class RandomSearchDto { this.country, this.createdAfter, this.createdBefore, - this.deviceId, this.isEncoded, this.isFavorite, this.isMotion, @@ -75,15 +74,6 @@ class RandomSearchDto { /// DateTime? createdBefore; - /// Device ID to filter by - /// - /// Please note: This property should have been non-nullable! Since the specification file - /// does not include a default value (using the "default:" property), however, the generated - /// source code must fall back to having a nullable type. - /// Consider adding a "default:" property in the specification file to hide this note. - /// - String? deviceId; - /// Filter by encoded status /// /// Please note: This property should have been non-nullable! Since the specification file @@ -290,7 +280,6 @@ class RandomSearchDto { other.country == country && other.createdAfter == createdAfter && other.createdBefore == createdBefore && - other.deviceId == deviceId && other.isEncoded == isEncoded && other.isFavorite == isFavorite && other.isMotion == isMotion && @@ -327,7 +316,6 @@ class RandomSearchDto { (country == null ? 0 : country!.hashCode) + (createdAfter == null ? 0 : createdAfter!.hashCode) + (createdBefore == null ? 0 : createdBefore!.hashCode) + - (deviceId == null ? 0 : deviceId!.hashCode) + (isEncoded == null ? 0 : isEncoded!.hashCode) + (isFavorite == null ? 0 : isFavorite!.hashCode) + (isMotion == null ? 0 : isMotion!.hashCode) + @@ -357,7 +345,7 @@ class RandomSearchDto { (withStacked == null ? 0 : withStacked!.hashCode); @override - String toString() => 'RandomSearchDto[albumIds=$albumIds, city=$city, country=$country, createdAfter=$createdAfter, createdBefore=$createdBefore, deviceId=$deviceId, isEncoded=$isEncoded, isFavorite=$isFavorite, isMotion=$isMotion, isNotInAlbum=$isNotInAlbum, isOffline=$isOffline, lensModel=$lensModel, libraryId=$libraryId, make=$make, model=$model, ocr=$ocr, personIds=$personIds, rating=$rating, size=$size, state=$state, tagIds=$tagIds, takenAfter=$takenAfter, takenBefore=$takenBefore, trashedAfter=$trashedAfter, trashedBefore=$trashedBefore, type=$type, updatedAfter=$updatedAfter, updatedBefore=$updatedBefore, visibility=$visibility, withDeleted=$withDeleted, withExif=$withExif, withPeople=$withPeople, withStacked=$withStacked]'; + String toString() => 'RandomSearchDto[albumIds=$albumIds, city=$city, country=$country, createdAfter=$createdAfter, createdBefore=$createdBefore, isEncoded=$isEncoded, isFavorite=$isFavorite, isMotion=$isMotion, isNotInAlbum=$isNotInAlbum, isOffline=$isOffline, lensModel=$lensModel, libraryId=$libraryId, make=$make, model=$model, ocr=$ocr, personIds=$personIds, rating=$rating, size=$size, state=$state, tagIds=$tagIds, takenAfter=$takenAfter, takenBefore=$takenBefore, trashedAfter=$trashedAfter, trashedBefore=$trashedBefore, type=$type, updatedAfter=$updatedAfter, updatedBefore=$updatedBefore, visibility=$visibility, withDeleted=$withDeleted, withExif=$withExif, withPeople=$withPeople, withStacked=$withStacked]'; Map toJson() { final json = {}; @@ -386,11 +374,6 @@ class RandomSearchDto { } else { // json[r'createdBefore'] = null; } - if (this.deviceId != null) { - json[r'deviceId'] = this.deviceId; - } else { - // json[r'deviceId'] = null; - } if (this.isEncoded != null) { json[r'isEncoded'] = this.isEncoded; } else { @@ -553,7 +536,6 @@ class RandomSearchDto { country: mapValueOfType(json, r'country'), createdAfter: mapDateTime(json, r'createdAfter', 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))$/'), createdBefore: mapDateTime(json, r'createdBefore', 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))$/'), - deviceId: mapValueOfType(json, r'deviceId'), isEncoded: mapValueOfType(json, r'isEncoded'), isFavorite: mapValueOfType(json, r'isFavorite'), isMotion: mapValueOfType(json, r'isMotion'), diff --git a/mobile/openapi/lib/model/smart_search_dto.dart b/mobile/openapi/lib/model/smart_search_dto.dart index 9c1192ff34..bf1465223e 100644 --- a/mobile/openapi/lib/model/smart_search_dto.dart +++ b/mobile/openapi/lib/model/smart_search_dto.dart @@ -18,7 +18,6 @@ class SmartSearchDto { this.country, this.createdAfter, this.createdBefore, - this.deviceId, this.isEncoded, this.isFavorite, this.isMotion, @@ -77,15 +76,6 @@ class SmartSearchDto { /// DateTime? createdBefore; - /// Device ID to filter by - /// - /// Please note: This property should have been non-nullable! Since the specification file - /// does not include a default value (using the "default:" property), however, the generated - /// source code must fall back to having a nullable type. - /// Consider adding a "default:" property in the specification file to hide this note. - /// - String? deviceId; - /// Filter by encoded status /// /// Please note: This property should have been non-nullable! Since the specification file @@ -312,7 +302,6 @@ class SmartSearchDto { other.country == country && other.createdAfter == createdAfter && other.createdBefore == createdBefore && - other.deviceId == deviceId && other.isEncoded == isEncoded && other.isFavorite == isFavorite && other.isMotion == isMotion && @@ -351,7 +340,6 @@ class SmartSearchDto { (country == null ? 0 : country!.hashCode) + (createdAfter == null ? 0 : createdAfter!.hashCode) + (createdBefore == null ? 0 : createdBefore!.hashCode) + - (deviceId == null ? 0 : deviceId!.hashCode) + (isEncoded == null ? 0 : isEncoded!.hashCode) + (isFavorite == null ? 0 : isFavorite!.hashCode) + (isMotion == null ? 0 : isMotion!.hashCode) + @@ -383,7 +371,7 @@ class SmartSearchDto { (withExif == null ? 0 : withExif!.hashCode); @override - String toString() => 'SmartSearchDto[albumIds=$albumIds, city=$city, country=$country, createdAfter=$createdAfter, createdBefore=$createdBefore, deviceId=$deviceId, isEncoded=$isEncoded, isFavorite=$isFavorite, isMotion=$isMotion, isNotInAlbum=$isNotInAlbum, isOffline=$isOffline, language=$language, lensModel=$lensModel, libraryId=$libraryId, make=$make, model=$model, ocr=$ocr, page=$page, personIds=$personIds, query=$query, queryAssetId=$queryAssetId, rating=$rating, size=$size, state=$state, tagIds=$tagIds, takenAfter=$takenAfter, takenBefore=$takenBefore, trashedAfter=$trashedAfter, trashedBefore=$trashedBefore, type=$type, updatedAfter=$updatedAfter, updatedBefore=$updatedBefore, visibility=$visibility, withDeleted=$withDeleted, withExif=$withExif]'; + String toString() => 'SmartSearchDto[albumIds=$albumIds, city=$city, country=$country, createdAfter=$createdAfter, createdBefore=$createdBefore, isEncoded=$isEncoded, isFavorite=$isFavorite, isMotion=$isMotion, isNotInAlbum=$isNotInAlbum, isOffline=$isOffline, language=$language, lensModel=$lensModel, libraryId=$libraryId, make=$make, model=$model, ocr=$ocr, page=$page, personIds=$personIds, query=$query, queryAssetId=$queryAssetId, rating=$rating, size=$size, state=$state, tagIds=$tagIds, takenAfter=$takenAfter, takenBefore=$takenBefore, trashedAfter=$trashedAfter, trashedBefore=$trashedBefore, type=$type, updatedAfter=$updatedAfter, updatedBefore=$updatedBefore, visibility=$visibility, withDeleted=$withDeleted, withExif=$withExif]'; Map toJson() { final json = {}; @@ -412,11 +400,6 @@ class SmartSearchDto { } else { // json[r'createdBefore'] = null; } - if (this.deviceId != null) { - json[r'deviceId'] = this.deviceId; - } else { - // json[r'deviceId'] = null; - } if (this.isEncoded != null) { json[r'isEncoded'] = this.isEncoded; } else { @@ -589,7 +572,6 @@ class SmartSearchDto { country: mapValueOfType(json, r'country'), createdAfter: mapDateTime(json, r'createdAfter', 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))$/'), createdBefore: mapDateTime(json, r'createdBefore', 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))$/'), - deviceId: mapValueOfType(json, r'deviceId'), isEncoded: mapValueOfType(json, r'isEncoded'), isFavorite: mapValueOfType(json, r'isFavorite'), isMotion: mapValueOfType(json, r'isMotion'), diff --git a/mobile/openapi/lib/model/statistics_search_dto.dart b/mobile/openapi/lib/model/statistics_search_dto.dart index 729b7f127c..d0070e8e12 100644 --- a/mobile/openapi/lib/model/statistics_search_dto.dart +++ b/mobile/openapi/lib/model/statistics_search_dto.dart @@ -19,7 +19,6 @@ class StatisticsSearchDto { this.createdAfter, this.createdBefore, this.description, - this.deviceId, this.isEncoded, this.isFavorite, this.isMotion, @@ -80,15 +79,6 @@ class StatisticsSearchDto { /// String? description; - /// Device ID to filter by - /// - /// Please note: This property should have been non-nullable! Since the specification file - /// does not include a default value (using the "default:" property), however, the generated - /// source code must fall back to having a nullable type. - /// Consider adding a "default:" property in the specification file to hide this note. - /// - String? deviceId; - /// Filter by encoded status /// /// Please note: This property should have been non-nullable! Since the specification file @@ -248,7 +238,6 @@ class StatisticsSearchDto { other.createdAfter == createdAfter && other.createdBefore == createdBefore && other.description == description && - other.deviceId == deviceId && other.isEncoded == isEncoded && other.isFavorite == isFavorite && other.isMotion == isMotion && @@ -281,7 +270,6 @@ class StatisticsSearchDto { (createdAfter == null ? 0 : createdAfter!.hashCode) + (createdBefore == null ? 0 : createdBefore!.hashCode) + (description == null ? 0 : description!.hashCode) + - (deviceId == null ? 0 : deviceId!.hashCode) + (isEncoded == null ? 0 : isEncoded!.hashCode) + (isFavorite == null ? 0 : isFavorite!.hashCode) + (isMotion == null ? 0 : isMotion!.hashCode) + @@ -306,7 +294,7 @@ class StatisticsSearchDto { (visibility == null ? 0 : visibility!.hashCode); @override - String toString() => 'StatisticsSearchDto[albumIds=$albumIds, city=$city, country=$country, createdAfter=$createdAfter, createdBefore=$createdBefore, description=$description, deviceId=$deviceId, isEncoded=$isEncoded, isFavorite=$isFavorite, isMotion=$isMotion, isNotInAlbum=$isNotInAlbum, isOffline=$isOffline, lensModel=$lensModel, libraryId=$libraryId, make=$make, model=$model, ocr=$ocr, personIds=$personIds, rating=$rating, state=$state, tagIds=$tagIds, takenAfter=$takenAfter, takenBefore=$takenBefore, trashedAfter=$trashedAfter, trashedBefore=$trashedBefore, type=$type, updatedAfter=$updatedAfter, updatedBefore=$updatedBefore, visibility=$visibility]'; + String toString() => 'StatisticsSearchDto[albumIds=$albumIds, city=$city, country=$country, createdAfter=$createdAfter, createdBefore=$createdBefore, description=$description, isEncoded=$isEncoded, isFavorite=$isFavorite, isMotion=$isMotion, isNotInAlbum=$isNotInAlbum, isOffline=$isOffline, lensModel=$lensModel, libraryId=$libraryId, make=$make, model=$model, ocr=$ocr, personIds=$personIds, rating=$rating, state=$state, tagIds=$tagIds, takenAfter=$takenAfter, takenBefore=$takenBefore, trashedAfter=$trashedAfter, trashedBefore=$trashedBefore, type=$type, updatedAfter=$updatedAfter, updatedBefore=$updatedBefore, visibility=$visibility]'; Map toJson() { final json = {}; @@ -340,11 +328,6 @@ class StatisticsSearchDto { } else { // json[r'description'] = null; } - if (this.deviceId != null) { - json[r'deviceId'] = this.deviceId; - } else { - // json[r'deviceId'] = null; - } if (this.isEncoded != null) { json[r'isEncoded'] = this.isEncoded; } else { @@ -483,7 +466,6 @@ class StatisticsSearchDto { createdAfter: mapDateTime(json, r'createdAfter', 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))$/'), createdBefore: mapDateTime(json, r'createdBefore', 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))$/'), description: mapValueOfType(json, r'description'), - deviceId: mapValueOfType(json, r'deviceId'), isEncoded: mapValueOfType(json, r'isEncoded'), isFavorite: mapValueOfType(json, r'isFavorite'), isMotion: mapValueOfType(json, r'isMotion'), diff --git a/open-api/immich-openapi-specs.json b/open-api/immich-openapi-specs.json index 1b2e6af587..fd6f9c5151 100644 --- a/open-api/immich-openapi-specs.json +++ b/open-api/immich-openapi-specs.json @@ -3084,126 +3084,6 @@ "x-immich-state": "Stable" } }, - "/assets/device/{deviceId}": { - "get": { - "deprecated": true, - "description": "Get all asset of a device that are in the database, ID only.", - "operationId": "getAllUserAssetsByDeviceId", - "parameters": [ - { - "name": "deviceId", - "required": true, - "in": "path", - "description": "Device ID", - "schema": { - "type": "string" - } - } - ], - "responses": { - "200": { - "content": { - "application/json": { - "schema": { - "items": { - "type": "string" - }, - "type": "array" - } - } - }, - "description": "" - } - }, - "security": [ - { - "bearer": [] - }, - { - "cookie": [] - }, - { - "api_key": [] - } - ], - "summary": "Retrieve assets by device ID", - "tags": [ - "Assets", - "Deprecated" - ], - "x-immich-history": [ - { - "version": "v1", - "state": "Added" - }, - { - "version": "v2", - "state": "Deprecated" - } - ], - "x-immich-state": "Deprecated" - } - }, - "/assets/exist": { - "post": { - "description": "Checks if multiple assets exist on the server and returns all existing - used by background backup", - "operationId": "checkExistingAssets", - "parameters": [], - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/CheckExistingAssetsDto" - } - } - }, - "required": true - }, - "responses": { - "200": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/CheckExistingAssetsResponseDto" - } - } - }, - "description": "" - } - }, - "security": [ - { - "bearer": [] - }, - { - "cookie": [] - }, - { - "api_key": [] - } - ], - "summary": "Check existing assets", - "tags": [ - "Assets" - ], - "x-immich-history": [ - { - "version": "v1", - "state": "Added" - }, - { - "version": "v1", - "state": "Beta" - }, - { - "version": "v2", - "state": "Stable" - } - ], - "x-immich-permission": "asset.upload", - "x-immich-state": "Stable" - } - }, "/assets/jobs": { "post": { "description": "Run a specific job on a set of assets.", @@ -9255,15 +9135,6 @@ "type": "string" } }, - { - "name": "deviceId", - "required": false, - "in": "query", - "description": "Device ID to filter by", - "schema": { - "type": "string" - } - }, { "name": "isEncoded", "required": false, @@ -16343,14 +16214,6 @@ "format": "binary", "type": "string" }, - "deviceAssetId": { - "description": "Device asset ID", - "type": "string" - }, - "deviceId": { - "description": "Device ID", - "type": "string" - }, "duration": { "description": "Duration (for videos)", "type": "string" @@ -16401,8 +16264,6 @@ }, "required": [ "assetData", - "deviceAssetId", - "deviceId", "fileCreatedAt", "fileModifiedAt" ], @@ -16718,14 +16579,6 @@ "format": "date-time", "type": "string" }, - "deviceAssetId": { - "description": "Device asset ID", - "type": "string" - }, - "deviceId": { - "description": "Device ID", - "type": "string" - }, "duplicateId": { "description": "Duplicate group ID", "nullable": true, @@ -16907,8 +16760,6 @@ "required": [ "checksum", "createdAt", - "deviceAssetId", - "deviceId", "duration", "fileCreatedAt", "fileModifiedAt", @@ -17184,42 +17035,6 @@ ], "type": "object" }, - "CheckExistingAssetsDto": { - "properties": { - "deviceAssetIds": { - "description": "Device asset IDs to check", - "items": { - "type": "string" - }, - "minItems": 1, - "type": "array" - }, - "deviceId": { - "description": "Device ID", - "type": "string" - } - }, - "required": [ - "deviceAssetIds", - "deviceId" - ], - "type": "object" - }, - "CheckExistingAssetsResponseDto": { - "properties": { - "existingIds": { - "description": "Existing asset IDs", - "items": { - "type": "string" - }, - "type": "array" - } - }, - "required": [ - "existingIds" - ], - "type": "object" - }, "Colorspace": { "description": "Colorspace", "enum": [ @@ -18794,14 +18609,6 @@ "description": "Filter by description text", "type": "string" }, - "deviceAssetId": { - "description": "Filter by device asset ID", - "type": "string" - }, - "deviceId": { - "description": "Device ID to filter by", - "type": "string" - }, "encodedVideoPath": { "description": "Filter by encoded video file path", "type": "string" @@ -20675,10 +20482,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))$", "type": "string" }, - "deviceId": { - "description": "Device ID to filter by", - "type": "string" - }, "isEncoded": { "description": "Filter by encoded status", "type": "boolean" @@ -22054,10 +21857,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))$", "type": "string" }, - "deviceId": { - "description": "Device ID to filter by", - "type": "string" - }, "isEncoded": { "description": "Filter by encoded status", "type": "boolean" @@ -22337,10 +22136,6 @@ "description": "Filter by description text", "type": "string" }, - "deviceId": { - "description": "Device ID to filter by", - "type": "string" - }, "isEncoded": { "description": "Filter by encoded status", "type": "boolean" diff --git a/open-api/typescript-sdk/src/fetch-client.ts b/open-api/typescript-sdk/src/fetch-client.ts index a452d86b8c..fd7b800f93 100644 --- a/open-api/typescript-sdk/src/fetch-client.ts +++ b/open-api/typescript-sdk/src/fetch-client.ts @@ -557,10 +557,6 @@ export type AssetResponseDto = { checksum: string; /** The UTC timestamp when the asset was originally uploaded to Immich. */ createdAt: string; - /** Device asset ID */ - deviceAssetId: string; - /** Device ID */ - deviceId: string; /** Duplicate group ID */ duplicateId?: string | null; /** Video duration (for videos) */ @@ -773,10 +769,6 @@ export type AssetMetadataUpsertItemDto = { export type AssetMediaCreateDto = { /** Asset file data */ assetData: Blob; - /** Device asset ID */ - deviceAssetId: string; - /** Device ID */ - deviceId: string; /** Duration (for videos) */ duration?: string; /** File creation date */ @@ -863,16 +855,6 @@ export type AssetCopyDto = { /** Target asset ID */ targetId: string; }; -export type CheckExistingAssetsDto = { - /** Device asset IDs to check */ - deviceAssetIds: string[]; - /** Device ID */ - deviceId: string; -}; -export type CheckExistingAssetsResponseDto = { - /** Existing asset IDs */ - existingIds: string[]; -}; export type AssetJobsDto = { /** Asset IDs */ assetIds: string[]; @@ -1661,10 +1643,6 @@ export type MetadataSearchDto = { createdBefore?: string; /** Filter by description text */ description?: string; - /** Filter by device asset ID */ - deviceAssetId?: string; - /** Device ID to filter by */ - deviceId?: string; /** Filter by encoded video file path */ encodedVideoPath?: string; /** Filter by asset ID */ @@ -1790,8 +1768,6 @@ export type RandomSearchDto = { createdAfter?: string; /** Filter by creation date (before) */ createdBefore?: string; - /** Device ID to filter by */ - deviceId?: string; /** Filter by encoded status */ isEncoded?: boolean; /** Filter by favorite status */ @@ -1856,8 +1832,6 @@ export type SmartSearchDto = { createdAfter?: string; /** Filter by creation date (before) */ createdBefore?: string; - /** Device ID to filter by */ - deviceId?: string; /** Filter by encoded status */ isEncoded?: boolean; /** Filter by favorite status */ @@ -1928,8 +1902,6 @@ export type StatisticsSearchDto = { createdBefore?: string; /** Filter by description text */ description?: string; - /** Device ID to filter by */ - deviceId?: string; /** Filter by encoded status */ isEncoded?: boolean; /** Filter by favorite status */ @@ -3958,34 +3930,6 @@ export function copyAsset({ assetCopyDto }: { body: assetCopyDto }))); } -/** - * Retrieve assets by device ID - */ -export function getAllUserAssetsByDeviceId({ deviceId }: { - deviceId: string; -}, opts?: Oazapfts.RequestOpts) { - return oazapfts.ok(oazapfts.fetchJson<{ - status: 200; - data: string[]; - }>(`/assets/device/${encodeURIComponent(deviceId)}`, { - ...opts - })); -} -/** - * Check existing assets - */ -export function checkExistingAssets({ checkExistingAssetsDto }: { - checkExistingAssetsDto: CheckExistingAssetsDto; -}, opts?: Oazapfts.RequestOpts) { - return oazapfts.ok(oazapfts.fetchJson<{ - status: 200; - data: CheckExistingAssetsResponseDto; - }>("/assets/exist", oazapfts.json({ - ...opts, - method: "POST", - body: checkExistingAssetsDto - }))); -} /** * Run an asset job */ @@ -5365,13 +5309,12 @@ export function getExploreData(opts?: Oazapfts.RequestOpts) { /** * Search large assets */ -export function searchLargeAssets({ albumIds, city, country, createdAfter, createdBefore, deviceId, isEncoded, isFavorite, isMotion, isNotInAlbum, isOffline, lensModel, libraryId, make, minFileSize, model, ocr, personIds, rating, size, state, tagIds, takenAfter, takenBefore, trashedAfter, trashedBefore, $type, updatedAfter, updatedBefore, visibility, withDeleted, withExif }: { +export function searchLargeAssets({ albumIds, city, country, createdAfter, createdBefore, isEncoded, isFavorite, isMotion, isNotInAlbum, isOffline, lensModel, libraryId, make, minFileSize, model, ocr, personIds, rating, size, state, tagIds, takenAfter, takenBefore, trashedAfter, trashedBefore, $type, updatedAfter, updatedBefore, visibility, withDeleted, withExif }: { albumIds?: string[]; city?: string | null; country?: string | null; createdAfter?: string; createdBefore?: string; - deviceId?: string; isEncoded?: boolean; isFavorite?: boolean; isMotion?: boolean; @@ -5408,7 +5351,6 @@ export function searchLargeAssets({ albumIds, city, country, createdAfter, creat country, createdAfter, createdBefore, - deviceId, isEncoded, isFavorite, isMotion, diff --git a/server/src/controllers/asset-media.controller.spec.ts b/server/src/controllers/asset-media.controller.spec.ts index 71206c3855..7ccd0d644d 100644 --- a/server/src/controllers/asset-media.controller.spec.ts +++ b/server/src/controllers/asset-media.controller.spec.ts @@ -9,8 +9,6 @@ import { automock, ControllerContext, controllerSetup, mockBaseService } from 't const makeUploadDto = (options?: { omit: string }): Record => { const dto: Record = { - deviceAssetId: 'example-image', - deviceId: 'TEST', fileCreatedAt: new Date().toISOString(), fileModifiedAt: new Date().toISOString(), isFavorite: 'false', @@ -87,28 +85,6 @@ describe(AssetMediaController.name, () => { ); }); - it('should require `deviceAssetId`', async () => { - const { status, body } = await request(ctx.getHttpServer()) - .post('/assets') - .attach('assetData', assetData, filename) - .field({ ...makeUploadDto({ omit: 'deviceAssetId' }) }); - expect(status).toBe(400); - expect(body).toEqual( - factory.responses.badRequest(['[deviceAssetId] Invalid input: expected string, received undefined']), - ); - }); - - it('should require `deviceId`', async () => { - const { status, body } = await request(ctx.getHttpServer()) - .post('/assets') - .attach('assetData', assetData, filename) - .field({ ...makeUploadDto({ omit: 'deviceId' }) }); - expect(status).toBe(400); - expect(body).toEqual( - factory.responses.badRequest(['[deviceId] Invalid input: expected string, received undefined']), - ); - }); - it('should require `fileCreatedAt`', async () => { const { status, body } = await request(ctx.getHttpServer()) .post('/assets') diff --git a/server/src/controllers/asset-media.controller.ts b/server/src/controllers/asset-media.controller.ts index dbb906fd02..240e5b8b3c 100644 --- a/server/src/controllers/asset-media.controller.ts +++ b/server/src/controllers/asset-media.controller.ts @@ -21,14 +21,12 @@ import { AssetBulkUploadCheckResponseDto, AssetMediaResponseDto, AssetMediaStatus, - CheckExistingAssetsResponseDto, } from 'src/dtos/asset-media-response.dto'; import { AssetBulkUploadCheckDto, AssetMediaCreateDto, AssetMediaOptionsDto, AssetMediaSize, - CheckExistingAssetsDto, } from 'src/dtos/asset-media.dto'; import { AssetDownloadOriginalDto } from 'src/dtos/asset.dto'; import { AuthDto } from 'src/dtos/auth.dto'; @@ -179,21 +177,6 @@ export class AssetMediaController { await sendFile(res, next, () => this.service.playbackVideo(auth, id), this.logger); } - @Post('exist') - @Authenticated({ permission: Permission.AssetUpload }) - @Endpoint({ - summary: 'Check existing assets', - description: 'Checks if multiple assets exist on the server and returns all existing - used by background backup', - history: new HistoryBuilder().added('v1').beta('v1').stable('v2'), - }) - @HttpCode(HttpStatus.OK) - checkExistingAssets( - @Auth() auth: AuthDto, - @Body() dto: CheckExistingAssetsDto, - ): Promise { - return this.service.checkExistingAssets(auth, dto); - } - @Post('bulk-upload-check') @Authenticated({ permission: Permission.AssetUpload }) @Endpoint({ diff --git a/server/src/controllers/asset.controller.ts b/server/src/controllers/asset.controller.ts index d36a9dd7fd..1c9afebc1b 100644 --- a/server/src/controllers/asset.controller.ts +++ b/server/src/controllers/asset.controller.ts @@ -15,7 +15,6 @@ import { AssetMetadataUpsertDto, AssetStatsDto, AssetStatsResponseDto, - DeviceIdDto, UpdateAssetDto, } from 'src/dtos/asset.dto'; import { AuthDto } from 'src/dtos/auth.dto'; @@ -31,17 +30,6 @@ import { UUIDParamDto } from 'src/validation'; export class AssetController { constructor(private service: AssetService) {} - @Get('/device/:deviceId') - @Endpoint({ - summary: 'Retrieve assets by device ID', - description: 'Get all asset of a device that are in the database, ID only.', - history: new HistoryBuilder().added('v1').deprecated('v2'), - }) - @Authenticated() - getAllUserAssetsByDeviceId(@Auth() auth: AuthDto, @Param() { deviceId }: DeviceIdDto) { - return this.service.getUserAssetsByDeviceId(auth, deviceId); - } - @Get('statistics') @Authenticated({ permission: Permission.AssetStatistics }) @Endpoint({ diff --git a/server/src/database.ts b/server/src/database.ts index f8065ffd2c..7235af46fa 100644 --- a/server/src/database.ts +++ b/server/src/database.ts @@ -114,8 +114,6 @@ export type Asset = { id: string; checksum: Buffer; checksumAlgorithm: ChecksumAlgorithm; - deviceAssetId: string; - deviceId: string; fileCreatedAt: Date; fileModifiedAt: Date; isExternal: boolean; @@ -333,8 +331,6 @@ export const columns = { 'asset.id', 'asset.checksum', 'asset.checksumAlgorithm', - 'asset.deviceAssetId', - 'asset.deviceId', 'asset.fileCreatedAt', 'asset.fileModifiedAt', 'asset.isExternal', diff --git a/server/src/dtos/asset-media-response.dto.ts b/server/src/dtos/asset-media-response.dto.ts index fa3c472765..ab79b8428c 100644 --- a/server/src/dtos/asset-media-response.dto.ts +++ b/server/src/dtos/asset-media-response.dto.ts @@ -49,12 +49,5 @@ const AssetBulkUploadCheckResponseSchema = z }) .meta({ id: 'AssetBulkUploadCheckResponseDto' }); -const CheckExistingAssetsResponseSchema = z - .object({ - existingIds: z.array(z.string()).describe('Existing asset IDs'), - }) - .meta({ id: 'CheckExistingAssetsResponseDto' }); - export class AssetMediaResponseDto extends createZodDto(AssetMediaResponseSchema) {} export class AssetBulkUploadCheckResponseDto extends createZodDto(AssetBulkUploadCheckResponseSchema) {} -export class CheckExistingAssetsResponseDto extends createZodDto(CheckExistingAssetsResponseSchema) {} diff --git a/server/src/dtos/asset-media.dto.ts b/server/src/dtos/asset-media.dto.ts index 7178d4184e..cd9c7de641 100644 --- a/server/src/dtos/asset-media.dto.ts +++ b/server/src/dtos/asset-media.dto.ts @@ -36,8 +36,6 @@ export enum UploadFieldName { } const AssetMediaBaseSchema = z.object({ - deviceAssetId: z.string().describe('Device asset ID'), - deviceId: z.string().describe('Device ID'), fileCreatedAt: isoDatetimeToDate.describe('File creation date'), fileModifiedAt: isoDatetimeToDate.describe('File modification date'), duration: z.string().optional().describe('Duration (for videos)'), @@ -71,14 +69,6 @@ const AssetBulkUploadCheckSchema = z }) .meta({ id: 'AssetBulkUploadCheckDto' }); -const CheckExistingAssetsSchema = z - .object({ - deviceAssetIds: z.array(z.string()).min(1).describe('Device asset IDs to check'), - deviceId: z.string().describe('Device ID'), - }) - .meta({ id: 'CheckExistingAssetsDto' }); - export class AssetMediaOptionsDto extends createZodDto(AssetMediaOptionsSchema) {} export class AssetMediaCreateDto extends createZodDto(AssetMediaCreateSchema) {} export class AssetBulkUploadCheckDto extends createZodDto(AssetBulkUploadCheckSchema) {} -export class CheckExistingAssetsDto extends createZodDto(CheckExistingAssetsSchema) {} diff --git a/server/src/dtos/asset-response.dto.ts b/server/src/dtos/asset-response.dto.ts index a95d2f3c1e..b4a398b573 100644 --- a/server/src/dtos/asset-response.dto.ts +++ b/server/src/dtos/asset-response.dto.ts @@ -72,8 +72,6 @@ export const AssetResponseSchema = SanitizedAssetResponseSchema.extend( .string() .meta({ format: 'date-time' }) .describe('The UTC timestamp when the asset was originally uploaded to Immich.'), - deviceAssetId: z.string().describe('Device asset ID'), - deviceId: z.string().describe('Device ID'), ownerId: z.string().describe('Owner user ID'), owner: UserResponseSchema.optional(), libraryId: z @@ -137,8 +135,6 @@ export type MapAsset = { status: AssetStatus; checksum: Buffer; checksumAlgorithm: ChecksumAlgorithm; - deviceAssetId: string; - deviceId: string; duplicateId: string | null; duration: string | null; edits?: ShallowDehydrateObject[]; @@ -239,10 +235,8 @@ export function mapAsset(entity: MaybeDehydrated, options: AssetMapOpt return { id: entity.id, createdAt: asDateString(entity.createdAt), - deviceAssetId: entity.deviceAssetId, ownerId: entity.ownerId, owner: entity.owner ? mapUser(entity.owner) : undefined, - deviceId: entity.deviceId, libraryId: entity.libraryId, type: entity.type, originalPath: entity.originalPath, diff --git a/server/src/dtos/asset.dto.ts b/server/src/dtos/asset.dto.ts index c05462c554..1362a86ed7 100644 --- a/server/src/dtos/asset.dto.ts +++ b/server/src/dtos/asset.dto.ts @@ -6,12 +6,6 @@ import { AssetStats } from 'src/repositories/asset.repository'; import { IsNotSiblingOf, isoDatetimeToDate, latitudeSchema, longitudeSchema, stringToBool } from 'src/validation'; import z from 'zod'; -const DeviceIdSchema = z - .object({ - deviceId: z.string().describe('Device ID'), - }) - .meta({ id: 'DeviceIdDto' }); - const UpdateAssetBaseSchema = z .object({ isFavorite: z.boolean().optional().describe('Mark as favorite'), @@ -182,7 +176,6 @@ export const mapStats = (stats: AssetStats): AssetStatsResponseDto => { }; }; -export class DeviceIdDto extends createZodDto(DeviceIdSchema) {} export class AssetBulkUpdateDto extends createZodDto(AssetBulkUpdateSchema) {} export class UpdateAssetDto extends createZodDto(UpdateAssetSchema) {} export class AssetBulkDeleteDto extends createZodDto(AssetBulkDeleteSchema) {} diff --git a/server/src/dtos/search.dto.ts b/server/src/dtos/search.dto.ts index 43da0b8709..c0362cdb5d 100644 --- a/server/src/dtos/search.dto.ts +++ b/server/src/dtos/search.dto.ts @@ -9,7 +9,6 @@ import z from 'zod'; const BaseSearchSchema = z.object({ libraryId: z.uuidv4().nullish().describe('Library ID to filter by'), - deviceId: z.string().optional().describe('Device ID to filter by'), type: AssetTypeSchema.optional(), isEncoded: z.boolean().optional().describe('Filter by encoded status'), isFavorite: z.boolean().optional().describe('Filter by favorite status'), @@ -68,7 +67,6 @@ const LargeAssetSearchSchema = BaseSearchWithResultsSchema.extend({ const MetadataSearchSchema = RandomSearchSchema.extend({ id: z.uuidv4().optional().describe('Filter by asset ID'), - deviceAssetId: z.string().optional().describe('Filter by device asset ID'), description: z.string().trim().optional().describe('Filter by description text'), checksum: z.string().optional().describe('Filter by file checksum'), originalFileName: z.string().trim().optional().describe('Filter by original file name'), diff --git a/server/src/queries/asset.job.repository.sql b/server/src/queries/asset.job.repository.sql index 333d4ace81..bd903f3952 100644 --- a/server/src/queries/asset.job.repository.sql +++ b/server/src/queries/asset.job.repository.sql @@ -250,8 +250,6 @@ select "asset"."id", "asset"."checksum", "asset"."checksumAlgorithm", - "asset"."deviceAssetId", - "asset"."deviceId", "asset"."fileCreatedAt", "asset"."fileModifiedAt", "asset"."isExternal", diff --git a/server/src/queries/asset.repository.sql b/server/src/queries/asset.repository.sql index 88cd9c08d5..ebc2de90e1 100644 --- a/server/src/queries/asset.repository.sql +++ b/server/src/queries/asset.repository.sql @@ -242,17 +242,6 @@ where limit $3 --- AssetRepository.getAllByDeviceId -select - "deviceAssetId" -from - "asset" -where - "ownerId" = $1::uuid - and "deviceId" = $2 - and "visibility" != $3 - and "deletedAt" is null - -- AssetRepository.getLivePhotoCount select count(*) as "count" @@ -312,9 +301,8 @@ limit -- AssetRepository.updateAll update "asset" set - "deviceId" = $1 where - "id" = any ($2::uuid[]) + "id" = any ($1::uuid[]) -- AssetRepository.getByChecksum select diff --git a/server/src/repositories/asset.repository.ts b/server/src/repositories/asset.repository.ts index 686a0345a4..784cf68b5b 100644 --- a/server/src/repositories/asset.repository.ts +++ b/server/src/repositories/asset.repository.ts @@ -448,18 +448,6 @@ export class AssetRepository { await this.db.deleteFrom('asset').where('ownerId', '=', ownerId).execute(); } - async getByDeviceIds(ownerId: string, deviceId: string, deviceAssetIds: string[]): Promise { - const assets = await this.db - .selectFrom('asset') - .select(['deviceAssetId']) - .where('deviceAssetId', 'in', deviceAssetIds) - .where('deviceId', '=', deviceId) - .where('ownerId', '=', asUuid(ownerId)) - .execute(); - - return assets.map((asset) => asset.deviceAssetId); - } - @GenerateSql({ params: [DummyValue.UUID, DummyValue.STRING] }) getByLibraryIdAndOriginalPath(libraryId: string, originalPath: string) { return this.db @@ -471,27 +459,6 @@ export class AssetRepository { .executeTakeFirst(); } - /** - * Get assets by device's Id on the database - * @param ownerId - * @param deviceId - * - * @returns Promise - Array of assetIds belong to the device - */ - @GenerateSql({ params: [DummyValue.UUID, DummyValue.STRING] }) - async getAllByDeviceId(ownerId: string, deviceId: string): Promise { - const items = await this.db - .selectFrom('asset') - .select(['deviceAssetId']) - .where('ownerId', '=', asUuid(ownerId)) - .where('deviceId', '=', deviceId) - .where('visibility', '!=', AssetVisibility.Hidden) - .where('deletedAt', 'is', null) - .execute(); - - return items.map((asset) => asset.deviceAssetId); - } - @GenerateSql({ params: [DummyValue.UUID] }) async getLivePhotoCount(motionId: string): Promise { const [{ count }] = await this.db @@ -568,7 +535,7 @@ export class AssetRepository { .executeTakeFirst(); } - @GenerateSql({ params: [[DummyValue.UUID], { deviceId: DummyValue.STRING }] }) + @GenerateSql({ params: [[DummyValue.UUID], {}] }) @Chunked() async updateAll(ids: string[], options: Updateable): Promise { if (ids.length === 0) { diff --git a/server/src/repositories/search.repository.ts b/server/src/repositories/search.repository.ts index 8f8a5be0bd..171102a660 100644 --- a/server/src/repositories/search.repository.ts +++ b/server/src/repositories/search.repository.ts @@ -14,12 +14,10 @@ import { isValidInteger } from 'src/validation'; export interface SearchAssetIdOptions { checksum?: Buffer; - deviceAssetId?: string; id?: string; } export interface SearchUserIdOptions { - deviceId?: string; libraryId?: string | null; userIds?: string[]; } diff --git a/server/src/schema/migrations/1776263790468-DropDeviceIdAndDeviceAssetId.ts b/server/src/schema/migrations/1776263790468-DropDeviceIdAndDeviceAssetId.ts new file mode 100644 index 0000000000..97ed6eceda --- /dev/null +++ b/server/src/schema/migrations/1776263790468-DropDeviceIdAndDeviceAssetId.ts @@ -0,0 +1,11 @@ +import { Kysely, sql } from 'kysely'; + +export async function up(db: Kysely): Promise { + await sql`ALTER TABLE "asset" DROP COLUMN "deviceAssetId";`.execute(db); + await sql`ALTER TABLE "asset" DROP COLUMN "deviceId";`.execute(db); +} + +export async function down(db: Kysely): Promise { + await sql`ALTER TABLE "asset" ADD "deviceAssetId" character varying NOT NULL;`.execute(db); + await sql`ALTER TABLE "asset" ADD "deviceId" character varying NOT NULL;`.execute(db); +} diff --git a/server/src/schema/tables/asset.table.ts b/server/src/schema/tables/asset.table.ts index 7418dab102..e19a52af9c 100644 --- a/server/src/schema/tables/asset.table.ts +++ b/server/src/schema/tables/asset.table.ts @@ -65,15 +65,9 @@ export class AssetTable { @PrimaryGeneratedColumn() id!: Generated; - @Column() - deviceAssetId!: string; - @ForeignKeyColumn(() => UserTable, { onDelete: 'CASCADE', onUpdate: 'CASCADE', nullable: false }) ownerId!: string; - @Column() - deviceId!: string; - @Column() type!: AssetType; diff --git a/server/src/services/asset-media.service.spec.ts b/server/src/services/asset-media.service.spec.ts index 3dba0c6d34..ce8ecac72c 100644 --- a/server/src/services/asset-media.service.spec.ts +++ b/server/src/services/asset-media.service.spec.ts @@ -145,8 +145,6 @@ const uploadTests = [ ]; const createDto = Object.freeze({ - deviceAssetId: 'deviceAssetId', - deviceId: 'deviceId', fileCreatedAt: new Date('2022-06-19T23:41:36.910Z'), fileModifiedAt: new Date('2022-06-19T23:41:36.910Z'), isFavorite: false, @@ -156,8 +154,6 @@ const createDto = Object.freeze({ const assetEntity = Object.freeze({ id: 'id_1', ownerId: 'user_id_1', - deviceAssetId: 'device_asset_id_1', - deviceId: 'device_id_1', type: AssetType.Video, originalPath: 'fake_path/asset_1.jpeg', fileModifiedAt: new Date('2022-06-19T23:41:36.910Z'), @@ -765,17 +761,6 @@ describe(AssetMediaService.name, () => { }); }); - describe('checkExistingAssets', () => { - it('should get existing asset ids', async () => { - mocks.asset.getByDeviceIds.mockResolvedValue(['42']); - await expect( - sut.checkExistingAssets(authStub.admin, { deviceId: '420', deviceAssetIds: ['69'] }), - ).resolves.toEqual({ existingIds: ['42'] }); - - expect(mocks.asset.getByDeviceIds).toHaveBeenCalledWith(userStub.admin.id, '420', ['69']); - }); - }); - describe('bulkUploadCheck', () => { it('should accept hex and base64 checksums', async () => { const file1 = Buffer.from('d2947b871a706081be194569951b7db246907957', 'hex'); diff --git a/server/src/services/asset-media.service.ts b/server/src/services/asset-media.service.ts index 74e7d08012..74aaa8fcbd 100644 --- a/server/src/services/asset-media.service.ts +++ b/server/src/services/asset-media.service.ts @@ -9,14 +9,12 @@ import { AssetMediaStatus, AssetRejectReason, AssetUploadAction, - CheckExistingAssetsResponseDto, } from 'src/dtos/asset-media-response.dto'; import { AssetBulkUploadCheckDto, AssetMediaCreateDto, AssetMediaOptionsDto, AssetMediaSize, - CheckExistingAssetsDto, UploadFieldName, } from 'src/dtos/asset-media.dto'; import { AssetDownloadOriginalDto } from 'src/dtos/asset.dto'; @@ -251,18 +249,6 @@ export class AssetMediaService extends BaseService { }); } - async checkExistingAssets( - auth: AuthDto, - checkExistingAssetsDto: CheckExistingAssetsDto, - ): Promise { - const existingIds = await this.assetRepository.getByDeviceIds( - auth.user.id, - checkExistingAssetsDto.deviceId, - checkExistingAssetsDto.deviceAssetIds, - ); - return { existingIds }; - } - async bulkUploadCheck(auth: AuthDto, dto: AssetBulkUploadCheckDto): Promise { const checksums: Buffer[] = dto.assets.map((asset) => fromChecksum(asset.checksum)); const results = await this.assetRepository.getByChecksums(auth.user.id, checksums); @@ -340,9 +326,6 @@ export class AssetMediaService extends BaseService { checksumAlgorithm: ChecksumAlgorithm.sha1File, originalPath: file.originalPath, - deviceAssetId: dto.deviceAssetId, - deviceId: dto.deviceId, - fileCreatedAt: dto.fileCreatedAt, fileModifiedAt: dto.fileModifiedAt, localDateTime: dto.fileCreatedAt, diff --git a/server/src/services/asset.service.spec.ts b/server/src/services/asset.service.spec.ts index bef6f332d3..13462a3246 100755 --- a/server/src/services/asset.service.spec.ts +++ b/server/src/services/asset.service.spec.ts @@ -685,20 +685,6 @@ describe(AssetService.name, () => { }); }); - describe('getUserAssetsByDeviceId', () => { - it('get assets by device id', async () => { - const assets = [AssetFactory.create(), AssetFactory.create()]; - - mocks.asset.getAllByDeviceId.mockResolvedValue(assets.map((asset) => asset.deviceAssetId)); - - const deviceId = 'device-id'; - const result = await sut.getUserAssetsByDeviceId(authStub.user1, deviceId); - - expect(result.length).toEqual(2); - expect(result).toEqual(assets.map((asset) => asset.deviceAssetId)); - }); - }); - describe('upsertMetadata', () => { it('should throw a bad request exception if duplicate keys are sent', async () => { const asset = AssetFactory.create(); diff --git a/server/src/services/asset.service.ts b/server/src/services/asset.service.ts index dde358234b..613029fe3c 100644 --- a/server/src/services/asset.service.ts +++ b/server/src/services/asset.service.ts @@ -59,10 +59,6 @@ export class AssetService extends BaseService { return mapStats(stats); } - async getUserAssetsByDeviceId(auth: AuthDto, deviceId: string) { - return this.assetRepository.getAllByDeviceId(auth.user.id, deviceId); - } - async get(auth: AuthDto, id: string): Promise { await this.requireAccess({ auth, permission: Permission.AssetRead, ids: [id] }); diff --git a/server/src/services/library.service.spec.ts b/server/src/services/library.service.spec.ts index 9a2ec00815..5b43ed9f61 100644 --- a/server/src/services/library.service.spec.ts +++ b/server/src/services/library.service.spec.ts @@ -570,7 +570,6 @@ describe(LibraryService.name, () => { ownerId: library.ownerId, libraryId: library.id, originalPath: '/data/user1/photo.jpg', - deviceId: 'Library Import', type: AssetType.Image, originalFileName: 'photo.jpg', isExternal: true, diff --git a/server/src/services/library.service.ts b/server/src/services/library.service.ts index 81e8c99d49..085650be6f 100644 --- a/server/src/services/library.service.ts +++ b/server/src/services/library.service.ts @@ -2,7 +2,7 @@ import { BadRequestException, Injectable } from '@nestjs/common'; import { Insertable } from 'kysely'; import { R_OK } from 'node:constants'; import { Stats } from 'node:fs'; -import path, { basename, isAbsolute, parse } from 'node:path'; +import path, { isAbsolute, parse } from 'node:path'; import picomatch from 'picomatch'; import { JOBS_LIBRARY_PAGINATION_SIZE } from 'src/constants'; import { StorageCore } from 'src/cores/storage.core'; @@ -411,9 +411,6 @@ export class LibraryService extends BaseService { fileCreatedAt: stat.mtime, fileModifiedAt: stat.mtime, localDateTime: stat.mtime, - // TODO: device asset id is deprecated, remove it - deviceAssetId: `${basename(assetPath)}`.replaceAll(/\s+/g, ''), - deviceId: 'Library Import', type: mimeTypes.isVideo(assetPath) ? AssetType.Video : AssetType.Image, originalFileName: parse(assetPath).base, isExternal: true, diff --git a/server/src/services/metadata.service.spec.ts b/server/src/services/metadata.service.spec.ts index f9d17079e3..245bb441a6 100644 --- a/server/src/services/metadata.service.spec.ts +++ b/server/src/services/metadata.service.spec.ts @@ -654,8 +654,6 @@ describe(MetadataService.name, () => { expect(mocks.asset.create).toHaveBeenCalledWith({ checksum: expect.any(Buffer), checksumAlgorithm: ChecksumAlgorithm.sha1File, - deviceAssetId: 'NONE', - deviceId: 'NONE', fileCreatedAt: asset.fileCreatedAt, fileModifiedAt: asset.fileModifiedAt, id: motionAsset.id, @@ -708,8 +706,6 @@ describe(MetadataService.name, () => { expect(mocks.asset.create).toHaveBeenCalledWith({ checksum: expect.any(Buffer), checksumAlgorithm: ChecksumAlgorithm.sha1File, - deviceAssetId: 'NONE', - deviceId: 'NONE', fileCreatedAt: asset.fileCreatedAt, fileModifiedAt: asset.fileModifiedAt, id: motionAsset.id, @@ -762,8 +758,6 @@ describe(MetadataService.name, () => { expect(mocks.asset.create).toHaveBeenCalledWith({ checksum: expect.any(Buffer), checksumAlgorithm: ChecksumAlgorithm.sha1File, - deviceAssetId: 'NONE', - deviceId: 'NONE', fileCreatedAt: asset.fileCreatedAt, fileModifiedAt: asset.fileModifiedAt, id: motionAsset.id, diff --git a/server/src/services/metadata.service.ts b/server/src/services/metadata.service.ts index c2cf66ad57..c548d94c74 100644 --- a/server/src/services/metadata.service.ts +++ b/server/src/services/metadata.service.ts @@ -681,8 +681,6 @@ export class MetadataService extends BaseService { originalPath: StorageCore.getAndroidMotionPath(asset, motionAssetId), originalFileName: `${parse(asset.originalFileName).name}.mp4`, visibility: AssetVisibility.Hidden, - deviceAssetId: 'NONE', - deviceId: 'NONE', }); isNewMotionAsset = true; diff --git a/server/src/utils/database.ts b/server/src/utils/database.ts index d25d99b491..44a4b37ad7 100644 --- a/server/src/utils/database.ts +++ b/server/src/utils/database.ts @@ -351,8 +351,6 @@ export function searchAssetBuilder(kysely: Kysely, options: AssetSearchBuild .where('asset_exif.rating', options.rating === null ? 'is' : '=', options.rating!), ) .$if(!!options.checksum, (qb) => qb.where('asset.checksum', '=', options.checksum!)) - .$if(!!options.deviceAssetId, (qb) => qb.where('asset.deviceAssetId', '=', options.deviceAssetId!)) - .$if(!!options.deviceId, (qb) => qb.where('asset.deviceId', '=', options.deviceId!)) .$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('asset.ownerId', '=', anyUuid(options.userIds!))) diff --git a/server/src/utils/duplicate.spec.ts b/server/src/utils/duplicate.spec.ts index 9c8822518b..d63f0d3e32 100644 --- a/server/src/utils/duplicate.spec.ts +++ b/server/src/utils/duplicate.spec.ts @@ -21,8 +21,6 @@ const createAsset = ( width: 1920, height: 1080, createdAt: new Date().toISOString(), - deviceAssetId: 'device-asset-1', - deviceId: 'device-1', ownerId: 'owner-1', originalPath: '/path/to/asset', originalFileName: 'asset.jpg', diff --git a/server/test/factories/asset.factory.ts b/server/test/factories/asset.factory.ts index 9caec31d6f..0a992719e3 100644 --- a/server/test/factories/asset.factory.ts +++ b/server/test/factories/asset.factory.ts @@ -54,8 +54,6 @@ export class AssetFactory { status: AssetStatus.Active, checksum: newSha1(), checksumAlgorithm: ChecksumAlgorithm.sha1File, - deviceAssetId: '', - deviceId: '', duplicateId: null, duration: null, fileCreatedAt: new Date(now++), diff --git a/server/test/mappers.ts b/server/test/mappers.ts index ed2c9431f3..edd85a386d 100644 --- a/server/test/mappers.ts +++ b/server/test/mappers.ts @@ -127,8 +127,6 @@ export const getForMetadataExtraction = (asset: ReturnType> = {}) => { const id = asset.id || newUuid(); const now = newDate(); const defaults: Insertable = { - deviceAssetId: '', - deviceId: '', originalFileName: '', checksum: randomBytes(32), checksumAlgorithm: ChecksumAlgorithm.sha1File, diff --git a/server/test/medium/specs/services/asset-media.service.spec.ts b/server/test/medium/specs/services/asset-media.service.spec.ts index f10844ca4a..1cfe8e9f84 100644 --- a/server/test/medium/specs/services/asset-media.service.spec.ts +++ b/server/test/medium/specs/services/asset-media.service.spec.ts @@ -52,8 +52,6 @@ describe(AssetService.name, () => { sut.uploadAsset( auth, { - deviceId: 'some-id', - deviceAssetId: 'some-id', fileModifiedAt: new Date(), fileCreatedAt: new Date(), assetData: Buffer.from('some data'), @@ -66,7 +64,7 @@ describe(AssetService.name, () => { }); expect(ctx.getMock(EventRepository).emit).toHaveBeenCalledWith('AssetCreate', { - asset: expect.objectContaining({ deviceAssetId: 'some-id' }), + asset: expect.objectContaining({}), }); }); @@ -87,8 +85,6 @@ describe(AssetService.name, () => { sut.uploadAsset( auth, { - deviceId: 'some-id', - deviceAssetId: 'some-id', fileModifiedAt: new Date(), fileCreatedAt: new Date(), assetData: Buffer.from('some data'), @@ -125,8 +121,6 @@ describe(AssetService.name, () => { const auth = factory.auth({ user: { id: user.id }, sharedLink }); const file = mediumFactory.uploadFile(); const uploadDto = { - deviceId: 'some-id', - deviceAssetId: 'some-id', fileModifiedAt: new Date(), fileCreatedAt: new Date(), assetData: Buffer.from('some data'), @@ -166,8 +160,6 @@ describe(AssetService.name, () => { const auth = factory.auth({ user: { id: user.id }, sharedLink }); const uploadDto = { - deviceId: 'some-id', - deviceAssetId: 'some-id', fileModifiedAt: new Date(), fileCreatedAt: new Date(), assetData: Buffer.from('some data'), @@ -206,8 +198,6 @@ describe(AssetService.name, () => { const auth = factory.auth({ user: { id: user.id }, sharedLink }); const uploadDto = { - deviceId: 'some-id', - deviceAssetId: 'some-id', fileModifiedAt: new Date(), fileCreatedAt: new Date(), assetData: Buffer.from('some data'), @@ -248,8 +238,6 @@ describe(AssetService.name, () => { const auth = factory.auth({ user: { id: user.id }, sharedLink }); const uploadDto = { - deviceId: 'some-id', - deviceAssetId: 'some-id', fileModifiedAt: new Date(), fileCreatedAt: new Date(), assetData: Buffer.from('some data'), diff --git a/server/test/repositories/asset.repository.mock.ts b/server/test/repositories/asset.repository.mock.ts index f3d6693578..e3a1dbdf05 100644 --- a/server/test/repositories/asset.repository.mock.ts +++ b/server/test/repositories/asset.repository.mock.ts @@ -15,12 +15,10 @@ export const newAssetRepositoryMock = (): Mocked fileUploader({ assetFile: file, deviceAssetId, albumId, isLockedAssets })), + uploadExecutionQueue.addTask(() => fileUploader({ deviceAssetId, assetFile: file, albumId, isLockedAssets })), ); } else { toastManager.warning(get(t)('unsupported_file_type', { values: { file: file.name, type: file.type } }), { @@ -132,6 +132,7 @@ type FileUploaderParams = { albumId?: string; replaceAssetId?: string; isLockedAssets?: boolean; + // TODO rework the asset uploader and remove this deviceAssetId: string; }; @@ -151,8 +152,6 @@ async function fileUploader({ try { const formData = new FormData(); for (const [key, value] of Object.entries({ - deviceAssetId, - deviceId: 'WEB', fileCreatedAt, fileModifiedAt: new Date(assetFile.lastModified).toISOString(), isFavorite: 'false', diff --git a/web/src/test-data/factories/asset-factory.ts b/web/src/test-data/factories/asset-factory.ts index f3d2e80747..10800bfb8f 100644 --- a/web/src/test-data/factories/asset-factory.ts +++ b/web/src/test-data/factories/asset-factory.ts @@ -7,9 +7,7 @@ import { Sync } from 'factory.ts'; export const assetFactory = Sync.makeFactory({ id: Sync.each(() => faker.string.uuid()), createdAt: Sync.each(() => faker.date.past().toISOString()), - deviceAssetId: Sync.each(() => faker.string.uuid()), ownerId: Sync.each(() => faker.string.uuid()), - deviceId: '', libraryId: Sync.each(() => faker.string.uuid()), type: Sync.each(() => faker.helpers.enumValue(AssetTypeEnum)), originalPath: Sync.each(() => faker.system.filePath()), From 792cb9148b229ecf4a908abdf016d48f18c18c0a Mon Sep 17 00:00:00 2001 From: Jason Rasmussen Date: Wed, 15 Apr 2026 15:58:26 -0400 Subject: [PATCH 06/16] chore!: rename API key schemas (#27828) chore!: rename API schemas --- mobile/openapi/README.md | 12 +- mobile/openapi/lib/api.dart | 8 +- mobile/openapi/lib/api/api_keys_api.dart | 42 ++-- mobile/openapi/lib/api/users_api.dart | 4 +- mobile/openapi/lib/api_client.dart | 16 +- .../openapi/lib/model/api_key_create_dto.dart | 38 ++-- .../model/api_key_create_response_dto.dart | 42 ++-- .../lib/model/api_key_response_dto.dart | 38 ++-- .../openapi/lib/model/api_key_update_dto.dart | 38 ++-- open-api/immich-openapi-specs.json | 204 +++++++++--------- server/src/controllers/api-key.controller.ts | 14 +- server/src/controllers/user.controller.ts | 2 +- server/src/dtos/api-key.dto.ts | 26 +-- server/src/services/api-key.service.ts | 14 +- .../user-settings-list.svelte | 4 +- 15 files changed, 251 insertions(+), 251 deletions(-) diff --git a/mobile/openapi/README.md b/mobile/openapi/README.md index e95790b020..0d41dbdcf4 100644 --- a/mobile/openapi/README.md +++ b/mobile/openapi/README.md @@ -56,10 +56,10 @@ import 'package:openapi/api.dart'; //defaultApiClient.getAuthentication('bearer').setAccessToken(yourTokenGeneratorFunction); final api_instance = APIKeysApi(); -final aPIKeyCreateDto = APIKeyCreateDto(); // APIKeyCreateDto | +final apiKeyCreateDto = ApiKeyCreateDto(); // ApiKeyCreateDto | try { - final result = api_instance.createApiKey(aPIKeyCreateDto); + final result = api_instance.createApiKey(apiKeyCreateDto); print(result); } catch (e) { print('Exception when calling APIKeysApi->createApiKey: $e\n'); @@ -319,10 +319,6 @@ Class | Method | HTTP request | Description ## Documentation For Models - - [APIKeyCreateDto](doc//APIKeyCreateDto.md) - - [APIKeyCreateResponseDto](doc//APIKeyCreateResponseDto.md) - - [APIKeyResponseDto](doc//APIKeyResponseDto.md) - - [APIKeyUpdateDto](doc//APIKeyUpdateDto.md) - [ActivityCreateDto](doc//ActivityCreateDto.md) - [ActivityResponseDto](doc//ActivityResponseDto.md) - [ActivityStatisticsResponseDto](doc//ActivityStatisticsResponseDto.md) @@ -338,6 +334,10 @@ Class | Method | HTTP request | Description - [AlbumsAddAssetsResponseDto](doc//AlbumsAddAssetsResponseDto.md) - [AlbumsResponse](doc//AlbumsResponse.md) - [AlbumsUpdate](doc//AlbumsUpdate.md) + - [ApiKeyCreateDto](doc//ApiKeyCreateDto.md) + - [ApiKeyCreateResponseDto](doc//ApiKeyCreateResponseDto.md) + - [ApiKeyResponseDto](doc//ApiKeyResponseDto.md) + - [ApiKeyUpdateDto](doc//ApiKeyUpdateDto.md) - [AssetBulkDeleteDto](doc//AssetBulkDeleteDto.md) - [AssetBulkUpdateDto](doc//AssetBulkUpdateDto.md) - [AssetBulkUploadCheckDto](doc//AssetBulkUploadCheckDto.md) diff --git a/mobile/openapi/lib/api.dart b/mobile/openapi/lib/api.dart index 15bc8190dc..ec1ea6e6b2 100644 --- a/mobile/openapi/lib/api.dart +++ b/mobile/openapi/lib/api.dart @@ -68,10 +68,6 @@ part 'api/users_admin_api.dart'; part 'api/views_api.dart'; part 'api/workflows_api.dart'; -part 'model/api_key_create_dto.dart'; -part 'model/api_key_create_response_dto.dart'; -part 'model/api_key_response_dto.dart'; -part 'model/api_key_update_dto.dart'; part 'model/activity_create_dto.dart'; part 'model/activity_response_dto.dart'; part 'model/activity_statistics_response_dto.dart'; @@ -87,6 +83,10 @@ part 'model/albums_add_assets_dto.dart'; part 'model/albums_add_assets_response_dto.dart'; part 'model/albums_response.dart'; part 'model/albums_update.dart'; +part 'model/api_key_create_dto.dart'; +part 'model/api_key_create_response_dto.dart'; +part 'model/api_key_response_dto.dart'; +part 'model/api_key_update_dto.dart'; part 'model/asset_bulk_delete_dto.dart'; part 'model/asset_bulk_update_dto.dart'; part 'model/asset_bulk_upload_check_dto.dart'; diff --git a/mobile/openapi/lib/api/api_keys_api.dart b/mobile/openapi/lib/api/api_keys_api.dart index 0bd26575c6..3ca85265c4 100644 --- a/mobile/openapi/lib/api/api_keys_api.dart +++ b/mobile/openapi/lib/api/api_keys_api.dart @@ -24,13 +24,13 @@ class APIKeysApi { /// /// Parameters: /// - /// * [APIKeyCreateDto] aPIKeyCreateDto (required): - Future createApiKeyWithHttpInfo(APIKeyCreateDto aPIKeyCreateDto,) async { + /// * [ApiKeyCreateDto] apiKeyCreateDto (required): + Future createApiKeyWithHttpInfo(ApiKeyCreateDto apiKeyCreateDto,) async { // ignore: prefer_const_declarations final apiPath = r'/api-keys'; // ignore: prefer_final_locals - Object? postBody = aPIKeyCreateDto; + Object? postBody = apiKeyCreateDto; final queryParams = []; final headerParams = {}; @@ -56,9 +56,9 @@ class APIKeysApi { /// /// Parameters: /// - /// * [APIKeyCreateDto] aPIKeyCreateDto (required): - Future createApiKey(APIKeyCreateDto aPIKeyCreateDto,) async { - final response = await createApiKeyWithHttpInfo(aPIKeyCreateDto,); + /// * [ApiKeyCreateDto] apiKeyCreateDto (required): + Future createApiKey(ApiKeyCreateDto apiKeyCreateDto,) async { + final response = await createApiKeyWithHttpInfo(apiKeyCreateDto,); if (response.statusCode >= HttpStatus.badRequest) { throw ApiException(response.statusCode, await _decodeBodyBytes(response)); } @@ -66,7 +66,7 @@ class APIKeysApi { // 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), 'APIKeyCreateResponseDto',) as APIKeyCreateResponseDto; + return await apiClient.deserializeAsync(await _decodeBodyBytes(response), 'ApiKeyCreateResponseDto',) as ApiKeyCreateResponseDto; } return null; @@ -163,7 +163,7 @@ class APIKeysApi { /// Parameters: /// /// * [String] id (required): - Future getApiKey(String id,) async { + Future getApiKey(String id,) async { final response = await getApiKeyWithHttpInfo(id,); if (response.statusCode >= HttpStatus.badRequest) { throw ApiException(response.statusCode, await _decodeBodyBytes(response)); @@ -172,7 +172,7 @@ class APIKeysApi { // 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), 'APIKeyResponseDto',) as APIKeyResponseDto; + return await apiClient.deserializeAsync(await _decodeBodyBytes(response), 'ApiKeyResponseDto',) as ApiKeyResponseDto; } return null; @@ -211,7 +211,7 @@ class APIKeysApi { /// List all API keys /// /// Retrieve all API keys of the current user. - Future?> getApiKeys() async { + Future?> getApiKeys() async { final response = await getApiKeysWithHttpInfo(); if (response.statusCode >= HttpStatus.badRequest) { throw ApiException(response.statusCode, await _decodeBodyBytes(response)); @@ -221,8 +221,8 @@ class APIKeysApi { // FormatException when trying to decode an empty string. if (response.body.isNotEmpty && response.statusCode != HttpStatus.noContent) { final responseBody = await _decodeBodyBytes(response); - return (await apiClient.deserializeAsync(responseBody, 'List') as List) - .cast() + return (await apiClient.deserializeAsync(responseBody, 'List') as List) + .cast() .toList(growable: false); } @@ -262,7 +262,7 @@ class APIKeysApi { /// Retrieve the current API key /// /// Retrieve the API key that is used to access this endpoint. - Future getMyApiKey() async { + Future getMyApiKey() async { final response = await getMyApiKeyWithHttpInfo(); if (response.statusCode >= HttpStatus.badRequest) { throw ApiException(response.statusCode, await _decodeBodyBytes(response)); @@ -271,7 +271,7 @@ class APIKeysApi { // 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), 'APIKeyResponseDto',) as APIKeyResponseDto; + return await apiClient.deserializeAsync(await _decodeBodyBytes(response), 'ApiKeyResponseDto',) as ApiKeyResponseDto; } return null; @@ -287,14 +287,14 @@ class APIKeysApi { /// /// * [String] id (required): /// - /// * [APIKeyUpdateDto] aPIKeyUpdateDto (required): - Future updateApiKeyWithHttpInfo(String id, APIKeyUpdateDto aPIKeyUpdateDto,) async { + /// * [ApiKeyUpdateDto] apiKeyUpdateDto (required): + Future updateApiKeyWithHttpInfo(String id, ApiKeyUpdateDto apiKeyUpdateDto,) async { // ignore: prefer_const_declarations final apiPath = r'/api-keys/{id}' .replaceAll('{id}', id); // ignore: prefer_final_locals - Object? postBody = aPIKeyUpdateDto; + Object? postBody = apiKeyUpdateDto; final queryParams = []; final headerParams = {}; @@ -322,9 +322,9 @@ class APIKeysApi { /// /// * [String] id (required): /// - /// * [APIKeyUpdateDto] aPIKeyUpdateDto (required): - Future updateApiKey(String id, APIKeyUpdateDto aPIKeyUpdateDto,) async { - final response = await updateApiKeyWithHttpInfo(id, aPIKeyUpdateDto,); + /// * [ApiKeyUpdateDto] apiKeyUpdateDto (required): + Future updateApiKey(String id, ApiKeyUpdateDto apiKeyUpdateDto,) async { + final response = await updateApiKeyWithHttpInfo(id, apiKeyUpdateDto,); if (response.statusCode >= HttpStatus.badRequest) { throw ApiException(response.statusCode, await _decodeBodyBytes(response)); } @@ -332,7 +332,7 @@ class APIKeysApi { // 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), 'APIKeyResponseDto',) as APIKeyResponseDto; + return await apiClient.deserializeAsync(await _decodeBodyBytes(response), 'ApiKeyResponseDto',) as ApiKeyResponseDto; } return null; diff --git a/mobile/openapi/lib/api/users_api.dart b/mobile/openapi/lib/api/users_api.dart index 1d905b1e22..401cf4e94b 100644 --- a/mobile/openapi/lib/api/users_api.dart +++ b/mobile/openapi/lib/api/users_api.dart @@ -731,7 +731,7 @@ class UsersApi { /// Update current user /// - /// Update the current user making teh API request. + /// Update the current user making the API request. /// /// Note: This method returns the HTTP [Response]. /// @@ -765,7 +765,7 @@ class UsersApi { /// Update current user /// - /// Update the current user making teh API request. + /// Update the current user making the API request. /// /// Parameters: /// diff --git a/mobile/openapi/lib/api_client.dart b/mobile/openapi/lib/api_client.dart index c4782a2340..3bc85a9070 100644 --- a/mobile/openapi/lib/api_client.dart +++ b/mobile/openapi/lib/api_client.dart @@ -182,14 +182,6 @@ class ApiClient { return valueString == 'true' || valueString == '1'; case 'DateTime': return value is DateTime ? value : DateTime.tryParse(value); - case 'APIKeyCreateDto': - return APIKeyCreateDto.fromJson(value); - case 'APIKeyCreateResponseDto': - return APIKeyCreateResponseDto.fromJson(value); - case 'APIKeyResponseDto': - return APIKeyResponseDto.fromJson(value); - case 'APIKeyUpdateDto': - return APIKeyUpdateDto.fromJson(value); case 'ActivityCreateDto': return ActivityCreateDto.fromJson(value); case 'ActivityResponseDto': @@ -220,6 +212,14 @@ class ApiClient { return AlbumsResponse.fromJson(value); case 'AlbumsUpdate': return AlbumsUpdate.fromJson(value); + case 'ApiKeyCreateDto': + return ApiKeyCreateDto.fromJson(value); + case 'ApiKeyCreateResponseDto': + return ApiKeyCreateResponseDto.fromJson(value); + case 'ApiKeyResponseDto': + return ApiKeyResponseDto.fromJson(value); + case 'ApiKeyUpdateDto': + return ApiKeyUpdateDto.fromJson(value); case 'AssetBulkDeleteDto': return AssetBulkDeleteDto.fromJson(value); case 'AssetBulkUpdateDto': diff --git a/mobile/openapi/lib/model/api_key_create_dto.dart b/mobile/openapi/lib/model/api_key_create_dto.dart index e64b127820..6d3ffc1eb1 100644 --- a/mobile/openapi/lib/model/api_key_create_dto.dart +++ b/mobile/openapi/lib/model/api_key_create_dto.dart @@ -10,9 +10,9 @@ part of openapi.api; -class APIKeyCreateDto { - /// Returns a new [APIKeyCreateDto] instance. - APIKeyCreateDto({ +class ApiKeyCreateDto { + /// Returns a new [ApiKeyCreateDto] instance. + ApiKeyCreateDto({ this.name, this.permissions = const [], }); @@ -30,7 +30,7 @@ class APIKeyCreateDto { List permissions; @override - bool operator ==(Object other) => identical(this, other) || other is APIKeyCreateDto && + bool operator ==(Object other) => identical(this, other) || other is ApiKeyCreateDto && other.name == name && _deepEquality.equals(other.permissions, permissions); @@ -41,7 +41,7 @@ class APIKeyCreateDto { (permissions.hashCode); @override - String toString() => 'APIKeyCreateDto[name=$name, permissions=$permissions]'; + String toString() => 'ApiKeyCreateDto[name=$name, permissions=$permissions]'; Map toJson() { final json = {}; @@ -54,15 +54,15 @@ class APIKeyCreateDto { return json; } - /// Returns a new [APIKeyCreateDto] instance and imports its values from + /// Returns a new [ApiKeyCreateDto] instance and imports its values from /// [value] if it's a [Map], null otherwise. // ignore: prefer_constructors_over_static_methods - static APIKeyCreateDto? fromJson(dynamic value) { - upgradeDto(value, "APIKeyCreateDto"); + static ApiKeyCreateDto? fromJson(dynamic value) { + upgradeDto(value, "ApiKeyCreateDto"); if (value is Map) { final json = value.cast(); - return APIKeyCreateDto( + return ApiKeyCreateDto( name: mapValueOfType(json, r'name'), permissions: Permission.listFromJson(json[r'permissions']), ); @@ -70,11 +70,11 @@ class APIKeyCreateDto { return null; } - static List listFromJson(dynamic json, {bool growable = false,}) { - final result = []; + static List listFromJson(dynamic json, {bool growable = false,}) { + final result = []; if (json is List && json.isNotEmpty) { for (final row in json) { - final value = APIKeyCreateDto.fromJson(row); + final value = ApiKeyCreateDto.fromJson(row); if (value != null) { result.add(value); } @@ -83,12 +83,12 @@ class APIKeyCreateDto { return result.toList(growable: growable); } - static Map mapFromJson(dynamic json) { - final map = {}; + static Map mapFromJson(dynamic json) { + final map = {}; if (json is Map && json.isNotEmpty) { json = json.cast(); // ignore: parameter_assignments for (final entry in json.entries) { - final value = APIKeyCreateDto.fromJson(entry.value); + final value = ApiKeyCreateDto.fromJson(entry.value); if (value != null) { map[entry.key] = value; } @@ -97,14 +97,14 @@ class APIKeyCreateDto { return map; } - // maps a json object with a list of APIKeyCreateDto-objects as value to a dart map - static Map> mapListFromJson(dynamic json, {bool growable = false,}) { - final map = >{}; + // maps a json object with a list of ApiKeyCreateDto-objects as value to a dart map + static Map> mapListFromJson(dynamic json, {bool growable = false,}) { + final map = >{}; if (json is Map && json.isNotEmpty) { // ignore: parameter_assignments json = json.cast(); for (final entry in json.entries) { - map[entry.key] = APIKeyCreateDto.listFromJson(entry.value, growable: growable,); + map[entry.key] = ApiKeyCreateDto.listFromJson(entry.value, growable: growable,); } } return map; diff --git a/mobile/openapi/lib/model/api_key_create_response_dto.dart b/mobile/openapi/lib/model/api_key_create_response_dto.dart index 7540c4bb26..77b19ebfd2 100644 --- a/mobile/openapi/lib/model/api_key_create_response_dto.dart +++ b/mobile/openapi/lib/model/api_key_create_response_dto.dart @@ -10,20 +10,20 @@ part of openapi.api; -class APIKeyCreateResponseDto { - /// Returns a new [APIKeyCreateResponseDto] instance. - APIKeyCreateResponseDto({ +class ApiKeyCreateResponseDto { + /// Returns a new [ApiKeyCreateResponseDto] instance. + ApiKeyCreateResponseDto({ required this.apiKey, required this.secret, }); - APIKeyResponseDto apiKey; + ApiKeyResponseDto apiKey; /// API key secret (only shown once) String secret; @override - bool operator ==(Object other) => identical(this, other) || other is APIKeyCreateResponseDto && + bool operator ==(Object other) => identical(this, other) || other is ApiKeyCreateResponseDto && other.apiKey == apiKey && other.secret == secret; @@ -34,7 +34,7 @@ class APIKeyCreateResponseDto { (secret.hashCode); @override - String toString() => 'APIKeyCreateResponseDto[apiKey=$apiKey, secret=$secret]'; + String toString() => 'ApiKeyCreateResponseDto[apiKey=$apiKey, secret=$secret]'; Map toJson() { final json = {}; @@ -43,27 +43,27 @@ class APIKeyCreateResponseDto { return json; } - /// Returns a new [APIKeyCreateResponseDto] instance and imports its values from + /// Returns a new [ApiKeyCreateResponseDto] instance and imports its values from /// [value] if it's a [Map], null otherwise. // ignore: prefer_constructors_over_static_methods - static APIKeyCreateResponseDto? fromJson(dynamic value) { - upgradeDto(value, "APIKeyCreateResponseDto"); + static ApiKeyCreateResponseDto? fromJson(dynamic value) { + upgradeDto(value, "ApiKeyCreateResponseDto"); if (value is Map) { final json = value.cast(); - return APIKeyCreateResponseDto( - apiKey: APIKeyResponseDto.fromJson(json[r'apiKey'])!, + return ApiKeyCreateResponseDto( + apiKey: ApiKeyResponseDto.fromJson(json[r'apiKey'])!, secret: mapValueOfType(json, r'secret')!, ); } return null; } - static List listFromJson(dynamic json, {bool growable = false,}) { - final result = []; + static List listFromJson(dynamic json, {bool growable = false,}) { + final result = []; if (json is List && json.isNotEmpty) { for (final row in json) { - final value = APIKeyCreateResponseDto.fromJson(row); + final value = ApiKeyCreateResponseDto.fromJson(row); if (value != null) { result.add(value); } @@ -72,12 +72,12 @@ class APIKeyCreateResponseDto { return result.toList(growable: growable); } - static Map mapFromJson(dynamic json) { - final map = {}; + static Map mapFromJson(dynamic json) { + final map = {}; if (json is Map && json.isNotEmpty) { json = json.cast(); // ignore: parameter_assignments for (final entry in json.entries) { - final value = APIKeyCreateResponseDto.fromJson(entry.value); + final value = ApiKeyCreateResponseDto.fromJson(entry.value); if (value != null) { map[entry.key] = value; } @@ -86,14 +86,14 @@ class APIKeyCreateResponseDto { return map; } - // maps a json object with a list of APIKeyCreateResponseDto-objects as value to a dart map - static Map> mapListFromJson(dynamic json, {bool growable = false,}) { - final map = >{}; + // maps a json object with a list of ApiKeyCreateResponseDto-objects as value to a dart map + static Map> mapListFromJson(dynamic json, {bool growable = false,}) { + final map = >{}; if (json is Map && json.isNotEmpty) { // ignore: parameter_assignments json = json.cast(); for (final entry in json.entries) { - map[entry.key] = APIKeyCreateResponseDto.listFromJson(entry.value, growable: growable,); + map[entry.key] = ApiKeyCreateResponseDto.listFromJson(entry.value, growable: growable,); } } return map; diff --git a/mobile/openapi/lib/model/api_key_response_dto.dart b/mobile/openapi/lib/model/api_key_response_dto.dart index d5b8bf8b41..79099188a3 100644 --- a/mobile/openapi/lib/model/api_key_response_dto.dart +++ b/mobile/openapi/lib/model/api_key_response_dto.dart @@ -10,9 +10,9 @@ part of openapi.api; -class APIKeyResponseDto { - /// Returns a new [APIKeyResponseDto] instance. - APIKeyResponseDto({ +class ApiKeyResponseDto { + /// Returns a new [ApiKeyResponseDto] instance. + ApiKeyResponseDto({ required this.createdAt, required this.id, required this.name, @@ -36,7 +36,7 @@ class APIKeyResponseDto { DateTime updatedAt; @override - bool operator ==(Object other) => identical(this, other) || other is APIKeyResponseDto && + bool operator ==(Object other) => identical(this, other) || other is ApiKeyResponseDto && other.createdAt == createdAt && other.id == id && other.name == name && @@ -53,7 +53,7 @@ class APIKeyResponseDto { (updatedAt.hashCode); @override - String toString() => 'APIKeyResponseDto[createdAt=$createdAt, id=$id, name=$name, permissions=$permissions, updatedAt=$updatedAt]'; + String toString() => 'ApiKeyResponseDto[createdAt=$createdAt, id=$id, name=$name, permissions=$permissions, updatedAt=$updatedAt]'; Map toJson() { final json = {}; @@ -69,15 +69,15 @@ class APIKeyResponseDto { return json; } - /// Returns a new [APIKeyResponseDto] instance and imports its values from + /// Returns a new [ApiKeyResponseDto] instance and imports its values from /// [value] if it's a [Map], null otherwise. // ignore: prefer_constructors_over_static_methods - static APIKeyResponseDto? fromJson(dynamic value) { - upgradeDto(value, "APIKeyResponseDto"); + static ApiKeyResponseDto? fromJson(dynamic value) { + upgradeDto(value, "ApiKeyResponseDto"); if (value is Map) { final json = value.cast(); - return APIKeyResponseDto( + return ApiKeyResponseDto( createdAt: mapDateTime(json, r'createdAt', 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))$/')!, id: mapValueOfType(json, r'id')!, name: mapValueOfType(json, r'name')!, @@ -88,11 +88,11 @@ class APIKeyResponseDto { return null; } - static List listFromJson(dynamic json, {bool growable = false,}) { - final result = []; + static List listFromJson(dynamic json, {bool growable = false,}) { + final result = []; if (json is List && json.isNotEmpty) { for (final row in json) { - final value = APIKeyResponseDto.fromJson(row); + final value = ApiKeyResponseDto.fromJson(row); if (value != null) { result.add(value); } @@ -101,12 +101,12 @@ class APIKeyResponseDto { return result.toList(growable: growable); } - static Map mapFromJson(dynamic json) { - final map = {}; + static Map mapFromJson(dynamic json) { + final map = {}; if (json is Map && json.isNotEmpty) { json = json.cast(); // ignore: parameter_assignments for (final entry in json.entries) { - final value = APIKeyResponseDto.fromJson(entry.value); + final value = ApiKeyResponseDto.fromJson(entry.value); if (value != null) { map[entry.key] = value; } @@ -115,14 +115,14 @@ class APIKeyResponseDto { return map; } - // maps a json object with a list of APIKeyResponseDto-objects as value to a dart map - static Map> mapListFromJson(dynamic json, {bool growable = false,}) { - final map = >{}; + // maps a json object with a list of ApiKeyResponseDto-objects as value to a dart map + static Map> mapListFromJson(dynamic json, {bool growable = false,}) { + final map = >{}; if (json is Map && json.isNotEmpty) { // ignore: parameter_assignments json = json.cast(); for (final entry in json.entries) { - map[entry.key] = APIKeyResponseDto.listFromJson(entry.value, growable: growable,); + map[entry.key] = ApiKeyResponseDto.listFromJson(entry.value, growable: growable,); } } return map; diff --git a/mobile/openapi/lib/model/api_key_update_dto.dart b/mobile/openapi/lib/model/api_key_update_dto.dart index ba107bcda2..c8df4be654 100644 --- a/mobile/openapi/lib/model/api_key_update_dto.dart +++ b/mobile/openapi/lib/model/api_key_update_dto.dart @@ -10,9 +10,9 @@ part of openapi.api; -class APIKeyUpdateDto { - /// Returns a new [APIKeyUpdateDto] instance. - APIKeyUpdateDto({ +class ApiKeyUpdateDto { + /// Returns a new [ApiKeyUpdateDto] instance. + ApiKeyUpdateDto({ this.name, this.permissions = const [], }); @@ -30,7 +30,7 @@ class APIKeyUpdateDto { List permissions; @override - bool operator ==(Object other) => identical(this, other) || other is APIKeyUpdateDto && + bool operator ==(Object other) => identical(this, other) || other is ApiKeyUpdateDto && other.name == name && _deepEquality.equals(other.permissions, permissions); @@ -41,7 +41,7 @@ class APIKeyUpdateDto { (permissions.hashCode); @override - String toString() => 'APIKeyUpdateDto[name=$name, permissions=$permissions]'; + String toString() => 'ApiKeyUpdateDto[name=$name, permissions=$permissions]'; Map toJson() { final json = {}; @@ -54,15 +54,15 @@ class APIKeyUpdateDto { return json; } - /// Returns a new [APIKeyUpdateDto] instance and imports its values from + /// Returns a new [ApiKeyUpdateDto] instance and imports its values from /// [value] if it's a [Map], null otherwise. // ignore: prefer_constructors_over_static_methods - static APIKeyUpdateDto? fromJson(dynamic value) { - upgradeDto(value, "APIKeyUpdateDto"); + static ApiKeyUpdateDto? fromJson(dynamic value) { + upgradeDto(value, "ApiKeyUpdateDto"); if (value is Map) { final json = value.cast(); - return APIKeyUpdateDto( + return ApiKeyUpdateDto( name: mapValueOfType(json, r'name'), permissions: Permission.listFromJson(json[r'permissions']), ); @@ -70,11 +70,11 @@ class APIKeyUpdateDto { return null; } - static List listFromJson(dynamic json, {bool growable = false,}) { - final result = []; + static List listFromJson(dynamic json, {bool growable = false,}) { + final result = []; if (json is List && json.isNotEmpty) { for (final row in json) { - final value = APIKeyUpdateDto.fromJson(row); + final value = ApiKeyUpdateDto.fromJson(row); if (value != null) { result.add(value); } @@ -83,12 +83,12 @@ class APIKeyUpdateDto { return result.toList(growable: growable); } - static Map mapFromJson(dynamic json) { - final map = {}; + static Map mapFromJson(dynamic json) { + final map = {}; if (json is Map && json.isNotEmpty) { json = json.cast(); // ignore: parameter_assignments for (final entry in json.entries) { - final value = APIKeyUpdateDto.fromJson(entry.value); + final value = ApiKeyUpdateDto.fromJson(entry.value); if (value != null) { map[entry.key] = value; } @@ -97,14 +97,14 @@ class APIKeyUpdateDto { return map; } - // maps a json object with a list of APIKeyUpdateDto-objects as value to a dart map - static Map> mapListFromJson(dynamic json, {bool growable = false,}) { - final map = >{}; + // maps a json object with a list of ApiKeyUpdateDto-objects as value to a dart map + static Map> mapListFromJson(dynamic json, {bool growable = false,}) { + final map = >{}; if (json is Map && json.isNotEmpty) { // ignore: parameter_assignments json = json.cast(); for (final entry in json.entries) { - map[entry.key] = APIKeyUpdateDto.listFromJson(entry.value, growable: growable,); + map[entry.key] = ApiKeyUpdateDto.listFromJson(entry.value, growable: growable,); } } return map; diff --git a/open-api/immich-openapi-specs.json b/open-api/immich-openapi-specs.json index fd6f9c5151..2d83a302fe 100644 --- a/open-api/immich-openapi-specs.json +++ b/open-api/immich-openapi-specs.json @@ -2441,7 +2441,7 @@ "application/json": { "schema": { "items": { - "$ref": "#/components/schemas/APIKeyResponseDto" + "$ref": "#/components/schemas/ApiKeyResponseDto" }, "type": "array" } @@ -2490,7 +2490,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/APIKeyCreateDto" + "$ref": "#/components/schemas/ApiKeyCreateDto" } } }, @@ -2501,7 +2501,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/APIKeyCreateResponseDto" + "$ref": "#/components/schemas/ApiKeyCreateResponseDto" } } }, @@ -2551,7 +2551,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/APIKeyResponseDto" + "$ref": "#/components/schemas/ApiKeyResponseDto" } } }, @@ -2663,7 +2663,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/APIKeyResponseDto" + "$ref": "#/components/schemas/ApiKeyResponseDto" } } }, @@ -2721,7 +2721,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/APIKeyUpdateDto" + "$ref": "#/components/schemas/ApiKeyUpdateDto" } } }, @@ -2732,7 +2732,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/APIKeyResponseDto" + "$ref": "#/components/schemas/ApiKeyResponseDto" } } }, @@ -13831,7 +13831,7 @@ "x-immich-state": "Stable" }, "put": { - "description": "Update the current user making teh API request.", + "description": "Update the current user making the API request.", "operationId": "updateMyUser", "parameters": [], "requestBody": { @@ -15090,100 +15090,6 @@ } }, "schemas": { - "APIKeyCreateDto": { - "properties": { - "name": { - "description": "API key name", - "type": "string" - }, - "permissions": { - "description": "List of permissions", - "items": { - "$ref": "#/components/schemas/Permission" - }, - "minItems": 1, - "type": "array" - } - }, - "required": [ - "permissions" - ], - "type": "object" - }, - "APIKeyCreateResponseDto": { - "properties": { - "apiKey": { - "$ref": "#/components/schemas/APIKeyResponseDto" - }, - "secret": { - "description": "API key secret (only shown once)", - "type": "string" - } - }, - "required": [ - "apiKey", - "secret" - ], - "type": "object" - }, - "APIKeyResponseDto": { - "properties": { - "createdAt": { - "description": "Creation date", - "example": "2024-01-01T00:00:00.000Z", - "format": "date-time", - "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))$", - "type": "string" - }, - "id": { - "description": "API key ID", - "type": "string" - }, - "name": { - "description": "API key name", - "type": "string" - }, - "permissions": { - "description": "List of permissions", - "items": { - "$ref": "#/components/schemas/Permission" - }, - "type": "array" - }, - "updatedAt": { - "description": "Last update date", - "example": "2024-01-01T00:00:00.000Z", - "format": "date-time", - "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))$", - "type": "string" - } - }, - "required": [ - "createdAt", - "id", - "name", - "permissions", - "updatedAt" - ], - "type": "object" - }, - "APIKeyUpdateDto": { - "properties": { - "name": { - "description": "API key name", - "type": "string" - }, - "permissions": { - "description": "List of permissions", - "items": { - "$ref": "#/components/schemas/Permission" - }, - "minItems": 1, - "type": "array" - } - }, - "type": "object" - }, "ActivityCreateDto": { "description": "Activity create", "properties": { @@ -15564,6 +15470,100 @@ }, "type": "object" }, + "ApiKeyCreateDto": { + "properties": { + "name": { + "description": "API key name", + "type": "string" + }, + "permissions": { + "description": "List of permissions", + "items": { + "$ref": "#/components/schemas/Permission" + }, + "minItems": 1, + "type": "array" + } + }, + "required": [ + "permissions" + ], + "type": "object" + }, + "ApiKeyCreateResponseDto": { + "properties": { + "apiKey": { + "$ref": "#/components/schemas/ApiKeyResponseDto" + }, + "secret": { + "description": "API key secret (only shown once)", + "type": "string" + } + }, + "required": [ + "apiKey", + "secret" + ], + "type": "object" + }, + "ApiKeyResponseDto": { + "properties": { + "createdAt": { + "description": "Creation date", + "example": "2024-01-01T00:00:00.000Z", + "format": "date-time", + "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))$", + "type": "string" + }, + "id": { + "description": "API key ID", + "type": "string" + }, + "name": { + "description": "API key name", + "type": "string" + }, + "permissions": { + "description": "List of permissions", + "items": { + "$ref": "#/components/schemas/Permission" + }, + "type": "array" + }, + "updatedAt": { + "description": "Last update date", + "example": "2024-01-01T00:00:00.000Z", + "format": "date-time", + "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))$", + "type": "string" + } + }, + "required": [ + "createdAt", + "id", + "name", + "permissions", + "updatedAt" + ], + "type": "object" + }, + "ApiKeyUpdateDto": { + "properties": { + "name": { + "description": "API key name", + "type": "string" + }, + "permissions": { + "description": "List of permissions", + "items": { + "$ref": "#/components/schemas/Permission" + }, + "minItems": 1, + "type": "array" + } + }, + "type": "object" + }, "AssetBulkDeleteDto": { "properties": { "force": { diff --git a/server/src/controllers/api-key.controller.ts b/server/src/controllers/api-key.controller.ts index 61ad203331..9238a8b57c 100644 --- a/server/src/controllers/api-key.controller.ts +++ b/server/src/controllers/api-key.controller.ts @@ -1,7 +1,7 @@ import { Body, Controller, Delete, Get, HttpCode, HttpStatus, Param, Post, Put } from '@nestjs/common'; import { ApiTags } from '@nestjs/swagger'; import { Endpoint, HistoryBuilder } from 'src/decorators'; -import { APIKeyCreateDto, APIKeyCreateResponseDto, APIKeyResponseDto, APIKeyUpdateDto } from 'src/dtos/api-key.dto'; +import { ApiKeyCreateDto, ApiKeyCreateResponseDto, ApiKeyResponseDto, ApiKeyUpdateDto } from 'src/dtos/api-key.dto'; import { AuthDto } from 'src/dtos/auth.dto'; import { ApiTag, Permission } from 'src/enum'; import { Auth, Authenticated } from 'src/middleware/auth.guard'; @@ -20,7 +20,7 @@ export class ApiKeyController { description: 'Creates a new API key. It will be limited to the permissions specified.', history: new HistoryBuilder().added('v1').beta('v1').stable('v2'), }) - createApiKey(@Auth() auth: AuthDto, @Body() dto: APIKeyCreateDto): Promise { + createApiKey(@Auth() auth: AuthDto, @Body() dto: ApiKeyCreateDto): Promise { return this.service.create(auth, dto); } @@ -31,7 +31,7 @@ export class ApiKeyController { description: 'Retrieve all API keys of the current user.', history: new HistoryBuilder().added('v1').beta('v1').stable('v2'), }) - getApiKeys(@Auth() auth: AuthDto): Promise { + getApiKeys(@Auth() auth: AuthDto): Promise { return this.service.getAll(auth); } @@ -42,7 +42,7 @@ export class ApiKeyController { description: 'Retrieve the API key that is used to access this endpoint.', history: new HistoryBuilder().added('v1').beta('v1').stable('v2'), }) - async getMyApiKey(@Auth() auth: AuthDto): Promise { + async getMyApiKey(@Auth() auth: AuthDto): Promise { return this.service.getMine(auth); } @@ -53,7 +53,7 @@ export class ApiKeyController { description: 'Retrieve an API key by its ID. The current user must own this API key.', history: new HistoryBuilder().added('v1').beta('v1').stable('v2'), }) - getApiKey(@Auth() auth: AuthDto, @Param() { id }: UUIDParamDto): Promise { + getApiKey(@Auth() auth: AuthDto, @Param() { id }: UUIDParamDto): Promise { return this.service.getById(auth, id); } @@ -67,8 +67,8 @@ export class ApiKeyController { updateApiKey( @Auth() auth: AuthDto, @Param() { id }: UUIDParamDto, - @Body() dto: APIKeyUpdateDto, - ): Promise { + @Body() dto: ApiKeyUpdateDto, + ): Promise { return this.service.update(auth, id, dto); } diff --git a/server/src/controllers/user.controller.ts b/server/src/controllers/user.controller.ts index 9c0dd3db7a..2db0ca182b 100644 --- a/server/src/controllers/user.controller.ts +++ b/server/src/controllers/user.controller.ts @@ -64,7 +64,7 @@ export class UserController { @Authenticated({ permission: Permission.UserUpdate }) @Endpoint({ summary: 'Update current user', - description: 'Update the current user making teh API request.', + description: 'Update the current user making the API request.', history: new HistoryBuilder().added('v1').beta('v1').stable('v2'), }) updateMyUser(@Auth() auth: AuthDto, @Body() dto: UserUpdateMeDto): Promise { diff --git a/server/src/dtos/api-key.dto.ts b/server/src/dtos/api-key.dto.ts index 8c1ffb53ca..176812cc4f 100644 --- a/server/src/dtos/api-key.dto.ts +++ b/server/src/dtos/api-key.dto.ts @@ -5,21 +5,21 @@ import z from 'zod'; const PermissionSchema = z.enum(Permission).describe('List of permissions').meta({ id: 'Permission' }); -const APIKeyCreateSchema = z +const ApiKeyCreateSchema = z .object({ name: z.string().optional().describe('API key name'), permissions: z.array(PermissionSchema).min(1).describe('List of permissions'), }) - .meta({ id: 'APIKeyCreateDto' }); + .meta({ id: 'ApiKeyCreateDto' }); -const APIKeyUpdateSchema = z +const ApiKeyUpdateSchema = z .object({ name: z.string().optional().describe('API key name'), permissions: z.array(PermissionSchema).min(1).optional().describe('List of permissions'), }) - .meta({ id: 'APIKeyUpdateDto' }); + .meta({ id: 'ApiKeyUpdateDto' }); -const APIKeyResponseSchema = z +const ApiKeyResponseSchema = z .object({ id: z.string().describe('API key ID'), name: z.string().describe('API key name'), @@ -27,16 +27,16 @@ const APIKeyResponseSchema = z updatedAt: isoDatetimeToDate.describe('Last update date'), permissions: z.array(PermissionSchema).describe('List of permissions'), }) - .meta({ id: 'APIKeyResponseDto' }); + .meta({ id: 'ApiKeyResponseDto' }); -const APIKeyCreateResponseSchema = z +const ApiKeyCreateResponseSchema = z .object({ secret: z.string().describe('API key secret (only shown once)'), - apiKey: APIKeyResponseSchema, + apiKey: ApiKeyResponseSchema, }) - .meta({ id: 'APIKeyCreateResponseDto' }); + .meta({ id: 'ApiKeyCreateResponseDto' }); -export class APIKeyCreateDto extends createZodDto(APIKeyCreateSchema) {} -export class APIKeyUpdateDto extends createZodDto(APIKeyUpdateSchema) {} -export class APIKeyResponseDto extends createZodDto(APIKeyResponseSchema) {} -export class APIKeyCreateResponseDto extends createZodDto(APIKeyCreateResponseSchema) {} +export class ApiKeyCreateDto extends createZodDto(ApiKeyCreateSchema) {} +export class ApiKeyUpdateDto extends createZodDto(ApiKeyUpdateSchema) {} +export class ApiKeyResponseDto extends createZodDto(ApiKeyResponseSchema) {} +export class ApiKeyCreateResponseDto extends createZodDto(ApiKeyCreateResponseSchema) {} diff --git a/server/src/services/api-key.service.ts b/server/src/services/api-key.service.ts index 534de69107..acd3b17e94 100644 --- a/server/src/services/api-key.service.ts +++ b/server/src/services/api-key.service.ts @@ -1,6 +1,6 @@ import { BadRequestException, ForbiddenException, Injectable } from '@nestjs/common'; import { ApiKey } from 'src/database'; -import { APIKeyCreateDto, APIKeyCreateResponseDto, APIKeyResponseDto, APIKeyUpdateDto } from 'src/dtos/api-key.dto'; +import { ApiKeyCreateDto, ApiKeyCreateResponseDto, ApiKeyResponseDto, ApiKeyUpdateDto } from 'src/dtos/api-key.dto'; import { AuthDto } from 'src/dtos/auth.dto'; import { Permission } from 'src/enum'; import { BaseService } from 'src/services/base.service'; @@ -8,7 +8,7 @@ import { isGranted } from 'src/utils/access'; @Injectable() export class ApiKeyService extends BaseService { - async create(auth: AuthDto, dto: APIKeyCreateDto): Promise { + async create(auth: AuthDto, dto: ApiKeyCreateDto): Promise { const token = this.cryptoRepository.randomBytesAsText(32); const hashed = this.cryptoRepository.hashSha256(token); @@ -26,7 +26,7 @@ export class ApiKeyService extends BaseService { return { secret: token, apiKey: this.map(entity) }; } - async update(auth: AuthDto, id: string, dto: APIKeyUpdateDto): Promise { + async update(auth: AuthDto, id: string, dto: ApiKeyUpdateDto): Promise { const exists = await this.apiKeyRepository.getById(auth.user.id, id); if (!exists) { throw new BadRequestException('API Key not found'); @@ -54,7 +54,7 @@ export class ApiKeyService extends BaseService { await this.apiKeyRepository.delete(auth.user.id, id); } - async getMine(auth: AuthDto): Promise { + async getMine(auth: AuthDto): Promise { if (!auth.apiKey) { throw new ForbiddenException('Not authenticated with an API Key'); } @@ -67,7 +67,7 @@ export class ApiKeyService extends BaseService { return this.map(key); } - async getById(auth: AuthDto, id: string): Promise { + async getById(auth: AuthDto, id: string): Promise { const key = await this.apiKeyRepository.getById(auth.user.id, id); if (!key) { throw new BadRequestException('API Key not found'); @@ -75,12 +75,12 @@ export class ApiKeyService extends BaseService { return this.map(key); } - async getAll(auth: AuthDto): Promise { + async getAll(auth: AuthDto): Promise { const keys = await this.apiKeyRepository.getByUserId(auth.user.id); return keys.map((key) => this.map(key)); } - private map(entity: ApiKey): APIKeyResponseDto { + private map(entity: ApiKey): ApiKeyResponseDto { return { id: entity.id, name: entity.name, diff --git a/web/src/lib/components/user-settings-page/user-settings-list.svelte b/web/src/lib/components/user-settings-page/user-settings-list.svelte index 4df3f115f4..dced7fd5ba 100644 --- a/web/src/lib/components/user-settings-page/user-settings-list.svelte +++ b/web/src/lib/components/user-settings-page/user-settings-list.svelte @@ -33,7 +33,7 @@ import DeviceList from './device-list.svelte'; import OAuthSettings from './oauth-settings.svelte'; import PartnerSettings from './partner-settings.svelte'; - import UserAPIKeyList from './user-api-key-list.svelte'; + import UserApiKeyList from './user-api-key-list.svelte'; import UserProfileSettings from './user-profile-settings.svelte'; interface Props { @@ -72,7 +72,7 @@ - + Date: Wed, 15 Apr 2026 15:58:34 -0400 Subject: [PATCH 07/16] feat: album map markers endpoint (#27830) --- mobile/openapi/README.md | 1 + mobile/openapi/lib/api/albums_api.dart | 75 +++++++++++++++++++ open-api/immich-openapi-specs.json | 71 ++++++++++++++++++ open-api/typescript-sdk/src/fetch-client.ts | 46 ++++++++---- server/src/controllers/album.controller.ts | 12 +++ server/src/queries/map.repository.sql | 24 +++++- server/src/repositories/map.repository.ts | 50 ++++++++----- server/src/services/album.service.ts | 11 +++ .../components/album-page/album-map.svelte | 40 ++++------ 9 files changed, 269 insertions(+), 61 deletions(-) diff --git a/mobile/openapi/README.md b/mobile/openapi/README.md index 0d41dbdcf4..df469cee3e 100644 --- a/mobile/openapi/README.md +++ b/mobile/openapi/README.md @@ -89,6 +89,7 @@ Class | Method | HTTP request | Description *AlbumsApi* | [**createAlbum**](doc//AlbumsApi.md#createalbum) | **POST** /albums | Create an album *AlbumsApi* | [**deleteAlbum**](doc//AlbumsApi.md#deletealbum) | **DELETE** /albums/{id} | Delete an album *AlbumsApi* | [**getAlbumInfo**](doc//AlbumsApi.md#getalbuminfo) | **GET** /albums/{id} | Retrieve an album +*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* | [**removeAssetFromAlbum**](doc//AlbumsApi.md#removeassetfromalbum) | **DELETE** /albums/{id}/assets | Remove assets from an album diff --git a/mobile/openapi/lib/api/albums_api.dart b/mobile/openapi/lib/api/albums_api.dart index 000125ac7b..fa52ef2eb8 100644 --- a/mobile/openapi/lib/api/albums_api.dart +++ b/mobile/openapi/lib/api/albums_api.dart @@ -383,6 +383,81 @@ class AlbumsApi { return null; } + /// Retrieve album map markers + /// + /// Retrieve map marker information for a specific album by its ID. + /// + /// Note: This method returns the HTTP [Response]. + /// + /// Parameters: + /// + /// * [String] id (required): + /// + /// * [String] key: + /// + /// * [String] slug: + Future getAlbumMapMarkersWithHttpInfo(String id, { String? key, String? slug, }) async { + // ignore: prefer_const_declarations + final apiPath = r'/albums/{id}/map-markers' + .replaceAll('{id}', id); + + // ignore: prefer_final_locals + Object? postBody; + + final queryParams = []; + final headerParams = {}; + final formParams = {}; + + if (key != null) { + queryParams.addAll(_queryParams('', 'key', key)); + } + if (slug != null) { + queryParams.addAll(_queryParams('', 'slug', slug)); + } + + const contentTypes = []; + + + return apiClient.invokeAPI( + apiPath, + 'GET', + queryParams, + postBody, + headerParams, + formParams, + contentTypes.isEmpty ? null : contentTypes.first, + ); + } + + /// Retrieve album map markers + /// + /// Retrieve map marker information for a specific album by its ID. + /// + /// Parameters: + /// + /// * [String] id (required): + /// + /// * [String] key: + /// + /// * [String] slug: + Future?> getAlbumMapMarkers(String id, { String? key, String? slug, }) async { + final response = await getAlbumMapMarkersWithHttpInfo(id, key: key, slug: slug, ); + 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) { + final responseBody = await _decodeBodyBytes(response); + return (await apiClient.deserializeAsync(responseBody, 'List') as List) + .cast() + .toList(growable: false); + + } + return null; + } + /// Retrieve album statistics /// /// Returns statistics about the albums available to the authenticated user. diff --git a/open-api/immich-openapi-specs.json b/open-api/immich-openapi-specs.json index 2d83a302fe..99d77f9b24 100644 --- a/open-api/immich-openapi-specs.json +++ b/open-api/immich-openapi-specs.json @@ -2227,6 +2227,77 @@ "x-immich-state": "Stable" } }, + "/albums/{id}/map-markers": { + "get": { + "description": "Retrieve map marker information for a specific album by its ID.", + "operationId": "getAlbumMapMarkers", + "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" + } + }, + { + "name": "key", + "required": false, + "in": "query", + "schema": { + "type": "string" + } + }, + { + "name": "slug", + "required": false, + "in": "query", + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "items": { + "$ref": "#/components/schemas/MapMarkerResponseDto" + }, + "type": "array" + } + } + }, + "description": "" + } + }, + "security": [ + { + "bearer": [] + }, + { + "cookie": [] + }, + { + "api_key": [] + } + ], + "summary": "Retrieve album map markers", + "tags": [ + "Albums" + ], + "x-immich-history": [ + { + "version": "v3", + "state": "Added" + } + ], + "x-immich-permission": "album.read" + } + }, "/albums/{id}/user/{userId}": { "delete": { "description": "Remove a user from an album. Use an ID of \"me\" to leave a shared album.", diff --git a/open-api/typescript-sdk/src/fetch-client.ts b/open-api/typescript-sdk/src/fetch-client.ts index fd7b800f93..da38eaeace 100644 --- a/open-api/typescript-sdk/src/fetch-client.ts +++ b/open-api/typescript-sdk/src/fetch-client.ts @@ -710,6 +710,20 @@ export type BulkIdResponseDto = { /** Whether operation succeeded */ success: boolean; }; +export type MapMarkerResponseDto = { + /** City name */ + city: string | null; + /** Country name */ + country: string | null; + /** Asset ID */ + id: string; + /** Latitude */ + lat: number; + /** Longitude */ + lon: number; + /** State/Province name */ + state: string | null; +}; export type UpdateAlbumUserDto = { role: AlbumUserRole; }; @@ -1305,20 +1319,6 @@ export type ValidateLibraryResponseDto = { /** Validation results for import paths */ importPaths?: ValidateLibraryImportPathResponseDto[]; }; -export type MapMarkerResponseDto = { - /** City name */ - city: string | null; - /** Country name */ - country: string | null; - /** Asset ID */ - id: string; - /** Latitude */ - lat: number; - /** Longitude */ - lon: number; - /** State/Province name */ - state: string | null; -}; export type MapReverseGeocodeResponseDto = { /** City name */ city: string | null; @@ -3733,6 +3733,24 @@ export function addAssetsToAlbum({ id, bulkIdsDto }: { body: bulkIdsDto }))); } +/** + * Retrieve album map markers + */ +export function getAlbumMapMarkers({ id, key, slug }: { + id: string; + key?: string; + slug?: string; +}, opts?: Oazapfts.RequestOpts) { + return oazapfts.ok(oazapfts.fetchJson<{ + status: 200; + data: MapMarkerResponseDto[]; + }>(`/albums/${encodeURIComponent(id)}/map-markers${QS.query(QS.explode({ + key, + slug + }))}`, { + ...opts + })); +} /** * Remove user from album */ diff --git a/server/src/controllers/album.controller.ts b/server/src/controllers/album.controller.ts index df326a395f..62e3f53ad2 100644 --- a/server/src/controllers/album.controller.ts +++ b/server/src/controllers/album.controller.ts @@ -15,6 +15,7 @@ import { } from 'src/dtos/album.dto'; import { BulkIdResponseDto, BulkIdsDto } from 'src/dtos/asset-ids.response.dto'; import { AuthDto } from 'src/dtos/auth.dto'; +import { MapMarkerResponseDto } from 'src/dtos/map.dto'; import { ApiTag, Permission } from 'src/enum'; import { Auth, Authenticated } from 'src/middleware/auth.guard'; import { AlbumService } from 'src/services/album.service'; @@ -102,6 +103,17 @@ export class AlbumController { return this.service.delete(auth, id); } + @Authenticated({ permission: Permission.AlbumRead, sharedLink: true }) + @Get(':id/map-markers') + @Endpoint({ + summary: 'Retrieve album map markers', + description: 'Retrieve map marker information for a specific album by its ID.', + history: new HistoryBuilder().added('v3'), + }) + getAlbumMapMarkers(@Auth() auth: AuthDto, @Param() { id }: UUIDParamDto): Promise { + return this.service.getMapMarkers(auth, id); + } + @Put(':id/assets') @Authenticated({ permission: Permission.AlbumAssetCreate }) @Endpoint({ diff --git a/server/src/queries/map.repository.sql b/server/src/queries/map.repository.sql index d7e98b1cd2..8ae844b36c 100644 --- a/server/src/queries/map.repository.sql +++ b/server/src/queries/map.repository.sql @@ -1,5 +1,25 @@ -- NOTE: This file is auto generated by ./sql-generator +-- MapRepository.getAlbumMapMarkers +select + "id", + "asset_exif"."latitude" as "lat", + "asset_exif"."longitude" as "lon", + "asset_exif"."city", + "asset_exif"."state", + "asset_exif"."country" +from + "asset" + inner join "asset_exif" on "asset"."id" = "asset_exif"."assetId" + and "asset_exif"."latitude" is not null + and "asset_exif"."longitude" is not null + inner join "album_asset" on "asset"."id" = "album_asset"."assetId" +where + "asset"."deletedAt" is null + and "album_asset"."albumId" = $1 +order by + "fileCreatedAt" desc + -- MapRepository.getMapMarkers select "id", @@ -14,8 +34,8 @@ from and "asset_exif"."latitude" is not null and "asset_exif"."longitude" is not null where - "asset"."visibility" = $1 - and "deletedAt" is null + "asset"."deletedAt" is null + and "asset"."visibility" = $1 and ( "ownerId" in ($2) or exists ( diff --git a/server/src/repositories/map.repository.ts b/server/src/repositories/map.repository.ts index 304cf89c32..7c384b44b2 100644 --- a/server/src/repositories/map.repository.ts +++ b/server/src/repositories/map.repository.ts @@ -76,29 +76,21 @@ export class MapRepository { this.logger.log('Geodata import completed'); } + @GenerateSql({ params: [DummyValue.UUID] }) + getAlbumMapMarkers(albumId: string) { + return this.mapMarkersQuery() + .innerJoin('album_asset', 'asset.id', 'album_asset.assetId') + .where('album_asset.albumId', '=', albumId) + .execute(); + } + @GenerateSql({ params: [[DummyValue.UUID], [DummyValue.UUID]] }) getMapMarkers( ownerIds: string[], albumIds: string[], { isArchived, isFavorite, fileCreatedAfter, fileCreatedBefore }: MapMarkerSearchOptions = {}, ) { - return this.db - .selectFrom('asset') - .innerJoin('asset_exif', (builder) => - builder - .onRef('asset.id', '=', 'asset_exif.assetId') - .on('asset_exif.latitude', 'is not', null) - .on('asset_exif.longitude', 'is not', null), - ) - .select([ - 'id', - 'asset_exif.latitude as lat', - 'asset_exif.longitude as lon', - 'asset_exif.city', - 'asset_exif.state', - 'asset_exif.country', - ]) - .$narrowType<{ lat: NotNull; lon: NotNull }>() + return this.mapMarkersQuery() .$if(isArchived === true, (qb) => qb.where((eb) => eb.or([ @@ -113,7 +105,6 @@ export class MapRepository { .$if(isFavorite !== undefined, (q) => q.where('isFavorite', '=', isFavorite!)) .$if(fileCreatedAfter !== undefined, (q) => q.where('fileCreatedAt', '>=', fileCreatedAfter!)) .$if(fileCreatedBefore !== undefined, (q) => q.where('fileCreatedAt', '<=', fileCreatedBefore!)) - .where('deletedAt', 'is', null) .where((eb) => { const expression: Expression[] = []; @@ -134,10 +125,31 @@ export class MapRepository { return eb.or(expression); }) - .orderBy('fileCreatedAt', 'desc') .execute(); } + private mapMarkersQuery() { + return this.db + .selectFrom('asset') + .innerJoin('asset_exif', (builder) => + builder + .onRef('asset.id', '=', 'asset_exif.assetId') + .on('asset_exif.latitude', 'is not', null) + .on('asset_exif.longitude', 'is not', null), + ) + .where('asset.deletedAt', 'is', null) + .orderBy('fileCreatedAt', 'desc') + .select([ + 'id', + 'asset_exif.latitude as lat', + 'asset_exif.longitude as lon', + 'asset_exif.city', + 'asset_exif.state', + 'asset_exif.country', + ]) + .$narrowType<{ lat: NotNull; lon: NotNull }>(); + } + async reverseGeocode(point: GeoPoint): Promise { this.logger.debug(`Request: ${point.latitude},${point.longitude}`); diff --git a/server/src/services/album.service.ts b/server/src/services/album.service.ts index 24b9b165c9..1e93b8eace 100644 --- a/server/src/services/album.service.ts +++ b/server/src/services/album.service.ts @@ -17,6 +17,7 @@ import { } 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 { Permission } from 'src/enum'; import { AlbumAssetCount, AlbumInfoOptions } from 'src/repositories/album.repository'; import { BaseService } from 'src/services/base.service'; @@ -94,6 +95,16 @@ export class AlbumService extends BaseService { }; } + async getMapMarkers(auth: AuthDto, id: string): Promise { + await this.requireAccess({ auth, permission: Permission.AlbumRead, ids: [id] }); + + if (auth.sharedLink && !auth.sharedLink.showExif) { + return []; + } + + return this.mapRepository.getAlbumMapMarkers(id); + } + async create(auth: AuthDto, dto: CreateAlbumDto): Promise { const albumUsers = dto.albumUsers || []; diff --git a/web/src/lib/components/album-page/album-map.svelte b/web/src/lib/components/album-page/album-map.svelte index 623ac48ded..eb9d72b89c 100644 --- a/web/src/lib/components/album-page/album-map.svelte +++ b/web/src/lib/components/album-page/album-map.svelte @@ -2,8 +2,9 @@ import { assetViewerManager } from '$lib/managers/asset-viewer-manager.svelte'; import { authManager } from '$lib/managers/auth-manager.svelte'; import MapModal from '$lib/modals/MapModal.svelte'; + import { handleError } from '$lib/utils/handle-error'; import { navigate } from '$lib/utils/navigation'; - import { getAlbumInfo, type AlbumResponseDto, type MapMarkerResponseDto } from '@immich/sdk'; + import { getAlbumMapMarkers, type AlbumResponseDto, type MapMarkerResponseDto } from '@immich/sdk'; import { IconButton, modalManager } from '@immich/ui'; import { mdiMapOutline } from '@mdi/js'; import { onDestroy, onMount } from 'svelte'; @@ -14,7 +15,7 @@ } let { album }: Props = $props(); - let abortController: AbortController; + let cancelable: AbortController; let returnToMap = $state(false); let mapMarkers: MapMarkerResponseDto[] = $state([]); @@ -24,7 +25,7 @@ }); onDestroy(() => { - abortController?.abort(); + cancelable?.abort(); assetViewerManager.showAssetViewer(false); }); @@ -35,30 +36,17 @@ } }); - async function loadMapMarkers() { - if (abortController) { - abortController.abort(); + const loadMapMarkers = async () => { + cancelable?.abort(); + cancelable = new AbortController(); + + try { + return await getAlbumMapMarkers({ ...authManager.params, id: album.id }, { signal: cancelable.signal }); + } catch (error) { + handleError(error, $t('errors.something_went_wrong')); + return []; } - abortController = new AbortController(); - - let albumInfo: AlbumResponseDto = await getAlbumInfo({ id: album.id, withoutAssets: false, ...authManager.params }); - - let markers: MapMarkerResponseDto[] = []; - for (const asset of albumInfo.assets) { - if (asset.exifInfo?.latitude && asset.exifInfo?.longitude) { - markers.push({ - id: asset.id, - lat: asset.exifInfo.latitude, - lon: asset.exifInfo.longitude, - city: asset.exifInfo?.city ?? null, - country: asset.exifInfo?.country ?? null, - state: asset.exifInfo?.state ?? null, - }); - } - } - - return markers; - } + }; const onClick = async () => { const assetIds = await modalManager.show(MapModal, { mapMarkers }); From 4ffa26c9695a5bdbf4d5b4d7d49841955efe9408 Mon Sep 17 00:00:00 2001 From: Jason Rasmussen Date: Wed, 15 Apr 2026 16:33:52 -0400 Subject: [PATCH 08/16] feat: auth logout page (#27831) * feat: auth logout page * feat: skip login if already logged in --- .../navigation-bar/account-info-panel.svelte | 13 +++++----- .../navigation-bar/navigation-bar.svelte | 5 +--- web/src/lib/managers/auth-manager.svelte.ts | 26 ++++++++++--------- web/src/lib/managers/event-manager.svelte.ts | 1 + web/src/lib/route.ts | 1 + web/src/lib/stores/websocket.ts | 2 +- web/src/lib/utils/server.ts | 2 ++ .../routes/auth/change-password/+page.svelte | 4 ++- web/src/routes/auth/login/+page.ts | 8 +++++- web/src/routes/auth/logout/+page.svelte | 12 +++++++++ 10 files changed, 48 insertions(+), 26 deletions(-) create mode 100644 web/src/routes/auth/logout/+page.svelte diff --git a/web/src/lib/components/shared-components/navigation-bar/account-info-panel.svelte b/web/src/lib/components/shared-components/navigation-bar/account-info-panel.svelte index a2aa26efd8..21e093a272 100644 --- a/web/src/lib/components/shared-components/navigation-bar/account-info-panel.svelte +++ b/web/src/lib/components/shared-components/navigation-bar/account-info-panel.svelte @@ -14,12 +14,11 @@ import { fade } from 'svelte/transition'; import UserAvatar from '../user-avatar.svelte'; - interface Props { - onLogout: () => void; + type Props = { onClose?: () => void; - } + }; - let { onLogout, onClose = () => {} }: Props = $props(); + let { onClose }: Props = $props(); let info: ServerAboutResponseDto | undefined = $state(); @@ -48,7 +47,7 @@ size="tiny" shape="round" onclick={async () => { - onClose(); + onClose?.(); await modalManager.show(AvatarEditModal); }} /> @@ -99,7 +98,7 @@
{ - onClose(); + onClose?.(); if (info) { await modalManager.show(HelpAndFeedbackModal, { info }); } diff --git a/web/src/lib/components/shared-components/navigation-bar/navigation-bar.svelte b/web/src/lib/components/shared-components/navigation-bar/navigation-bar.svelte index 29f125097f..4ec153ad23 100644 --- a/web/src/lib/components/shared-components/navigation-bar/navigation-bar.svelte +++ b/web/src/lib/components/shared-components/navigation-bar/navigation-bar.svelte @@ -178,10 +178,7 @@ {#if shouldShowAccountInfoPanel} - authManager.logout()} - onClose={() => (shouldShowAccountInfoPanel = false)} - /> + (shouldShowAccountInfoPanel = false)} /> {/if}
diff --git a/web/src/lib/managers/auth-manager.svelte.ts b/web/src/lib/managers/auth-manager.svelte.ts index a6fe5bf6af..43de2221fb 100644 --- a/web/src/lib/managers/auth-manager.svelte.ts +++ b/web/src/lib/managers/auth-manager.svelte.ts @@ -41,6 +41,12 @@ class AuthManager { return this.#preferences; } + constructor() { + eventManager.on({ + SessionDelete: () => goto(Route.logout()), + }); + } + async load() { if (authManager.authenticated) { return; @@ -84,30 +90,26 @@ class AuthManager { } async logout() { - let redirectUri; + let redirectUri = Route.login(); try { const response = await logout(); if (response.redirectUri) { redirectUri = response.redirectUri; } - } catch (error) { - console.log('Error logging out:', error); + } catch { + // noop } - redirectUri = redirectUri ?? Route.login(); - - try { - if (redirectUri.startsWith('/')) { - await goto(redirectUri); - } else { - globalThis.location.href = redirectUri; - } - } finally { + if (redirectUri.startsWith('/')) { this.isPurchased = false; this.reset(); eventManager.emit('AuthLogout'); + + await goto(redirectUri); + } else { + globalThis.location.href = redirectUri; } } diff --git a/web/src/lib/managers/event-manager.svelte.ts b/web/src/lib/managers/event-manager.svelte.ts index 323d6a8315..c5764c5af2 100644 --- a/web/src/lib/managers/event-manager.svelte.ts +++ b/web/src/lib/managers/event-manager.svelte.ts @@ -74,6 +74,7 @@ export type Events = { UserAdminDeleted: [{ id: string }]; SessionLocked: []; + SessionDelete: []; SystemConfigUpdate: [SystemConfigDto]; diff --git a/web/src/lib/route.ts b/web/src/lib/route.ts index 4cb1965122..2fdfb4a052 100644 --- a/web/src/lib/route.ts +++ b/web/src/lib/route.ts @@ -51,6 +51,7 @@ export const Docs = { export const Route = { // auth login: (params?: { continue?: string; autoLaunch?: 0 | 1 }) => '/auth/login' + asQueryString(params), + logout: (params?: { continue?: string }) => '/auth/logout' + asQueryString(params), register: () => '/auth/register', changePassword: () => '/auth/change-password', onboarding: (params?: { step?: string }) => '/auth/onboarding' + asQueryString(params), diff --git a/web/src/lib/stores/websocket.ts b/web/src/lib/stores/websocket.ts index 4c47c84d5d..fec6e1d178 100644 --- a/web/src/lib/stores/websocket.ts +++ b/web/src/lib/stores/websocket.ts @@ -78,7 +78,7 @@ websocket } }) .on('on_new_release', (event) => eventManager.emit('ReleaseEvent', event)) - .on('on_session_delete', () => authManager.logout()) + .on('on_session_delete', () => eventManager.emit('SessionDelete')) .on('on_user_delete', (id) => eventManager.emit('UserAdminDeleted', { id })) .on('on_asset_update', (asset) => eventManager.emit('AssetUpdate', asset)) .on('on_person_thumbnail', (id) => eventManager.emit('PersonThumbnailReady', { id })) diff --git a/web/src/lib/utils/server.ts b/web/src/lib/utils/server.ts index 50fe4d72fe..150260addf 100644 --- a/web/src/lib/utils/server.ts +++ b/web/src/lib/utils/server.ts @@ -1,3 +1,4 @@ +import { authManager } from '$lib/managers/auth-manager.svelte'; import { featureFlagsManager } from '$lib/managers/feature-flags-manager.svelte'; import { serverConfigManager } from '$lib/managers/server-config-manager.svelte'; import { initLanguage } from '$lib/utils'; @@ -13,6 +14,7 @@ async function _init(fetch: Fetch) { defaults.fetch = fetch; await initLanguage(); await serverConfigManager.init(); + await authManager.load(); if (!serverConfigManager.value.maintenanceMode) { await featureFlagsManager.init(); diff --git a/web/src/routes/auth/change-password/+page.svelte b/web/src/routes/auth/change-password/+page.svelte index d8b4d9e00f..49578ff804 100644 --- a/web/src/routes/auth/change-password/+page.svelte +++ b/web/src/routes/auth/change-password/+page.svelte @@ -1,6 +1,8 @@ diff --git a/web/src/routes/auth/login/+page.ts b/web/src/routes/auth/login/+page.ts index 323f608754..0a2318abd7 100644 --- a/web/src/routes/auth/login/+page.ts +++ b/web/src/routes/auth/login/+page.ts @@ -1,3 +1,4 @@ +import { authManager } from '$lib/managers/auth-manager.svelte'; import { serverConfigManager } from '$lib/managers/server-config-manager.svelte'; import { Route } from '$lib/route'; import { getFormatter } from '$lib/utils/i18n'; @@ -7,6 +8,11 @@ import type { PageLoad } from './$types'; export const load = (async ({ parent, url }) => { await parent(); + const continueUrl = url.searchParams.get('continue') || Route.photos(); + if (authManager.authenticated) { + redirect(307, continueUrl); + } + if (!serverConfigManager.value.isInitialized) { // Admin not registered redirect(307, Route.register()); @@ -17,6 +23,6 @@ export const load = (async ({ parent, url }) => { meta: { title: $t('login'), }, - continueUrl: url.searchParams.get('continue') || Route.photos(), + continueUrl, }; }) satisfies PageLoad; diff --git a/web/src/routes/auth/logout/+page.svelte b/web/src/routes/auth/logout/+page.svelte new file mode 100644 index 0000000000..d7bd401681 --- /dev/null +++ b/web/src/routes/auth/logout/+page.svelte @@ -0,0 +1,12 @@ + + +
+
+ +
+
From a69eecf3bc499450be813b34ad6b2b6af6177af9 Mon Sep 17 00:00:00 2001 From: Jason Rasmussen Date: Wed, 15 Apr 2026 18:34:22 -0400 Subject: [PATCH 09/16] chore!: remove without assets (#27835) * chore!: remove without assets * fix: linting and e2e --------- Co-authored-by: Daniel Dietzler --- e2e/src/specs/server/api/album.e2e-spec.ts | 35 +- .../ui/generators/timeline/rest-response.ts | 1 - mobile/openapi/lib/api/albums_api.dart | 15 +- .../openapi/lib/model/album_response_dto.dart | 10 +- open-api/immich-openapi-specs.json | 16 - open-api/typescript-sdk/src/fetch-client.ts | 361 +++++++++--------- server/src/controllers/album.controller.ts | 9 +- server/src/dtos/album-response.dto.spec.ts | 4 +- server/src/dtos/album.dto.ts | 21 +- server/src/dtos/shared-link.dto.ts | 4 +- server/src/services/album.service.spec.ts | 14 +- server/src/services/album.service.ts | 18 +- web/src/lib/modals/AlbumOptionsModal.svelte | 2 +- .../[[assetId=id]]/+page.svelte | 2 +- .../[[photos=photos]]/[[assetId=id]]/+page.ts | 2 +- web/src/test-data/factories/album-factory.ts | 1 - 16 files changed, 214 insertions(+), 301 deletions(-) diff --git a/e2e/src/specs/server/api/album.e2e-spec.ts b/e2e/src/specs/server/api/album.e2e-spec.ts index a9e90940ab..3725de8d26 100644 --- a/e2e/src/specs/server/api/album.e2e-spec.ts +++ b/e2e/src/specs/server/api/album.e2e-spec.ts @@ -130,12 +130,11 @@ describe('/albums', () => { describe('GET /albums', () => { it("should not show other users' favorites", async () => { const { status, body } = await request(app) - .get(`/albums/${user1Albums[0].id}?withoutAssets=false`) + .get(`/albums/${user1Albums[0].id}`) .set('Authorization', `Bearer ${user2.accessToken}`); expect(status).toEqual(200); expect(body).toEqual({ ...user1Albums[0], - assets: [expect.objectContaining({ isFavorite: false })], contributorCounts: [{ userId: user1.userId, assetCount: 1 }], lastModifiedAssetTimestamp: expect.any(String), startDate: expect.any(String), @@ -304,13 +303,12 @@ describe('/albums', () => { describe('GET /albums/:id', () => { it('should return album info for own album', async () => { const { status, body } = await request(app) - .get(`/albums/${user1Albums[0].id}?withoutAssets=false`) + .get(`/albums/${user1Albums[0].id}`) .set('Authorization', `Bearer ${user1.accessToken}`); expect(status).toBe(200); expect(body).toEqual({ ...user1Albums[0], - assets: [expect.objectContaining({ id: user1Albums[0].assets[0].id })], contributorCounts: [{ userId: user1.userId, assetCount: 1 }], lastModifiedAssetTimestamp: expect.any(String), startDate: expect.any(String), @@ -322,7 +320,7 @@ describe('/albums', () => { it('should return album info for shared album (editor)', async () => { const { status, body } = await request(app) - .get(`/albums/${user2Albums[0].id}?withoutAssets=false`) + .get(`/albums/${user2Albums[0].id}`) .set('Authorization', `Bearer ${user1.accessToken}`); expect(status).toBe(200); @@ -331,14 +329,14 @@ describe('/albums', () => { it('should return album info for shared album (viewer)', async () => { const { status, body } = await request(app) - .get(`/albums/${user1Albums[3].id}?withoutAssets=false`) + .get(`/albums/${user1Albums[3].id}`) .set('Authorization', `Bearer ${user2.accessToken}`); expect(status).toBe(200); expect(body).toMatchObject({ id: user1Albums[3].id }); }); - it('should return album info with assets when withoutAssets is undefined', async () => { + it('should return album info', async () => { const { status, body } = await request(app) .get(`/albums/${user1Albums[0].id}`) .set('Authorization', `Bearer ${user1.accessToken}`); @@ -346,25 +344,6 @@ describe('/albums', () => { expect(status).toBe(200); expect(body).toEqual({ ...user1Albums[0], - assets: [expect.objectContaining({ id: user1Albums[0].assets[0].id })], - contributorCounts: [{ userId: user1.userId, assetCount: 1 }], - lastModifiedAssetTimestamp: expect.any(String), - startDate: expect.any(String), - endDate: expect.any(String), - albumUsers: expect.any(Array), - shared: true, - }); - }); - - it('should return album info without assets when withoutAssets is true', async () => { - const { status, body } = await request(app) - .get(`/albums/${user1Albums[0].id}?withoutAssets=true`) - .set('Authorization', `Bearer ${user1.accessToken}`); - - expect(status).toBe(200); - expect(body).toEqual({ - ...user1Albums[0], - assets: [], contributorCounts: [{ userId: user1.userId, assetCount: 1 }], assetCount: 1, lastModifiedAssetTimestamp: expect.any(String), @@ -379,13 +358,12 @@ describe('/albums', () => { await utils.deleteAssets(user1.accessToken, [user1Asset2.id]); const { status, body } = await request(app) - .get(`/albums/${user2Albums[0].id}?withoutAssets=true`) + .get(`/albums/${user2Albums[0].id}`) .set('Authorization', `Bearer ${user1.accessToken}`); expect(status).toBe(200); expect(body).toEqual({ ...user2Albums[0], - assets: [], contributorCounts: [{ userId: user1.userId, assetCount: 1 }], assetCount: 1, lastModifiedAssetTimestamp: expect.any(String), @@ -426,7 +404,6 @@ describe('/albums', () => { shared: false, albumUsers: [], hasSharedLink: false, - assets: [], assetCount: 0, owner: expect.objectContaining({ email: user1.userEmail }), isActivityEnabled: true, diff --git a/e2e/src/ui/generators/timeline/rest-response.ts b/e2e/src/ui/generators/timeline/rest-response.ts index de8aa3ee05..3114e3676d 100644 --- a/e2e/src/ui/generators/timeline/rest-response.ts +++ b/e2e/src/ui/generators/timeline/rest-response.ts @@ -427,7 +427,6 @@ export function getAlbum( hasSharedLink: false, isActivityEnabled: true, assetCount: albumAssets.length, - assets: albumAssets, startDate: albumAssets.length > 0 ? albumAssets.at(-1)?.fileCreatedAt : undefined, endDate: albumAssets.length > 0 ? albumAssets[0].fileCreatedAt : undefined, lastModifiedAssetTimestamp: albumAssets.length > 0 ? albumAssets[0].fileCreatedAt : undefined, diff --git a/mobile/openapi/lib/api/albums_api.dart b/mobile/openapi/lib/api/albums_api.dart index fa52ef2eb8..d08d1cba9d 100644 --- a/mobile/openapi/lib/api/albums_api.dart +++ b/mobile/openapi/lib/api/albums_api.dart @@ -315,10 +315,7 @@ class AlbumsApi { /// * [String] key: /// /// * [String] slug: - /// - /// * [bool] withoutAssets: - /// Exclude assets from response - Future getAlbumInfoWithHttpInfo(String id, { String? key, String? slug, bool? withoutAssets, }) async { + Future getAlbumInfoWithHttpInfo(String id, { String? key, String? slug, }) async { // ignore: prefer_const_declarations final apiPath = r'/albums/{id}' .replaceAll('{id}', id); @@ -336,9 +333,6 @@ class AlbumsApi { if (slug != null) { queryParams.addAll(_queryParams('', 'slug', slug)); } - if (withoutAssets != null) { - queryParams.addAll(_queryParams('', 'withoutAssets', withoutAssets)); - } const contentTypes = []; @@ -365,11 +359,8 @@ class AlbumsApi { /// * [String] key: /// /// * [String] slug: - /// - /// * [bool] withoutAssets: - /// Exclude assets from response - Future getAlbumInfo(String id, { String? key, String? slug, bool? withoutAssets, }) async { - final response = await getAlbumInfoWithHttpInfo(id, key: key, slug: slug, withoutAssets: withoutAssets, ); + Future getAlbumInfo(String id, { String? key, String? slug, }) async { + final response = await getAlbumInfoWithHttpInfo(id, key: key, slug: slug, ); if (response.statusCode >= HttpStatus.badRequest) { throw ApiException(response.statusCode, await _decodeBodyBytes(response)); } diff --git a/mobile/openapi/lib/model/album_response_dto.dart b/mobile/openapi/lib/model/album_response_dto.dart index ca0c087027..348e25ddaf 100644 --- a/mobile/openapi/lib/model/album_response_dto.dart +++ b/mobile/openapi/lib/model/album_response_dto.dart @@ -17,7 +17,6 @@ class AlbumResponseDto { required this.albumThumbnailAssetId, this.albumUsers = const [], required this.assetCount, - this.assets = const [], this.contributorCounts = const [], required this.createdAt, required this.description, @@ -48,8 +47,6 @@ class AlbumResponseDto { /// Maximum value: 9007199254740991 int assetCount; - List assets; - List contributorCounts; /// Creation date @@ -119,7 +116,6 @@ class AlbumResponseDto { other.albumThumbnailAssetId == albumThumbnailAssetId && _deepEquality.equals(other.albumUsers, albumUsers) && other.assetCount == assetCount && - _deepEquality.equals(other.assets, assets) && _deepEquality.equals(other.contributorCounts, contributorCounts) && other.createdAt == createdAt && other.description == description && @@ -142,7 +138,6 @@ class AlbumResponseDto { (albumThumbnailAssetId == null ? 0 : albumThumbnailAssetId!.hashCode) + (albumUsers.hashCode) + (assetCount.hashCode) + - (assets.hashCode) + (contributorCounts.hashCode) + (createdAt.hashCode) + (description.hashCode) + @@ -159,7 +154,7 @@ class AlbumResponseDto { (updatedAt.hashCode); @override - String toString() => 'AlbumResponseDto[albumName=$albumName, albumThumbnailAssetId=$albumThumbnailAssetId, albumUsers=$albumUsers, assetCount=$assetCount, assets=$assets, contributorCounts=$contributorCounts, createdAt=$createdAt, description=$description, endDate=$endDate, hasSharedLink=$hasSharedLink, id=$id, isActivityEnabled=$isActivityEnabled, lastModifiedAssetTimestamp=$lastModifiedAssetTimestamp, order=$order, owner=$owner, ownerId=$ownerId, shared=$shared, startDate=$startDate, updatedAt=$updatedAt]'; + String toString() => 'AlbumResponseDto[albumName=$albumName, albumThumbnailAssetId=$albumThumbnailAssetId, albumUsers=$albumUsers, assetCount=$assetCount, contributorCounts=$contributorCounts, createdAt=$createdAt, description=$description, endDate=$endDate, hasSharedLink=$hasSharedLink, id=$id, isActivityEnabled=$isActivityEnabled, lastModifiedAssetTimestamp=$lastModifiedAssetTimestamp, order=$order, owner=$owner, ownerId=$ownerId, shared=$shared, startDate=$startDate, updatedAt=$updatedAt]'; Map toJson() { final json = {}; @@ -171,7 +166,6 @@ class AlbumResponseDto { } json[r'albumUsers'] = this.albumUsers; json[r'assetCount'] = this.assetCount; - json[r'assets'] = this.assets; json[r'contributorCounts'] = this.contributorCounts; json[r'createdAt'] = this.createdAt.toUtc().toIso8601String(); json[r'description'] = this.description; @@ -218,7 +212,6 @@ class AlbumResponseDto { albumThumbnailAssetId: mapValueOfType(json, r'albumThumbnailAssetId'), albumUsers: AlbumUserResponseDto.listFromJson(json[r'albumUsers']), assetCount: mapValueOfType(json, r'assetCount')!, - assets: AssetResponseDto.listFromJson(json[r'assets']), contributorCounts: ContributorCountResponseDto.listFromJson(json[r'contributorCounts']), createdAt: mapDateTime(json, r'createdAt', r'')!, description: mapValueOfType(json, r'description')!, @@ -284,7 +277,6 @@ class AlbumResponseDto { 'albumThumbnailAssetId', 'albumUsers', 'assetCount', - 'assets', 'createdAt', 'description', 'hasSharedLink', diff --git a/open-api/immich-openapi-specs.json b/open-api/immich-openapi-specs.json index 99d77f9b24..33a692474a 100644 --- a/open-api/immich-openapi-specs.json +++ b/open-api/immich-openapi-specs.json @@ -1956,15 +1956,6 @@ "schema": { "type": "string" } - }, - { - "name": "withoutAssets", - "required": false, - "in": "query", - "description": "Exclude assets from response", - "schema": { - "type": "boolean" - } } ], "responses": { @@ -15305,12 +15296,6 @@ "minimum": 0, "type": "integer" }, - "assets": { - "items": { - "$ref": "#/components/schemas/AssetResponseDto" - }, - "type": "array" - }, "contributorCounts": { "items": { "$ref": "#/components/schemas/ContributorCountResponseDto" @@ -15378,7 +15363,6 @@ "albumThumbnailAssetId", "albumUsers", "assetCount", - "assets", "createdAt", "description", "hasSharedLink", diff --git a/open-api/typescript-sdk/src/fetch-client.ts b/open-api/typescript-sdk/src/fetch-client.ts index da38eaeace..0875715beb 100644 --- a/open-api/typescript-sdk/src/fetch-client.ts +++ b/open-api/typescript-sdk/src/fetch-client.ts @@ -446,172 +446,6 @@ export type AlbumUserResponseDto = { role: AlbumUserRole; user: UserResponseDto; }; -export type ExifResponseDto = { - /** City name */ - city?: string | null; - /** Country name */ - country?: string | null; - /** Original date/time */ - dateTimeOriginal?: string | null; - /** Image description */ - description?: string | null; - /** Image height in pixels */ - exifImageHeight?: number | null; - /** Image width in pixels */ - exifImageWidth?: number | null; - /** Exposure time */ - exposureTime?: string | null; - /** F-number (aperture) */ - fNumber?: number | null; - /** File size in bytes */ - fileSizeInByte?: number | null; - /** Focal length in mm */ - focalLength?: number | null; - /** ISO sensitivity */ - iso?: number | null; - /** GPS latitude */ - latitude?: number | null; - /** Lens model */ - lensModel?: string | null; - /** GPS longitude */ - longitude?: number | null; - /** Camera make */ - make?: string | null; - /** Camera model */ - model?: string | null; - /** Modification date/time */ - modifyDate?: string | null; - /** Image orientation */ - orientation?: string | null; - /** Projection type */ - projectionType?: string | null; - /** Rating */ - rating?: number | null; - /** State/province name */ - state?: string | null; - /** Time zone */ - timeZone?: string | null; -}; -export type AssetFaceWithoutPersonResponseDto = { - /** Bounding box X1 coordinate */ - boundingBoxX1: number; - /** Bounding box X2 coordinate */ - boundingBoxX2: number; - /** Bounding box Y1 coordinate */ - boundingBoxY1: number; - /** Bounding box Y2 coordinate */ - boundingBoxY2: number; - /** Face ID */ - id: string; - /** Image height in pixels */ - imageHeight: number; - /** Image width in pixels */ - imageWidth: number; - sourceType?: SourceType; -}; -export type PersonWithFacesResponseDto = { - /** Person date of birth */ - birthDate: string | null; - /** Person color (hex) */ - color?: string; - faces: AssetFaceWithoutPersonResponseDto[]; - /** Person ID */ - id: string; - /** Is favorite */ - isFavorite?: boolean; - /** Is hidden */ - isHidden: boolean; - /** Person name */ - name: string; - /** Thumbnail path */ - thumbnailPath: string; - /** Last update date */ - updatedAt?: string; -}; -export type AssetStackResponseDto = { - /** Number of assets in stack */ - assetCount: number; - /** Stack ID */ - id: string; - /** Primary asset ID */ - primaryAssetId: string; -}; -export type TagResponseDto = { - /** Tag color (hex) */ - color?: string; - /** Creation date */ - createdAt: string; - /** Tag ID */ - id: string; - /** Tag name */ - name: string; - /** Parent tag ID */ - parentId?: string; - /** Last update date */ - updatedAt: string; - /** Tag value (full path) */ - value: string; -}; -export type AssetResponseDto = { - /** Base64 encoded SHA1 hash */ - checksum: string; - /** The UTC timestamp when the asset was originally uploaded to Immich. */ - createdAt: string; - /** Duplicate group ID */ - duplicateId?: string | null; - /** Video duration (for videos) */ - duration: string; - exifInfo?: ExifResponseDto; - /** The actual UTC timestamp when the file was created/captured, preserving timezone information. This is the authoritative timestamp for chronological sorting within timeline groups. Combined with timezone data, this can be used to determine the exact moment the photo was taken. */ - fileCreatedAt: string; - /** The UTC timestamp when the file was last modified on the filesystem. This reflects the last time the physical file was changed, which may be different from when the photo was originally taken. */ - fileModifiedAt: string; - /** Whether asset has metadata */ - hasMetadata: boolean; - /** Asset height */ - height: number | null; - /** Asset ID */ - id: string; - /** Is archived */ - isArchived: boolean; - /** Is edited */ - isEdited: boolean; - /** Is favorite */ - isFavorite: boolean; - /** Is offline */ - isOffline: boolean; - /** Is trashed */ - isTrashed: boolean; - /** Library ID */ - libraryId?: string | null; - /** Live photo video ID */ - livePhotoVideoId?: string | null; - /** The local date and time when the photo/video was taken, derived from EXIF metadata. This represents the photographer's local time regardless of timezone, stored as a timezone-agnostic timestamp. Used for timeline grouping by "local" days and months. */ - localDateTime: string; - /** Original file name */ - originalFileName: string; - /** Original MIME type */ - originalMimeType?: string; - /** Original file path */ - originalPath: string; - owner?: UserResponseDto; - /** Owner user ID */ - ownerId: string; - people?: PersonWithFacesResponseDto[]; - /** Is resized */ - resized?: boolean; - stack?: (AssetStackResponseDto) | null; - tags?: TagResponseDto[]; - /** Thumbhash for thumbnail generation (base64) also used as the c query param for thumbnail cache busting. */ - thumbhash: string | null; - "type": AssetTypeEnum; - unassignedFaces?: AssetFaceWithoutPersonResponseDto[]; - /** The UTC timestamp when the asset record was last updated in the database. This is automatically maintained by the database and reflects when any field in the asset was last modified. */ - updatedAt: string; - visibility: AssetVisibility; - /** Asset width */ - width: number | null; -}; export type ContributorCountResponseDto = { /** Number of assets contributed */ assetCount: number; @@ -626,7 +460,6 @@ export type AlbumResponseDto = { albumUsers: AlbumUserResponseDto[]; /** Number of assets */ assetCount: number; - assets: AssetResponseDto[]; contributorCounts?: ContributorCountResponseDto[]; /** Creation date */ createdAt: string; @@ -910,6 +743,172 @@ export type AssetMetadataBulkResponseDto = { [key: string]: any; }; }; +export type ExifResponseDto = { + /** City name */ + city?: string | null; + /** Country name */ + country?: string | null; + /** Original date/time */ + dateTimeOriginal?: string | null; + /** Image description */ + description?: string | null; + /** Image height in pixels */ + exifImageHeight?: number | null; + /** Image width in pixels */ + exifImageWidth?: number | null; + /** Exposure time */ + exposureTime?: string | null; + /** F-number (aperture) */ + fNumber?: number | null; + /** File size in bytes */ + fileSizeInByte?: number | null; + /** Focal length in mm */ + focalLength?: number | null; + /** ISO sensitivity */ + iso?: number | null; + /** GPS latitude */ + latitude?: number | null; + /** Lens model */ + lensModel?: string | null; + /** GPS longitude */ + longitude?: number | null; + /** Camera make */ + make?: string | null; + /** Camera model */ + model?: string | null; + /** Modification date/time */ + modifyDate?: string | null; + /** Image orientation */ + orientation?: string | null; + /** Projection type */ + projectionType?: string | null; + /** Rating */ + rating?: number | null; + /** State/province name */ + state?: string | null; + /** Time zone */ + timeZone?: string | null; +}; +export type AssetFaceWithoutPersonResponseDto = { + /** Bounding box X1 coordinate */ + boundingBoxX1: number; + /** Bounding box X2 coordinate */ + boundingBoxX2: number; + /** Bounding box Y1 coordinate */ + boundingBoxY1: number; + /** Bounding box Y2 coordinate */ + boundingBoxY2: number; + /** Face ID */ + id: string; + /** Image height in pixels */ + imageHeight: number; + /** Image width in pixels */ + imageWidth: number; + sourceType?: SourceType; +}; +export type PersonWithFacesResponseDto = { + /** Person date of birth */ + birthDate: string | null; + /** Person color (hex) */ + color?: string; + faces: AssetFaceWithoutPersonResponseDto[]; + /** Person ID */ + id: string; + /** Is favorite */ + isFavorite?: boolean; + /** Is hidden */ + isHidden: boolean; + /** Person name */ + name: string; + /** Thumbnail path */ + thumbnailPath: string; + /** Last update date */ + updatedAt?: string; +}; +export type AssetStackResponseDto = { + /** Number of assets in stack */ + assetCount: number; + /** Stack ID */ + id: string; + /** Primary asset ID */ + primaryAssetId: string; +}; +export type TagResponseDto = { + /** Tag color (hex) */ + color?: string; + /** Creation date */ + createdAt: string; + /** Tag ID */ + id: string; + /** Tag name */ + name: string; + /** Parent tag ID */ + parentId?: string; + /** Last update date */ + updatedAt: string; + /** Tag value (full path) */ + value: string; +}; +export type AssetResponseDto = { + /** Base64 encoded SHA1 hash */ + checksum: string; + /** The UTC timestamp when the asset was originally uploaded to Immich. */ + createdAt: string; + /** Duplicate group ID */ + duplicateId?: string | null; + /** Video duration (for videos) */ + duration: string; + exifInfo?: ExifResponseDto; + /** The actual UTC timestamp when the file was created/captured, preserving timezone information. This is the authoritative timestamp for chronological sorting within timeline groups. Combined with timezone data, this can be used to determine the exact moment the photo was taken. */ + fileCreatedAt: string; + /** The UTC timestamp when the file was last modified on the filesystem. This reflects the last time the physical file was changed, which may be different from when the photo was originally taken. */ + fileModifiedAt: string; + /** Whether asset has metadata */ + hasMetadata: boolean; + /** Asset height */ + height: number | null; + /** Asset ID */ + id: string; + /** Is archived */ + isArchived: boolean; + /** Is edited */ + isEdited: boolean; + /** Is favorite */ + isFavorite: boolean; + /** Is offline */ + isOffline: boolean; + /** Is trashed */ + isTrashed: boolean; + /** Library ID */ + libraryId?: string | null; + /** Live photo video ID */ + livePhotoVideoId?: string | null; + /** The local date and time when the photo/video was taken, derived from EXIF metadata. This represents the photographer's local time regardless of timezone, stored as a timezone-agnostic timestamp. Used for timeline grouping by "local" days and months. */ + localDateTime: string; + /** Original file name */ + originalFileName: string; + /** Original MIME type */ + originalMimeType?: string; + /** Original file path */ + originalPath: string; + owner?: UserResponseDto; + /** Owner user ID */ + ownerId: string; + people?: PersonWithFacesResponseDto[]; + /** Is resized */ + resized?: boolean; + stack?: (AssetStackResponseDto) | null; + tags?: TagResponseDto[]; + /** Thumbhash for thumbnail generation (base64) also used as the c query param for thumbnail cache busting. */ + thumbhash: string | null; + "type": AssetTypeEnum; + unassignedFaces?: AssetFaceWithoutPersonResponseDto[]; + /** The UTC timestamp when the asset record was last updated in the database. This is automatically maintained by the database and reflects when any field in the asset was last modified. */ + updatedAt: string; + visibility: AssetVisibility; + /** Asset width */ + width: number | null; +}; export type UpdateAssetDto = { /** Original date and time */ dateTimeOriginal?: string; @@ -3668,19 +3667,17 @@ export function deleteAlbum({ id }: { /** * Retrieve an album */ -export function getAlbumInfo({ id, key, slug, withoutAssets }: { +export function getAlbumInfo({ id, key, slug }: { id: string; key?: string; slug?: string; - withoutAssets?: boolean; }, opts?: Oazapfts.RequestOpts) { return oazapfts.ok(oazapfts.fetchJson<{ status: 200; data: AlbumResponseDto; }>(`/albums/${encodeURIComponent(id)}${QS.query(QS.explode({ key, - slug, - withoutAssets + slug }))}`, { ...opts })); @@ -6729,17 +6726,6 @@ export enum AlbumUserRole { Editor = "editor", Viewer = "viewer" } -export enum SourceType { - MachineLearning = "machine-learning", - Exif = "exif", - Manual = "manual" -} -export enum AssetTypeEnum { - Image = "IMAGE", - Video = "VIDEO", - Audio = "AUDIO", - Other = "OTHER" -} export enum BulkIdErrorReason { Duplicate = "duplicate", NoPermission = "no_permission", @@ -6923,6 +6909,17 @@ export enum AssetJobName { RegenerateThumbnail = "regenerate-thumbnail", TranscodeVideo = "transcode-video" } +export enum SourceType { + MachineLearning = "machine-learning", + Exif = "exif", + Manual = "manual" +} +export enum AssetTypeEnum { + Image = "IMAGE", + Video = "VIDEO", + Audio = "AUDIO", + Other = "OTHER" +} export enum AssetEditAction { Crop = "crop", Rotate = "rotate", diff --git a/server/src/controllers/album.controller.ts b/server/src/controllers/album.controller.ts index 62e3f53ad2..90a8fa5a25 100644 --- a/server/src/controllers/album.controller.ts +++ b/server/src/controllers/album.controller.ts @@ -3,7 +3,6 @@ import { ApiTags } from '@nestjs/swagger'; import { Endpoint, HistoryBuilder } from 'src/decorators'; import { AddUsersDto, - AlbumInfoDto, AlbumResponseDto, AlbumsAddAssetsDto, AlbumsAddAssetsResponseDto, @@ -66,12 +65,8 @@ export class AlbumController { description: 'Retrieve information about a specific album by its ID.', history: new HistoryBuilder().added('v1').beta('v1').stable('v2'), }) - getAlbumInfo( - @Auth() auth: AuthDto, - @Param() { id }: UUIDParamDto, - @Query() dto: AlbumInfoDto, - ): Promise { - return this.service.get(auth, id, dto); + getAlbumInfo(@Auth() auth: AuthDto, @Param() { id }: UUIDParamDto): Promise { + return this.service.get(auth, id); } @Patch(':id') diff --git a/server/src/dtos/album-response.dto.spec.ts b/server/src/dtos/album-response.dto.spec.ts index e82067580b..c03662288a 100644 --- a/server/src/dtos/album-response.dto.spec.ts +++ b/server/src/dtos/album-response.dto.spec.ts @@ -10,13 +10,13 @@ describe('mapAlbum', () => { .asset({ localDateTime: endDate }, (builder) => builder.exif()) .asset({ localDateTime: startDate }, (builder) => builder.exif()) .build(); - const dto = mapAlbum(getForAlbum(album), false); + const dto = mapAlbum(getForAlbum(album)); expect(dto.startDate).toEqual(startDate.toISOString()); expect(dto.endDate).toEqual(endDate.toISOString()); }); it('should not set start and end dates for empty assets', () => { - const dto = mapAlbum(getForAlbum(AlbumFactory.create()), false); + const dto = mapAlbum(getForAlbum(AlbumFactory.create())); expect(dto.startDate).toBeUndefined(); expect(dto.endDate).toBeUndefined(); }); diff --git a/server/src/dtos/album.dto.ts b/server/src/dtos/album.dto.ts index 1514809838..519094cd94 100644 --- a/server/src/dtos/album.dto.ts +++ b/server/src/dtos/album.dto.ts @@ -3,8 +3,7 @@ import _ from 'lodash'; import { createZodDto } from 'nestjs-zod'; import { AlbumUser, AuthSharedLink, User } from 'src/database'; import { BulkIdErrorReasonSchema } from 'src/dtos/asset-ids.response.dto'; -import { AssetResponseSchema, MapAsset, mapAsset } from 'src/dtos/asset-response.dto'; -import { AuthDto } from 'src/dtos/auth.dto'; +import { MapAsset } from 'src/dtos/asset-response.dto'; import { UserResponseSchema, mapUser } from 'src/dtos/user.dto'; import { AlbumUserRole, AlbumUserRoleSchema, AssetOrder, AssetOrderSchema } from 'src/enum'; import { MaybeDehydrated } from 'src/types'; @@ -12,12 +11,6 @@ import { asDateString } from 'src/utils/date'; import { stringToBool } from 'src/validation'; import z from 'zod'; -const AlbumInfoSchema = z - .object({ - withoutAssets: stringToBool.optional().describe('Exclude assets from response'), - }) - .meta({ id: 'AlbumInfoDto' }); - const AlbumUserAddSchema = z .object({ userId: z.uuidv4().describe('User ID'), @@ -122,7 +115,6 @@ export const AlbumResponseSchema = z shared: z.boolean().describe('Is shared album'), albumUsers: z.array(AlbumUserResponseSchema), hasSharedLink: z.boolean().describe('Has shared link'), - assets: z.array(AssetResponseSchema), owner: UserResponseSchema, assetCount: z.int().min(0).describe('Number of assets'), // TODO: use `isoDatetimeToDate` when using `ZodSerializerDto` on the controllers. @@ -141,7 +133,6 @@ export const AlbumResponseSchema = z }) .meta({ id: 'AlbumResponseDto' }); -export class AlbumInfoDto extends createZodDto(AlbumInfoSchema) {} export class AddUsersDto extends createZodDto(AddUsersSchema) {} export class AlbumUserCreateDto extends createZodDto(AlbumUserCreateSchema) {} export class CreateAlbumDto extends createZodDto(CreateAlbumSchema) {} @@ -170,11 +161,7 @@ export type MapAlbumDto = { order: AssetOrder; }; -export const mapAlbum = ( - entity: MaybeDehydrated, - withAssets: boolean, - auth?: AuthDto, -): AlbumResponseDto => { +export const mapAlbum = (entity: MaybeDehydrated): AlbumResponseDto => { const albumUsers: AlbumUserResponseDto[] = []; if (entity.albumUsers) { @@ -215,12 +202,8 @@ export const mapAlbum = ( hasSharedLink, startDate: asDateString(startDate), endDate: asDateString(endDate), - assets: (withAssets ? assets : []).map((asset) => mapAsset(asset, { auth })), assetCount: entity.assets?.length || 0, isActivityEnabled: entity.isActivityEnabled, order: entity.order, }; }; - -export const mapAlbumWithAssets = (entity: MaybeDehydrated) => mapAlbum(entity, true); -export const mapAlbumWithoutAssets = (entity: MaybeDehydrated) => mapAlbum(entity, false); diff --git a/server/src/dtos/shared-link.dto.ts b/server/src/dtos/shared-link.dto.ts index aa58c0833e..2e466c5014 100644 --- a/server/src/dtos/shared-link.dto.ts +++ b/server/src/dtos/shared-link.dto.ts @@ -1,7 +1,7 @@ import { createZodDto } from 'nestjs-zod'; import { SharedLink } from 'src/database'; import { HistoryBuilder } from 'src/decorators'; -import { AlbumResponseSchema, mapAlbumWithoutAssets } from 'src/dtos/album.dto'; +import { AlbumResponseSchema, mapAlbum } from 'src/dtos/album.dto'; import { AssetResponseSchema, mapAsset } from 'src/dtos/asset-response.dto'; import { SharedLinkTypeSchema } from 'src/enum'; import { emptyStringToNull, isoDatetimeToDate } from 'src/validation'; @@ -96,7 +96,7 @@ export function mapSharedLink(sharedLink: SharedLink, options: { stripAssetMetad createdAt: sharedLink.createdAt, expiresAt: sharedLink.expiresAt, assets: assets.map((asset) => mapAsset(asset, { stripMetadata: options.stripAssetMetadata })), - album: sharedLink.album ? mapAlbumWithoutAssets(sharedLink.album) : undefined, + album: sharedLink.album ? mapAlbum(sharedLink.album) : undefined, allowUpload: sharedLink.allowUpload, allowDownload: sharedLink.allowDownload, showMetadata: sharedLink.showExif, diff --git a/server/src/services/album.service.spec.ts b/server/src/services/album.service.spec.ts index d24786fbb6..4f5d4edd00 100644 --- a/server/src/services/album.service.spec.ts +++ b/server/src/services/album.service.spec.ts @@ -563,9 +563,9 @@ describe(AlbumService.name, () => { }, ]); - await sut.get(AuthFactory.create(album.owner), album.id, {}); + await sut.get(AuthFactory.create(album.owner), album.id); - expect(mocks.album.getById).toHaveBeenCalledWith(album.id, { withAssets: true }); + expect(mocks.album.getById).toHaveBeenCalledWith(album.id, { withAssets: false }); expect(mocks.access.album.checkOwnerAccess).toHaveBeenCalledWith(album.owner.id, new Set([album.id])); }); @@ -584,9 +584,9 @@ describe(AlbumService.name, () => { ]); const auth = AuthFactory.from().sharedLink().build(); - await sut.get(auth, album.id, {}); + await sut.get(auth, album.id); - expect(mocks.album.getById).toHaveBeenCalledWith(album.id, { withAssets: true }); + expect(mocks.album.getById).toHaveBeenCalledWith(album.id, { withAssets: false }); expect(mocks.access.album.checkSharedLinkAccess).toHaveBeenCalledWith(auth.sharedLink!.id, new Set([album.id])); }); @@ -605,9 +605,9 @@ describe(AlbumService.name, () => { }, ]); - await sut.get(AuthFactory.create(user), album.id, {}); + await sut.get(AuthFactory.create(user), album.id); - expect(mocks.album.getById).toHaveBeenCalledWith(album.id, { withAssets: true }); + expect(mocks.album.getById).toHaveBeenCalledWith(album.id, { withAssets: false }); expect(mocks.access.album.checkSharedAlbumAccess).toHaveBeenCalledWith( user.id, new Set([album.id]), @@ -617,7 +617,7 @@ describe(AlbumService.name, () => { it('should throw an error for no access', async () => { const auth = AuthFactory.create(); - await expect(sut.get(auth, 'album-123', {})).rejects.toBeInstanceOf(BadRequestException); + await expect(sut.get(auth, 'album-123')).rejects.toBeInstanceOf(BadRequestException); expect(mocks.access.album.checkOwnerAccess).toHaveBeenCalledWith(auth.user.id, new Set(['album-123'])); expect(mocks.access.album.checkSharedAlbumAccess).toHaveBeenCalledWith( diff --git a/server/src/services/album.service.ts b/server/src/services/album.service.ts index 1e93b8eace..8142bfeff5 100644 --- a/server/src/services/album.service.ts +++ b/server/src/services/album.service.ts @@ -1,7 +1,6 @@ import { BadRequestException, Injectable } from '@nestjs/common'; import { AddUsersDto, - AlbumInfoDto, AlbumResponseDto, AlbumsAddAssetsDto, AlbumsAddAssetsResponseDto, @@ -10,8 +9,6 @@ import { GetAlbumsDto, mapAlbum, MapAlbumDto, - mapAlbumWithAssets, - mapAlbumWithoutAssets, UpdateAlbumDto, UpdateAlbumUserDto, } from 'src/dtos/album.dto'; @@ -64,7 +61,7 @@ export class AlbumService extends BaseService { } return albums.map((album) => ({ - ...mapAlbumWithoutAssets(album), + ...mapAlbum(album), sharedLinks: undefined, startDate: asDateString(albumMetadata[album.id]?.startDate ?? undefined), endDate: asDateString(albumMetadata[album.id]?.endDate ?? undefined), @@ -74,11 +71,10 @@ export class AlbumService extends BaseService { })); } - async get(auth: AuthDto, id: string, dto: AlbumInfoDto): Promise { + async get(auth: AuthDto, id: string): Promise { await this.requireAccess({ auth, permission: Permission.AlbumRead, ids: [id] }); await this.albumRepository.updateThumbnails(); - const withAssets = dto.withoutAssets === undefined ? true : !dto.withoutAssets; - const album = await this.findOrFail(id, { withAssets }); + const album = await this.findOrFail(id, { withAssets: false }); const [albumMetadataForIds] = await this.albumRepository.getMetadataForIds([album.id]); const hasSharedUsers = album.albumUsers && album.albumUsers.length > 0; @@ -86,7 +82,7 @@ export class AlbumService extends BaseService { const isShared = hasSharedUsers || hasSharedLink; return { - ...mapAlbum(album, withAssets, auth), + ...mapAlbum(album), startDate: asDateString(albumMetadataForIds?.startDate ?? undefined), endDate: asDateString(albumMetadataForIds?.endDate ?? undefined), assetCount: albumMetadataForIds?.assetCount ?? 0, @@ -144,7 +140,7 @@ export class AlbumService extends BaseService { await this.eventRepository.emit('AlbumInvite', { id: album.id, userId }); } - return mapAlbumWithAssets(album); + return mapAlbum(album); } async update(auth: AuthDto, id: string, dto: UpdateAlbumDto): Promise { @@ -167,7 +163,7 @@ export class AlbumService extends BaseService { order: dto.order, }); - return mapAlbumWithoutAssets({ ...updatedAlbum, assets: album.assets }); + return mapAlbum({ ...updatedAlbum, assets: album.assets }); } async delete(auth: AuthDto, id: string): Promise { @@ -305,7 +301,7 @@ export class AlbumService extends BaseService { await this.eventRepository.emit('AlbumInvite', { id, userId }); } - return this.findOrFail(id, { withAssets: true }).then(mapAlbumWithoutAssets); + return this.findOrFail(id, { withAssets: true }).then(mapAlbum); } async removeUser(auth: AuthDto, id: string, userId: string | 'me'): Promise { diff --git a/web/src/lib/modals/AlbumOptionsModal.svelte b/web/src/lib/modals/AlbumOptionsModal.svelte index 4553f022df..80961969c5 100644 --- a/web/src/lib/modals/AlbumOptionsModal.svelte +++ b/web/src/lib/modals/AlbumOptionsModal.svelte @@ -39,7 +39,7 @@ }; const refreshAlbum = async () => { - album = await getAlbumInfo({ id: album.id, withoutAssets: true }); + album = await getAlbumInfo({ id: album.id }); }; const onAlbumUserDelete = async ({ userId }: { userId: string }) => { diff --git a/web/src/routes/(user)/albums/[albumId=id]/[[photos=photos]]/[[assetId=id]]/+page.svelte b/web/src/routes/(user)/albums/[albumId=id]/[[photos=photos]]/[[assetId=id]]/+page.svelte index 3532897ad9..dc3a7b60d4 100644 --- a/web/src/routes/(user)/albums/[albumId=id]/[[photos=photos]]/[[assetId=id]]/+page.svelte +++ b/web/src/routes/(user)/albums/[albumId=id]/[[photos=photos]]/[[assetId=id]]/+page.svelte @@ -133,7 +133,7 @@ }; const refreshAlbum = async () => { - album = await getAlbumInfo({ id: album.id, withoutAssets: true }); + album = await getAlbumInfo({ id: album.id }); }; const setModeToView = async () => { diff --git a/web/src/routes/(user)/albums/[albumId=id]/[[photos=photos]]/[[assetId=id]]/+page.ts b/web/src/routes/(user)/albums/[albumId=id]/[[photos=photos]]/[[assetId=id]]/+page.ts index 359bb84ca5..623c39b152 100644 --- a/web/src/routes/(user)/albums/[albumId=id]/[[photos=photos]]/[[assetId=id]]/+page.ts +++ b/web/src/routes/(user)/albums/[albumId=id]/[[photos=photos]]/[[assetId=id]]/+page.ts @@ -4,7 +4,7 @@ import type { PageLoad } from './$types'; export const load = (async ({ params, url }) => { await authenticate(url); - const album = await getAlbumInfo({ id: params.albumId, withoutAssets: true }); + const album = await getAlbumInfo({ id: params.albumId }); return { album, diff --git a/web/src/test-data/factories/album-factory.ts b/web/src/test-data/factories/album-factory.ts index 7ea3715f73..7ad44b7ac5 100644 --- a/web/src/test-data/factories/album-factory.ts +++ b/web/src/test-data/factories/album-factory.ts @@ -8,7 +8,6 @@ export const albumFactory = Sync.makeFactory({ description: '', albumThumbnailAssetId: null, assetCount: Sync.each((index) => index % 5), - assets: [], createdAt: Sync.each(() => faker.date.past().toISOString()), updatedAt: Sync.each(() => faker.date.past().toISOString()), id: Sync.each(() => faker.string.uuid()), From 2ff9f95527f4d51775f4d74968c9788588798644 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 15 Apr 2026 19:06:27 -0400 Subject: [PATCH 10/16] chore(deps): update dependency python-multipart to v0.0.26 [security] (#27838) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- machine-learning/uv.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/machine-learning/uv.lock b/machine-learning/uv.lock index 099a22b118..67cff1fdd7 100644 --- a/machine-learning/uv.lock +++ b/machine-learning/uv.lock @@ -2271,11 +2271,11 @@ wheels = [ [[package]] name = "python-multipart" -version = "0.0.22" +version = "0.0.26" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/94/01/979e98d542a70714b0cb2b6728ed0b7c46792b695e3eaec3e20711271ca3/python_multipart-0.0.22.tar.gz", hash = "sha256:7340bef99a7e0032613f56dc36027b959fd3b30a787ed62d310e951f7c3a3a58", size = 37612, upload-time = "2026-01-25T10:15:56.219Z" } +sdist = { url = "https://files.pythonhosted.org/packages/88/71/b145a380824a960ebd60e1014256dbb7d2253f2316ff2d73dfd8928ec2c3/python_multipart-0.0.26.tar.gz", hash = "sha256:08fadc45918cd615e26846437f50c5d6d23304da32c341f289a617127b081f17", size = 43501, upload-time = "2026-04-10T14:09:59.473Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/1b/d0/397f9626e711ff749a95d96b7af99b9c566a9bb5129b8e4c10fc4d100304/python_multipart-0.0.22-py3-none-any.whl", hash = "sha256:2b2cd894c83d21bf49d702499531c7bafd057d730c201782048f7945d82de155", size = 24579, upload-time = "2026-01-25T10:15:54.811Z" }, + { url = "https://files.pythonhosted.org/packages/9a/22/f1925cdda983ab66fc8ec6ec8014b959262747e58bdca26a4e3d1da29d56/python_multipart-0.0.26-py3-none-any.whl", hash = "sha256:c0b169f8c4484c13b0dcf2ef0ec3a4adb255c4b7d18d8e420477d2b1dd03f185", size = 28847, upload-time = "2026-04-10T14:09:58.131Z" }, ] [[package]] From 3d8df74b4327f6cd6571f91046eacdfa04428bb9 Mon Sep 17 00:00:00 2001 From: Min Idzelis Date: Wed, 15 Apr 2026 21:18:49 -0400 Subject: [PATCH 11/16] refactor(web): turn thumbhash action into Thumbhash component (#27741) refactor(web): extract thumbhash canvas into Thumbhash component Change-Id: If78955bed48b6e690df398e5e2ae61fb6a6a6964 --- web/src/lib/actions/thumbhash.ts | 29 -------------- web/src/lib/components/AdaptiveImage.svelte | 4 +- web/src/lib/components/Thumbhash.svelte | 40 +++++++++++++++++++ .../asset-viewer/photo-viewer.svelte | 7 +--- .../assets/thumbnail/thumbnail.svelte | 21 ++++------ 5 files changed, 51 insertions(+), 50 deletions(-) delete mode 100644 web/src/lib/actions/thumbhash.ts create mode 100644 web/src/lib/components/Thumbhash.svelte diff --git a/web/src/lib/actions/thumbhash.ts b/web/src/lib/actions/thumbhash.ts deleted file mode 100644 index 872d3d03bf..0000000000 --- a/web/src/lib/actions/thumbhash.ts +++ /dev/null @@ -1,29 +0,0 @@ -import { decodeBase64 } from '$lib/utils'; -import { thumbHashToRGBA } from 'thumbhash'; - -/** - * Renders a thumbnail onto a canvas from a base64 encoded hash. - */ -export function thumbhash(canvas: HTMLCanvasElement, options: { base64ThumbHash: string }) { - render(canvas, options); - - return { - update(newOptions: { base64ThumbHash: string }) { - render(canvas, newOptions); - }, - }; -} - -const render = (canvas: HTMLCanvasElement, options: { base64ThumbHash: string }) => { - const ctx = canvas.getContext('2d'); - if (!ctx) { - return; - } - - const { w, h, rgba } = thumbHashToRGBA(decodeBase64(options.base64ThumbHash)); - const pixels = ctx.createImageData(w, h); - canvas.width = w; - canvas.height = h; - pixels.data.set(rgba); - ctx.putImageData(pixels, 0, 0); -}; diff --git a/web/src/lib/components/AdaptiveImage.svelte b/web/src/lib/components/AdaptiveImage.svelte index f60260bd05..ed574cbe39 100644 --- a/web/src/lib/components/AdaptiveImage.svelte +++ b/web/src/lib/components/AdaptiveImage.svelte @@ -1,9 +1,9 @@ + + diff --git a/web/src/lib/components/asset-viewer/photo-viewer.svelte b/web/src/lib/components/asset-viewer/photo-viewer.svelte index a3c560ef8d..da00980f08 100644 --- a/web/src/lib/components/asset-viewer/photo-viewer.svelte +++ b/web/src/lib/components/asset-viewer/photo-viewer.svelte @@ -1,9 +1,9 @@ -
+
{@render backdrop?.()}
Date: Thu, 16 Apr 2026 03:20:08 +0200 Subject: [PATCH 14/16] fix(mobile): readonly redirect when not logged in (#27728) --- .../lib/providers/infrastructure/readonly_mode.provider.dart | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/mobile/lib/providers/infrastructure/readonly_mode.provider.dart b/mobile/lib/providers/infrastructure/readonly_mode.provider.dart index 9e96c3cfc4..d503919c90 100644 --- a/mobile/lib/providers/infrastructure/readonly_mode.provider.dart +++ b/mobile/lib/providers/infrastructure/readonly_mode.provider.dart @@ -1,5 +1,6 @@ import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/providers/app_settings.provider.dart'; +import 'package:immich_mobile/providers/auth.provider.dart'; import 'package:immich_mobile/routing/router.dart'; import 'package:immich_mobile/services/app_settings.service.dart'; @@ -14,10 +15,11 @@ class ReadOnlyModeNotifier extends Notifier { } void setMode(bool value) { + final isLoggedIn = ref.read(authProvider).isAuthenticated; _appSettingService.setSetting(AppSettingsEnum.readonlyModeEnabled, value); state = value; - if (value) { + if (value && isLoggedIn) { ref.read(appRouterProvider).navigate(const MainTimelineRoute()); } } From d5d2ebd9bf7bd20c2f4412537f9ce5f026582042 Mon Sep 17 00:00:00 2001 From: Min Idzelis Date: Wed, 15 Apr 2026 21:22:20 -0400 Subject: [PATCH 15/16] fix(web): close edit faces panel on Escape key press (#27519) Move `showEditFaces` state to `assetViewerManager` so the edit faces panel open/close state is globally accessible. Add Escape key handling to `PersonSidePanel` that closes the assign-face sub-panel first, then the edit faces panel. Guard the asset viewer's global Escape-to-close action so it doesn't fire while either face panel is open. Change-Id: I0c947fe0758aef0eac473f4cc72f6a3b6a6a6964 --- .../asset-viewer/asset-viewer-nav-bar.svelte | 2 +- .../asset-viewer/asset-viewer.svelte | 2 +- .../asset-viewer/detail-panel.svelte | 10 +++++----- .../faces-page/person-side-panel.svelte | 14 ++++++++++++++ .../managers/asset-viewer-manager.svelte.ts | 19 +++++++++++++++++++ 5 files changed, 40 insertions(+), 7 deletions(-) diff --git a/web/src/lib/components/asset-viewer/asset-viewer-nav-bar.svelte b/web/src/lib/components/asset-viewer/asset-viewer-nav-bar.svelte index 1b2d15994e..c1494b14f0 100644 --- a/web/src/lib/components/asset-viewer/asset-viewer-nav-bar.svelte +++ b/web/src/lib/components/asset-viewer/asset-viewer-nav-bar.svelte @@ -91,7 +91,7 @@ const Close: ActionItem = $derived({ title: $t('go_back'), icon: languageManager.rtl ? mdiArrowRight : mdiArrowLeft, - $if: () => !!onClose && !assetViewerManager.isFaceEditMode, + $if: () => !!onClose && !assetViewerManager.isFaceEditMode && !assetViewerManager.isEditFacesPanelOpen, onAction: () => onClose?.(), shortcuts: [{ key: 'Escape' }], }); diff --git a/web/src/lib/components/asset-viewer/asset-viewer.svelte b/web/src/lib/components/asset-viewer/asset-viewer.svelte index a2cb1b36dc..bf13cb4399 100644 --- a/web/src/lib/components/asset-viewer/asset-viewer.svelte +++ b/web/src/lib/components/asset-viewer/asset-viewer.svelte @@ -173,7 +173,7 @@ onDestroy(() => { activityManager.reset(); - assetViewerManager.closeEditor(); + assetViewerManager.resetPanelState(); syncAssetViewerOpenClass(false); preloadManager.destroy(); }); diff --git a/web/src/lib/components/asset-viewer/detail-panel.svelte b/web/src/lib/components/asset-viewer/detail-panel.svelte index daacb5f193..7cb486c5a6 100644 --- a/web/src/lib/components/asset-viewer/detail-panel.svelte +++ b/web/src/lib/components/asset-viewer/detail-panel.svelte @@ -54,7 +54,7 @@ let { asset, currentAlbum = null }: Props = $props(); - let showEditFaces = $state(false); + let showEditFaces = $derived(assetViewerManager.isEditFacesPanelOpen); let isOwner = $derived(authManager.authenticated && authManager.user.id === asset.ownerId); let people = $derived(asset.people || []); let unassignedFaces = $derived(asset.unassignedFaces || []); @@ -103,7 +103,7 @@ return; } - showEditFaces = false; + assetViewerManager.closeEditFacesPanel(); previousId = asset.id; }); @@ -119,7 +119,7 @@ const handleRefreshPeople = async () => { asset = await getAssetInfo({ id: asset.id }); - showEditFaces = false; + assetViewerManager.closeEditFacesPanel(); }; const getAssetFolderHref = (asset: AssetResponseDto) => { @@ -214,7 +214,7 @@ shape="round" color="secondary" variant="ghost" - onclick={() => (showEditFaces = true)} + onclick={() => assetViewerManager.openEditFacesPanel()} /> {/if}
@@ -572,7 +572,7 @@ (showEditFaces = false)} + onClose={() => assetViewerManager.closeEditFacesPanel()} onRefresh={handleRefreshPeople} /> {/if} diff --git a/web/src/lib/components/faces-page/person-side-panel.svelte b/web/src/lib/components/faces-page/person-side-panel.svelte index ddeddb1ed2..fa5afadfa6 100644 --- a/web/src/lib/components/faces-page/person-side-panel.svelte +++ b/web/src/lib/components/faces-page/person-side-panel.svelte @@ -1,4 +1,5 @@