mirror of
https://github.com/immich-app/immich.git
synced 2026-04-05 16:52:00 -04:00
chore: code review changes
chore: code review
This commit is contained in:
parent
67e3809921
commit
54042ea424
33
mobile/lib/constants/aspect_ratios.dart
Normal file
33
mobile/lib/constants/aspect_ratios.dart
Normal file
@ -0,0 +1,33 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class AspectRatioPreset {
|
||||
final double? ratio;
|
||||
final String label;
|
||||
final IconData icon;
|
||||
final bool iconRotated;
|
||||
|
||||
AspectRatioPreset({required this.ratio, required this.label, required this.icon, this.iconRotated = false});
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
if (identical(this, other)) return true;
|
||||
if (other is AspectRatioPreset) {
|
||||
return ratio == other.ratio && label == other.label;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@override
|
||||
int get hashCode => ratio.hashCode ^ label.hashCode;
|
||||
}
|
||||
|
||||
final aspectRatios = <String, AspectRatioPreset>{
|
||||
'Free': AspectRatioPreset(ratio: null, label: 'Free', icon: Icons.crop_free_rounded),
|
||||
'1:1': AspectRatioPreset(ratio: 1.0, label: '1:1', icon: Icons.crop_square_rounded),
|
||||
'16:9': AspectRatioPreset(ratio: 16 / 9, label: '16:9', icon: Icons.crop_16_9_rounded),
|
||||
'3:2': AspectRatioPreset(ratio: 3 / 2, label: '3:2', icon: Icons.crop_3_2_rounded),
|
||||
'7:5': AspectRatioPreset(ratio: 7 / 5, label: '7:5', icon: Icons.crop_7_5_rounded),
|
||||
'9:16': AspectRatioPreset(ratio: 9 / 16, label: '9:16', icon: Icons.crop_16_9_rounded, iconRotated: true),
|
||||
'2:3': AspectRatioPreset(ratio: 2 / 3, label: '2:3', icon: Icons.crop_3_2_rounded, iconRotated: true),
|
||||
'5:7': AspectRatioPreset(ratio: 5 / 7, label: '5:7', icon: Icons.crop_7_5_rounded, iconRotated: true),
|
||||
};
|
||||
@ -69,7 +69,7 @@ sealed class BaseAsset {
|
||||
bool get isLocalOnly => storage == AssetState.local;
|
||||
bool get isRemoteOnly => storage == AssetState.remote;
|
||||
|
||||
bool get isEditable => isImage && !isMotionPhoto && this is RemoteAsset;
|
||||
bool get isEditable => false;
|
||||
|
||||
// Overridden in subclasses
|
||||
AssetState get storage;
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -1,6 +1,5 @@
|
||||
import 'package:immich_mobile/domain/models/album/local_album.model.dart';
|
||||
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
|
||||
import 'package:immich_mobile/domain/models/asset_edit.model.dart';
|
||||
import 'package:immich_mobile/domain/models/exif.model.dart';
|
||||
import 'package:immich_mobile/extensions/platform_extensions.dart';
|
||||
import 'package:immich_mobile/infrastructure/repositories/local_asset.repository.dart';
|
||||
@ -117,12 +116,4 @@ class AssetService {
|
||||
Future<List<LocalAlbum>> getSourceAlbums(String localAssetId, {BackupSelection? backupSelection}) {
|
||||
return _localAssetRepository.getSourceAlbums(localAssetId, backupSelection: backupSelection);
|
||||
}
|
||||
|
||||
Future<List<AssetEdit>> getAssetEdits(String assetId) {
|
||||
return _remoteAssetRepository.getAssetEdits(assetId);
|
||||
}
|
||||
|
||||
Future<void> editAsset(String assetId, List<AssetEdit> edits) {
|
||||
return _remoteAssetRepository.editAsset(assetId, edits);
|
||||
}
|
||||
}
|
||||
|
||||
@ -4,7 +4,6 @@ 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/asset_edit.entity.drift.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';
|
||||
@ -12,7 +11,6 @@ import 'package:immich_mobile/infrastructure/entities/remote_asset.entity.drift.
|
||||
import 'package:immich_mobile/infrastructure/entities/stack.entity.drift.dart';
|
||||
import 'package:immich_mobile/infrastructure/repositories/db.repository.dart';
|
||||
import 'package:maplibre_gl/maplibre_gl.dart';
|
||||
import 'package:uuid/uuid.dart';
|
||||
|
||||
class RemoteAssetRepository extends DriftDatabaseRepository {
|
||||
final Drift _db;
|
||||
@ -269,34 +267,11 @@ class RemoteAssetRepository extends DriftDatabaseRepository {
|
||||
return _db.managers.remoteAssetEntity.count();
|
||||
}
|
||||
|
||||
Future<List<AssetEdit>> getAssetEdits(String assetId) async {
|
||||
Future<List<AssetEdit>> getAssetEdits(String assetId) {
|
||||
final query = _db.assetEditEntity.select()
|
||||
..where((row) => row.assetId.equals(assetId))
|
||||
..orderBy([(row) => OrderingTerm.asc(row.sequence)]);
|
||||
|
||||
return query.map((row) => row.toDto()).get();
|
||||
}
|
||||
|
||||
Future<void> editAsset(String assetId, List<AssetEdit> edits) async {
|
||||
await _db.transaction(() async {
|
||||
await _db.batch((batch) async {
|
||||
// delete existing edits
|
||||
batch.deleteWhere(_db.assetEditEntity, (row) => row.assetId.equals(assetId));
|
||||
|
||||
// insert new edits
|
||||
for (var i = 0; i < edits.length; i++) {
|
||||
final edit = edits[i];
|
||||
final companion = AssetEditEntityCompanion(
|
||||
id: Value(const Uuid().v4()),
|
||||
assetId: Value(assetId),
|
||||
action: Value(edit.action),
|
||||
parameters: Value(edit.parameters),
|
||||
sequence: Value(i),
|
||||
);
|
||||
|
||||
batch.insert(_db.assetEditEntity, companion);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@ -7,7 +7,7 @@ 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/domain/models/asset/base_asset.model.dart';
|
||||
import 'package:immich_mobile/constants/aspect_ratios.dart';
|
||||
import 'package:immich_mobile/domain/models/asset_edit.model.dart';
|
||||
import 'package:immich_mobile/domain/models/exif.model.dart';
|
||||
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
||||
@ -20,7 +20,6 @@ import 'package:openapi/api.dart' show CropParameters, RotateParameters, MirrorP
|
||||
@RoutePage()
|
||||
class DriftEditImagePage extends ConsumerStatefulWidget {
|
||||
final Image image;
|
||||
final BaseAsset asset;
|
||||
final List<AssetEdit> edits;
|
||||
final ExifInfo exifInfo;
|
||||
final Future<void> Function(List<AssetEdit> edits) applyEdits;
|
||||
@ -28,7 +27,6 @@ class DriftEditImagePage extends ConsumerStatefulWidget {
|
||||
const DriftEditImagePage({
|
||||
super.key,
|
||||
required this.image,
|
||||
required this.asset,
|
||||
required this.edits,
|
||||
required this.exifInfo,
|
||||
required this.applyEdits,
|
||||
@ -38,12 +36,10 @@ class DriftEditImagePage extends ConsumerStatefulWidget {
|
||||
ConsumerState<DriftEditImagePage> createState() => _DriftEditImagePageState();
|
||||
}
|
||||
|
||||
typedef AspectRatio = ({double? ratio, String label});
|
||||
|
||||
class _DriftEditImagePageState extends ConsumerState<DriftEditImagePage> with TickerProviderStateMixin {
|
||||
late final CropController cropController;
|
||||
|
||||
Duration _rotationAnimationDuration = const Duration(milliseconds: 250);
|
||||
Duration _rotationAnimationDuration = const Duration(milliseconds: 0);
|
||||
|
||||
int _rotationAngle = 0;
|
||||
bool _flipHorizontal = false;
|
||||
@ -56,18 +52,6 @@ class _DriftEditImagePageState extends ConsumerState<DriftEditImagePage> with Ti
|
||||
bool _isApplyingEdits = false;
|
||||
bool _hasSheetChanges = false;
|
||||
late final Rect _initialCrop;
|
||||
final String _selectedSegment = 'transform';
|
||||
|
||||
List<AspectRatio> aspectRatios = [
|
||||
(ratio: null, label: 'Free'),
|
||||
(ratio: 1.0, label: '1:1'),
|
||||
(ratio: 16.0 / 9.0, label: '16:9'),
|
||||
(ratio: 3.0 / 2.0, label: '3:2'),
|
||||
(ratio: 7.0 / 5.0, label: '7:5'),
|
||||
(ratio: 9.0 / 16.0, label: '9:16'),
|
||||
(ratio: 2.0 / 3.0, label: '2:3'),
|
||||
(ratio: 5.0 / 7.0, label: '5:7'),
|
||||
];
|
||||
|
||||
void initEditor() {
|
||||
final existingCrop = widget.edits.firstWhereOrNull((edit) => edit.action == AssetEditAction.crop);
|
||||
@ -83,9 +67,6 @@ class _DriftEditImagePageState extends ConsumerState<DriftEditImagePage> with Ti
|
||||
cropController = CropController(defaultCrop: crop);
|
||||
|
||||
final transform = normalizeTransformEdits(widget.edits);
|
||||
|
||||
// dont animate to initial rotation
|
||||
_rotationAnimationDuration = const Duration(milliseconds: 0);
|
||||
_rotationAngle = transform.rotation.toInt();
|
||||
|
||||
_flipHorizontal = transform.mirrorHorizontal;
|
||||
@ -160,7 +141,7 @@ class _DriftEditImagePageState extends ConsumerState<DriftEditImagePage> with Ti
|
||||
|
||||
void _rotateLeft() {
|
||||
setState(() {
|
||||
_rotationAnimationDuration = const Duration(milliseconds: 150);
|
||||
_rotationAnimationDuration = const Duration(milliseconds: 250);
|
||||
_rotationAngle -= 90;
|
||||
_hasSheetChanges = true;
|
||||
});
|
||||
@ -168,7 +149,7 @@ class _DriftEditImagePageState extends ConsumerState<DriftEditImagePage> with Ti
|
||||
|
||||
void _rotateRight() {
|
||||
setState(() {
|
||||
_rotationAnimationDuration = const Duration(milliseconds: 150);
|
||||
_rotationAnimationDuration = const Duration(milliseconds: 250);
|
||||
_rotationAngle += 90;
|
||||
_hasSheetChanges = true;
|
||||
});
|
||||
@ -230,30 +211,29 @@ class _DriftEditImagePageState extends ConsumerState<DriftEditImagePage> with Ti
|
||||
return isCropped || isRotated || isFlipped;
|
||||
}
|
||||
|
||||
Future<bool> _showDiscardChangesDialog() async {
|
||||
return await showDialog<bool>(
|
||||
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())),
|
||||
],
|
||||
Future<bool?> _showDiscardChangesDialog() {
|
||||
return showDialog<bool>(
|
||||
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()),
|
||||
),
|
||||
) ??
|
||||
false;
|
||||
TextButton(onPressed: () => Navigator.of(context).pop(true), child: Text('confirm'.tr())),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _handleClose() async {
|
||||
if (hasUnsavedChanges) {
|
||||
final shouldDiscard = await _showDiscardChangesDialog();
|
||||
final shouldDiscard = await _showDiscardChangesDialog() ?? false;
|
||||
if (shouldDiscard && mounted) {
|
||||
Navigator.of(context).pop();
|
||||
}
|
||||
@ -268,7 +248,7 @@ class _DriftEditImagePageState extends ConsumerState<DriftEditImagePage> with Ti
|
||||
canPop: !hasUnsavedChanges,
|
||||
onPopInvokedWithResult: (didPop, result) async {
|
||||
if (didPop) return;
|
||||
final shouldDiscard = await _showDiscardChangesDialog();
|
||||
final shouldDiscard = await _showDiscardChangesDialog() ?? false;
|
||||
if (shouldDiscard && mounted) {
|
||||
Navigator.of(context).pop();
|
||||
}
|
||||
@ -319,19 +299,10 @@ class _DriftEditImagePageState extends ConsumerState<DriftEditImagePage> with Ti
|
||||
padding: const EdgeInsets.all(10),
|
||||
width: (_rotationAngle % 180 == 0) ? baseWidth : baseHeight,
|
||||
height: (_rotationAngle % 180 == 0) ? baseHeight : baseWidth,
|
||||
child: FutureBuilder(
|
||||
future: resolveImage(widget.image.image),
|
||||
builder: (context, data) {
|
||||
if (!data.hasData) {
|
||||
return const Center(child: CircularProgressIndicator());
|
||||
}
|
||||
|
||||
return CropImage(
|
||||
controller: cropController,
|
||||
image: widget.image,
|
||||
gridColor: Colors.white,
|
||||
);
|
||||
},
|
||||
child: CropImage(
|
||||
controller: cropController,
|
||||
image: widget.image,
|
||||
gridColor: Colors.white,
|
||||
),
|
||||
),
|
||||
),
|
||||
@ -357,48 +328,18 @@ class _DriftEditImagePageState extends ConsumerState<DriftEditImagePage> with Ti
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
AnimatedCrossFade(
|
||||
duration: const Duration(milliseconds: 250),
|
||||
firstCurve: Curves.easeInOut,
|
||||
secondCurve: Curves.easeInOut,
|
||||
sizeCurve: Curves.easeInOut,
|
||||
crossFadeState: _selectedSegment == 'transform'
|
||||
? CrossFadeState.showFirst
|
||||
: CrossFadeState.showSecond,
|
||||
firstChild: _TransformControls(
|
||||
onRotateLeft: _rotateLeft,
|
||||
onRotateRight: _rotateRight,
|
||||
onFlipHorizontal: _flipHorizontally,
|
||||
onFlipVertical: _flipVertically,
|
||||
onAspectRatioSelected: _applyAspectRatio,
|
||||
aspectRatio: _aspectRatio,
|
||||
),
|
||||
// this will never show since the segmented button is not shown yet
|
||||
secondChild: const Text("Filters coming soon!"),
|
||||
_TransformControls(
|
||||
onRotateLeft: _rotateLeft,
|
||||
onRotateRight: _rotateRight,
|
||||
onFlipHorizontal: _flipHorizontally,
|
||||
onFlipVertical: _flipVertically,
|
||||
onAspectRatioSelected: _applyAspectRatio,
|
||||
aspectRatio: _aspectRatio,
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(bottom: 36, left: 24, right: 24),
|
||||
child: Row(
|
||||
children: [
|
||||
// SegmentedButton(
|
||||
// segments: [
|
||||
// const ButtonSegment<String>(
|
||||
// value: 'transform',
|
||||
// label: Text('Transform'),
|
||||
// icon: Icon(Icons.transform),
|
||||
// ),
|
||||
// const ButtonSegment<String>(
|
||||
// value: 'filters',
|
||||
// label: Text('Filters'),
|
||||
// icon: Icon(Icons.color_lens),
|
||||
// ),
|
||||
// ],
|
||||
// selected: {selectedSegment},
|
||||
// onSelectionChanged: (value) => setState(() {
|
||||
// selectedSegment = value.first;
|
||||
// }),
|
||||
// showSelectedIcon: false,
|
||||
// ),
|
||||
const Spacer(),
|
||||
ImmichTextButton(
|
||||
labelText: 'reset'.tr(),
|
||||
@ -424,17 +365,11 @@ class _DriftEditImagePageState extends ConsumerState<DriftEditImagePage> with Ti
|
||||
}
|
||||
|
||||
class _AspectRatioButton extends StatelessWidget {
|
||||
final double? currentAspectRatio;
|
||||
final double? ratio;
|
||||
final String label;
|
||||
final AspectRatioPreset ratio;
|
||||
final bool isSelected;
|
||||
final VoidCallback onPressed;
|
||||
|
||||
const _AspectRatioButton({
|
||||
required this.currentAspectRatio,
|
||||
required this.ratio,
|
||||
required this.label,
|
||||
required this.onPressed,
|
||||
});
|
||||
const _AspectRatioButton({required this.ratio, required this.isSelected, required this.onPressed});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
@ -444,22 +379,12 @@ class _AspectRatioButton extends StatelessWidget {
|
||||
IconButton(
|
||||
iconSize: 36,
|
||||
icon: Transform.rotate(
|
||||
angle: (ratio ?? 1.0) < 1.0 ? pi / 2 : 0,
|
||||
child: 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,
|
||||
'9:16' => Icons.crop_16_9_rounded,
|
||||
'2:3' => Icons.crop_3_2_rounded,
|
||||
'5:7' => Icons.crop_7_5_rounded,
|
||||
_ => Icons.crop_free_rounded,
|
||||
}, color: currentAspectRatio == ratio ? context.primaryColor : context.themeData.iconTheme.color),
|
||||
angle: ratio.iconRotated ? pi / 2 : 0,
|
||||
child: Icon(ratio.icon, color: isSelected ? context.primaryColor : context.themeData.iconTheme.color),
|
||||
),
|
||||
onPressed: onPressed,
|
||||
),
|
||||
Text(label, style: context.textTheme.displayMedium),
|
||||
Text(ratio.label, style: context.textTheme.displayMedium),
|
||||
],
|
||||
);
|
||||
}
|
||||
@ -473,17 +398,6 @@ class _AspectRatioSelector extends StatelessWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final aspectRatios = <String, double?>{
|
||||
'Free': null,
|
||||
'1:1': 1.0,
|
||||
'16:9': 16 / 9,
|
||||
'3:2': 3 / 2,
|
||||
'7:5': 7 / 5,
|
||||
'9:16': 9 / 16,
|
||||
'2:3': 2 / 3,
|
||||
'5:7': 5 / 7,
|
||||
};
|
||||
|
||||
return SingleChildScrollView(
|
||||
scrollDirection: Axis.horizontal,
|
||||
child: Row(
|
||||
@ -491,10 +405,9 @@ class _AspectRatioSelector extends StatelessWidget {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8.0),
|
||||
child: _AspectRatioButton(
|
||||
currentAspectRatio: currentAspectRatio,
|
||||
ratio: entry.value,
|
||||
label: entry.key,
|
||||
onPressed: () => onAspectRatioSelected(entry.value),
|
||||
isSelected: currentAspectRatio == entry.value.ratio,
|
||||
onPressed: () => onAspectRatioSelected(entry.value.ratio),
|
||||
),
|
||||
);
|
||||
}).toList(),
|
||||
|
||||
@ -63,7 +63,7 @@ class EditImageActionButton extends ConsumerWidget {
|
||||
}
|
||||
|
||||
await context.pushRoute(
|
||||
DriftEditImageRoute(asset: currentAsset, image: image, edits: edits, exifInfo: exifInfo, applyEdits: editImage),
|
||||
DriftEditImageRoute(image: image, edits: edits, exifInfo: exifInfo, applyEdits: editImage),
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@ -13,10 +13,11 @@ import 'package:openapi/api.dart';
|
||||
class RemoteImageProvider extends CancellableImageProvider<RemoteImageProvider>
|
||||
with CancellableImageProviderMixin<RemoteImageProvider> {
|
||||
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, bool edited = true})
|
||||
RemoteImageProvider.thumbnail({required String assetId, required String thumbhash, this.edited = true})
|
||||
: url = getThumbnailUrlForRemoteId(assetId, thumbhash: thumbhash, edited: edited);
|
||||
|
||||
@override
|
||||
@ -45,13 +46,13 @@ class RemoteImageProvider extends CancellableImageProvider<RemoteImageProvider>
|
||||
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<RemoteFullImageProvider>
|
||||
@ -148,7 +149,12 @@ class RemoteFullImageProvider extends CancellableImageProvider<RemoteFullImagePr
|
||||
}
|
||||
|
||||
final previewRequest = request = RemoteImageRequest(
|
||||
uri: getThumbnailUrlForRemoteId(key.assetId, type: AssetMediaSize.preview, thumbhash: key.thumbhash),
|
||||
uri: getThumbnailUrlForRemoteId(
|
||||
key.assetId,
|
||||
type: AssetMediaSize.preview,
|
||||
thumbhash: key.thumbhash,
|
||||
edited: key.edited,
|
||||
),
|
||||
);
|
||||
yield* loadRequest(previewRequest, decode, evictOnError: false);
|
||||
|
||||
@ -158,7 +164,9 @@ class RemoteFullImageProvider extends CancellableImageProvider<RemoteFullImagePr
|
||||
}
|
||||
|
||||
// always try original for animated, since previews don't support animation
|
||||
final originalRequest = request = RemoteImageRequest(uri: getOriginalUrlForRemoteId(key.assetId));
|
||||
final originalRequest = request = RemoteImageRequest(
|
||||
uri: getOriginalUrlForRemoteId(key.assetId, edited: key.edited),
|
||||
);
|
||||
final codec = await loadCodecRequest(originalRequest);
|
||||
if (codec == null) {
|
||||
throw StateError('Failed to load animated codec for asset ${key.assetId}');
|
||||
|
||||
@ -213,7 +213,7 @@ class WebsocketNotifier extends StateNotifier<WebsocketState> {
|
||||
timeout,
|
||||
onTimeout: () {
|
||||
state.socket?.off(event, handler);
|
||||
throw TimeoutException("Timeout waiting for event: $event");
|
||||
completer.completeError(TimeoutException("Timeout waiting for event: $event"));
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@ -107,23 +107,17 @@ class AssetApiRepository extends ApiRepository {
|
||||
return _api.updateAsset(assetId, UpdateAssetDto(rating: rating));
|
||||
}
|
||||
|
||||
Future<void> editAsset(String assetId, List<AssetEdit> edits) async {
|
||||
Future<AssetEditsResponseDto?> editAsset(String assetId, List<AssetEdit> edits) {
|
||||
final editDtos = edits
|
||||
.map((edit) {
|
||||
if (edit.action == AssetEditAction.other) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return AssetEditActionItemDto(action: edit.action.toDto()!, parameters: edit.parameters);
|
||||
})
|
||||
.whereType<AssetEditActionItemDto>()
|
||||
.where((edit) => edit.action != AssetEditAction.other)
|
||||
.map((edit) => AssetEditActionItemDto(action: edit.action.toDto()!, parameters: edit.parameters))
|
||||
.toList();
|
||||
|
||||
await _api.editAsset(assetId, AssetEditsCreateDto(edits: editDtos));
|
||||
return _api.editAsset(assetId, AssetEditsCreateDto(edits: editDtos));
|
||||
}
|
||||
|
||||
Future<void> removeEdits(String assetId) async {
|
||||
await _api.removeAssetEdits(assetId);
|
||||
return _api.removeAssetEdits(assetId);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -1009,7 +1009,6 @@ class DriftEditImageRoute extends PageRouteInfo<DriftEditImageRouteArgs> {
|
||||
DriftEditImageRoute({
|
||||
Key? key,
|
||||
required Image image,
|
||||
required BaseAsset asset,
|
||||
required List<AssetEdit> edits,
|
||||
required ExifInfo exifInfo,
|
||||
required Future<void> Function(List<AssetEdit>) applyEdits,
|
||||
@ -1019,7 +1018,6 @@ class DriftEditImageRoute extends PageRouteInfo<DriftEditImageRouteArgs> {
|
||||
args: DriftEditImageRouteArgs(
|
||||
key: key,
|
||||
image: image,
|
||||
asset: asset,
|
||||
edits: edits,
|
||||
exifInfo: exifInfo,
|
||||
applyEdits: applyEdits,
|
||||
@ -1036,7 +1034,6 @@ class DriftEditImageRoute extends PageRouteInfo<DriftEditImageRouteArgs> {
|
||||
return DriftEditImagePage(
|
||||
key: args.key,
|
||||
image: args.image,
|
||||
asset: args.asset,
|
||||
edits: args.edits,
|
||||
exifInfo: args.exifInfo,
|
||||
applyEdits: args.applyEdits,
|
||||
@ -1049,7 +1046,6 @@ class DriftEditImageRouteArgs {
|
||||
const DriftEditImageRouteArgs({
|
||||
this.key,
|
||||
required this.image,
|
||||
required this.asset,
|
||||
required this.edits,
|
||||
required this.exifInfo,
|
||||
required this.applyEdits,
|
||||
@ -1059,8 +1055,6 @@ class DriftEditImageRouteArgs {
|
||||
|
||||
final Image image;
|
||||
|
||||
final BaseAsset asset;
|
||||
|
||||
final List<AssetEdit> edits;
|
||||
|
||||
final ExifInfo exifInfo;
|
||||
@ -1069,7 +1063,7 @@ class DriftEditImageRouteArgs {
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'DriftEditImageRouteArgs{key: $key, image: $image, asset: $asset, edits: $edits, exifInfo: $exifInfo, applyEdits: $applyEdits}';
|
||||
return 'DriftEditImageRouteArgs{key: $key, image: $image, edits: $edits, exifInfo: $exifInfo, applyEdits: $applyEdits}';
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -253,8 +253,6 @@ class ActionService {
|
||||
} else {
|
||||
await _assetApiRepository.editAsset(remoteId, edits);
|
||||
}
|
||||
|
||||
await _remoteAssetRepository.editAsset(remoteId, edits);
|
||||
}
|
||||
|
||||
Future<int> _deleteLocalAssets(List<String> localIds) async {
|
||||
|
||||
@ -1,6 +1,4 @@
|
||||
import 'dart:async';
|
||||
import 'dart:math';
|
||||
import 'dart:ui' as ui;
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:immich_mobile/domain/models/asset_edit.model.dart';
|
||||
@ -17,10 +15,10 @@ Rect convertCropParametersToRect(CropParameters parameters, int originalWidth, i
|
||||
}
|
||||
|
||||
CropParameters convertRectToCropParameters(Rect rect, int originalWidth, int originalHeight) {
|
||||
final x = (rect.left * originalWidth).round();
|
||||
final y = (rect.top * originalHeight).round();
|
||||
final width = (rect.width * originalWidth).round();
|
||||
final height = (rect.height * originalHeight).round();
|
||||
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),
|
||||
@ -35,12 +33,22 @@ AffineMatrix buildAffineFromEdits(List<AssetEdit> edits) {
|
||||
edits.map<AffineMatrix>((edit) {
|
||||
switch (edit.action) {
|
||||
case AssetEditAction.rotate:
|
||||
final angleInDegrees = edit.parameters["angle"] as num;
|
||||
final parameters = RotateParameters.fromJson(edit.parameters);
|
||||
if (parameters == null) {
|
||||
throw ArgumentError("Unable to parse rotate parameters from edit: ${edit.parameters}");
|
||||
}
|
||||
|
||||
final angleInDegrees = parameters.angle;
|
||||
final angleInRadians = angleInDegrees * pi / 180;
|
||||
return AffineMatrix.rotate(angleInRadians);
|
||||
|
||||
case AssetEditAction.mirror:
|
||||
final axis = edit.parameters["axis"] as String;
|
||||
return axis == "horizontal" ? AffineMatrix.flipY() : AffineMatrix.flipX();
|
||||
final parameters = MirrorParameters.fromJson(edit.parameters);
|
||||
if (parameters == null) {
|
||||
throw ArgumentError("Unable to parse mirror parameters from edit: ${edit.parameters}");
|
||||
}
|
||||
|
||||
return parameters.axis == MirrorAxis.horizontal ? AffineMatrix.flipY() : AffineMatrix.flipX();
|
||||
default:
|
||||
return AffineMatrix.identity();
|
||||
}
|
||||
@ -70,28 +78,3 @@ NormalizedTransform normalizeTransformEdits(List<AssetEdit> edits) {
|
||||
mirrorVertical: isCloseToZero(a) ? b == c : a == -d,
|
||||
);
|
||||
}
|
||||
|
||||
/// Helper to resolve an ImageProvider to a ui.Image
|
||||
Future<ui.Image> resolveImage(ImageProvider provider) {
|
||||
final completer = Completer<ui.Image>();
|
||||
final stream = provider.resolve(const ImageConfiguration());
|
||||
|
||||
late final ImageStreamListener listener;
|
||||
listener = ImageStreamListener(
|
||||
(ImageInfo info, bool sync) {
|
||||
if (!completer.isCompleted) {
|
||||
completer.complete(info.image);
|
||||
}
|
||||
stream.removeListener(listener);
|
||||
},
|
||||
onError: (error, stackTrace) {
|
||||
if (!completer.isCompleted) {
|
||||
completer.completeError(error, stackTrace);
|
||||
}
|
||||
stream.removeListener(listener);
|
||||
},
|
||||
);
|
||||
|
||||
stream.addListener(listener);
|
||||
return completer.future;
|
||||
}
|
||||
|
||||
@ -1,14 +1,8 @@
|
||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||
import 'package:crop_image/crop_image.dart';
|
||||
import 'dart:ui'; // Import the dart:ui library for Rect
|
||||
|
||||
import 'package:crop_image/crop_image.dart';
|
||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||
|
||||
/// A hook that provides a [CropController] instance.
|
||||
CropController useCropController({Rect? initialCrop, CropRotation? initialRotation}) {
|
||||
return useMemoized(
|
||||
() => CropController(
|
||||
defaultCrop: initialCrop ?? const Rect.fromLTRB(0, 0, 1, 1),
|
||||
rotation: initialRotation ?? CropRotation.up,
|
||||
),
|
||||
);
|
||||
CropController useCropController() {
|
||||
return useMemoized(() => CropController(defaultCrop: const Rect.fromLTRB(0, 0, 1, 1)));
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user