diff --git a/auth/assets/simple-icons b/auth/assets/simple-icons
index 8a3731352a..8e7701d6a4 160000
--- a/auth/assets/simple-icons
+++ b/auth/assets/simple-icons
@@ -1 +1 @@
-Subproject commit 8a3731352af133a02223a6c7b1f37c4abb096af0
+Subproject commit 8e7701d6a40462733043f54b3849faf35af70a83
diff --git a/mobile/assets/video-editor/video-crop-free-action.svg b/mobile/assets/video-editor/video-crop-free-action.svg
new file mode 100644
index 0000000000..059c02153e
--- /dev/null
+++ b/mobile/assets/video-editor/video-crop-free-action.svg
@@ -0,0 +1,13 @@
+
diff --git a/mobile/assets/video-editor/video-crop-original-action.svg b/mobile/assets/video-editor/video-crop-original-action.svg
new file mode 100644
index 0000000000..9d754363f7
--- /dev/null
+++ b/mobile/assets/video-editor/video-crop-original-action.svg
@@ -0,0 +1,10 @@
+
diff --git a/mobile/assets/video-editor/video-crop-ratio_16_9-action.svg b/mobile/assets/video-editor/video-crop-ratio_16_9-action.svg
new file mode 100644
index 0000000000..82f8ca975f
--- /dev/null
+++ b/mobile/assets/video-editor/video-crop-ratio_16_9-action.svg
@@ -0,0 +1,7 @@
+
diff --git a/mobile/assets/video-editor/video-crop-ratio_1_1-action.svg b/mobile/assets/video-editor/video-crop-ratio_1_1-action.svg
new file mode 100644
index 0000000000..5785c5e547
--- /dev/null
+++ b/mobile/assets/video-editor/video-crop-ratio_1_1-action.svg
@@ -0,0 +1,14 @@
+
diff --git a/mobile/assets/video-editor/video-crop-ratio_3_4-action.svg b/mobile/assets/video-editor/video-crop-ratio_3_4-action.svg
new file mode 100644
index 0000000000..d6ffd9a02f
--- /dev/null
+++ b/mobile/assets/video-editor/video-crop-ratio_3_4-action.svg
@@ -0,0 +1,14 @@
+
diff --git a/mobile/assets/video-editor/video-crop-ratio_4_3-action.svg b/mobile/assets/video-editor/video-crop-ratio_4_3-action.svg
new file mode 100644
index 0000000000..ab173fb7d1
--- /dev/null
+++ b/mobile/assets/video-editor/video-crop-ratio_4_3-action.svg
@@ -0,0 +1,14 @@
+
diff --git a/mobile/assets/video-editor/video-crop-ratio_9_16-action.svg b/mobile/assets/video-editor/video-crop-ratio_9_16-action.svg
new file mode 100644
index 0000000000..207e906646
--- /dev/null
+++ b/mobile/assets/video-editor/video-crop-ratio_9_16-action.svg
@@ -0,0 +1,7 @@
+
diff --git a/mobile/assets/video-editor/video-editor-crop-action.svg b/mobile/assets/video-editor/video-editor-crop-action.svg
new file mode 100644
index 0000000000..0b713cb641
--- /dev/null
+++ b/mobile/assets/video-editor/video-editor-crop-action.svg
@@ -0,0 +1,4 @@
+
diff --git a/mobile/assets/video-editor/video-editor-rotate-action.svg b/mobile/assets/video-editor/video-editor-rotate-action.svg
new file mode 100644
index 0000000000..d143654d20
--- /dev/null
+++ b/mobile/assets/video-editor/video-editor-rotate-action.svg
@@ -0,0 +1,3 @@
+
diff --git a/mobile/assets/video-editor/video-editor-trim-action.svg b/mobile/assets/video-editor/video-editor-trim-action.svg
new file mode 100644
index 0000000000..c59bc03a90
--- /dev/null
+++ b/mobile/assets/video-editor/video-editor-trim-action.svg
@@ -0,0 +1,3 @@
+
diff --git a/mobile/lib/ente_theme_data.dart b/mobile/lib/ente_theme_data.dart
index d00655437b..0715ba8d19 100644
--- a/mobile/lib/ente_theme_data.dart
+++ b/mobile/lib/ente_theme_data.dart
@@ -220,6 +220,8 @@ TextTheme _buildTextTheme(Color textColor) {
}
extension CustomColorScheme on ColorScheme {
+ Color get videoPlayerPrimaryColor => const Color.fromRGBO(1, 222, 77, 1);
+
Color get defaultBackgroundColor =>
brightness == Brightness.light ? backgroundBaseLight : backgroundBaseDark;
@@ -392,7 +394,9 @@ ElevatedButtonThemeData buildElevatedButtonThemeData({
}) {
return ElevatedButtonThemeData(
style: ElevatedButton.styleFrom(
- foregroundColor: onPrimary, backgroundColor: primary, elevation: elevation,
+ foregroundColor: onPrimary,
+ backgroundColor: primary,
+ elevation: elevation,
alignment: Alignment.center,
textStyle: const TextStyle(
fontWeight: FontWeight.w600,
diff --git a/mobile/lib/generated/intl/messages_en.dart b/mobile/lib/generated/intl/messages_en.dart
index aab7f47bd8..0c5893c8c3 100644
--- a/mobile/lib/generated/intl/messages_en.dart
+++ b/mobile/lib/generated/intl/messages_en.dart
@@ -1193,6 +1193,7 @@ class MessageLookup extends MessageLookupByLibrary {
MessageLookupByLibrary.simpleMessage(
"Save your recovery key if you haven\'t already"),
"saving": MessageLookupByLibrary.simpleMessage("Saving..."),
+ "savingEdits": MessageLookupByLibrary.simpleMessage("Saving Edits!"),
"scanCode": MessageLookupByLibrary.simpleMessage("Scan code"),
"scanThisBarcodeWithnyourAuthenticatorApp":
MessageLookupByLibrary.simpleMessage(
diff --git a/mobile/lib/generated/intl/messages_pt.dart b/mobile/lib/generated/intl/messages_pt.dart
index e17cb674e8..6f9724193d 100644
--- a/mobile/lib/generated/intl/messages_pt.dart
+++ b/mobile/lib/generated/intl/messages_pt.dart
@@ -587,7 +587,7 @@ class MessageLookup extends MessageLookupByLibrary {
"descriptions": MessageLookupByLibrary.simpleMessage("Descrições"),
"deselectAll": MessageLookupByLibrary.simpleMessage("Desmarcar todos"),
"designedToOutlive":
- MessageLookupByLibrary.simpleMessage("Feito para ter logenvidade"),
+ MessageLookupByLibrary.simpleMessage("Feito para ter longevidade"),
"details": MessageLookupByLibrary.simpleMessage("Detalhes"),
"devAccountChanged": MessageLookupByLibrary.simpleMessage(
"A conta de desenvolvedor que usamos para publicar o Ente na App Store foi alterada. Por esse motivo, você precisará fazer entrar novamente.\n\nPedimos desculpas pelo inconveniente, mas isso era inevitável."),
diff --git a/mobile/lib/generated/intl/messages_zh.dart b/mobile/lib/generated/intl/messages_zh.dart
index db60c5e0b2..a906a081fb 100644
--- a/mobile/lib/generated/intl/messages_zh.dart
+++ b/mobile/lib/generated/intl/messages_zh.dart
@@ -543,7 +543,7 @@ class MessageLookup extends MessageLookupByLibrary {
"emailVerificationToggle":
MessageLookupByLibrary.simpleMessage("电子邮件验证"),
"emailYourLogs": MessageLookupByLibrary.simpleMessage("通过电子邮件发送您的日志"),
- "empty": MessageLookupByLibrary.simpleMessage("空的"),
+ "empty": MessageLookupByLibrary.simpleMessage("清空"),
"emptyTrash": MessageLookupByLibrary.simpleMessage("要清空回收站吗?"),
"enableMaps": MessageLookupByLibrary.simpleMessage("启用地图"),
"enableMapsDesc": MessageLookupByLibrary.simpleMessage(
diff --git a/mobile/lib/generated/l10n.dart b/mobile/lib/generated/l10n.dart
index 4c7679154f..b9fcb10b00 100644
--- a/mobile/lib/generated/l10n.dart
+++ b/mobile/lib/generated/l10n.dart
@@ -8703,6 +8703,16 @@ class S {
args: [],
);
}
+
+ /// `Saving Edits!`
+ String get savingEdits {
+ return Intl.message(
+ 'Saving Edits!',
+ name: 'savingEdits',
+ desc: '',
+ args: [],
+ );
+ }
}
class AppLocalizationDelegate extends LocalizationsDelegate {
diff --git a/mobile/lib/l10n/intl_en.arb b/mobile/lib/l10n/intl_en.arb
index 6bc8b59269..00c2de3f5d 100644
--- a/mobile/lib/l10n/intl_en.arb
+++ b/mobile/lib/l10n/intl_en.arb
@@ -1226,5 +1226,6 @@
"stopCastingBody": "Do you want to stop casting?",
"castIPMismatchTitle": "Failed to cast album",
"castIPMismatchBody": "Please make sure you are on the same network as the TV.",
- "pairingComplete": "Pairing complete"
+ "pairingComplete": "Pairing complete",
+ "savingEdits": "Saving Edits!"
}
\ No newline at end of file
diff --git a/mobile/lib/ui/tools/editor/crop_video_page.dart b/mobile/lib/ui/tools/editor/crop_video_page.dart
deleted file mode 100644
index c81a101974..0000000000
--- a/mobile/lib/ui/tools/editor/crop_video_page.dart
+++ /dev/null
@@ -1,177 +0,0 @@
-import 'package:flutter/material.dart';
-import 'package:fraction/fraction.dart';
-import 'package:video_editor/video_editor.dart';
-
-class CropPage extends StatelessWidget {
- const CropPage({super.key, required this.controller});
-
- final VideoEditorController controller;
-
- @override
- Widget build(BuildContext context) {
- return Scaffold(
- backgroundColor: Colors.black,
- body: SafeArea(
- child: Padding(
- padding: const EdgeInsets.symmetric(vertical: 30),
- child: Column(
- children: [
- Row(
- children: [
- Expanded(
- child: IconButton(
- onPressed: () =>
- controller.rotate90Degrees(RotateDirection.left),
- icon: const Icon(Icons.rotate_left),
- ),
- ),
- Expanded(
- child: IconButton(
- onPressed: () =>
- controller.rotate90Degrees(RotateDirection.right),
- icon: const Icon(Icons.rotate_right),
- ),
- ),
- ],
- ),
- const SizedBox(height: 15),
- Expanded(
- child: CropGridViewer.edit(
- controller: controller,
- rotateCropArea: false,
- margin: const EdgeInsets.symmetric(horizontal: 20),
- ),
- ),
- const SizedBox(height: 15),
- Row(
- crossAxisAlignment: CrossAxisAlignment.end,
- children: [
- Expanded(
- flex: 2,
- child: IconButton(
- onPressed: () => Navigator.pop(context),
- icon: const Center(
- child: Text(
- "cancel",
- style: TextStyle(fontWeight: FontWeight.bold),
- ),
- ),
- ),
- ),
- Expanded(
- flex: 4,
- child: AnimatedBuilder(
- animation: controller,
- builder: (_, __) => Column(
- children: [
- Row(
- mainAxisAlignment: MainAxisAlignment.center,
- children: [
- IconButton(
- onPressed: () =>
- controller.preferredCropAspectRatio =
- controller.preferredCropAspectRatio
- ?.toFraction()
- .inverse()
- .toDouble(),
- icon: controller.preferredCropAspectRatio !=
- null &&
- controller.preferredCropAspectRatio! < 1
- ? const Icon(
- Icons.panorama_vertical_select_rounded,
- )
- : const Icon(
- Icons.panorama_vertical_rounded,
- ),
- ),
- IconButton(
- onPressed: () =>
- controller.preferredCropAspectRatio =
- controller.preferredCropAspectRatio
- ?.toFraction()
- .inverse()
- .toDouble(),
- icon: controller.preferredCropAspectRatio !=
- null &&
- controller.preferredCropAspectRatio! > 1
- ? const Icon(
- Icons
- .panorama_horizontal_select_rounded,
- )
- : const Icon(
- Icons.panorama_horizontal_rounded,
- ),
- ),
- ],
- ),
- Row(
- children: [
- _buildCropButton(context, null),
- _buildCropButton(context, 1.toFraction()),
- _buildCropButton(
- context,
- Fraction.fromString("9/16"),
- ),
- _buildCropButton(
- context,
- Fraction.fromString("3/4"),
- ),
- ],
- ),
- ],
- ),
- ),
- ),
- Expanded(
- flex: 2,
- child: IconButton(
- onPressed: () {
- // WAY 1: validate crop parameters set in the crop view
- controller.applyCacheCrop();
- // WAY 2: update manually with Offset values
- // controller.updateCrop(const Offset(0.2, 0.2), const Offset(0.8, 0.8));
- Navigator.pop(context);
- },
- icon: Center(
- child: Text(
- "done",
- style: TextStyle(
- color:
- const CropGridStyle().selectedBoundariesColor,
- fontWeight: FontWeight.bold,
- ),
- ),
- ),
- ),
- ),
- ],
- ),
- ],
- ),
- ),
- ),
- );
- }
-
- Widget _buildCropButton(BuildContext context, Fraction? f) {
- if (controller.preferredCropAspectRatio != null &&
- controller.preferredCropAspectRatio! > 1) f = f?.inverse();
-
- return Flexible(
- child: TextButton(
- style: ElevatedButton.styleFrom(
- elevation: 0,
- backgroundColor: controller.preferredCropAspectRatio == f?.toDouble()
- ? Colors.grey.shade800
- : null,
- foregroundColor: controller.preferredCropAspectRatio == f?.toDouble()
- ? Colors.white
- : null,
- textStyle: Theme.of(context).textTheme.bodySmall,
- ),
- onPressed: () => controller.preferredCropAspectRatio = f?.toDouble(),
- child: Text(f == null ? 'free' : '${f.numerator}:${f.denominator}'),
- ),
- );
- }
-}
diff --git a/mobile/lib/ui/tools/editor/video_crop_page.dart b/mobile/lib/ui/tools/editor/video_crop_page.dart
new file mode 100644
index 0000000000..980b10f1fa
--- /dev/null
+++ b/mobile/lib/ui/tools/editor/video_crop_page.dart
@@ -0,0 +1,122 @@
+import 'package:flutter/material.dart';
+import "package:flutter_svg/flutter_svg.dart";
+import "package:photos/ui/tools/editor/video_editor/crop_value.dart";
+import "package:photos/ui/tools/editor/video_editor/video_editor_bottom_action.dart";
+import "package:photos/ui/tools/editor/video_editor/video_editor_main_actions.dart";
+import "package:photos/ui/tools/editor/video_editor/video_editor_navigation_options.dart";
+import "package:photos/ui/tools/editor/video_editor/video_editor_player_control.dart";
+import 'package:video_editor/video_editor.dart';
+
+class VideoCropPage extends StatefulWidget {
+ const VideoCropPage({super.key, required this.controller});
+
+ final VideoEditorController controller;
+
+ @override
+ State createState() => _VideoCropPageState();
+}
+
+class _VideoCropPageState extends State {
+ @override
+ Widget build(BuildContext context) {
+ return Scaffold(
+ backgroundColor: Colors.black,
+ body: SafeArea(
+ child: Column(
+ children: [
+ Expanded(
+ child: Hero(
+ tag: "video-editor-preview",
+ child: CropGridViewer.edit(
+ controller: widget.controller,
+ rotateCropArea: false,
+ margin: const EdgeInsets.symmetric(horizontal: 20),
+ ),
+ ),
+ ),
+ VideoEditorPlayerControl(
+ controller: widget.controller,
+ ),
+ Row(
+ crossAxisAlignment: CrossAxisAlignment.end,
+ children: [
+ Expanded(
+ flex: 4,
+ child: AnimatedBuilder(
+ animation: widget.controller,
+ builder: (_, __) => Column(
+ children: [
+ VideoEditorMainActions(
+ children: [
+ // _buildCropButton(context, CropValue.original),
+ // const SizedBox(width: 40),
+ _buildCropButton(context, CropValue.free),
+ const SizedBox(width: 40),
+ _buildCropButton(context, CropValue.ratio_1_1),
+ const SizedBox(width: 40),
+ _buildCropButton(
+ context,
+ CropValue.ratio_9_16,
+ ),
+ const SizedBox(width: 40),
+ _buildCropButton(
+ context,
+ CropValue.ratio_16_9,
+ ),
+ const SizedBox(width: 40),
+ _buildCropButton(
+ context,
+ CropValue.ratio_3_4,
+ ),
+ const SizedBox(width: 40),
+ _buildCropButton(
+ context,
+ CropValue.ratio_4_3,
+ ),
+ ],
+ ),
+ ],
+ ),
+ ),
+ ),
+ ],
+ ),
+ const SizedBox(height: 40),
+ VideoEditorNavigationOptions(
+ secondaryText: "Done",
+ onSecondaryPressed: () {
+ // WAY 1: validate crop parameters set in the crop view
+ widget.controller.applyCacheCrop();
+ // WAY 2: update manually with Offset values
+ // controller.updateCrop(const Offset(0.2, 0.2), const Offset(0.8, 0.8));
+ Navigator.pop(context);
+ },
+ ),
+ ],
+ ),
+ ),
+ );
+ }
+
+ Widget _buildCropButton(BuildContext context, CropValue value) {
+ final f = value.getFraction();
+
+ return VideoEditorBottomAction(
+ label: value.displayName,
+ isSelected: value != CropValue.original &&
+ widget.controller.preferredCropAspectRatio == f?.toDouble(),
+ onPressed: () {
+ if (value == CropValue.original) {
+ widget.controller.updateCrop(Offset.zero, const Offset(1.0, 1.0));
+ widget.controller.cropAspectRatio(null);
+ setState(() {});
+ } else {
+ widget.controller.preferredCropAspectRatio = f?.toDouble();
+ }
+ },
+ child: SvgPicture.asset(
+ "assets/video-editor/video-crop-${value.name}-action.svg",
+ ),
+ );
+ }
+}
diff --git a/mobile/lib/ui/tools/editor/video_editor/crop_value.dart b/mobile/lib/ui/tools/editor/video_editor/crop_value.dart
new file mode 100644
index 0000000000..96c00736c7
--- /dev/null
+++ b/mobile/lib/ui/tools/editor/video_editor/crop_value.dart
@@ -0,0 +1,51 @@
+import "package:fraction/fraction.dart";
+
+enum CropValue {
+ original,
+ free,
+ ratio_1_1,
+ ratio_9_16,
+ ratio_16_9,
+ ratio_3_4,
+ ratio_4_3;
+
+ getFraction() {
+ switch (this) {
+ case CropValue.original:
+ return null;
+ case CropValue.free:
+ return null;
+ case CropValue.ratio_1_1:
+ return 1.toFraction();
+ case CropValue.ratio_9_16:
+ return Fraction.fromString("9/16");
+ case CropValue.ratio_16_9:
+ return Fraction.fromString("16/9");
+ case CropValue.ratio_3_4:
+ return Fraction.fromString("3/4");
+ case CropValue.ratio_4_3:
+ return Fraction.fromString("4/3");
+ default:
+ return null;
+ }
+ }
+
+ String get displayName {
+ switch (this) {
+ case CropValue.original:
+ return "Original";
+ case CropValue.free:
+ return "Free";
+ case CropValue.ratio_1_1:
+ return "1:1";
+ case CropValue.ratio_9_16:
+ return "9:16";
+ case CropValue.ratio_16_9:
+ return "16:9";
+ case CropValue.ratio_3_4:
+ return "3:4";
+ case CropValue.ratio_4_3:
+ return "4:3";
+ }
+ }
+}
diff --git a/mobile/lib/ui/tools/editor/video_editor/video_editor_bottom_action.dart b/mobile/lib/ui/tools/editor/video_editor/video_editor_bottom_action.dart
new file mode 100644
index 0000000000..c3f6beed8e
--- /dev/null
+++ b/mobile/lib/ui/tools/editor/video_editor/video_editor_bottom_action.dart
@@ -0,0 +1,57 @@
+import "package:flutter/material.dart";
+
+class VideoEditorBottomAction extends StatelessWidget {
+ const VideoEditorBottomAction({
+ super.key,
+ required this.label,
+ this.icon,
+ this.child,
+ required this.onPressed,
+ this.isSelected = false,
+ }) : assert(icon != null || child != null);
+
+ final String label;
+ final IconData? icon;
+ final Widget? child;
+ final VoidCallback onPressed;
+ final bool isSelected;
+
+ @override
+ Widget build(BuildContext context) {
+ return GestureDetector(
+ onTap: onPressed,
+ behavior: HitTestBehavior.translucent,
+ child: Column(
+ children: [
+ Container(
+ height: 48,
+ width: 48,
+ decoration: BoxDecoration(
+ color: const Color(0xFF252525),
+ shape: BoxShape.circle,
+ border: Border.all(
+ color:
+ isSelected ? const Color(0xFFFFFFFF) : Colors.transparent,
+ width: 1,
+ ),
+ ),
+ child: icon != null
+ ? Icon(icon!)
+ : Padding(
+ padding: const EdgeInsets.all(2),
+ child: child!,
+ ),
+ ),
+ const SizedBox(height: 8),
+ Text(
+ label,
+ style: const TextStyle(
+ fontSize: 12,
+ fontWeight: FontWeight.w600,
+ ),
+ ),
+ ],
+ ),
+ );
+ }
+}
diff --git a/mobile/lib/ui/tools/editor/video_editor/video_editor_main_actions.dart b/mobile/lib/ui/tools/editor/video_editor/video_editor_main_actions.dart
new file mode 100644
index 0000000000..57fc21780d
--- /dev/null
+++ b/mobile/lib/ui/tools/editor/video_editor/video_editor_main_actions.dart
@@ -0,0 +1,25 @@
+import "package:flutter/material.dart";
+
+class VideoEditorMainActions extends StatelessWidget {
+ const VideoEditorMainActions({
+ super.key,
+ required this.children,
+ });
+
+ final List children;
+
+ @override
+ Widget build(BuildContext context) {
+ return Center(
+ child: SizedBox(
+ height: 76,
+ child: ListView(
+ padding: const EdgeInsets.symmetric(horizontal: 36),
+ scrollDirection: Axis.horizontal,
+ shrinkWrap: true,
+ children: children,
+ ),
+ ),
+ );
+ }
+}
diff --git a/mobile/lib/ui/tools/editor/video_editor/video_editor_navigation_options.dart b/mobile/lib/ui/tools/editor/video_editor/video_editor_navigation_options.dart
new file mode 100644
index 0000000000..6a1d98cde7
--- /dev/null
+++ b/mobile/lib/ui/tools/editor/video_editor/video_editor_navigation_options.dart
@@ -0,0 +1,41 @@
+import "package:flutter/material.dart";
+
+class VideoEditorNavigationOptions extends StatelessWidget {
+ const VideoEditorNavigationOptions({
+ super.key,
+ this.primaryText,
+ this.onPrimaryPressed,
+ required this.secondaryText,
+ required this.onSecondaryPressed,
+ });
+
+ final String? primaryText;
+ final VoidCallback? onPrimaryPressed;
+ final String secondaryText;
+ final VoidCallback? onSecondaryPressed;
+
+ @override
+ Widget build(BuildContext context) {
+ return Hero(
+ tag: "video-editor-navigation-options",
+ child: Padding(
+ padding: const EdgeInsets.only(bottom: 12),
+ child: Row(
+ children: [
+ const SizedBox(width: 28),
+ TextButton(
+ onPressed: onPrimaryPressed?.call ?? Navigator.of(context).pop,
+ child: Text(primaryText ?? "Cancel"),
+ ),
+ const Spacer(),
+ TextButton(
+ onPressed: onSecondaryPressed,
+ child: Text(secondaryText),
+ ),
+ const SizedBox(width: 28),
+ ],
+ ),
+ ),
+ );
+ }
+}
diff --git a/mobile/lib/ui/tools/editor/video_editor/video_editor_player_control.dart b/mobile/lib/ui/tools/editor/video_editor/video_editor_player_control.dart
new file mode 100644
index 0000000000..7b488e21a5
--- /dev/null
+++ b/mobile/lib/ui/tools/editor/video_editor/video_editor_player_control.dart
@@ -0,0 +1,77 @@
+import "package:flutter/material.dart";
+import "package:video_editor/video_editor.dart";
+
+class VideoEditorPlayerControl extends StatelessWidget {
+ const VideoEditorPlayerControl({
+ super.key,
+ required this.controller,
+ });
+
+ final VideoEditorController controller;
+
+ @override
+ Widget build(BuildContext context) {
+ return Hero(
+ tag: "video_editor_player_control",
+ child: AnimatedBuilder(
+ animation: Listenable.merge([
+ controller,
+ controller.video,
+ ]),
+ builder: (_, __) {
+ final duration = controller.trimmedDuration;
+ final pos = Duration(
+ seconds: (controller.videoPosition.inSeconds -
+ controller.startTrim.inSeconds),
+ );
+ final isPlaying = controller.isPlaying;
+
+ return GestureDetector(
+ onTap: () {
+ if (controller.isPlaying) {
+ controller.video.pause();
+ } else {
+ controller.video.play();
+ }
+ },
+ child: Container(
+ height: 28,
+ margin: const EdgeInsets.only(top: 24, bottom: 28),
+ padding: const EdgeInsets.symmetric(
+ horizontal: 10,
+ vertical: 4,
+ ),
+ decoration: BoxDecoration(
+ color: const Color(0xFF252525),
+ borderRadius: BorderRadius.circular(56),
+ ),
+ child: Row(
+ mainAxisSize: MainAxisSize.min,
+ children: [
+ Icon(
+ !isPlaying ? Icons.play_arrow : Icons.pause,
+ size: 21,
+ ),
+ const SizedBox(width: 4),
+ Text(
+ "${formatter(pos)} / ${formatter(duration)}",
+ // ignore: prefer_const_constructors
+ style: TextStyle(
+ fontSize: 12,
+ fontWeight: FontWeight.w500,
+ ),
+ ),
+ ],
+ ),
+ ),
+ );
+ },
+ ),
+ );
+ }
+
+ String formatter(Duration duration) => [
+ duration.inMinutes.remainder(60).toString().padLeft(2, '0'),
+ duration.inSeconds.remainder(60).toString().padLeft(2, '0'),
+ ].join(":");
+}
diff --git a/mobile/lib/ui/tools/editor/video_editor_page.dart b/mobile/lib/ui/tools/editor/video_editor_page.dart
index c84dfec09b..ed8a96b2c4 100644
--- a/mobile/lib/ui/tools/editor/video_editor_page.dart
+++ b/mobile/lib/ui/tools/editor/video_editor_page.dart
@@ -1,11 +1,31 @@
import 'dart:io';
+import "dart:math";
import 'package:flutter/material.dart';
+import "package:flutter_svg/flutter_svg.dart";
+import "package:logging/logging.dart";
+import 'package:path/path.dart' as path;
+import "package:pedantic/pedantic.dart";
+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";
-import 'package:photos/ui/tools/editor/crop_video_page.dart';
-import 'package:photos/ui/tools/editor/export_video_result.dart';
-import 'package:photos/ui/tools/editor/export_video_service.dart';
+import "package:photos/services/sync_service.dart";
+import "package:photos/ui/tools/editor/export_video_service.dart";
+import 'package:photos/ui/tools/editor/video_crop_page.dart';
+import "package:photos/ui/tools/editor/video_editor/video_editor_bottom_action.dart";
+import "package:photos/ui/tools/editor/video_editor/video_editor_main_actions.dart";
+import "package:photos/ui/tools/editor/video_editor/video_editor_navigation_options.dart";
+import "package:photos/ui/tools/editor/video_editor/video_editor_player_control.dart";
+import "package:photos/ui/tools/editor/video_rotate_page.dart";
+import "package:photos/ui/tools/editor/video_trim_page.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:photos/utils/toast_util.dart";
import "package:video_editor/video_editor.dart";
class VideoEditorPage extends StatefulWidget {
@@ -27,7 +47,7 @@ class VideoEditorPage extends StatefulWidget {
class _VideoEditorPageState extends State {
final _exportingProgress = ValueNotifier(0.0);
final _isExporting = ValueNotifier(false);
- final double height = 60;
+ final _logger = Logger("VideoEditor");
late final VideoEditorController _controller;
@@ -37,6 +57,10 @@ class _VideoEditorPageState extends State {
_controller = VideoEditorController.file(
widget.ioFile,
minDuration: const Duration(seconds: 1),
+ cropStyle: CropGridStyle(
+ selectedBoundariesColor:
+ const ColorScheme.dark().videoPlayerPrimaryColor,
+ ),
);
_controller.initialize().then((_) => setState(() {})).catchError(
(error) {
@@ -56,21 +80,107 @@ class _VideoEditorPageState extends State {
super.dispose();
}
- void _showErrorSnackBar(String message) =>
- ScaffoldMessenger.of(context).showSnackBar(
- SnackBar(
- content: Text(message),
- duration: const Duration(seconds: 1),
- ),
- );
+ @override
+ Widget build(BuildContext context) {
+ return PopScope(
+ canPop: false,
+ child: Scaffold(
+ backgroundColor: Colors.black,
+ body: _controller.initialized
+ ? SafeArea(
+ child: Stack(
+ children: [
+ Column(
+ children: [
+ Expanded(
+ child: Column(
+ children: [
+ Expanded(
+ child: Hero(
+ tag: "video-editor-preview",
+ child: CropGridViewer.preview(
+ controller: _controller,
+ ),
+ ),
+ ),
+ VideoEditorPlayerControl(
+ controller: _controller,
+ ),
+ VideoEditorMainActions(
+ children: [
+ VideoEditorBottomAction(
+ label: "Trim",
+ child: SvgPicture.asset(
+ "assets/video-editor/video-editor-trim-action.svg",
+ ),
+ onPressed: () => Navigator.push(
+ context,
+ MaterialPageRoute(
+ builder: (context) => VideoTrimPage(
+ controller: _controller,
+ ),
+ ),
+ ),
+ ),
+ const SizedBox(width: 40),
+ VideoEditorBottomAction(
+ label: "Crop",
+ child: SvgPicture.asset(
+ "assets/video-editor/video-editor-crop-action.svg",
+ ),
+ onPressed: () => Navigator.push(
+ context,
+ MaterialPageRoute(
+ builder: (context) => VideoCropPage(
+ controller: _controller,
+ ),
+ ),
+ ),
+ ),
+ const SizedBox(width: 40),
+ VideoEditorBottomAction(
+ label: "Rotate",
+ child: SvgPicture.asset(
+ "assets/video-editor/video-editor-rotate-action.svg",
+ ),
+ onPressed: () => Navigator.push(
+ context,
+ MaterialPageRoute(
+ builder: (context) => VideoRotatePage(
+ controller: _controller,
+ ),
+ ),
+ ),
+ ),
+ ],
+ ),
+ const SizedBox(height: 40),
+ VideoEditorNavigationOptions(
+ secondaryText: "Save copy",
+ onSecondaryPressed: () {
+ exportVideo();
+ },
+ ),
+ ],
+ ),
+ ),
+ ],
+ ),
+ ],
+ ),
+ )
+ : const Center(child: CircularProgressIndicator()),
+ ),
+ );
+ }
- void _exportVideo() async {
+ void exportVideo() async {
_exportingProgress.value = 0;
_isExporting.value = true;
final config = VideoFFmpegVideoEditorConfig(
_controller,
- // format: VideoExportFormat.gif,
+ format: VideoExportFormat.mp4,
// commandBuilder: (config, videoPath, outputPath) {
// final List filters = config.getExportFilters();
// filters.add('hflip'); // add horizontal flip
@@ -85,283 +195,60 @@ class _VideoEditorPageState extends State {
_exportingProgress.value =
config.getFFmpegProgress(stats.getTime().toInt());
},
- onError: (e, s) => _showErrorSnackBar("Error on export video :("),
- onCompleted: (file) {
+ onError: (e, s) => _logger.severe("Error exporting video", e, s),
+ onCompleted: (result) async {
_isExporting.value = false;
+ final dialog = createProgressDialog(context, S.of(context).savingEdits);
+ await dialog.show();
if (!mounted) return;
- showDialog(
- context: context,
- builder: (_) => VideoResultPopup(video: file),
+ final fileName = path.basenameWithoutExtension(widget.file.title!) +
+ "_edited_" +
+ DateTime.now().microsecondsSinceEpoch.toString() +
+ ".mp4";
+ //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.saveVideo(result, title: fileName));
+ result.deleteSync();
+ final newFile = await EnteFile.fromAsset(
+ widget.file.deviceFolder ?? '',
+ newAsset!,
);
- },
- );
- }
- void _exportCover() async {
- final config = CoverFFmpegVideoEditorConfig(_controller);
- final execute = await config.getExecuteConfig();
- if (execute == null) {
- _showErrorSnackBar("Error on cover exportation initialization.");
- return;
- }
-
- await ExportService.runFFmpegCommand(
- execute,
- onError: (e, s) => _showErrorSnackBar("Error on cover exportation :("),
- onCompleted: (cover) {
- if (!mounted) return;
-
- showDialog(
- context: context,
- builder: (_) => CoverResultPopup(cover: cover),
+ newFile.generatedID = await FilesDB.instance.insert(widget.file);
+ Bus.instance
+ .fire(LocalPhotosUpdatedEvent([newFile], source: "editSave"));
+ unawaited(SyncService.instance.sync());
+ showShortToast(context, S.of(context).editsSaved);
+ _logger.info("Original file " + widget.file.toString());
+ _logger.info("Saved edits to file " + newFile.toString());
+ final existingFiles = widget.detailPageConfig.files;
+ final files = (await widget.detailPageConfig.asyncLoader!(
+ existingFiles[existingFiles.length - 1].creationTime!,
+ existingFiles[0].creationTime!,
+ ))
+ .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;
+ }
+ replacePage(
+ context,
+ DetailPage(
+ widget.detailPageConfig.copyWith(
+ files: files,
+ selectedIndex: min(selectionIndex, files.length - 1),
+ ),
+ ),
);
+ await dialog.hide();
},
);
}
-
- @override
- Widget build(BuildContext context) {
- return WillPopScope(
- onWillPop: () async => false,
- child: Scaffold(
- backgroundColor: Colors.black,
- body: _controller.initialized
- ? SafeArea(
- child: Stack(
- children: [
- Column(
- children: [
- _topNavBar(),
- Expanded(
- child: DefaultTabController(
- length: 2,
- child: Column(
- children: [
- Expanded(
- child: TabBarView(
- physics:
- const NeverScrollableScrollPhysics(),
- children: [
- Stack(
- alignment: Alignment.center,
- children: [
- CropGridViewer.preview(
- controller: _controller,
- ),
- AnimatedBuilder(
- animation: _controller.video,
- builder: (_, __) => AnimatedOpacity(
- opacity:
- _controller.isPlaying ? 0 : 1,
- duration: kThemeAnimationDuration,
- child: GestureDetector(
- onTap: _controller.video.play,
- child: Container(
- width: 40,
- height: 40,
- decoration:
- const BoxDecoration(
- color: Colors.white,
- shape: BoxShape.circle,
- ),
- child: const Icon(
- Icons.play_arrow,
- color: Colors.black,
- ),
- ),
- ),
- ),
- ),
- ],
- ),
- CoverViewer(controller: _controller),
- ],
- ),
- ),
- Container(
- height: 200,
- margin: const EdgeInsets.only(top: 10),
- child: Column(
- mainAxisAlignment: MainAxisAlignment.center,
- children: _trimSlider(),
- ),
- ),
- ValueListenableBuilder(
- valueListenable: _isExporting,
- builder: (_, bool export, Widget? child) =>
- AnimatedSize(
- duration: kThemeAnimationDuration,
- child: export ? child : null,
- ),
- child: AlertDialog(
- title: ValueListenableBuilder(
- valueListenable: _exportingProgress,
- builder: (_, double value, __) => Text(
- "Exporting video ${(value * 100).ceil()}%",
- style: const TextStyle(fontSize: 12),
- ),
- ),
- ),
- ),
- ],
- ),
- ),
- ),
- ],
- ),
- ],
- ),
- )
- : const Center(child: CircularProgressIndicator()),
- ),
- );
- }
-
- Widget _topNavBar() {
- return SafeArea(
- child: SizedBox(
- height: height,
- child: Row(
- children: [
- Expanded(
- child: IconButton(
- onPressed: () => Navigator.of(context).pop(),
- icon: const Icon(Icons.exit_to_app),
- tooltip: 'Leave editor',
- ),
- ),
- const VerticalDivider(endIndent: 22, indent: 22),
- Expanded(
- child: IconButton(
- onPressed: () =>
- _controller.rotate90Degrees(RotateDirection.left),
- icon: const Icon(Icons.rotate_left),
- tooltip: 'Rotate unclockwise',
- ),
- ),
- Expanded(
- child: IconButton(
- onPressed: () =>
- _controller.rotate90Degrees(RotateDirection.right),
- icon: const Icon(Icons.rotate_right),
- tooltip: 'Rotate clockwise',
- ),
- ),
- Expanded(
- child: IconButton(
- onPressed: () => Navigator.push(
- context,
- MaterialPageRoute(
- builder: (context) => CropPage(controller: _controller),
- ),
- ),
- icon: const Icon(Icons.crop),
- tooltip: 'Open crop screen',
- ),
- ),
- const VerticalDivider(endIndent: 22, indent: 22),
- Expanded(
- child: PopupMenuButton(
- tooltip: 'Open export menu',
- icon: const Icon(Icons.save),
- itemBuilder: (context) => [
- PopupMenuItem(
- onTap: _exportCover,
- child: const Text('Export cover'),
- ),
- PopupMenuItem(
- onTap: _exportVideo,
- child: const Text('Export video'),
- ),
- ],
- ),
- ),
- ],
- ),
- ),
- );
- }
-
- String formatter(Duration duration) => [
- duration.inMinutes.remainder(60).toString().padLeft(2, '0'),
- duration.inSeconds.remainder(60).toString().padLeft(2, '0'),
- ].join(":");
-
- List _trimSlider() {
- return [
- AnimatedBuilder(
- animation: Listenable.merge([
- _controller,
- _controller.video,
- ]),
- builder: (_, __) {
- final int duration = _controller.videoDuration.inSeconds;
- final double pos = _controller.trimPosition * duration;
-
- return Padding(
- padding: EdgeInsets.symmetric(horizontal: height / 4),
- child: Row(
- children: [
- Text(formatter(Duration(seconds: pos.toInt()))),
- const Expanded(child: SizedBox()),
- AnimatedOpacity(
- opacity: _controller.isTrimming ? 1 : 0,
- duration: kThemeAnimationDuration,
- child: Row(
- mainAxisSize: MainAxisSize.min,
- children: [
- Text(formatter(_controller.startTrim)),
- const SizedBox(width: 10),
- Text(formatter(_controller.endTrim)),
- ],
- ),
- ),
- ],
- ),
- );
- },
- ),
- Container(
- width: MediaQuery.of(context).size.width,
- margin: EdgeInsets.symmetric(vertical: height / 4),
- child: TrimSlider(
- controller: _controller,
- height: height,
- horizontalMargin: height / 4,
- child: TrimTimeline(
- controller: _controller,
- padding: const EdgeInsets.only(top: 10),
- ),
- ),
- ),
- ];
- }
-
- Widget _coverSelection() {
- return SingleChildScrollView(
- child: Center(
- child: Container(
- margin: const EdgeInsets.all(15),
- child: CoverSelection(
- controller: _controller,
- size: height + 10,
- quantity: 8,
- selectedCoverBuilder: (cover, size) {
- return Stack(
- alignment: Alignment.center,
- children: [
- cover,
- Icon(
- Icons.check_circle,
- color: const CoverSelectionStyle().selectedBorderColor,
- ),
- ],
- );
- },
- ),
- ),
- ),
- );
- }
}
diff --git a/mobile/lib/ui/tools/editor/video_rotate_page.dart b/mobile/lib/ui/tools/editor/video_rotate_page.dart
new file mode 100644
index 0000000000..c2149e7306
--- /dev/null
+++ b/mobile/lib/ui/tools/editor/video_rotate_page.dart
@@ -0,0 +1,67 @@
+import 'package:flutter/material.dart';
+import "package:photos/ui/tools/editor/video_editor/video_editor_bottom_action.dart";
+import "package:photos/ui/tools/editor/video_editor/video_editor_main_actions.dart";
+import "package:photos/ui/tools/editor/video_editor/video_editor_navigation_options.dart";
+import "package:photos/ui/tools/editor/video_editor/video_editor_player_control.dart";
+import 'package:video_editor/video_editor.dart';
+
+class VideoRotatePage extends StatelessWidget {
+ const VideoRotatePage({super.key, required this.controller});
+
+ final VideoEditorController controller;
+
+ @override
+ Widget build(BuildContext context) {
+ final rotation = controller.rotation;
+ return Scaffold(
+ backgroundColor: Colors.black,
+ body: SafeArea(
+ child: Column(
+ children: [
+ Expanded(
+ child: Hero(
+ tag: "video-editor-preview",
+ child: CropGridViewer.preview(
+ controller: controller,
+ ),
+ ),
+ ),
+ VideoEditorPlayerControl(
+ controller: controller,
+ ),
+ VideoEditorMainActions(
+ children: [
+ VideoEditorBottomAction(
+ label: "Left",
+ onPressed: () =>
+ controller.rotate90Degrees(RotateDirection.left),
+ icon: Icons.rotate_left,
+ ),
+ const SizedBox(width: 40),
+ VideoEditorBottomAction(
+ label: "Right",
+ onPressed: () =>
+ controller.rotate90Degrees(RotateDirection.right),
+ icon: Icons.rotate_right,
+ ),
+ ],
+ ),
+ const SizedBox(height: 40),
+ VideoEditorNavigationOptions(
+ secondaryText: "Done",
+ onPrimaryPressed: () {
+ while (controller.rotation != rotation) {
+ controller.rotate90Degrees(RotateDirection.left);
+ }
+ Navigator.pop(context);
+ },
+ onSecondaryPressed: () {
+ Navigator.pop(context);
+ },
+ ),
+ ],
+ ),
+ ),
+ );
+ }
+}
diff --git a/mobile/lib/ui/tools/editor/video_trim_page.dart b/mobile/lib/ui/tools/editor/video_trim_page.dart
new file mode 100644
index 0000000000..44e5d9cbb1
--- /dev/null
+++ b/mobile/lib/ui/tools/editor/video_trim_page.dart
@@ -0,0 +1,80 @@
+import 'package:flutter/material.dart';
+import "package:photos/ui/tools/editor/video_editor/video_editor_navigation_options.dart";
+import "package:photos/ui/tools/editor/video_editor/video_editor_player_control.dart";
+import 'package:video_editor/video_editor.dart';
+
+class VideoTrimPage extends StatefulWidget {
+ const VideoTrimPage({super.key, required this.controller});
+
+ final VideoEditorController controller;
+
+ @override
+ State createState() => _VideoTrimPageState();
+}
+
+class _VideoTrimPageState extends State {
+ final double height = 60;
+
+ @override
+ Widget build(BuildContext context) {
+ final minTrim = widget.controller.minTrim;
+ final maxTrim = widget.controller.maxTrim;
+
+ return Scaffold(
+ backgroundColor: Colors.black,
+ body: SafeArea(
+ child: Column(
+ children: [
+ Expanded(
+ child: Hero(
+ tag: "video-editor-preview",
+ child: CropGridViewer.preview(
+ controller: widget.controller,
+ ),
+ ),
+ ),
+ VideoEditorPlayerControl(
+ controller: widget.controller,
+ ),
+ ..._trimSlider(),
+ const SizedBox(height: 40),
+ VideoEditorNavigationOptions(
+ secondaryText: "Done",
+ onPrimaryPressed: () {
+ // reset trim
+ widget.controller.updateTrim(minTrim, maxTrim);
+ Navigator.pop(context);
+ },
+ onSecondaryPressed: () {
+ // WAY 1: validate crop parameters set in the crop view
+ widget.controller.applyCacheCrop();
+ // WAY 2: update manually with Offset values
+ // controller.updateCrop(const Offset(0.2, 0.2), const Offset(0.8, 0.8));
+ Navigator.pop(context);
+ },
+ ),
+ ],
+ ),
+ ),
+ );
+ }
+
+ List _trimSlider() {
+ return [
+ Container(
+ width: MediaQuery.of(context).size.width,
+ margin: EdgeInsets.symmetric(vertical: height / 4, horizontal: 20),
+ child: TrimSlider(
+ controller: widget.controller,
+ height: height,
+ horizontalMargin: height / 4,
+ ),
+ ),
+ ];
+ }
+
+ String formatter(Duration duration) => [
+ duration.inMinutes.remainder(60).toString().padLeft(2, '0'),
+ duration.inSeconds.remainder(60).toString().padLeft(2, '0'),
+ ].join(":");
+}
diff --git a/mobile/lib/ui/viewer/file/file_bottom_bar.dart b/mobile/lib/ui/viewer/file/file_bottom_bar.dart
index b7a31f00d6..5e9442f089 100644
--- a/mobile/lib/ui/viewer/file/file_bottom_bar.dart
+++ b/mobile/lib/ui/viewer/file/file_bottom_bar.dart
@@ -151,66 +151,59 @@ class FileBottomBarState extends State {
return ValueListenableBuilder(
valueListenable: widget.enableFullScreenNotifier,
builder: (BuildContext context, bool isFullScreen, _) {
- return IgnorePointer(
- ignoring: isFullScreen,
- child: AnimatedOpacity(
- opacity: isFullScreen ? 0 : 1,
- duration: const Duration(milliseconds: 150),
- child: Align(
- alignment: Alignment.bottomCenter,
- child: Container(
- decoration: BoxDecoration(
- gradient: LinearGradient(
- begin: Alignment.topCenter,
- end: Alignment.bottomCenter,
- colors: [
- Colors.transparent,
- Colors.black.withOpacity(0.6),
- Colors.black.withOpacity(0.72),
- ],
- stops: const [0, 0.8, 1],
+ return Align(
+ alignment: Alignment.bottomCenter,
+ child: Container(
+ decoration: BoxDecoration(
+ gradient: LinearGradient(
+ begin: Alignment.topCenter,
+ end: Alignment.bottomCenter,
+ colors: [
+ Colors.transparent,
+ Colors.black.withOpacity(0.6),
+ Colors.black.withOpacity(0.72),
+ ],
+ stops: const [0, 0.8, 1],
+ ),
+ ),
+ child: Padding(
+ padding: EdgeInsets.only(bottom: safeAreaBottomPadding),
+ child: Column(
+ mainAxisSize: MainAxisSize.min,
+ children: [
+ widget.file.caption?.isNotEmpty ?? false
+ ? Padding(
+ padding: const EdgeInsets.fromLTRB(
+ 16,
+ 12,
+ 16,
+ 0,
+ ),
+ child: GestureDetector(
+ onTap: () async {
+ await _displayDetails(widget.file);
+ await Future.delayed(
+ const Duration(milliseconds: 500),
+ ); //Waiting for some time till the caption gets updated in db if the user closes the bottom sheet without pressing 'done'
+ safeRefresh();
+ },
+ child: Text(
+ widget.file.caption!,
+ overflow: TextOverflow.ellipsis,
+ maxLines: 1,
+ style: getEnteTextTheme(context)
+ .mini
+ .copyWith(color: textBaseDark),
+ textAlign: TextAlign.center,
+ ),
+ ),
+ )
+ : const SizedBox.shrink(),
+ Row(
+ mainAxisAlignment: MainAxisAlignment.spaceAround,
+ children: children,
),
- ),
- child: Padding(
- padding: EdgeInsets.only(bottom: safeAreaBottomPadding),
- child: Column(
- mainAxisSize: MainAxisSize.min,
- children: [
- widget.file.caption?.isNotEmpty ?? false
- ? Padding(
- padding: const EdgeInsets.fromLTRB(
- 16,
- 12,
- 16,
- 0,
- ),
- child: GestureDetector(
- onTap: () async {
- await _displayDetails(widget.file);
- await Future.delayed(
- const Duration(milliseconds: 500),
- ); //Waiting for some time till the caption gets updated in db if the user closes the bottom sheet without pressing 'done'
- safeRefresh();
- },
- child: Text(
- widget.file.caption!,
- overflow: TextOverflow.ellipsis,
- maxLines: 1,
- style: getEnteTextTheme(context)
- .mini
- .copyWith(color: textBaseDark),
- textAlign: TextAlign.center,
- ),
- ),
- )
- : const SizedBox.shrink(),
- Row(
- mainAxisAlignment: MainAxisAlignment.spaceAround,
- children: children,
- ),
- ],
- ),
- ),
+ ],
),
),
),
diff --git a/mobile/pubspec.lock b/mobile/pubspec.lock
index 5057efbce0..ad05638ed0 100644
--- a/mobile/pubspec.lock
+++ b/mobile/pubspec.lock
@@ -928,6 +928,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "0.6.2"
+ flutter_svg:
+ dependency: "direct main"
+ description:
+ name: flutter_svg
+ sha256: "7b4ca6cf3304575fe9c8ec64813c8d02ee41d2afe60bcfe0678bcb5375d596a2"
+ url: "https://pub.dev"
+ source: hosted
+ version: "2.0.10+1"
flutter_test:
dependency: "direct dev"
description: flutter
@@ -2476,6 +2484,30 @@ packages:
url: "https://pub.dev"
source: hosted
version: "3.0.7"
+ vector_graphics:
+ dependency: transitive
+ description:
+ name: vector_graphics
+ sha256: "32c3c684e02f9bc0afb0ae0aa653337a2fe022e8ab064bcd7ffda27a74e288e3"
+ url: "https://pub.dev"
+ source: hosted
+ version: "1.1.11+1"
+ vector_graphics_codec:
+ dependency: transitive
+ description:
+ name: vector_graphics_codec
+ sha256: c86987475f162fadff579e7320c7ddda04cd2fdeffbe1129227a85d9ac9e03da
+ url: "https://pub.dev"
+ source: hosted
+ version: "1.1.11+1"
+ vector_graphics_compiler:
+ dependency: transitive
+ description:
+ name: vector_graphics_compiler
+ sha256: "12faff3f73b1741a36ca7e31b292ddeb629af819ca9efe9953b70bd63fc8cd81"
+ url: "https://pub.dev"
+ source: hosted
+ version: "1.1.11+1"
vector_math:
dependency: transitive
description:
diff --git a/mobile/pubspec.yaml b/mobile/pubspec.yaml
index aa53b0756c..f127d4feda 100644
--- a/mobile/pubspec.yaml
+++ b/mobile/pubspec.yaml
@@ -90,6 +90,7 @@ dependencies:
flutter_secure_storage: ^8.0.0
flutter_sodium: ^0.2.0
flutter_staggered_grid_view: ^0.6.2
+ flutter_svg: ^2.0.10+1
fluttertoast: ^8.0.6
fraction: ^5.0.2
freezed_annotation: ^2.4.1
@@ -238,6 +239,7 @@ flutter:
- assets/models/mobilenet/
- assets/models/scenes/
- assets/models/clip/
+ - assets/video-editor/
fonts:
- family: Inter
fonts: