mirror of
https://github.com/immich-app/immich.git
synced 2026-02-28 09:38:43 +03:00
feat: mobile editing
This commit is contained in:
@@ -1,11 +1,21 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:auto_route/auto_route.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:immich_mobile/constants/enums.dart';
|
||||
import 'package:immich_mobile/domain/models/asset_edit.model.dart';
|
||||
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
||||
import 'package:immich_mobile/extensions/translate_extensions.dart';
|
||||
import 'package:immich_mobile/presentation/widgets/action_buttons/base_action_button.widget.dart';
|
||||
import 'package:immich_mobile/presentation/widgets/images/image_provider.dart';
|
||||
import 'package:immich_mobile/providers/infrastructure/action.provider.dart';
|
||||
import 'package:immich_mobile/providers/infrastructure/asset.provider.dart';
|
||||
import 'package:immich_mobile/providers/infrastructure/asset_viewer/asset.provider.dart';
|
||||
import 'package:immich_mobile/providers/websocket.provider.dart';
|
||||
import 'package:immich_mobile/routing/router.dart';
|
||||
import 'package:immich_mobile/widgets/common/immich_toast.dart';
|
||||
|
||||
class EditImageActionButton extends ConsumerWidget {
|
||||
const EditImageActionButton({super.key});
|
||||
@@ -14,13 +24,47 @@ class EditImageActionButton extends ConsumerWidget {
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final currentAsset = ref.watch(currentAssetNotifier);
|
||||
|
||||
onPress() {
|
||||
if (currentAsset == null) {
|
||||
Future<void> editImage(List<AssetEdit> edits) async {
|
||||
if (currentAsset == null || currentAsset.remoteId == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
final image = Image(image: getFullImageProvider(currentAsset));
|
||||
context.pushRoute(DriftEditImageRoute(asset: currentAsset, image: image, isEdited: false));
|
||||
try {
|
||||
final completer = ref.read(websocketProvider.notifier).waitForEvent("AssetEditReadyV1", (dynamic data) {
|
||||
final eventData = data as Map<String, dynamic>;
|
||||
return eventData["asset"]['id'] == currentAsset.remoteId;
|
||||
}, const Duration(seconds: 10));
|
||||
|
||||
await ref.read(actionProvider.notifier).applyEdits(ActionSource.viewer, edits);
|
||||
await completer;
|
||||
|
||||
ImmichToast.show(context: context, msg: 'asset_edit_success'.tr(), toastType: ToastType.success);
|
||||
|
||||
context.pop();
|
||||
} catch (e) {
|
||||
ImmichToast.show(context: context, msg: 'asset_edit_failed'.tr(), toastType: ToastType.error);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> onPress() async {
|
||||
if (currentAsset == null || currentAsset.remoteId == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
final imageProvider = getFullImageProvider(currentAsset, edited: false);
|
||||
|
||||
final image = Image(image: imageProvider);
|
||||
final edits = await ref.read(remoteAssetRepositoryProvider).getAssetEdits(currentAsset.remoteId!);
|
||||
final exifInfo = await ref.read(remoteAssetRepositoryProvider).getExif(currentAsset.remoteId!);
|
||||
|
||||
if (exifInfo == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
await context.pushRoute(
|
||||
DriftEditImageRoute(asset: currentAsset, image: image, edits: edits, exifInfo: exifInfo, applyEdits: editImage),
|
||||
);
|
||||
}
|
||||
|
||||
return BaseActionButton(
|
||||
|
||||
@@ -3,12 +3,12 @@ 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/presentation/widgets/asset_viewer/asset_viewer.state.dart';
|
||||
import 'package:immich_mobile/providers/infrastructure/asset_viewer/asset.provider.dart';
|
||||
import 'package:immich_mobile/providers/infrastructure/readonly_mode.provider.dart';
|
||||
@@ -39,7 +39,7 @@ class ViewerBottomBar extends ConsumerWidget {
|
||||
|
||||
if (!isInLockedView) ...[
|
||||
if (asset.isLocalOnly) const UploadActionButton(source: ActionSource.viewer),
|
||||
if (asset.type == AssetType.image) const EditImageActionButton(),
|
||||
if (asset.isEditable) const EditImageActionButton(),
|
||||
if (asset.hasRemote) AddActionButton(originalTheme: originalTheme),
|
||||
|
||||
if (isOwner) ...[
|
||||
|
||||
@@ -105,7 +105,7 @@ mixin CancellableImageProviderMixin<T extends Object> 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)) {
|
||||
@@ -123,13 +123,13 @@ ImageProvider getFullImageProvider(BaseAsset asset, {Size size = const Size(1080
|
||||
} else {
|
||||
throw ArgumentError("Unsupported asset type: ${asset.runtimeType}");
|
||||
}
|
||||
provider = RemoteFullImageProvider(assetId: assetId, thumbhash: thumbhash, assetType: asset.type);
|
||||
provider = RemoteFullImageProvider(assetId: assetId, thumbhash: thumbhash, assetType: asset.type, 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);
|
||||
@@ -137,7 +137,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) =>
|
||||
|
||||
@@ -16,8 +16,8 @@ class RemoteImageProvider extends CancellableImageProvider<RemoteImageProvider>
|
||||
|
||||
RemoteImageProvider({required this.url});
|
||||
|
||||
RemoteImageProvider.thumbnail({required String assetId, required String thumbhash})
|
||||
: url = getThumbnailUrlForRemoteId(assetId, thumbhash: thumbhash);
|
||||
RemoteImageProvider.thumbnail({required String assetId, required String thumbhash, bool edited = true})
|
||||
: url = getThumbnailUrlForRemoteId(assetId, thumbhash: thumbhash, edited: edited);
|
||||
|
||||
@override
|
||||
Future<RemoteImageProvider> obtainKey(ImageConfiguration configuration) {
|
||||
@@ -59,8 +59,14 @@ class RemoteFullImageProvider extends CancellableImageProvider<RemoteFullImagePr
|
||||
final String assetId;
|
||||
final String thumbhash;
|
||||
final AssetType assetType;
|
||||
final bool edited;
|
||||
|
||||
RemoteFullImageProvider({required this.assetId, required this.thumbhash, required this.assetType});
|
||||
RemoteFullImageProvider({
|
||||
required this.assetId,
|
||||
required this.thumbhash,
|
||||
required this.assetType,
|
||||
this.edited = true,
|
||||
});
|
||||
|
||||
@override
|
||||
Future<RemoteFullImageProvider> obtainKey(ImageConfiguration configuration) {
|
||||
@@ -71,7 +77,9 @@ class RemoteFullImageProvider extends CancellableImageProvider<RemoteFullImagePr
|
||||
ImageStreamCompleter loadImage(RemoteFullImageProvider key, ImageDecoderCallback decode) {
|
||||
return OneFramePlaceholderImageStreamCompleter(
|
||||
_codec(key, decode),
|
||||
initialImage: getInitialImage(RemoteImageProvider.thumbnail(assetId: key.assetId, thumbhash: key.thumbhash)),
|
||||
initialImage: getInitialImage(
|
||||
RemoteImageProvider.thumbnail(assetId: key.assetId, thumbhash: key.thumbhash, edited: key.edited),
|
||||
),
|
||||
informationCollector: () => <DiagnosticsNode>[
|
||||
DiagnosticsProperty<ImageProvider>('Image provider', this),
|
||||
DiagnosticsProperty<String>('Asset Id', key.assetId),
|
||||
@@ -90,7 +98,12 @@ class RemoteFullImageProvider extends CancellableImageProvider<RemoteFullImagePr
|
||||
|
||||
final headers = ApiService.getRequestHeaders();
|
||||
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,
|
||||
),
|
||||
headers: headers,
|
||||
);
|
||||
yield* loadRequest(previewRequest, decode);
|
||||
@@ -104,7 +117,10 @@ class RemoteFullImageProvider extends CancellableImageProvider<RemoteFullImagePr
|
||||
return;
|
||||
}
|
||||
|
||||
final originalRequest = request = RemoteImageRequest(uri: getOriginalUrlForRemoteId(key.assetId), headers: headers);
|
||||
final originalRequest = request = RemoteImageRequest(
|
||||
uri: getOriginalUrlForRemoteId(key.assetId, edited: key.edited),
|
||||
headers: headers,
|
||||
);
|
||||
yield* loadRequest(originalRequest, decode);
|
||||
}
|
||||
|
||||
@@ -112,12 +128,12 @@ class RemoteFullImageProvider extends CancellableImageProvider<RemoteFullImagePr
|
||||
bool operator ==(Object other) {
|
||||
if (identical(this, other)) return true;
|
||||
if (other is RemoteFullImageProvider) {
|
||||
return assetId == other.assetId && thumbhash == other.thumbhash;
|
||||
return assetId == other.assetId && thumbhash == other.thumbhash && edited == other.edited;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
@override
|
||||
int get hashCode => assetId.hashCode ^ thumbhash.hashCode;
|
||||
int get hashCode => assetId.hashCode ^ thumbhash.hashCode ^ edited.hashCode;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user