mirror of
https://github.com/immich-app/immich.git
synced 2026-02-22 19:20:19 -05:00
* init * fix * styling * temporary workaround for 500 error **Root cause:** The autogenerated Dart OpenAPI client (`UsersApi.createProfileImage()`) had two issues: 1. It set `Content-Type: multipart/form-data` without a boundary, which overrode the correct header that Dart's `MultipartRequest` would set (`multipart/form-data; boundary=...`). 2. It added the file to both `mp.fields` and `mp.files`, creating a duplicate text field. **Result:** Multer on the server failed to parse the multipart body, so `@UploadedFile()` was `undefined` → accessing `file.path` in `UserService.createProfileImage()` threw → **500 Internal Server Error**. **Workaround:** Bypass the autogenerated method in `UserApiRepository.createProfileImage()` and send the multipart request directly using the same `ApiClient` (basePath + auth), ensuring: - No manual `Content-Type` header (let `MultipartRequest` set it with boundary) - File only in `mp.files`, not `mp.fields` - Proper filename fallback * Revert "temporary workaround for 500 error" This reverts commit 8436cd402632ca7be9272a1c72fdaf0763dcefb6. * generate route for ProfilePictureCropPage * add route import * simplify * try this * Revert "try this" This reverts commit fcf37d2801055c49010ddb4fd271feb900ee645a. * try patching * Reapply "temporary workaround for 500 error" This reverts commit faeed810c21e4c9f0839dfff1f34aa6183469e56. * Revert "Reapply "temporary workaround for 500 error"" This reverts commit a14a0b76d14975af98ef91748576a79cef959635. * fix upload * Refactor image conversion logic by introducing a new utility function. Replace inline image-to-Uint8List conversion with the new utility in EditImagePage, DriftEditImagePage, and ProfilePictureCropPage. * use toast over snack * format * Revert "try patching" This reverts commit 68a616522a1eee88c4a9755a314c0017e6450c0f. * Enhance toast notification in ProfilePictureCropPage to include success type for better user feedback. * Revert "simplify" This reverts commit 8e85057a40678c25bfffa8578ddcc8fd7d1e143e. * format * add tests * refactor to use statefulwidget * format --------- Co-authored-by: Alex <alex.tran1502@gmail.com>
132 lines
5.2 KiB
Dart
132 lines
5.2 KiB
Dart
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<void> _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: <Widget>[
|
|
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: <Widget>[
|
|
Column(
|
|
mainAxisAlignment: MainAxisAlignment.center,
|
|
children: <Widget>[
|
|
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: <Widget>[
|
|
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),
|
|
],
|
|
),
|
|
],
|
|
),
|
|
),
|
|
);
|
|
}
|
|
}
|