소스 검색

Merge pull request #843 from ente-io/picker

New pickers
Neeraj Gupta 2 년 전
부모
커밋
cfc656ce13

+ 2 - 0
lib/core/constants.dart

@@ -55,3 +55,5 @@ const int intMaxValue = 9223372036854775807;
 const double restrictedMaxWidth = 430;
 
 const double mobileSmallThreshold = 336;
+
+const publicLinkDeviceLimits = [50, 25, 10, 5, 2, 1];

+ 6 - 6
lib/ui/account/sessions_page.dart

@@ -22,7 +22,9 @@ class _SessionsPageState extends State<SessionsPage> {
 
   @override
   void initState() {
-    _fetchActiveSessions();
+    _fetchActiveSessions().onError((error, stackTrace) {
+      showToast(context, "Failed to fetch active sessions");
+    });
     super.initState();
   }
 
@@ -115,9 +117,9 @@ class _SessionsPageState extends State<SessionsPage> {
       await UserService.instance.terminateSession(session.token);
       await _fetchActiveSessions();
       await dialog.hide();
-    } catch (e, s) {
+    } catch (e) {
       await dialog.hide();
-      _logger.severe('failed to terminate', e, s);
+      _logger.severe('failed to terminate');
       showErrorDialog(
         context,
         'Oops',
@@ -129,9 +131,7 @@ class _SessionsPageState extends State<SessionsPage> {
   Future<void> _fetchActiveSessions() async {
     _sessions = await UserService.instance.getActiveSessions().onError((e, s) {
       _logger.severe("failed to fetch active sessions", e, s);
-      if (mounted) {
-        showToast(context, "Failed to fetch active sessions");
-      }
+      throw e!;
     });
     if (_sessions != null) {
       _sessions!.sessions.sort((first, second) {

+ 11 - 110
lib/ui/advanced_settings_screen.dart

@@ -1,8 +1,4 @@
-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';
@@ -10,6 +6,7 @@ import 'package:photos/ui/components/menu_item_widget/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/ui/tools/debug/app_storage_viewer.dart';
+import 'package:photos/ui/viewer/gallery/photo_grid_size_picker_page.dart';
 import 'package:photos/utils/local_settings.dart';
 import 'package:photos/utils/navigation_util.dart';
 
@@ -21,12 +18,11 @@ class AdvancedSettingsScreen extends StatefulWidget {
 }
 
 class _AdvancedSettingsScreenState extends State<AdvancedSettingsScreen> {
-  late int _photoGridSize, _chosenGridSize;
+  late int _photoGridSize;
 
   @override
   void initState() {
     _photoGridSize = LocalSettings.instance.getPhotoGridSize();
-    _chosenGridSize = _photoGridSize;
     super.initState();
   }
 
@@ -66,7 +62,15 @@ class _AdvancedSettingsScreenState extends State<AdvancedSettingsScreen> {
                           children: [
                             GestureDetector(
                               onTap: () {
-                                _showPhotoGridSizePicker(delegateBuildContext);
+                                routeToPage(
+                                  context,
+                                  const PhotoGridSizePickerPage(),
+                                ).then((value) {
+                                  setState(() {
+                                    _photoGridSize = LocalSettings.instance
+                                        .getPhotoGridSize();
+                                  });
+                                });
                               },
                               child: MenuItemWidget(
                                 captionedTextWidget: CaptionedTextWidget(
@@ -80,7 +84,6 @@ class _AdvancedSettingsScreenState extends State<AdvancedSettingsScreen> {
                                 ),
                                 singleBorderRadius: 8,
                                 alignCaptionedTextToLeft: true,
-                                // isBottomBorderRadiusRemoved: true,
                                 isGestureDetectorDisabled: true,
                               ),
                             ),
@@ -116,106 +119,4 @@ class _AdvancedSettingsScreenState extends State<AdvancedSettingsScreen> {
       ),
     );
   }
-
-  Future<void> _showPhotoGridSizePicker(BuildContext buildContext) async {
-    final textTheme = getEnteTextTheme(buildContext);
-    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(buildContext).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(buildContext).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;
-  }
 }

+ 15 - 251
lib/ui/sharing/manage_links_widget.dart

@@ -4,9 +4,7 @@ import 'dart:typed_data';
 import 'package:collection/collection.dart';
 import 'package:flutter/cupertino.dart';
 import 'package:flutter/material.dart';
-import 'package:flutter_datetime_picker/flutter_datetime_picker.dart';
 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/theme/colors.dart';
@@ -16,11 +14,13 @@ import 'package:photos/ui/components/captioned_text_widget.dart';
 import 'package:photos/ui/components/divider_widget.dart';
 import 'package:photos/ui/components/menu_item_widget/menu_item_widget.dart';
 import 'package:photos/ui/components/menu_section_description_widget.dart';
+import 'package:photos/ui/sharing/pickers/device_limit_picker_page.dart';
+import 'package:photos/ui/sharing/pickers/link_expiry_picker_page.dart';
 import 'package:photos/utils/crypto_util.dart';
 import 'package:photos/utils/date_time_util.dart';
 import 'package:photos/utils/dialog_util.dart';
+import 'package:photos/utils/navigation_util.dart';
 import 'package:photos/utils/toast_util.dart';
-import 'package:tuple/tuple.dart';
 
 class ManageSharedLinkWidget extends StatefulWidget {
   final Collection? collection;
@@ -32,26 +32,11 @@ class ManageSharedLinkWidget extends StatefulWidget {
 }
 
 class _ManageSharedLinkWidgetState extends State<ManageSharedLinkWidget> {
-  // index, title, milliseconds in future post which link should expire (when >0)
-  final List<Tuple3<int, String, int>> _expiryOptions = [
-    const Tuple3(0, "Never", 0),
-    Tuple3(1, "After 1 hour", const Duration(hours: 1).inMicroseconds),
-    Tuple3(2, "After 1 day", const Duration(days: 1).inMicroseconds),
-    Tuple3(3, "After 1 week", const Duration(days: 7).inMicroseconds),
-    // todo: make this time calculation perfect
-    Tuple3(4, "After 1 month", const Duration(days: 30).inMicroseconds),
-    Tuple3(5, "After 1 year", const Duration(days: 365).inMicroseconds),
-    const Tuple3(6, "Custom", -1),
-  ];
-
-  late Tuple3<int, String, int> _selectedExpiry;
-  int _selectedDeviceLimitIndex = 0;
   final CollectionActions sharingActions =
       CollectionActions(CollectionsService.instance);
 
   @override
   void initState() {
-    _selectedExpiry = _expiryOptions.first;
     super.initState();
   }
 
@@ -114,7 +99,12 @@ class _ManageSharedLinkWidgetState extends State<ManageSharedLinkWidget> {
                     menuItemColor: enteColorScheme.fillFaint,
                     surfaceExecutionStates: false,
                     onTap: () async {
-                      await showPicker();
+                      routeToPage(
+                        context,
+                        LinkExpiryPickerPage(widget.collection!),
+                      ).then((value) {
+                        setState(() {});
+                      });
                     },
                   ),
                   url.hasExpiry
@@ -138,7 +128,12 @@ class _ManageSharedLinkWidgetState extends State<ManageSharedLinkWidget> {
                     alignCaptionedTextToLeft: true,
                     isBottomBorderRadiusRemoved: true,
                     onTap: () async {
-                      await _showDeviceLimitPicker();
+                      routeToPage(
+                        context,
+                        DeviceLimitPickerPage(widget.collection!),
+                      ).then((value) {
+                        setState(() {});
+                      });
                     },
                     surfaceExecutionStates: false,
                   ),
@@ -231,7 +226,6 @@ class _ManageSharedLinkWidgetState extends State<ManageSharedLinkWidget> {
                       );
                       if (result && mounted) {
                         Navigator.of(context).pop();
-                        // setState(() => {});
                       }
                     },
                   ),
@@ -244,153 +238,6 @@ class _ManageSharedLinkWidgetState extends State<ManageSharedLinkWidget> {
     );
   }
 
-  Future<void> showPicker() async {
-    return showCupertinoModalPopup(
-      context: context,
-      builder: (context) {
-        return Column(
-          mainAxisAlignment: MainAxisAlignment.end,
-          children: <Widget>[
-            Container(
-              decoration: BoxDecoration(
-                color: Theme.of(context).colorScheme.cupertinoPickerTopColor,
-                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: Theme.of(context).textTheme.subtitle1,
-                    ),
-                  ),
-                  CupertinoButton(
-                    onPressed: () async {
-                      int newValidTill = -1;
-                      bool hasSelectedCustom = false;
-                      final int expireAfterInMicroseconds =
-                          _selectedExpiry.item3;
-                      // need to manually select time
-                      if (expireAfterInMicroseconds < 0) {
-                        hasSelectedCustom = true;
-                        Navigator.of(context).pop('');
-                        final timeInMicrosecondsFromEpoch =
-                            await _showDateTimePicker();
-                        if (timeInMicrosecondsFromEpoch != null) {
-                          newValidTill = timeInMicrosecondsFromEpoch;
-                        }
-                      } else if (expireAfterInMicroseconds == 0) {
-                        // no expiry
-                        newValidTill = 0;
-                      } else {
-                        newValidTill = DateTime.now().microsecondsSinceEpoch +
-                            expireAfterInMicroseconds;
-                      }
-                      if (!hasSelectedCustom) {
-                        Navigator.of(context).pop('');
-                      }
-                      if (newValidTill >= 0) {
-                        debugPrint("Setting expirty $newValidTill");
-                        await updateTime(newValidTill);
-                      }
-                    },
-                    padding: const EdgeInsets.symmetric(
-                      horizontal: 16.0,
-                      vertical: 2.0,
-                    ),
-                    child: Text(
-                      'Confirm',
-                      style: Theme.of(context).textTheme.subtitle1,
-                    ),
-                  )
-                ],
-              ),
-            ),
-            Container(
-              height: 220.0,
-              color: const Color(0xfff7f7f7),
-              child: CupertinoPicker(
-                backgroundColor:
-                    Theme.of(context).backgroundColor.withOpacity(0.95),
-                onSelectedItemChanged: (value) {
-                  final firstWhere = _expiryOptions
-                      .firstWhere((element) => element.item1 == value);
-                  setState(() {
-                    _selectedExpiry = firstWhere;
-                  });
-                },
-                magnification: 1.3,
-                useMagnifier: true,
-                itemExtent: 25,
-                diameterRatio: 1,
-                children: _expiryOptions
-                    .map(
-                      (e) => Text(
-                        e.item2,
-                        style: Theme.of(context).textTheme.subtitle1,
-                      ),
-                    )
-                    .toList(),
-              ),
-            )
-          ],
-        );
-      },
-    );
-  }
-
-  Future<void> updateTime(int newValidTill) async {
-    await _updateUrlSettings(
-      context,
-      {'validTill': newValidTill},
-    );
-    if (mounted) {
-      // reset to default value. THis is needed will we move to
-      // new selection menu as per figma/
-      _selectedExpiry = _expiryOptions.first;
-      setState(() {});
-    }
-  }
-
-  // _showDateTimePicker return null if user doesn't select date-time
-  Future<int?> _showDateTimePicker() async {
-    final dateResult = await DatePicker.showDatePicker(
-      context,
-      minTime: DateTime.now(),
-      currentTime: DateTime.now(),
-      locale: LocaleType.en,
-      theme: Theme.of(context).colorScheme.dateTimePickertheme,
-    );
-    if (dateResult == null) {
-      return null;
-    }
-    final dateWithTimeResult = await DatePicker.showTime12hPicker(
-      context,
-      showTitleActions: true,
-      currentTime: dateResult,
-      locale: LocaleType.en,
-      theme: Theme.of(context).colorScheme.dateTimePickertheme,
-    );
-    if (dateWithTimeResult == null) {
-      return null;
-    } else {
-      return dateWithTimeResult.microsecondsSinceEpoch;
-    }
-  }
-
   final TextEditingController _textFieldController = TextEditingController();
 
   Future<String?> _displayLinkPasswordInput(BuildContext context) async {
@@ -495,87 +342,4 @@ class _ManageSharedLinkWidgetState extends State<ManageSharedLinkWidget> {
       await showGenericErrorDialog(context: context);
     }
   }
-
-  Future<void> _showDeviceLimitPicker() async {
-    final List<Text> options = [];
-    for (int i = 50; i > 0; i--) {
-      options.add(
-        Text(i.toString(), style: Theme.of(context).textTheme.subtitle1),
-      );
-    }
-    return showCupertinoModalPopup(
-      context: context,
-      builder: (context) {
-        return Column(
-          mainAxisAlignment: MainAxisAlignment.end,
-          children: <Widget>[
-            Container(
-              decoration: BoxDecoration(
-                color: Theme.of(context).colorScheme.cupertinoPickerTopColor,
-                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: Theme.of(context).textTheme.subtitle1,
-                    ),
-                  ),
-                  CupertinoButton(
-                    onPressed: () async {
-                      await _updateUrlSettings(context, {
-                        'deviceLimit': int.tryParse(
-                          options[_selectedDeviceLimitIndex].data!,
-                        ),
-                      });
-                      setState(() {});
-                      Navigator.of(context).pop('');
-                    },
-                    padding: const EdgeInsets.symmetric(
-                      horizontal: 16.0,
-                      vertical: 2.0,
-                    ),
-                    child: Text(
-                      'Confirm',
-                      style: Theme.of(context).textTheme.subtitle1,
-                    ),
-                  )
-                ],
-              ),
-            ),
-            Container(
-              height: 220.0,
-              color: const Color(0xfff7f7f7),
-              child: CupertinoPicker(
-                backgroundColor:
-                    Theme.of(context).backgroundColor.withOpacity(0.95),
-                onSelectedItemChanged: (value) {
-                  _selectedDeviceLimitIndex = value;
-                },
-                magnification: 1.3,
-                useMagnifier: true,
-                itemExtent: 25,
-                diameterRatio: 1,
-                children: options,
-              ),
-            )
-          ],
-        );
-      },
-    );
-  }
 }

+ 149 - 0
lib/ui/sharing/pickers/device_limit_picker_page.dart

@@ -0,0 +1,149 @@
+import 'package:flutter/material.dart';
+import 'package:photos/core/constants.dart';
+import 'package:photos/models/collection.dart';
+import 'package:photos/services/collections_service.dart';
+import 'package:photos/theme/ente_theme.dart';
+import 'package:photos/ui/components/captioned_text_widget.dart';
+import 'package:photos/ui/components/divider_widget.dart';
+import 'package:photos/ui/components/menu_item_widget/menu_item_widget.dart';
+import 'package:photos/ui/components/menu_section_description_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/dialog_util.dart';
+import 'package:photos/utils/separators_util.dart';
+
+class DeviceLimitPickerPage extends StatelessWidget {
+  final Collection collection;
+  const DeviceLimitPickerPage(this.collection, {super.key});
+
+  @override
+  Widget build(BuildContext context) {
+    return Scaffold(
+      body: CustomScrollView(
+        primary: false,
+        slivers: <Widget>[
+          const TitleBarWidget(
+            flexibleSpaceTitle: TitleBarTitleWidget(
+              title: "Device Limit",
+            ),
+          ),
+          SliverList(
+            delegate: SliverChildBuilderDelegate(
+              (context, index) {
+                return Padding(
+                  padding: const EdgeInsets.symmetric(
+                    horizontal: 16,
+                    vertical: 20,
+                  ),
+                  child: Column(
+                    mainAxisSize: MainAxisSize.min,
+                    children: [
+                      ClipRRect(
+                        borderRadius:
+                            const BorderRadius.all(Radius.circular(8)),
+                        child: ItemsWidget(collection),
+                      ),
+                      const MenuSectionDescriptionWidget(
+                        content:
+                            "When set to the maximum (50), the device limit will be relaxed"
+                            " to allow for temporary spikes of large number of viewers.",
+                      )
+                    ],
+                  ),
+                );
+              },
+              childCount: 1,
+            ),
+          ),
+          const SliverPadding(padding: EdgeInsets.symmetric(vertical: 12)),
+        ],
+      ),
+    );
+  }
+}
+
+class ItemsWidget extends StatefulWidget {
+  final Collection collection;
+  const ItemsWidget(this.collection, {super.key});
+
+  @override
+  State<ItemsWidget> createState() => _ItemsWidgetState();
+}
+
+class _ItemsWidgetState extends State<ItemsWidget> {
+  late int currentDeviceLimit;
+  late int initialDeviceLimit;
+  List<Widget> items = [];
+  bool isCustomLimit = false;
+  @override
+  void initState() {
+    currentDeviceLimit = widget.collection.publicURLs!.first!.deviceLimit;
+    initialDeviceLimit = currentDeviceLimit;
+    if (!publicLinkDeviceLimits.contains(currentDeviceLimit)) {
+      isCustomLimit = true;
+    }
+    super.initState();
+  }
+
+  @override
+  Widget build(BuildContext context) {
+    items.clear();
+    if (isCustomLimit) {
+      items.add(
+        _menuItemForPicker(initialDeviceLimit),
+      );
+    }
+    for (int deviceLimit in publicLinkDeviceLimits) {
+      items.add(
+        _menuItemForPicker(deviceLimit),
+      );
+    }
+    items = addSeparators(
+      items,
+      DividerWidget(
+        dividerType: DividerType.menuNoIcon,
+        bgColor: getEnteColorScheme(context).fillFaint,
+      ),
+    );
+    return Column(
+      mainAxisSize: MainAxisSize.min,
+      children: items,
+    );
+  }
+
+  Widget _menuItemForPicker(int deviceLimit) {
+    return MenuItemWidget(
+      key: ValueKey(deviceLimit),
+      menuItemColor: getEnteColorScheme(context).fillFaint,
+      captionedTextWidget: CaptionedTextWidget(
+        title: "$deviceLimit",
+      ),
+      trailingIcon: currentDeviceLimit == deviceLimit ? Icons.check : null,
+      alignCaptionedTextToLeft: true,
+      isTopBorderRadiusRemoved: true,
+      isBottomBorderRadiusRemoved: true,
+      showOnlyLoadingState: true,
+      onTap: () async {
+        await _updateUrlSettings(context, {
+          'deviceLimit': deviceLimit,
+        }).then(
+          (value) => setState(() {
+            currentDeviceLimit = deviceLimit;
+          }),
+        );
+      },
+    );
+  }
+
+  Future<void> _updateUrlSettings(
+    BuildContext context,
+    Map<String, dynamic> prop,
+  ) async {
+    try {
+      await CollectionsService.instance.updateShareUrl(widget.collection, prop);
+    } catch (e) {
+      showGenericErrorDialog(context: context);
+      rethrow;
+    }
+  }
+}

+ 181 - 0
lib/ui/sharing/pickers/link_expiry_picker_page.dart

@@ -0,0 +1,181 @@
+import 'package:flutter/material.dart';
+import 'package:flutter_datetime_picker/flutter_datetime_picker.dart';
+import 'package:photos/ente_theme_data.dart';
+import 'package:photos/models/collection.dart';
+import 'package:photos/services/collections_service.dart';
+import 'package:photos/theme/ente_theme.dart';
+import 'package:photos/ui/components/captioned_text_widget.dart';
+import 'package:photos/ui/components/divider_widget.dart';
+import 'package:photos/ui/components/menu_item_widget/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/dialog_util.dart';
+import 'package:photos/utils/separators_util.dart';
+import 'package:tuple/tuple.dart';
+
+class LinkExpiryPickerPage extends StatelessWidget {
+  final Collection collection;
+  const LinkExpiryPickerPage(this.collection, {super.key});
+
+  @override
+  Widget build(BuildContext context) {
+    return Scaffold(
+      body: CustomScrollView(
+        primary: false,
+        slivers: <Widget>[
+          const TitleBarWidget(
+            flexibleSpaceTitle: TitleBarTitleWidget(
+              title: "Link expiry",
+            ),
+          ),
+          SliverList(
+            delegate: SliverChildBuilderDelegate(
+              (context, index) {
+                return Padding(
+                  padding: const EdgeInsets.symmetric(
+                    horizontal: 16,
+                    vertical: 20,
+                  ),
+                  child: Column(
+                    mainAxisSize: MainAxisSize.min,
+                    children: [
+                      ClipRRect(
+                        borderRadius:
+                            const BorderRadius.all(Radius.circular(8)),
+                        child: ItemsWidget(collection),
+                      ),
+                    ],
+                  ),
+                );
+              },
+              childCount: 1,
+            ),
+          ),
+          const SliverPadding(padding: EdgeInsets.symmetric(vertical: 12)),
+        ],
+      ),
+    );
+  }
+}
+
+class ItemsWidget extends StatelessWidget {
+  final Collection collection;
+  ItemsWidget(this.collection, {super.key});
+
+  // index, title, milliseconds in future post which link should expire (when >0)
+  final List<Tuple2<String, int>> _expiryOptions = [
+    const Tuple2("Never", 0),
+    Tuple2("After 1 hour", const Duration(hours: 1).inMicroseconds),
+    Tuple2("After 1 day", const Duration(days: 1).inMicroseconds),
+    Tuple2("After 1 week", const Duration(days: 7).inMicroseconds),
+    // todo: make this time calculation perfect
+    Tuple2("After 1 month", const Duration(days: 30).inMicroseconds),
+    Tuple2("After 1 year", const Duration(days: 365).inMicroseconds),
+    const Tuple2("Custom", -1),
+  ];
+
+  @override
+  Widget build(BuildContext context) {
+    List<Widget> items = [];
+    for (Tuple2<String, int> expiryOpiton in _expiryOptions) {
+      items.add(
+        _menuItemForPicker(context, expiryOpiton),
+      );
+    }
+    items = addSeparators(
+      items,
+      DividerWidget(
+        dividerType: DividerType.menuNoIcon,
+        bgColor: getEnteColorScheme(context).fillFaint,
+      ),
+    );
+    return Column(
+      mainAxisSize: MainAxisSize.min,
+      children: items,
+    );
+  }
+
+  Widget _menuItemForPicker(
+    BuildContext context,
+    Tuple2<String, int> expiryOpiton,
+  ) {
+    return MenuItemWidget(
+      menuItemColor: getEnteColorScheme(context).fillFaint,
+      captionedTextWidget: CaptionedTextWidget(
+        title: expiryOpiton.item1,
+      ),
+      alignCaptionedTextToLeft: true,
+      isTopBorderRadiusRemoved: true,
+      isBottomBorderRadiusRemoved: true,
+      alwaysShowSuccessState: true,
+      surfaceExecutionStates: expiryOpiton.item2 == -1 ? false : true,
+      onTap: () async {
+        int newValidTill = -1;
+        final int expireAfterInMicroseconds = expiryOpiton.item2;
+        // need to manually select time
+        if (expireAfterInMicroseconds < 0) {
+          final timeInMicrosecondsFromEpoch =
+              await _showDateTimePicker(context);
+          if (timeInMicrosecondsFromEpoch != null) {
+            newValidTill = timeInMicrosecondsFromEpoch;
+          }
+        } else if (expireAfterInMicroseconds == 0) {
+          // no expiry
+          newValidTill = 0;
+        } else {
+          newValidTill =
+              DateTime.now().microsecondsSinceEpoch + expireAfterInMicroseconds;
+        }
+        if (newValidTill >= 0) {
+          debugPrint("Setting expirty $newValidTill");
+          await updateTime(newValidTill, context);
+        }
+      },
+    );
+  }
+
+  // _showDateTimePicker return null if user doesn't select date-time
+  Future<int?> _showDateTimePicker(BuildContext context) async {
+    final dateResult = await DatePicker.showDatePicker(
+      context,
+      minTime: DateTime.now(),
+      currentTime: DateTime.now(),
+      locale: LocaleType.en,
+      theme: Theme.of(context).colorScheme.dateTimePickertheme,
+    );
+    if (dateResult == null) {
+      return null;
+    }
+    final dateWithTimeResult = await DatePicker.showTime12hPicker(
+      context,
+      showTitleActions: true,
+      currentTime: dateResult,
+      locale: LocaleType.en,
+      theme: Theme.of(context).colorScheme.dateTimePickertheme,
+    );
+    if (dateWithTimeResult == null) {
+      return null;
+    } else {
+      return dateWithTimeResult.microsecondsSinceEpoch;
+    }
+  }
+
+  Future<void> updateTime(int newValidTill, BuildContext context) async {
+    await _updateUrlSettings(
+      context,
+      {'validTill': newValidTill},
+    );
+  }
+
+  Future<void> _updateUrlSettings(
+    BuildContext context,
+    Map<String, dynamic> prop,
+  ) async {
+    try {
+      await CollectionsService.instance.updateShareUrl(collection, prop);
+    } catch (e) {
+      showGenericErrorDialog(context: context);
+      rethrow;
+    }
+  }
+}

+ 124 - 0
lib/ui/viewer/gallery/photo_grid_size_picker_page.dart

@@ -0,0 +1,124 @@
+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/divider_widget.dart';
+import 'package:photos/ui/components/menu_item_widget/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';
+import 'package:photos/utils/separators_util.dart';
+
+class PhotoGridSizePickerPage extends StatelessWidget {
+  const PhotoGridSizePickerPage({super.key});
+
+  @override
+  Widget build(BuildContext context) {
+    return Scaffold(
+      body: CustomScrollView(
+        primary: false,
+        slivers: <Widget>[
+          const TitleBarWidget(
+            flexibleSpaceTitle: TitleBarTitleWidget(
+              title: "Photo grid size",
+            ),
+          ),
+          SliverList(
+            delegate: SliverChildBuilderDelegate(
+              (context, index) {
+                return Padding(
+                  padding: const EdgeInsets.symmetric(
+                    horizontal: 16,
+                    vertical: 20,
+                  ),
+                  child: Column(
+                    mainAxisSize: MainAxisSize.min,
+                    children: const [
+                      ClipRRect(
+                        borderRadius: BorderRadius.all(Radius.circular(8)),
+                        child: ItemsWidget(),
+                      ),
+                    ],
+                  ),
+                );
+              },
+              childCount: 1,
+            ),
+          ),
+          const SliverPadding(padding: EdgeInsets.symmetric(vertical: 12)),
+        ],
+      ),
+    );
+  }
+}
+
+class ItemsWidget extends StatefulWidget {
+  const ItemsWidget({super.key});
+
+  @override
+  State<ItemsWidget> createState() => _ItemsWidgetState();
+}
+
+class _ItemsWidgetState extends State<ItemsWidget> {
+  late int currentGridSize;
+  List<Widget> items = [];
+  final List<int> gridSizes = [];
+  @override
+  void initState() {
+    currentGridSize = LocalSettings.instance.getPhotoGridSize();
+    for (int gridSize = photoGridSizeMin;
+        gridSize <= photoGridSizeMax;
+        gridSize++) {
+      gridSizes.add(gridSize);
+    }
+    super.initState();
+  }
+
+  @override
+  Widget build(BuildContext context) {
+    items.clear();
+    for (int girdSize in gridSizes) {
+      items.add(
+        _menuItemForPicker(girdSize),
+      );
+    }
+    items = addSeparators(
+      items,
+      DividerWidget(
+        dividerType: DividerType.menuNoIcon,
+        bgColor: getEnteColorScheme(context).fillFaint,
+      ),
+    );
+    return Column(
+      mainAxisSize: MainAxisSize.min,
+      children: items,
+    );
+  }
+
+  Widget _menuItemForPicker(int gridSize) {
+    return MenuItemWidget(
+      key: ValueKey(gridSize),
+      menuItemColor: getEnteColorScheme(context).fillFaint,
+      captionedTextWidget: CaptionedTextWidget(
+        title: "$gridSize",
+      ),
+      trailingIcon: currentGridSize == gridSize ? Icons.check : null,
+      alignCaptionedTextToLeft: true,
+      isTopBorderRadiusRemoved: true,
+      isBottomBorderRadiusRemoved: true,
+      showOnlyLoadingState: true,
+      onTap: () async {
+        await LocalSettings.instance.setPhotoGridSize(gridSize).then(
+              (value) => setState(() {
+                currentGridSize = gridSize;
+              }),
+            );
+        Bus.instance.fire(
+          ForceReloadHomeGalleryEvent("grid size changed"),
+        );
+      },
+    );
+  }
+}