diff --git a/mobile/apps/photos/assets/image-editor/image-editor-brightness.svg b/mobile/apps/photos/assets/image-editor/image-editor-brightness.svg
new file mode 100644
index 0000000000..a468e3b80f
--- /dev/null
+++ b/mobile/apps/photos/assets/image-editor/image-editor-brightness.svg
@@ -0,0 +1,6 @@
+
diff --git a/mobile/apps/photos/assets/image-editor/image-editor-contrast.svg b/mobile/apps/photos/assets/image-editor/image-editor-contrast.svg
new file mode 100644
index 0000000000..22e2dcaf3f
--- /dev/null
+++ b/mobile/apps/photos/assets/image-editor/image-editor-contrast.svg
@@ -0,0 +1,3 @@
+
diff --git a/mobile/apps/photos/assets/image-editor/image-editor-crop-original.svg b/mobile/apps/photos/assets/image-editor/image-editor-crop-original.svg
new file mode 100644
index 0000000000..e2cf9c3492
--- /dev/null
+++ b/mobile/apps/photos/assets/image-editor/image-editor-crop-original.svg
@@ -0,0 +1,5 @@
+
diff --git a/mobile/apps/photos/assets/image-editor/image-editor-crop-rotate.svg b/mobile/apps/photos/assets/image-editor/image-editor-crop-rotate.svg
new file mode 100644
index 0000000000..c7f604b8b3
--- /dev/null
+++ b/mobile/apps/photos/assets/image-editor/image-editor-crop-rotate.svg
@@ -0,0 +1,4 @@
+
diff --git a/mobile/apps/photos/assets/image-editor/image-editor-crop.svg b/mobile/apps/photos/assets/image-editor/image-editor-crop.svg
new file mode 100644
index 0000000000..ccd9d13ccd
--- /dev/null
+++ b/mobile/apps/photos/assets/image-editor/image-editor-crop.svg
@@ -0,0 +1,6 @@
+
diff --git a/mobile/apps/photos/assets/image-editor/image-editor-delete.svg b/mobile/apps/photos/assets/image-editor/image-editor-delete.svg
new file mode 100644
index 0000000000..392fc4cb52
--- /dev/null
+++ b/mobile/apps/photos/assets/image-editor/image-editor-delete.svg
@@ -0,0 +1,6 @@
+
diff --git a/mobile/apps/photos/assets/image-editor/image-editor-exposure.svg b/mobile/apps/photos/assets/image-editor/image-editor-exposure.svg
new file mode 100644
index 0000000000..2d4c5c3f4e
--- /dev/null
+++ b/mobile/apps/photos/assets/image-editor/image-editor-exposure.svg
@@ -0,0 +1,3 @@
+
diff --git a/mobile/apps/photos/assets/image-editor/image-editor-fade.svg b/mobile/apps/photos/assets/image-editor/image-editor-fade.svg
new file mode 100644
index 0000000000..9aafd259cf
--- /dev/null
+++ b/mobile/apps/photos/assets/image-editor/image-editor-fade.svg
@@ -0,0 +1,3 @@
+
diff --git a/mobile/apps/photos/assets/image-editor/image-editor-filter.svg b/mobile/apps/photos/assets/image-editor/image-editor-filter.svg
new file mode 100644
index 0000000000..639e801937
--- /dev/null
+++ b/mobile/apps/photos/assets/image-editor/image-editor-filter.svg
@@ -0,0 +1,3 @@
+
diff --git a/mobile/apps/photos/assets/image-editor/image-editor-flip.svg b/mobile/apps/photos/assets/image-editor/image-editor-flip.svg
new file mode 100644
index 0000000000..ba76ac2697
--- /dev/null
+++ b/mobile/apps/photos/assets/image-editor/image-editor-flip.svg
@@ -0,0 +1,4 @@
+
diff --git a/mobile/apps/photos/assets/image-editor/image-editor-hue.svg b/mobile/apps/photos/assets/image-editor/image-editor-hue.svg
new file mode 100644
index 0000000000..96b3df92f3
--- /dev/null
+++ b/mobile/apps/photos/assets/image-editor/image-editor-hue.svg
@@ -0,0 +1,8 @@
+
diff --git a/mobile/apps/photos/assets/image-editor/image-editor-luminance.svg b/mobile/apps/photos/assets/image-editor/image-editor-luminance.svg
new file mode 100644
index 0000000000..02ffd024bd
--- /dev/null
+++ b/mobile/apps/photos/assets/image-editor/image-editor-luminance.svg
@@ -0,0 +1,3 @@
+
diff --git a/mobile/apps/photos/assets/image-editor/image-editor-paint.svg b/mobile/apps/photos/assets/image-editor/image-editor-paint.svg
new file mode 100644
index 0000000000..1c914cd569
--- /dev/null
+++ b/mobile/apps/photos/assets/image-editor/image-editor-paint.svg
@@ -0,0 +1,3 @@
+
diff --git a/mobile/apps/photos/assets/image-editor/image-editor-redo.svg b/mobile/apps/photos/assets/image-editor/image-editor-redo.svg
new file mode 100644
index 0000000000..8d82ac9ecf
--- /dev/null
+++ b/mobile/apps/photos/assets/image-editor/image-editor-redo.svg
@@ -0,0 +1,3 @@
+
diff --git a/mobile/apps/photos/assets/image-editor/image-editor-saturation.svg b/mobile/apps/photos/assets/image-editor/image-editor-saturation.svg
new file mode 100644
index 0000000000..9820828c78
--- /dev/null
+++ b/mobile/apps/photos/assets/image-editor/image-editor-saturation.svg
@@ -0,0 +1,4 @@
+
diff --git a/mobile/apps/photos/assets/image-editor/image-editor-sharpness.svg b/mobile/apps/photos/assets/image-editor/image-editor-sharpness.svg
new file mode 100644
index 0000000000..976aeec184
--- /dev/null
+++ b/mobile/apps/photos/assets/image-editor/image-editor-sharpness.svg
@@ -0,0 +1,3 @@
+
diff --git a/mobile/apps/photos/assets/image-editor/image-editor-sticker.svg b/mobile/apps/photos/assets/image-editor/image-editor-sticker.svg
new file mode 100644
index 0000000000..cd4cd7e01e
--- /dev/null
+++ b/mobile/apps/photos/assets/image-editor/image-editor-sticker.svg
@@ -0,0 +1,3 @@
+
diff --git a/mobile/apps/photos/assets/image-editor/image-editor-temperature.svg b/mobile/apps/photos/assets/image-editor/image-editor-temperature.svg
new file mode 100644
index 0000000000..2616b4a463
--- /dev/null
+++ b/mobile/apps/photos/assets/image-editor/image-editor-temperature.svg
@@ -0,0 +1,3 @@
+
diff --git a/mobile/apps/photos/assets/image-editor/image-editor-text-align-center.svg b/mobile/apps/photos/assets/image-editor/image-editor-text-align-center.svg
new file mode 100644
index 0000000000..755e31b8d0
--- /dev/null
+++ b/mobile/apps/photos/assets/image-editor/image-editor-text-align-center.svg
@@ -0,0 +1,7 @@
+
diff --git a/mobile/apps/photos/assets/image-editor/image-editor-text-align-left.svg b/mobile/apps/photos/assets/image-editor/image-editor-text-align-left.svg
new file mode 100644
index 0000000000..931fe8d5cd
--- /dev/null
+++ b/mobile/apps/photos/assets/image-editor/image-editor-text-align-left.svg
@@ -0,0 +1,5 @@
+
diff --git a/mobile/apps/photos/assets/image-editor/image-editor-text-align-right.svg b/mobile/apps/photos/assets/image-editor/image-editor-text-align-right.svg
new file mode 100644
index 0000000000..5a03574c99
--- /dev/null
+++ b/mobile/apps/photos/assets/image-editor/image-editor-text-align-right.svg
@@ -0,0 +1,5 @@
+
diff --git a/mobile/apps/photos/assets/image-editor/image-editor-text-background.svg b/mobile/apps/photos/assets/image-editor/image-editor-text-background.svg
new file mode 100644
index 0000000000..6e9ae615dd
--- /dev/null
+++ b/mobile/apps/photos/assets/image-editor/image-editor-text-background.svg
@@ -0,0 +1,3 @@
+
diff --git a/mobile/apps/photos/assets/image-editor/image-editor-text-color.svg b/mobile/apps/photos/assets/image-editor/image-editor-text-color.svg
new file mode 100644
index 0000000000..9f176ab63c
--- /dev/null
+++ b/mobile/apps/photos/assets/image-editor/image-editor-text-color.svg
@@ -0,0 +1,5 @@
+
diff --git a/mobile/apps/photos/assets/image-editor/image-editor-text-font.svg b/mobile/apps/photos/assets/image-editor/image-editor-text-font.svg
new file mode 100644
index 0000000000..2c248aab4f
--- /dev/null
+++ b/mobile/apps/photos/assets/image-editor/image-editor-text-font.svg
@@ -0,0 +1,4 @@
+
diff --git a/mobile/apps/photos/assets/image-editor/image-editor-text.svg b/mobile/apps/photos/assets/image-editor/image-editor-text.svg
new file mode 100644
index 0000000000..f2b7eb9ff8
--- /dev/null
+++ b/mobile/apps/photos/assets/image-editor/image-editor-text.svg
@@ -0,0 +1,4 @@
+
diff --git a/mobile/apps/photos/assets/image-editor/image-editor-tune.svg b/mobile/apps/photos/assets/image-editor/image-editor-tune.svg
new file mode 100644
index 0000000000..9de251f75e
--- /dev/null
+++ b/mobile/apps/photos/assets/image-editor/image-editor-tune.svg
@@ -0,0 +1,7 @@
+
diff --git a/mobile/apps/photos/assets/image-editor/image-editor-undo.svg b/mobile/apps/photos/assets/image-editor/image-editor-undo.svg
new file mode 100644
index 0000000000..cb6e99a24d
--- /dev/null
+++ b/mobile/apps/photos/assets/image-editor/image-editor-undo.svg
@@ -0,0 +1,4 @@
+
diff --git a/mobile/apps/photos/lib/ente_theme_data.dart b/mobile/apps/photos/lib/ente_theme_data.dart
index c7ed51640b..6434e3ffe1 100644
--- a/mobile/apps/photos/lib/ente_theme_data.dart
+++ b/mobile/apps/photos/lib/ente_theme_data.dart
@@ -233,6 +233,8 @@ extension CustomColorScheme on ColorScheme {
? const Color(0xFF424242)
: const Color(0xFFFFFFFF);
+ Color get imageEditorPrimaryColor => const Color.fromRGBO(8, 194, 37, 1);
+
Color get defaultBackgroundColor =>
brightness == Brightness.light ? backgroundBaseLight : backgroundBaseDark;
diff --git a/mobile/apps/photos/lib/ui/tools/editor/image_editor/circular_icon_button.dart b/mobile/apps/photos/lib/ui/tools/editor/image_editor/circular_icon_button.dart
new file mode 100644
index 0000000000..7a025fe81f
--- /dev/null
+++ b/mobile/apps/photos/lib/ui/tools/editor/image_editor/circular_icon_button.dart
@@ -0,0 +1,76 @@
+import 'package:flutter/material.dart';
+import "package:flutter_svg/svg.dart";
+import "package:photos/ente_theme_data.dart";
+import "package:photos/theme/ente_theme.dart";
+
+class CircularIconButton extends StatelessWidget {
+ final String label;
+ final VoidCallback onTap;
+ final String? svgPath;
+ final double size;
+ final bool isSelected;
+
+ const CircularIconButton({
+ super.key,
+ required this.label,
+ required this.onTap,
+ this.svgPath,
+ this.size = 60,
+ this.isSelected = false,
+ });
+
+ @override
+ Widget build(BuildContext context) {
+ final textTheme = getEnteTextTheme(context);
+ final colorScheme = getEnteColorScheme(context);
+ return SizedBox(
+ width: 90,
+ child: Column(
+ mainAxisSize: MainAxisSize.min,
+ mainAxisAlignment: MainAxisAlignment.center,
+ children: [
+ GestureDetector(
+ onTap: onTap,
+ child: Container(
+ height: size,
+ width: size,
+ decoration: BoxDecoration(
+ color: isSelected
+ ? Theme.of(context)
+ .colorScheme
+ .imageEditorPrimaryColor
+ .withOpacity(0.24)
+ : colorScheme.backgroundElevated2,
+ shape: BoxShape.circle,
+ border: Border.all(
+ color: isSelected
+ ? Theme.of(context).colorScheme.imageEditorPrimaryColor
+ : colorScheme.backgroundElevated2,
+ width: 2,
+ ),
+ ),
+ child: svgPath != null
+ ? SvgPicture.asset(
+ svgPath!,
+ width: 12,
+ height: 12,
+ fit: BoxFit.scaleDown,
+ colorFilter: ColorFilter.mode(
+ colorScheme.tabIcon,
+ BlendMode.srcIn,
+ ),
+ )
+ : const SizedBox(),
+ ),
+ ),
+ const SizedBox(height: 6),
+ Text(
+ label,
+ style: textTheme.small,
+ textAlign: TextAlign.center,
+ ),
+ ],
+ ),
+ );
+ }
+}
diff --git a/mobile/apps/photos/lib/ui/tools/editor/image_editor/image_editor_app_bar.dart b/mobile/apps/photos/lib/ui/tools/editor/image_editor/image_editor_app_bar.dart
new file mode 100644
index 0000000000..03e84cf75d
--- /dev/null
+++ b/mobile/apps/photos/lib/ui/tools/editor/image_editor/image_editor_app_bar.dart
@@ -0,0 +1,110 @@
+import 'package:flutter/material.dart';
+import "package:flutter_svg/svg.dart";
+import "package:photos/ente_theme_data.dart";
+import "package:photos/theme/ente_theme.dart";
+import "package:pro_image_editor/models/editor_configs/pro_image_editor_configs.dart";
+
+class ImageEditorAppBar extends StatelessWidget implements PreferredSizeWidget {
+ const ImageEditorAppBar({
+ super.key,
+ required this.configs,
+ this.undo,
+ this.redo,
+ required this.done,
+ required this.close,
+ this.enableUndo = false,
+ this.enableRedo = false,
+ this.isMainEditor = false,
+ });
+
+ final ProImageEditorConfigs configs;
+ final Function()? undo;
+ final Function()? redo;
+ final Function() done;
+ final Function() close;
+ final bool enableUndo;
+ final bool enableRedo;
+ final bool isMainEditor;
+
+ @override
+ Size get preferredSize => const Size.fromHeight(kToolbarHeight);
+
+ @override
+ Widget build(BuildContext context) {
+ final colorScheme = getEnteColorScheme(context);
+ return AppBar(
+ elevation: 0,
+ automaticallyImplyLeading: false,
+ title: Row(
+ mainAxisAlignment: MainAxisAlignment.spaceBetween,
+ children: [
+ TextButton(
+ onPressed: () {
+ enableUndo ? close() : Navigator.of(context).pop();
+ },
+ child: Text(
+ 'Cancel',
+ style: getEnteTextTheme(context).body,
+ ),
+ ),
+ if (undo != null && redo != null)
+ Row(
+ mainAxisAlignment: MainAxisAlignment.spaceEvenly,
+ children: [
+ IconButton(
+ tooltip: 'Undo',
+ onPressed: () {
+ undo != null ? undo!() : null;
+ },
+ icon: SvgPicture.asset(
+ "assets/image-editor/image-editor-undo.svg",
+ colorFilter: ColorFilter.mode(
+ enableUndo ? colorScheme.textBase : colorScheme.textMuted,
+ BlendMode.srcIn,
+ ),
+ ),
+ ),
+ const SizedBox(width: 12),
+ IconButton(
+ tooltip: 'Redo',
+ onPressed: () {
+ redo != null ? redo!() : null;
+ },
+ icon: SvgPicture.asset(
+ 'assets/image-editor/image-editor-redo.svg',
+ colorFilter: ColorFilter.mode(
+ enableRedo ? colorScheme.textBase : colorScheme.textMuted,
+ BlendMode.srcIn,
+ ),
+ ),
+ ),
+ ],
+ ),
+ AnimatedSwitcher(
+ duration: const Duration(milliseconds: 250),
+ transitionBuilder: (child, animation) =>
+ FadeTransition(opacity: animation, child: child),
+ child: TextButton(
+ key: ValueKey(isMainEditor ? 'save_copy' : 'done'),
+ onPressed: done,
+ child: Text(
+ isMainEditor ? 'Save Copy' : 'Done',
+ style: getEnteTextTheme(context).body.copyWith(
+ color: isMainEditor
+ ? (enableUndo
+ ? Theme.of(context)
+ .colorScheme
+ .imageEditorPrimaryColor
+ : colorScheme.textMuted)
+ : Theme.of(context)
+ .colorScheme
+ .imageEditorPrimaryColor,
+ ),
+ ),
+ ),
+ ),
+ ],
+ ),
+ );
+ }
+}
diff --git a/mobile/apps/photos/lib/ui/tools/editor/image_editor/image_editor_color_picker.dart b/mobile/apps/photos/lib/ui/tools/editor/image_editor/image_editor_color_picker.dart
new file mode 100644
index 0000000000..b87175d757
--- /dev/null
+++ b/mobile/apps/photos/lib/ui/tools/editor/image_editor/image_editor_color_picker.dart
@@ -0,0 +1,163 @@
+import "package:flutter/material.dart";
+import "package:photos/theme/ente_theme.dart";
+
+class ImageEditorColorPicker extends StatefulWidget {
+ final double value;
+ final ValueChanged onChanged;
+
+ const ImageEditorColorPicker({
+ super.key,
+ required this.value,
+ required this.onChanged,
+ });
+
+ @override
+ ColorSliderState createState() => ColorSliderState();
+}
+
+class ColorSliderState extends State {
+ Color get _selectedColor {
+ final hue = widget.value * 360;
+ return HSVColor.fromAHSV(1.0, hue, 1.0, 1.0).toColor();
+ }
+
+ @override
+ Widget build(BuildContext context) {
+ final colorScheme = getEnteColorScheme(context);
+ return Padding(
+ padding: const EdgeInsets.symmetric(horizontal: 20.0),
+ child: SizedBox(
+ height: 40,
+ child: Stack(
+ alignment: Alignment.center,
+ children: [
+ Container(
+ height: 36,
+ decoration: BoxDecoration(
+ borderRadius: BorderRadius.circular(25),
+ gradient: const LinearGradient(
+ colors: [
+ Color(0xFFFF0000),
+ Color(0xFFFF8000),
+ Color(0xFFFFFF00),
+ Color(0xFF80FF00),
+ Color(0xFF00FF00),
+ Color(0xFF00FF80),
+ Color(0xFF00FFFF),
+ Color(0xFF0080FF),
+ Color(0xFF0000FF),
+ Color(0xFF8000FF),
+ Color(0xFFFF00FF),
+ Color(0xFFFF0080),
+ Color(0xFFFF0000),
+ ],
+ begin: Alignment.centerLeft,
+ end: Alignment.centerRight,
+ ),
+ border: Border.all(
+ color: colorScheme.backgroundElevated2,
+ width: 6,
+ ),
+ ),
+ ),
+ Padding(
+ padding: const EdgeInsets.symmetric(horizontal: 16),
+ child: SliderTheme(
+ data: SliderTheme.of(context).copyWith(
+ trackHeight: 36,
+ thumbShape: _CustomThumbShape(_selectedColor),
+ activeTrackColor: Colors.transparent,
+ inactiveTrackColor: Colors.transparent,
+ overlayShape: const RoundSliderOverlayShape(overlayRadius: 0),
+ trackShape: const _TransparentTrackShape(),
+ ),
+ child: Slider(
+ value: widget.value,
+ onChanged: widget.onChanged,
+ min: 0.0,
+ max: 1.0,
+ ),
+ ),
+ ),
+ ],
+ ),
+ ),
+ );
+ }
+}
+
+class _CustomThumbShape extends SliderComponentShape {
+ final Color color;
+
+ const _CustomThumbShape(this.color);
+
+ @override
+ Size getPreferredSize(bool isEnabled, bool isDiscrete) {
+ return const Size(28, 28);
+ }
+
+ @override
+ void paint(
+ PaintingContext context,
+ Offset center, {
+ required Animation activationAnimation,
+ required Animation enableAnimation,
+ required bool isDiscrete,
+ required TextPainter labelPainter,
+ required RenderBox parentBox,
+ required SliderThemeData sliderTheme,
+ required TextDirection textDirection,
+ required double value,
+ required double textScaleFactor,
+ required Size sizeWithOverflow,
+ }) {
+ final Canvas canvas = context.canvas;
+
+ final Paint thumbPaint = Paint()
+ ..color = color
+ ..style = PaintingStyle.fill;
+
+ canvas.drawCircle(center, 11, thumbPaint);
+
+ final Paint borderPaint = Paint()
+ ..color = Colors.white
+ ..style = PaintingStyle.stroke
+ ..strokeWidth = 3;
+
+ canvas.drawCircle(center, 13, borderPaint);
+ }
+}
+
+class _TransparentTrackShape extends SliderTrackShape {
+ const _TransparentTrackShape();
+
+ @override
+ Rect getPreferredRect({
+ required RenderBox parentBox,
+ Offset offset = Offset.zero,
+ required SliderThemeData sliderTheme,
+ bool isEnabled = false,
+ bool isDiscrete = false,
+ }) {
+ final double trackHeight = sliderTheme.trackHeight!;
+ final double trackLeft = offset.dx;
+ final double trackTop =
+ offset.dy + (parentBox.size.height - trackHeight) / 2;
+ final double trackWidth = parentBox.size.width;
+ return Rect.fromLTWH(trackLeft, trackTop, trackWidth, trackHeight);
+ }
+
+ @override
+ void paint(
+ PaintingContext context,
+ Offset offset, {
+ required RenderBox parentBox,
+ required SliderThemeData sliderTheme,
+ required Animation enableAnimation,
+ required TextDirection textDirection,
+ required Offset thumbCenter,
+ Offset? secondaryOffset,
+ bool isDiscrete = false,
+ bool isEnabled = false,
+ }) {}
+}
diff --git a/mobile/apps/photos/lib/ui/tools/editor/image_editor/image_editor_configs_mixin.dart b/mobile/apps/photos/lib/ui/tools/editor/image_editor/image_editor_configs_mixin.dart
new file mode 100644
index 0000000000..dd0a5c9c42
--- /dev/null
+++ b/mobile/apps/photos/lib/ui/tools/editor/image_editor/image_editor_configs_mixin.dart
@@ -0,0 +1,20 @@
+import 'package:flutter/material.dart';
+import "package:pro_image_editor/mixins/converted_configs.dart";
+import "package:pro_image_editor/models/editor_callbacks/pro_image_editor_callbacks.dart";
+import "package:pro_image_editor/models/editor_configs/pro_image_editor_configs.dart";
+
+/// A mixin providing access to simple editor configurations.
+mixin SimpleConfigsAccess on StatefulWidget {
+ ProImageEditorConfigs get configs;
+ ProImageEditorCallbacks get callbacks;
+}
+
+mixin SimpleConfigsAccessState
+ on State, ImageEditorConvertedConfigs {
+ SimpleConfigsAccess get _widget => (widget as SimpleConfigsAccess);
+
+ @override
+ ProImageEditorConfigs get configs => _widget.configs;
+
+ ProImageEditorCallbacks get callbacks => _widget.callbacks;
+}
diff --git a/mobile/apps/photos/lib/ui/tools/editor/image_editor/image_editor_constants.dart b/mobile/apps/photos/lib/ui/tools/editor/image_editor/image_editor_constants.dart
new file mode 100644
index 0000000000..c798b278b4
--- /dev/null
+++ b/mobile/apps/photos/lib/ui/tools/editor/image_editor/image_editor_constants.dart
@@ -0,0 +1,8 @@
+/// The duration used for fade-in animations.
+const fadeInDuration = Duration(milliseconds: 220);
+
+/// The stagger delay between multiple fade-in animations
+const fadeInDelay = Duration(milliseconds: 25);
+
+/// The height of the sub-bar.
+const editorBottomBarHeight = 180.0;
diff --git a/mobile/apps/photos/lib/ui/tools/editor/image_editor/image_editor_crop_rotate.dart b/mobile/apps/photos/lib/ui/tools/editor/image_editor/image_editor_crop_rotate.dart
new file mode 100644
index 0000000000..a222ed92e4
--- /dev/null
+++ b/mobile/apps/photos/lib/ui/tools/editor/image_editor/image_editor_crop_rotate.dart
@@ -0,0 +1,226 @@
+import 'package:flutter/material.dart';
+import "package:flutter_svg/svg.dart";
+import "package:photos/theme/ente_theme.dart";
+import "package:photos/ui/tools/editor/image_editor/circular_icon_button.dart";
+import "package:photos/ui/tools/editor/image_editor/image_editor_configs_mixin.dart";
+import "package:photos/ui/tools/editor/image_editor/image_editor_constants.dart";
+import "package:pro_image_editor/mixins/converted_configs.dart";
+import "package:pro_image_editor/models/editor_callbacks/pro_image_editor_callbacks.dart";
+import "package:pro_image_editor/models/editor_configs/pro_image_editor_configs.dart";
+import "package:pro_image_editor/modules/crop_rotate_editor/crop_rotate_editor.dart";
+import "package:pro_image_editor/widgets/animated/fade_in_up.dart";
+
+enum CropAspectRatioType {
+ original(
+ label: "Original",
+ ratio: null,
+ svg: "assets/image-editor/image-editor-crop-original.svg",
+ ),
+ free(
+ label: "Free",
+ ratio: null,
+ svg: "assets/video-editor/video-crop-free-action.svg",
+ ),
+ square(
+ label: "1:1",
+ ratio: 1.0,
+ svg: "assets/video-editor/video-crop-ratio_1_1-action.svg",
+ ),
+ widescreen(
+ label: "16:9",
+ ratio: 16.0 / 9.0,
+ svg: "assets/video-editor/video-crop-ratio_16_9-action.svg",
+ ),
+ portrait(
+ label: "9:16",
+ ratio: 9.0 / 16.0,
+ svg: "assets/video-editor/video-crop-ratio_9_16-action.svg",
+ ),
+ photo(
+ label: "4:3",
+ ratio: 4.0 / 3.0,
+ svg: "assets/video-editor/video-crop-ratio_4_3-action.svg",
+ ),
+ photo_3_4(
+ label: "3:4",
+ ratio: 3.0 / 4.0,
+ svg: "assets/video-editor/video-crop-ratio_3_4-action.svg",
+ );
+
+ const CropAspectRatioType({
+ required this.label,
+ required this.ratio,
+ required this.svg,
+ });
+
+ final String label;
+ final String svg;
+ final double? ratio;
+}
+
+class ImageEditorCropRotateBar extends StatefulWidget with SimpleConfigsAccess {
+ const ImageEditorCropRotateBar({
+ super.key,
+ required this.configs,
+ required this.callbacks,
+ required this.editor,
+ });
+ final CropRotateEditorState editor;
+
+ @override
+ final ProImageEditorConfigs configs;
+
+ @override
+ final ProImageEditorCallbacks callbacks;
+
+ @override
+ State createState() =>
+ _ImageEditorCropRotateBarState();
+}
+
+class _ImageEditorCropRotateBarState extends State
+ with ImageEditorConvertedConfigs, SimpleConfigsAccessState {
+ CropAspectRatioType selectedAspectRatio = CropAspectRatioType.original;
+
+ @override
+ Widget build(BuildContext context) {
+ return LayoutBuilder(
+ builder: (context, constraints) {
+ return Column(
+ mainAxisSize: MainAxisSize.min,
+ children: [
+ _buildFunctions(constraints),
+ ],
+ );
+ },
+ );
+ }
+
+ Widget _buildFunctions(BoxConstraints constraints) {
+ return BottomAppBar(
+ color: getEnteColorScheme(context).backgroundBase,
+ padding: EdgeInsets.zero,
+ height: editorBottomBarHeight,
+ child: Align(
+ alignment: Alignment.bottomCenter,
+ child: FadeInUp(
+ duration: fadeInDuration,
+ child: Column(
+ mainAxisAlignment: MainAxisAlignment.spaceAround,
+ children: [
+ Row(
+ mainAxisAlignment: MainAxisAlignment.center,
+ children: [
+ CircularIconButton(
+ svgPath: "assets/image-editor/image-editor-crop-rotate.svg",
+ label: "Rotate",
+ onTap: () {
+ widget.editor.rotate();
+ },
+ ),
+ const SizedBox(width: 6),
+ CircularIconButton(
+ svgPath: "assets/image-editor/image-editor-flip.svg",
+ label: "Flip",
+ onTap: () {
+ widget.editor.flip();
+ },
+ ),
+ ],
+ ),
+ SizedBox(
+ height: 40,
+ child: ListView.builder(
+ scrollDirection: Axis.horizontal,
+ itemCount: CropAspectRatioType.values.length,
+ itemBuilder: (context, index) {
+ final aspectRatio = CropAspectRatioType.values[index];
+ final isSelected = selectedAspectRatio == aspectRatio;
+ return Padding(
+ padding: const EdgeInsets.only(
+ left: 6.0,
+ right: 6.0,
+ ),
+ child: CropAspectChip(
+ label: aspectRatio.label,
+ svg: aspectRatio.svg,
+ isSelected: isSelected,
+ onTap: () {
+ setState(() {
+ selectedAspectRatio = aspectRatio;
+ });
+ widget.editor
+ .updateAspectRatio(aspectRatio.ratio ?? -1);
+ },
+ ),
+ );
+ },
+ ),
+ ),
+ ],
+ ),
+ ),
+ ),
+ );
+ }
+}
+
+class CropAspectChip extends StatelessWidget {
+ final String? label;
+ final IconData? icon;
+ final String? svg;
+ final bool isSelected;
+ final VoidCallback? onTap;
+
+ const CropAspectChip({
+ super.key,
+ this.label,
+ this.icon,
+ this.svg,
+ required this.isSelected,
+ this.onTap,
+ });
+
+ @override
+ Widget build(BuildContext context) {
+ final colorScheme = getEnteColorScheme(context);
+ return GestureDetector(
+ onTap: onTap,
+ child: Container(
+ decoration: BoxDecoration(
+ color: isSelected
+ ? colorScheme.fillBasePressed
+ : colorScheme.backgroundElevated2,
+ borderRadius: BorderRadius.circular(25),
+ ),
+ padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
+ child: Row(
+ mainAxisAlignment: MainAxisAlignment.spaceBetween,
+ children: [
+ if (svg != null) ...[
+ SvgPicture.asset(
+ svg!,
+ height: 32,
+ colorFilter: ColorFilter.mode(
+ isSelected ? colorScheme.backdropBase : colorScheme.tabIcon,
+ BlendMode.srcIn,
+ ),
+ ),
+ ],
+ const SizedBox(width: 4),
+ if (label != null)
+ Text(
+ label!,
+ style: TextStyle(
+ color: isSelected
+ ? colorScheme.backdropBase
+ : colorScheme.tabIcon,
+ ),
+ ),
+ const SizedBox(width: 4),
+ ],
+ ),
+ ),
+ );
+ }
+}
diff --git a/mobile/apps/photos/lib/ui/tools/editor/image_editor/image_editor_filter_bar.dart b/mobile/apps/photos/lib/ui/tools/editor/image_editor/image_editor_filter_bar.dart
new file mode 100644
index 0000000000..8a29cdba52
--- /dev/null
+++ b/mobile/apps/photos/lib/ui/tools/editor/image_editor/image_editor_filter_bar.dart
@@ -0,0 +1,275 @@
+import 'package:figma_squircle/figma_squircle.dart';
+import 'package:flutter/material.dart';
+import "package:photos/ente_theme_data.dart";
+import "package:photos/theme/ente_theme.dart";
+import 'package:pro_image_editor/pro_image_editor.dart';
+
+class GlacierFilterMatrix {
+ static const saturation = [
+ 0.97,
+ 0.02,
+ 0.00,
+ 0.00,
+ 0.00,
+ 0.01,
+ 0.98,
+ 0.00,
+ 0.00,
+ 0.00,
+ 0.01,
+ 0.02,
+ 0.96,
+ 0.00,
+ 0.00,
+ 0.00,
+ 0.00,
+ 0.00,
+ 1.00,
+ 0.00,
+ ];
+
+ static const contrast = [
+ 0.94,
+ 0.00,
+ 0.00,
+ 0.00,
+ 7.07,
+ 0.00,
+ 0.94,
+ 0.00,
+ 0.00,
+ 7.07,
+ 0.00,
+ 0.00,
+ 0.94,
+ 0.00,
+ 7.07,
+ 0.00,
+ 0.00,
+ 0.00,
+ 1.00,
+ 0.00,
+ ];
+
+ static const hue = [
+ 1.01,
+ 0.40,
+ -0.41,
+ 0.00,
+ 0.00,
+ -0.04,
+ 0.91,
+ 0.14,
+ 0.00,
+ 0.00,
+ 0.38,
+ -0.25,
+ 0.87,
+ 0.00,
+ 0.00,
+ 0.00,
+ 0.00,
+ 0.00,
+ 1.00,
+ 0.00,
+ ];
+
+ static const temperature = [
+ 0.80,
+ 0.00,
+ 0.00,
+ 0.00,
+ 0.00,
+ 0.00,
+ 1.00,
+ 0.00,
+ 0.00,
+ 0.00,
+ 0.00,
+ 0.00,
+ 1.00,
+ 0.00,
+ 0.00,
+ 0.00,
+ 0.00,
+ 0.00,
+ 1.00,
+ 0.00,
+ ];
+}
+
+final filterList = [
+ const FilterModel(
+ name: "None",
+ filters: [],
+ ),
+ FilterModel(
+ name: 'Pop',
+ filters: [
+ ColorFilterAddons.saturation(0.3),
+ ColorFilterAddons.brightness(0.15),
+ ],
+ ),
+ FilterModel(
+ name: "Amber",
+ filters: [
+ ColorFilterAddons.rgbScale(1.01, 1.04, 1),
+ ColorFilterAddons.saturation(0.3),
+ ],
+ ),
+ FilterModel(
+ name: 'Dust',
+ filters: [
+ ColorFilterAddons.sepia(0.4),
+ ColorFilterAddons.brightness(0.13),
+ ColorFilterAddons.contrast(-.05),
+ ],
+ ),
+ FilterModel(
+ name: 'Carbon',
+ filters: [
+ ColorFilterAddons.contrast(0.2),
+ ColorFilterAddons.grayscale(),
+ ],
+ ),
+ const FilterModel(
+ name: 'Glacier',
+ filters: [
+ GlacierFilterMatrix.saturation,
+ GlacierFilterMatrix.temperature,
+ GlacierFilterMatrix.hue,
+ GlacierFilterMatrix.contrast,
+ ],
+ ),
+ FilterModel(
+ name: 'Haze',
+ filters: [
+ ColorFilterAddons.colorOverlay(228, 130, 225, 0.13),
+ ColorFilterAddons.saturation(-0.2),
+ ],
+ ),
+ FilterModel(
+ name: 'Meadow',
+ filters: [
+ ColorFilterAddons.rgbScale(1.05, 1.1, 1),
+ ],
+ ),
+ FilterModel(
+ name: 'Zest',
+ filters: [
+ ColorFilterAddons.brightness(.1),
+ ColorFilterAddons.contrast(.1),
+ ColorFilterAddons.saturation(.15),
+ ],
+ ),
+ FilterModel(
+ name: 'Retro',
+ filters: [
+ ColorFilterAddons.colorOverlay(25, 240, 252, 0.05),
+ ColorFilterAddons.sepia(0.3),
+ ],
+ ),
+ FilterModel(
+ name: 'Sepia',
+ filters: [
+ ColorFilterAddons.contrast(-0.15),
+ ColorFilterAddons.saturation(0.1),
+ ],
+ ),
+];
+
+class ImageEditorFilterBar extends StatefulWidget {
+ const ImageEditorFilterBar({
+ required this.filterModel,
+ required this.isSelected,
+ required this.onSelectFilter,
+ required this.editorImage,
+ this.filterKey,
+ super.key,
+ });
+
+ final FilterModel filterModel;
+ final bool isSelected;
+ final VoidCallback onSelectFilter;
+ final Widget editorImage;
+ final Key? filterKey;
+
+ @override
+ State createState() => _ImageEditorFilterBarState();
+}
+
+class _ImageEditorFilterBarState extends State {
+ @override
+ Widget build(BuildContext context) {
+ return buildFilteredOptions(
+ widget.editorImage,
+ widget.isSelected,
+ widget.filterModel.name,
+ widget.onSelectFilter,
+ widget.filterKey ?? ValueKey(widget.filterModel.name),
+ );
+ }
+
+ Widget buildFilteredOptions(
+ Widget editorImage,
+ bool isSelected,
+ String filterName,
+ VoidCallback onSelectFilter,
+ Key filterKey,
+ ) {
+ return GestureDetector(
+ onTap: () => onSelectFilter(),
+ child: SizedBox(
+ height: 90,
+ width: 80,
+ child: Column(
+ mainAxisAlignment: MainAxisAlignment.spaceAround,
+ children: [
+ Container(
+ height: 60,
+ width: 60,
+ decoration: ShapeDecoration(
+ shape: SmoothRectangleBorder(
+ borderRadius: SmoothBorderRadius(
+ cornerRadius: 12,
+ cornerSmoothing: 0.6,
+ ),
+ side: BorderSide(
+ color: isSelected
+ ? Theme.of(context).colorScheme.imageEditorPrimaryColor
+ : Colors.transparent,
+ width: 1.5,
+ ),
+ ),
+ ),
+ child: Padding(
+ padding: isSelected ? const EdgeInsets.all(2) : EdgeInsets.zero,
+ child: ClipSmoothRect(
+ radius: SmoothBorderRadius(
+ cornerRadius: 9.69,
+ cornerSmoothing: 0.4,
+ ),
+ child: SizedBox(
+ height: isSelected ? 56 : 60,
+ width: isSelected ? 56 : 60,
+ child: editorImage,
+ ),
+ ),
+ ),
+ ),
+ const SizedBox(height: 8),
+ Text(
+ filterName,
+ style: isSelected
+ ? getEnteTextTheme(context).smallBold
+ : getEnteTextTheme(context).smallMuted,
+ textAlign: TextAlign.center,
+ maxLines: 1,
+ overflow: TextOverflow.ellipsis,
+ ),
+ ],
+ ),
+ ),
+ );
+ }
+}
diff --git a/mobile/apps/photos/lib/ui/tools/editor/image_editor/image_editor_main_bottom_bar.dart b/mobile/apps/photos/lib/ui/tools/editor/image_editor/image_editor_main_bottom_bar.dart
new file mode 100644
index 0000000000..3321e645c1
--- /dev/null
+++ b/mobile/apps/photos/lib/ui/tools/editor/image_editor/image_editor_main_bottom_bar.dart
@@ -0,0 +1,136 @@
+import 'dart:math';
+
+import 'package:flutter/material.dart';
+import "package:photos/ui/tools/editor/image_editor/circular_icon_button.dart";
+import "package:photos/ui/tools/editor/image_editor/image_editor_configs_mixin.dart";
+import "package:photos/ui/tools/editor/image_editor/image_editor_constants.dart";
+import "package:pro_image_editor/mixins/converted_configs.dart";
+import 'package:pro_image_editor/pro_image_editor.dart';
+
+class ImageEditorMainBottomBar extends StatefulWidget with SimpleConfigsAccess {
+ const ImageEditorMainBottomBar({
+ super.key,
+ required this.configs,
+ required this.callbacks,
+ required this.editor,
+ });
+
+ final ProImageEditorState editor;
+
+ @override
+ final ProImageEditorConfigs configs;
+ @override
+ final ProImageEditorCallbacks callbacks;
+
+ @override
+ State createState() =>
+ ImageEditorMainBottomBarState();
+}
+
+class ImageEditorMainBottomBarState extends State
+ with ImageEditorConvertedConfigs, SimpleConfigsAccessState {
+ @override
+ Widget build(BuildContext context) {
+ return LayoutBuilder(
+ builder: (context, constraints) {
+ return Column(
+ mainAxisSize: MainAxisSize.min,
+ children: [
+ _buildFunctions(constraints),
+ ],
+ );
+ },
+ );
+ }
+
+ Widget _buildFunctions(BoxConstraints constraints) {
+ return BottomAppBar(
+ height: editorBottomBarHeight,
+ padding: EdgeInsets.zero,
+ clipBehavior: Clip.none,
+ child: AnimatedSwitcher(
+ layoutBuilder: (currentChild, previousChildren) => Stack(
+ clipBehavior: Clip.none,
+ alignment: Alignment.bottomCenter,
+ children: [
+ ...previousChildren,
+ if (currentChild != null) currentChild,
+ ],
+ ),
+ duration: const Duration(milliseconds: 400),
+ reverseDuration: const Duration(milliseconds: 0),
+ transitionBuilder: (child, animation) {
+ return FadeTransition(
+ opacity: animation,
+ child: SizeTransition(
+ sizeFactor: animation,
+ axis: Axis.vertical,
+ axisAlignment: -1,
+ child: child,
+ ),
+ );
+ },
+ switchInCurve: Curves.ease,
+ child: widget.editor.isSubEditorOpen &&
+ !widget.editor.isSubEditorClosing
+ ? const SizedBox.shrink()
+ : Align(
+ alignment: Alignment.center,
+ child: SingleChildScrollView(
+ clipBehavior: Clip.none,
+ scrollDirection: Axis.horizontal,
+ child: ConstrainedBox(
+ constraints: BoxConstraints(
+ minWidth: min(constraints.maxWidth, 600),
+ maxWidth: 600,
+ ),
+ child: Row(
+ mainAxisAlignment: MainAxisAlignment.spaceBetween,
+ mainAxisSize: MainAxisSize.min,
+ children: [
+ CircularIconButton(
+ svgPath: "assets/image-editor/image-editor-crop.svg",
+ label: "Crop",
+ onTap: () {
+ widget.editor.openCropRotateEditor();
+ },
+ ),
+ CircularIconButton(
+ svgPath:
+ "assets/image-editor/image-editor-filter.svg",
+ label: "Filter",
+ onTap: () {
+ widget.editor.openFilterEditor();
+ },
+ ),
+ CircularIconButton(
+ svgPath: "assets/image-editor/image-editor-tune.svg",
+ label: "Adjust",
+ onTap: () {
+ widget.editor.openTuneEditor();
+ },
+ ),
+ CircularIconButton(
+ svgPath: "assets/image-editor/image-editor-paint.svg",
+ label: "Draw",
+ onTap: () {
+ widget.editor.openPaintingEditor();
+ },
+ ),
+ CircularIconButton(
+ svgPath:
+ "assets/image-editor/image-editor-sticker.svg",
+ label: "Sticker",
+ onTap: () {
+ widget.editor.openEmojiEditor();
+ },
+ ),
+ ],
+ ),
+ ),
+ ),
+ ),
+ ),
+ );
+ }
+}
diff --git a/mobile/apps/photos/lib/ui/tools/editor/image_editor/image_editor_page_new.dart b/mobile/apps/photos/lib/ui/tools/editor/image_editor/image_editor_page_new.dart
new file mode 100644
index 0000000000..f99799378e
--- /dev/null
+++ b/mobile/apps/photos/lib/ui/tools/editor/image_editor/image_editor_page_new.dart
@@ -0,0 +1,561 @@
+import "dart:async";
+import "dart:io";
+import "dart:math";
+import "dart:typed_data";
+import 'dart:ui' as ui show Image;
+
+import 'package:flutter/material.dart';
+import "package:flutter_image_compress/flutter_image_compress.dart";
+import "package:flutter_svg/svg.dart";
+import "package:logging/logging.dart";
+import 'package:path/path.dart' as path;
+import "package:photo_manager/photo_manager.dart";
+import "package:photos/core/event_bus.dart";
+import "package:photos/db/files_db.dart";
+import "package:photos/ente_theme_data.dart";
+import "package:photos/events/local_photos_updated_event.dart";
+import "package:photos/generated/l10n.dart";
+import 'package:photos/models/file/file.dart' as ente;
+import "package:photos/models/location/location.dart";
+import "package:photos/services/sync/sync_service.dart";
+import "package:photos/theme/ente_theme.dart";
+import "package:photos/ui/components/action_sheet_widget.dart";
+import "package:photos/ui/components/buttons/button_widget.dart";
+import "package:photos/ui/components/models/button_type.dart";
+import "package:photos/ui/notification/toast.dart";
+import "package:photos/ui/tools/editor/image_editor/image_editor_app_bar.dart";
+import "package:photos/ui/tools/editor/image_editor/image_editor_constants.dart";
+import "package:photos/ui/tools/editor/image_editor/image_editor_crop_rotate.dart";
+import "package:photos/ui/tools/editor/image_editor/image_editor_filter_bar.dart";
+import "package:photos/ui/tools/editor/image_editor/image_editor_main_bottom_bar.dart";
+import "package:photos/ui/tools/editor/image_editor/image_editor_paint_bar.dart";
+import "package:photos/ui/tools/editor/image_editor/image_editor_text_bar.dart";
+import "package:photos/ui/tools/editor/image_editor/image_editor_tune_bar.dart";
+import "package:photos/ui/viewer/file/detail_page.dart";
+import "package:photos/utils/dialog_util.dart";
+import "package:photos/utils/navigation_util.dart";
+import "package:pro_image_editor/models/editor_configs/main_editor_configs.dart";
+import 'package:pro_image_editor/pro_image_editor.dart';
+
+class NewImageEditor extends StatefulWidget {
+ final ente.EnteFile originalFile;
+ final File file;
+ final DetailPageConfiguration detailPageConfig;
+
+ const NewImageEditor({
+ super.key,
+ required this.file,
+ required this.originalFile,
+ required this.detailPageConfig,
+ });
+
+ @override
+ State createState() => _NewImageEditorState();
+}
+
+class _NewImageEditorState extends State {
+ final _mainEditorBarKey = GlobalKey();
+ final editorKey = GlobalKey();
+ final _logger = Logger("ImageEditor");
+
+ Future saveImage(Uint8List? bytes) async {
+ if (bytes == null) return;
+
+ final dialog = createProgressDialog(context, S.of(context).saving);
+ await dialog.show();
+
+ debugPrint("Image saved with size: ${bytes.length} bytes");
+ final DateTime start = DateTime.now();
+
+ final ui.Image decodedResult = await decodeImageFromList(bytes);
+ final result = await FlutterImageCompress.compressWithList(
+ bytes,
+ minWidth: decodedResult.width,
+ minHeight: decodedResult.height,
+ );
+ _logger.info('Size after compression = ${result.length}');
+ final Duration diff = DateTime.now().difference(start);
+ _logger.info('image_editor time : $diff');
+
+ try {
+ final fileName =
+ path.basenameWithoutExtension(widget.originalFile.title!) +
+ "_edited_" +
+ DateTime.now().microsecondsSinceEpoch.toString() +
+ ".JPEG";
+ //Disabling notifications for assets changing to insert the file into
+ //files db before triggering a sync.
+ await PhotoManager.stopChangeNotify();
+ final AssetEntity newAsset =
+ await (PhotoManager.editor.saveImage(result, filename: fileName));
+ final newFile = await ente.EnteFile.fromAsset(
+ widget.originalFile.deviceFolder ?? '',
+ newAsset,
+ );
+
+ newFile.creationTime = widget.originalFile.creationTime;
+ newFile.collectionID = widget.originalFile.collectionID;
+ newFile.location = widget.originalFile.location;
+ if (!newFile.hasLocation && widget.originalFile.localID != null) {
+ final assetEntity = await widget.originalFile.getAsset;
+ if (assetEntity != null) {
+ final latLong = await assetEntity.latlngAsync();
+ newFile.location = Location(
+ latitude: latLong.latitude,
+ longitude: latLong.longitude,
+ );
+ }
+ }
+ newFile.generatedID = await FilesDB.instance.insertAndGetId(newFile);
+ Bus.instance.fire(LocalPhotosUpdatedEvent([newFile], source: "editSave"));
+ unawaited(SyncService.instance.sync());
+ showShortToast(context, S.of(context).editsSaved);
+ _logger.info("Original file " + widget.originalFile.toString());
+ _logger.info("Saved edits to file " + newFile.toString());
+ final files = widget.detailPageConfig.files;
+
+ // the index could be -1 if the files fetched doesn't contain the newly
+ // edited files
+ int selectionIndex =
+ files.indexWhere((file) => file.generatedID == newFile.generatedID);
+ if (selectionIndex == -1) {
+ files.add(newFile);
+ selectionIndex = files.length - 1;
+ }
+ await dialog.hide();
+ replacePage(
+ context,
+ DetailPage(
+ widget.detailPageConfig.copyWith(
+ files: files,
+ selectedIndex: min(selectionIndex, files.length - 1),
+ ),
+ ),
+ );
+ } catch (e, s) {
+ await dialog.hide();
+ showToast(context, S.of(context).oopsCouldNotSaveEdits);
+ _logger.severe(e, s);
+ } finally {
+ await PhotoManager.startChangeNotify();
+ }
+ }
+
+ Future _showExitConfirmationDialog(BuildContext context) async {
+ final actionResult = await showActionSheet(
+ context: context,
+ buttons: [
+ ButtonWidget(
+ labelText: S.of(context).yesDiscardChanges,
+ buttonType: ButtonType.critical,
+ buttonSize: ButtonSize.large,
+ shouldStickToDarkTheme: true,
+ buttonAction: ButtonAction.first,
+ isInAlert: true,
+ ),
+ ButtonWidget(
+ labelText: S.of(context).no,
+ buttonType: ButtonType.secondary,
+ buttonSize: ButtonSize.large,
+ buttonAction: ButtonAction.second,
+ shouldStickToDarkTheme: true,
+ isInAlert: true,
+ ),
+ ],
+ body: S.of(context).doYouWantToDiscardTheEditsYouHaveMade,
+ actionSheetType: ActionSheetType.defaultActionSheet,
+ );
+ if (actionResult?.action != null &&
+ actionResult!.action == ButtonAction.first) {
+ replacePage(context, DetailPage(widget.detailPageConfig));
+ }
+ }
+
+ @override
+ Widget build(BuildContext context) {
+ final isLightMode = Theme.of(context).brightness == Brightness.light;
+ final colorScheme = getEnteColorScheme(context);
+ final textTheme = getEnteTextTheme(context);
+ return Scaffold(
+ resizeToAvoidBottomInset: false,
+ backgroundColor: colorScheme.backgroundBase,
+ body: PopScope(
+ canPop: false,
+ onPopInvoked: (didPop) {
+ if (didPop) return;
+ editorKey.currentState?.disablePopScope = true;
+ _showExitConfirmationDialog(context);
+ },
+ child: ProImageEditor.file(
+ key: editorKey,
+ widget.file,
+ callbacks: ProImageEditorCallbacks(
+ onCloseEditor: () {
+ editorKey.currentState?.disablePopScope = true;
+ _showExitConfirmationDialog(context);
+ },
+ mainEditorCallbacks: MainEditorCallbacks(
+ onStartCloseSubEditor: (value) {
+ _mainEditorBarKey.currentState?.setState(() {});
+ },
+ onPopInvoked: (didPop, result) {
+ editorKey.currentState?.disablePopScope = false;
+ },
+ ),
+ ),
+ configs: ProImageEditorConfigs(
+ imageEditorTheme: ImageEditorTheme(
+ appBarBackgroundColor: colorScheme.backgroundBase,
+ background: colorScheme.backgroundBase,
+ bottomBarBackgroundColor: colorScheme.backgroundBase,
+ filterEditor: FilterEditorTheme(
+ background: colorScheme.backgroundBase,
+ ),
+ paintingEditor: PaintingEditorTheme(
+ background: colorScheme.backgroundBase,
+ ),
+ textEditor: const TextEditorTheme(
+ background: Colors.transparent,
+ textFieldMargin: EdgeInsets.only(top: kToolbarHeight),
+ ),
+ cropRotateEditor: CropRotateEditorTheme(
+ background: colorScheme.backgroundBase,
+ cropCornerColor:
+ Theme.of(context).colorScheme.imageEditorPrimaryColor,
+ ),
+ tuneEditor: TuneEditorTheme(
+ background: colorScheme.backgroundBase,
+ ),
+ emojiEditor: EmojiEditorTheme(
+ backgroundColor: colorScheme.backgroundBase,
+ ),
+ ),
+ imageGenerationConfigs: const ImageGenerationConfigs(
+ jpegQuality: 100,
+ generateInsideSeparateThread: true,
+ pngLevel: 0,
+ ),
+ layerInteraction: const LayerInteraction(
+ hideToolbarOnInteraction: false,
+ ),
+ theme: ThemeData(
+ scaffoldBackgroundColor: colorScheme.backgroundBase,
+ appBarTheme: AppBarTheme(
+ titleTextStyle: textTheme.body,
+ backgroundColor: colorScheme.backgroundBase,
+ ),
+ bottomAppBarTheme: BottomAppBarTheme(
+ color: colorScheme.backgroundBase,
+ ),
+ brightness: isLightMode ? Brightness.light : Brightness.dark,
+ ),
+ customWidgets: ImageEditorCustomWidgets(
+ filterEditor: CustomWidgetsFilterEditor(
+ slider: (
+ editorState,
+ rebuildStream,
+ value,
+ onChanged,
+ onChangeEnd,
+ ) =>
+ ReactiveCustomWidget(
+ builder: (context) {
+ return const SizedBox.shrink();
+ },
+ stream: rebuildStream,
+ ),
+ filterButton: (
+ filter,
+ isSelected,
+ scaleFactor,
+ onSelectFilter,
+ editorImage,
+ filterKey,
+ ) {
+ return ImageEditorFilterBar(
+ filterModel: filter,
+ isSelected: isSelected,
+ onSelectFilter: () {
+ onSelectFilter.call();
+ editorKey.currentState?.setState(() {});
+ },
+ editorImage: editorImage,
+ filterKey: filterKey,
+ );
+ },
+ appBar: (editor, rebuildStream) {
+ return ReactiveCustomAppbar(
+ builder: (context) {
+ return ImageEditorAppBar(
+ key: const Key('image_editor_app_bar'),
+ configs: editor.configs,
+ done: () => editor.done(),
+ close: () => editor.close(),
+ );
+ },
+ stream: rebuildStream,
+ );
+ },
+ ),
+ tuneEditor: CustomWidgetsTuneEditor(
+ appBar: (editor, rebuildStream) {
+ return ReactiveCustomAppbar(
+ builder: (context) {
+ return ImageEditorAppBar(
+ enableRedo: editor.canRedo,
+ enableUndo: editor.canUndo,
+ key: const Key('image_editor_app_bar'),
+ redo: () => editor.redo(),
+ undo: () => editor.undo(),
+ configs: editor.configs,
+ done: () => editor.done(),
+ close: () => editor.close(),
+ );
+ },
+ stream: rebuildStream,
+ );
+ },
+ bottomBar: (editorState, rebuildStream) {
+ return ReactiveCustomWidget(
+ builder: (context) {
+ return ImageEditorTuneBar(
+ configs: editorState.configs,
+ callbacks: editorState.callbacks,
+ editor: editorState,
+ );
+ },
+ stream: rebuildStream,
+ );
+ },
+ ),
+ mainEditor: CustomWidgetsMainEditor(
+ removeLayerArea: (key, rebuildStream) {
+ return ReactiveCustomWidget(
+ key: key,
+ builder: (context) {
+ return Align(
+ alignment: Alignment.bottomCenter,
+ child: StreamBuilder(
+ stream: rebuildStream,
+ builder: (context, snapshot) {
+ final isHovered = editorKey.currentState!
+ .layerInteractionManager.hoverRemoveBtn;
+
+ return AnimatedContainer(
+ key: key,
+ duration: const Duration(milliseconds: 150),
+ height: 56,
+ width: 56,
+ margin: const EdgeInsets.only(bottom: 24),
+ decoration: BoxDecoration(
+ color: isHovered
+ ? colorScheme.warning400.withOpacity(0.8)
+ : Colors.white,
+ shape: BoxShape.circle,
+ ),
+ padding: const EdgeInsets.all(12),
+ child: Center(
+ child: SvgPicture.asset(
+ "assets/image-editor/image-editor-delete.svg",
+ colorFilter: ColorFilter.mode(
+ isHovered
+ ? Colors.white
+ : colorScheme.warning400
+ .withOpacity(0.8),
+ BlendMode.srcIn,
+ ),
+ ),
+ ),
+ );
+ },
+ ),
+ );
+ },
+ stream: rebuildStream,
+ );
+ },
+ appBar: (editor, rebuildStream) {
+ return ReactiveCustomAppbar(
+ builder: (context) {
+ return ImageEditorAppBar(
+ enableRedo: editor.canRedo,
+ enableUndo: editor.canUndo,
+ key: const Key('image_editor_app_bar'),
+ redo: () => editor.redoAction(),
+ undo: () => editor.undoAction(),
+ configs: editor.configs,
+ done: () async {
+ final Uint8List bytes = await editorKey.currentState!
+ .captureEditorImage();
+ await saveImage(bytes);
+ },
+ close: () {
+ _showExitConfirmationDialog(context);
+ },
+ isMainEditor: true,
+ );
+ },
+ stream: rebuildStream,
+ );
+ },
+ bottomBar: (editor, rebuildStream, key) => ReactiveCustomWidget(
+ key: key,
+ builder: (context) {
+ return ImageEditorMainBottomBar(
+ key: _mainEditorBarKey,
+ editor: editor,
+ configs: editor.configs,
+ callbacks: editor.callbacks,
+ );
+ },
+ stream: rebuildStream,
+ ),
+ ),
+ paintEditor: CustomWidgetsPaintEditor(
+ appBar: (editor, rebuildStream) {
+ return ReactiveCustomAppbar(
+ builder: (context) {
+ return ImageEditorAppBar(
+ enableRedo: editor.canRedo,
+ enableUndo: editor.canUndo,
+ key: const Key('image_editor_app_bar'),
+ redo: () => editor.redoAction(),
+ undo: () => editor.undoAction(),
+ configs: editor.configs,
+ done: () => editor.done(),
+ close: () => editor.close(),
+ );
+ },
+ stream: rebuildStream,
+ );
+ },
+ colorPicker:
+ (paintEditor, rebuildStream, currentColor, setColor) =>
+ null,
+ bottomBar: (editorState, rebuildStream) {
+ return ReactiveCustomWidget(
+ builder: (context) {
+ return ImageEditorPaintBar(
+ configs: editorState.configs,
+ callbacks: editorState.callbacks,
+ editor: editorState,
+ i18nColor: 'Color',
+ );
+ },
+ stream: rebuildStream,
+ );
+ },
+ ),
+ textEditor: CustomWidgetsTextEditor(
+ appBar: (textEditor, rebuildStream) => ReactiveCustomAppbar(
+ builder: (context) {
+ return ImageEditorAppBar(
+ key: const Key('image_editor_app_bar'),
+ configs: textEditor.configs,
+ done: () => textEditor.done(),
+ close: () => textEditor.close(),
+ );
+ },
+ stream: rebuildStream,
+ ),
+ bodyItems: (editor, rebuildStream) {
+ return [
+ ReactiveCustomWidget(
+ builder: (context) {
+ return Positioned.fill(
+ child: GestureDetector(
+ onTap: () {},
+ child: Container(
+ color: Colors.transparent,
+ ),
+ ),
+ );
+ },
+ stream: rebuildStream,
+ ),
+ ];
+ },
+ colorPicker:
+ (textEditor, rebuildStream, currentColor, setColor) => null,
+ bottomBar: (editorState, rebuildStream) {
+ return ReactiveCustomWidget(
+ builder: (context) {
+ return ImageEditorTextBar(
+ configs: editorState.configs,
+ callbacks: editorState.callbacks,
+ editor: editorState,
+ );
+ },
+ stream: rebuildStream,
+ );
+ },
+ ),
+ cropRotateEditor: CustomWidgetsCropRotateEditor(
+ appBar: (editor, rebuildStream) {
+ return ReactiveCustomAppbar(
+ builder: (context) {
+ return ImageEditorAppBar(
+ key: const Key('image_editor_app_bar'),
+ configs: editor.configs,
+ done: () => editor.done(),
+ close: () => editor.close(),
+ enableRedo: editor.canRedo,
+ enableUndo: editor.canUndo,
+ redo: () => editor.redoAction(),
+ undo: () => editor.undoAction(),
+ );
+ },
+ stream: rebuildStream,
+ );
+ },
+ bottomBar: (cropRotateEditor, rebuildStream) =>
+ ReactiveCustomWidget(
+ stream: rebuildStream,
+ builder: (_) => ImageEditorCropRotateBar(
+ configs: cropRotateEditor.configs,
+ callbacks: cropRotateEditor.callbacks,
+ editor: cropRotateEditor,
+ ),
+ ),
+ ),
+ ),
+ mainEditorConfigs: const MainEditorConfigs(enableZoom: true),
+ paintEditorConfigs: const PaintEditorConfigs(enabled: true),
+ textEditorConfigs: const TextEditorConfigs(
+ enabled: false,
+ canToggleBackgroundMode: true,
+ canToggleTextAlign: true,
+ ),
+ cropRotateEditorConfigs: const CropRotateEditorConfigs(
+ canChangeAspectRatio: true,
+ canFlip: true,
+ canRotate: true,
+ canReset: true,
+ enabled: true,
+ ),
+ filterEditorConfigs: FilterEditorConfigs(
+ enabled: true,
+ fadeInUpDuration: fadeInDuration,
+ fadeInUpStaggerDelayDuration: fadeInDelay,
+ filterList: filterList,
+ ),
+ tuneEditorConfigs: const TuneEditorConfigs(enabled: true),
+ blurEditorConfigs: const BlurEditorConfigs(
+ enabled: false,
+ ),
+ emojiEditorConfigs: const EmojiEditorConfigs(
+ enabled: true,
+ checkPlatformCompatibility: true,
+ ),
+ stickerEditorConfigs: StickerEditorConfigs(
+ enabled: false,
+ buildStickers: (setLayer, scrollController) {
+ return const SizedBox.shrink();
+ },
+ ),
+ ),
+ ),
+ ),
+ );
+ }
+}
diff --git a/mobile/apps/photos/lib/ui/tools/editor/image_editor/image_editor_paint_bar.dart b/mobile/apps/photos/lib/ui/tools/editor/image_editor/image_editor_paint_bar.dart
new file mode 100644
index 0000000000..0e4d1dd44f
--- /dev/null
+++ b/mobile/apps/photos/lib/ui/tools/editor/image_editor/image_editor_paint_bar.dart
@@ -0,0 +1,88 @@
+import 'package:flutter/material.dart';
+import "package:photos/theme/ente_theme.dart";
+import "package:photos/ui/tools/editor/image_editor/image_editor_color_picker.dart";
+import "package:photos/ui/tools/editor/image_editor/image_editor_configs_mixin.dart";
+import "package:photos/ui/tools/editor/image_editor/image_editor_constants.dart";
+import "package:pro_image_editor/mixins/converted_configs.dart";
+import "package:pro_image_editor/models/editor_callbacks/pro_image_editor_callbacks.dart";
+import "package:pro_image_editor/models/editor_configs/pro_image_editor_configs.dart";
+import "package:pro_image_editor/modules/paint_editor/paint_editor.dart";
+import "package:pro_image_editor/widgets/animated/fade_in_up.dart";
+
+class ImageEditorPaintBar extends StatefulWidget with SimpleConfigsAccess {
+ const ImageEditorPaintBar({
+ super.key,
+ required this.configs,
+ required this.callbacks,
+ required this.editor,
+ required this.i18nColor,
+ });
+
+ final PaintingEditorState editor;
+
+ @override
+ final ProImageEditorConfigs configs;
+ @override
+ final ProImageEditorCallbacks callbacks;
+
+ final String i18nColor;
+
+ @override
+ State createState() => _ImageEditorPaintBarState();
+}
+
+class _ImageEditorPaintBarState extends State
+ with ImageEditorConvertedConfigs, SimpleConfigsAccessState {
+ double colorSliderValue = 0.5;
+ @override
+ Widget build(BuildContext context) {
+ return LayoutBuilder(
+ builder: (context, constraints) {
+ return Column(
+ mainAxisSize: MainAxisSize.min,
+ children: [
+ _buildFunctions(constraints),
+ ],
+ );
+ },
+ );
+ }
+
+ Widget _buildFunctions(BoxConstraints constraints) {
+ return BottomAppBar(
+ height: editorBottomBarHeight,
+ padding: EdgeInsets.zero,
+ child: Align(
+ alignment: Alignment.center,
+ child: FadeInUp(
+ duration: fadeInDuration,
+ child: Column(
+ mainAxisAlignment: MainAxisAlignment.center,
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ Padding(
+ padding: const EdgeInsets.only(left: 20.0),
+ child: Text(
+ "Brush Color",
+ style: getEnteTextTheme(context).body,
+ ),
+ ),
+ const SizedBox(height: 24),
+ ImageEditorColorPicker(
+ value: colorSliderValue,
+ onChanged: (value) {
+ setState(() {
+ colorSliderValue = value;
+ });
+ final hue = value * 360;
+ final color = HSVColor.fromAHSV(1.0, hue, 1.0, 1.0).toColor();
+ widget.editor.colorChanged(color);
+ },
+ ),
+ ],
+ ),
+ ),
+ ),
+ );
+ }
+}
diff --git a/mobile/apps/photos/lib/ui/tools/editor/image_editor/image_editor_text_bar.dart b/mobile/apps/photos/lib/ui/tools/editor/image_editor/image_editor_text_bar.dart
new file mode 100644
index 0000000000..3115aa3933
--- /dev/null
+++ b/mobile/apps/photos/lib/ui/tools/editor/image_editor/image_editor_text_bar.dart
@@ -0,0 +1,380 @@
+import 'package:flutter/material.dart';
+import "package:flutter_svg/svg.dart";
+import "package:photos/theme/ente_theme.dart";
+import "package:photos/ui/tools/editor/image_editor/circular_icon_button.dart";
+import "package:photos/ui/tools/editor/image_editor/image_editor_color_picker.dart";
+import "package:photos/ui/tools/editor/image_editor/image_editor_configs_mixin.dart";
+import "package:photos/ui/tools/editor/image_editor/image_editor_constants.dart";
+import "package:pro_image_editor/mixins/converted_configs.dart";
+import 'package:pro_image_editor/pro_image_editor.dart';
+
+class ImageEditorTextBar extends StatefulWidget with SimpleConfigsAccess {
+ const ImageEditorTextBar({
+ super.key,
+ required this.configs,
+ required this.callbacks,
+ required this.editor,
+ });
+
+ final TextEditorState editor;
+
+ @override
+ final ProImageEditorConfigs configs;
+ @override
+ final ProImageEditorCallbacks callbacks;
+
+ @override
+ State createState() => _ImageEditorTextBarState();
+}
+
+class _ImageEditorTextBarState extends State
+ with ImageEditorConvertedConfigs, SimpleConfigsAccessState {
+ int selectedActionIndex = -1;
+ double colorSliderValue = 0.5;
+
+ void _selectAction(int index) {
+ setState(() {
+ selectedActionIndex = selectedActionIndex == index ? -1 : index;
+ });
+ }
+
+ @override
+ Widget build(BuildContext context) {
+ return LayoutBuilder(
+ builder: (context, constraints) {
+ return Column(
+ mainAxisSize: MainAxisSize.min,
+ children: [
+ _buildFunctions(constraints),
+ ],
+ );
+ },
+ );
+ }
+
+ Widget _buildFunctions(BoxConstraints constraints) {
+ return BottomAppBar(
+ padding: EdgeInsets.zero,
+ height: editorBottomBarHeight,
+ child: FadeInUp(
+ duration: fadeInDuration,
+ child: Column(
+ mainAxisAlignment: MainAxisAlignment.spaceEvenly,
+ children: [
+ _buildMainActionButtons(),
+ _buildHelperWidget(),
+ ],
+ ),
+ ),
+ );
+ }
+
+ Widget _buildMainActionButtons() {
+ return Row(
+ mainAxisAlignment: MainAxisAlignment.spaceEvenly,
+ children: [
+ CircularIconButton(
+ svgPath: "assets/image-editor/image-editor-text-color.svg",
+ label: "Color",
+ isSelected: selectedActionIndex == 0,
+ onTap: () {
+ _selectAction(0);
+ },
+ ),
+ CircularIconButton(
+ svgPath: "assets/image-editor/image-editor-text-font.svg",
+ label: "Font",
+ isSelected: selectedActionIndex == 1,
+ onTap: () {
+ _selectAction(1);
+ },
+ ),
+ CircularIconButton(
+ svgPath: "assets/image-editor/image-editor-text-background.svg",
+ label: "Background",
+ isSelected: selectedActionIndex == 2,
+ onTap: () {
+ setState(() {
+ selectedActionIndex = 2;
+ });
+ },
+ ),
+ CircularIconButton(
+ svgPath: "assets/image-editor/image-editor-text-align-left.svg",
+ label: "Align",
+ isSelected: selectedActionIndex == 3,
+ onTap: () {
+ setState(() {
+ selectedActionIndex = 3;
+ });
+ },
+ ),
+ ],
+ );
+ }
+
+ Widget _buildHelperWidget() {
+ return AnimatedSwitcher(
+ duration: const Duration(milliseconds: 300),
+ switchInCurve: Curves.easeInOut,
+ switchOutCurve: Curves.easeInOut,
+ child: switch (selectedActionIndex) {
+ 0 => ImageEditorColorPicker(
+ value: colorSliderValue,
+ onChanged: (value) {
+ setState(() {
+ colorSliderValue = value;
+ });
+ final hue = value * 360;
+ final color = HSVColor.fromAHSV(1.0, hue, 1.0, 1.0).toColor();
+ widget.editor.primaryColor = color;
+ },
+ ),
+ 1 => _FontPickerWidget(editor: widget.editor),
+ 2 => _BackgroundPickerWidget(editor: widget.editor),
+ 3 => _AlignPickerWidget(editor: widget.editor),
+ _ => const SizedBox.shrink(),
+ },
+ );
+ }
+}
+
+class _FontPickerWidget extends StatelessWidget {
+ final TextEditorState editor;
+
+ const _FontPickerWidget({required this.editor});
+
+ @override
+ Widget build(BuildContext context) {
+ final colorScheme = getEnteColorScheme(context);
+ if (editor.textEditorConfigs.customTextStyles == null) {
+ return const SizedBox.shrink();
+ }
+
+ return Row(
+ mainAxisAlignment: MainAxisAlignment.center,
+ children: editor.textEditorConfigs.customTextStyles!
+ .asMap()
+ .entries
+ .map((entry) {
+ final item = entry.value;
+ final selected = editor.selectedTextStyle;
+ final bool isSelected = selected.hashCode == item.hashCode;
+
+ return Padding(
+ padding: const EdgeInsets.only(right: 12),
+ child: GestureDetector(
+ onTap: () {
+ editor.setTextStyle(item);
+ },
+ child: Container(
+ height: 40,
+ width: 48,
+ decoration: BoxDecoration(
+ color: isSelected
+ ? colorScheme.fillBasePressed
+ : colorScheme.backgroundElevated2,
+ borderRadius: BorderRadius.circular(25),
+ ),
+ child: Center(
+ child: Text(
+ 'Aa',
+ style: item.copyWith(
+ color: isSelected
+ ? colorScheme.backdropBase
+ : colorScheme.tabIcon,
+ ),
+ ),
+ ),
+ ),
+ ),
+ );
+ }).toList(),
+ );
+ }
+}
+
+class _BackgroundPickerWidget extends StatelessWidget {
+ final TextEditorState editor;
+
+ const _BackgroundPickerWidget({required this.editor});
+
+ @override
+ Widget build(BuildContext context) {
+ final colorScheme = getEnteColorScheme(context);
+ final isLightMode = Theme.of(context).brightness == Brightness.light;
+ final backgroundStyles = {
+ LayerBackgroundMode.background: {
+ 'text': 'Aa',
+ 'selectedBackgroundColor':
+ isLightMode ? colorScheme.fillFaint : Colors.white,
+ 'backgroundColor': colorScheme.backgroundElevated2,
+ 'border': null,
+ 'textColor': Colors.white,
+ 'selectedInnerBackgroundColor': Colors.black,
+ 'innerBackgroundColor': Colors.transparent,
+ },
+ LayerBackgroundMode.backgroundAndColor: {
+ 'text': 'Aa',
+ 'selectedBackgroundColor':
+ isLightMode ? colorScheme.fillFaint : Colors.white,
+ 'backgroundColor': colorScheme.backgroundElevated2,
+ 'border': null,
+ 'textColor': Colors.black,
+ 'selectedInnerBackgroundColor': Colors.transparent,
+ 'innerBackgroundColor': Colors.transparent,
+ },
+ LayerBackgroundMode.backgroundAndColorWithOpacity: {
+ 'text': 'Aa',
+ 'selectedBackgroundColor':
+ isLightMode ? colorScheme.fillFaint : Colors.white,
+ 'backgroundColor': colorScheme.backgroundElevated2,
+ 'border': null,
+ 'textColor': Colors.black,
+ 'selectedInnerBackgroundColor': Colors.black.withOpacity(0.11),
+ 'innerBackgroundColor': isLightMode
+ ? Colors.black.withOpacity(0.11)
+ : Colors.white.withOpacity(0.11),
+ },
+ LayerBackgroundMode.onlyColor: {
+ 'text': 'Aa',
+ 'selectedBackgroundColor':
+ isLightMode ? colorScheme.fillFaint : Colors.black,
+ 'backgroundColor': colorScheme.backgroundElevated2,
+ 'border':
+ isLightMode ? null : Border.all(color: Colors.white, width: 2),
+ 'textColor': Colors.black,
+ 'selectedInnerBackgroundColor': Colors.white,
+ 'innerBackgroundColor': Colors.white.withOpacity(0.6),
+ },
+ };
+
+ return Row(
+ mainAxisAlignment: MainAxisAlignment.center,
+ children: backgroundStyles.entries.map((entry) {
+ final mode = entry.key;
+ final style = entry.value;
+ final isSelected = editor.backgroundColorMode == mode;
+
+ return Padding(
+ padding: const EdgeInsets.only(right: 12),
+ child: GestureDetector(
+ onTap: () {
+ editor.setState(() {
+ editor.backgroundColorMode = mode;
+ });
+ },
+ child: SizedBox(
+ height: 40,
+ width: 48,
+ child: Container(
+ decoration: BoxDecoration(
+ color: isSelected
+ ? style['selectedBackgroundColor'] as Color
+ : style['backgroundColor'] as Color,
+ borderRadius: BorderRadius.circular(25),
+ border: isSelected ? style['border'] as Border? : null,
+ ),
+ child: Stack(
+ alignment: Alignment.center,
+ children: [
+ Center(
+ child: Container(
+ height: 20,
+ width: 22,
+ decoration: ShapeDecoration(
+ color: isSelected
+ ? style['selectedInnerBackgroundColor'] as Color
+ : style['innerBackgroundColor'] as Color,
+ shape: RoundedRectangleBorder(
+ borderRadius: BorderRadius.circular(4),
+ ),
+ ),
+ ),
+ ),
+ Center(
+ child: Text(
+ style['text'] as String,
+ style: TextStyle(
+ color: isSelected
+ ? style['textColor'] as Color
+ : colorScheme.tabIcon,
+ ),
+ ),
+ ),
+ ],
+ ),
+ ),
+ ),
+ ),
+ );
+ }).toList(),
+ );
+ }
+}
+
+class _AlignPickerWidget extends StatelessWidget {
+ final TextEditorState editor;
+
+ const _AlignPickerWidget({required this.editor});
+
+ @override
+ Widget build(BuildContext context) {
+ final colorScheme = getEnteColorScheme(context);
+ final alignments = [
+ (TextAlign.left, "assets/image-editor/image-editor-text-align-left.svg"),
+ (
+ TextAlign.center,
+ "assets/image-editor/image-editor-text-align-center.svg"
+ ),
+ (
+ TextAlign.right,
+ "assets/image-editor/image-editor-text-align-right.svg"
+ ),
+ ];
+
+ return Row(
+ mainAxisAlignment: MainAxisAlignment.center,
+ children: alignments.map((alignmentData) {
+ final (alignment, svgPath) = alignmentData;
+ final isSelected = editor.align == alignment;
+
+ return Padding(
+ padding: const EdgeInsets.only(right: 12.0),
+ child: GestureDetector(
+ onTap: () {
+ editor.setState(() {
+ editor.align = alignment;
+ });
+ },
+ child: Container(
+ height: 40,
+ width: 48,
+ decoration: BoxDecoration(
+ color: isSelected
+ ? colorScheme.fillBasePressed
+ : colorScheme.backgroundElevated2,
+ borderRadius: BorderRadius.circular(25),
+ border: isSelected
+ ? Border.all(color: Colors.black, width: 2)
+ : null,
+ ),
+ child: Center(
+ child: SvgPicture.asset(
+ svgPath,
+ width: 22,
+ height: 22,
+ fit: BoxFit.scaleDown,
+ colorFilter: ColorFilter.mode(
+ isSelected ? colorScheme.backdropBase : colorScheme.tabIcon,
+ BlendMode.srcIn,
+ ),
+ ),
+ ),
+ ),
+ ),
+ );
+ }).toList(),
+ );
+ }
+}
diff --git a/mobile/apps/photos/lib/ui/tools/editor/image_editor/image_editor_tune_bar.dart b/mobile/apps/photos/lib/ui/tools/editor/image_editor/image_editor_tune_bar.dart
new file mode 100644
index 0000000000..a7b5c69da4
--- /dev/null
+++ b/mobile/apps/photos/lib/ui/tools/editor/image_editor/image_editor_tune_bar.dart
@@ -0,0 +1,651 @@
+import 'dart:math';
+
+import 'package:flutter/material.dart';
+import "package:flutter_svg/svg.dart";
+import "package:photos/ente_theme_data.dart";
+import "package:photos/theme/ente_theme.dart";
+import "package:photos/ui/tools/editor/image_editor/image_editor_configs_mixin.dart";
+import "package:photos/ui/tools/editor/image_editor/image_editor_constants.dart";
+import "package:pro_image_editor/mixins/converted_configs.dart";
+import "package:pro_image_editor/models/editor_callbacks/pro_image_editor_callbacks.dart";
+import "package:pro_image_editor/models/editor_configs/pro_image_editor_configs.dart";
+import "package:pro_image_editor/modules/tune_editor/tune_editor.dart";
+import "package:pro_image_editor/widgets/animated/fade_in_up.dart";
+
+class ImageEditorTuneBar extends StatefulWidget with SimpleConfigsAccess {
+ const ImageEditorTuneBar({
+ super.key,
+ required this.configs,
+ required this.callbacks,
+ required this.editor,
+ });
+
+ final TuneEditorState editor;
+
+ @override
+ final ProImageEditorConfigs configs;
+
+ @override
+ final ProImageEditorCallbacks callbacks;
+
+ @override
+ State createState() => _ImageEditorTuneBarState();
+}
+
+class _ImageEditorTuneBarState extends State
+ with ImageEditorConvertedConfigs, SimpleConfigsAccessState {
+ TuneEditorState get tuneEditor => widget.editor;
+
+ @override
+ Widget build(BuildContext context) {
+ return LayoutBuilder(
+ builder: (context, constraints) {
+ return Column(
+ mainAxisSize: MainAxisSize.min,
+ children: [
+ _buildFunctions(constraints),
+ ],
+ );
+ },
+ );
+ }
+
+ Widget _buildFunctions(BoxConstraints constraints) {
+ return SizedBox(
+ width: double.infinity,
+ height: editorBottomBarHeight,
+ child: FadeInUp(
+ duration: fadeInDuration,
+ child: Column(
+ mainAxisAlignment: MainAxisAlignment.spaceEvenly,
+ children: [
+ SizedBox(
+ height: 90,
+ child: SingleChildScrollView(
+ controller: tuneEditor.bottomBarScrollCtrl,
+ scrollDirection: Axis.horizontal,
+ child: Row(
+ mainAxisAlignment: MainAxisAlignment.spaceBetween,
+ mainAxisSize: MainAxisSize.min,
+ children: List.generate(
+ tuneEditor.tuneAdjustmentMatrix.length, (index) {
+ final item = tuneEditor.tuneAdjustmentList[index];
+ return TuneItem(
+ icon: item.icon,
+ label: item.label,
+ isSelected: tuneEditor.selectedIndex == index,
+ value: tuneEditor.tuneAdjustmentMatrix[index].value,
+ max: item.max,
+ min: item.min,
+ onTap: () {
+ tuneEditor.setState(() {
+ tuneEditor.selectedIndex = index;
+ });
+ },
+ );
+ }),
+ ),
+ ),
+ ),
+ RepaintBoundary(
+ child: StreamBuilder(
+ stream: tuneEditor.uiStream.stream,
+ builder: (context, snapshot) {
+ final activeOption =
+ tuneEditor.tuneAdjustmentList[tuneEditor.selectedIndex];
+ final activeMatrix =
+ tuneEditor.tuneAdjustmentMatrix[tuneEditor.selectedIndex];
+
+ return _TuneAdjustWidget(
+ min: activeOption.min,
+ max: activeOption.max,
+ value: activeMatrix.value,
+ onChanged: tuneEditor.onChanged,
+ );
+ },
+ ),
+ ),
+ ],
+ ),
+ ),
+ );
+ }
+}
+
+class TuneItem extends StatelessWidget {
+ final IconData icon;
+ final String label;
+ final bool isSelected;
+ final double value;
+ final double min;
+ final double max;
+ final VoidCallback onTap;
+
+ const TuneItem({
+ super.key,
+ required this.icon,
+ required this.label,
+ required this.isSelected,
+ required this.value,
+ required this.min,
+ required this.max,
+ required this.onTap,
+ });
+
+ @override
+ Widget build(BuildContext context) {
+ return GestureDetector(
+ onTap: onTap,
+ child: SizedBox(
+ width: 90,
+ child: Column(
+ mainAxisSize: MainAxisSize.min,
+ children: [
+ CircularProgressWithValue(
+ value: value,
+ min: min,
+ max: max,
+ size: 60,
+ icon: icon,
+ isSelected: isSelected,
+ progressColor:
+ Theme.of(context).colorScheme.imageEditorPrimaryColor,
+ svgPath:
+ "assets/image-editor/image-editor-${label.toLowerCase()}.svg",
+ ),
+ const SizedBox(height: 8),
+ Text(
+ label,
+ style: getEnteTextTheme(context).small,
+ textAlign: TextAlign.center,
+ ),
+ ],
+ ),
+ ),
+ );
+ }
+}
+
+class CircularProgressWithValue extends StatefulWidget {
+ final double value;
+ final double min;
+ final double max;
+ final IconData icon;
+ final bool isSelected;
+ final double size;
+ final Color progressColor;
+ final String? svgPath;
+
+ const CircularProgressWithValue({
+ super.key,
+ required this.value,
+ required this.min,
+ required this.max,
+ required this.icon,
+ required this.progressColor,
+ this.isSelected = false,
+ this.size = 60,
+ this.svgPath,
+ });
+
+ @override
+ State createState() =>
+ _CircularProgressWithValueState();
+}
+
+class _CircularProgressWithValueState extends State
+ with SingleTickerProviderStateMixin {
+ late AnimationController _animationController;
+ late Animation _progressAnimation;
+ double _previousValue = 0.0;
+
+ @override
+ void initState() {
+ super.initState();
+ _animationController = AnimationController(
+ vsync: this,
+ duration: const Duration(milliseconds: 300),
+ animationBehavior: AnimationBehavior.preserve,
+ );
+
+ _progressAnimation = Tween(
+ begin: 0.0,
+ end: widget.value,
+ ).animate(
+ CurvedAnimation(
+ parent: _animationController,
+ curve: Curves.easeInOut,
+ ),
+ );
+
+ _animationController.forward();
+ }
+
+ @override
+ void didUpdateWidget(CircularProgressWithValue oldWidget) {
+ super.didUpdateWidget(oldWidget);
+
+ if (oldWidget.value != widget.value) {
+ _previousValue = oldWidget.value;
+ _progressAnimation = Tween(
+ begin: _previousValue,
+ end: widget.value,
+ ).animate(
+ CurvedAnimation(
+ parent: _animationController,
+ curve: Curves.easeInOut,
+ ),
+ );
+ _animationController.forward(from: 0.0);
+ }
+ }
+
+ @override
+ void dispose() {
+ _animationController.dispose();
+ super.dispose();
+ }
+
+ int _normalizeValueForDisplay(double value, double min, double max) {
+ if (min == -0.5 && max == 0.5) {
+ return (value * 200).round();
+ } else if (min == 0 && max == 1) {
+ return (value * 100).round();
+ } else if (min == -0.25 && max == 0.25) {
+ return (value * 400).round();
+ } else {
+ return (value * 100).round();
+ }
+ }
+
+ double _normalizeValueForProgress(double value, double min, double max) {
+ if (min == -0.5 && max == 0.5) {
+ return (value.abs() / 0.5).clamp(0.0, 1.0);
+ } else if (min == 0 && max == 1) {
+ return (value / 1.0).clamp(0.0, 1.0);
+ } else if (min == -0.25 && max == 0.25) {
+ return (value.abs() / 0.25).clamp(0.0, 1.0);
+ } else {
+ return (value.abs() / 1.0).clamp(0.0, 1.0);
+ }
+ }
+
+ bool _isClockwise(double value, double min, double max) {
+ if (min >= 0) {
+ return true;
+ } else {
+ return value >= 0;
+ }
+ }
+
+ @override
+ Widget build(BuildContext context) {
+ final colorTheme = getEnteColorScheme(context);
+ final textTheme = getEnteTextTheme(context);
+ final displayValue =
+ _normalizeValueForDisplay(widget.value, widget.min, widget.max);
+ final displayText = displayValue.toString();
+ final prefix = displayValue > 0 ? "+" : "";
+ final progressColor = widget.progressColor;
+
+ final showValue = displayValue != 0 || widget.isSelected;
+
+ return SizedBox(
+ width: widget.size,
+ height: widget.size,
+ child: Stack(
+ alignment: Alignment.center,
+ children: [
+ Container(
+ width: widget.size,
+ height: widget.size,
+ decoration: BoxDecoration(
+ shape: BoxShape.circle,
+ color: showValue || widget.isSelected
+ ? progressColor.withOpacity(0.2)
+ : colorTheme.backgroundElevated2,
+ border: Border.all(
+ color: widget.isSelected
+ ? progressColor.withOpacity(0.4)
+ : colorTheme.backgroundElevated2,
+ width: 2,
+ ),
+ ),
+ ),
+ AnimatedBuilder(
+ animation: _progressAnimation,
+ builder: (context, child) {
+ final animatedValue =
+ displayValue == 0 ? 0.0 : _progressAnimation.value;
+
+ final isClockwise =
+ _isClockwise(animatedValue, widget.min, widget.max);
+ final progressValue = _normalizeValueForProgress(
+ animatedValue,
+ widget.min,
+ widget.max,
+ );
+
+ return SizedBox(
+ width: widget.size,
+ height: widget.size,
+ child: CustomPaint(
+ painter: CircularProgressPainter(
+ progress: progressValue,
+ isClockwise: isClockwise,
+ color: progressColor,
+ ),
+ ),
+ );
+ },
+ ),
+ Align(
+ alignment: Alignment.center,
+ child: showValue
+ ? Text(
+ "$prefix$displayText",
+ style: textTheme.smallBold,
+ )
+ : widget.svgPath != null
+ ? SvgPicture.asset(
+ widget.svgPath!,
+ width: 22,
+ height: 22,
+ fit: BoxFit.scaleDown,
+ colorFilter: ColorFilter.mode(
+ colorTheme.tabIcon,
+ BlendMode.srcIn,
+ ),
+ )
+ : Icon(
+ widget.icon,
+ color: colorTheme.tabIcon,
+ size: 20,
+ ),
+ ),
+ ],
+ ),
+ );
+ }
+}
+
+class _TuneAdjustWidget extends StatelessWidget {
+ final double min;
+ final double max;
+ final double value;
+ final ValueChanged onChanged;
+
+ const _TuneAdjustWidget({
+ required this.min,
+ required this.max,
+ required this.value,
+ required this.onChanged,
+ });
+
+ @override
+ Widget build(BuildContext context) {
+ final colorScheme = getEnteColorScheme(context);
+ return SizedBox(
+ height: 40,
+ child: Stack(
+ alignment: Alignment.center,
+ children: [
+ Positioned.fill(
+ child: Container(
+ margin: const EdgeInsets.symmetric(horizontal: 20),
+ decoration: BoxDecoration(
+ borderRadius: BorderRadius.circular(25),
+ color: colorScheme.backgroundElevated2,
+ ),
+ ),
+ ),
+ Container(
+ margin: const EdgeInsets.symmetric(horizontal: 20),
+ decoration: BoxDecoration(
+ borderRadius: BorderRadius.circular(25),
+ ),
+ child: SliderTheme(
+ data: SliderTheme.of(context).copyWith(
+ thumbShape: const _ColorPickerThumbShape(),
+ overlayShape: const RoundSliderOverlayShape(overlayRadius: 0),
+ activeTrackColor:
+ Theme.of(context).colorScheme.imageEditorPrimaryColor,
+ inactiveTrackColor: colorScheme.backgroundElevated2,
+ trackShape: const _CenterBasedTrackShape(),
+ trackHeight: 24,
+ ),
+ child: Slider(
+ value: value,
+ onChanged: onChanged,
+ min: min,
+ max: max,
+ ),
+ ),
+ ),
+ Container(
+ margin: const EdgeInsets.symmetric(horizontal: 38),
+ child: Row(
+ mainAxisAlignment: MainAxisAlignment.spaceBetween,
+ children: [
+ Container(
+ width: 6,
+ height: 6,
+ decoration: BoxDecoration(
+ shape: BoxShape.circle,
+ color: colorScheme.fillBase.withAlpha(30),
+ ),
+ ),
+ Container(
+ width: 6,
+ height: 6,
+ decoration: BoxDecoration(
+ shape: BoxShape.circle,
+ color: colorScheme.fillBase.withAlpha(30),
+ ),
+ ),
+ Container(
+ width: 6,
+ height: 6,
+ decoration: BoxDecoration(
+ shape: BoxShape.circle,
+ color: colorScheme.fillBase.withAlpha(30),
+ ),
+ ),
+ ],
+ ),
+ ),
+ ],
+ ),
+ );
+ }
+}
+
+class _ColorPickerThumbShape extends SliderComponentShape {
+ const _ColorPickerThumbShape();
+
+ @override
+ Size getPreferredSize(bool isEnabled, bool isDiscrete) {
+ return const Size(20, 20);
+ }
+
+ @override
+ void paint(
+ PaintingContext context,
+ Offset center, {
+ required Animation activationAnimation,
+ required Animation enableAnimation,
+ required bool isDiscrete,
+ required TextPainter labelPainter,
+ required RenderBox parentBox,
+ required Size sizeWithOverflow,
+ required SliderThemeData sliderTheme,
+ required TextDirection textDirection,
+ required double textScaleFactor,
+ required double value,
+ }) {
+ final canvas = context.canvas;
+
+ final trackRect = sliderTheme.trackShape!.getPreferredRect(
+ parentBox: parentBox,
+ offset: Offset.zero,
+ sliderTheme: sliderTheme,
+ isEnabled: true,
+ isDiscrete: isDiscrete,
+ );
+
+ final constrainedCenter = Offset(
+ center.dx.clamp(trackRect.left + 15, trackRect.right - 15),
+ center.dy,
+ );
+
+ final paint = Paint()
+ ..color = Colors.white
+ ..style = PaintingStyle.fill;
+
+ canvas.drawCircle(constrainedCenter, 15, paint);
+
+ final innerPaint = Paint()
+ ..color = const Color.fromRGBO(8, 194, 37, 1)
+ ..style = PaintingStyle.fill;
+ canvas.drawCircle(constrainedCenter, 12.5, innerPaint);
+ }
+}
+
+class _CenterBasedTrackShape extends SliderTrackShape {
+ const _CenterBasedTrackShape();
+
+ static const double horizontalPadding = 6.0;
+
+ @override
+ Rect getPreferredRect({
+ required RenderBox parentBox,
+ Offset offset = Offset.zero,
+ required SliderThemeData sliderTheme,
+ bool isEnabled = false,
+ bool isDiscrete = false,
+ }) {
+ final double trackHeight = sliderTheme.trackHeight ?? 8;
+ final double trackLeft = offset.dx + horizontalPadding;
+ final double trackTop =
+ offset.dy + (parentBox.size.height - trackHeight) / 2;
+ final double trackWidth = parentBox.size.width - (horizontalPadding * 2);
+ return Rect.fromLTWH(trackLeft, trackTop, trackWidth, trackHeight);
+ }
+
+ @override
+ void paint(
+ PaintingContext context,
+ Offset offset, {
+ required RenderBox parentBox,
+ required SliderThemeData sliderTheme,
+ required Animation enableAnimation,
+ required TextDirection textDirection,
+ required Offset thumbCenter,
+ Offset? secondaryOffset,
+ bool isEnabled = false,
+ bool isDiscrete = false,
+ double? additionalActiveTrackHeight,
+ }) {
+ final Canvas canvas = context.canvas;
+ final Rect trackRect = getPreferredRect(
+ parentBox: parentBox,
+ offset: offset,
+ sliderTheme: sliderTheme,
+ isEnabled: isEnabled,
+ isDiscrete: isDiscrete,
+ );
+
+ final double centerX = trackRect.left + trackRect.width / 2;
+
+ final double clampedThumbDx = thumbCenter.dx.clamp(
+ trackRect.left,
+ trackRect.right,
+ );
+
+ final Paint inactivePaint = Paint()
+ ..color = sliderTheme.inactiveTrackColor!
+ ..style = PaintingStyle.fill;
+
+ final RRect inactiveRRect = RRect.fromRectAndRadius(
+ trackRect,
+ Radius.circular(trackRect.height / 2),
+ );
+
+ canvas.drawRRect(inactiveRRect, inactivePaint);
+
+ if (clampedThumbDx != centerX) {
+ final Paint activePaint = Paint()
+ ..color = sliderTheme.activeTrackColor!
+ ..style = PaintingStyle.fill;
+
+ final Rect activeRect = clampedThumbDx >= centerX
+ ? Rect.fromLTWH(
+ centerX,
+ trackRect.top,
+ clampedThumbDx - centerX,
+ trackRect.height,
+ )
+ : Rect.fromLTWH(
+ clampedThumbDx,
+ trackRect.top,
+ centerX - clampedThumbDx,
+ trackRect.height,
+ );
+
+ final RRect activeRRect = RRect.fromRectAndRadius(
+ activeRect,
+ Radius.circular(trackRect.height / 2),
+ );
+
+ canvas.drawRRect(activeRRect, activePaint);
+ }
+ }
+}
+
+class CircularProgressPainter extends CustomPainter {
+ final double progress;
+ final bool isClockwise;
+ final Color color;
+
+ CircularProgressPainter({
+ required this.progress,
+ required this.isClockwise,
+ required this.color,
+ });
+
+ @override
+ void paint(Canvas canvas, Size size) {
+ const strokeWidth = 2.5;
+ final center = Offset(size.width / 2, size.height / 2);
+ final radius = (size.width - strokeWidth) / 2;
+
+ final backgroundPaint = Paint()
+ ..color = Colors.transparent
+ ..strokeWidth = strokeWidth
+ ..style = PaintingStyle.stroke;
+
+ final foregroundPaint = Paint()
+ ..color = color
+ ..strokeWidth = strokeWidth
+ ..style = PaintingStyle.stroke
+ ..strokeCap = StrokeCap.round;
+
+ canvas.drawCircle(center, radius, backgroundPaint);
+
+ if (progress > 0) {
+ const startAngle = -pi / 2;
+ final sweepAngle = 2 * pi * progress * (isClockwise ? 1 : -1);
+
+ canvas.drawArc(
+ Rect.fromCircle(center: center, radius: radius),
+ startAngle,
+ sweepAngle,
+ false,
+ foregroundPaint,
+ );
+ }
+ }
+
+ @override
+ bool shouldRepaint(covariant CustomPainter oldDelegate) => true;
+}
diff --git a/mobile/apps/photos/lib/ui/viewer/file/detail_page.dart b/mobile/apps/photos/lib/ui/viewer/file/detail_page.dart
index 2df8e6520b..5cb0d8fcbe 100644
--- a/mobile/apps/photos/lib/ui/viewer/file/detail_page.dart
+++ b/mobile/apps/photos/lib/ui/viewer/file/detail_page.dart
@@ -19,6 +19,7 @@ import "package:photos/services/local_authentication_service.dart";
import "package:photos/states/detail_page_state.dart";
import "package:photos/ui/common/fast_scroll_physics.dart";
import 'package:photos/ui/notification/toast.dart';
+import "package:photos/ui/tools/editor/image_editor/image_editor_page_new.dart";
import 'package:photos/ui/tools/editor/image_editor_page.dart';
import "package:photos/ui/tools/editor/video_editor_page.dart";
import "package:photos/ui/viewer/file/file_app_bar.dart";
@@ -175,7 +176,7 @@ class _DetailPageState extends State {
builder: (BuildContext context, int selectedIndex, _) {
return FileBottomBar(
_files![selectedIndex],
- _onEditFileRequested,
+ _onNewImageEditor,
widget.config.mode == DetailPageMode.minimalistic &&
!isGuestView,
onFileRemoved: _onFileRemoved,
@@ -357,6 +358,68 @@ class _DetailPageState extends State {
}
}
+ Future _onNewImageEditor(EnteFile file) async {
+ if (file.uploadedFileID != null &&
+ file.ownerID != Configuration.instance.getUserID()) {
+ _logger.severe(
+ "Attempt to edit unowned file",
+ UnauthorizedEditError(),
+ StackTrace.current,
+ );
+ // ignore: unawaited_futures
+ showErrorDialog(
+ context,
+ S.of(context).sorry,
+ S.of(context).weDontSupportEditingPhotosAndAlbumsThatYouDont,
+ );
+ return;
+ }
+ final dialog = createProgressDialog(context, S.of(context).pleaseWait);
+ await dialog.show();
+
+ try {
+ final ioFile = await getFile(file);
+ if (ioFile == null) {
+ showShortToast(context, S.of(context).failedToFetchOriginalForEdit);
+ await dialog.hide();
+ return;
+ }
+ if (file.fileType == FileType.video) {
+ await dialog.hide();
+ replacePage(
+ context,
+ VideoEditorPage(
+ file: file,
+ ioFile: ioFile,
+ detailPageConfig: widget.config.copyWith(
+ files: _files,
+ selectedIndex: _selectedIndexNotifier.value,
+ ),
+ ),
+ );
+ return;
+ }
+ final imageProvider =
+ ExtendedFileImageProvider(ioFile, cacheRawData: true);
+ await precacheImage(imageProvider, context);
+ await dialog.hide();
+ replacePage(
+ context,
+ NewImageEditor(
+ originalFile: file,
+ file: ioFile,
+ detailPageConfig: widget.config.copyWith(
+ files: _files,
+ selectedIndex: _selectedIndexNotifier.value,
+ ),
+ ),
+ );
+ } catch (e) {
+ await dialog.hide();
+ _logger.warning("Failed to initiate edit", e);
+ }
+ }
+
Future _onEditFileRequested(EnteFile file) async {
if (file.uploadedFileID != null &&
file.ownerID != Configuration.instance.getUserID()) {
diff --git a/mobile/apps/photos/pubspec.lock b/mobile/apps/photos/pubspec.lock
index f8cf5809fe..b5f0505424 100644
--- a/mobile/apps/photos/pubspec.lock
+++ b/mobile/apps/photos/pubspec.lock
@@ -5,10 +5,10 @@ packages:
dependency: transitive
description:
name: _fe_analyzer_shared
- sha256: "16e298750b6d0af7ce8a3ba7c18c69c3785d11b15ec83f6dcd0ad2a0009b3cab"
+ sha256: f256b0c0ba6c7577c15e2e4e114755640a875e885099367bf6e012b19314c834
url: "https://pub.dev"
source: hosted
- version: "76.0.0"
+ version: "72.0.0"
_flutterfire_internals:
dependency: transitive
description:
@@ -21,7 +21,7 @@ packages:
dependency: transitive
description: dart
source: sdk
- version: "0.3.3"
+ version: "0.3.2"
adaptive_theme:
dependency: "direct main"
description:
@@ -34,10 +34,10 @@ packages:
dependency: transitive
description:
name: analyzer
- sha256: "1f14db053a8c23e260789e9b0980fa27f2680dd640932cae5e1137cce0e46e1e"
+ sha256: b652861553cd3990d8ed361f7979dc6d7053a9ac8843fa73820ab68ce5410139
url: "https://pub.dev"
source: hosted
- version: "6.11.0"
+ version: "6.7.0"
android_intent_plus:
dependency: "direct main"
description:
@@ -317,10 +317,10 @@ packages:
dependency: "direct main"
description:
name: collection
- sha256: a1ace0a119f20aabc852d165077c036cd864315bd99b7eaa10a60100341941bf
+ sha256: ee67cb0715911d28db6bf4af1026078bd6f0128b07a5f66fb2ed94ec6783c09a
url: "https://pub.dev"
source: hosted
- version: "1.19.0"
+ version: "1.18.0"
computer:
dependency: "direct main"
description:
@@ -514,6 +514,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "2.1.17"
+ emoji_picker_flutter:
+ dependency: transitive
+ description:
+ name: emoji_picker_flutter
+ sha256: "08567e6f914d36c32091a96cf2f51d2558c47aa2bd47a590dc4f50e42e0965f6"
+ url: "https://pub.dev"
+ source: hosted
+ version: "3.1.0"
encrypt:
dependency: "direct main"
description:
@@ -1416,18 +1424,18 @@ packages:
dependency: transitive
description:
name: leak_tracker
- sha256: "7bb2830ebd849694d1ec25bf1f44582d6ac531a57a365a803a6034ff751d2d06"
+ sha256: "3f87a60e8c63aecc975dda1ceedbc8f24de75f09e4856ea27daf8958f2f0ce05"
url: "https://pub.dev"
source: hosted
- version: "10.0.7"
+ version: "10.0.5"
leak_tracker_flutter_testing:
dependency: transitive
description:
name: leak_tracker_flutter_testing
- sha256: "9491a714cca3667b60b5c420da8217e6de0d1ba7a5ec322fab01758f6998f379"
+ sha256: "932549fb305594d82d7183ecd9fa93463e9914e1b67cacc34bc40906594a1806"
url: "https://pub.dev"
source: hosted
- version: "3.0.8"
+ version: "3.0.5"
leak_tracker_testing:
dependency: transitive
description:
@@ -1536,10 +1544,10 @@ packages:
dependency: transitive
description:
name: macros
- sha256: "1d9e801cd66f7ea3663c45fc708450db1fa57f988142c64289142c9b7ee80656"
+ sha256: "0acaed5d6b7eab89f63350bccd82119e6c602df0f391260d0e32b5e23db79536"
url: "https://pub.dev"
source: hosted
- version: "0.1.3-main.0"
+ version: "0.1.2-main.4"
maps_launcher:
dependency: "direct main"
description:
@@ -2064,6 +2072,14 @@ packages:
url: "https://github.com/eddyuan/privacy_screen.git"
source: git
version: "0.0.6"
+ pro_image_editor:
+ dependency: "direct main"
+ description:
+ name: pro_image_editor
+ sha256: ee86d144ec76957578fb3dc7dee3d5e9cd03383cb153eb58f531be16ac528c63
+ url: "https://pub.dev"
+ source: hosted
+ version: "6.0.0"
process:
dependency: transitive
description:
@@ -2309,7 +2325,7 @@ packages:
dependency: transitive
description: flutter
source: sdk
- version: "0.0.0"
+ version: "0.0.99"
source_gen:
dependency: transitive
description:
@@ -2434,10 +2450,10 @@ packages:
dependency: transitive
description:
name: stack_trace
- sha256: "9f47fd3630d76be3ab26f0ee06d213679aa425996925ff3feffdec504931c377"
+ sha256: "73713990125a6d93122541237550ee3352a2d84baad52d375a4cad2eb9b7ce0b"
url: "https://pub.dev"
source: hosted
- version: "1.12.0"
+ version: "1.11.1"
step_progress_indicator:
dependency: "direct main"
description:
@@ -2466,10 +2482,10 @@ packages:
dependency: transitive
description:
name: string_scanner
- sha256: "688af5ed3402a4bde5b3a6c15fd768dbf2621a614950b17f04626c431ab3c4c3"
+ sha256: "556692adab6cfa87322a115640c11f13cb77b3f076ddcc5d6ae3c20242bedcde"
url: "https://pub.dev"
source: hosted
- version: "1.3.0"
+ version: "1.2.0"
styled_text:
dependency: "direct main"
description:
@@ -2530,26 +2546,26 @@ packages:
dependency: "direct dev"
description:
name: test
- sha256: "713a8789d62f3233c46b4a90b174737b2c04cb6ae4500f2aa8b1be8f03f5e67f"
+ sha256: "7ee44229615f8f642b68120165ae4c2a75fe77ae2065b1e55ae4711f6cf0899e"
url: "https://pub.dev"
source: hosted
- version: "1.25.8"
+ version: "1.25.7"
test_api:
dependency: transitive
description:
name: test_api
- sha256: "664d3a9a64782fcdeb83ce9c6b39e78fd2971d4e37827b9b06c3aa1edc5e760c"
+ sha256: "5b8a98dafc4d5c4c9c72d8b31ab2b23fc13422348d2997120294d3bac86b4ddb"
url: "https://pub.dev"
source: hosted
- version: "0.7.3"
+ version: "0.7.2"
test_core:
dependency: transitive
description:
name: test_core
- sha256: "12391302411737c176b0b5d6491f466b0dd56d4763e347b6714efbaa74d7953d"
+ sha256: "55ea5a652e38a1dfb32943a7973f3681a60f872f8c3a05a14664ad54ef9c6696"
url: "https://pub.dev"
source: hosted
- version: "0.6.5"
+ version: "0.6.4"
thermal:
dependency: "direct main"
description:
@@ -2742,6 +2758,22 @@ packages:
url: "https://pub.dev"
source: hosted
version: "2.1.4"
+ vibration:
+ dependency: transitive
+ description:
+ name: vibration
+ sha256: "3b08a0579c2f9c18d5d78cb5c74f1005f731e02eeca6d72561a2e8059bf98ec3"
+ url: "https://pub.dev"
+ source: hosted
+ version: "2.1.0"
+ vibration_platform_interface:
+ dependency: transitive
+ description:
+ name: vibration_platform_interface
+ sha256: "6ffeee63547562a6fef53c05a41d4fdcae2c0595b83ef59a4813b0612cd2bc36"
+ url: "https://pub.dev"
+ source: hosted
+ version: "0.0.3"
video_editor:
dependency: "direct main"
description:
@@ -2813,10 +2845,10 @@ packages:
dependency: transitive
description:
name: vm_service
- sha256: f6be3ed8bd01289b34d679c2b62226f63c0e69f9fd2e50a6b3c1c729a961041b
+ sha256: "5c5f338a667b4c644744b661f309fb8080bb94b18a7e91ef1dbd343bed00ed6d"
url: "https://pub.dev"
source: hosted
- version: "14.3.0"
+ version: "14.2.5"
volume_controller:
dependency: transitive
description:
@@ -2877,10 +2909,10 @@ packages:
dependency: transitive
description:
name: webdriver
- sha256: "3d773670966f02a646319410766d3b5e1037efb7f07cc68f844d5e06cd4d61c8"
+ sha256: "003d7da9519e1e5f329422b36c4dcdf18d7d2978d1ba099ea4e45ba490ed845e"
url: "https://pub.dev"
source: hosted
- version: "3.0.4"
+ version: "3.0.3"
webkit_inspection_protocol:
dependency: transitive
description:
diff --git a/mobile/apps/photos/pubspec.yaml b/mobile/apps/photos/pubspec.yaml
index c57dedf75c..6bc57ac4d9 100644
--- a/mobile/apps/photos/pubspec.yaml
+++ b/mobile/apps/photos/pubspec.yaml
@@ -173,6 +173,7 @@ dependencies:
git:
url: https://github.com/eddyuan/privacy_screen.git
ref: 855418e
+ pro_image_editor: 6.0.0
receive_sharing_intent: # pub.dev is behind
git:
url: https://github.com/KasemJaffer/receive_sharing_intent.git
@@ -342,6 +343,7 @@ flutter:
assets:
- assets/
- assets/video-editor/
+ - assets/image-editor/
- assets/icons/
- assets/launcher_icon/
fonts: