فهرست منبع

Merge branch 'master' into grid

vishnukvmd 2 سال پیش
والد
کامیت
7133930af4

+ 7 - 0
ios/Podfile.lock

@@ -102,6 +102,9 @@ PODS:
     - Flutter
   - in_app_purchase_storekit (0.0.1):
     - Flutter
+  - keyboard_visibility (0.5.0):
+    - Flutter
+    - Reachability
   - libwebp (1.2.3):
     - libwebp/demux (= 1.2.3)
     - libwebp/mux (= 1.2.3)
@@ -193,6 +196,7 @@ DEPENDENCIES:
   - fluttertoast (from `.symlinks/plugins/fluttertoast/ios`)
   - image_editor (from `.symlinks/plugins/image_editor/ios`)
   - in_app_purchase_storekit (from `.symlinks/plugins/in_app_purchase_storekit/ios`)
+  - keyboard_visibility (from `.symlinks/plugins/keyboard_visibility/ios`)
   - local_auth (from `.symlinks/plugins/local_auth/ios`)
   - media_extension (from `.symlinks/plugins/media_extension/ios`)
   - motionphoto (from `.symlinks/plugins/motionphoto/ios`)
@@ -271,6 +275,8 @@ EXTERNAL SOURCES:
     :path: ".symlinks/plugins/image_editor/ios"
   in_app_purchase_storekit:
     :path: ".symlinks/plugins/in_app_purchase_storekit/ios"
+  keyboard_visibility:
+    :path: ".symlinks/plugins/keyboard_visibility/ios"
   local_auth:
     :path: ".symlinks/plugins/local_auth/ios"
   media_extension:
@@ -336,6 +342,7 @@ SPEC CHECKSUMS:
   GoogleUtilities: 1d20a6ad97ef46f67bbdec158ce00563a671ebb7
   image_editor: eab82a302a6623a866da5145b7c4c0ee8a4ffbb4
   in_app_purchase_storekit: d7fcf4646136ec258e237872755da8ea6c1b6096
+  keyboard_visibility: 96a24de806fe6823c3ad956c01ba2ec6d056616f
   libwebp: 60305b2e989864154bd9be3d772730f08fc6a59c
   local_auth: 1740f55d7af0a2e2a8684ce225fe79d8931e808c
   Mantle: c5aa8794a29a022dfbbfc9799af95f477a69b62d

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

@@ -287,6 +287,7 @@
 				"${BUILT_PRODUCTS_DIR}/fluttertoast/fluttertoast.framework",
 				"${BUILT_PRODUCTS_DIR}/image_editor/image_editor.framework",
 				"${BUILT_PRODUCTS_DIR}/in_app_purchase_storekit/in_app_purchase_storekit.framework",
+				"${BUILT_PRODUCTS_DIR}/keyboard_visibility/keyboard_visibility.framework",
 				"${BUILT_PRODUCTS_DIR}/libwebp/libwebp.framework",
 				"${BUILT_PRODUCTS_DIR}/local_auth/local_auth.framework",
 				"${BUILT_PRODUCTS_DIR}/media_extension/media_extension.framework",
@@ -341,6 +342,7 @@
 				"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/fluttertoast.framework",
 				"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/image_editor.framework",
 				"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/in_app_purchase_storekit.framework",
+				"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/keyboard_visibility.framework",
 				"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/libwebp.framework",
 				"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/local_auth.framework",
 				"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/media_extension.framework",

+ 26 - 0
lib/core/configuration.dart

@@ -324,6 +324,32 @@ class Configuration {
     );
   }
 
+  Future<void> verifyPassword(String password) async {
+    final KeyAttributes attributes = getKeyAttributes()!;
+    _logger.info('state validation done');
+    final kek = await CryptoUtil.deriveKey(
+      utf8.encode(password) as Uint8List,
+      Sodium.base642bin(attributes.kekSalt),
+      attributes.memLimit,
+      attributes.opsLimit,
+    ).onError((e, s) {
+      _logger.severe('deriveKey failed', e, s);
+      throw KeyDerivationError();
+    });
+
+    _logger.info('user-key done');
+    try {
+      final Uint8List key = CryptoUtil.decryptSync(
+        Sodium.base642bin(attributes.encryptedKey),
+        kek,
+        Sodium.base642bin(attributes.keyDecryptionNonce),
+      );
+    } catch (e) {
+      _logger.severe('master-key failed, incorrect password?', e);
+      throw Exception("Incorrect password");
+    }
+  }
+
   Future<KeyAttributes> createNewRecoveryKey() async {
     final masterKey = getKey()!;
     final existingAttributes = getKeyAttributes();

+ 2 - 12
lib/services/billing_service.dart

@@ -8,7 +8,6 @@ import 'package:flutter/material.dart';
 // import 'package:flutter_inapp_purchase/flutter_inapp_purchase.dart';
 import 'package:in_app_purchase/in_app_purchase.dart';
 import 'package:logging/logging.dart';
-import 'package:photos/core/configuration.dart';
 import 'package:photos/core/errors.dart';
 import 'package:photos/core/network.dart';
 import 'package:photos/models/billing_plan.dart';
@@ -35,9 +34,7 @@ class BillingService {
   static final BillingService instance = BillingService._privateConstructor();
 
   final _logger = Logger("BillingService");
-  final _dio = Network.instance.getDio();
   final _enteDio = Network.instance.enteDio;
-  final _config = Configuration.instance;
 
   bool _isOnSubscriptionPage = false;
 
@@ -74,23 +71,16 @@ class BillingService {
   }
 
   Future<BillingPlans> getBillingPlans() {
-    _future ??= (_config.isLoggedIn()
-            ? _fetchPublicBillingPlans()
-            : _fetchPrivateBillingPlans())
-        .then((response) {
+    _future ??= _fetchBillingPlans().then((response) {
       return BillingPlans.fromMap(response.data);
     });
     return _future;
   }
 
-  Future<Response<dynamic>> _fetchPrivateBillingPlans() {
+  Future<Response<dynamic>> _fetchBillingPlans() {
     return _enteDio.get("/billing/user-plans/");
   }
 
-  Future<Response<dynamic>> _fetchPublicBillingPlans() {
-    return _dio.get(_config.getHttpEndpoint() + "/billing/plans/v2");
-  }
-
   Future<Subscription> verifySubscription(
     final productID,
     final verificationData, {

+ 2 - 1
lib/services/update_service.dart

@@ -38,7 +38,8 @@ class UpdateService {
     return _prefs.setInt(changeLogVersionKey, currentChangeLogVersion);
   }
 
-  Future<bool> resetChangeLog() {
+  Future<bool> resetChangeLog() async {
+    await _prefs.remove("userNotify.passwordReminderFlag");
     return _prefs.remove(changeLogVersionKey);
   }
 

+ 18 - 3
lib/services/user_remote_flag_service.dart

@@ -20,6 +20,8 @@ class UserRemoteFlagService {
       UserRemoteFlagService._privateConstructor();
 
   static const String recoveryVerificationFlag = "recoveryKeyVerified";
+  static const String _passwordReminderFlag = "userNotify"
+      ".passwordReminderFlag";
   static const String needRecoveryKeyVerification =
       "needRecoveryKeyVerification";
 
@@ -27,6 +29,20 @@ class UserRemoteFlagService {
     _prefs = await SharedPreferences.getInstance();
   }
 
+  bool showPasswordReminder() {
+    if (Platform.isAndroid) {
+      return false;
+    }
+    return !_prefs.containsKey(_passwordReminderFlag);
+  }
+
+  Future<bool> stopPasswordReminder() async {
+    if (Platform.isAndroid) {
+      return Future.value(true);
+    }
+    return _prefs.setBool(_passwordReminderFlag, true);
+  }
+
   bool shouldShowRecoveryVerification() {
     if (!_prefs.containsKey(needRecoveryKeyVerification)) {
       // fetch the status from remote
@@ -46,14 +62,13 @@ class UserRemoteFlagService {
   // recovery key in the past or not. This helps in avoid showing the same
   // prompt to the user on re-install or signing into a different device
   Future<void> markRecoveryVerificationAsDone() async {
-    await _updateKeyValue(recoveryVerificationFlag, true.toString());
+    await _updateKeyValue(_passwordReminderFlag, true.toString());
     await _prefs.setBool(needRecoveryKeyVerification, false);
   }
 
   Future<void> _refreshRecoveryVerificationFlag() async {
     _logger.finest('refresh recovery key verification flag');
-    final remoteStatusValue =
-        await _getValue(recoveryVerificationFlag, "false");
+    final remoteStatusValue = await _getValue(_passwordReminderFlag, "false");
     final bool isNeedVerificationFlagSet =
         _prefs.containsKey(needRecoveryKeyVerification);
     if (remoteStatusValue.toLowerCase() == "true") {

+ 32 - 0
lib/ui/components/keyboard/keybiard_oveylay.dart

@@ -0,0 +1,32 @@
+import 'package:flutter/widgets.dart';
+
+class KeyboardOverlay {
+  static OverlayEntry? _overlayEntry;
+
+  static showOverlay(BuildContext context, Widget child) {
+    if (_overlayEntry != null) {
+      return;
+    }
+
+    final OverlayState? overlayState = Overlay.of(context);
+    _overlayEntry = OverlayEntry(
+      builder: (context) {
+        return Positioned(
+          bottom: MediaQuery.of(context).viewInsets.bottom,
+          right: 0.0,
+          left: 0.0,
+          child: child,
+        );
+      },
+    );
+
+    overlayState!.insert(_overlayEntry!);
+  }
+
+  static removeOverlay() {
+    if (_overlayEntry != null) {
+      _overlayEntry!.remove();
+      _overlayEntry = null;
+    }
+  }
+}

+ 52 - 0
lib/ui/components/keyboard/keyboard_top_button.dart

@@ -0,0 +1,52 @@
+import 'package:flutter/cupertino.dart';
+import 'package:flutter/material.dart';
+import 'package:photos/theme/ente_theme.dart';
+
+class KeyboardTopButton extends StatelessWidget {
+  final VoidCallback? onDoneTap;
+  final VoidCallback? onCancelTap;
+  final String doneText;
+  final String cancelText;
+
+  const KeyboardTopButton({
+    super.key,
+    this.doneText = "Done",
+    this.cancelText = "Cancel",
+    this.onDoneTap,
+    this.onCancelTap,
+  });
+
+  @override
+  Widget build(BuildContext context) {
+    final enteTheme = getEnteTextTheme(context);
+    final colorScheme = getEnteColorScheme(context);
+    return Container(
+      width: double.infinity,
+      decoration: BoxDecoration(
+        border: Border(
+          top: BorderSide(width: 1.0, color: colorScheme.strokeFaint),
+          bottom: BorderSide(width: 1.0, color: colorScheme.strokeFaint),
+        ),
+        color: colorScheme.backgroundElevated2,
+      ),
+      child: Padding(
+        padding: const EdgeInsets.symmetric(horizontal: 10.0),
+        child: Row(
+          mainAxisAlignment: MainAxisAlignment.spaceBetween,
+          children: [
+            CupertinoButton(
+              padding: const EdgeInsets.symmetric(vertical: 10, horizontal: 14),
+              onPressed: onCancelTap,
+              child: Text(cancelText, style: enteTheme.bodyBold),
+            ),
+            CupertinoButton(
+              padding: const EdgeInsets.symmetric(vertical: 10, horizontal: 14),
+              onPressed: onDoneTap,
+              child: Text(doneText, style: enteTheme.bodyBold),
+            ),
+          ],
+        ),
+      ),
+    );
+  }
+}

+ 3 - 0
lib/ui/home/landing_page_widget.dart

@@ -7,6 +7,7 @@ import 'package:flutter/material.dart';
 import 'package:photos/core/configuration.dart';
 import 'package:photos/ente_theme_data.dart';
 import 'package:photos/services/update_service.dart';
+import 'package:photos/services/user_remote_flag_service.dart';
 import 'package:photos/ui/account/email_entry_page.dart';
 import 'package:photos/ui/account/login_page.dart';
 import 'package:photos/ui/account/password_entry_page.dart';
@@ -154,6 +155,7 @@ class _LandingPageWidgetState extends State<LandingPageWidget> {
 
   void _navigateToSignUpPage() {
     UpdateService.instance.hideChangeLog().ignore();
+    UserRemoteFlagService.instance.stopPasswordReminder().ignore();
     Widget page;
     if (Configuration.instance.getEncryptedToken() == null) {
       page = const EmailEntryPage();
@@ -181,6 +183,7 @@ class _LandingPageWidgetState extends State<LandingPageWidget> {
 
   void _navigateToSignInPage() {
     UpdateService.instance.hideChangeLog().ignore();
+    UserRemoteFlagService.instance.stopPasswordReminder().ignore();
     Widget page;
     if (Configuration.instance.getEncryptedToken() == null) {
       page = const LoginPage();

+ 6 - 0
lib/ui/home_widget.dart

@@ -25,6 +25,7 @@ import 'package:photos/models/selected_files.dart';
 import 'package:photos/services/collections_service.dart';
 import 'package:photos/services/local_sync_service.dart';
 import 'package:photos/services/update_service.dart';
+import 'package:photos/services/user_remote_flag_service.dart';
 import 'package:photos/services/user_service.dart';
 import 'package:photos/states/user_details_state.dart';
 import 'package:photos/theme/colors.dart';
@@ -41,6 +42,7 @@ import 'package:photos/ui/home/landing_page_widget.dart';
 import 'package:photos/ui/home/preserve_footer_widget.dart';
 import 'package:photos/ui/home/start_backup_hook_widget.dart';
 import 'package:photos/ui/loading_photos_widget.dart';
+import 'package:photos/ui/notification/prompts/password_reminder.dart';
 import 'package:photos/ui/notification/update/change_log_page.dart';
 import 'package:photos/ui/settings/app_update_dialog.dart';
 import 'package:photos/ui/settings_page.dart';
@@ -318,6 +320,10 @@ class _HomeWidgetState extends State<HomeWidget> {
     if (!LocalSyncService.instance.hasCompletedFirstImport()) {
       return const LoadingPhotosWidget();
     }
+
+    if (UserRemoteFlagService.instance.showPasswordReminder()) {
+      return const PasswordReminder();
+    }
     if (_sharedFiles != null && _sharedFiles.isNotEmpty) {
       ReceiveSharingIntent.reset();
       return CreateCollectionPage(null, _sharedFiles);

+ 3 - 1
lib/ui/huge_listview/lazy_loading_gallery.dart

@@ -162,7 +162,9 @@ class _LazyLoadingGalleryState extends State<LazyLoadingGallery> {
     _reloadEventSubscription.cancel();
     _currentIndexSubscription.cancel();
     widget.selectedFiles.removeListener(_selectedFilesListener);
-
+    _toggleSelectAllFromDay.dispose();
+    _showSelectAllButton.dispose();
+    _areAllFromDaySelected.dispose();
     super.dispose();
   }
 

+ 371 - 0
lib/ui/notification/prompts/password_reminder.dart

@@ -0,0 +1,371 @@
+import 'dart:ui';
+
+import 'package:flutter/material.dart';
+import 'package:logging/logging.dart';
+import 'package:pedantic/pedantic.dart';
+import 'package:photos/core/configuration.dart';
+import 'package:photos/ente_theme_data.dart';
+import 'package:photos/services/local_authentication_service.dart';
+import 'package:photos/services/user_remote_flag_service.dart';
+import 'package:photos/theme/colors.dart';
+import 'package:photos/theme/ente_theme.dart';
+import 'package:photos/ui/account/password_entry_page.dart';
+import 'package:photos/ui/common/gradient_button.dart';
+import 'package:photos/ui/home_widget.dart';
+import 'package:photos/utils/dialog_util.dart';
+import 'package:photos/utils/navigation_util.dart';
+
+class PasswordReminder extends StatefulWidget {
+  const PasswordReminder({Key? key}) : super(key: key);
+
+  @override
+  State<PasswordReminder> createState() => _PasswordReminderState();
+}
+
+class _PasswordReminderState extends State<PasswordReminder> {
+  final _passwordController = TextEditingController();
+  final Logger _logger = Logger((_PasswordReminderState).toString());
+  bool _password2Visible = false;
+  bool _incorrectPassword = false;
+
+  Future<void> _verifyRecoveryKey() async {
+    final dialog = createProgressDialog(context, "Verifying password...");
+    await dialog.show();
+    try {
+      final String inputKey = _passwordController.text;
+      await Configuration.instance.verifyPassword(inputKey);
+      await dialog.hide();
+      UserRemoteFlagService.instance.stopPasswordReminder().ignore();
+      // todo: change this as per figma once the component is ready
+      await showErrorDialog(
+        context,
+        "Password verified",
+        "Great! Thank you for verifying.\n"
+            "\nPlease"
+            " remember to keep your recovery key safely backed up.",
+      );
+
+      unawaited(
+        Navigator.of(context).pushAndRemoveUntil(
+          MaterialPageRoute(
+            builder: (BuildContext context) {
+              return const HomeWidget();
+            },
+          ),
+          (route) => false,
+        ),
+      );
+    } catch (e, s) {
+      _logger.severe("failed to verify password", e, s);
+      await dialog.hide();
+      _incorrectPassword = true;
+      if (mounted) {
+        setState(() => {});
+      }
+    }
+  }
+
+  Future<void> _onChangePasswordClick() async {
+    try {
+      final hasAuthenticated =
+          await LocalAuthenticationService.instance.requestLocalAuthentication(
+        context,
+        "Please authenticate to change your password",
+      );
+      if (hasAuthenticated) {
+        UserRemoteFlagService.instance.stopPasswordReminder().ignore();
+        await routeToPage(
+          context,
+          const PasswordEntryPage(
+            mode: PasswordEntryMode.update,
+          ),
+          forceCustomPageRoute: true,
+        );
+        unawaited(
+          Navigator.of(context).pushAndRemoveUntil(
+            MaterialPageRoute(
+              builder: (BuildContext context) {
+                return const HomeWidget();
+              },
+            ),
+            (route) => false,
+          ),
+        );
+      }
+    } catch (e) {
+      showGenericErrorDialog(context);
+      return;
+    }
+  }
+
+  Future<void> _onSkipClick() async {
+    final enteTextTheme = getEnteTextTheme(context);
+    final enteColor = getEnteColorScheme(context);
+    final content = Column(
+      crossAxisAlignment: CrossAxisAlignment.start,
+      mainAxisSize: MainAxisSize.min,
+      children: [
+        Text(
+          "You will not be able to access your photos if you forget "
+          "your password.\n\nIf you do not remember your password, "
+          "now is a good time to change it.",
+          style: enteTextTheme.body.copyWith(
+            color: enteColor.textMuted,
+          ),
+        ),
+        const Padding(padding: EdgeInsets.all(8)),
+        SizedBox(
+          width: double.infinity,
+          height: 52,
+          child: OutlinedButton(
+            style: Theme.of(context).outlinedButtonTheme.style?.copyWith(
+              textStyle: MaterialStateProperty.resolveWith<TextStyle>(
+                (Set<MaterialState> states) {
+                  return enteTextTheme.bodyBold;
+                },
+              ),
+            ),
+            onPressed: () async {
+              Navigator.of(context, rootNavigator: true).pop('dialog');
+
+              _onChangePasswordClick();
+            },
+            child: const Text(
+              "Change password",
+            ),
+          ),
+        ),
+        const Padding(padding: EdgeInsets.all(8)),
+        SizedBox(
+          width: double.infinity,
+          height: 52,
+          child: OutlinedButton(
+            style: Theme.of(context).outlinedButtonTheme.style?.copyWith(
+              textStyle: MaterialStateProperty.resolveWith<TextStyle>(
+                (Set<MaterialState> states) {
+                  return enteTextTheme.bodyBold;
+                },
+              ),
+              backgroundColor: MaterialStateProperty.resolveWith<Color>(
+                (Set<MaterialState> states) {
+                  return enteColor.fillFaint;
+                },
+              ),
+              foregroundColor: MaterialStateProperty.resolveWith<Color>(
+                (Set<MaterialState> states) {
+                  return Theme.of(context).colorScheme.defaultTextColor;
+                },
+              ),
+            ),
+            onPressed: () async {
+              Navigator.of(context, rootNavigator: true).pop('dialog');
+            },
+            child: Text(
+              "Cancel",
+              style: enteTextTheme.bodyBold,
+            ),
+          ),
+        )
+      ],
+    );
+    return showDialog(
+      context: context,
+      builder: (BuildContext context) {
+        return AlertDialog(
+          backgroundColor: enteColor.backgroundElevated,
+          title: Column(
+            crossAxisAlignment: CrossAxisAlignment.start,
+            children: [
+              Icon(
+                Icons.report_outlined,
+                size: 36,
+                color: getEnteColorScheme(context).strokeBase,
+              ),
+            ],
+          ),
+          content: content,
+        );
+      },
+      barrierColor: enteColor.backdropBaseMute,
+    );
+  }
+
+  @override
+  void dispose() {
+    _passwordController.dispose();
+    super.dispose();
+  }
+
+  @override
+  Widget build(BuildContext context) {
+    final enteTheme = Theme.of(context).colorScheme.enteTheme;
+    final List<Widget> actions = <Widget>[];
+    actions.add(
+      PopupMenuButton(
+        itemBuilder: (context) {
+          return [
+            PopupMenuItem(
+              value: 1,
+              child: SizedBox(
+                width: 120,
+                height: 32,
+                child: Row(
+                  mainAxisAlignment: MainAxisAlignment.start,
+                  crossAxisAlignment: CrossAxisAlignment.center,
+                  children: [
+                    const Icon(
+                      Icons.report_outlined,
+                      color: warning500,
+                      size: 20,
+                    ),
+                    const Padding(padding: EdgeInsets.symmetric(horizontal: 6)),
+                    Text(
+                      "Skip",
+                      style: getEnteTextTheme(context)
+                          .bodyBold
+                          .copyWith(color: warning500),
+                    ),
+                  ],
+                ),
+              ),
+            ),
+          ];
+        },
+        onSelected: (value) async {
+          _onSkipClick();
+        },
+      ),
+    );
+
+    return Scaffold(
+      appBar: AppBar(
+        elevation: 0,
+        leading: null,
+        automaticallyImplyLeading: false,
+        actions: actions,
+      ),
+      body: Padding(
+        padding: const EdgeInsets.symmetric(horizontal: 20.0),
+        child: LayoutBuilder(
+          builder: (context, constraints) {
+            return SingleChildScrollView(
+              child: ConstrainedBox(
+                constraints: BoxConstraints(
+                  minWidth: constraints.maxWidth,
+                  minHeight: constraints.maxHeight,
+                ),
+                child: IntrinsicHeight(
+                  child: Column(
+                    mainAxisAlignment: MainAxisAlignment.start,
+                    children: [
+                      SizedBox(
+                        width: double.infinity,
+                        child: Column(
+                          crossAxisAlignment: CrossAxisAlignment.start,
+                          mainAxisAlignment: MainAxisAlignment.start,
+                          children: [
+                            Text(
+                              'Password reminder',
+                              style: enteTheme.textTheme.h3Bold,
+                            ),
+                            Text(
+                              Configuration.instance.getEmail()!,
+                              style: enteTheme.textTheme.small.copyWith(
+                                color: enteTheme.colorScheme.textMuted,
+                              ),
+                            ),
+                          ],
+                        ),
+                      ),
+                      const SizedBox(height: 18),
+                      Text(
+                        "Enter your password to ensure you remember it."
+                        "\n\nThe developer account we use to publish ente on App Store will change in the next version, so you will need to login again when the next version is released.",
+                        style: enteTheme.textTheme.small
+                            .copyWith(color: enteTheme.colorScheme.textMuted),
+                      ),
+                      const SizedBox(height: 24),
+                      TextFormField(
+                        autofillHints: const [AutofillHints.password],
+                        decoration: InputDecoration(
+                          filled: true,
+                          hintText: "Password",
+                          suffixIcon: IconButton(
+                            icon: Icon(
+                              _password2Visible
+                                  ? Icons.visibility
+                                  : Icons.visibility_off,
+                              color: Theme.of(context).iconTheme.color,
+                              size: 20,
+                            ),
+                            onPressed: () {
+                              setState(() {
+                                _password2Visible = !_password2Visible;
+                              });
+                            },
+                          ),
+                          contentPadding: const EdgeInsets.all(20),
+                          border: UnderlineInputBorder(
+                            borderSide: BorderSide.none,
+                            borderRadius: BorderRadius.circular(6),
+                          ),
+                        ),
+                        style: const TextStyle(
+                          fontSize: 14,
+                          fontFeatures: [FontFeature.tabularFigures()],
+                        ),
+                        controller: _passwordController,
+                        autofocus: false,
+                        autocorrect: false,
+                        obscureText: !_password2Visible,
+                        keyboardType: TextInputType.visiblePassword,
+                        onChanged: (_) {
+                          _incorrectPassword = false;
+                          setState(() {});
+                        },
+                      ),
+                      _incorrectPassword
+                          ? const SizedBox(height: 2)
+                          : const SizedBox.shrink(),
+                      _incorrectPassword
+                          ? Align(
+                              alignment: Alignment.centerLeft,
+                              child: Text(
+                                "Incorrect password",
+                                style: enteTheme.textTheme.small.copyWith(
+                                  color: enteTheme.colorScheme.warning700,
+                                ),
+                              ),
+                            )
+                          : const SizedBox.shrink(),
+                      const SizedBox(height: 12),
+                      Expanded(
+                        child: Container(
+                          alignment: Alignment.bottomCenter,
+                          width: double.infinity,
+                          padding: const EdgeInsets.fromLTRB(0, 12, 0, 40),
+                          child: Column(
+                            mainAxisAlignment: MainAxisAlignment.end,
+                            crossAxisAlignment: CrossAxisAlignment.stretch,
+                            children: [
+                              GradientButton(
+                                onTap: _verifyRecoveryKey,
+                                text: "Verify",
+                              ),
+                              const SizedBox(height: 8),
+                            ],
+                          ),
+                        ),
+                      ),
+                      const SizedBox(height: 20)
+                    ],
+                  ),
+                ),
+              ),
+            );
+          },
+        ),
+      ),
+    );
+  }
+}

+ 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);

+ 41 - 10
lib/ui/viewer/file/file_caption_widget.dart

@@ -2,6 +2,8 @@ import 'package:flutter/material.dart';
 import 'package:photos/core/constants.dart';
 import 'package:photos/models/file.dart';
 import 'package:photos/theme/ente_theme.dart';
+import 'package:photos/ui/components/keyboard/keybiard_oveylay.dart';
+import 'package:photos/ui/components/keyboard/keyboard_top_button.dart';
 import 'package:photos/utils/magic_util.dart';
 
 class FileCaptionWidget extends StatefulWidget {
@@ -18,10 +20,12 @@ class _FileCaptionWidgetState extends State<FileCaptionWidget> {
   // currentLength/maxLength will show up
   static const int counterThreshold = 1000;
   int currentLength = 0;
+
   final _textController = TextEditingController();
   final _focusNode = FocusNode();
   String? editedCaption;
   String hintText = fileCaptionDefaultHint;
+  Widget? keyboardTopButtoms;
 
   @override
   void initState() {
@@ -49,15 +53,7 @@ class _FileCaptionWidgetState extends State<FileCaptionWidget> {
     final textTheme = getEnteTextTheme(context);
     return TextField(
       onSubmitted: (value) async {
-        if (editedCaption != null) {
-          final isSuccesful =
-              await editFileCaption(context, widget.file, editedCaption);
-          if (isSuccesful) {
-            if (mounted) {
-              Navigator.pop(context);
-            }
-          }
-        }
+        await _onDoneClick(context);
       },
       controller: _textController,
       focusNode: _focusNode,
@@ -94,7 +90,7 @@ class _FileCaptionWidgetState extends State<FileCaptionWidget> {
       minLines: 1,
       maxLines: 10,
       textCapitalization: TextCapitalization.sentences,
-      keyboardType: TextInputType.text,
+      keyboardType: TextInputType.multiline,
       onChanged: (value) {
         setState(() {
           hintText = fileCaptionDefaultHint;
@@ -105,11 +101,46 @@ class _FileCaptionWidgetState extends State<FileCaptionWidget> {
     );
   }
 
+  Future<void> _onDoneClick(BuildContext context) async {
+    if (editedCaption != null) {
+      final isSuccesful =
+          await editFileCaption(context, widget.file, editedCaption);
+      if (isSuccesful) {
+        if (mounted) {
+          Navigator.pop(context);
+        }
+      }
+    }
+  }
+
+  void onCancelTap() {
+    _textController.text = widget.file.caption ?? '';
+    _focusNode.unfocus();
+    editedCaption = null;
+  }
+
+  void onDoneTap() {
+    _focusNode.unfocus();
+    _onDoneClick(context);
+  }
+
   void _focusNodeListener() {
     final caption = widget.file.caption;
     if (_focusNode.hasFocus && caption != null) {
       _textController.text = caption;
       editedCaption = caption;
     }
+    final bool hasFocus = _focusNode.hasFocus;
+    keyboardTopButtoms ??= KeyboardTopButton(
+      onDoneTap: onDoneTap,
+      onCancelTap: onCancelTap,
+    );
+    if (hasFocus) {
+      KeyboardOverlay.showOverlay(context, keyboardTopButtoms!);
+    } else {
+      debugPrint("Removing listener");
+
+      KeyboardOverlay.removeOverlay();
+    }
   }
 }

+ 1 - 1
lib/ui/viewer/gallery/collection_page.dart

@@ -20,7 +20,7 @@ class CollectionPage extends StatelessWidget {
   final String tagPrefix;
   final GalleryType appBarType;
   final _selectedFiles = SelectedFiles();
-  bool hasVerifiedLock;
+  final bool hasVerifiedLock;
 
   CollectionPage(
     this.c, {

+ 15 - 24
lib/utils/date_time_util.dart

@@ -45,7 +45,7 @@ Map<int, String> _days = {
   7: "Sun",
 };
 
-final currentYear = int.parse(DateTime.now().year.toString());
+final currentYear = DateTime.now().year;
 const searchStartYear = 1970;
 
 //Jun 2022
@@ -267,30 +267,18 @@ bool isValidDate({
   return true;
 }
 
-@Deprecated("Use parseDateTimeV2 ")
-DateTime? parseDateFromFileName(String fileName) {
-  if (fileName.startsWith('IMG-') || fileName.startsWith('VID-')) {
-// Whatsapp media files
-    return DateTime.tryParse(fileName.split('-')[1]);
-  } else if (fileName.startsWith("Screenshot_")) {
-// Screenshots on droid
-    return DateTime.tryParse(
-      (fileName).replaceAll('Screenshot_', '').replaceAll('-', 'T'),
-    );
-  } else {
-    return DateTime.tryParse(
-      (fileName)
-          .replaceAll("IMG_", "")
-          .replaceAll("VID_", "")
-          .replaceAll("DCIM_", "")
-          .replaceAll("_", " "),
-    );
-  }
-}
-
 final RegExp exp = RegExp('[\\.A-Za-z]*');
 
-DateTime? parseDateTimeFromFileNameV2(String fileName) {
+DateTime? parseDateTimeFromFileNameV2(
+  String fileName, {
+  /* to avoid parsing incorrect date time from the filename, the max and min
+    year limits the chances of parsing incorrect date times
+    */
+  int minYear = 1990,
+  int? maxYear,
+}) {
+  // add next year to avoid corner cases for 31st Dec
+  maxYear ??= currentYear + 1;
   String val = fileName.replaceAll(exp, '');
   if (val.isNotEmpty && !isNumeric(val[0])) {
     val = val.substring(1, val.length);
@@ -319,7 +307,10 @@ DateTime? parseDateTimeFromFileNameV2(String fileName) {
   if (kDebugMode && result == null) {
     debugPrint("Failed to parse $fileName dateTime from $valForParser");
   }
-  return result;
+  if (result != null && result.year >= minYear && result.year <= maxYear) {
+    return result;
+  }
+  return null;
 }
 
 bool isNumeric(String? s) {

+ 1 - 1
lib/utils/file_uploader.dart

@@ -153,7 +153,7 @@ class FileUploader {
       }
       return CollectionsService.instance
           .addToCollection(collectionID, [uploadedFile]).then((aVoid) {
-        return uploadedFile;
+        return uploadedFile as File;
       });
     });
   }

+ 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) {

+ 7 - 0
pubspec.lock

@@ -700,6 +700,13 @@ packages:
       url: "https://pub.dartlang.org"
     source: hosted
     version: "4.7.0"
+  keyboard_visibility:
+    dependency: "direct main"
+    description:
+      name: keyboard_visibility
+      url: "https://pub.dartlang.org"
+    source: hosted
+    version: "0.5.6"
   like_button:
     dependency: "direct main"
     description:

+ 1 - 0
pubspec.yaml

@@ -72,6 +72,7 @@ dependencies:
   implicitly_animated_reorderable_list: ^0.4.0
   in_app_purchase: ^3.0.7
   intl: ^0.17.0
+  keyboard_visibility: ^0.5.6
   like_button: ^2.0.2
   loading_animations: ^2.1.0
   local_auth: ^1.1.5

+ 17 - 2
test/utils/date_time_util_test.dart

@@ -9,14 +9,14 @@ void main() {
       "IMG-20221109-WA0000",
       '''Screenshot_20220807-195908_Firefox''',
       '''Screenshot_20220507-195908''',
-      "2019-02-18 16.00.12-DCMX.png",
+      "2022-02-18 16.00.12-DCMX.png",
       "20221107_231730",
       "2020-11-01 02.31.02",
       "IMG_20210921_144423",
       "2019-10-31 155703",
       "IMG_20210921_144423_783",
       "Screenshot_2022-06-21-16-51-29-164_newFormat.heic",
-      "Screenshot 20221106 211633.com.google.android.apps.nbu.paisa.user.jpg"
+      "Screenshot 20221106 211633.com.google.android.apps.nbu.paisa.user.jpg",
     ];
     for (String val in validParsing) {
       final parsedValue = parseDateTimeFromFileNameV2(val);
@@ -31,6 +31,21 @@ void main() {
     }
   });
 
+  test("test invalid datetime parsing", () {
+    final List<String> badParsing = ["Snapchat-431959199.mp4."];
+    for (String val in badParsing) {
+      final parsedValue = parseDateTimeFromFileNameV2(val);
+      expect(
+        parsedValue == null,
+        true,
+        reason: "parsing should have failed $val",
+      );
+      if (kDebugMode) {
+        debugPrint("Parsed $val as ${parsedValue?.toIso8601String()}");
+      }
+    }
+  });
+
   test("verify constants", () {
     final date = DateTime.fromMicrosecondsSinceEpoch(jan011981Time).toUtc();
     expect(