Browse Source

Merge branch 'master' into redesign-with-components

ashilkn 2 years ago
parent
commit
e244bc1e76
32 changed files with 722 additions and 368 deletions
  1. 1 8
      ios/Podfile.lock
  2. 0 2
      ios/Runner.xcodeproj/project.pbxproj
  3. 3 0
      lib/core/constants.dart
  4. 24 0
      lib/models/file.dart
  5. 39 0
      lib/services/billing_service.dart
  6. 27 0
      lib/services/update_service.dart
  7. 200 0
      lib/ui/advanced_settings_screen.dart
  8. 3 3
      lib/ui/backup_settings_screen.dart
  9. 32 25
      lib/ui/components/captioned_text_widget.dart
  10. 3 3
      lib/ui/components/menu_item_widget.dart
  11. 29 16
      lib/ui/huge_listview/lazy_loading_gallery.dart
  12. 14 8
      lib/ui/huge_listview/place_holder_widget.dart
  13. 1 31
      lib/ui/payment/stripe_subscription_page.dart
  14. 1 33
      lib/ui/payment/subscription_page.dart
  15. 11 16
      lib/ui/settings/about_section_widget.dart
  16. 70 0
      lib/ui/settings/account_section_widget.dart
  17. 7 21
      lib/ui/settings/backup_section_widget.dart
  18. 0 101
      lib/ui/settings/danger_section_widget.dart
  19. 97 0
      lib/ui/settings/general_section_widget.dart
  20. 4 4
      lib/ui/settings/security_section_widget.dart
  21. 26 17
      lib/ui/settings/social_section_widget.dart
  22. 6 0
      lib/ui/settings/storage_card_widget.dart
  23. 9 3
      lib/ui/settings/support_section_widget.dart
  24. 3 7
      lib/ui/settings_page.dart
  25. 6 1
      lib/ui/viewer/file/thumbnail_widget.dart
  26. 5 9
      lib/ui/viewer/gallery/gallery.dart
  27. 3 9
      lib/ui/viewer/gallery/gallery_app_bar_widget.dart
  28. 34 7
      lib/utils/date_time_util.dart
  29. 15 0
      lib/utils/local_settings.dart
  30. 30 15
      lib/utils/share_util.dart
  31. 14 28
      pubspec.lock
  32. 5 1
      test/utils/date_time_util_test.dart

+ 1 - 8
ios/Podfile.lock

@@ -102,9 +102,6 @@ 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)
@@ -196,7 +193,6 @@ 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`)
@@ -275,8 +271,6 @@ 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:
@@ -327,7 +321,7 @@ SPEC CHECKSUMS:
   FirebaseInstallations: 0a115432c4e223c5ab20b0dbbe4cbefa793a0e8e
   FirebaseMessaging: 732623518591384f61c287e3d8f65294beb7ffb3
   fk_user_agent: 1f47ec39291e8372b1d692b50084b0d54103c545
-  Flutter: 50d75fe2f02b26cc09d224853bb45737f8b3214a
+  Flutter: f04841e97a9d0b0a8025694d0796dd46242b2854
   flutter_email_sender: 02d7443217d8c41483223627972bfdc09f74276b
   flutter_image_compress: 5a5e9aee05b6553048b8df1c3bc456d0afaac433
   flutter_inappwebview: bfd58618f49dc62f2676de690fc6dcda1d6c3721
@@ -342,7 +336,6 @@ SPEC CHECKSUMS:
   GoogleUtilities: 1d20a6ad97ef46f67bbdec158ce00563a671ebb7
   image_editor: eab82a302a6623a866da5145b7c4c0ee8a4ffbb4
   in_app_purchase_storekit: d7fcf4646136ec258e237872755da8ea6c1b6096
-  keyboard_visibility: 96a24de806fe6823c3ad956c01ba2ec6d056616f
   libwebp: 60305b2e989864154bd9be3d772730f08fc6a59c
   local_auth: 1740f55d7af0a2e2a8684ce225fe79d8931e808c
   Mantle: c5aa8794a29a022dfbbfc9799af95f477a69b62d

+ 0 - 2
ios/Runner.xcodeproj/project.pbxproj

@@ -287,7 +287,6 @@
 				"${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",
@@ -342,7 +341,6 @@
 				"${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",

+ 3 - 0
lib/core/constants.dart

@@ -15,6 +15,9 @@ const int jan011981Time = 347155200000000;
 const int galleryLoadStartTime = -8000000000000000; // Wednesday, March 6, 1748
 const int galleryLoadEndTime = 9223372036854775807; // 2^63 -1
 const int batchSize = 1000;
+const photoGridSizeDefault = 4;
+const photoGridSizeMin = 2;
+const photoGridSizeMax = 6;
 
 // used to identify which ente file are available in app cache
 // todo: 6Jun22: delete old media identifier after 3 months

+ 24 - 0
lib/models/file.dart

@@ -1,3 +1,5 @@
+import 'dart:io';
+
 import 'package:logging/logging.dart';
 import 'package:path/path.dart';
 import 'package:photo_manager/photo_manager.dart';
@@ -177,13 +179,35 @@ class File extends EnteFile {
         duration = asset.duration;
       }
     }
+    bool hasExifTime = false;
     if (fileType == FileType.image && mediaUploadData.sourceFile != null) {
       final exifTime =
           await getCreationTimeFromEXIF(mediaUploadData.sourceFile!);
       if (exifTime != null) {
+        hasExifTime = true;
         creationTime = exifTime.microsecondsSinceEpoch;
       }
     }
+    // Try to get the timestamp from fileName. In case of iOS, file names are
+    // generic IMG_XXXX, so only parse it on Android devices
+    if (!hasExifTime && Platform.isAndroid && title != null) {
+      final timeFromFileName = parseDateTimeFromFileNameV2(title!);
+      if (timeFromFileName != null) {
+        // only use timeFromFileName if the existing creationTime and
+        // timeFromFilename belongs to different date.
+        // This is done because many times the fileTimeStamp will only give us
+        // the date, not time value but the photo_manager's creation time will
+        // contain the time.
+        final bool useFileTimeStamp = creationTime == null ||
+            !areFromSameDay(
+              creationTime!,
+              timeFromFileName.microsecondsSinceEpoch,
+            );
+        if (useFileTimeStamp) {
+          creationTime = timeFromFileName.microsecondsSinceEpoch;
+        }
+      }
+    }
     hash = mediaUploadData.hashData?.fileHash;
     return metadata;
   }

+ 39 - 0
lib/services/billing_service.dart

@@ -3,6 +3,7 @@
 import 'dart:io';
 
 import 'package:dio/dio.dart';
+import 'package:flutter/material.dart';
 // import 'package:flutter/foundation.dart';
 // import 'package:flutter_inapp_purchase/flutter_inapp_purchase.dart';
 import 'package:in_app_purchase/in_app_purchase.dart';
@@ -11,6 +12,10 @@ import 'package:photos/core/errors.dart';
 import 'package:photos/core/network.dart';
 import 'package:photos/models/billing_plan.dart';
 import 'package:photos/models/subscription.dart';
+import 'package:photos/models/user_details.dart';
+import 'package:photos/services/user_service.dart';
+import 'package:photos/ui/common/web_page.dart';
+import 'package:photos/utils/dialog_util.dart';
 
 const kWebPaymentRedirectUrl = "https://payments.ente.io/frameRedirect";
 const kWebPaymentBaseEndpoint = String.fromEnvironment(
@@ -159,4 +164,38 @@ class BillingService {
   void setIsOnSubscriptionPage(bool isOnSubscriptionPage) {
     _isOnSubscriptionPage = isOnSubscriptionPage;
   }
+
+  Future<void> launchFamilyPortal(
+    BuildContext context,
+    UserDetails userDetails,
+  ) async {
+    if (userDetails.subscription.productID == freeProductID) {
+      await showErrorDialog(
+        context,
+        "Share your storage plan with your family members!",
+        "Customers on paid plans can add up to 5 family members without paying extra. Each member gets their own private space.",
+      );
+      return;
+    }
+    final dialog = createProgressDialog(context, "Please wait...");
+    await dialog.show();
+    try {
+      final String jwtToken = await UserService.instance.getFamiliesToken();
+      final bool familyExist = userDetails.isPartOfFamily();
+      Navigator.of(context).push(
+        MaterialPageRoute(
+          builder: (BuildContext context) {
+            return WebPage(
+              "Family",
+              '$kFamilyPlanManagementUrl?token=$jwtToken&isFamilyCreated=$familyExist',
+            );
+          },
+        ),
+      );
+    } catch (e) {
+      await dialog.hide();
+      showGenericErrorDialog(context);
+    }
+    await dialog.hide();
+  }
 }

+ 27 - 0
lib/services/update_service.dart

@@ -9,6 +9,7 @@ import 'package:photos/core/constants.dart';
 import 'package:photos/core/network.dart';
 import 'package:photos/services/notification_service.dart';
 import 'package:shared_preferences/shared_preferences.dart';
+import 'package:tuple/tuple.dart';
 
 class UpdateService {
   UpdateService._privateConstructor();
@@ -121,6 +122,32 @@ class UpdateService {
     }
     return _packageInfo.packageName.startsWith("io.ente.photos.independent");
   }
+
+  bool isFdroidFlavor() {
+    if (Platform.isIOS) {
+      return false;
+    }
+    return _packageInfo.packageName.startsWith("io.ente.photos.fdroid");
+  }
+
+  // getRateDetails returns details about the place
+  Tuple2<String, String> getRateDetails() {
+    if (isFdroidFlavor() || isIndependentFlavor()) {
+      return const Tuple2(
+        "AlternativeTo",
+        "https://alternativeto.net/software/ente/about/",
+      );
+    }
+    return Platform.isAndroid
+        ? const Tuple2(
+            "play store",
+            "https://play.google.com/store/apps/details?id=io.ente.photos",
+          )
+        : const Tuple2(
+            "app store",
+            "https://apps.apple.com/in/app/ente-photos/id1542026904",
+          );
+  }
 }
 
 class LatestVersionInfo {

+ 200 - 0
lib/ui/advanced_settings_screen.dart

@@ -0,0 +1,200 @@
+import 'package:flutter/cupertino.dart';
+import 'package:flutter/material.dart';
+import 'package:photos/core/constants.dart';
+import 'package:photos/core/event_bus.dart';
+import 'package:photos/events/force_reload_home_gallery_event.dart';
+import 'package:photos/theme/ente_theme.dart';
+import 'package:photos/ui/components/captioned_text_widget.dart';
+import 'package:photos/ui/components/icon_button_widget.dart';
+import 'package:photos/ui/components/menu_item_widget.dart';
+import 'package:photos/ui/components/title_bar_title_widget.dart';
+import 'package:photos/ui/components/title_bar_widget.dart';
+import 'package:photos/utils/local_settings.dart';
+
+class AdvancedSettingsScreen extends StatefulWidget {
+  const AdvancedSettingsScreen({super.key});
+
+  @override
+  State<AdvancedSettingsScreen> createState() => _AdvancedSettingsScreenState();
+}
+
+class _AdvancedSettingsScreenState extends State<AdvancedSettingsScreen> {
+  late int _photoGridSize, _chosenGridSize;
+
+  @override
+  void initState() {
+    _photoGridSize = LocalSettings.instance.getPhotoGridSize();
+    _chosenGridSize = _photoGridSize;
+    super.initState();
+  }
+
+  @override
+  Widget build(BuildContext context) {
+    final colorScheme = getEnteColorScheme(context);
+    return Scaffold(
+      body: CustomScrollView(
+        primary: false,
+        slivers: <Widget>[
+          TitleBarWidget(
+            flexibleSpaceTitle: const TitleBarTitleWidget(
+              title: "Advanced",
+            ),
+            actionIcons: [
+              IconButtonWidget(
+                icon: Icons.close_outlined,
+                iconButtonType: IconButtonType.secondary,
+                onTap: () {
+                  Navigator.pop(context);
+                  Navigator.pop(context);
+                },
+              ),
+            ],
+          ),
+          SliverList(
+            delegate: SliverChildBuilderDelegate(
+              (context, index) {
+                return Padding(
+                  padding: const EdgeInsets.symmetric(horizontal: 16),
+                  child: Padding(
+                    padding: const EdgeInsets.symmetric(vertical: 20),
+                    child: Column(
+                      mainAxisSize: MainAxisSize.min,
+                      children: [
+                        Column(
+                          children: [
+                            GestureDetector(
+                              onTap: () {
+                                _showPhotoGridSizePicker();
+                              },
+                              child: MenuItemWidget(
+                                captionedTextWidget: CaptionedTextWidget(
+                                  title: "Photo grid size",
+                                  subTitle: _photoGridSize.toString(),
+                                ),
+                                menuItemColor: colorScheme.fillFaint,
+                                trailingWidget: Icon(
+                                  Icons.chevron_right,
+                                  color: colorScheme.strokeMuted,
+                                ),
+                                borderRadius: 8,
+                                alignCaptionedTextToLeft: true,
+                                // isBottomBorderRadiusRemoved: true,
+                                isGestureDetectorDisabled: true,
+                              ),
+                            ),
+                          ],
+                        ),
+                      ],
+                    ),
+                  ),
+                );
+              },
+              childCount: 1,
+            ),
+          ),
+        ],
+      ),
+    );
+  }
+
+  Future<void> _showPhotoGridSizePicker() async {
+    final textTheme = getEnteTextTheme(context);
+    final List<Text> options = [];
+    for (int gridSize = photoGridSizeMin;
+        gridSize <= photoGridSizeMax;
+        gridSize++) {
+      options.add(
+        Text(
+          gridSize.toString(),
+          style: textTheme.body,
+        ),
+      );
+    }
+    return showCupertinoModalPopup(
+      context: context,
+      builder: (context) {
+        return Column(
+          mainAxisAlignment: MainAxisAlignment.end,
+          children: <Widget>[
+            Container(
+              decoration: BoxDecoration(
+                color: getEnteColorScheme(context).backgroundElevated2,
+                border: const Border(
+                  bottom: BorderSide(
+                    color: Color(0xff999999),
+                    width: 0.0,
+                  ),
+                ),
+              ),
+              child: Row(
+                mainAxisAlignment: MainAxisAlignment.spaceBetween,
+                children: <Widget>[
+                  CupertinoButton(
+                    onPressed: () {
+                      Navigator.of(context).pop('cancel');
+                    },
+                    padding: const EdgeInsets.symmetric(
+                      horizontal: 8.0,
+                      vertical: 5.0,
+                    ),
+                    child: Text(
+                      'Cancel',
+                      style: textTheme.body,
+                    ),
+                  ),
+                  CupertinoButton(
+                    onPressed: () async {
+                      await LocalSettings.instance
+                          .setPhotoGridSize(_chosenGridSize);
+                      Bus.instance.fire(
+                        ForceReloadHomeGalleryEvent("grid size changed"),
+                      );
+                      _photoGridSize = _chosenGridSize;
+                      setState(() {});
+                      Navigator.of(context).pop('');
+                    },
+                    padding: const EdgeInsets.symmetric(
+                      horizontal: 16.0,
+                      vertical: 2.0,
+                    ),
+                    child: Text(
+                      'Confirm',
+                      style: textTheme.body,
+                    ),
+                  )
+                ],
+              ),
+            ),
+            Container(
+              height: 220.0,
+              color: const Color(0xfff7f7f7),
+              child: CupertinoPicker(
+                backgroundColor: getEnteColorScheme(context).backgroundElevated,
+                onSelectedItemChanged: (index) {
+                  _chosenGridSize = _getPhotoGridSizeFromIndex(index);
+                  setState(() {});
+                },
+                scrollController: FixedExtentScrollController(
+                  initialItem: _getIndexFromPhotoGridSize(_chosenGridSize),
+                ),
+                magnification: 1.3,
+                useMagnifier: true,
+                itemExtent: 25,
+                diameterRatio: 1,
+                children: options,
+              ),
+            )
+          ],
+        );
+      },
+    );
+  }
+
+  int _getPhotoGridSizeFromIndex(int index) {
+    return index + 2;
+  }
+
+  int _getIndexFromPhotoGridSize(int gridSize) {
+    return gridSize - 2;
+  }
+}

+ 3 - 3
lib/ui/backup_settings_screen.dart

@@ -54,7 +54,7 @@ class BackupSettingsScreen extends StatelessWidget {
                                 title: "Backup over mobile data",
                               ),
                               menuItemColor: colorScheme.fillFaint,
-                              trailingSwitch: ToggleSwitchWidget(
+                              trailingWidget: ToggleSwitchWidget(
                                 value: () => Configuration.instance
                                     .shouldBackupOverMobileData(),
                                 onChanged: () async {
@@ -79,7 +79,7 @@ class BackupSettingsScreen extends StatelessWidget {
                                 title: "Backup videos",
                               ),
                               menuItemColor: colorScheme.fillFaint,
-                              trailingSwitch: ToggleSwitchWidget(
+                              trailingWidget: ToggleSwitchWidget(
                                 value: () =>
                                     Configuration.instance.shouldBackupVideos(),
                                 onChanged: () => Configuration.instance
@@ -104,7 +104,7 @@ class BackupSettingsScreen extends StatelessWidget {
                                       title: "Disable auto lock",
                                     ),
                                     menuItemColor: colorScheme.fillFaint,
-                                    trailingSwitch: ToggleSwitchWidget(
+                                    trailingWidget: ToggleSwitchWidget(
                                       value: () => Configuration.instance
                                           .shouldKeepDeviceAwake(),
                                       onChanged: () {

+ 32 - 25
lib/ui/components/captioned_text_widget.dart

@@ -21,35 +21,42 @@ class CaptionedTextWidget extends StatelessWidget {
     final enteColorScheme = Theme.of(context).colorScheme.enteTheme.colorScheme;
     final enteTextTheme = Theme.of(context).colorScheme.enteTheme.textTheme;
 
+    final List<Widget> children = [
+      Flexible(
+        child: Text(
+          title,
+          style: textStyle ??
+              (makeTextBold
+                  ? enteTextTheme.bodyBold.copyWith(color: textColor)
+                  : enteTextTheme.body.copyWith(color: textColor)),
+        ),
+      ),
+    ];
+    if (subTitle != null) {
+      children.add(const SizedBox(width: 4));
+      children.add(
+        Text(
+          '\u2022',
+          style: enteTextTheme.small.copyWith(
+            color: enteColorScheme.textMuted,
+          ),
+        ),
+      );
+      children.add(const SizedBox(width: 4));
+      children.add(
+        Text(
+          subTitle!,
+          style: enteTextTheme.small.copyWith(
+            color: enteColorScheme.textMuted,
+          ),
+        ),
+      );
+    }
     return Flexible(
       child: Padding(
         padding: const EdgeInsets.symmetric(vertical: 14, horizontal: 2),
         child: Row(
-          children: [
-            Flexible(
-              child: RichText(
-                text: TextSpan(
-                  style: textStyle ??
-                      (makeTextBold
-                          ? enteTextTheme.bodyBold.copyWith(color: textColor)
-                          : enteTextTheme.body.copyWith(color: textColor)),
-                  children: [
-                    TextSpan(
-                      text: title,
-                    ),
-                    subTitle != null
-                        ? TextSpan(
-                            text: ' \u2022 $subTitle',
-                            style: enteTextTheme.small.copyWith(
-                              color: enteColorScheme.textMuted,
-                            ),
-                          )
-                        : const TextSpan(text: ''),
-                  ],
-                ),
-              ),
-            )
-          ],
+          children: children,
         ),
       ),
     );

+ 3 - 3
lib/ui/components/menu_item_widget.dart

@@ -14,7 +14,7 @@ class MenuItemWidget extends StatefulWidget {
   /// trailing icon can be passed without size as default size set by
   /// flutter is what this component expects
   final IconData? trailingIcon;
-  final Widget? trailingSwitch;
+  final Widget? trailingWidget;
   final bool trailingIconIsMuted;
   final VoidCallback? onTap;
   final VoidCallback? onDoubleTap;
@@ -34,7 +34,7 @@ class MenuItemWidget extends StatefulWidget {
     this.leadingIcon,
     this.leadingIconColor,
     this.trailingIcon,
-    this.trailingSwitch,
+    this.trailingWidget,
     this.trailingIconIsMuted = false,
     this.onTap,
     this.onDoubleTap,
@@ -163,7 +163,7 @@ class _MenuItemWidgetState extends State<MenuItemWidget> {
                           ? enteColorScheme.strokeMuted
                           : null,
                     )
-                  : widget.trailingSwitch ?? const SizedBox.shrink(),
+                  : widget.trailingWidget ?? const SizedBox.shrink(),
         ],
       ),
     );

+ 29 - 16
lib/ui/huge_listview/lazy_loading_gallery.dart

@@ -5,7 +5,6 @@ import 'dart:math';
 
 import 'package:flutter/foundation.dart';
 import 'package:flutter/material.dart';
-import 'package:flutter/rendering.dart';
 import 'package:flutter/services.dart';
 import 'package:logging/logging.dart';
 import 'package:photos/core/constants.dart';
@@ -33,6 +32,7 @@ class LazyLoadingGallery extends StatefulWidget {
   final String tag;
   final String logTag;
   final Stream<int> currentIndexStream;
+  final int photoGirdSize;
 
   LazyLoadingGallery(
     this.files,
@@ -44,6 +44,7 @@ class LazyLoadingGallery extends StatefulWidget {
     this.tag,
     this.currentIndexStream, {
     this.logTag = "",
+    this.photoGirdSize = photoGridSizeDefault,
     Key key,
   }) : super(key: key ?? UniqueKey());
 
@@ -162,7 +163,9 @@ class _LazyLoadingGalleryState extends State<LazyLoadingGallery> {
     _reloadEventSubscription.cancel();
     _currentIndexSubscription.cancel();
     widget.selectedFiles.removeListener(_selectedFilesListener);
-
+    _toggleSelectAllFromDay.dispose();
+    _showSelectAllButton.dispose();
+    _areAllFromDaySelected.dispose();
     super.dispose();
   }
 
@@ -185,12 +188,10 @@ class _LazyLoadingGalleryState extends State<LazyLoadingGallery> {
         Row(
           mainAxisAlignment: MainAxisAlignment.spaceBetween,
           children: [
-            Padding(
-              padding: const EdgeInsets.all(12),
-              child: getDayWidget(
-                context,
-                _files[0].creationTime,
-              ),
+            getDayWidget(
+              context,
+              _files[0].creationTime,
+              widget.photoGirdSize,
             ),
             ValueListenableBuilder(
               valueListenable: _showSelectAllButton,
@@ -230,7 +231,12 @@ class _LazyLoadingGalleryState extends State<LazyLoadingGallery> {
             )
           ],
         ),
-        _shouldRender ? _getGallery() : PlaceHolderWidget(_files.length),
+        _shouldRender
+            ? _getGallery()
+            : PlaceHolderWidget(
+                _files.length,
+                widget.photoGirdSize,
+              ),
       ],
     );
   }
@@ -251,6 +257,7 @@ class _LazyLoadingGalleryState extends State<LazyLoadingGallery> {
           _files.length > kRecycleLimit,
           _toggleSelectAllFromDay,
           _areAllFromDaySelected,
+          widget.photoGirdSize,
         ),
       );
     }
@@ -278,6 +285,7 @@ class LazyLoadingGridView extends StatefulWidget {
   final bool shouldRecycle;
   final ValueNotifier toggleSelectAllFromDay;
   final ValueNotifier areAllFilesSelected;
+  final int photoGridSize;
 
   LazyLoadingGridView(
     this.tag,
@@ -287,7 +295,8 @@ class LazyLoadingGridView extends StatefulWidget {
     this.shouldRender,
     this.shouldRecycle,
     this.toggleSelectAllFromDay,
-    this.areAllFilesSelected, {
+    this.areAllFilesSelected,
+    this.photoGridSize, {
     Key key,
   }) : super(key: key ?? UniqueKey());
 
@@ -352,7 +361,7 @@ class _LazyLoadingGridViewState extends State<LazyLoadingGridView> {
       },
       child: _shouldRender
           ? _getGridView()
-          : PlaceHolderWidget(widget.filesInDay.length),
+          : PlaceHolderWidget(widget.filesInDay.length, widget.photoGridSize),
     );
   }
 
@@ -367,7 +376,8 @@ class _LazyLoadingGridViewState extends State<LazyLoadingGridView> {
             });
           }
         },
-        child: PlaceHolderWidget(widget.filesInDay.length),
+        child:
+            PlaceHolderWidget(widget.filesInDay.length, widget.photoGridSize),
       );
     } else {
       return _getGridView();
@@ -377,16 +387,16 @@ class _LazyLoadingGridViewState extends State<LazyLoadingGridView> {
   Widget _getGridView() {
     return GridView.builder(
       shrinkWrap: true,
-      physics:
-          const NeverScrollableScrollPhysics(), // to disable GridView's scrolling
+      physics: const NeverScrollableScrollPhysics(),
+      // to disable GridView's scrolling
       itemBuilder: (context, index) {
         return _buildFile(context, widget.filesInDay[index]);
       },
       itemCount: widget.filesInDay.length,
-      gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
+      gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
         crossAxisSpacing: 2,
         mainAxisSpacing: 2,
-        crossAxisCount: 4,
+        crossAxisCount: widget.photoGridSize,
       ),
       padding: const EdgeInsets.all(0),
     );
@@ -424,6 +434,9 @@ class _LazyLoadingGridViewState extends State<LazyLoadingGridView> {
                   serverLoadDeferDuration: thumbnailServerLoadDeferDuration,
                   shouldShowLivePhotoOverlay: true,
                   key: Key(widget.tag + file.tag),
+                  thumbnailSize: widget.photoGridSize < photoGridSizeDefault
+                      ? thumbnailLargeSize
+                      : thumbnailSmallSize,
                 ),
               ),
             ),

+ 14 - 8
lib/ui/huge_listview/place_holder_widget.dart

@@ -4,18 +4,20 @@ import 'package:flutter/material.dart';
 
 class PlaceHolderWidget extends StatelessWidget {
   const PlaceHolderWidget(
-    this.count, {
+    this.count,
+    this.columns, {
     Key key,
   }) : super(key: key);
 
-  final int count;
+  final int count, columns;
 
-  static final _gridViewCache = <int, GridView>{};
+  static final _gridViewCache = <String, GridView>{};
 
   @override
   Widget build(BuildContext context) {
-    if (!_gridViewCache.containsKey(count)) {
-      _gridViewCache[count] = GridView.builder(
+    final key = _getCacheKey(count, columns);
+    if (!_gridViewCache.containsKey(key)) {
+      _gridViewCache[key] = GridView.builder(
         padding: const EdgeInsets.all(0),
         shrinkWrap: true,
         physics: const NeverScrollableScrollPhysics(),
@@ -26,11 +28,15 @@ class PlaceHolderWidget extends StatelessWidget {
           );
         },
         itemCount: count,
-        gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
-          crossAxisCount: 4,
+        gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
+          crossAxisCount: columns,
         ),
       );
     }
-    return _gridViewCache[count];
+    return _gridViewCache[key];
+  }
+
+  String _getCacheKey(int totalCount, int columns) {
+    return totalCount.toString() + ":" + columns.toString();
   }
 }

+ 1 - 31
lib/ui/payment/stripe_subscription_page.dart

@@ -278,7 +278,7 @@ class _StripeSubscriptionPageState extends State<StripeSubscriptionPage> {
           alignment: Alignment.topCenter,
           child: GestureDetector(
             onTap: () async {
-              await _launchFamilyPortal();
+              _billingService.launchFamilyPortal(context, _userDetails);
             },
             child: Container(
               padding: const EdgeInsets.fromLTRB(40, 0, 40, 80),
@@ -327,36 +327,6 @@ class _StripeSubscriptionPageState extends State<StripeSubscriptionPage> {
     await _dialog.hide();
   }
 
-  Future<void> _launchFamilyPortal() async {
-    if (_userDetails.subscription.productID == freeProductID) {
-      await showErrorDialog(
-        context,
-        "Now you can share your storage plan with your family members!",
-        "Customers on paid plans can add up to 5 family members without paying extra. Each member gets their own private space.",
-      );
-      return;
-    }
-    await _dialog.show();
-    try {
-      final String jwtToken = await _userService.getFamiliesToken();
-      final bool familyExist = _userDetails.isPartOfFamily();
-      Navigator.of(context).push(
-        MaterialPageRoute(
-          builder: (BuildContext context) {
-            return WebPage(
-              "Family",
-              '$kFamilyPlanManagementUrl?token=$jwtToken&isFamilyCreated=$familyExist',
-            );
-          },
-        ),
-      ).then((value) => onWebPaymentGoBack);
-    } catch (e) {
-      await _dialog.hide();
-      showGenericErrorDialog(context);
-    }
-    await _dialog.hide();
-  }
-
   Widget _stripeRenewOrCancelButton() {
     final bool isRenewCancelled =
         _currentSubscription.attributes?.isCancelled ?? false;

+ 1 - 33
lib/ui/payment/subscription_page.dart

@@ -16,7 +16,6 @@ import 'package:photos/services/billing_service.dart';
 import 'package:photos/services/user_service.dart';
 import 'package:photos/ui/common/loading_widget.dart';
 import 'package:photos/ui/common/progress_dialog.dart';
-import 'package:photos/ui/common/web_page.dart';
 import 'package:photos/ui/payment/child_subscription_widget.dart';
 import 'package:photos/ui/payment/skip_subscription_widget.dart';
 import 'package:photos/ui/payment/subscription_common_widgets.dart';
@@ -286,7 +285,7 @@ class _SubscriptionPageState extends State<SubscriptionPage> {
           alignment: Alignment.topCenter,
           child: GestureDetector(
             onTap: () async {
-              _launchFamilyPortal();
+              _billingService.launchFamilyPortal(context, _userDetails);
             },
             child: Container(
               padding: const EdgeInsets.fromLTRB(40, 0, 40, 80),
@@ -465,35 +464,4 @@ class _SubscriptionPageState extends State<SubscriptionPage> {
       ),
     );
   }
-
-  // todo: refactor manage family in common widget
-  Future<void> _launchFamilyPortal() async {
-    if (_userDetails.subscription.productID == freeProductID) {
-      await showErrorDialog(
-        context,
-        "Share your storage plan with your family members!",
-        "Customers on paid plans can add up to 5 family members without paying extra. Each member gets their own private space.",
-      );
-      return;
-    }
-    await _dialog.show();
-    try {
-      final String jwtToken = await _userService.getFamiliesToken();
-      final bool familyExist = _userDetails.isPartOfFamily();
-      Navigator.of(context).push(
-        MaterialPageRoute(
-          builder: (BuildContext context) {
-            return WebPage(
-              "Family",
-              '$kFamilyPlanManagementUrl?token=$jwtToken&isFamilyCreated=$familyExist',
-            );
-          },
-        ),
-      );
-    } catch (e) {
-      await _dialog.hide();
-      showGenericErrorDialog(context);
-    }
-    await _dialog.hide();
-  }
 }

+ 11 - 16
lib/ui/settings/about_section_widget.dart

@@ -28,25 +28,10 @@ class AboutSectionWidget extends StatelessWidget {
   Widget _getSectionOptions(BuildContext context) {
     return Column(
       children: [
-        sectionOptionSpacing,
-        const AboutMenuItemWidget(
-          title: "FAQ",
-          url: "https://ente.io/faq",
-        ),
-        sectionOptionSpacing,
-        const AboutMenuItemWidget(
-          title: "Terms",
-          url: "https://ente.io/terms",
-        ),
-        sectionOptionSpacing,
-        const AboutMenuItemWidget(
-          title: "Privacy",
-          url: "https://ente.io/privacy",
-        ),
         sectionOptionSpacing,
         MenuItemWidget(
           captionedTextWidget: const CaptionedTextWidget(
-            title: "Source code",
+            title: "We are open source!",
           ),
           pressedColor: getEnteColorScheme(context).fillFaint,
           trailingIcon: Icons.chevron_right_outlined,
@@ -56,6 +41,16 @@ class AboutSectionWidget extends StatelessWidget {
           },
         ),
         sectionOptionSpacing,
+        const AboutMenuItemWidget(
+          title: "Privacy",
+          url: "https://ente.io/privacy",
+        ),
+        sectionOptionSpacing,
+        const AboutMenuItemWidget(
+          title: "Terms",
+          url: "https://ente.io/terms",
+        ),
+        sectionOptionSpacing,
         UpdateService.instance.isIndependent()
             ? Column(
                 children: [

+ 70 - 0
lib/ui/settings/account_section_widget.dart

@@ -4,10 +4,12 @@ import 'dart:async';
 
 import 'package:flutter/material.dart';
 import 'package:flutter_sodium/flutter_sodium.dart';
+import 'package:photos/ente_theme_data.dart';
 import 'package:photos/services/local_authentication_service.dart';
 import 'package:photos/services/user_service.dart';
 import 'package:photos/theme/ente_theme.dart';
 import 'package:photos/ui/account/change_email_dialog.dart';
+import 'package:photos/ui/account/delete_account_page.dart';
 import 'package:photos/ui/account/password_entry_page.dart';
 import 'package:photos/ui/account/recovery_key_page.dart';
 import 'package:photos/ui/components/captioned_text_widget.dart';
@@ -122,6 +124,30 @@ class AccountSectionWidget extends StatelessWidget {
           },
         ),
         sectionOptionSpacing,
+        MenuItemWidget(
+          captionedTextWidget: const CaptionedTextWidget(
+            title: "Logout",
+          ),
+          pressedColor: getEnteColorScheme(context).fillFaint,
+          trailingIcon: Icons.chevron_right_outlined,
+          trailingIconIsMuted: true,
+          onTap: () {
+            _onLogoutTapped(context);
+          },
+        ),
+        sectionOptionSpacing,
+        MenuItemWidget(
+          captionedTextWidget: const CaptionedTextWidget(
+            title: "Delete account",
+          ),
+          pressedColor: getEnteColorScheme(context).fillFaint,
+          trailingIcon: Icons.chevron_right_outlined,
+          trailingIconIsMuted: true,
+          onTap: () {
+            routeToPage(context, const DeleteAccountPage());
+          },
+        ),
+        sectionOptionSpacing,
       ],
     );
   }
@@ -131,4 +157,48 @@ class AccountSectionWidget extends StatelessWidget {
       await UserService.instance.getOrCreateRecoveryKey(context),
     );
   }
+
+    Future<void> _onLogoutTapped(BuildContext context) async {
+    final AlertDialog alert = AlertDialog(
+      title: const Text(
+        "Logout",
+        style: TextStyle(
+          color: Colors.red,
+        ),
+      ),
+      content: const Text("Are you sure you want to logout?"),
+      actions: [
+        TextButton(
+          child: const Text(
+            "Yes, logout",
+            style: TextStyle(
+              color: Colors.red,
+            ),
+          ),
+          onPressed: () async {
+            Navigator.of(context, rootNavigator: true).pop('dialog');
+            await UserService.instance.logout(context);
+          },
+        ),
+        TextButton(
+          child: Text(
+            "No",
+            style: TextStyle(
+              color: Theme.of(context).colorScheme.greenAlternative,
+            ),
+          ),
+          onPressed: () {
+            Navigator.of(context, rootNavigator: true).pop('dialog');
+          },
+        ),
+      ],
+    );
+
+    await showDialog(
+      context: context,
+      builder: (BuildContext context) {
+        return alert;
+      },
+    );
+  }
 }

+ 7 - 21
lib/ui/settings/backup_section_widget.dart

@@ -8,6 +8,7 @@ import 'package:photos/models/backup_status.dart';
 import 'package:photos/models/duplicate_files.dart';
 import 'package:photos/services/deduplication_service.dart';
 import 'package:photos/services/sync_service.dart';
+import 'package:photos/services/update_service.dart';
 import 'package:photos/theme/ente_theme.dart';
 import 'package:photos/ui/backup_folder_selection_page.dart';
 import 'package:photos/ui/backup_settings_screen.dart';
@@ -81,7 +82,7 @@ class BackupSectionWidgetState extends State<BackupSectionWidget> {
       [
         MenuItemWidget(
           captionedTextWidget: const CaptionedTextWidget(
-            title: "Free up space",
+            title: "Free up device space",
           ),
           pressedColor: getEnteColorScheme(context).fillFaint,
           trailingIcon: Icons.chevron_right_outlined,
@@ -117,7 +118,7 @@ class BackupSectionWidgetState extends State<BackupSectionWidget> {
         sectionOptionSpacing,
         MenuItemWidget(
           captionedTextWidget: const CaptionedTextWidget(
-            title: "Deduplicate files",
+            title: "Remove duplicates",
           ),
           pressedColor: getEnteColorScheme(context).fillFaint,
           trailingIcon: Icons.chevron_right_outlined,
@@ -175,16 +176,8 @@ class BackupSectionWidgetState extends State<BackupSectionWidget> {
           ),
           onPressed: () {
             Navigator.of(context, rootNavigator: true).pop('dialog');
-            // TODO: Replace with https://pub.dev/packages/in_app_review
-            if (Platform.isAndroid) {
-              launchUrlString(
-                "https://play.google.com/store/apps/details?id=io.ente.photos",
-              );
-            } else {
-              launchUrlString(
-                "https://apps.apple.com/in/app/ente-photos/id1542026904",
-              );
-            }
+            final url = UpdateService.instance.getRateDetails().item2;
+            launchUrlString(url);
           },
         ),
         TextButton(
@@ -238,15 +231,8 @@ class BackupSectionWidgetState extends State<BackupSectionWidget> {
           onPressed: () {
             Navigator.of(context, rootNavigator: true).pop('dialog');
             // TODO: Replace with https://pub.dev/packages/in_app_review
-            if (Platform.isAndroid) {
-              launchUrlString(
-                "https://play.google.com/store/apps/details?id=io.ente.photos",
-              );
-            } else {
-              launchUrlString(
-                "https://apps.apple.com/in/app/ente-photos/id1542026904",
-              );
-            }
+            final url = UpdateService.instance.getRateDetails().item2;
+            launchUrlString(url);
           },
         ),
         TextButton(

+ 0 - 101
lib/ui/settings/danger_section_widget.dart

@@ -1,101 +0,0 @@
-// @dart=2.9
-
-import 'package:flutter/material.dart';
-import 'package:photos/ente_theme_data.dart';
-import 'package:photos/services/user_service.dart';
-import 'package:photos/theme/ente_theme.dart';
-import 'package:photos/ui/account/delete_account_page.dart';
-import 'package:photos/ui/components/captioned_text_widget.dart';
-import 'package:photos/ui/components/expandable_menu_item_widget.dart';
-import 'package:photos/ui/components/menu_item_widget.dart';
-import 'package:photos/ui/settings/common_settings.dart';
-import 'package:photos/utils/navigation_util.dart';
-
-class DangerSectionWidget extends StatelessWidget {
-  const DangerSectionWidget({Key key}) : super(key: key);
-
-  @override
-  Widget build(BuildContext context) {
-    return ExpandableMenuItemWidget(
-      title: "Exit",
-      selectionOptionsWidget: _getSectionOptions(context),
-      leadingIcon: Icons.logout_outlined,
-    );
-  }
-
-  Widget _getSectionOptions(BuildContext context) {
-    return Column(
-      children: [
-        sectionOptionSpacing,
-        MenuItemWidget(
-          captionedTextWidget: const CaptionedTextWidget(
-            title: "Logout",
-          ),
-          pressedColor: getEnteColorScheme(context).fillFaint,
-          trailingIcon: Icons.chevron_right_outlined,
-          trailingIconIsMuted: true,
-          onTap: () {
-            _onLogoutTapped(context);
-          },
-        ),
-        sectionOptionSpacing,
-        MenuItemWidget(
-          captionedTextWidget: const CaptionedTextWidget(
-            title: "Delete account",
-          ),
-          pressedColor: getEnteColorScheme(context).fillFaint,
-          trailingIcon: Icons.chevron_right_outlined,
-          trailingIconIsMuted: true,
-          onTap: () {
-            routeToPage(context, const DeleteAccountPage());
-          },
-        ),
-        sectionOptionSpacing,
-      ],
-    );
-  }
-
-  Future<void> _onLogoutTapped(BuildContext context) async {
-    final AlertDialog alert = AlertDialog(
-      title: const Text(
-        "Logout",
-        style: TextStyle(
-          color: Colors.red,
-        ),
-      ),
-      content: const Text("Are you sure you want to logout?"),
-      actions: [
-        TextButton(
-          child: const Text(
-            "Yes, logout",
-            style: TextStyle(
-              color: Colors.red,
-            ),
-          ),
-          onPressed: () async {
-            Navigator.of(context, rootNavigator: true).pop('dialog');
-            await UserService.instance.logout(context);
-          },
-        ),
-        TextButton(
-          child: Text(
-            "No",
-            style: TextStyle(
-              color: Theme.of(context).colorScheme.greenAlternative,
-            ),
-          ),
-          onPressed: () {
-            Navigator.of(context, rootNavigator: true).pop('dialog');
-          },
-        ),
-      ],
-    );
-
-    await showDialog(
-      context: context,
-      builder: (BuildContext context) {
-        return alert;
-      },
-    );
-  }
-}

+ 97 - 0
lib/ui/settings/general_section_widget.dart

@@ -0,0 +1,97 @@
+// @dart=2.9
+
+import 'package:flutter/material.dart';
+import 'package:photos/services/billing_service.dart';
+import 'package:photos/services/user_service.dart';
+import 'package:photos/theme/ente_theme.dart';
+import 'package:photos/ui/advanced_settings_screen.dart';
+import 'package:photos/ui/components/captioned_text_widget.dart';
+import 'package:photos/ui/components/expandable_menu_item_widget.dart';
+import 'package:photos/ui/components/menu_item_widget.dart';
+import 'package:photos/ui/payment/subscription.dart';
+import 'package:photos/ui/settings/common_settings.dart';
+import 'package:photos/utils/dialog_util.dart';
+import 'package:photos/utils/navigation_util.dart';
+
+class GeneralSectionWidget extends StatelessWidget {
+  const GeneralSectionWidget({Key key}) : super(key: key);
+
+  @override
+  Widget build(BuildContext context) {
+    return ExpandableMenuItemWidget(
+      title: "General",
+      selectionOptionsWidget: _getSectionOptions(context),
+      leadingIcon: Icons.graphic_eq,
+    );
+  }
+
+  Widget _getSectionOptions(BuildContext context) {
+    return Column(
+      children: [
+        sectionOptionSpacing,
+        MenuItemWidget(
+          captionedTextWidget: const CaptionedTextWidget(
+            title: "Manage subscription",
+          ),
+          pressedColor: getEnteColorScheme(context).fillFaint,
+          trailingIcon: Icons.chevron_right_outlined,
+          trailingIconIsMuted: true,
+          onTap: () {
+            _onManageSubscriptionTapped(context);
+          },
+        ),
+        sectionOptionSpacing,
+        MenuItemWidget(
+          captionedTextWidget: const CaptionedTextWidget(
+            title: "Family plans",
+          ),
+          pressedColor: getEnteColorScheme(context).fillFaint,
+          trailingIcon: Icons.chevron_right_outlined,
+          trailingIconIsMuted: true,
+          onTap: () {
+            _onFamilyPlansTapped(context);
+          },
+        ),
+        sectionOptionSpacing,
+        MenuItemWidget(
+          captionedTextWidget: const CaptionedTextWidget(
+            title: "Advanced",
+          ),
+          pressedColor: getEnteColorScheme(context).fillFaint,
+          trailingIcon: Icons.chevron_right_outlined,
+          trailingIconIsMuted: true,
+          onTap: () {
+            _onAdvancedTapped(context);
+          },
+        ),
+        sectionOptionSpacing,
+      ],
+    );
+  }
+
+  void _onManageSubscriptionTapped(BuildContext context) {
+    Navigator.of(context).push(
+      MaterialPageRoute(
+        builder: (BuildContext context) {
+          return getSubscriptionPage();
+        },
+      ),
+    );
+  }
+
+  Future<void> _onFamilyPlansTapped(BuildContext context) async {
+    final dialog = createProgressDialog(context, "Please wait...");
+    await dialog.show();
+    final userDetails =
+        await UserService.instance.getUserDetailsV2(memoryCount: false);
+    await dialog.hide();
+    BillingService.instance.launchFamilyPortal(context, userDetails);
+  }
+
+  void _onAdvancedTapped(BuildContext context) {
+    routeToPage(
+      context,
+      const AdvancedSettingsScreen(),
+    );
+  }
+}

+ 4 - 4
lib/ui/settings/security_section_widget.dart

@@ -68,7 +68,7 @@ class _SecuritySectionWidgetState extends State<SecuritySectionWidget> {
             captionedTextWidget: const CaptionedTextWidget(
               title: "Two-factor",
             ),
-            trailingSwitch: ToggleSwitchWidget(
+            trailingWidget: ToggleSwitchWidget(
               value: () => UserService.instance.hasEnabledTwoFactor(),
               onChanged: () async {
                 final hasAuthenticated = await LocalAuthenticationService
@@ -101,7 +101,7 @@ class _SecuritySectionWidgetState extends State<SecuritySectionWidget> {
         captionedTextWidget: const CaptionedTextWidget(
           title: "Lockscreen",
         ),
-        trailingSwitch: ToggleSwitchWidget(
+        trailingWidget: ToggleSwitchWidget(
           value: () => _config.shouldShowLockScreen(),
           onChanged: () async {
             await LocalAuthenticationService.instance
@@ -123,7 +123,7 @@ class _SecuritySectionWidgetState extends State<SecuritySectionWidget> {
             captionedTextWidget: const CaptionedTextWidget(
               title: "Hide from recents",
             ),
-            trailingSwitch: ToggleSwitchWidget(
+            trailingWidget: ToggleSwitchWidget(
               value: () => _config.shouldHideFromRecents(),
               onChanged: _hideFromRecentsOnChanged,
             ),
@@ -135,7 +135,7 @@ class _SecuritySectionWidgetState extends State<SecuritySectionWidget> {
     children.addAll([
       MenuItemWidget(
         captionedTextWidget: const CaptionedTextWidget(
-          title: "Active sessions",
+          title: "View active sessions",
         ),
         pressedColor: getEnteColorScheme(context).fillFaint,
         trailingIcon: Icons.chevron_right_outlined,

+ 26 - 17
lib/ui/settings/social_section_widget.dart

@@ -1,7 +1,5 @@
 // @dart=2.9
 
-import 'dart:io';
-
 import 'package:flutter/material.dart';
 import 'package:photos/services/update_service.dart';
 import 'package:photos/theme/ente_theme.dart';
@@ -24,28 +22,39 @@ class SocialSectionWidget extends StatelessWidget {
   }
 
   Widget _getSectionOptions(BuildContext context) {
-    final List<Widget> options = [
-      sectionOptionSpacing,
-      const SocialsMenuItemWidget("Twitter", "https://twitter.com/enteio"),
-      sectionOptionSpacing,
-      const SocialsMenuItemWidget("Discord", "https://ente.io/discord"),
-      sectionOptionSpacing,
-      const SocialsMenuItemWidget("Reddit", "https://reddit.com/r/enteio"),
-      sectionOptionSpacing,
-    ];
+    final List<Widget> options = [];
+    final result = UpdateService.instance.getRateDetails();
+    final String ratePlace = result.item1;
+    final String rateUrl = result.item2;
     if (!UpdateService.instance.isIndependent()) {
       options.addAll(
         [
-          SocialsMenuItemWidget(
-            "Rate us! ✨",
-            Platform.isAndroid
-                ? "https://play.google.com/store/apps/details?id=io.ente.photos"
-                : "https://apps.apple.com/in/app/ente-photos/id1542026904",
-          ),
+          SocialsMenuItemWidget("Rate us on $ratePlace", rateUrl),
           sectionOptionSpacing,
         ],
       );
     }
+    options.addAll(
+      [
+        sectionOptionSpacing,
+        const SocialsMenuItemWidget("Blog", "https://ente.io/blog"),
+        sectionOptionSpacing,
+        const SocialsMenuItemWidget("Twitter", "https://twitter.com/enteio"),
+        sectionOptionSpacing,
+        const SocialsMenuItemWidget("Mastodon", "https://mstdn.social/@ente"),
+        sectionOptionSpacing,
+        const SocialsMenuItemWidget(
+          "Matrix",
+          "https://ente.io/matrix/",
+        ),
+        sectionOptionSpacing,
+        const SocialsMenuItemWidget("Discord", "https://ente.io/discord"),
+        sectionOptionSpacing,
+        const SocialsMenuItemWidget("Reddit", "https://reddit.com/r/enteio"),
+        sectionOptionSpacing,
+      ],
+    );
+
     return Column(children: options);
   }
 }

+ 6 - 0
lib/ui/settings/storage_card_widget.dart

@@ -42,6 +42,12 @@ class _StorageCardWidgetState extends State<StorageCardWidget> {
     precacheImage(_background.image, context);
   }
 
+  @override
+  void dispose() {
+    _isStorageCardPressed.dispose();
+    super.dispose();
+  }
+
   @override
   Widget build(BuildContext context) {
     final inheritedUserDetails = InheritedUserDetails.of(context);

+ 9 - 3
lib/ui/settings/support_section_widget.dart

@@ -10,6 +10,7 @@ import 'package:photos/ui/common/web_page.dart';
 import 'package:photos/ui/components/captioned_text_widget.dart';
 import 'package:photos/ui/components/expandable_menu_item_widget.dart';
 import 'package:photos/ui/components/menu_item_widget.dart';
+import 'package:photos/ui/settings/about_section_widget.dart';
 import 'package:photos/ui/settings/common_settings.dart';
 import 'package:photos/utils/email_util.dart';
 
@@ -33,7 +34,7 @@ class SupportSectionWidget extends StatelessWidget {
         sectionOptionSpacing,
         MenuItemWidget(
           captionedTextWidget: const CaptionedTextWidget(
-            title: "Email",
+            title: "Contact support",
           ),
           pressedColor: getEnteColorScheme(context).fillFaint,
           trailingIcon: Icons.chevron_right_outlined,
@@ -43,9 +44,14 @@ class SupportSectionWidget extends StatelessWidget {
           },
         ),
         sectionOptionSpacing,
+        const AboutMenuItemWidget(
+          title: "Frequently asked questions",
+          url: "https://ente.io/faq",
+        ),
+        sectionOptionSpacing,
         MenuItemWidget(
           captionedTextWidget: const CaptionedTextWidget(
-            title: "Roadmap",
+            title: "Suggest features",
           ),
           pressedColor: getEnteColorScheme(context).fillFaint,
           trailingIcon: Icons.chevron_right_outlined,
@@ -59,7 +65,7 @@ class SupportSectionWidget extends StatelessWidget {
                   final url = Configuration.instance.isLoggedIn()
                       ? endpoint + "?token=" + Configuration.instance.getToken()
                       : roadmapURL;
-                  return WebPage("Roadmap", url);
+                  return WebPage("Suggest features", url);
                 },
               ),
             );

+ 3 - 7
lib/ui/settings_page.dart

@@ -12,8 +12,8 @@ import 'package:photos/ui/settings/about_section_widget.dart';
 import 'package:photos/ui/settings/account_section_widget.dart';
 import 'package:photos/ui/settings/app_version_widget.dart';
 import 'package:photos/ui/settings/backup_section_widget.dart';
-import 'package:photos/ui/settings/danger_section_widget.dart';
 import 'package:photos/ui/settings/debug_section_widget.dart';
+import 'package:photos/ui/settings/general_section_widget.dart';
 import 'package:photos/ui/settings/security_section_widget.dart';
 import 'package:photos/ui/settings/settings_title_bar_widget.dart';
 import 'package:photos/ui/settings/social_section_widget.dart';
@@ -77,6 +77,8 @@ class SettingsPage extends StatelessWidget {
     contents.addAll([
       const SecuritySectionWidget(),
       sectionSpacing,
+      const GeneralSectionWidget(),
+      sectionSpacing,
     ]);
 
     if (Platform.isAndroid || kDebugMode) {
@@ -93,12 +95,6 @@ class SettingsPage extends StatelessWidget {
       sectionSpacing,
       const AboutSectionWidget(),
     ]);
-    if (hasLoggedIn) {
-      contents.addAll([
-        sectionSpacing,
-        const DangerSectionWidget(),
-      ]);
-    }
 
     if (FeatureFlagService.instance.isInternalUserOrDebugBuild() &&
         hasLoggedIn) {

+ 6 - 1
lib/ui/viewer/file/thumbnail_widget.dart

@@ -27,6 +27,7 @@ class ThumbnailWidget extends StatefulWidget {
   final bool shouldShowLivePhotoOverlay;
   final Duration diskLoadDeferDuration;
   final Duration serverLoadDeferDuration;
+  final int thumbnailSize;
 
   ThumbnailWidget(
     this.file, {
@@ -38,6 +39,7 @@ class ThumbnailWidget extends StatefulWidget {
     this.showFavForAlbumOnly = false,
     this.diskLoadDeferDuration,
     this.serverLoadDeferDuration,
+    this.thumbnailSize = thumbnailSmallSize,
   }) : super(key: key ?? Key(file.tag));
 
   @override
@@ -166,7 +168,10 @@ class _ThumbnailWidgetState extends State<ThumbnailWidget> {
   }
 
   Future _getThumbnailFromDisk() async {
-    getThumbnailFromLocal(widget.file).then((thumbData) async {
+    getThumbnailFromLocal(
+      widget.file,
+      size: widget.thumbnailSize,
+    ).then((thumbData) async {
       if (thumbData == null) {
         if (widget.file.uploadedFileID != null) {
           _logger.fine("Removing localID reference for " + widget.file.tag);

+ 5 - 9
lib/ui/viewer/gallery/gallery.dart

@@ -19,6 +19,7 @@ import 'package:photos/ui/huge_listview/huge_listview.dart';
 import 'package:photos/ui/huge_listview/lazy_loading_gallery.dart';
 import 'package:photos/ui/viewer/gallery/empty_state.dart';
 import 'package:photos/utils/date_time_util.dart';
+import 'package:photos/utils/local_settings.dart';
 import 'package:scrollable_positioned_list/scrollable_positioned_list.dart';
 
 typedef GalleryLoader = Future<FileLoadResult> Function(
@@ -77,6 +78,7 @@ class _GalleryState extends State<Gallery> {
   StreamSubscription<TabDoubleTapEvent> _tabDoubleTapEvent;
   final _forceReloadEventSubscriptions = <StreamSubscription<Event>>[];
   String _logTag;
+  int _photoGridSize;
 
   @override
   void initState() {
@@ -200,6 +202,7 @@ class _GalleryState extends State<Gallery> {
     if (!_hasLoadedFiles) {
       return const EnteLoadingWidget();
     }
+    _photoGridSize = LocalSettings.instance.getPhotoGridSize();
     return _getListView();
   }
 
@@ -246,6 +249,7 @@ class _GalleryState extends State<Gallery> {
               .where((event) => event.tag == widget.tagPrefix)
               .map((event) => event.index),
           logTag: _logTag,
+          photoGirdSize: _photoGridSize,
         );
         if (widget.header != null && index == 0) {
           gallery = Column(children: [widget.header, gallery]);
@@ -281,7 +285,7 @@ class _GalleryState extends State<Gallery> {
     final List<List<File>> collatedFiles = [];
     for (int index = 0; index < files.length; index++) {
       if (index > 0 &&
-          !_areFromSameDay(
+          !areFromSameDay(
             files[index - 1].creationTime,
             files[index].creationTime,
           )) {
@@ -299,14 +303,6 @@ class _GalleryState extends State<Gallery> {
         .sort((a, b) => b[0].creationTime.compareTo(a[0].creationTime));
     return collatedFiles;
   }
-
-  bool _areFromSameDay(int firstCreationTime, int secondCreationTime) {
-    final firstDate = DateTime.fromMicrosecondsSinceEpoch(firstCreationTime);
-    final secondDate = DateTime.fromMicrosecondsSinceEpoch(secondCreationTime);
-    return firstDate.year == secondDate.year &&
-        firstDate.month == secondDate.month &&
-        firstDate.day == secondDate.day;
-  }
 }
 
 class GalleryIndexUpdatedEvent {

+ 3 - 9
lib/ui/viewer/gallery/gallery_app_bar_widget.dart

@@ -18,6 +18,7 @@ import 'package:photos/models/magic_metadata.dart';
 import 'package:photos/models/selected_files.dart';
 import 'package:photos/services/collections_service.dart';
 import 'package:photos/services/sync_service.dart';
+import 'package:photos/services/update_service.dart';
 import 'package:photos/ui/common/dialogs.dart';
 import 'package:photos/ui/common/rename_dialog.dart';
 import 'package:photos/ui/sharing/share_collection_widget.dart';
@@ -213,15 +214,8 @@ class _GalleryAppBarWidgetState extends State<GalleryAppBarWidget> {
           onPressed: () {
             Navigator.of(context, rootNavigator: true).pop('dialog');
             // TODO: Replace with https://pub.dev/packages/in_app_review
-            if (Platform.isAndroid) {
-              launchUrlString(
-                "https://play.google.com/store/apps/details?id=io.ente.photos",
-              );
-            } else {
-              launchUrlString(
-                "https://apps.apple.com/in/app/ente-photos/id1542026904",
-              );
-            }
+            final url = UpdateService.instance.getRateDetails().item2;
+            launchUrlString(url);
           },
         ),
         TextButton(

+ 34 - 7
lib/utils/date_time_util.dart

@@ -1,6 +1,7 @@
 import 'package:flutter/foundation.dart';
 import 'package:flutter/material.dart';
 import 'package:intl/intl.dart';
+import 'package:photos/core/constants.dart';
 import 'package:photos/theme/ente_theme.dart';
 
 const Set<int> monthWith31Days = {1, 3, 5, 7, 8, 10, 12};
@@ -53,6 +54,20 @@ String getMonthAndYear(DateTime dateTime) {
   return _months[dateTime.month]! + " " + dateTime.year.toString();
 }
 
+int daysBetween(DateTime from, DateTime to) {
+  from = DateTime(from.year, from.month, from.day);
+  to = DateTime(to.year, to.month, to.day);
+  return (to.difference(from).inHours / 24).round();
+}
+
+bool areFromSameDay(int firstCreationTime, int secondCreationTime) {
+  final firstDate = DateTime.fromMicrosecondsSinceEpoch(firstCreationTime);
+  final secondDate = DateTime.fromMicrosecondsSinceEpoch(secondCreationTime);
+  return firstDate.year == secondDate.year &&
+      firstDate.month == secondDate.month &&
+      firstDate.day == secondDate.day;
+}
+
 //Thu, 30 Jun
 String getDayAndMonth(DateTime dateTime) {
   return _days[dateTime.weekday]! +
@@ -195,16 +210,28 @@ bool isLeapYear(DateTime dateTime) {
 Widget getDayWidget(
   BuildContext context,
   int timestamp,
+  int photoGridSize,
 ) {
   final colorScheme = getEnteColorScheme(context);
   final textTheme = getEnteTextTheme(context);
-  return Container(
-    alignment: Alignment.centerLeft,
-    child: Text(
-      getDayTitle(timestamp),
-      style: (getDayTitle(timestamp) == "Today")
-          ? textTheme.body
-          : textTheme.body.copyWith(color: colorScheme.textMuted),
+  final textStyle =
+      photoGridSize < photoGridSizeMax ? textTheme.body : textTheme.small;
+  final double horizontalPadding =
+      photoGridSize < photoGridSizeMax ? 12.0 : 8.0;
+  final double verticalPadding = photoGridSize < photoGridSizeMax ? 12.0 : 14.0;
+  return Padding(
+    padding: EdgeInsets.symmetric(
+      horizontal: horizontalPadding,
+      vertical: verticalPadding,
+    ),
+    child: Container(
+      alignment: Alignment.centerLeft,
+      child: Text(
+        getDayTitle(timestamp),
+        style: (getDayTitle(timestamp) == "Today")
+            ? textStyle
+            : textStyle.copyWith(color: colorScheme.textMuted),
+      ),
     ),
   );
 }

+ 15 - 0
lib/utils/local_settings.dart

@@ -1,3 +1,4 @@
+import 'package:photos/core/constants.dart';
 import 'package:shared_preferences/shared_preferences.dart';
 
 enum AlbumSortKey {
@@ -11,6 +12,8 @@ class LocalSettings {
 
   static final LocalSettings instance = LocalSettings._privateConstructor();
   static const kCollectionSortPref = "collection_sort_pref";
+  static const kPhotoGridSize = "photo_grid_size";
+
   late SharedPreferences _prefs;
 
   void init(SharedPreferences preferences) {
@@ -24,4 +27,16 @@ class LocalSettings {
   Future<bool> setAlbumSortKey(AlbumSortKey key) {
     return _prefs.setInt(kCollectionSortPref, key.index);
   }
+
+  int getPhotoGridSize() {
+    if (_prefs.containsKey(kPhotoGridSize)) {
+      return _prefs.getInt(kPhotoGridSize)!;
+    } else {
+      return photoGridSizeDefault;
+    }
+  }
+
+  Future<void> setPhotoGridSize(int value) async {
+    await _prefs.setInt(kPhotoGridSize, value);
+  }
 }

+ 30 - 15
lib/utils/share_util.dart

@@ -26,23 +26,38 @@ Future<void> share(
 }) async {
   final dialog = createProgressDialog(context, "Preparing...");
   await dialog.show();
-  final List<Future<String>> pathFutures = [];
-  for (File file in files) {
-    // Note: We are requesting the origin file for performance reasons on iOS.
-    // This will eat up storage, which will be reset only when the app restarts.
-    // We could have cleared the cache had there been a callback to the share API.
-    pathFutures.add(getFile(file, isOrigin: true).then((file) => file.path));
-    if (file.fileType == FileType.livePhoto) {
-      pathFutures.add(getFile(file, liveVideo: true).then((file) => file.path));
+  try {
+    final List<Future<String>> pathFutures = [];
+    for (File file in files) {
+      // Note: We are requesting the origin file for performance reasons on iOS.
+      // This will eat up storage, which will be reset only when the app restarts.
+      // We could have cleared the cache had there been a callback to the share API.
+      pathFutures.add(
+        getFile(file, isOrigin: true).then((fetchedFile) => fetchedFile.path),
+      );
+      if (file.fileType == FileType.livePhoto) {
+        pathFutures.add(
+          getFile(file, liveVideo: true)
+              .then((fetchedFile) => fetchedFile.path),
+        );
+      }
     }
+    final paths = await Future.wait(pathFutures);
+    await dialog.hide();
+    return Share.shareFiles(
+      paths,
+      // required for ipad https://github.com/flutter/flutter/issues/47220#issuecomment-608453383
+      sharePositionOrigin: shareButtonRect(context, shareButtonKey),
+    );
+  } catch (e, s) {
+    _logger.severe(
+      "failed to fetch files for system share ${files.length}",
+      e,
+      s,
+    );
+    await dialog.hide();
+    await showGenericErrorDialog(context);
   }
-  final paths = await Future.wait(pathFutures);
-  await dialog.hide();
-  return Share.shareFiles(
-    paths,
-    // required for ipad https://github.com/flutter/flutter/issues/47220#issuecomment-608453383
-    sharePositionOrigin: shareButtonRect(context, shareButtonKey),
-  );
 }
 
 Rect shareButtonRect(BuildContext context, GlobalKey shareButtonKey) {

+ 14 - 28
pubspec.lock

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

+ 5 - 1
test/utils/date_time_util_test.dart

@@ -32,7 +32,11 @@ void main() {
   });
 
   test("test invalid datetime parsing", () {
-    final List<String> badParsing = ["Snapchat-431959199.mp4."];
+    final List<String> badParsing = [
+      "Snapchat-431959199.mp4.",
+      "Snapchat-400000000.mp4",
+      "Snapchat-900000000.mp4"
+    ];
     for (String val in badParsing) {
       final parsedValue = parseDateTimeFromFileNameV2(val);
       expect(