Jelajahi Sumber

Merge branch 'redesign-with-components' into collaboration_view

Neeraj Gupta 2 tahun lalu
induk
melakukan
1aadc08fdf

+ 8 - 1
ios/Podfile.lock

@@ -102,6 +102,9 @@ PODS:
     - Flutter
   - in_app_purchase_storekit (0.0.1):
     - Flutter
+  - keyboard_visibility (0.5.0):
+    - Flutter
+    - Reachability
   - libwebp (1.2.3):
     - libwebp/demux (= 1.2.3)
     - libwebp/mux (= 1.2.3)
@@ -193,6 +196,7 @@ DEPENDENCIES:
   - fluttertoast (from `.symlinks/plugins/fluttertoast/ios`)
   - image_editor (from `.symlinks/plugins/image_editor/ios`)
   - in_app_purchase_storekit (from `.symlinks/plugins/in_app_purchase_storekit/ios`)
+  - keyboard_visibility (from `.symlinks/plugins/keyboard_visibility/ios`)
   - local_auth (from `.symlinks/plugins/local_auth/ios`)
   - media_extension (from `.symlinks/plugins/media_extension/ios`)
   - motionphoto (from `.symlinks/plugins/motionphoto/ios`)
@@ -271,6 +275,8 @@ EXTERNAL SOURCES:
     :path: ".symlinks/plugins/image_editor/ios"
   in_app_purchase_storekit:
     :path: ".symlinks/plugins/in_app_purchase_storekit/ios"
+  keyboard_visibility:
+    :path: ".symlinks/plugins/keyboard_visibility/ios"
   local_auth:
     :path: ".symlinks/plugins/local_auth/ios"
   media_extension:
@@ -321,7 +327,7 @@ SPEC CHECKSUMS:
   FirebaseInstallations: 0a115432c4e223c5ab20b0dbbe4cbefa793a0e8e
   FirebaseMessaging: 732623518591384f61c287e3d8f65294beb7ffb3
   fk_user_agent: 1f47ec39291e8372b1d692b50084b0d54103c545
-  Flutter: f04841e97a9d0b0a8025694d0796dd46242b2854
+  Flutter: 50d75fe2f02b26cc09d224853bb45737f8b3214a
   flutter_email_sender: 02d7443217d8c41483223627972bfdc09f74276b
   flutter_image_compress: 5a5e9aee05b6553048b8df1c3bc456d0afaac433
   flutter_inappwebview: bfd58618f49dc62f2676de690fc6dcda1d6c3721
@@ -336,6 +342,7 @@ SPEC CHECKSUMS:
   GoogleUtilities: 1d20a6ad97ef46f67bbdec158ce00563a671ebb7
   image_editor: eab82a302a6623a866da5145b7c4c0ee8a4ffbb4
   in_app_purchase_storekit: d7fcf4646136ec258e237872755da8ea6c1b6096
+  keyboard_visibility: 96a24de806fe6823c3ad956c01ba2ec6d056616f
   libwebp: 60305b2e989864154bd9be3d772730f08fc6a59c
   local_auth: 1740f55d7af0a2e2a8684ce225fe79d8931e808c
   Mantle: c5aa8794a29a022dfbbfc9799af95f477a69b62d

+ 16 - 5
ios/Runner.xcodeproj/project.pbxproj

@@ -16,7 +16,7 @@
 		97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; };
 		97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; };
 		97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; };
-		DA6BE5E826B3BC8600656280 /* (null) in Resources */ = {isa = PBXBuildFile; };
+		DA6BE5E826B3BC8600656280 /* BuildFile in Resources */ = {isa = PBXBuildFile; };
 /* End PBXBuildFile section */
 
 /* Begin PBXCopyFilesBuildPhase section */
@@ -213,7 +213,7 @@
 				3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */,
 				97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */,
 				97C146FC1CF9000F007C117D /* Main.storyboard in Resources */,
-				DA6BE5E826B3BC8600656280 /* (null) in Resources */,
+				DA6BE5E826B3BC8600656280 /* BuildFile in Resources */,
 				277218A0270F596900FFE3CC /* GoogleService-Info.plist in Resources */,
 			);
 			runOnlyForDeploymentPostprocessing = 0;
@@ -287,6 +287,7 @@
 				"${BUILT_PRODUCTS_DIR}/fluttertoast/fluttertoast.framework",
 				"${BUILT_PRODUCTS_DIR}/image_editor/image_editor.framework",
 				"${BUILT_PRODUCTS_DIR}/in_app_purchase_storekit/in_app_purchase_storekit.framework",
+				"${BUILT_PRODUCTS_DIR}/keyboard_visibility/keyboard_visibility.framework",
 				"${BUILT_PRODUCTS_DIR}/libwebp/libwebp.framework",
 				"${BUILT_PRODUCTS_DIR}/local_auth/local_auth.framework",
 				"${BUILT_PRODUCTS_DIR}/media_extension/media_extension.framework",
@@ -341,6 +342,7 @@
 				"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/fluttertoast.framework",
 				"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/image_editor.framework",
 				"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/in_app_purchase_storekit.framework",
+				"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/keyboard_visibility.framework",
 				"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/libwebp.framework",
 				"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/local_auth.framework",
 				"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/media_extension.framework",
@@ -499,7 +501,10 @@
 					"$(PROJECT_DIR)/Flutter",
 				);
 				INFOPLIST_FILE = Runner/Info.plist;
-				LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
+				LD_RUNPATH_SEARCH_PATHS = (
+					"$(inherited)",
+					"@executable_path/Frameworks",
+				);
 				LIBRARY_SEARCH_PATHS = (
 					"$(inherited)",
 					"$(PROJECT_DIR)/Flutter",
@@ -656,7 +661,10 @@
 					"$(PROJECT_DIR)/Flutter",
 				);
 				INFOPLIST_FILE = Runner/Info.plist;
-				LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
+				LD_RUNPATH_SEARCH_PATHS = (
+					"$(inherited)",
+					"@executable_path/Frameworks",
+				);
 				LIBRARY_SEARCH_PATHS = (
 					"$(inherited)",
 					"$(PROJECT_DIR)/Flutter",
@@ -690,7 +698,10 @@
 					"$(PROJECT_DIR)/Flutter",
 				);
 				INFOPLIST_FILE = Runner/Info.plist;
-				LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
+				LD_RUNPATH_SEARCH_PATHS = (
+					"$(inherited)",
+					"@executable_path/Frameworks",
+				);
 				LIBRARY_SEARCH_PATHS = (
 					"$(inherited)",
 					"$(PROJECT_DIR)/Flutter",

+ 3 - 1
lib/core/constants.dart

@@ -44,9 +44,11 @@ const supportEmail = 'support@ente.io';
 class FFDefault {
   static const bool enableStripe = true;
   static const bool disableCFWorker = false;
-  static const bool enableCollect = false;
 }
 
 const kDefaultProductionEndpoint = 'https://api.ente.io';
 
 const int intMaxValue = 9223372036854775807;
+
+//Screen width of iPhone 14 pro max in points is taken as maximum
+const double restrictedMaxWidth = 430;

+ 3 - 0
lib/ente_theme_data.dart

@@ -343,6 +343,9 @@ extension CustomColorScheme on ColorScheme {
 
   EnteTheme get enteTheme =>
       brightness == Brightness.light ? lightTheme : darkTheme;
+
+  EnteTheme get inverseEnteTheme =>
+      brightness == Brightness.light ? darkTheme : lightTheme;
 }
 
 OutlinedButtonThemeData buildOutlinedButtonThemeData({

+ 0 - 17
lib/services/feature_flag_service.dart

@@ -62,18 +62,6 @@ class FeatureFlagService {
     }
   }
 
-  bool enableCollect() {
-    if (isInternalUserOrDebugBuild()) {
-      return true;
-    }
-    try {
-      return _getFeatureFlags().enableCollect;
-    } catch (e) {
-      _logger.severe(e);
-      return FFDefault.enableCollect;
-    }
-  }
-
   bool isInternalUserOrDebugBuild() {
     final String? email = Configuration.instance.getEmail();
     return (email != null && email.endsWith("@ente.io")) || kDebugMode;
@@ -97,24 +85,20 @@ class FeatureFlags {
   static FeatureFlags defaultFlags = FeatureFlags(
     disableCFWorker: FFDefault.disableCFWorker,
     enableStripe: FFDefault.enableStripe,
-    enableCollect: FFDefault.enableCollect,
   );
 
   final bool disableCFWorker;
   final bool enableStripe;
-  final bool enableCollect;
 
   FeatureFlags({
     required this.disableCFWorker,
     required this.enableStripe,
-    required this.enableCollect,
   });
 
   Map<String, dynamic> toMap() {
     return {
       "disableCFWorker": disableCFWorker,
       "enableStripe": enableStripe,
-      "enableCollect": enableCollect,
     };
   }
 
@@ -127,7 +111,6 @@ class FeatureFlags {
     return FeatureFlags(
       disableCFWorker: json["disableCFWorker"] ?? FFDefault.disableCFWorker,
       enableStripe: json["enableStripe"] ?? FFDefault.enableStripe,
-      enableCollect: json["enableCollect"] ?? FFDefault.enableCollect,
     );
   }
 }

+ 29 - 22
lib/theme/colors.dart

@@ -23,6 +23,7 @@ class EnteColorScheme {
   final Color fillBase;
   final Color fillMuted;
   final Color fillFaint;
+  final Color fillFaintPressed;
 
   // Stroke Colors
   final Color strokeBase;
@@ -63,6 +64,7 @@ class EnteColorScheme {
     this.fillBase,
     this.fillMuted,
     this.fillFaint,
+    this.fillFaintPressed,
     this.strokeBase,
     this.strokeMuted,
     this.strokeFaint,
@@ -84,28 +86,30 @@ class EnteColorScheme {
 }
 
 const EnteColorScheme lightScheme = EnteColorScheme(
-    backgroundBaseLight,
-    backgroundElevatedLight,
-    backgroundElevated2Light,
-    backdropBaseLight,
-    backdropMutedLight,
-    backdropFaintLight,
-    textBaseLight,
-    textMutedLight,
-    textFaintLight,
-    blurTextBaseLight,
-    fillBaseLight,
-    fillMutedLight,
-    fillFaintLight,
-    strokeBaseLight,
-    strokeMutedLight,
-    strokeFaintLight,
-    strokeFainterLight,
-    blurStrokeBaseLight,
-    blurStrokeFaintLight,
-    blurStrokePressedLight,
-    tabIconLight,
-    avatarLight);
+  backgroundBaseLight,
+  backgroundElevatedLight,
+  backgroundElevated2Light,
+  backdropBaseLight,
+  backdropMutedLight,
+  backdropFaintLight,
+  textBaseLight,
+  textMutedLight,
+  textFaintLight,
+  blurTextBaseLight,
+  fillBaseLight,
+  fillMutedLight,
+  fillFaintLight,
+  fillFaintPressedLight,
+  strokeBaseLight,
+  strokeMutedLight,
+  strokeFaintLight,
+  strokeFainterLight,
+  blurStrokeBaseLight,
+  blurStrokeFaintLight,
+  blurStrokePressedLight,
+  tabIconLight,
+  avatarLight,
+);
 
 const EnteColorScheme darkScheme = EnteColorScheme(
   backgroundBaseDark,
@@ -121,6 +125,7 @@ const EnteColorScheme darkScheme = EnteColorScheme(
   fillBaseDark,
   fillMutedDark,
   fillFaintDark,
+  fillFaintPressedDark,
   strokeBaseDark,
   strokeMutedDark,
   strokeFaintDark,
@@ -165,10 +170,12 @@ const Color blurTextBaseDark = Color.fromRGBO(255, 255, 255, 0.95);
 const Color fillBaseLight = Color.fromRGBO(0, 0, 0, 1);
 const Color fillMutedLight = Color.fromRGBO(0, 0, 0, 0.12);
 const Color fillFaintLight = Color.fromRGBO(0, 0, 0, 0.04);
+const Color fillFaintPressedLight = Color.fromRGBO(0, 0, 0, 0.08);
 
 const Color fillBaseDark = Color.fromRGBO(255, 255, 255, 1);
 const Color fillMutedDark = Color.fromRGBO(255, 255, 255, 0.16);
 const Color fillFaintDark = Color.fromRGBO(255, 255, 255, 0.12);
+const Color fillFaintPressedDark = Color.fromRGBO(255, 255, 255, 0.06);
 
 // Stroke Colors
 const Color strokeBaseLight = Color.fromRGBO(0, 0, 0, 1);

+ 14 - 4
lib/theme/ente_theme.dart

@@ -36,10 +36,20 @@ EnteTheme darkTheme = EnteTheme(
   shadowButton: shadowButtonDark,
 );
 
-EnteColorScheme getEnteColorScheme(BuildContext context) {
-  return Theme.of(context).colorScheme.enteTheme.colorScheme;
+EnteColorScheme getEnteColorScheme(
+  BuildContext context, {
+  bool inverse = false,
+}) {
+  return inverse
+      ? Theme.of(context).colorScheme.inverseEnteTheme.colorScheme
+      : Theme.of(context).colorScheme.enteTheme.colorScheme;
 }
 
-EnteTextTheme getEnteTextTheme(BuildContext context) {
-  return Theme.of(context).colorScheme.enteTheme.textTheme;
+EnteTextTheme getEnteTextTheme(
+  BuildContext context, {
+  bool inverse = false,
+}) {
+  return inverse
+      ? Theme.of(context).colorScheme.inverseEnteTheme.textTheme
+      : Theme.of(context).colorScheme.enteTheme.textTheme;
 }

+ 4 - 3
lib/ui/common/loading_widget.dart

@@ -1,16 +1,17 @@
-import 'package:flutter/cupertino.dart';
 import 'package:flutter/material.dart';
 import 'package:photos/theme/ente_theme.dart';
 
 class EnteLoadingWidget extends StatelessWidget {
   final Color? color;
-  const EnteLoadingWidget({this.color, Key? key}) : super(key: key);
+  final bool is20pts;
+  const EnteLoadingWidget({this.is20pts = false, this.color, Key? key})
+      : super(key: key);
 
   @override
   Widget build(BuildContext context) {
     return Center(
       child: Padding(
-        padding: const EdgeInsets.all(5),
+        padding: EdgeInsets.all(is20pts ? 3 : 5),
         child: SizedBox.fromSize(
           size: const Size.square(14),
           child: CircularProgressIndicator(

+ 181 - 0
lib/ui/components/action_sheet_widget.dart

@@ -0,0 +1,181 @@
+import 'dart:ui';
+
+import 'package:flutter/material.dart';
+import 'package:modal_bottom_sheet/modal_bottom_sheet.dart';
+import 'package:photos/core/constants.dart';
+import 'package:photos/theme/colors.dart';
+import 'package:photos/theme/effects.dart';
+import 'package:photos/theme/ente_theme.dart';
+import 'package:photos/utils/separators_util.dart';
+
+enum ActionSheetType {
+  defaultActionSheet,
+  iconOnly,
+}
+
+void showActionSheet({
+  required BuildContext context,
+  required List<Widget> buttons,
+  required ActionSheetType actionSheetType,
+  bool isCheckIconGreen = false,
+  String? title,
+  String? body,
+}) {
+  showMaterialModalBottomSheet(
+    backgroundColor: Colors.transparent,
+    barrierColor: backdropMutedDark,
+    useRootNavigator: true,
+    context: context,
+    builder: (_) {
+      return ActionSheetWidget(
+        title: title,
+        body: body,
+        actionButtons: buttons,
+        actionSheetType: actionSheetType,
+        isCheckIconGreen: isCheckIconGreen,
+      );
+    },
+  );
+}
+
+class ActionSheetWidget extends StatelessWidget {
+  final String? title;
+  final String? body;
+  final List<Widget> actionButtons;
+  final ActionSheetType actionSheetType;
+  final bool isCheckIconGreen;
+
+  const ActionSheetWidget({
+    required this.actionButtons,
+    required this.actionSheetType,
+    required this.isCheckIconGreen,
+    this.title,
+    this.body,
+    super.key,
+  });
+
+  @override
+  Widget build(BuildContext context) {
+    final isTitleAndBodyNull = title == null && body == null;
+    final blur = MediaQuery.of(context).platformBrightness == Brightness.light
+        ? blurMuted
+        : blurBase;
+    final extraWidth = MediaQuery.of(context).size.width - restrictedMaxWidth;
+    final double? horizontalPadding = extraWidth > 0 ? extraWidth / 2 : null;
+    return Padding(
+      padding: EdgeInsets.fromLTRB(
+        horizontalPadding ?? 12,
+        12,
+        horizontalPadding ?? 12,
+        32,
+      ),
+      child: Container(
+        decoration: BoxDecoration(boxShadow: shadowMenuLight),
+        child: ClipRRect(
+          borderRadius: const BorderRadius.all(Radius.circular(8)),
+          child: BackdropFilter(
+            filter: ImageFilter.blur(sigmaX: blur, sigmaY: blur),
+            child: Container(
+              color: backdropBaseDark,
+              child: Padding(
+                padding: EdgeInsets.fromLTRB(
+                  24,
+                  24,
+                  24,
+                  isTitleAndBodyNull ? 24 : 28,
+                ),
+                child: Column(
+                  mainAxisSize: MainAxisSize.min,
+                  children: [
+                    isTitleAndBodyNull
+                        ? const SizedBox.shrink()
+                        : Padding(
+                            padding: const EdgeInsets.only(bottom: 36),
+                            child: ContentContainerWidget(
+                              title: title,
+                              body: body,
+                              actionSheetType: actionSheetType,
+                              isCheckIconGreen: isCheckIconGreen,
+                            ),
+                          ),
+                    ActionButtons(
+                      actionButtons,
+                    ),
+                  ],
+                ),
+              ),
+            ),
+          ),
+        ),
+      ),
+    );
+  }
+}
+
+class ContentContainerWidget extends StatelessWidget {
+  final String? title;
+  final String? body;
+  final ActionSheetType actionSheetType;
+  final bool isCheckIconGreen;
+  const ContentContainerWidget({
+    required this.actionSheetType,
+    required this.isCheckIconGreen,
+    this.title,
+    this.body,
+    super.key,
+  });
+
+  @override
+  Widget build(BuildContext context) {
+    final textTheme = getEnteTextTheme(context);
+    return Column(
+      mainAxisSize: MainAxisSize.min,
+      //todo: set cross axis to center when icon should be shown in place of body
+      crossAxisAlignment: actionSheetType == ActionSheetType.defaultActionSheet
+          ? CrossAxisAlignment.stretch
+          : CrossAxisAlignment.center,
+      children: [
+        title == null
+            ? const SizedBox.shrink()
+            : Text(
+                title!,
+                style: textTheme.h3Bold
+                    .copyWith(color: textBaseDark), //constant color
+              ),
+        title == null || body == null
+            ? const SizedBox.shrink()
+            : const SizedBox(height: 19),
+        actionSheetType == ActionSheetType.defaultActionSheet
+            ? body == null
+                ? const SizedBox.shrink()
+                : Text(
+                    body!,
+                    style: textTheme.body
+                        .copyWith(color: textMutedDark), //constant color
+                  )
+            : Icon(Icons.check_outlined,
+                size: 48,
+                color: isCheckIconGreen
+                    ? getEnteColorScheme(context).primary700
+                    : strokeBaseDark)
+      ],
+    );
+  }
+}
+
+class ActionButtons extends StatelessWidget {
+  final List<Widget> actionButtons;
+  const ActionButtons(this.actionButtons, {super.key});
+
+  @override
+  Widget build(BuildContext context) {
+    final actionButtonsWithSeparators = actionButtons;
+    return Column(
+      children:
+          //Separator height is 8pts in figma. -2pts here as the action
+          //buttons are 2pts extra in height in code compared to figma because
+          //of the border(1pt top + 1pt bottom) of action buttons.
+          addSeparators(actionButtonsWithSeparators, const SizedBox(height: 6)),
+    );
+  }
+}

+ 111 - 0
lib/ui/components/blur_menu_item_widget.dart

@@ -0,0 +1,111 @@
+import 'package:flutter/material.dart';
+import 'package:photos/theme/ente_theme.dart';
+
+class BlurMenuItemWidget extends StatefulWidget {
+  final IconData? leadingIcon;
+  final String? labelText;
+  final Color? menuItemColor;
+  final Color? pressedColor;
+  final VoidCallback? onTap;
+  const BlurMenuItemWidget({
+    this.leadingIcon,
+    this.labelText,
+    this.menuItemColor,
+    this.pressedColor,
+    this.onTap,
+    super.key,
+  });
+
+  @override
+  State<BlurMenuItemWidget> createState() => _BlurMenuItemWidgetState();
+}
+
+class _BlurMenuItemWidgetState extends State<BlurMenuItemWidget> {
+  Color? menuItemColor;
+  @override
+  void initState() {
+    menuItemColor = widget.menuItemColor;
+    super.initState();
+  }
+
+  @override
+  void didChangeDependencies() {
+    menuItemColor = widget.menuItemColor;
+    super.didChangeDependencies();
+  }
+
+  @override
+  Widget build(BuildContext context) {
+    final colorScheme = getEnteColorScheme(context);
+    return GestureDetector(
+      onTap: widget.onTap,
+      onTapDown: _onTapDown,
+      onTapUp: _onTapUp,
+      onTapCancel: _onCancel,
+      child: AnimatedContainer(
+        duration: const Duration(milliseconds: 20),
+        color: menuItemColor,
+        padding: const EdgeInsets.only(left: 16, right: 12),
+        child: Padding(
+          padding: const EdgeInsets.symmetric(vertical: 14),
+          child: Row(
+            children: [
+              widget.leadingIcon != null
+                  ? Padding(
+                      padding: const EdgeInsets.only(right: 10),
+                      child: Icon(
+                        widget.leadingIcon,
+                        size: 20,
+                        color: colorScheme.blurStrokeBase,
+                      ),
+                    )
+                  : const SizedBox.shrink(),
+              widget.labelText != null
+                  ? Flexible(
+                      child: Padding(
+                        padding: const EdgeInsets.symmetric(horizontal: 2),
+                        child: Row(
+                          children: [
+                            Flexible(
+                              child: Text(
+                                widget.labelText!,
+                                overflow: TextOverflow.ellipsis,
+                                maxLines: 1,
+                                style: getEnteTextTheme(context)
+                                    .bodyBold
+                                    .copyWith(color: colorScheme.blurTextBase),
+                              ),
+                            ),
+                          ],
+                        ),
+                      ),
+                    )
+                  : const SizedBox.shrink(),
+            ],
+          ),
+        ),
+      ),
+    );
+  }
+
+  void _onTapDown(details) {
+    setState(() {
+      menuItemColor = widget.pressedColor ?? widget.menuItemColor;
+    });
+  }
+
+  void _onTapUp(details) {
+    Future.delayed(
+      const Duration(milliseconds: 100),
+      () => setState(() {
+        menuItemColor = widget.menuItemColor;
+      }),
+    );
+  }
+
+  void _onCancel() {
+    setState(() {
+      menuItemColor = widget.menuItemColor;
+    });
+  }
+}

+ 107 - 0
lib/ui/components/bottom_action_bar/action_bar_widget.dart

@@ -0,0 +1,107 @@
+import 'package:flutter/material.dart';
+import 'package:photos/models/selected_files.dart';
+import 'package:photos/theme/ente_theme.dart';
+
+class ActionBarWidget extends StatefulWidget {
+  final String? text;
+  final List<Widget> iconButtons;
+  final SelectedFiles? selectedFiles;
+  const ActionBarWidget({
+    required this.iconButtons,
+    this.text,
+    this.selectedFiles,
+    super.key,
+  });
+
+  @override
+  State<ActionBarWidget> createState() => _ActionBarWidgetState();
+}
+
+class _ActionBarWidgetState extends State<ActionBarWidget> {
+  final ValueNotifier<int> _selectedFilesNotifier = ValueNotifier(0);
+
+  @override
+  void initState() {
+    widget.selectedFiles?.addListener(_selectedFilesListener);
+    super.initState();
+  }
+
+  @override
+  void dispose() {
+    widget.selectedFiles?.removeListener(_selectedFilesListener);
+    super.dispose();
+  }
+
+  @override
+  Widget build(BuildContext context) {
+    return SizedBox(
+      child: Row(
+        mainAxisAlignment: MainAxisAlignment.spaceBetween,
+        children: _actionBarWidgets(context),
+      ),
+    );
+  }
+
+  List<Widget> _actionBarWidgets(BuildContext context) {
+    final actionBarWidgets = <Widget>[];
+    final initialLength = widget.iconButtons.length;
+    final textTheme = getEnteTextTheme(context);
+    final colorScheme = getEnteColorScheme(context);
+
+    actionBarWidgets.addAll(widget.iconButtons);
+    if (widget.text != null) {
+      //adds 12 px spacing at the start and between iconButton elements
+      for (var i = 0; i < initialLength; i++) {
+        actionBarWidgets.insert(
+          2 * i,
+          const SizedBox(
+            width: 12,
+          ),
+        );
+      }
+      actionBarWidgets.insertAll(0, [
+        const SizedBox(width: 20),
+        Flexible(
+          child: Row(
+            children: [
+              widget.selectedFiles != null
+                  ? ValueListenableBuilder(
+                      valueListenable: _selectedFilesNotifier,
+                      builder: (context, value, child) {
+                        return Text(
+                          "${_selectedFilesNotifier.value} selectedsss",
+                          style: textTheme.small.copyWith(
+                            color: colorScheme.blurTextBase,
+                          ),
+                        );
+                      },
+                    )
+                  : Text(
+                      widget.text!,
+                      style: textTheme.small
+                          .copyWith(color: colorScheme.textMuted),
+                    ),
+            ],
+          ),
+        ),
+      ]);
+      //to add whitespace of 8pts or 12 pts at the end
+      if (widget.iconButtons.length > 1) {
+        actionBarWidgets.add(
+          const SizedBox(width: 8),
+        );
+      } else {
+        actionBarWidgets.add(
+          const SizedBox(width: 12),
+        );
+      }
+    }
+    return actionBarWidgets;
+  }
+
+  void _selectedFilesListener() {
+    if (widget.selectedFiles!.files.isNotEmpty) {
+      _selectedFilesNotifier.value = widget.selectedFiles!.files.length;
+    }
+  }
+}

+ 176 - 0
lib/ui/components/bottom_action_bar/bottom_action_bar_widget.dart

@@ -0,0 +1,176 @@
+import 'dart:ui';
+
+import 'package:expandable/expandable.dart';
+import 'package:flutter/material.dart';
+import 'package:photos/core/constants.dart';
+import 'package:photos/models/selected_files.dart';
+import 'package:photos/theme/effects.dart';
+import 'package:photos/theme/ente_theme.dart';
+import 'package:photos/ui/components/bottom_action_bar/action_bar_widget.dart';
+import 'package:photos/ui/components/icon_button_widget.dart';
+
+class BottomActionBarWidget extends StatelessWidget {
+  final String? text;
+  final List<Widget>? iconButtons;
+  final Widget expandedMenu;
+  final SelectedFiles? selectedFiles;
+  final VoidCallback? onCancel;
+  final bool hasSmallerBottomPadding;
+
+  BottomActionBarWidget({
+    required this.expandedMenu,
+    required this.hasSmallerBottomPadding,
+    this.selectedFiles,
+    this.text,
+    this.iconButtons,
+    this.onCancel,
+    super.key,
+  });
+
+  final ExpandableController _expandableController =
+      ExpandableController(initialExpanded: false);
+
+  @override
+  Widget build(BuildContext context) {
+    final widthOfScreen = MediaQuery.of(context).size.width;
+    final colorScheme = getEnteColorScheme(context);
+    final textTheme = getEnteTextTheme(context);
+    final double leftRightPadding = widthOfScreen > restrictedMaxWidth
+        ? (widthOfScreen - restrictedMaxWidth) / 2
+        : 0;
+    return ClipRRect(
+      child: BackdropFilter(
+        filter: ImageFilter.blur(sigmaX: blurBase, sigmaY: blurBase),
+        child: Container(
+          color: colorScheme.backdropBase,
+          padding: EdgeInsets.only(
+            top: 4,
+            bottom: hasSmallerBottomPadding ? 24 : 36,
+            right: leftRightPadding,
+            left: leftRightPadding,
+          ),
+          child: Column(
+            mainAxisSize: MainAxisSize.min,
+            children: [
+              ExpandableNotifier(
+                controller: _expandableController,
+                child: ExpandablePanel(
+                  theme: _getExpandableTheme(),
+                  header: Padding(
+                    padding: EdgeInsets.symmetric(
+                      horizontal: text == null ? 12 : 0,
+                    ),
+                    child: ActionBarWidget(
+                      selectedFiles: selectedFiles,
+                      text: text,
+                      iconButtons: _iconButtons(context),
+                    ),
+                  ),
+                  expanded: expandedMenu,
+                  collapsed: const SizedBox.shrink(),
+                  controller: _expandableController,
+                ),
+              ),
+              GestureDetector(
+                onTap: () {
+                  onCancel?.call();
+                  _expandableController.value = false;
+                },
+                child: Container(
+                  width: double.infinity,
+                  padding: const EdgeInsets.symmetric(
+                    horizontal: 16,
+                    vertical: 14,
+                  ),
+                  child: Center(
+                    child: Text(
+                      "Cancel",
+                      style: textTheme.bodyBold
+                          .copyWith(color: colorScheme.blurTextBase),
+                    ),
+                  ),
+                ),
+              )
+            ],
+          ),
+        ),
+      ),
+    );
+  }
+
+  List<Widget> _iconButtons(BuildContext context) {
+    final iconButtonsWithExpansionIcon = <Widget?>[
+      ...?iconButtons,
+      ExpansionIconWidget(expandableController: _expandableController)
+    ];
+    iconButtonsWithExpansionIcon.removeWhere((element) => element == null);
+    return iconButtonsWithExpansionIcon as List<Widget>;
+  }
+
+  ExpandableThemeData _getExpandableTheme() {
+    return const ExpandableThemeData(
+      hasIcon: false,
+      useInkWell: false,
+      tapBodyToCollapse: false,
+      tapBodyToExpand: false,
+      tapHeaderToExpand: false,
+      animationDuration: Duration(milliseconds: 400),
+      crossFadePoint: 0.5,
+    );
+  }
+}
+
+class ExpansionIconWidget extends StatefulWidget {
+  final ExpandableController expandableController;
+  const ExpansionIconWidget({required this.expandableController, super.key});
+
+  @override
+  State<ExpansionIconWidget> createState() => _ExpansionIconWidgetState();
+}
+
+class _ExpansionIconWidgetState extends State<ExpansionIconWidget> {
+  @override
+  void initState() {
+    widget.expandableController.addListener(_expandableListener);
+    super.initState();
+  }
+
+  @override
+  void dispose() {
+    widget.expandableController.removeListener(_expandableListener);
+    super.dispose();
+  }
+
+  @override
+  Widget build(BuildContext context) {
+    return AnimatedSwitcher(
+      duration: const Duration(milliseconds: 200),
+      switchInCurve: Curves.easeInOutExpo,
+      child: widget.expandableController.value
+          ? IconButtonWidget(
+              key: const ValueKey<bool>(false),
+              onTap: () {
+                widget.expandableController.value = false;
+                setState(() {});
+              },
+              icon: Icons.expand_more_outlined,
+              iconButtonType: IconButtonType.primary,
+            )
+          : IconButtonWidget(
+              key: const ValueKey<bool>(true),
+              onTap: () {
+                widget.expandableController.value = true;
+                setState(() {});
+              },
+              icon: Icons.more_horiz_outlined,
+              iconButtonType: IconButtonType.primary,
+            ),
+    );
+  }
+
+  _expandableListener() {
+    if (mounted) {
+      setState(() {});
+    }
+  }
+}

+ 68 - 0
lib/ui/components/bottom_action_bar/expanded_menu_widget.dart

@@ -0,0 +1,68 @@
+import 'package:flutter/material.dart';
+import 'package:photos/ui/components/blur_menu_item_widget.dart';
+import 'package:photos/ui/components/divider_widget.dart';
+
+class ExpandedMenuWidget extends StatelessWidget {
+  final List<List<BlurMenuItemWidget>> items;
+  const ExpandedMenuWidget({
+    required this.items,
+    super.key,
+  });
+
+  @override
+  Widget build(BuildContext context) {
+    const double itemHeight = 48;
+    const double whiteSpaceBetweenSections = 16;
+    const double dividerHeightBetweenItems = 1;
+    int numberOfDividers = 0;
+    double combinedHeightOfItems = 0;
+
+    for (List<BlurMenuItemWidget> group in items) {
+      //no divider if there is only one item in the section/group
+      if (group.length != 1) {
+        numberOfDividers += (group.length - 1);
+      }
+      combinedHeightOfItems += group.length * itemHeight;
+    }
+
+    return Padding(
+      padding: const EdgeInsets.symmetric(vertical: 12, horizontal: 16),
+      child: SizedBox(
+        height: combinedHeightOfItems +
+            (dividerHeightBetweenItems * numberOfDividers) +
+            (whiteSpaceBetweenSections * (items.length - 1)),
+        child: ListView.separated(
+          padding: const EdgeInsets.all(0),
+          physics: const NeverScrollableScrollPhysics(),
+          itemBuilder: (context, sectionIndex) {
+            return ClipRRect(
+              borderRadius: const BorderRadius.all(Radius.circular(8)),
+              child: SizedBox(
+                height: itemHeight * items[sectionIndex].length +
+                    (dividerHeightBetweenItems *
+                        (items[sectionIndex].length - 1)),
+                child: ListView.separated(
+                  padding: const EdgeInsets.all(0),
+                  physics: const NeverScrollableScrollPhysics(),
+                  itemBuilder: (context, itemIndex) {
+                    return items[sectionIndex][itemIndex];
+                  },
+                  separatorBuilder: (context, index) {
+                    return const DividerWidget(
+                      dividerType: DividerType.bottomBar,
+                    );
+                  },
+                  itemCount: items[sectionIndex].length,
+                ),
+              ),
+            );
+          },
+          separatorBuilder: (context, index) {
+            return const SizedBox(height: whiteSpaceBetweenSections);
+          },
+          itemCount: items.length,
+        ),
+      ),
+    );
+  }
+}

+ 1 - 1
lib/ui/components/expandable_menu_item_widget.dart

@@ -71,7 +71,7 @@ class _ExpandableMenuItemWidgetState extends State<ExpandableMenuItemWidget> {
                 padding: const EdgeInsets.only(bottom: 4),
                 child: widget.selectionOptionsWidget,
               ),
-              theme: getExpandableTheme(context),
+              theme: getExpandableTheme(),
               controller: expandableController,
             ),
           ),

+ 320 - 0
lib/ui/components/large_button_widget.dart

@@ -0,0 +1,320 @@
+import 'package:flutter/material.dart';
+import 'package:photos/theme/colors.dart';
+import 'package:photos/theme/ente_theme.dart';
+import 'package:photos/theme/text_style.dart';
+import 'package:photos/ui/common/loading_widget.dart';
+import 'package:photos/ui/components/models/button_type.dart';
+import 'package:photos/ui/components/models/large_button_style.dart';
+import 'package:photos/utils/debouncer.dart';
+
+enum ExecutionState {
+  idle,
+  inProgress,
+  successful,
+}
+
+typedef FutureVoidCallback = Future<void> Function();
+
+class LargeButtonWidget extends StatelessWidget {
+  final IconData? icon;
+  final String? labelText;
+  final ButtonType buttonType;
+  final FutureVoidCallback? onTap;
+  final bool isDisabled;
+
+  ///setting this flag to true will make the button appear like how it would
+  ///on dark theme irrespective of the app's theme.
+  final bool isInActionSheet;
+  const LargeButtonWidget({
+    required this.buttonType,
+    this.icon,
+    this.labelText,
+    this.onTap,
+    this.isInActionSheet = false,
+    this.isDisabled = false,
+    super.key,
+  });
+
+  @override
+  Widget build(BuildContext context) {
+    final colorScheme =
+        isInActionSheet ? darkScheme : getEnteColorScheme(context);
+    final inverseColorScheme = isInActionSheet
+        ? lightScheme
+        : getEnteColorScheme(context, inverse: true);
+    final textTheme =
+        isInActionSheet ? darkTextTheme : getEnteTextTheme(context);
+    final inverseTextTheme = isInActionSheet
+        ? lightTextTheme
+        : getEnteTextTheme(context, inverse: true);
+    final buttonStyle = LargeButtonStyle(
+      //Dummy default values since we need to keep these properties non-nullable
+      defaultButtonColor: Colors.transparent,
+      defaultBorderColor: Colors.transparent,
+      defaultIconColor: Colors.transparent,
+      defaultLabelStyle: textTheme.body,
+    );
+    buttonStyle.defaultButtonColor = buttonType.defaultButtonColor(colorScheme);
+    buttonStyle.pressedButtonColor = buttonType.pressedButtonColor(colorScheme);
+    buttonStyle.disabledButtonColor =
+        buttonType.disabledButtonColor(colorScheme);
+    buttonStyle.defaultBorderColor = buttonType.defaultBorderColor(colorScheme);
+    buttonStyle.pressedBorderColor = buttonType.pressedBorderColor(colorScheme);
+    buttonStyle.disabledBorderColor =
+        buttonType.disabledBorderColor(colorScheme);
+    buttonStyle.defaultIconColor = buttonType.defaultIconColor(
+      colorScheme: colorScheme,
+      inverseColorScheme: inverseColorScheme,
+    );
+    buttonStyle.pressedIconColor = buttonType.pressedIconColor(colorScheme);
+    buttonStyle.disabledIconColor = buttonType.disabledIconColor(colorScheme);
+    buttonStyle.defaultLabelStyle = buttonType.defaultLabelStyle(
+      textTheme: textTheme,
+      inverseTextTheme: inverseTextTheme,
+    );
+    buttonStyle.pressedLabelStyle =
+        buttonType.pressedLabelStyle(textTheme, colorScheme);
+    buttonStyle.disabledLabelStyle =
+        buttonType.disabledLabelStyle(textTheme, colorScheme);
+    buttonStyle.checkIconColor = buttonType.checkIconColor(colorScheme);
+
+    return LargeButtonChildWidget(
+      buttonStyle: buttonStyle,
+      buttonType: buttonType,
+      isDisabled: isDisabled,
+      onTap: onTap,
+      labelText: labelText,
+      icon: icon,
+    );
+  }
+}
+
+class LargeButtonChildWidget extends StatefulWidget {
+  final LargeButtonStyle buttonStyle;
+  final FutureVoidCallback? onTap;
+  final ButtonType buttonType;
+  final String? labelText;
+  final IconData? icon;
+  final bool isDisabled;
+  const LargeButtonChildWidget({
+    required this.buttonStyle,
+    required this.buttonType,
+    required this.isDisabled,
+    this.onTap,
+    this.labelText,
+    this.icon,
+    super.key,
+  });
+
+  @override
+  State<LargeButtonChildWidget> createState() => _LargeButtonChildWidgetState();
+}
+
+class _LargeButtonChildWidgetState extends State<LargeButtonChildWidget> {
+  late Color buttonColor;
+  late Color borderColor;
+  late Color iconColor;
+  late TextStyle labelStyle;
+  late Color checkIconColor;
+  late Color loadingIconColor;
+  late bool hasExecutionStates;
+  final _debouncer = Debouncer(const Duration(milliseconds: 300));
+  ExecutionState executionState = ExecutionState.idle;
+  @override
+  void initState() {
+    checkIconColor = widget.buttonStyle.checkIconColor ??
+        widget.buttonStyle.defaultIconColor;
+    loadingIconColor = widget.buttonStyle.defaultIconColor;
+    hasExecutionStates = widget.buttonType.hasExecutionStates;
+    if (widget.isDisabled) {
+      buttonColor = widget.buttonStyle.disabledButtonColor ??
+          widget.buttonStyle.defaultButtonColor;
+      borderColor = widget.buttonStyle.disabledBorderColor ??
+          widget.buttonStyle.defaultBorderColor;
+      iconColor = widget.buttonStyle.disabledIconColor ??
+          widget.buttonStyle.defaultIconColor;
+      labelStyle = widget.buttonStyle.disabledLabelStyle ??
+          widget.buttonStyle.defaultLabelStyle;
+    } else {
+      buttonColor = widget.buttonStyle.defaultButtonColor;
+      borderColor = widget.buttonStyle.defaultBorderColor;
+      iconColor = widget.buttonStyle.defaultIconColor;
+      labelStyle = widget.buttonStyle.defaultLabelStyle;
+    }
+    super.initState();
+  }
+
+  @override
+  Widget build(BuildContext context) {
+    return GestureDetector(
+      onTap: _shouldRegisterGestures ? _onTap : null,
+      onTapDown: _shouldRegisterGestures ? _onTapDown : null,
+      onTapUp: _shouldRegisterGestures ? _onTapUp : null,
+      onTapCancel: _shouldRegisterGestures ? _onTapCancel : null,
+      child: AnimatedContainer(
+        duration: const Duration(milliseconds: 16),
+        width: double.infinity,
+        decoration: BoxDecoration(
+          borderRadius: const BorderRadius.all(Radius.circular(4)),
+          color: buttonColor,
+          border: Border.all(color: borderColor),
+        ),
+        child: Padding(
+          padding: const EdgeInsets.symmetric(vertical: 14, horizontal: 16),
+          child: AnimatedSwitcher(
+            duration: const Duration(milliseconds: 175),
+            switchInCurve: Curves.easeInOutExpo,
+            switchOutCurve: Curves.easeInOutExpo,
+            child: executionState == ExecutionState.idle
+                ? widget.buttonType.hasTrailingIcon
+                    ? Row(
+                        mainAxisAlignment: MainAxisAlignment.spaceBetween,
+                        children: [
+                          widget.labelText == null
+                              ? const SizedBox.shrink()
+                              : Flexible(
+                                  child: Padding(
+                                    padding: widget.icon == null
+                                        ? const EdgeInsets.symmetric(
+                                            horizontal: 8,
+                                          )
+                                        : const EdgeInsets.only(right: 16),
+                                    child: Text(
+                                      widget.labelText!,
+                                      overflow: TextOverflow.ellipsis,
+                                      maxLines: 2,
+                                      style: labelStyle,
+                                    ),
+                                  ),
+                                ),
+                          widget.icon == null
+                              ? const SizedBox.shrink()
+                              : Icon(
+                                  widget.icon,
+                                  size: 20,
+                                  color: iconColor,
+                                ),
+                        ],
+                      )
+                    : Row(
+                        mainAxisAlignment: MainAxisAlignment.center,
+                        children: [
+                          widget.icon == null
+                              ? const SizedBox.shrink()
+                              : Icon(
+                                  widget.icon,
+                                  size: 20,
+                                  color: iconColor,
+                                ),
+                          widget.icon == null || widget.labelText == null
+                              ? const SizedBox.shrink()
+                              : const SizedBox(width: 8),
+                          widget.labelText == null
+                              ? const SizedBox.shrink()
+                              : Flexible(
+                                  child: Padding(
+                                    padding: const EdgeInsets.symmetric(
+                                      horizontal: 8,
+                                    ),
+                                    child: Text(
+                                      widget.labelText!,
+                                      style: labelStyle,
+                                      maxLines: 2,
+                                      overflow: TextOverflow.ellipsis,
+                                    ),
+                                  ),
+                                )
+                        ],
+                      )
+                : executionState == ExecutionState.inProgress
+                    ? EnteLoadingWidget(
+                        is20pts: true,
+                        color: loadingIconColor,
+                      )
+                    : executionState == ExecutionState.successful
+                        ? Icon(
+                            Icons.check_outlined,
+                            size: 20,
+                            color: checkIconColor,
+                          )
+                        : const SizedBox.shrink(), //fallback
+          ),
+        ),
+      ),
+    );
+  }
+
+  bool get _shouldRegisterGestures =>
+      !widget.isDisabled &&
+      (widget.onTap != null) &&
+      executionState == ExecutionState.idle;
+
+  void _onTap() async {
+    if (hasExecutionStates) {
+      _debouncer.run(
+        () => Future(() {
+          setState(() {
+            executionState = ExecutionState.inProgress;
+          });
+        }),
+      );
+      await widget.onTap!
+          .call()
+          .onError((error, stackTrace) => _debouncer.cancelDebounce());
+      _debouncer.cancelDebounce();
+      // when the time taken by widget.onTap is approximately equal to the debounce
+      // time, the callback is getting executed when/after the if condition
+      // below is executing/executed which results in execution state stuck at
+      // idle state. This Future is for delaying the execution of the if
+      // condition so that the calback in the debouncer finishes execution before.
+      await Future.delayed(const Duration(milliseconds: 5));
+      if (executionState == ExecutionState.inProgress) {
+        setState(() {
+          executionState = ExecutionState.successful;
+          Future.delayed(const Duration(seconds: 2), () {
+            setState(() {
+              executionState = ExecutionState.idle;
+            });
+          });
+        });
+      }
+    } else {
+      widget.onTap!.call();
+    }
+  }
+
+  void _onTapDown(details) {
+    setState(() {
+      buttonColor = widget.buttonStyle.pressedButtonColor ??
+          widget.buttonStyle.defaultButtonColor;
+      borderColor = widget.buttonStyle.pressedBorderColor ??
+          widget.buttonStyle.defaultBorderColor;
+      iconColor = widget.buttonStyle.pressedIconColor ??
+          widget.buttonStyle.defaultIconColor;
+      labelStyle = widget.buttonStyle.pressedLabelStyle ??
+          widget.buttonStyle.defaultLabelStyle;
+    });
+  }
+
+  void _onTapUp(details) {
+    Future.delayed(
+      const Duration(milliseconds: 84),
+      () => setState(() {
+        setAllStylesToDefault();
+      }),
+    );
+  }
+
+  void _onTapCancel() {
+    setState(() {
+      setAllStylesToDefault();
+    });
+  }
+
+  void setAllStylesToDefault() {
+    buttonColor = widget.buttonStyle.defaultButtonColor;
+    borderColor = widget.buttonStyle.defaultBorderColor;
+    iconColor = widget.buttonStyle.defaultIconColor;
+    labelStyle = widget.buttonStyle.defaultLabelStyle;
+  }
+}

+ 197 - 0
lib/ui/components/models/button_type.dart

@@ -0,0 +1,197 @@
+import 'package:flutter/material.dart';
+import 'package:photos/theme/colors.dart';
+import 'package:photos/theme/text_style.dart';
+
+enum ButtonType {
+  primary,
+  secondary,
+  neutral,
+  trailingIcon,
+  critical,
+  tertiaryCritical,
+  trailingIconPrimary,
+  trailingIconSecondary;
+
+  bool get isPrimary =>
+      this == ButtonType.primary || this == ButtonType.trailingIconPrimary;
+
+  bool get hasTrailingIcon =>
+      this == ButtonType.trailingIcon ||
+      this == ButtonType.trailingIconPrimary ||
+      this == ButtonType.trailingIconSecondary;
+
+  bool get isSecondary =>
+      this == ButtonType.secondary || this == ButtonType.trailingIconSecondary;
+
+  bool get isCritical =>
+      this == ButtonType.critical || this == ButtonType.tertiaryCritical;
+
+  Color defaultButtonColor(EnteColorScheme colorScheme) {
+    if (isPrimary) {
+      return colorScheme.primary500;
+    }
+    if (isSecondary) {
+      return colorScheme.fillFaint;
+    }
+    if (this == ButtonType.neutral || this == ButtonType.trailingIcon) {
+      return colorScheme.fillBase;
+    }
+    if (this == ButtonType.critical) {
+      return colorScheme.warning700;
+    }
+    if (this == ButtonType.tertiaryCritical) {
+      return Colors.transparent;
+    }
+    return Colors.transparent;
+  }
+
+  //Returning null to fallback to default color
+  Color? pressedButtonColor(EnteColorScheme colorScheme) {
+    if (this == ButtonType.primary) {
+      return colorScheme.primary700;
+    }
+    return null;
+  }
+
+  //Returning null to fallback to default color
+  Color? disabledButtonColor(EnteColorScheme colorScheme) {
+    if (this == ButtonType.primary || this == ButtonType.critical) {
+      return colorScheme.fillFaint;
+    }
+    return null;
+  }
+
+  Color defaultBorderColor(EnteColorScheme colorScheme) {
+    if (this == ButtonType.tertiaryCritical) {
+      return colorScheme.warning700;
+    }
+    return Colors.transparent;
+  }
+
+  //Returning null to fallback to default color
+  Color? pressedBorderColor(EnteColorScheme colorScheme) {
+    if (this == ButtonType.primary) {
+      return colorScheme.strokeMuted;
+    }
+    if (this == ButtonType.secondary ||
+        this == ButtonType.critical ||
+        this == ButtonType.tertiaryCritical) {
+      return colorScheme.strokeBase;
+    }
+    return null;
+  }
+
+  //Returning null to fallback to default color
+  Color? disabledBorderColor(EnteColorScheme colorScheme) {
+    if (this == ButtonType.primary ||
+        this == ButtonType.secondary ||
+        this == ButtonType.critical) {
+      return Colors.transparent;
+    }
+    if (this == ButtonType.tertiaryCritical) {
+      return colorScheme.strokeMuted;
+    }
+    return null;
+  }
+
+  Color defaultIconColor({
+    required EnteColorScheme colorScheme,
+    required EnteColorScheme inverseColorScheme,
+  }) {
+    if (isPrimary || this == ButtonType.critical) {
+      return strokeBaseDark;
+    }
+    if (isSecondary) {
+      return colorScheme.strokeBase;
+    }
+    if (this == ButtonType.neutral || this == ButtonType.trailingIcon) {
+      return inverseColorScheme.strokeBase;
+    }
+    if (this == ButtonType.tertiaryCritical) {
+      return colorScheme.warning500;
+    }
+    //fallback
+    return colorScheme.strokeBase;
+  }
+
+  //Returning null to fallback to default color
+  Color? pressedIconColor(EnteColorScheme colorScheme) {
+    if (this == ButtonType.tertiaryCritical) {
+      return colorScheme.strokeBase;
+    }
+    return null;
+  }
+
+  //Returning null to fallback to default color
+  Color? disabledIconColor(EnteColorScheme colorScheme) {
+    if (this == ButtonType.primary || this == ButtonType.secondary) {
+      return colorScheme.strokeMuted;
+    }
+    if (this == ButtonType.critical || this == ButtonType.tertiaryCritical) {
+      return colorScheme.strokeFaint;
+    }
+    return null;
+  }
+
+  TextStyle defaultLabelStyle({
+    required EnteTextTheme textTheme,
+    required EnteTextTheme inverseTextTheme,
+  }) {
+    if (isPrimary || this == ButtonType.critical) {
+      return textTheme.bodyBold.copyWith(color: textBaseDark);
+    }
+    if (isSecondary) {
+      return textTheme.bodyBold;
+    }
+    if (this == ButtonType.neutral || this == ButtonType.trailingIcon) {
+      return inverseTextTheme.bodyBold;
+    }
+    if (this == ButtonType.tertiaryCritical) {
+      return textTheme.bodyBold.copyWith(color: warning500);
+    }
+    //fallback
+    return textTheme.bodyBold;
+  }
+
+  //Returning null to fallback to default color
+  TextStyle? pressedLabelStyle(
+    EnteTextTheme textTheme,
+    EnteColorScheme colorScheme,
+  ) {
+    if (this == ButtonType.tertiaryCritical) {
+      return textTheme.bodyBold.copyWith(color: colorScheme.strokeBase);
+    }
+    return null;
+  }
+
+  //Returning null to fallback to default color
+  TextStyle? disabledLabelStyle(
+    EnteTextTheme textTheme,
+    EnteColorScheme colorScheme,
+  ) {
+    if (this == ButtonType.primary ||
+        this == ButtonType.secondary ||
+        this == ButtonType.critical ||
+        this == ButtonType.tertiaryCritical) {
+      return textTheme.bodyBold.copyWith(color: colorScheme.textFaint);
+    }
+    return null;
+  }
+
+  Color? checkIconColor(EnteColorScheme colorScheme) {
+    if (this == ButtonType.secondary) {
+      return colorScheme.primary500;
+    }
+    return null;
+  }
+
+  bool get hasExecutionStates {
+    if (this == ButtonType.primary ||
+        this == ButtonType.secondary ||
+        this == ButtonType.neutral) {
+      return true;
+    } else {
+      return false;
+    }
+  }
+}

+ 33 - 0
lib/ui/components/models/large_button_style.dart

@@ -0,0 +1,33 @@
+import 'package:flutter/material.dart';
+
+class LargeButtonStyle {
+  Color defaultButtonColor;
+  Color? pressedButtonColor;
+  Color? disabledButtonColor;
+  Color defaultBorderColor;
+  Color? pressedBorderColor;
+  Color? disabledBorderColor;
+  Color defaultIconColor;
+  Color? pressedIconColor;
+  Color? disabledIconColor;
+  TextStyle defaultLabelStyle;
+  TextStyle? pressedLabelStyle;
+  TextStyle? disabledLabelStyle;
+  Color? checkIconColor;
+
+  LargeButtonStyle({
+    required this.defaultButtonColor,
+    this.pressedButtonColor,
+    this.disabledButtonColor,
+    required this.defaultBorderColor,
+    this.pressedBorderColor,
+    this.disabledBorderColor,
+    required this.defaultIconColor,
+    this.pressedIconColor,
+    this.disabledIconColor,
+    required this.defaultLabelStyle,
+    this.pressedLabelStyle,
+    this.disabledLabelStyle,
+    this.checkIconColor,
+  });
+}

+ 1 - 1
lib/ui/settings/common_settings.dart

@@ -4,7 +4,7 @@ import 'package:flutter/material.dart';
 
 Widget sectionOptionSpacing = const SizedBox(height: 6);
 
-ExpandableThemeData getExpandableTheme(BuildContext context) {
+ExpandableThemeData getExpandableTheme() {
   return const ExpandableThemeData(
     hasIcon: false,
     useInkWell: false,

+ 27 - 38
lib/ui/sharing/manage_links_widget.dart

@@ -10,7 +10,6 @@ import 'package:flutter_sodium/flutter_sodium.dart';
 import 'package:photos/ente_theme_data.dart';
 import 'package:photos/models/collection.dart';
 import 'package:photos/services/collections_service.dart';
-import 'package:photos/services/feature_flag_service.dart';
 import 'package:photos/theme/colors.dart';
 import 'package:photos/theme/ente_theme.dart';
 import 'package:photos/ui/actions/collection/collection_sharing_actions.dart';
@@ -62,42 +61,6 @@ class _ManageSharedLinkWidgetState extends State<ManageSharedLinkWidget> {
   Widget build(BuildContext context) {
     final enteColorScheme = getEnteColorScheme(context);
     final PublicURL url = widget.collection?.publicURLs?.firstOrNull;
-
-    final enableCollectFeature = FeatureFlagService.instance.enableCollect();
-    final Widget collect = enableCollectFeature
-        ? Column(
-            crossAxisAlignment: CrossAxisAlignment.start,
-            children: [
-              MenuItemWidget(
-                captionedTextWidget: const CaptionedTextWidget(
-                  title: "Allow adding photos",
-                ),
-                alignCaptionedTextToLeft: true,
-                menuItemColor: getEnteColorScheme(context).fillFaint,
-                pressedColor: getEnteColorScheme(context).fillFaint,
-                trailingWidget: Switch.adaptive(
-                  value: widget
-                          .collection.publicURLs?.firstOrNull?.enableCollect ??
-                      false,
-                  onChanged: (value) async {
-                    await _updateUrlSettings(
-                      context,
-                      {'enableCollect': value},
-                    );
-
-                    setState(() {});
-                  },
-                ),
-              ),
-              const MenuSectionDescriptionWidget(
-                content:
-                    "Allow people with the link to also add photos to the shared "
-                    "album.",
-              ),
-              const SizedBox(height: 24)
-            ],
-          )
-        : const SizedBox.shrink();
     return Scaffold(
       backgroundColor: Theme.of(context).backgroundColor,
       appBar: AppBar(
@@ -114,7 +77,33 @@ class _ManageSharedLinkWidgetState extends State<ManageSharedLinkWidget> {
               child: Column(
                 crossAxisAlignment: CrossAxisAlignment.start,
                 children: [
-                  collect,
+                  MenuItemWidget(
+                    captionedTextWidget: const CaptionedTextWidget(
+                      title: "Allow adding photos",
+                    ),
+                    alignCaptionedTextToLeft: true,
+                    menuItemColor: getEnteColorScheme(context).fillFaint,
+                    pressedColor: getEnteColorScheme(context).fillFaint,
+                    trailingWidget: Switch.adaptive(
+                      value: widget.collection.publicURLs?.firstOrNull
+                              ?.enableCollect ??
+                          false,
+                      onChanged: (value) async {
+                        await _updateUrlSettings(
+                          context,
+                          {'enableCollect': value},
+                        );
+
+                        setState(() {});
+                      },
+                    ),
+                  ),
+                  const MenuSectionDescriptionWidget(
+                    content:
+                        "Allow people with the link to also add photos to the shared "
+                        "album.",
+                  ),
+                  const SizedBox(height: 24),
                   MenuItemWidget(
                     alignCaptionedTextToLeft: true,
                     captionedTextWidget: CaptionedTextWidget(

+ 1 - 1
lib/ui/sharing/user_avator_widget.dart

@@ -23,7 +23,7 @@ class UserAvatarWidget extends StatelessWidget {
     final enteTextTheme = getEnteTextTheme(context);
     final colorScheme = getEnteColorScheme(context);
     final displayChar = (user.name == null || user.name!.isEmpty)
-        ? ((user.email.isEmpty ?? true) ? " " : user.email.substring(0, 1))
+        ? ((user.email.isEmpty) ? " " : user.email.substring(0, 1))
         : user.name!.substring(0, 1);
     final randomColor = colorScheme.avatarColors[
         (user.id ?? 0).remainder(colorScheme.avatarColors.length)];

+ 98 - 17
lib/ui/viewer/gallery/collection_page.dart

@@ -1,6 +1,7 @@
 // @dart=2.9
 
 import 'package:flutter/material.dart';
+import 'package:photos/core/configuration.dart';
 import 'package:photos/core/event_bus.dart';
 import 'package:photos/db/files_db.dart';
 import 'package:photos/events/collection_updated_event.dart';
@@ -10,19 +11,24 @@ import 'package:photos/models/file_load_result.dart';
 import 'package:photos/models/gallery_type.dart';
 import 'package:photos/models/selected_files.dart';
 import 'package:photos/services/ignored_files_service.dart';
+import 'package:photos/theme/ente_theme.dart';
+import 'package:photos/ui/components/blur_menu_item_widget.dart';
+import 'package:photos/ui/components/bottom_action_bar/bottom_action_bar_widget.dart';
+import 'package:photos/ui/components/bottom_action_bar/expanded_menu_widget.dart';
+import 'package:photos/ui/components/icon_button_widget.dart';
 import 'package:photos/ui/viewer/gallery/empty_state.dart';
 import 'package:photos/ui/viewer/gallery/gallery.dart';
 import 'package:photos/ui/viewer/gallery/gallery_app_bar_widget.dart';
-import 'package:photos/ui/viewer/gallery/gallery_overlay_widget.dart';
+import 'package:photos/utils/delete_file_util.dart';
+import 'package:photos/utils/share_util.dart';
 
-class CollectionPage extends StatelessWidget {
+class CollectionPage extends StatefulWidget {
   final CollectionWithThumbnail c;
   final String tagPrefix;
   final GalleryType appBarType;
-  final _selectedFiles = SelectedFiles();
   final bool hasVerifiedLock;
 
-  CollectionPage(
+  const CollectionPage(
     this.c, {
     this.tagPrefix = "collection",
     this.appBarType = GalleryType.ownedCollection,
@@ -30,17 +36,41 @@ class CollectionPage extends StatelessWidget {
     Key key,
   }) : super(key: key);
 
+  @override
+  State<CollectionPage> createState() => _CollectionPageState();
+}
+
+class _CollectionPageState extends State<CollectionPage> {
+  final _selectedFiles = SelectedFiles();
+  final GlobalKey shareButtonKey = GlobalKey();
+
+  final ValueNotifier<double> _bottomPosition = ValueNotifier(-150.0);
+  @override
+  void initState() {
+    _selectedFiles.addListener(_selectedFilesListener);
+    super.initState();
+  }
+
+  @override
+  void dispose() {
+    _selectedFiles.removeListener(_selectedFilesListener);
+    super.dispose();
+  }
+
   @override
   Widget build(Object context) {
-    if (hasVerifiedLock == false && c.collection.isHidden()) {
+    if (widget.hasVerifiedLock == false && widget.c.collection.isHidden()) {
       return const EmptyState();
     }
-    final initialFiles = c.thumbnail != null ? [c.thumbnail] : null;
+    final int ownerID = Configuration.instance.getUserID();
+    final int collectionOwner = widget.c.collection.owner.id;
+    final initialFiles =
+        widget.c.thumbnail != null ? [widget.c.thumbnail] : null;
     final gallery = Gallery(
       asyncLoader: (creationStartTime, creationEndTime, {limit, asc}) async {
         final FileLoadResult result =
             await FilesDB.instance.getFilesInCollection(
-          c.collection.id,
+          widget.c.collection.id,
           creationStartTime,
           creationEndTime,
           limit: limit,
@@ -57,38 +87,89 @@ class CollectionPage extends StatelessWidget {
       },
       reloadEvent: Bus.instance
           .on<CollectionUpdatedEvent>()
-          .where((event) => event.collectionID == c.collection.id),
+          .where((event) => event.collectionID == widget.c.collection.id),
       removalEventTypes: const {
         EventType.deletedFromRemote,
         EventType.deletedFromEverywhere,
         EventType.hide,
       },
-      tagPrefix: tagPrefix,
+      tagPrefix: widget.tagPrefix,
       selectedFiles: _selectedFiles,
       initialFiles: initialFiles,
-      albumName: c.collection.name,
+      albumName: widget.c.collection.name,
     );
     return Scaffold(
       appBar: PreferredSize(
         preferredSize: const Size.fromHeight(50.0),
         child: GalleryAppBarWidget(
-          appBarType,
-          c.collection.name,
+          widget.appBarType,
+          widget.c.collection.name,
           _selectedFiles,
-          collection: c.collection,
+          collection: widget.c.collection,
         ),
       ),
       body: Stack(
         alignment: Alignment.bottomCenter,
         children: [
           gallery,
-          GalleryOverlayWidget(
-            appBarType,
-            _selectedFiles,
-            collection: c.collection,
+          ValueListenableBuilder(
+            valueListenable: _bottomPosition,
+            builder: (context, value, child) {
+              final colorScheme = getEnteColorScheme(context);
+              return AnimatedPositioned(
+                curve: Curves.easeInOutExpo,
+                bottom: _bottomPosition.value,
+                right: 0,
+                left: 0,
+                duration: const Duration(milliseconds: 400),
+                child: BottomActionBarWidget(
+                  selectedFiles: _selectedFiles,
+                  hasSmallerBottomPadding: true,
+                  expandedMenu: ExpandedMenuWidget(
+                    items: [
+                      [
+                        BlurMenuItemWidget(
+                          leadingIcon: Icons.add_outlined,
+                          labelText: "One",
+                          menuItemColor: colorScheme.fillFaint,
+                        ),
+                      ],
+                    ],
+                  ),
+                  text: _selectedFiles.files.length.toString() + ' selected',
+                  onCancel: () {
+                    if (_selectedFiles.files.isNotEmpty) {
+                      _selectedFiles.clearAll();
+                    }
+                  },
+                  iconButtons: [
+                    IconButtonWidget(
+                      icon: Icons.delete_outlined,
+                      iconButtonType: IconButtonType.primary,
+                      onTap: () => showDeleteSheet(context, _selectedFiles),
+                    ),
+                    IconButtonWidget(
+                      icon: Icons.ios_share_outlined,
+                      iconButtonType: IconButtonType.primary,
+                      onTap: () => shareSelected(
+                        context,
+                        shareButtonKey,
+                        _selectedFiles.files,
+                      ),
+                    ),
+                  ],
+                ),
+              );
+            },
           ),
         ],
       ),
     );
   }
+
+  _selectedFilesListener() {
+    _selectedFiles.files.isNotEmpty
+        ? _bottomPosition.value = 0.0
+        : _bottomPosition.value = -150.0;
+  }
 }

+ 99 - 0
lib/utils/delete_file_util.dart

@@ -6,6 +6,7 @@ import 'dart:io';
 import 'dart:math';
 
 import 'package:device_info/device_info.dart';
+import 'package:flutter/cupertino.dart';
 import 'package:flutter/material.dart';
 import 'package:logging/logging.dart';
 import 'package:photo_manager/photo_manager.dart';
@@ -16,6 +17,7 @@ import 'package:photos/events/collection_updated_event.dart';
 import 'package:photos/events/files_updated_event.dart';
 import 'package:photos/events/local_photos_updated_event.dart';
 import 'package:photos/models/file.dart';
+import 'package:photos/models/selected_files.dart';
 import 'package:photos/models/trash_item_request.dart';
 import 'package:photos/services/remote_sync_service.dart';
 import 'package:photos/services/sync_service.dart';
@@ -485,3 +487,100 @@ Future<bool> shouldProceedWithDeletion(BuildContext context) async {
   );
   return choice == DialogUserChoice.secondChoice;
 }
+
+void showDeleteSheet(BuildContext context, SelectedFiles selectedFiles) {
+  final count = selectedFiles.files.length;
+  bool containsUploadedFile = false, containsLocalFile = false;
+  for (final file in selectedFiles.files) {
+    if (file.uploadedFileID != null) {
+      containsUploadedFile = true;
+    }
+    if (file.localID != null) {
+      containsLocalFile = true;
+    }
+  }
+  final actions = <Widget>[];
+  if (containsUploadedFile && containsLocalFile) {
+    actions.add(
+      CupertinoActionSheetAction(
+        isDestructiveAction: true,
+        onPressed: () async {
+          Navigator.of(context, rootNavigator: true).pop();
+          await deleteFilesOnDeviceOnly(
+            context,
+            selectedFiles.files.toList(),
+          );
+          selectedFiles.clearAll();
+          showToast(context, "Files deleted from device");
+        },
+        child: const Text("Device"),
+      ),
+    );
+    actions.add(
+      CupertinoActionSheetAction(
+        isDestructiveAction: true,
+        onPressed: () async {
+          Navigator.of(context, rootNavigator: true).pop();
+          await deleteFilesFromRemoteOnly(
+            context,
+            selectedFiles.files.toList(),
+          );
+          selectedFiles.clearAll();
+
+          showShortToast(context, "Moved to trash");
+        },
+        child: const Text("ente"),
+      ),
+    );
+    actions.add(
+      CupertinoActionSheetAction(
+        isDestructiveAction: true,
+        onPressed: () async {
+          Navigator.of(context, rootNavigator: true).pop();
+          await deleteFilesFromEverywhere(
+            context,
+            selectedFiles.files.toList(),
+          );
+          selectedFiles.clearAll();
+        },
+        child: const Text("Everywhere"),
+      ),
+    );
+  } else {
+    actions.add(
+      CupertinoActionSheetAction(
+        isDestructiveAction: true,
+        onPressed: () async {
+          Navigator.of(context, rootNavigator: true).pop();
+          await deleteFilesFromEverywhere(
+            context,
+            selectedFiles.files.toList(),
+          );
+          selectedFiles.clearAll();
+        },
+        child: const Text("Delete"),
+      ),
+    );
+  }
+  final action = CupertinoActionSheet(
+    title: Text(
+      "Delete " +
+          count.toString() +
+          " file" +
+          (count == 1 ? "" : "s") +
+          (containsUploadedFile && containsLocalFile ? " from" : "?"),
+    ),
+    actions: actions,
+    cancelButton: CupertinoActionSheetAction(
+      child: const Text("Cancel"),
+      onPressed: () {
+        Navigator.of(context, rootNavigator: true).pop();
+      },
+    ),
+  );
+  showCupertinoModalPopup(
+    context: context,
+    builder: (_) => action,
+    barrierColor: Colors.black.withOpacity(0.75),
+  );
+}

+ 13 - 0
lib/utils/separators_util.dart

@@ -0,0 +1,13 @@
+import 'package:flutter/material.dart';
+
+//This method returns a newly declared list with separators. It will not
+//modify the original list
+List<Widget> addSeparators(List<Widget> listOfWidgets, Widget separator) {
+  final int initialLength = listOfWidgets.length;
+  final listOfWidgetsWithSeparators = <Widget>[];
+  listOfWidgetsWithSeparators.addAll(listOfWidgets);
+  for (var i = 1; i < initialLength; i++) {
+    listOfWidgetsWithSeparators.insert((2 * i) - 1, separator);
+  }
+  return listOfWidgetsWithSeparators;
+}

+ 12 - 0
lib/utils/share_util.dart

@@ -146,3 +146,15 @@ DateTime parseDateFromFileNam1e(String fileName) {
     );
   }
 }
+
+void shareSelected(
+  BuildContext context,
+  GlobalKey shareButtonKey,
+  Set<File> selectedFiles,
+) {
+  share(
+    context,
+    selectedFiles.toList(),
+    shareButtonKey: shareButtonKey,
+  );
+}

+ 28 - 14
pubspec.lock

@@ -49,7 +49,7 @@ packages:
       name: async
       url: "https://pub.dartlang.org"
     source: hosted
-    version: "2.9.0"
+    version: "2.8.2"
   background_fetch:
     dependency: "direct main"
     description:
@@ -98,7 +98,14 @@ packages:
       name: characters
       url: "https://pub.dartlang.org"
     source: hosted
-    version: "1.2.1"
+    version: "1.2.0"
+  charcode:
+    dependency: transitive
+    description:
+      name: charcode
+      url: "https://pub.dartlang.org"
+    source: hosted
+    version: "1.3.1"
   chewie:
     dependency: "direct main"
     description:
@@ -112,7 +119,7 @@ packages:
       name: clock
       url: "https://pub.dartlang.org"
     source: hosted
-    version: "1.1.1"
+    version: "1.1.0"
   collection:
     dependency: "direct main"
     description:
@@ -301,7 +308,7 @@ packages:
       name: fake_async
       url: "https://pub.dartlang.org"
     source: hosted
-    version: "1.3.1"
+    version: "1.3.0"
   fast_base58:
     dependency: "direct main"
     description:
@@ -700,6 +707,13 @@ packages:
       url: "https://pub.dartlang.org"
     source: hosted
     version: "4.7.0"
+  keyboard_visibility:
+    dependency: "direct main"
+    description:
+      name: keyboard_visibility
+      url: "https://pub.dartlang.org"
+    source: hosted
+    version: "0.5.6"
   like_button:
     dependency: "direct main"
     description:
@@ -755,14 +769,14 @@ packages:
       name: matcher
       url: "https://pub.dartlang.org"
     source: hosted
-    version: "0.12.12"
+    version: "0.12.11"
   material_color_utilities:
     dependency: transitive
     description:
       name: material_color_utilities
       url: "https://pub.dartlang.org"
     source: hosted
-    version: "0.1.5"
+    version: "0.1.4"
   media_extension:
     dependency: "direct main"
     description:
@@ -778,7 +792,7 @@ packages:
       name: meta
       url: "https://pub.dartlang.org"
     source: hosted
-    version: "1.8.0"
+    version: "1.7.0"
   mime:
     dependency: transitive
     description:
@@ -906,7 +920,7 @@ packages:
       name: path
       url: "https://pub.dartlang.org"
     source: hosted
-    version: "1.8.2"
+    version: "1.8.1"
   path_drawing:
     dependency: transitive
     description:
@@ -1247,7 +1261,7 @@ packages:
       name: source_span
       url: "https://pub.dartlang.org"
     source: hosted
-    version: "1.9.0"
+    version: "1.8.2"
   sprintf:
     dependency: transitive
     description:
@@ -1303,7 +1317,7 @@ packages:
       name: string_scanner
       url: "https://pub.dartlang.org"
     source: hosted
-    version: "1.1.1"
+    version: "1.1.0"
   syncfusion_flutter_core:
     dependency: "direct main"
     description:
@@ -1331,28 +1345,28 @@ packages:
       name: term_glyph
       url: "https://pub.dartlang.org"
     source: hosted
-    version: "1.2.1"
+    version: "1.2.0"
   test:
     dependency: "direct dev"
     description:
       name: test
       url: "https://pub.dartlang.org"
     source: hosted
-    version: "1.21.4"
+    version: "1.21.1"
   test_api:
     dependency: transitive
     description:
       name: test_api
       url: "https://pub.dartlang.org"
     source: hosted
-    version: "0.4.12"
+    version: "0.4.9"
   test_core:
     dependency: transitive
     description:
       name: test_core
       url: "https://pub.dartlang.org"
     source: hosted
-    version: "0.4.16"
+    version: "0.4.13"
   timezone:
     dependency: transitive
     description:

+ 1 - 1
pubspec.yaml

@@ -12,7 +12,7 @@ description: ente photos application
 # Read more about iOS versioning at
 # https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
 
-version: 0.6.72+392
+version: 0.6.74+394
 
 environment:
   sdk: '>=2.17.0 <3.0.0'