Explorar o código

Resolved merge conflicts

ashilkn %!s(int64=2) %!d(string=hai) anos
pai
achega
caeb3ef84e
Modificáronse 100 ficheiros con 1444 adicións e 1404 borrados
  1. 4 0
      android/app/build.gradle
  2. 10 0
      android/app/src/dev/AndroidManifest.xml
  3. 4 0
      android/app/src/dev/res/values/strings.xml
  4. 9 18
      ios/Runner.xcodeproj/project.pbxproj
  5. 2 2
      lib/app.dart
  6. 3 3
      lib/core/configuration.dart
  7. 45 41
      lib/core/error-reporting/super_logging.dart
  8. 11 13
      lib/core/error-reporting/tunneled_transport.dart
  9. 25 23
      lib/db/device_files_db.dart
  10. 4 5
      lib/db/file_updation_db.dart
  11. 70 63
      lib/db/files_db.dart
  12. 2 4
      lib/db/ignored_files_db.dart
  13. 4 5
      lib/db/memories_db.dart
  14. 12 14
      lib/db/trash_db.dart
  15. 1 3
      lib/events/collection_updated_event.dart
  16. 0 2
      lib/events/files_updated_event.dart
  17. 3 3
      lib/main.dart
  18. 4 2
      lib/models/collection.dart
  19. 3 4
      lib/models/duplicate_files.dart
  20. 1 4
      lib/models/file.dart
  21. 2 2
      lib/models/key_attributes.dart
  22. 1 3
      lib/services/billing_service.dart
  23. 158 156
      lib/services/collections_service.dart
  24. 7 8
      lib/services/deduplication_service.dart
  25. 28 32
      lib/services/favorites_service.dart
  26. 34 30
      lib/services/file_magic_service.dart
  27. 4 5
      lib/services/hidden_service.dart
  28. 21 18
      lib/services/ignored_files_service.dart
  29. 14 14
      lib/services/local/local_sync_util.dart
  30. 5 7
      lib/services/local_authentication_service.dart
  31. 23 23
      lib/services/local_file_update_service.dart
  32. 54 17
      lib/services/local_sync_service.dart
  33. 6 8
      lib/services/memories_service.dart
  34. 10 8
      lib/services/push_service.dart
  35. 40 36
      lib/services/remote_sync_service.dart
  36. 20 21
      lib/services/search_service.dart
  37. 14 14
      lib/services/sync_service.dart
  38. 4 6
      lib/services/trash_sync_service.dart
  39. 6 8
      lib/services/update_service.dart
  40. 36 37
      lib/services/user_service.dart
  41. 1 1
      lib/ui/account/change_email_dialog.dart
  42. 5 7
      lib/ui/account/delete_account_page.dart
  43. 15 17
      lib/ui/account/email_entry_page.dart
  44. 10 10
      lib/ui/account/login_page.dart
  45. 6 9
      lib/ui/account/ott_verification_page.dart
  46. 15 17
      lib/ui/account/password_entry_page.dart
  47. 6 15
      lib/ui/account/password_reentry_page.dart
  48. 13 13
      lib/ui/account/recovery_key_page.dart
  49. 4 4
      lib/ui/account/recovery_page.dart
  50. 0 1
      lib/ui/actions/collection/collection_file_actions.dart
  51. 1 2
      lib/ui/actions/collection/collection_sharing_actions.dart
  52. 18 20
      lib/ui/backup_folder_selection_page.dart
  53. 1 1
      lib/ui/collections/archived_collections_button_widget.dart
  54. 7 7
      lib/ui/collections/device_folders_grid_view_widget.dart
  55. 3 3
      lib/ui/collections/hidden_collections_button_widget.dart
  56. 6 6
      lib/ui/collections/remote_collections_grid_view_widget.dart
  57. 23 15
      lib/ui/collections_gallery_widget.dart
  58. 2 2
      lib/ui/common/DividerWithPadding.dart
  59. 3 3
      lib/ui/common/bottom_shadow.dart
  60. 4 4
      lib/ui/common/dialogs.dart
  61. 15 15
      lib/ui/common/dynamic_fab.dart
  62. 5 5
      lib/ui/common/gradient_button.dart
  63. 3 3
      lib/ui/common/linear_progress_dialog.dart
  64. 36 36
      lib/ui/common/progress_dialog.dart
  65. 7 7
      lib/ui/common/rename_dialog.dart
  66. 2 2
      lib/ui/common/web_page.dart
  67. 2 3
      lib/ui/components/bottom_action_bar/bottom_action_bar_widget.dart
  68. 33 0
      lib/ui/components/button_widget.dart
  69. 0 1
      lib/ui/components/keyboard/keyboard_top_button.dart
  70. 26 24
      lib/ui/create_collection_page.dart
  71. 24 28
      lib/ui/extents_page_view.dart
  72. 4 4
      lib/ui/home/header_error_widget.dart
  73. 0 1
      lib/ui/home/home_bottom_nav_bar.dart
  74. 6 7
      lib/ui/home/home_gallery_widget.dart
  75. 16 16
      lib/ui/home/memories_widget.dart
  76. 29 31
      lib/ui/home/status_bar_widget.dart
  77. 18 18
      lib/ui/home_widget.dart
  78. 14 16
      lib/ui/huge_listview/draggable_scrollbar.dart
  79. 15 15
      lib/ui/huge_listview/huge_listview.dart
  80. 39 40
      lib/ui/huge_listview/lazy_loading_gallery.dart
  81. 14 14
      lib/ui/huge_listview/scroll_bar_thumb.dart
  82. 5 5
      lib/ui/lifecycle_event_handler.dart
  83. 6 6
      lib/ui/loading_photos_widget.dart
  84. 93 93
      lib/ui/nav_bar.dart
  85. 2 2
      lib/ui/notification/update/change_log_page.dart
  86. 12 16
      lib/ui/payment/billing_questions_widget.dart
  87. 27 28
      lib/ui/payment/payment_web_page.dart
  88. 30 32
      lib/ui/payment/stripe_subscription_page.dart
  89. 14 14
      lib/ui/payment/subscription_common_widgets.dart
  90. 33 35
      lib/ui/payment/subscription_page.dart
  91. 7 7
      lib/ui/payment/subscription_plan_widget.dart
  92. 3 1
      lib/ui/settings/about_section_widget.dart
  93. 15 15
      lib/ui/settings/app_update_dialog.dart
  94. 4 4
      lib/ui/settings/backup_section_widget.dart
  95. 0 2
      lib/ui/settings/settings_title_bar_widget.dart
  96. 0 1
      lib/ui/settings/social_section_widget.dart
  97. 4 4
      lib/ui/settings/theme_switch_widget.dart
  98. 5 5
      lib/ui/settings_page.dart
  99. 28 28
      lib/ui/shared_collections_gallery.dart
  100. 1 1
      lib/ui/sharing/add_partipant_page.dart

+ 4 - 0
android/app/build.gradle

@@ -69,6 +69,10 @@ android {
             dimension "default"
             applicationIdSuffix ".independent"
         }
+        dev {
+            dimension "default"
+            applicationIdSuffix ".dev"
+        }
         playstore {
             dimension "default"
         }

+ 10 - 0
android/app/src/dev/AndroidManifest.xml

@@ -0,0 +1,10 @@
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+    package="io.ente.photos">
+    <!-- Flutter needs it to communicate with the running application
+         to allow setting breakpoints, to provide hot reload, etc.
+    -->
+    <uses-permission android:name="android.permission.INTERNET"/>
+    <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
+    <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
+    <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
+</manifest>

+ 4 - 0
android/app/src/dev/res/values/strings.xml

@@ -0,0 +1,4 @@
+<resources>
+    <string name="app_name">ente dev</string>
+    <string name="backup">backup dev</string>
+</resources>

+ 9 - 18
ios/Runner.xcodeproj/project.pbxproj

@@ -16,7 +16,7 @@
 		97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; };
 		97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; };
 		97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; };
-		DA6BE5E826B3BC8600656280 /* BuildFile in Resources */ = {isa = PBXBuildFile; };
+		DA6BE5E826B3BC8600656280 /* (null) in Resources */ = {isa = PBXBuildFile; };
 /* End PBXBuildFile section */
 
 /* Begin PBXCopyFilesBuildPhase section */
@@ -180,7 +180,7 @@
 				TargetAttributes = {
 					97C146ED1CF9000F007C117D = {
 						CreatedOnToolsVersion = 7.3.1;
-						DevelopmentTeam = 2BUSYC7FN9;
+						DevelopmentTeam = 6Z68YJY9Q2;
 						LastSwiftMigration = 1100;
 						ProvisioningStyle = Automatic;
 					};
@@ -213,7 +213,7 @@
 				3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */,
 				97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */,
 				97C146FC1CF9000F007C117D /* Main.storyboard in Resources */,
-				DA6BE5E826B3BC8600656280 /* BuildFile in Resources */,
+				DA6BE5E826B3BC8600656280 /* (null) in Resources */,
 				277218A0270F596900FFE3CC /* GoogleService-Info.plist in Resources */,
 			);
 			runOnlyForDeploymentPostprocessing = 0;
@@ -494,17 +494,14 @@
 				CODE_SIGN_IDENTITY = "Apple Development";
 				CODE_SIGN_STYLE = Automatic;
 				CURRENT_PROJECT_VERSION = 11;
-				DEVELOPMENT_TEAM = 2BUSYC7FN9;
+				DEVELOPMENT_TEAM = 6Z68YJY9Q2;
 				ENABLE_BITCODE = NO;
 				FRAMEWORK_SEARCH_PATHS = (
 					"$(inherited)",
 					"$(PROJECT_DIR)/Flutter",
 				);
 				INFOPLIST_FILE = Runner/Info.plist;
-				LD_RUNPATH_SEARCH_PATHS = (
-					"$(inherited)",
-					"@executable_path/Frameworks",
-				);
+				LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
 				LIBRARY_SEARCH_PATHS = (
 					"$(inherited)",
 					"$(PROJECT_DIR)/Flutter",
@@ -654,17 +651,14 @@
 				CODE_SIGN_IDENTITY = "Apple Development";
 				CODE_SIGN_STYLE = Automatic;
 				CURRENT_PROJECT_VERSION = 11;
-				DEVELOPMENT_TEAM = 2BUSYC7FN9;
+				DEVELOPMENT_TEAM = 6Z68YJY9Q2;
 				ENABLE_BITCODE = NO;
 				FRAMEWORK_SEARCH_PATHS = (
 					"$(inherited)",
 					"$(PROJECT_DIR)/Flutter",
 				);
 				INFOPLIST_FILE = Runner/Info.plist;
-				LD_RUNPATH_SEARCH_PATHS = (
-					"$(inherited)",
-					"@executable_path/Frameworks",
-				);
+				LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
 				LIBRARY_SEARCH_PATHS = (
 					"$(inherited)",
 					"$(PROJECT_DIR)/Flutter",
@@ -691,17 +685,14 @@
 				CODE_SIGN_IDENTITY = "Apple Development";
 				CODE_SIGN_STYLE = Automatic;
 				CURRENT_PROJECT_VERSION = 11;
-				DEVELOPMENT_TEAM = 2BUSYC7FN9;
+				DEVELOPMENT_TEAM = 6Z68YJY9Q2;
 				ENABLE_BITCODE = NO;
 				FRAMEWORK_SEARCH_PATHS = (
 					"$(inherited)",
 					"$(PROJECT_DIR)/Flutter",
 				);
 				INFOPLIST_FILE = Runner/Info.plist;
-				LD_RUNPATH_SEARCH_PATHS = (
-					"$(inherited)",
-					"@executable_path/Frameworks",
-				);
+				LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
 				LIBRARY_SEARCH_PATHS = (
 					"$(inherited)",
 					"$(PROJECT_DIR)/Flutter",

+ 2 - 2
lib/app.dart

@@ -1,4 +1,4 @@
-// @dart=2.9
+
 
 import 'dart:io';
 
@@ -23,7 +23,7 @@ class EnteApp extends StatefulWidget {
   const EnteApp(
     this.runBackgroundTask,
     this.killBackgroundTask, {
-    Key key,
+    Key? key,
   }) : super(key: key);
 
   @override

+ 3 - 3
lib/core/configuration.dart

@@ -525,9 +525,9 @@ class Configuration {
   Uint8List getRecoveryKey() {
     final keyAttributes = getKeyAttributes()!;
     return CryptoUtil.decryptSync(
-      Sodium.base642bin(keyAttributes.recoveryKeyEncryptedWithMasterKey),
+      Sodium.base642bin(keyAttributes.recoveryKeyEncryptedWithMasterKey!),
       getKey(),
-      Sodium.base642bin(keyAttributes.recoveryKeyDecryptionNonce),
+      Sodium.base642bin(keyAttributes.recoveryKeyDecryptionNonce!),
     );
   }
 
@@ -614,7 +614,7 @@ class Configuration {
     return _preferences.setBool(keyShouldHideFromRecents, value);
   }
 
-  void setVolatilePassword(String volatilePassword) {
+  void setVolatilePassword(String? volatilePassword) {
     _volatilePassword = volatilePassword;
   }
 

+ 45 - 41
lib/core/error-reporting/super_logging.dart

@@ -1,5 +1,3 @@
-// @dart=2.9
-
 library super_logging;
 
 import 'dart:async';
@@ -38,7 +36,7 @@ extension SuperString on String {
 }
 
 extension SuperLogRecord on LogRecord {
-  String toPrettyString([String extraLines]) {
+  String toPrettyString([String? extraLines]) {
     final header = "[$loggerName] [$level] [$time]";
 
     var msg = "$header $message";
@@ -76,9 +74,9 @@ class LogConfig {
   /// ```
   ///
   /// If this is [null], Sentry logger is completely disabled (default).
-  String sentryDsn;
+  String? sentryDsn;
 
-  String tunnel;
+  String? tunnel;
 
   /// A built-in retry mechanism for sending errors to sentry.
   ///
@@ -95,7 +93,7 @@ class LogConfig {
   /// A non-empty string will be treated as an explicit path to a directory.
   ///
   /// The chosen directory can be accessed using [SuperLogging.logFile.parent].
-  String logDirPath;
+  String? logDirPath;
 
   /// The maximum number of log files inside [logDirPath].
   ///
@@ -113,12 +111,12 @@ class LogConfig {
   /// any uncaught errors during its execution will be reported.
   ///
   /// Works by using [FlutterError.onError] and [runZoned].
-  FutureOrVoidCallback body;
+  FutureOrVoidCallback? body;
 
   /// The date format for storing log files.
   ///
   /// `DateFormat('y-M-d')` by default.
-  DateFormat dateFmt;
+  DateFormat? dateFmt;
 
   String prefix;
 
@@ -142,24 +140,24 @@ class SuperLogging {
   static final $ = Logger('ente_logging');
 
   /// The current super logging configuration
-  static LogConfig config;
+  static late LogConfig config;
 
-  static Future<void> main([LogConfig config]) async {
-    config ??= LogConfig();
-    SuperLogging.config = config;
+  static Future<void> main([LogConfig? appConfig]) async {
+    appConfig ??= LogConfig();
+    SuperLogging.config = appConfig;
 
     WidgetsFlutterBinding.ensureInitialized();
 
     appVersion ??= await getAppVersion();
     final isFDroidClient = await isFDroidBuild();
     if (isFDroidClient) {
-      config.sentryDsn = null;
-      config.tunnel = null;
+      appConfig.sentryDsn = null;
+      appConfig.tunnel = null;
     }
 
-    final enable = config.enableInDebugMode || kReleaseMode;
-    sentryIsEnabled = enable && config.sentryDsn != null && !isFDroidClient;
-    fileIsEnabled = enable && config.logDirPath != null;
+    final enable = appConfig.enableInDebugMode || kReleaseMode;
+    sentryIsEnabled = enable && appConfig.sentryDsn != null && !isFDroidClient;
+    fileIsEnabled = enable && appConfig.logDirPath != null;
 
     if (fileIsEnabled) {
       await setupLogDir();
@@ -175,7 +173,7 @@ class SuperLogging {
       assert(
         sentryIsEnabled == false,
         "sentry dsn should be disabled for "
-        "f-droid config  ${config.sentryDsn} & ${config.tunnel}",
+        "f-droid config  ${appConfig.sentryDsn} & ${appConfig.tunnel}",
       );
     }
 
@@ -183,39 +181,42 @@ class SuperLogging {
       $.info("detected debug mode; sentry & file logging disabled.");
     }
     if (fileIsEnabled) {
-      $.info("log file for today: $logFile with prefix ${config.prefix}");
+      $.info("log file for today: $logFile with prefix ${appConfig.prefix}");
     }
     if (sentryIsEnabled) {
       $.info("sentry uploader started");
     }
 
-    if (config.body == null) return;
+    if (appConfig.body == null) return;
 
     if (enable && sentryIsEnabled) {
       await SentryFlutter.init(
         (options) {
-          options.dsn = config.sentryDsn;
+          options.dsn = appConfig!.sentryDsn;
           options.httpClient = http.Client();
-          if (config.tunnel != null) {
+          if (appConfig.tunnel != null) {
             options.transport =
-                TunneledTransport(Uri.parse(config.tunnel), options);
+                TunneledTransport(Uri.parse(appConfig.tunnel!), options);
           }
         },
-        appRunner: () => config.body(),
+        appRunner: () => appConfig!.body!(),
       );
     } else {
-      await config.body();
+      await appConfig.body!();
     }
   }
 
   static void setUserID(String userID) async {
-    if (config?.sentryDsn != null) {
+    if (config.sentryDsn != null) {
       Sentry.configureScope((scope) => scope.user = SentryUser(id: userID));
       $.info("setting sentry user ID to: $userID");
     }
   }
 
-  static Future<void> _sendErrorToSentry(Object error, StackTrace stack) async {
+  static Future<void> _sendErrorToSentry(
+    Object error,
+    StackTrace? stack,
+  ) async {
     try {
       await Sentry.captureException(
         error,
@@ -231,14 +232,14 @@ class SuperLogging {
 
   static Future onLogRecord(LogRecord rec) async {
     // log misc info if it changed
-    String extraLines = "app version: '$appVersion'\n";
+    String? extraLines = "app version: '$appVersion'\n";
     if (extraLines != _lastExtraLines) {
       _lastExtraLines = extraLines;
     } else {
       extraLines = null;
     }
 
-    final str = config.prefix + " " + rec.toPrettyString(extraLines);
+    final str = (config.prefix) + " " + rec.toPrettyString(extraLines);
 
     // write to stdout
     printLog(str);
@@ -253,7 +254,7 @@ class SuperLogging {
 
     // add error to sentry queue
     if (sentryIsEnabled && rec.error != null) {
-      _sendErrorToSentry(rec.error, null);
+      _sendErrorToSentry(rec.error!, null);
     }
   }
 
@@ -261,12 +262,12 @@ class SuperLogging {
   static bool isFlushing = false;
 
   static void flushQueue() async {
-    if (isFlushing) {
+    if (isFlushing || logFile == null) {
       return;
     }
     isFlushing = true;
     final entry = fileQueueEntries.removeFirst();
-    await logFile.writeAsString(entry, mode: FileMode.append, flush: true);
+    await logFile!.writeAsString(entry, mode: FileMode.append, flush: true);
     isFlushing = false;
     if (fileQueueEntries.isNotEmpty) {
       flushQueue();
@@ -285,7 +286,7 @@ class SuperLogging {
   static final sentryQueueControl = StreamController<Error>();
 
   /// Whether sentry logging is currently enabled or not.
-  static bool sentryIsEnabled;
+  static bool sentryIsEnabled = false;
 
   static Future<void> setupSentry() async {
     await for (final error in sentryQueueControl.stream.asBroadcastStream()) {
@@ -308,18 +309,18 @@ class SuperLogging {
   }
 
   /// The log file currently in use.
-  static File logFile;
+  static File? logFile;
 
   /// Whether file logging is currently enabled or not.
-  static bool fileIsEnabled;
+  static bool fileIsEnabled = false;
 
   static Future<void> setupLogDir() async {
     var dirPath = config.logDirPath;
 
     // choose [logDir]
-    if (dirPath.isEmpty) {
+    if (dirPath == null || dirPath.isEmpty) {
       final root = await getExternalStorageDirectory();
-      dirPath = '${root.path}/logs';
+      dirPath = '${root!.path}/logs';
     }
 
     // create [logDir]
@@ -332,16 +333,19 @@ class SuperLogging {
     // collect all log files with valid names
     await for (final file in dir.list()) {
       try {
-        final date = config.dateFmt.parse(basename(file.path));
+        final date = config.dateFmt!.parse(basename(file.path));
         dates[file as File] = date;
         files.add(file);
       } on FormatException {}
     }
+    final nowTime = DateTime.now();
 
     // delete old log files, if [maxLogFiles] is exceeded.
     if (files.length > config.maxLogFiles) {
       // sort files based on ascending order of date (older first)
-      files.sort((a, b) => dates[a].compareTo(dates[b]));
+      files.sort(
+        (a, b) => (dates[a] ?? nowTime).compareTo((dates[b] ?? nowTime)),
+      );
 
       final extra = files.length - config.maxLogFiles;
       final toDelete = files.sublist(0, extra);
@@ -356,13 +360,13 @@ class SuperLogging {
       }
     }
 
-    logFile = File("$dirPath/${config.dateFmt.format(DateTime.now())}.txt");
+    logFile = File("$dirPath/${config.dateFmt!.format(DateTime.now())}.txt");
   }
 
   /// Current app version, obtained from package_info plugin.
   ///
   /// See: [getAppVersion]
-  static String appVersion;
+  static String? appVersion;
 
   static Future<String> getAppVersion() async {
     final pkgInfo = await PackageInfo.fromPlatform();

+ 11 - 13
lib/core/error-reporting/tunneled_transport.dart

@@ -1,5 +1,3 @@
-// @dart=2.9
-
 import 'dart:convert';
 
 import 'package:http/http.dart';
@@ -10,9 +8,9 @@ class TunneledTransport implements Transport {
   final Uri _tunnel;
   final SentryOptions _options;
 
-  final Dsn _dsn;
+  final Dsn? _dsn;
 
-  _CredentialBuilder _credentialBuilder;
+  _CredentialBuilder? _credentialBuilder;
 
   final Map<String, String> _headers;
 
@@ -21,7 +19,7 @@ class TunneledTransport implements Transport {
   }
 
   TunneledTransport._(this._tunnel, this._options)
-      : _dsn = Dsn.parse(_options.dsn),
+      : _dsn = _options.dsn != null ? Dsn.parse(_options.dsn!) : null,
         _headers = _buildHeaders(
           _options.platformChecker.isWeb,
           _options.sdk.identifier,
@@ -34,7 +32,7 @@ class TunneledTransport implements Transport {
   }
 
   @override
-  Future<SentryId> send(SentryEnvelope envelope) async {
+  Future<SentryId?> send(SentryEnvelope envelope) async {
     final streamedRequest = await _createStreamedRequest(envelope);
     final response = await _options.httpClient
         .send(streamedRequest)
@@ -74,7 +72,7 @@ class TunneledTransport implements Transport {
         .listen(streamedRequest.sink.add)
         .onDone(streamedRequest.sink.close);
 
-    streamedRequest.headers.addAll(_credentialBuilder.configure(_headers));
+    streamedRequest.headers.addAll(_credentialBuilder!.configure(_headers));
 
     return streamedRequest;
   }
@@ -92,13 +90,13 @@ class _CredentialBuilder {
         _clock = clock;
 
   factory _CredentialBuilder(
-    Dsn dsn,
+    Dsn? dsn,
     String sdkIdentifier,
     ClockProvider clock,
   ) {
     final authHeader = _buildAuthHeader(
-      publicKey: dsn.publicKey,
-      secretKey: dsn.secretKey,
+      publicKey: dsn?.publicKey,
+      secretKey: dsn?.secretKey,
       sdkIdentifier: sdkIdentifier,
     );
 
@@ -106,9 +104,9 @@ class _CredentialBuilder {
   }
 
   static String _buildAuthHeader({
-    String publicKey,
-    String secretKey,
-    String sdkIdentifier,
+    String? publicKey,
+    String? secretKey,
+    String? sdkIdentifier,
   }) {
     var header = 'Sentry sentry_version=7, sentry_client=$sdkIdentifier, '
         'sentry_key=$publicKey';

+ 25 - 23
lib/db/device_files_db.dart

@@ -1,4 +1,4 @@
-// @dart = 2.9
+import 'package:collection/collection.dart';
 import 'package:flutter/foundation.dart';
 import 'package:logging/logging.dart';
 import 'package:photo_manager/photo_manager.dart';
@@ -85,7 +85,7 @@ extension DeviceFiles on FilesDB {
       );
       final result = <String, int>{};
       for (final row in rows) {
-        result[row['path_id']] = row["count"];
+        result[row['path_id'] as String] = row["count"] as int;
       }
       return result;
     } catch (e) {
@@ -102,11 +102,11 @@ extension DeviceFiles on FilesDB {
       );
       final result = <String, Set<String>>{};
       for (final row in rows) {
-        final String pathID = row['path_id'];
+        final String pathID = row['path_id'] as String;
         if (!result.containsKey(pathID)) {
           result[pathID] = <String>{};
         }
-        result[pathID].add(row['id']);
+        result[pathID]!.add(row['id'] as String);
       }
       return result;
     } catch (e) {
@@ -124,7 +124,7 @@ extension DeviceFiles on FilesDB {
     );
     final Set<String> result = <String>{};
     for (final row in rows) {
-      result.add(row['id']);
+      result.add(row['id'] as String);
     }
     return result;
   }
@@ -220,8 +220,10 @@ extension DeviceFiles on FilesDB {
       existingPathIds.removeAll(devicePathInfo.map((e) => e.item1.id).toSet());
       if (existingPathIds.isNotEmpty) {
         hasUpdated = true;
-        _logger.info('Deleting non-backed up pathIds from local '
-            '$existingPathIds');
+        _logger.info(
+          'Deleting non-backed up pathIds from local '
+          '$existingPathIds',
+        );
         for (String pathID in existingPathIds) {
           // do not delete device collection entries for paths which are
           // marked for backup. This is to handle "Free up space"
@@ -260,7 +262,7 @@ extension DeviceFiles on FilesDB {
     );
     final Set<int> result = <int>{};
     for (final row in rows) {
-      result.add(row['collection_id']);
+      result.add(row['collection_id'] as int);
     }
     return result;
   }
@@ -307,8 +309,8 @@ extension DeviceFiles on FilesDB {
     DeviceCollection deviceCollection,
     int startTime,
     int endTime, {
-    int limit,
-    bool asc,
+    int? limit,
+    bool? asc,
   }) async {
     final db = await database;
     final order = (asc ?? false ? 'ASC' : 'DESC');
@@ -348,8 +350,9 @@ extension DeviceFiles on FilesDB {
     final localIDs = <String>{};
     final uploadedIDs = <int>{};
     for (final result in results) {
-      localIDs.add(result[FilesDB.columnLocalID]);
-      uploadedIDs.add(result[FilesDB.columnUploadedFileID]);
+      // FilesDB.[columnLocalID,columnUploadedFileID] is not null check in query
+      localIDs.add(result[FilesDB.columnLocalID] as String);
+      uploadedIDs.add(result[FilesDB.columnUploadedFileID] as int);
     }
     return BackedUpFileIDs(localIDs.toList(), uploadedIDs.toList());
   }
@@ -378,21 +381,20 @@ extension DeviceFiles on FilesDB {
       final List<DeviceCollection> deviceCollections = [];
       for (var row in deviceCollectionRows) {
         final DeviceCollection deviceCollection = DeviceCollection(
-          row["id"],
-          row['name'],
-          count: row['count'],
-          collectionID: row["collection_id"],
-          coverId: row["cover_id"],
+          row["id"] as String,
+          (row['name'] ?? '') as String,
+          count: row['count'] as int,
+          collectionID: (row["collection_id"] ?? -1) as int,
+          coverId: row["cover_id"] as String?,
           shouldBackup: (row["should_backup"] ?? _sqlBoolFalse) == _sqlBoolTrue,
-          uploadStrategy: getUploadType(row["upload_strategy"] ?? 0),
+          uploadStrategy: getUploadType((row["upload_strategy"] ?? 0) as int),
         );
         if (includeCoverThumbnail) {
-          deviceCollection.thumbnail = coverFiles.firstWhere(
+          deviceCollection.thumbnail = coverFiles.firstWhereOrNull(
             (element) => element.localID == deviceCollection.coverId,
-            orElse: () => null,
           );
           if (deviceCollection.thumbnail == null) {
-            final File result =
+            final File? result =
                 await getDeviceCollectionThumbnail(deviceCollection.id);
             if (result == null) {
               _logger.finest(
@@ -409,7 +411,7 @@ extension DeviceFiles on FilesDB {
       if (includeCoverThumbnail) {
         deviceCollections.sort(
           (a, b) =>
-              b.thumbnail.creationTime.compareTo(a.thumbnail.creationTime),
+              b.thumbnail!.creationTime!.compareTo(a.thumbnail!.creationTime!),
         );
       }
       return deviceCollections;
@@ -419,7 +421,7 @@ extension DeviceFiles on FilesDB {
     }
   }
 
-  Future<File> getDeviceCollectionThumbnail(String pathID) async {
+  Future<File?> getDeviceCollectionThumbnail(String pathID) async {
     debugPrint("Call fallback method to get potential thumbnail");
     final db = await database;
     final fileRows = await db.rawQuery(

+ 4 - 5
lib/db/file_updation_db.dart

@@ -128,7 +128,7 @@ class FileUpdationDB {
     );
   }
 
-  Future<List<String?>> getLocalIDsForPotentialReUpload(
+  Future<List<String>> getLocalIDsForPotentialReUpload(
     int limit,
     String reason,
   ) async {
@@ -139,15 +139,14 @@ class FileUpdationDB {
       limit: limit,
       where: whereClause,
     );
-    final result = <String?>[];
+    final result = <String>[];
     for (final row in rows) {
-      result.add(row[columnLocalID] as String?);
+      result.add(row[columnLocalID] as String);
     }
     return result;
   }
 
-  Map<String, dynamic> _getRowForReUploadTable(String? localID, String reason) {
-    assert(localID != null);
+  Map<String, dynamic> _getRowForReUploadTable(String localID, String reason) {
     final row = <String, dynamic>{};
     row[columnLocalID] = localID;
     row[columnReason] = reason;

+ 70 - 63
lib/db/files_db.dart

@@ -1,5 +1,3 @@
-// @dart=2.9
-
 import 'dart:io' as io;
 
 import 'package:flutter/foundation.dart';
@@ -94,12 +92,12 @@ class FilesDB {
   static final FilesDB instance = FilesDB._privateConstructor();
 
   // only have a single app-wide reference to the database
-  static Future<Database> _dbFuture;
+  static Future<Database>? _dbFuture;
 
   Future<Database> get database async {
     // lazily instantiate the db the first time it is accessed
     _dbFuture ??= _initDatabase();
-    return _dbFuture;
+    return _dbFuture!;
   }
 
   // this opens the database (and creates it if it doesn't exist)
@@ -415,7 +413,7 @@ class FilesDB {
     );
   }
 
-  Future<File> getFile(int generatedID) async {
+  Future<File?> getFile(int generatedID) async {
     final db = await instance.database;
     final results = await db.query(
       filesTable,
@@ -428,7 +426,7 @@ class FilesDB {
     return convertToFiles(results)[0];
   }
 
-  Future<File> getUploadedFile(int uploadedID, int collectionID) async {
+  Future<File?> getUploadedFile(int uploadedID, int collectionID) async {
     final db = await instance.database;
     final results = await db.query(
       filesTable,
@@ -449,14 +447,15 @@ class FilesDB {
     final results = await db.query(
       filesTable,
       columns: [columnUploadedFileID],
-      where: '$columnCollectionID = ?',
+      where:
+          '$columnCollectionID = ? AND ($columnUploadedFileID IS NOT NULL AND $columnUploadedFileID IS NOT -1)',
       whereArgs: [
         collectionID,
       ],
     );
     final ids = <int>{};
     for (final result in results) {
-      ids.add(result[columnUploadedFileID]);
+      ids.add(result[columnUploadedFileID] as int);
     }
     return ids;
   }
@@ -472,8 +471,8 @@ class FilesDB {
     final localIDs = <String>{};
     final uploadedIDs = <int>{};
     for (final result in results) {
-      localIDs.add(result[columnLocalID]);
-      uploadedIDs.add(result[columnUploadedFileID]);
+      localIDs.add(result[columnLocalID] as String);
+      uploadedIDs.add(result[columnUploadedFileID] as int);
     }
     return BackedUpFileIDs(localIDs.toList(), uploadedIDs.toList());
   }
@@ -482,10 +481,10 @@ class FilesDB {
     int startTime,
     int endTime,
     int ownerID, {
-    int limit,
-    bool asc,
+    int? limit,
+    bool? asc,
     int visibility = visibilityVisible,
-    Set<int> ignoredCollectionIDs,
+    Set<int>? ignoredCollectionIDs,
   }) async {
     final db = await instance.database;
     final order = (asc ?? false ? 'ASC' : 'DESC');
@@ -509,9 +508,9 @@ class FilesDB {
     int startTime,
     int endTime,
     int ownerID, {
-    int limit,
-    bool asc,
-    Set<int> ignoredCollectionIDs,
+    int? limit,
+    bool? asc,
+    Set<int>? ignoredCollectionIDs,
   }) async {
     final db = await instance.database;
     final order = (asc ?? false ? 'ASC' : 'DESC');
@@ -536,7 +535,10 @@ class FilesDB {
     final List<File> deduplicatedFiles = [];
     for (final file in files) {
       final id = file.localID;
-      if (id != null && localIDs.contains(id)) {
+      if (id == null) {
+        continue;
+      }
+      if (localIDs.contains(id)) {
         continue;
       }
       localIDs.add(id);
@@ -547,7 +549,7 @@ class FilesDB {
 
   List<File> _deduplicatedAndFilterIgnoredFiles(
     List<File> files,
-    Set<int> ignoredCollectionIDs,
+    Set<int>? ignoredCollectionIDs,
   ) {
     final Set<int> uploadedFileIDs = <int>{};
     // ignoredFileUploadIDs is to keep a track of files which are part of
@@ -574,7 +576,9 @@ class FilesDB {
       if (isFileUploaded && uploadedFileIDs.contains(id)) {
         continue;
       }
-      uploadedFileIDs.add(id);
+      if (isFileUploaded) {
+        uploadedFileIDs.add(id);
+      }
       deduplicatedFiles.add(file);
     }
     return deduplicatedFiles;
@@ -584,8 +588,8 @@ class FilesDB {
     int collectionID,
     int startTime,
     int endTime, {
-    int limit,
-    bool asc,
+    int? limit,
+    bool? asc,
     int visibility = visibilityVisible,
   }) async {
     final db = await instance.database;
@@ -611,8 +615,8 @@ class FilesDB {
     int startTime,
     int endTime,
     int userID, {
-    int limit,
-    bool asc,
+    int? limit,
+    bool? asc,
   }) async {
     if (collectionIDs.isEmpty) {
       return FileLoadResult(<File>[], false);
@@ -725,12 +729,12 @@ class FilesDB {
     );
     final uploadedFileIDs = <int>[];
     for (final row in rows) {
-      uploadedFileIDs.add(row[columnUploadedFileID]);
+      uploadedFileIDs.add(row[columnUploadedFileID] as int);
     }
     return uploadedFileIDs;
   }
 
-  Future<File> getUploadedFileInAnyCollection(int uploadedFileID) async {
+  Future<File?> getUploadedFileInAnyCollection(int uploadedFileID) async {
     final db = await instance.database;
     final results = await db.query(
       filesTable,
@@ -756,7 +760,7 @@ class FilesDB {
     );
     final result = <String>{};
     for (final row in rows) {
-      result.add(row[columnLocalID]);
+      result.add(row[columnLocalID] as String);
     }
     return result;
   }
@@ -775,7 +779,7 @@ class FilesDB {
     );
     final result = <String>{};
     for (final row in rows) {
-      result.add(row[columnLocalID]);
+      result.add(row[columnLocalID] as String);
     }
     return result;
   }
@@ -790,7 +794,7 @@ class FilesDB {
     );
     final result = <String>{};
     for (final row in rows) {
-      result.add(row[columnLocalID]);
+      result.add(row[columnLocalID] as String);
     }
     return result;
   }
@@ -819,19 +823,19 @@ class FilesDB {
 
   Future<int> updateUploadedFile(
     String localID,
-    String title,
-    Location location,
+    String? title,
+    Location? location,
     int creationTime,
     int modificationTime,
-    int updationTime,
+    int? updationTime,
   ) async {
     final db = await instance.database;
     return await db.update(
       filesTable,
       {
         columnTitle: title,
-        columnLatitude: location.latitude,
-        columnLongitude: location.longitude,
+        columnLatitude: location?.latitude,
+        columnLongitude: location?.longitude,
         columnCreationTime: creationTime,
         columnModificationTime: modificationTime,
         columnUpdationTime: updationTime,
@@ -849,8 +853,8 @@ class FilesDB {
     int ownerID,
     String localID,
     FileType fileType, {
-    @required String title,
-    @required String deviceFolder,
+    required String title,
+    required String deviceFolder,
   }) async {
     final db = await instance.database;
     // on iOS, match using localID and fileType. title can either match or
@@ -1069,7 +1073,7 @@ class FilesDB {
         '$collectionID AND $columnUploadedFileID IS NOT -1',
       ),
     );
-    return count;
+    return count ?? 0;
   }
 
   Future<int> fileCountWithVisibility(int visibility, int ownerID) async {
@@ -1081,7 +1085,7 @@ class FilesDB {
         ' = $visibility AND $columnOwnerID = $ownerID',
       ),
     );
-    return count;
+    return count ?? 0;
   }
 
   Future<int> deleteCollection(int collectionID) async {
@@ -1120,7 +1124,9 @@ class FilesDB {
   ) async {
     String inParam = "";
     for (final existingFile in existingFiles) {
-      inParam += "'" + existingFile.localID + "',";
+      if (existingFile.localID != null) {
+        inParam += "'" + existingFile.localID! + "',";
+      }
     }
     inParam = inParam.substring(0, inParam.length - 1);
     final db = await instance.database;
@@ -1134,7 +1140,7 @@ class FilesDB {
     );
     final result = <String>{};
     for (final row in rows) {
-      result.add(row[columnLocalID]);
+      result.add(row[columnLocalID] as String);
     }
     return result;
   }
@@ -1166,11 +1172,12 @@ class FilesDB {
     final collectionMap = <int, File>{};
     for (final file in files) {
       if (collectionMap.containsKey(file.collectionID)) {
-        if (collectionMap[file.collectionID].updationTime < file.updationTime) {
+        if (collectionMap[file.collectionID]!.updationTime! <
+            file.updationTime!) {
           continue;
         }
       }
-      collectionMap[file.collectionID] = file;
+      collectionMap[file.collectionID!] = file;
     }
     return collectionMap.values.toList();
   }
@@ -1226,7 +1233,7 @@ class FilesDB {
     );
     final files = convertToFiles(results);
     for (final file in files) {
-      result[file.uploadedFileID] = file;
+      result[file.uploadedFileID!] = file;
     }
     return result;
   }
@@ -1248,7 +1255,7 @@ class FilesDB {
     );
     final files = convertToFiles(results);
     for (final file in files) {
-      result[file.generatedID] = file;
+      result[file.generatedID as int] = file;
     }
     return result;
   }
@@ -1273,9 +1280,9 @@ class FilesDB {
     final files = convertToFiles(results);
     for (File eachFile in files) {
       if (!result.containsKey(eachFile.collectionID)) {
-        result[eachFile.collectionID] = <File>[];
+        result[eachFile.collectionID as int] = <File>[];
       }
-      result[eachFile.collectionID].add(eachFile);
+      result[eachFile.collectionID]!.add(eachFile);
     }
     return result;
   }
@@ -1293,7 +1300,7 @@ class FilesDB {
     );
     final collectionIDsOfFile = <int>{};
     for (var result in results) {
-      collectionIDsOfFile.add(result['collection_id']);
+      collectionIDsOfFile.add(result['collection_id'] as int);
     }
     return collectionIDsOfFile;
   }
@@ -1343,7 +1350,9 @@ class FilesDB {
 
     final filesCount = <FileType, int>{};
     for (var e in result) {
-      filesCount.addAll({getFileType(e[columnFileType]): e.values.last});
+      filesCount.addAll(
+        {getFileType(e[columnFileType] as int): e.values.last as int},
+      );
     }
     return filesCount;
   }
@@ -1360,8 +1369,8 @@ class FilesDB {
     row[columnTitle] = file.title;
     row[columnDeviceFolder] = file.deviceFolder;
     if (file.location != null) {
-      row[columnLatitude] = file.location.latitude;
-      row[columnLongitude] = file.location.longitude;
+      row[columnLatitude] = file.location!.latitude;
+      row[columnLongitude] = file.location!.longitude;
     }
     row[columnFileType] = getInt(file.fileType);
     row[columnCreationTime] = file.creationTime;
@@ -1378,17 +1387,16 @@ class FilesDB {
     row[columnHash] = file.hash;
     row[columnMetadataVersion] = file.metadataVersion;
     row[columnFileSize] = file.fileSize;
-    row[columnMMdVersion] = file.mMdVersion ?? 0;
+    row[columnMMdVersion] = file.mMdVersion;
     row[columnMMdEncodedJson] = file.mMdEncodedJson ?? '{}';
-    row[columnMMdVisibility] =
-        file.magicMetadata?.visibility ?? visibilityVisible;
-    row[columnPubMMdVersion] = file.pubMmdVersion ?? 0;
+    row[columnMMdVisibility] = file.magicMetadata.visibility;
+    row[columnPubMMdVersion] = file.pubMmdVersion;
     row[columnPubMMdEncodedJson] = file.pubMmdEncodedJson ?? '{}';
     if (file.pubMagicMetadata != null &&
-        file.pubMagicMetadata.editedTime != null) {
+        file.pubMagicMetadata!.editedTime != null) {
       // override existing creationTime to avoid re-writing all queries related
       // to loading the gallery
-      row[columnCreationTime] = file.pubMagicMetadata.editedTime;
+      row[columnCreationTime] = file.pubMagicMetadata!.editedTime;
     }
     return row;
   }
@@ -1401,8 +1409,8 @@ class FilesDB {
     row[columnTitle] = file.title;
     row[columnDeviceFolder] = file.deviceFolder;
     if (file.location != null) {
-      row[columnLatitude] = file.location.latitude;
-      row[columnLongitude] = file.location.longitude;
+      row[columnLatitude] = file.location!.latitude;
+      row[columnLongitude] = file.location!.longitude;
     }
     row[columnFileType] = getInt(file.fileType);
     row[columnCreationTime] = file.creationTime;
@@ -1417,18 +1425,17 @@ class FilesDB {
     row[columnHash] = file.hash;
     row[columnMetadataVersion] = file.metadataVersion;
 
-    row[columnMMdVersion] = file.mMdVersion ?? 0;
+    row[columnMMdVersion] = file.mMdVersion;
     row[columnMMdEncodedJson] = file.mMdEncodedJson ?? '{}';
-    row[columnMMdVisibility] =
-        file.magicMetadata?.visibility ?? visibilityVisible;
+    row[columnMMdVisibility] = file.magicMetadata.visibility;
 
-    row[columnPubMMdVersion] = file.pubMmdVersion ?? 0;
+    row[columnPubMMdVersion] = file.pubMmdVersion;
     row[columnPubMMdEncodedJson] = file.pubMmdEncodedJson ?? '{}';
     if (file.pubMagicMetadata != null &&
-        file.pubMagicMetadata.editedTime != null) {
+        file.pubMagicMetadata!.editedTime != null) {
       // override existing creationTime to avoid re-writing all queries related
       // to loading the gallery
-      row[columnCreationTime] = file.pubMagicMetadata.editedTime;
+      row[columnCreationTime] = file.pubMagicMetadata!.editedTime!;
     }
     return row;
   }

+ 2 - 4
lib/db/ignored_files_db.dart

@@ -1,5 +1,3 @@
-// @dart=2.9
-
 import 'dart:io';
 
 import 'package:logging/logging.dart';
@@ -44,12 +42,12 @@ class IgnoredFilesDB {
   static final IgnoredFilesDB instance = IgnoredFilesDB._privateConstructor();
 
   // only have a single app-wide reference to the database
-  static Future<Database> _dbFuture;
+  static Future<Database>? _dbFuture;
 
   Future<Database> get database async {
     // lazily instantiate the db the first time it is accessed
     _dbFuture ??= _initDatabase();
-    return _dbFuture;
+    return _dbFuture!;
   }
 
   // this opens the database (and creates it if it doesn't exist)

+ 4 - 5
lib/db/memories_db.dart

@@ -1,5 +1,3 @@
-// @dart=2.9
-
 import 'dart:async';
 import 'dart:io';
 
@@ -20,14 +18,15 @@ class MemoriesDB {
   MemoriesDB._privateConstructor();
   static final MemoriesDB instance = MemoriesDB._privateConstructor();
 
-  static Future<Database> _dbFuture;
+  static Future<Database>? _dbFuture;
   Future<Database> get database async {
     _dbFuture ??= _initDatabase();
-    return _dbFuture;
+    return _dbFuture!;
   }
 
   Future<Database> _initDatabase() async {
-    final Directory documentsDirectory = await getApplicationDocumentsDirectory();
+    final Directory documentsDirectory =
+        await getApplicationDocumentsDirectory();
     final String path = join(documentsDirectory.path, _databaseName);
     return await openDatabase(
       path,

+ 12 - 14
lib/db/trash_db.dart

@@ -1,5 +1,3 @@
-// @dart=2.9
-
 import 'dart:convert';
 import 'dart:io';
 
@@ -77,12 +75,12 @@ class TrashDB {
   static final TrashDB instance = TrashDB._privateConstructor();
 
   // only have a single app-wide reference to the database
-  static Future<Database> _dbFuture;
+  static Future<Database>? _dbFuture;
 
   Future<Database> get database async {
     // lazily instantiate the db the first time it is accessed
     _dbFuture ??= _initDatabase();
-    return _dbFuture;
+    return _dbFuture!;
   }
 
   // this opens the database (and creates it if it doesn't exist)
@@ -104,14 +102,14 @@ class TrashDB {
   }
 
   // getRecentlyTrashedFile returns the file which was trashed recently
-  Future<TrashFile> getRecentlyTrashedFile() async {
+  Future<TrashFile?> getRecentlyTrashedFile() async {
     final db = await instance.database;
     final rows = await db.query(
       tableName,
       orderBy: '$columnTrashDeleteBy DESC',
       limit: 1,
     );
-    if (rows == null || rows.isEmpty) {
+    if (rows.isEmpty) {
       return null;
     }
     return _getTrashFromRow(rows[0]);
@@ -122,7 +120,7 @@ class TrashDB {
     final count = Sqflite.firstIntValue(
       await db.rawQuery('SELECT COUNT(*) FROM $tableName'),
     );
-    return count;
+    return count ?? 0;
   }
 
   Future<void> insertMultiple(List<TrashFile> trashFiles) async {
@@ -188,8 +186,8 @@ class TrashDB {
   Future<FileLoadResult> getTrashedFiles(
     int startTime,
     int endTime, {
-    int limit,
-    bool asc,
+    int? limit,
+    bool? asc,
   }) async {
     final db = await instance.database;
     final order = (asc ?? false ? 'ASC' : 'DESC');
@@ -218,7 +216,7 @@ class TrashDB {
     trashFile.deleteBy = row[columnTrashDeleteBy];
     trashFile.uploadedFileID = row[columnUploadedFileID];
     // dirty hack to ensure that the file_downloads & cache mechanism works
-    trashFile.generatedID = -1 * trashFile.uploadedFileID;
+    trashFile.generatedID = -1 * trashFile.uploadedFileID!;
     trashFile.ownerID = row[columnOwnerID];
     trashFile.collectionID =
         row[columnCollectionID] == -1 ? null : row[columnCollectionID];
@@ -240,10 +238,10 @@ class TrashDB {
     trashFile.pubMmdEncodedJson = row[columnPubMMdEncodedJson] ?? '{}';
 
     if (trashFile.pubMagicMetadata != null &&
-        trashFile.pubMagicMetadata.editedTime != null) {
+        trashFile.pubMagicMetadata!.editedTime != null) {
       // override existing creationTime to avoid re-writing all queries related
       // to loading the gallery
-      row[columnCreationTime] = trashFile.pubMagicMetadata.editedTime;
+      row[columnCreationTime] = trashFile.pubMagicMetadata!.editedTime!;
     }
 
     return trashFile;
@@ -266,10 +264,10 @@ class TrashDB {
     row[columnCreationTime] = trash.creationTime;
     row[columnFileMetadata] = jsonEncode(trash.metadata);
 
-    row[columnMMdVersion] = trash.mMdVersion ?? 0;
+    row[columnMMdVersion] = trash.mMdVersion;
     row[columnMMdEncodedJson] = trash.mMdEncodedJson ?? '{}';
 
-    row[columnPubMMdVersion] = trash.pubMmdVersion ?? 0;
+    row[columnPubMMdVersion] = trash.pubMmdVersion;
     row[columnPubMMdEncodedJson] = trash.pubMmdEncodedJson ?? '{}';
     return row;
   }

+ 1 - 3
lib/events/collection_updated_event.dart

@@ -1,9 +1,7 @@
-// @dart=2.9
-
 import 'package:photos/events/files_updated_event.dart';
 
 class CollectionUpdatedEvent extends FilesUpdatedEvent {
-  final int collectionID;
+  final int? collectionID;
 
   CollectionUpdatedEvent(this.collectionID, updatedFiles, source, {type})
       : super(

+ 0 - 2
lib/events/files_updated_event.dart

@@ -1,5 +1,3 @@
-// @dart=2.9
-
 import 'package:photos/events/event.dart';
 import 'package:photos/models/file.dart';
 

+ 3 - 3
lib/main.dart

@@ -1,4 +1,4 @@
-// @dart=2.9
+
 
 import 'dart:async';
 import 'dart:io';
@@ -230,7 +230,7 @@ Future<bool> _isRunningInForeground() async {
       (currentTime - kFGTaskDeathTimeoutInMicroseconds);
 }
 
-Future<void> _killBGTask([String taskId]) async {
+Future<void> _killBGTask([String? taskId]) async {
   await UploadLocksDB.instance.releaseLocksAcquiredByOwnerBefore(
     ProcessType.background.toString(),
     DateTime.now().microsecondsSinceEpoch,
@@ -281,7 +281,7 @@ Future<void> _logFGHeartBeatInfo() async {
   _logger.info('isAlreaduunningFG: $isRunningInFG, last Beat: $lastRun');
 }
 
-void _scheduleSuicide(Duration duration, [String taskID]) {
+void _scheduleSuicide(Duration duration, [String? taskID]) {
   final taskIDVal = taskID ?? 'no taskID';
   _logger.warning("Schedule seppuku taskID: $taskIDVal");
   Future.delayed(duration, () {

+ 4 - 2
lib/models/collection.dart

@@ -10,8 +10,10 @@ class Collection {
   final String encryptedKey;
   final String? keyDecryptionNonce;
   final String? name;
-  final String encryptedName;
-  final String nameDecryptionNonce;
+  // encryptedName & nameDecryptionNonce will be null for collections
+  // created before we started encrypting collection name
+  final String? encryptedName;
+  final String? nameDecryptionNonce;
   final CollectionType type;
   final CollectionAttributes attributes;
   final List<User?>? sharees;

+ 3 - 4
lib/models/duplicate_files.dart

@@ -1,5 +1,3 @@
-// @dart=2.9
-
 import 'dart:convert';
 
 import 'package:photos/models/file.dart';
@@ -58,9 +56,10 @@ class DuplicateFiles {
   sortByCollectionName() {
     files.sort((first, second) {
       final firstName =
-          collectionsService.getCollectionByID(first.collectionID).name;
+          collectionsService.getCollectionByID(first.collectionID!)!.name ?? '';
       final secondName =
-          collectionsService.getCollectionByID(second.collectionID).name;
+          collectionsService.getCollectionByID(second.collectionID!)!.name ??
+              '';
       return firstName.compareTo(secondName);
     });
   }

+ 1 - 4
lib/models/file.dart

@@ -10,12 +10,9 @@ import 'package:photos/models/ente_file.dart';
 import 'package:photos/models/file_type.dart';
 import 'package:photos/models/location.dart';
 import 'package:photos/models/magic_metadata.dart';
-// ignore: import_of_legacy_library_into_null_safe
 import 'package:photos/services/feature_flag_service.dart';
 import 'package:photos/utils/date_time_util.dart';
-// ignore: import_of_legacy_library_into_null_safe
 import 'package:photos/utils/exif_util.dart';
-// ignore: import_of_legacy_library_into_null_safe
 import 'package:photos/utils/file_uploader_util.dart';
 
 class File extends EnteFile {
@@ -152,7 +149,7 @@ class File extends EnteFile {
     } else {
       location = Location(latitude, longitude);
     }
-    fileType = getFileType(metadata["fileType"]);
+    fileType = getFileType(metadata["fileType"] ?? -1);
     fileSubType = metadata["subType"] ?? -1;
     duration = metadata["duration"] ?? 0;
     exif = metadata["exif"];

+ 2 - 2
lib/models/key_attributes.dart

@@ -11,8 +11,8 @@ class KeyAttributes {
   final int opsLimit;
   final String masterKeyEncryptedWithRecoveryKey;
   final String masterKeyDecryptionNonce;
-  final String recoveryKeyEncryptedWithMasterKey;
-  final String recoveryKeyDecryptionNonce;
+  final String? recoveryKeyEncryptedWithMasterKey;
+  final String? recoveryKeyDecryptionNonce;
 
   KeyAttributes(
     this.kekSalt,

+ 1 - 3
lib/services/billing_service.dart

@@ -53,9 +53,7 @@ class BillingService {
             purchase.productID,
             purchase.verificationData.serverVerificationData,
           ).then((response) {
-            if (response != null) {
-              InAppPurchase.instance.completePurchase(purchase);
-            }
+            InAppPurchase.instance.completePurchase(purchase);
           });
         } else if (Platform.isIOS && purchase.pendingCompletePurchase) {
           InAppPurchase.instance.completePurchase(purchase);

+ 158 - 156
lib/services/collections_service.dart

@@ -1,5 +1,3 @@
-// @dart=2.9
-
 import 'dart:async';
 import 'dart:convert';
 import 'dart:math';
@@ -47,17 +45,18 @@ class CollectionsService {
 
   final _logger = Logger("CollectionsService");
 
-  CollectionsDB _db;
-  FilesDB _filesDB;
-  Configuration _config;
-  SharedPreferences _prefs;
-  Future<List<File>> _cachedLatestFiles;
+  late CollectionsDB _db;
+  late FilesDB _filesDB;
+  late Configuration _config;
+  late SharedPreferences _prefs;
+
   final _enteDio = Network.instance.enteDio;
   final _localPathToCollectionID = <String, int>{};
   final _collectionIDToCollections = <int, Collection>{};
   final _cachedKeys = <int, Uint8List>{};
   final _cachedUserIdToUser = <int, User>{};
-  Collection cachedDefaultHiddenCollection;
+  Collection? cachedDefaultHiddenCollection;
+  Future<List<File>>? _cachedLatestFiles;
 
   CollectionsService._privateConstructor() {
     _db = CollectionsDB.instance;
@@ -117,7 +116,7 @@ class CollectionsService {
         }
       }
       // remove reference for incoming collections when unshared/deleted
-      if (collection.isDeleted && ownerID != collection?.owner?.id) {
+      if (collection.isDeleted && ownerID != collection.owner?.id) {
         await _db.deleteCollection(collection.id);
       } else {
         // keep entry for deletedCollection as collectionKey may be used during
@@ -205,10 +204,10 @@ class CollectionsService {
 
   Future<List<File>> getLatestCollectionFiles() {
     _cachedLatestFiles ??= _filesDB.getLatestCollectionFiles();
-    return _cachedLatestFiles;
+    return _cachedLatestFiles!;
   }
 
-  Future<void> setCollectionSyncTime(int collectionID, int time) async {
+  Future<bool> setCollectionSyncTime(int collectionID, int? time) async {
     final key = _collectionSyncTimeKeyPrefix + collectionID.toString();
     if (time == null) {
       return _prefs.remove(key);
@@ -224,21 +223,21 @@ class CollectionsService {
         .toList();
   }
 
-  User getFileOwner(int userID, int collectionID) {
+  User getFileOwner(int userID, int? collectionID) {
     if (_cachedUserIdToUser.containsKey(userID)) {
-      return _cachedUserIdToUser[userID];
+      return _cachedUserIdToUser[userID]!;
     }
     if (collectionID != null) {
-      final Collection collection = getCollectionByID(collectionID);
+      final Collection? collection = getCollectionByID(collectionID);
       if (collection != null) {
-        if (collection.owner.id == userID) {
-          _cachedUserIdToUser[userID] = collection.owner;
+        if (collection.owner?.id == userID) {
+          _cachedUserIdToUser[userID] = collection.owner!;
         } else {
           final matchingUser = collection.getSharees().firstWhereOrNull(
                 (u) => u.id == userID,
               );
           if (matchingUser != null) {
-            _cachedUserIdToUser[userID] = collection.owner;
+            _cachedUserIdToUser[userID] = collection.owner!;
           }
         }
       }
@@ -265,20 +264,20 @@ class CollectionsService {
       if (includeCollabCollections) {
         usersCollection.removeWhere(
           (c) =>
-              (c.owner.id != userID) &&
+              (c.owner?.id != userID) &&
               (c.getSharees().any((u) => (u.id ?? -1) == userID && u.isViewer)),
         );
       } else {
-        usersCollection.removeWhere((c) => c.owner.id != userID);
+        usersCollection.removeWhere((c) => c.owner?.id != userID);
       }
     }
     final latestCollectionFiles = await getLatestCollectionFiles();
     final Map<int, File> collectionToThumbnailMap = Map.fromEntries(
-      latestCollectionFiles.map((e) => MapEntry(e.collectionID, e)),
+      latestCollectionFiles.map((e) => MapEntry(e.collectionID!, e)),
     );
 
     for (final c in usersCollection) {
-      final File thumbnail = collectionToThumbnailMap[c.id];
+      final File? thumbnail = collectionToThumbnailMap[c.id];
       collectionsWithThumbnail.add(CollectionWithThumbnail(c, thumbnail));
     }
     return collectionsWithThumbnail;
@@ -325,12 +324,12 @@ class CollectionsService {
         sharees.add(User.fromMap(user));
       }
       _collectionIDToCollections[collectionID] =
-          _collectionIDToCollections[collectionID].copyWith(sharees: sharees);
-      unawaited(_db.insert([_collectionIDToCollections[collectionID]]));
+          _collectionIDToCollections[collectionID]!.copyWith(sharees: sharees);
+      unawaited(_db.insert([_collectionIDToCollections[collectionID]!]));
       RemoteSyncService.instance.sync(silently: true).ignore();
       return sharees;
     } on DioError catch (e) {
-      if (e.response.statusCode == 402) {
+      if (e.response?.statusCode == 402) {
         throw SharingNotPermittedForFreeAccountsError();
       }
       rethrow;
@@ -351,15 +350,14 @@ class CollectionsService {
         sharees.add(User.fromMap(user));
       }
       _collectionIDToCollections[collectionID] =
-          _collectionIDToCollections[collectionID].copyWith(sharees: sharees);
-      unawaited(_db.insert([_collectionIDToCollections[collectionID]]));
+          _collectionIDToCollections[collectionID]!.copyWith(sharees: sharees);
+      unawaited(_db.insert([_collectionIDToCollections[collectionID]!]));
       RemoteSyncService.instance.sync(silently: true).ignore();
       return sharees;
     } catch (e) {
       _logger.severe(e);
       rethrow;
     }
-    RemoteSyncService.instance.sync(silently: true).ignore();
   }
 
   Future<void> trashCollection(
@@ -397,6 +395,29 @@ class CollectionsService {
     }
   }
 
+  Future<void> trashEmptyCollection(Collection collection) async {
+    try {
+      // While trashing empty albums, we must pass keepFiles flag as True.
+      // The server will verify that the collection is actually empty before
+      // deleting the files. If keepFiles is set as False and the collection
+      // is not empty, then the files in the collections will be moved to trash.
+      await _enteDio.delete(
+        "/collections/v3/${collection.id}?keepFiles=True&collectionID=${collection.id}",
+      );
+      final deletedCollection = collection.copyWith(isDeleted: true);
+      _collectionIDToCollections[collection.id] = deletedCollection;
+      unawaited(_db.insert([deletedCollection]));
+    } on DioError catch (e) {
+      if (e.response != null) {
+        debugPrint("Error " + e.response!.toString());
+      }
+      rethrow;
+    } catch (e) {
+      _logger.severe('failed to trash empty collection', e);
+      rethrow;
+    }
+  }
+
   Future<void> _handleCollectionDeletion(Collection collection) async {
     await _filesDB.deleteCollection(collection.id);
     final deletedCollection = collection.copyWith(isDeleted: true);
@@ -427,7 +448,7 @@ class CollectionsService {
       _cachedKeys[collectionID] =
           _getAndCacheDecryptedKey(collection, source: "getCollectionKey");
     }
-    return _cachedKeys[collectionID];
+    return _cachedKeys[collectionID]!;
   }
 
   Uint8List _getAndCacheDecryptedKey(
@@ -435,33 +456,31 @@ class CollectionsService {
     String source = "",
   }) {
     if (_cachedKeys.containsKey(collection.id)) {
-      return _cachedKeys[collection.id];
+      return _cachedKeys[collection.id]!;
     }
     debugPrint(
       "Compute collection decryption key for ${collection.id} source"
       " $source",
     );
     final encryptedKey = Sodium.base642bin(collection.encryptedKey);
-    Uint8List collectionKey;
-    if (collection.owner.id == _config.getUserID()) {
+    Uint8List? collectionKey;
+    if (collection.owner?.id == _config.getUserID()) {
       if (_config.getKey() == null) {
         throw Exception("key can not be null");
       }
       collectionKey = CryptoUtil.decryptSync(
         encryptedKey,
         _config.getKey(),
-        Sodium.base642bin(collection.keyDecryptionNonce),
+        Sodium.base642bin(collection.keyDecryptionNonce!),
       );
     } else {
       collectionKey = CryptoUtil.openSealSync(
         encryptedKey,
-        Sodium.base642bin(_config.getKeyAttributes().publicKey),
-        _config.getSecretKey(),
+        Sodium.base642bin(_config.getKeyAttributes()!.publicKey),
+        _config.getSecretKey()!,
       );
     }
-    if (collectionKey != null) {
-      _cachedKeys[collection.id] = collectionKey;
-    }
+    _cachedKeys[collection.id] = collectionKey;
     return collectionKey;
   }
 
@@ -473,15 +492,15 @@ class CollectionsService {
         await updateMagicMetadata(collection, {"subType": 0});
       }
       final encryptedName = CryptoUtil.encryptSync(
-        utf8.encode(newName),
+        utf8.encode(newName) as Uint8List,
         getCollectionKey(collection.id),
       );
       await _enteDio.post(
         "/collections/rename",
         data: {
           "collectionID": collection.id,
-          "encryptedName": Sodium.bin2base64(encryptedName.encryptedData),
-          "nameDecryptionNonce": Sodium.bin2base64(encryptedName.nonce)
+          "encryptedName": Sodium.bin2base64(encryptedName.encryptedData!),
+          "nameDecryptionNonce": Sodium.bin2base64(encryptedName.nonce!)
         },
       );
       // trigger sync to fetch the latest name from server
@@ -508,9 +527,9 @@ class CollectionsService {
     Collection collection,
     Map<String, dynamic> newMetadataUpdate,
   ) async {
-    final int ownerID = Configuration.instance.getUserID();
+    final int ownerID = Configuration.instance.getUserID()!;
     try {
-      if (collection.owner.id != ownerID) {
+      if (collection.owner?.id != ownerID) {
         throw AssertionError("cannot modify albums not owned by you");
       }
       // read the existing magic metadata and apply new updates to existing data
@@ -528,7 +547,7 @@ class CollectionsService {
 
       final key = getCollectionKey(collection.id);
       final encryptedMMd = await CryptoUtil.encryptChaCha(
-        utf8.encode(jsonEncode(jsonToUpdate)),
+        utf8.encode(jsonEncode(jsonToUpdate)) as Uint8List,
         key,
       );
       // for required field, the json validator on golang doesn't treat 0 as valid
@@ -539,8 +558,8 @@ class CollectionsService {
         magicMetadata: MetadataRequest(
           version: currentVersion,
           count: jsonToUpdate.length,
-          data: Sodium.bin2base64(encryptedMMd.encryptedData),
-          header: Sodium.bin2base64(encryptedMMd.header),
+          data: Sodium.bin2base64(encryptedMMd.encryptedData!),
+          header: Sodium.bin2base64(encryptedMMd.header!),
         ),
       );
       await _enteDio.put(
@@ -552,7 +571,7 @@ class CollectionsService {
       // trigger sync to fetch the latest collection state from server
       sync().ignore();
     } on DioError catch (e) {
-      if (e.response != null && e.response.statusCode == 409) {
+      if (e.response != null && e.response?.statusCode == 409) {
         _logger.severe('collection magic data out of sync');
         sync().ignore();
       }
@@ -578,7 +597,7 @@ class CollectionsService {
         CollectionUpdatedEvent(collection.id, <File>[], "shareUrL"),
       );
     } on DioError catch (e) {
-      if (e.response.statusCode == 402) {
+      if (e.response?.statusCode == 402) {
         throw SharingNotPermittedForFreeAccountsError();
       }
       rethrow;
@@ -606,7 +625,7 @@ class CollectionsService {
       Bus.instance
           .fire(CollectionUpdatedEvent(collection.id, <File>[], "updateUrl"));
     } on DioError catch (e) {
-      if (e.response.statusCode == 402) {
+      if (e.response?.statusCode == 402) {
         throw SharingNotPermittedForFreeAccountsError();
       }
       rethrow;
@@ -621,7 +640,7 @@ class CollectionsService {
       await _enteDio.delete(
         "/collections/share-url/" + collection.id.toString(),
       );
-      collection.publicURLs.clear();
+      collection.publicURLs?.clear();
       await _db.insert(List.from([collection]));
       _cacheCollectionAttributes(collection);
       Bus.instance.fire(
@@ -647,26 +666,24 @@ class CollectionsService {
         },
       );
       final List<Collection> collections = [];
-      if (response != null) {
-        final c = response.data["collections"];
-        for (final collectionData in c) {
-          final collection = Collection.fromMap(collectionData);
-          if (collectionData['magicMetadata'] != null) {
-            final decryptionKey =
-                _getAndCacheDecryptedKey(collection, source: "fetchCollection");
-            final utfEncodedMmd = await CryptoUtil.decryptChaCha(
-              Sodium.base642bin(collectionData['magicMetadata']['data']),
-              decryptionKey,
-              Sodium.base642bin(collectionData['magicMetadata']['header']),
-            );
-            collection.mMdEncodedJson = utf8.decode(utfEncodedMmd);
-            collection.mMdVersion = collectionData['magicMetadata']['version'];
-            collection.magicMetadata = CollectionMagicMetadata.fromEncodedJson(
-              collection.mMdEncodedJson,
-            );
-          }
-          collections.add(collection);
+      final c = response.data["collections"];
+      for (final collectionData in c) {
+        final collection = Collection.fromMap(collectionData);
+        if (collectionData['magicMetadata'] != null) {
+          final decryptionKey =
+              _getAndCacheDecryptedKey(collection, source: "fetchCollection");
+          final utfEncodedMmd = await CryptoUtil.decryptChaCha(
+            Sodium.base642bin(collectionData['magicMetadata']['data']),
+            decryptionKey,
+            Sodium.base642bin(collectionData['magicMetadata']['header']),
+          );
+          collection.mMdEncodedJson = utf8.decode(utfEncodedMmd);
+          collection.mMdVersion = collectionData['magicMetadata']['version'];
+          collection.magicMetadata = CollectionMagicMetadata.fromEncodedJson(
+            collection.mMdEncodedJson,
+          );
         }
+        collections.add(collection);
       }
       return collections;
     } catch (e) {
@@ -677,28 +694,23 @@ class CollectionsService {
     }
   }
 
-  Collection getCollectionByID(int collectionID) {
+  Collection? getCollectionByID(int collectionID) {
     return _collectionIDToCollections[collectionID];
   }
 
   Future<Collection> createAlbum(String albumName) async {
     final key = CryptoUtil.generateKey();
-    final encryptedKeyData = CryptoUtil.encryptSync(key, _config.getKey());
-    final encryptedName = CryptoUtil.encryptSync(utf8.encode(albumName), key);
+    final encryptedKeyData = CryptoUtil.encryptSync(key, _config.getKey()!);
+    final encryptedName =
+        CryptoUtil.encryptSync(utf8.encode(albumName) as Uint8List, key);
     final collection = await createAndCacheCollection(
-      Collection(
-        null,
-        null,
-        Sodium.bin2base64(encryptedKeyData.encryptedData),
-        Sodium.bin2base64(encryptedKeyData.nonce),
-        null,
-        Sodium.bin2base64(encryptedName.encryptedData),
-        Sodium.bin2base64(encryptedName.nonce),
-        CollectionType.album,
-        CollectionAttributes(),
-        null,
-        null,
-        null,
+      CreateRequest(
+        encryptedKey: Sodium.bin2base64(encryptedKeyData.encryptedData!),
+        keyDecryptionNonce: Sodium.bin2base64(encryptedKeyData.nonce!),
+        encryptedName: Sodium.bin2base64(encryptedName.encryptedData!),
+        nameDecryptionNonce: Sodium.bin2base64(encryptedName.nonce!),
+        type: CollectionType.album,
+        attributes: CollectionAttributes(),
       ),
     );
     return collection;
@@ -710,7 +722,7 @@ class CollectionsService {
       final response = await _enteDio.get(
         "/collections/$collectionID",
       );
-      assert(response != null && response.data != null);
+      assert(response.data != null);
       final collectionData = response.data["collection"];
       final collection = Collection.fromMap(collectionData);
       if (collectionData['magicMetadata'] != null) {
@@ -742,44 +754,38 @@ class CollectionsService {
 
   Future<Collection> getOrCreateForPath(String path) async {
     if (_localPathToCollectionID.containsKey(path)) {
-      final Collection cachedCollection =
+      final Collection? cachedCollection =
           _collectionIDToCollections[_localPathToCollectionID[path]];
       if (cachedCollection != null &&
           !cachedCollection.isDeleted &&
-          cachedCollection.owner.id == _config.getUserID()) {
+          cachedCollection.owner?.id == _config.getUserID()) {
         return cachedCollection;
       }
     }
     final key = CryptoUtil.generateKey();
-    final encryptedKeyData = CryptoUtil.encryptSync(key, _config.getKey());
-    final encryptedPath = CryptoUtil.encryptSync(utf8.encode(path), key);
+    final encryptedKeyData = CryptoUtil.encryptSync(key, _config.getKey()!);
+    final encryptedPath =
+        CryptoUtil.encryptSync(utf8.encode(path) as Uint8List, key);
     final collection = await createAndCacheCollection(
-      Collection(
-        null,
-        null,
-        Sodium.bin2base64(encryptedKeyData.encryptedData),
-        Sodium.bin2base64(encryptedKeyData.nonce),
-        null,
-        Sodium.bin2base64(encryptedPath.encryptedData),
-        Sodium.bin2base64(encryptedPath.nonce),
-        CollectionType.folder,
-        CollectionAttributes(
-          encryptedPath: Sodium.bin2base64(encryptedPath.encryptedData),
-          pathDecryptionNonce: Sodium.bin2base64(encryptedPath.nonce),
+      CreateRequest(
+        encryptedKey: Sodium.bin2base64(encryptedKeyData.encryptedData!),
+        keyDecryptionNonce: Sodium.bin2base64(encryptedKeyData.nonce!),
+        encryptedName: Sodium.bin2base64(encryptedPath.encryptedData!),
+        nameDecryptionNonce: Sodium.bin2base64(encryptedPath.nonce!),
+        type: CollectionType.folder,
+        attributes: CollectionAttributes(
+          encryptedPath: Sodium.bin2base64(encryptedPath.encryptedData!),
+          pathDecryptionNonce: Sodium.bin2base64(encryptedPath.nonce!),
           version: 1,
         ),
-        null,
-        null,
-        null,
       ),
     );
     return collection;
   }
 
   Future<void> addToCollection(int collectionID, List<File> files) async {
-    final containsUploadedFile = files.firstWhere(
+    final containsUploadedFile = files.firstWhereOrNull(
           (element) => element.uploadedFileID != null,
-          orElse: () => null,
         ) !=
         null;
     if (containsUploadedFile) {
@@ -808,13 +814,13 @@ class CollectionsService {
         file.collectionID = collectionID;
         final encryptedKeyData =
             CryptoUtil.encryptSync(key, getCollectionKey(collectionID));
-        file.encryptedKey = Sodium.bin2base64(encryptedKeyData.encryptedData);
-        file.keyDecryptionNonce = Sodium.bin2base64(encryptedKeyData.nonce);
+        file.encryptedKey = Sodium.bin2base64(encryptedKeyData.encryptedData!);
+        file.keyDecryptionNonce = Sodium.bin2base64(encryptedKeyData.nonce!);
         params["files"].add(
           CollectionFileItem(
-            file.uploadedFileID,
-            file.encryptedKey,
-            file.keyDecryptionNonce,
+            file.uploadedFileID!,
+            file.encryptedKey!,
+            file.keyDecryptionNonce!,
           ).toMap(),
         );
       }
@@ -834,13 +840,13 @@ class CollectionsService {
 
   Future<File> linkLocalFileToExistingUploadedFileInAnotherCollection(
     int destCollectionID, {
-    @required File localFileToUpload,
-    @required File existingUploadedFile,
+    required File localFileToUpload,
+    required File existingUploadedFile,
   }) async {
     final params = <String, dynamic>{};
     params["collectionID"] = destCollectionID;
     params["files"] = [];
-    final int uploadedFileID = existingUploadedFile.uploadedFileID;
+    final int uploadedFileID = existingUploadedFile.uploadedFileID!;
 
     // encrypt the fileKey with destination collection's key
     final fileKey = decryptFileKey(existingUploadedFile);
@@ -848,15 +854,15 @@ class CollectionsService {
         CryptoUtil.encryptSync(fileKey, getCollectionKey(destCollectionID));
 
     localFileToUpload.encryptedKey =
-        Sodium.bin2base64(encryptedKeyData.encryptedData);
+        Sodium.bin2base64(encryptedKeyData.encryptedData!);
     localFileToUpload.keyDecryptionNonce =
-        Sodium.bin2base64(encryptedKeyData.nonce);
+        Sodium.bin2base64(encryptedKeyData.nonce!);
 
     params["files"].add(
       CollectionFileItem(
         uploadedFileID,
-        localFileToUpload.encryptedKey,
-        localFileToUpload.keyDecryptionNonce,
+        localFileToUpload.encryptedKey!,
+        localFileToUpload.keyDecryptionNonce!,
       ).toMap(),
     );
 
@@ -887,13 +893,13 @@ class CollectionsService {
             null; // So that a new entry is created in the FilesDB
         file.collectionID = toCollectionID;
         final encryptedKeyData = CryptoUtil.encryptSync(key, toCollectionKey);
-        file.encryptedKey = Sodium.bin2base64(encryptedKeyData.encryptedData);
-        file.keyDecryptionNonce = Sodium.bin2base64(encryptedKeyData.nonce);
+        file.encryptedKey = Sodium.bin2base64(encryptedKeyData.encryptedData!);
+        file.keyDecryptionNonce = Sodium.bin2base64(encryptedKeyData.nonce!);
         params["files"].add(
           CollectionFileItem(
-            file.uploadedFileID,
-            file.encryptedKey,
-            file.keyDecryptionNonce,
+            file.uploadedFileID!,
+            file.encryptedKey!,
+            file.keyDecryptionNonce!,
           ).toMap(),
         );
       }
@@ -904,7 +910,7 @@ class CollectionsService {
         );
         await _filesDB.insertMultiple(batch);
         await TrashDB.instance
-            .delete(batch.map((e) => e.uploadedFileID).toList());
+            .delete(batch.map((e) => e.uploadedFileID!).toList());
         Bus.instance.fire(
           CollectionUpdatedEvent(toCollectionID, batch, "restore"),
         );
@@ -914,7 +920,7 @@ class CollectionsService {
         // but not uploaded automatically as it was trashed.
         final localIDs = batch
             .where((e) => e.localID != null)
-            .map((e) => e.localID)
+            .map((e) => e.localID!)
             .toSet()
             .toList();
         if (localIDs.isNotEmpty) {
@@ -953,13 +959,13 @@ class CollectionsService {
         file.collectionID = toCollectionID;
         final encryptedKeyData =
             CryptoUtil.encryptSync(fileKey, getCollectionKey(toCollectionID));
-        file.encryptedKey = Sodium.bin2base64(encryptedKeyData.encryptedData);
-        file.keyDecryptionNonce = Sodium.bin2base64(encryptedKeyData.nonce);
+        file.encryptedKey = Sodium.bin2base64(encryptedKeyData.encryptedData!);
+        file.keyDecryptionNonce = Sodium.bin2base64(encryptedKeyData.nonce!);
         params["files"].add(
           CollectionFileItem(
-            file.uploadedFileID,
-            file.encryptedKey,
-            file.keyDecryptionNonce,
+            file.uploadedFileID!,
+            file.encryptedKey!,
+            file.keyDecryptionNonce!,
           ).toMap(),
         );
       }
@@ -972,7 +978,7 @@ class CollectionsService {
     // remove files from old collection
     await _filesDB.removeFromCollection(
       fromCollectionID,
-      files.map((e) => e.uploadedFileID).toList(),
+      files.map((e) => e.uploadedFileID!).toList(),
     );
     Bus.instance.fire(
       CollectionUpdatedEvent(
@@ -1038,11 +1044,9 @@ class CollectionsService {
   }
 
   Future<Collection> createAndCacheCollection(
-    Collection collection, {
     CreateRequest createRequest,
-  }) async {
-    final dynamic payload =
-        createRequest != null ? createRequest.toJson() : collection.toMap();
+  ) async {
+    final dynamic payload = createRequest.toJson();
     return _enteDio
         .post(
       "/collections",
@@ -1051,21 +1055,19 @@ class CollectionsService {
         .then((response) async {
       final collectionData = response.data["collection"];
       final collection = Collection.fromMap(collectionData);
-      if (createRequest != null) {
-        if (collectionData['magicMetadata'] != null) {
-          final decryptionKey =
-              _getAndCacheDecryptedKey(collection, source: "create");
-          final utfEncodedMmd = await CryptoUtil.decryptChaCha(
-            Sodium.base642bin(collectionData['magicMetadata']['data']),
-            decryptionKey,
-            Sodium.base642bin(collectionData['magicMetadata']['header']),
-          );
-          collection.mMdEncodedJson = utf8.decode(utfEncodedMmd);
-          collection.mMdVersion = collectionData['magicMetadata']['version'];
-          collection.magicMetadata = CollectionMagicMetadata.fromEncodedJson(
-            collection.mMdEncodedJson,
-          );
-        }
+      if (collectionData['magicMetadata'] != null) {
+        final decryptionKey =
+            _getAndCacheDecryptedKey(collection, source: "create");
+        final utfEncodedMmd = await CryptoUtil.decryptChaCha(
+          Sodium.base642bin(collectionData['magicMetadata']['data']),
+          decryptionKey,
+          Sodium.base642bin(collectionData['magicMetadata']['header']),
+        );
+        collection.mMdEncodedJson = utf8.decode(utfEncodedMmd);
+        collection.mMdVersion = collectionData['magicMetadata']['version'];
+        collection.magicMetadata = CollectionMagicMetadata.fromEncodedJson(
+          collection.mMdEncodedJson,
+        );
       }
       return _cacheCollectionAttributes(collection);
     });
@@ -1076,7 +1078,7 @@ class CollectionsService {
         _getCollectionWithDecryptedName(collection);
     if (collection.attributes.encryptedPath != null &&
         !collection.isDeleted &&
-        collection.owner.id == _config.getUserID()) {
+        collection.owner?.id == _config.getUserID()) {
       _localPathToCollectionID[decryptCollectionPath(collection)] =
           collection.id;
     }
@@ -1090,9 +1092,9 @@ class CollectionsService {
         : _config.getKey();
     return utf8.decode(
       CryptoUtil.decryptSync(
-        Sodium.base642bin(collection.attributes.encryptedPath),
+        Sodium.base642bin(collection.attributes.encryptedPath!),
         key,
-        Sodium.base642bin(collection.attributes.pathDecryptionNonce),
+        Sodium.base642bin(collection.attributes.pathDecryptionNonce!),
       ),
     );
   }
@@ -1106,7 +1108,7 @@ class CollectionsService {
       return collection.copyWith(name: "Deleted Album");
     }
     if (collection.encryptedName != null &&
-        collection.encryptedName.isNotEmpty) {
+        collection.encryptedName!.isNotEmpty) {
       String name;
       try {
         final collectionKey = _getAndCacheDecryptedKey(
@@ -1114,9 +1116,9 @@ class CollectionsService {
           source: "Name",
         );
         final result = CryptoUtil.decryptSync(
-          Sodium.base642bin(collection.encryptedName),
+          Sodium.base642bin(collection.encryptedName!),
           collectionKey,
-          Sodium.base642bin(collection.nameDecryptionNonce),
+          Sodium.base642bin(collection.nameDecryptionNonce!),
         );
         name = utf8.decode(result);
       } catch (e, s) {

+ 7 - 8
lib/services/deduplication_service.dart

@@ -1,5 +1,3 @@
-// @dart=2.9
-
 import 'package:logging/logging.dart';
 import 'package:photos/core/errors.dart';
 import 'package:photos/core/network.dart';
@@ -76,16 +74,17 @@ class DeduplicationService {
       int mostFrequentCreationTime = 0, mostFrequentCreationTimeCount = 0;
       // Counts the frequency of creationTimes within the supposed duplicates
       for (final file in dupe.files) {
-        if (creationTimeCounter.containsKey(file.creationTime)) {
-          creationTimeCounter[file.creationTime]++;
+        if (creationTimeCounter.containsKey(file.creationTime!)) {
+          creationTimeCounter[file.creationTime!] =
+              creationTimeCounter[file.creationTime!]! + 1;
         } else {
-          creationTimeCounter[file.creationTime] = 0;
+          creationTimeCounter[file.creationTime!] = 0;
         }
-        if (creationTimeCounter[file.creationTime] >
+        if (creationTimeCounter[file.creationTime]! >
             mostFrequentCreationTimeCount) {
           mostFrequentCreationTimeCount =
-              creationTimeCounter[file.creationTime];
-          mostFrequentCreationTime = file.creationTime;
+              creationTimeCounter[file.creationTime]!;
+          mostFrequentCreationTime = file.creationTime!;
         }
         files.add(file);
       }

+ 28 - 32
lib/services/favorites_service.dart

@@ -1,7 +1,6 @@
-// @dart=2.9
-
 import 'dart:async';
 import 'dart:convert';
+import 'dart:typed_data';
 
 import 'package:flutter/material.dart';
 import 'package:flutter_sodium/flutter_sodium.dart';
@@ -10,6 +9,7 @@ import 'package:photos/core/event_bus.dart';
 import 'package:photos/db/files_db.dart';
 import 'package:photos/events/collection_updated_event.dart';
 import 'package:photos/events/files_updated_event.dart';
+import 'package:photos/models/api/collection/create_request.dart';
 import 'package:photos/models/collection.dart';
 import 'package:photos/models/file.dart';
 import 'package:photos/services/collections_service.dart';
@@ -17,13 +17,14 @@ import 'package:photos/services/remote_sync_service.dart';
 import 'package:photos/utils/crypto_util.dart';
 
 class FavoritesService {
-  Configuration _config;
-  CollectionsService _collectionsService;
-  FilesDB _filesDB;
-  int _cachedFavoritesCollectionID;
+  late Configuration _config;
+  late CollectionsService _collectionsService;
+  late FilesDB _filesDB;
+  int? _cachedFavoritesCollectionID;
   final Set<int> _cachedFavUploadedIDs = {};
   final Set<String> _cachedPendingLocalIDs = {};
-  StreamSubscription<CollectionUpdatedEvent> _collectionUpdatesSubscription;
+  late StreamSubscription<CollectionUpdatedEvent>
+      _collectionUpdatesSubscription;
 
   FavoritesService._privateConstructor() {
     _config = Configuration.instance;
@@ -89,27 +90,27 @@ class FavoritesService {
 
   Future<bool> isFavorite(File file) async {
     final collection = await _getFavoritesCollection();
-    if (collection == null) {
+    if (collection == null || file.uploadedFileID == null) {
       return false;
     }
     return _filesDB.doesFileExistInCollection(
-      file.uploadedFileID,
+      file.uploadedFileID!,
       collection.id,
     );
   }
 
-  void _updateFavoriteFilesCache(List<File> files, {@required bool favFlag}) {
+  void _updateFavoriteFilesCache(List<File> files, {required bool favFlag}) {
     final Set<int> updatedIDs = {};
     final Set<String> localIDs = {};
     for (var file in files) {
       if (file.uploadedFileID != null) {
-        updatedIDs.add(file.uploadedFileID);
+        updatedIDs.add(file.uploadedFileID!);
       } else if (file.localID != null || file.localID != "") {
         /* Note: Favorite un-uploaded files
         For such files, as we don't have uploaded IDs yet, we will cache
         cache the local ID for showing the fav icon in the gallery
          */
-        localIDs.add(file.localID);
+        localIDs.add(file.localID!);
       }
     }
     if (favFlag) {
@@ -134,7 +135,7 @@ class FavoritesService {
   }
 
   Future<void> updateFavorites(List<File> files, bool favFlag) async {
-    final int currentUserID = Configuration.instance.getUserID();
+    final int currentUserID = Configuration.instance.getUserID()!;
     if (files.any((f) => f.uploadedFileID == null)) {
       throw AssertionError("Can only favorite uploaded items");
     }
@@ -161,11 +162,11 @@ class FavoritesService {
     _updateFavoriteFilesCache([file], favFlag: false);
   }
 
-  Future<Collection> _getFavoritesCollection() async {
+  Future<Collection?> _getFavoritesCollection() async {
     if (_cachedFavoritesCollectionID == null) {
       final collections = _collectionsService.getActiveCollections();
       for (final collection in collections) {
-        if (collection.owner.id == _config.getUserID() &&
+        if (collection.owner!.id == _config.getUserID() &&
             collection.type == CollectionType.favorites) {
           _cachedFavoritesCollectionID = collection.id;
           return collection;
@@ -173,30 +174,25 @@ class FavoritesService {
       }
       return null;
     }
-    return _collectionsService.getCollectionByID(_cachedFavoritesCollectionID);
+    return _collectionsService.getCollectionByID(_cachedFavoritesCollectionID!);
   }
 
   Future<int> _getOrCreateFavoriteCollectionID() async {
     if (_cachedFavoritesCollectionID != null) {
-      return _cachedFavoritesCollectionID;
+      return _cachedFavoritesCollectionID!;
     }
     final key = CryptoUtil.generateKey();
-    final encryptedKeyData = CryptoUtil.encryptSync(key, _config.getKey());
-    final encryptedName = CryptoUtil.encryptSync(utf8.encode("Favorites"), key);
+    final encKey = CryptoUtil.encryptSync(key, _config.getKey()!);
+    final encName =
+        CryptoUtil.encryptSync(utf8.encode("Favorites") as Uint8List, key);
     final collection = await _collectionsService.createAndCacheCollection(
-      Collection(
-        null,
-        null,
-        Sodium.bin2base64(encryptedKeyData.encryptedData),
-        Sodium.bin2base64(encryptedKeyData.nonce),
-        null,
-        Sodium.bin2base64(encryptedName.encryptedData),
-        Sodium.bin2base64(encryptedName.nonce),
-        CollectionType.favorites,
-        CollectionAttributes(),
-        null,
-        null,
-        null,
+      CreateRequest(
+        encryptedKey: Sodium.bin2base64(encKey.encryptedData!),
+        keyDecryptionNonce: Sodium.bin2base64(encKey.nonce!),
+        encryptedName: Sodium.bin2base64(encName.encryptedData!),
+        nameDecryptionNonce: Sodium.bin2base64(encName.nonce!),
+        type: CollectionType.favorites,
+        attributes: CollectionAttributes(),
       ),
     );
     _cachedFavoritesCollectionID = collection.id;

+ 34 - 30
lib/services/file_magic_service.dart

@@ -1,6 +1,5 @@
-// @dart=2.9
-
 import 'dart:convert';
+import 'dart:typed_data';
 
 import 'package:dio/dio.dart';
 import 'package:flutter_sodium/flutter_sodium.dart';
@@ -22,8 +21,8 @@ import 'package:photos/utils/file_download_util.dart';
 
 class FileMagicService {
   final _logger = Logger("FileMagicService");
-  Dio _enteDio;
-  FilesDB _filesDB;
+  late Dio _enteDio;
+  late FilesDB _filesDB;
 
   FileMagicService._privateConstructor() {
     _filesDB = FilesDB.instance;
@@ -59,12 +58,12 @@ class FileMagicService {
 
   Future<void> updatePublicMagicMetadata(
     List<File> files,
-    Map<String, dynamic> newMetadataUpdate, {
-    Map<int, Map<String, dynamic>> metadataUpdateMap,
+    Map<String, dynamic>? newMetadataUpdate, {
+    Map<int, Map<String, dynamic>>? metadataUpdateMap,
   }) async {
     final params = <String, dynamic>{};
     params['metadataList'] = [];
-    final int ownerID = Configuration.instance.getUserID();
+    final int ownerID = Configuration.instance.getUserID()!;
     try {
       for (final file in files) {
         if (file.uploadedFileID == null) {
@@ -85,8 +84,8 @@ class FileMagicService {
           "can not apply empty updates",
         );
         final Map<String, dynamic> jsonToUpdate =
-            jsonDecode(file.pubMmdEncodedJson);
-        newUpdates.forEach((key, value) {
+            jsonDecode(file.pubMmdEncodedJson ?? '{}');
+        newUpdates!.forEach((key, value) {
           jsonToUpdate[key] = value;
         });
 
@@ -96,17 +95,17 @@ class FileMagicService {
 
         final fileKey = decryptFileKey(file);
         final encryptedMMd = await CryptoUtil.encryptChaCha(
-          utf8.encode(jsonEncode(jsonToUpdate)),
+          utf8.encode(jsonEncode(jsonToUpdate)) as Uint8List,
           fileKey,
         );
         params['metadataList'].add(
           UpdateMagicMetadataRequest(
-            id: file.uploadedFileID,
+            id: file.uploadedFileID!,
             magicMetadata: MetadataRequest(
               version: file.pubMmdVersion,
               count: jsonToUpdate.length,
-              data: Sodium.bin2base64(encryptedMMd.encryptedData),
-              header: Sodium.bin2base64(encryptedMMd.header),
+              data: Sodium.bin2base64(encryptedMMd.encryptedData!),
+              header: Sodium.bin2base64(encryptedMMd.header!),
             ),
           ),
         );
@@ -119,7 +118,7 @@ class FileMagicService {
       await _filesDB.insertMultiple(files);
       RemoteSyncService.instance.sync(silently: true);
     } on DioError catch (e) {
-      if (e.response != null && e.response.statusCode == 409) {
+      if (e.response != null && e.response!.statusCode == 409) {
         RemoteSyncService.instance.sync(silently: true);
       }
       rethrow;
@@ -134,7 +133,7 @@ class FileMagicService {
     Map<String, dynamic> newMetadataUpdate,
   ) async {
     final params = <String, dynamic>{};
-    final int ownerID = Configuration.instance.getUserID();
+    final int ownerID = Configuration.instance.getUserID()!;
     final batchedFiles = files.chunks(batchSize);
     try {
       for (final batch in batchedFiles) {
@@ -151,7 +150,7 @@ class FileMagicService {
           // current update is simple replace. This will be enhanced in the future,
           // as required.
           final Map<String, dynamic> jsonToUpdate =
-              jsonDecode(file.mMdEncodedJson);
+              jsonDecode(file.mMdEncodedJson ?? '{}');
           newMetadataUpdate.forEach((key, value) {
             jsonToUpdate[key] = value;
           });
@@ -162,17 +161,17 @@ class FileMagicService {
 
           final fileKey = decryptFileKey(file);
           final encryptedMMd = await CryptoUtil.encryptChaCha(
-            utf8.encode(jsonEncode(jsonToUpdate)),
+            utf8.encode(jsonEncode(jsonToUpdate)) as Uint8List,
             fileKey,
           );
           params['metadataList'].add(
             UpdateMagicMetadataRequest(
-              id: file.uploadedFileID,
+              id: file.uploadedFileID!,
               magicMetadata: MetadataRequest(
                 version: file.mMdVersion,
                 count: jsonToUpdate.length,
-                data: Sodium.bin2base64(encryptedMMd.encryptedData),
-                header: Sodium.bin2base64(encryptedMMd.header),
+                data: Sodium.bin2base64(encryptedMMd.encryptedData!),
+                header: Sodium.bin2base64(encryptedMMd.header!),
               ),
             ),
           );
@@ -187,7 +186,7 @@ class FileMagicService {
       // should be eventually synced after remote sync has completed
       RemoteSyncService.instance.sync(silently: true);
     } on DioError catch (e) {
-      if (e.response != null && e.response.statusCode == 409) {
+      if (e.response != null && e.response!.statusCode == 409) {
         RemoteSyncService.instance.sync(silently: true);
       }
       rethrow;
@@ -200,9 +199,9 @@ class FileMagicService {
 
 class UpdateMagicMetadataRequest {
   final int id;
-  final MetadataRequest magicMetadata;
+  final MetadataRequest? magicMetadata;
 
-  UpdateMagicMetadataRequest({this.id, this.magicMetadata});
+  UpdateMagicMetadataRequest({required this.id, required this.magicMetadata});
 
   factory UpdateMagicMetadataRequest.fromJson(dynamic json) {
     return UpdateMagicMetadataRequest(
@@ -217,19 +216,24 @@ class UpdateMagicMetadataRequest {
     final map = <String, dynamic>{};
     map['id'] = id;
     if (magicMetadata != null) {
-      map['magicMetadata'] = magicMetadata.toJson();
+      map['magicMetadata'] = magicMetadata!.toJson();
     }
     return map;
   }
 }
 
 class MetadataRequest {
-  int version;
-  int count;
-  String data;
-  String header;
-
-  MetadataRequest({this.version, this.count, this.data, this.header});
+  int? version;
+  int? count;
+  String? data;
+  String? header;
+
+  MetadataRequest({
+    required this.version,
+    required this.count,
+    required this.data,
+    required this.header,
+  });
 
   MetadataRequest.fromJson(dynamic json) {
     version = json['version'];

+ 4 - 5
lib/services/hidden_service.dart

@@ -24,7 +24,7 @@ extension HiddenService on CollectionsService {
   // collection
   Future<Collection> getDefaultHiddenCollection() async {
     if (cachedDefaultHiddenCollection != null) {
-      return cachedDefaultHiddenCollection;
+      return cachedDefaultHiddenCollection!;
     }
     final int userID = config.getUserID()!;
     final Collection? defaultHidden =
@@ -33,12 +33,12 @@ extension HiddenService on CollectionsService {
     );
     if (defaultHidden != null) {
       cachedDefaultHiddenCollection = defaultHidden;
-      return cachedDefaultHiddenCollection;
+      return cachedDefaultHiddenCollection!;
     }
     final Collection createdHiddenCollection =
         await _createDefaultHiddenAlbum();
     cachedDefaultHiddenCollection = createdHiddenCollection;
-    return cachedDefaultHiddenCollection;
+    return cachedDefaultHiddenCollection!;
   }
 
   Future<bool> hideFiles(
@@ -105,8 +105,7 @@ extension HiddenService on CollectionsService {
       subType: subTypeDefaultHidden,
     );
     _logger.info("Creating Hidden Collection");
-    final collection =
-        await createAndCacheCollection(null, createRequest: createRequest);
+    final collection = await createAndCacheCollection(createRequest);
     _logger.info("Creating Hidden Collection Created Successfully");
     final Collection collectionFromServer =
         await fetchCollectionByID(collection.id);

+ 21 - 18
lib/services/ignored_files_service.dart

@@ -1,5 +1,3 @@
-// @dart=2.9
-
 import 'dart:async';
 import 'dart:io';
 
@@ -17,22 +15,22 @@ class IgnoredFilesService {
   static final IgnoredFilesService instance =
       IgnoredFilesService._privateConstructor();
 
-  Future<Set<String>> _ignoredIDs;
+  Future<Set<String>>? _ignoredIDs;
 
   Future<Set<String>> get ignoredIDs async {
     // lazily instantiate the db the first time it is accessed
     _ignoredIDs ??= _loadExistingIDs();
-    return _ignoredIDs;
+    return _ignoredIDs!;
   }
 
   Future<void> cacheAndInsert(List<IgnoredFile> ignoredFiles) async {
     final existingIDs = await ignoredIDs;
-    existingIDs.addAll(
-      ignoredFiles
-          .map((e) => _idForIgnoredFile(e))
-          .where((id) => id != null)
-          .toSet(),
-    );
+    for (IgnoredFile iFile in ignoredFiles) {
+      final id = _idForIgnoredFile(iFile);
+      if (id != null) {
+        existingIDs.add(id);
+      }
+    }
     return _db.insertMultiple(ignoredFiles);
   }
 
@@ -61,7 +59,8 @@ class IgnoredFilesService {
       if (!shouldSkipUpload(currentlyIgnoredIDs, file)) {
         continue;
       }
-      final id = _getIgnoreID(file.localID, file.deviceFolder, file.title);
+      // _shouldSkipUpload checks for null ignoreID
+      final id = _getIgnoreID(file.localID, file.deviceFolder, file.title)!;
       idsToRemoveFromCache.add(id);
       ignoredFiles.add(
         IgnoredFile(file.localID, file.title, file.deviceFolder, ""),
@@ -81,14 +80,18 @@ class IgnoredFilesService {
 
   Future<Set<String>> _loadExistingIDs() async {
     _logger.fine('loading existing IDs');
-    final result = await _db.getAll();
-    return result
-        .map((e) => _idForIgnoredFile(e))
-        .where((id) => id != null)
-        .toSet();
+    final dbResult = await _db.getAll();
+    final Set<String> result = <String>{};
+    for (IgnoredFile iFile in dbResult) {
+      final id = _idForIgnoredFile(iFile);
+      if (id != null) {
+        result.add(id);
+      }
+    }
+    return result;
   }
 
-  String _idForIgnoredFile(IgnoredFile ignoredFile) {
+  String? _idForIgnoredFile(IgnoredFile ignoredFile) {
     return _getIgnoreID(
       ignoredFile.localID,
       ignoredFile.deviceFolder,
@@ -102,7 +105,7 @@ class IgnoredFilesService {
   // For Android: It returns deviceFolder-title as ID for Android.
   // For iOS, it returns localID as localID is uuid and the title or deviceFolder (aka
   // album name) can be missing due to various reasons.
-  String _getIgnoreID(String localID, String deviceFolder, String title) {
+  String? _getIgnoreID(String? localID, String? deviceFolder, String? title) {
     // file was not uploaded from mobile device
     if (localID == null || localID.isEmpty) {
       return null;

+ 14 - 14
lib/services/local/local_sync_util.dart

@@ -1,9 +1,7 @@
-// @dart = 2.9
 import 'dart:io';
 import 'dart:math';
 
 import 'package:computer/computer.dart';
-import 'package:flutter/foundation.dart';
 import 'package:logging/logging.dart';
 import 'package:photo_manager/photo_manager.dart';
 import 'package:photos/core/event_bus.dart';
@@ -129,8 +127,10 @@ Future<LocalDiffResult> getDiffWithLocal(
   args['pathToLocalIDs'] = pathToLocalIDs;
   final LocalDiffResult diffResult =
       await computer.compute(_getLocalAssetsDiff, param: args);
-  diffResult.uniqueLocalFiles =
-      await _convertLocalAssetsToUniqueFiles(diffResult.localPathAssets);
+  if (diffResult.localPathAssets != null) {
+    diffResult.uniqueLocalFiles =
+        await _convertLocalAssetsToUniqueFiles(diffResult.localPathAssets!);
+  }
   return diffResult;
 }
 
@@ -211,12 +211,12 @@ Future<List<File>> _convertLocalAssetsToUniqueFiles(
 /// [needTitle] impacts the performance for fetching the actual [AssetEntity]
 /// in iOS. Same is true for [containsModifiedPath]
 Future<List<AssetPathEntity>> _getGalleryList({
-  final int updateFromTime,
-  final int updateToTime,
+  final int? updateFromTime,
+  final int? updateToTime,
   final bool containsModifiedPath = false,
   // in iOS fetching the AssetEntity title impacts performance
   final bool needsTitle = true,
-  final OrderOption orderOption,
+  final OrderOption? orderOption,
 }) async {
   final filterOptionGroup = FilterOptionGroup();
   filterOptionGroup.setOption(
@@ -312,24 +312,24 @@ class LocalPathAsset {
   final String pathName;
 
   LocalPathAsset({
-    @required this.localIDs,
-    @required this.pathName,
-    @required this.pathID,
+    required this.localIDs,
+    required this.pathName,
+    required this.pathID,
   });
 }
 
 class LocalDiffResult {
   // unique localPath Assets.
-  final List<LocalPathAsset> localPathAssets;
+  final List<LocalPathAsset>? localPathAssets;
 
   // set of File object created from localPathAssets
-  List<File> uniqueLocalFiles;
+  List<File>? uniqueLocalFiles;
 
   // newPathToLocalIDs represents new entries which needs to be synced to
   // the local db
-  final Map<String, Set<String>> newPathToLocalIDs;
+  final Map<String, Set<String>>? newPathToLocalIDs;
 
-  final Map<String, Set<String>> deletePathToLocalIDs;
+  final Map<String, Set<String>>? deletePathToLocalIDs;
 
   LocalDiffResult({
     this.uniqueLocalFiles,

+ 5 - 7
lib/services/local_authentication_service.dart

@@ -1,5 +1,3 @@
-// @dart=2.9
-
 import 'package:flutter/material.dart';
 import 'package:local_auth/local_auth.dart';
 import 'package:photos/core/configuration.dart';
@@ -18,9 +16,9 @@ class LocalAuthenticationService {
     String infoMessage,
   ) async {
     if (await _isLocalAuthSupportedOnDevice()) {
-      AppLock.of(context).setEnabled(false);
+      AppLock.of(context)!.setEnabled(false);
       final result = await requestAuthentication(infoMessage);
-      AppLock.of(context).setEnabled(
+      AppLock.of(context)!.setEnabled(
         Configuration.instance.shouldShowLockScreen(),
       );
       if (!result) {
@@ -41,17 +39,17 @@ class LocalAuthenticationService {
     String errorDialogTitle = "",
   ]) async {
     if (await _isLocalAuthSupportedOnDevice()) {
-      AppLock.of(context).disable();
+      AppLock.of(context)!.disable();
       final result = await requestAuthentication(
         infoMessage,
       );
       if (result) {
-        AppLock.of(context).setEnabled(shouldEnableLockScreen);
+        AppLock.of(context)!.setEnabled(shouldEnableLockScreen);
         await Configuration.instance
             .setShouldShowLockScreen(shouldEnableLockScreen);
         return true;
       } else {
-        AppLock.of(context)
+        AppLock.of(context)!
             .setEnabled(Configuration.instance.shouldShowLockScreen());
       }
     } else {

+ 23 - 23
lib/services/local_file_update_service.dart

@@ -1,5 +1,3 @@
-// @dart=2.9
-
 import 'dart:async';
 import 'dart:core';
 import 'dart:io';
@@ -20,15 +18,15 @@ import 'package:shared_preferences/shared_preferences.dart';
 // LocalFileUpdateService tracks all the potential local file IDs which have
 // changed/modified on the device and needed to be uploaded again.
 class LocalFileUpdateService {
-  FileUpdationDB _fileUpdationDB;
-  SharedPreferences _prefs;
-  Logger _logger;
+  late FileUpdationDB _fileUpdationDB;
+  late SharedPreferences _prefs;
+  late Logger _logger;
   static const isLocationMigrationComplete = "fm_isLocationMigrationComplete";
   static const isLocalImportDone = "fm_IsLocalImportDone";
   static const isBadCreationTimeImportDone = 'fm_badCreationTime';
   static const isBadCreationTimeMigrationComplete =
       'fm_badCreationTimeCompleted';
-  Completer<void> _existingMigration;
+  Completer<void>? _existingMigration;
 
   LocalFileUpdateService._privateConstructor() {
     _logger = Logger((LocalFileUpdateService).toString());
@@ -43,13 +41,13 @@ class LocalFileUpdateService {
       LocalFileUpdateService._privateConstructor();
 
   bool isBadCreationMigrationCompleted() {
-    return _prefs.get(isBadCreationTimeMigrationComplete) ?? false;
+    return (_prefs.getBool(isBadCreationTimeMigrationComplete) ?? false);
   }
 
   Future<void> markUpdatedFilesForReUpload() async {
     if (_existingMigration != null) {
       _logger.info("migration is already in progress, skipping");
-      return _existingMigration.future;
+      return _existingMigration!.future;
     }
     _existingMigration = Completer<void>();
     try {
@@ -107,11 +105,10 @@ class LocalFileUpdateService {
       MediaUploadData uploadData;
       try {
         uploadData = await getUploadData(file);
-        if (uploadData != null &&
-            uploadData.hashData != null &&
+        if (uploadData.hashData != null &&
             file.hash != null &&
-            (file.hash == uploadData.hashData.fileHash ||
-                file.hash == uploadData.hashData.zipHash)) {
+            (file.hash == uploadData.hashData!.fileHash ||
+                file.hash == uploadData.hashData!.zipHash)) {
           _logger.info("Skip file update as hash matched ${file.tag}");
         } else {
           _logger.info(
@@ -119,15 +116,15 @@ class LocalFileUpdateService {
           );
           await clearCache(file);
           await FilesDB.instance.updateUploadedFile(
-            file.localID,
+            file.localID!,
             file.title,
             file.location,
-            file.creationTime,
-            file.modificationTime,
+            file.creationTime!,
+            file.modificationTime!,
             null,
           );
         }
-        processedIDs.add(file.localID);
+        processedIDs.add(file.localID!);
       } catch (e) {
         _logger.severe("Failed to get file uploadData", e);
       } finally {}
@@ -144,10 +141,8 @@ class LocalFileUpdateService {
     // delete the file from app's internal cache if it was copied to app
     // for upload. Shared Media should only be cleared when the upload
     // succeeds.
-    if (Platform.isIOS &&
-        mediaUploadData != null &&
-        mediaUploadData.sourceFile != null) {
-      await mediaUploadData.sourceFile.delete();
+    if (Platform.isIOS && mediaUploadData.sourceFile != null) {
+      await mediaUploadData.sourceFile?.delete();
     }
     return mediaUploadData;
   }
@@ -165,8 +160,13 @@ class LocalFileUpdateService {
         FileUpdationDB.badCreationTime,
       );
       if (generatedIDs.isNotEmpty) {
-        final List<int> genIdIntList =
-            generatedIDs.map((e) => int.tryParse(e)).toList();
+        final List<int> genIdIntList = [];
+        for (String genIdString in generatedIDs) {
+          final int? genIdInt = int.tryParse(genIdString);
+          if (genIdInt != null) {
+            genIdIntList.add(genIdInt);
+          }
+        }
 
         final filesWithBadTime =
             (await FilesDB.instance.getFilesFromGeneratedIDs(genIdIntList))
@@ -196,7 +196,7 @@ class LocalFileUpdateService {
     }
     _logger.info('_importFilesWithBadCreationTime');
     final EnteWatch watch = EnteWatch("_importFilesWithBadCreationTime");
-    final int ownerID = Configuration.instance.getUserID();
+    final int ownerID = Configuration.instance.getUserID()!;
     final filesGeneratedID = await FilesDB.instance
         .getGeneratedIDForFilesOlderThan(jan011981Time, ownerID);
     await _fileUpdationDB.insertMultiple(

+ 54 - 17
lib/services/local_sync_service.dart

@@ -1,11 +1,8 @@
-// @dart=2.9
-
 import 'dart:async';
 import 'dart:io';
 
 import 'package:computer/computer.dart';
 import 'package:flutter/foundation.dart';
-import 'package:flutter/widgets.dart';
 import 'package:logging/logging.dart';
 import 'package:photo_manager/photo_manager.dart';
 import 'package:photos/core/configuration.dart';
@@ -28,8 +25,8 @@ class LocalSyncService {
   final _logger = Logger("LocalSyncService");
   final _db = FilesDB.instance;
   final Computer _computer = Computer();
-  SharedPreferences _prefs;
-  Completer<void> _existingSync;
+  late SharedPreferences _prefs;
+  Completer<void>? _existingSync;
 
   static const kDbUpdationTimeKey = "db_updation_time";
   static const kHasCompletedFirstImportKey = "has_completed_firstImport";
@@ -75,7 +72,7 @@ class LocalSyncService {
     }
     if (_existingSync != null) {
       _logger.warning("Sync already in progress, skipping.");
-      return _existingSync.future;
+      return _existingSync!.future;
     }
     _existingSync = Completer<void>();
     final existingLocalFileIDs = await _db.getExistingLocalFileIDs();
@@ -118,7 +115,7 @@ class LocalSyncService {
       );
     }
     if (!_prefs.containsKey(kHasCompletedFirstImportKey) ||
-        !_prefs.getBool(kHasCompletedFirstImportKey)) {
+        !(_prefs.getBool(kHasCompletedFirstImportKey)!)) {
       await _prefs.setBool(kHasCompletedFirstImportKey, true);
       // mark device collection has imported on first import
       await _refreshDeviceFolderCountAndCover(isFirstSync: true);
@@ -130,7 +127,7 @@ class LocalSyncService {
     final endTime = DateTime.now().microsecondsSinceEpoch;
     final duration = Duration(microseconds: endTime - startTime);
     _logger.info("Load took " + duration.inMilliseconds.toString() + "ms");
-    _existingSync.complete();
+    _existingSync?.complete();
     _existingSync = null;
   }
 
@@ -202,23 +199,24 @@ class LocalSyncService {
     );
     bool hasAnyMappingChanged = false;
     if (localDiffResult.newPathToLocalIDs?.isNotEmpty ?? false) {
-      await _db.insertPathIDToLocalIDMapping(localDiffResult.newPathToLocalIDs);
+      await _db
+          .insertPathIDToLocalIDMapping(localDiffResult.newPathToLocalIDs!);
       hasAnyMappingChanged = true;
     }
     if (localDiffResult.deletePathToLocalIDs?.isNotEmpty ?? false) {
       await _db
-          .deletePathIDToLocalIDMapping(localDiffResult.deletePathToLocalIDs);
+          .deletePathIDToLocalIDMapping(localDiffResult.deletePathToLocalIDs!);
       hasAnyMappingChanged = true;
     }
     final bool hasUnsyncedFiles =
         localDiffResult.uniqueLocalFiles?.isNotEmpty ?? false;
     if (hasUnsyncedFiles) {
       await _db.insertMultiple(
-        localDiffResult.uniqueLocalFiles,
+        localDiffResult.uniqueLocalFiles!,
         conflictAlgorithm: ConflictAlgorithm.ignore,
       );
       _logger.info(
-        "Inserted ${localDiffResult.uniqueLocalFiles.length} "
+        "Inserted ${localDiffResult.uniqueLocalFiles?.length} "
         "un-synced files",
       );
     }
@@ -239,29 +237,54 @@ class LocalSyncService {
   }
 
   Future<void> trackEditedFile(File file) async {
+    if (file.localID == null) {
+      debugPrint("Warning: Edit file has no localID");
+      return;
+    }
     final editedIDs = _getEditedFileIDs();
-    editedIDs.add(file.localID);
+    editedIDs.add(file.localID!);
     await _prefs.setStringList(kEditedFileIDsKey, editedIDs);
   }
 
   List<String> _getEditedFileIDs() {
     if (_prefs.containsKey(kEditedFileIDsKey)) {
-      return _prefs.getStringList(kEditedFileIDsKey);
+      return _prefs.getStringList(kEditedFileIDsKey)!;
     } else {
       final List<String> editedIDs = [];
       return editedIDs;
     }
   }
 
+<<<<<<< HEAD
+=======
+  Future<void> trackDownloadedFile(String localID) async {
+    final downloadedIDs = _getDownloadedFileIDs();
+    downloadedIDs.add(localID);
+    await _prefs.setStringList(kDownloadedFileIDsKey, downloadedIDs);
+  }
+
+  List<String> _getDownloadedFileIDs() {
+    if (_prefs.containsKey(kDownloadedFileIDsKey)) {
+      return _prefs.getStringList(kDownloadedFileIDsKey)!;
+    } else {
+      return <String>[];
+    }
+  }
+
+>>>>>>> main
   Future<void> trackInvalidFile(File file) async {
+    if (file.localID == null) {
+      debugPrint("Warning: Invalid file has no localID");
+      return;
+    }
     final invalidIDs = _getInvalidFileIDs();
-    invalidIDs.add(file.localID);
+    invalidIDs.add(file.localID!);
     await _prefs.setStringList(kInvalidFileIDsKey, invalidIDs);
   }
 
   List<String> _getInvalidFileIDs() {
     if (_prefs.containsKey(kInvalidFileIDsKey)) {
-      return _prefs.getStringList(kInvalidFileIDsKey);
+      return _prefs.getStringList(kInvalidFileIDsKey)!;
     } else {
       return <String>[];
     }
@@ -363,7 +386,9 @@ class LocalSyncService {
       );
       final List<String> updatedLocalIDs = [];
       for (final file in updatedFiles) {
-        updatedLocalIDs.add(file.localID);
+        if (file.localID != null) {
+          updatedLocalIDs.add(file.localID!);
+        }
       }
       await FileUpdationDB.instance.insertMultiple(
         updatedLocalIDs,
@@ -376,7 +401,19 @@ class LocalSyncService {
     // In case of iOS limit permission, this call back is fired immediately
     // after file selection dialog is dismissed.
     PhotoManager.addChangeCallback((value) async {
+<<<<<<< HEAD
       checkAndSync();
+=======
+      _logger.info("Something changed on disk");
+      if (_existingSync != null) {
+        await _existingSync!.future;
+      }
+      if (hasGrantedLimitedPermissions()) {
+        syncAll();
+      } else {
+        sync().then((value) => _refreshDeviceFolderCountAndCover());
+      }
+>>>>>>> main
     });
     PhotoManager.startChangeNotify();
   }

+ 6 - 8
lib/services/memories_service.dart

@@ -1,5 +1,3 @@
-// @dart=2.9
-
 import 'package:flutter/foundation.dart';
 import 'package:logging/logging.dart';
 import 'package:photos/core/constants.dart';
@@ -18,8 +16,8 @@ class MemoriesService extends ChangeNotifier {
   static const daysBefore = 7;
   static const daysAfter = 1;
 
-  List<Memory> _cachedMemories;
-  Future<List<Memory>> _future;
+  List<Memory>? _cachedMemories;
+  Future<List<Memory>>? _future;
 
   MemoriesService._privateConstructor();
 
@@ -45,13 +43,13 @@ class MemoriesService extends ChangeNotifier {
 
   Future<List<Memory>> getMemories() async {
     if (_cachedMemories != null) {
-      return _cachedMemories;
+      return _cachedMemories!;
     }
     if (_future != null) {
-      return _future;
+      return _future!;
     }
     _future = _fetchMemories();
-    return _future;
+    return _future!;
   }
 
   Future<List<Memory>> _fetchMemories() async {
@@ -90,7 +88,7 @@ class MemoriesService extends ChangeNotifier {
       }
     }
     _cachedMemories = memories;
-    return _cachedMemories;
+    return _cachedMemories!;
   }
 
   DateTime _getDate(DateTime present, int yearAgo) {

+ 10 - 8
lib/services/push_service.dart

@@ -1,5 +1,3 @@
-// @dart=2.9
-
 import 'package:firebase_core/firebase_core.dart';
 import 'package:firebase_messaging/firebase_messaging.dart';
 import 'package:logging/logging.dart';
@@ -21,7 +19,7 @@ class PushService {
   static final PushService instance = PushService._privateConstructor();
   static final _logger = Logger("PushService");
 
-  SharedPreferences _prefs;
+  late SharedPreferences _prefs;
 
   PushService._privateConstructor();
 
@@ -46,14 +44,15 @@ class PushService {
   }
 
   Future<void> _configurePushToken() async {
-    final fcmToken = await FirebaseMessaging.instance.getToken();
+    final String? fcmToken = await FirebaseMessaging.instance.getToken();
     final shouldForceRefreshServerToken =
         DateTime.now().microsecondsSinceEpoch -
                 (_prefs.getInt(kLastFCMTokenUpdationTime) ?? 0) >
             kFCMTokenUpdationIntervalInMicroSeconds;
-    if (_prefs.getString(kFCMPushToken) != fcmToken ||
-        shouldForceRefreshServerToken) {
-      final apnsToken = await FirebaseMessaging.instance.getAPNSToken();
+    if (fcmToken != null &&
+        (_prefs.getString(kFCMPushToken) != fcmToken ||
+            shouldForceRefreshServerToken)) {
+      final String? apnsToken = await FirebaseMessaging.instance.getAPNSToken();
       try {
         _logger.info("Updating token on server");
         await _setPushTokenOnServer(fcmToken, apnsToken);
@@ -71,7 +70,10 @@ class PushService {
     }
   }
 
-  Future<void> _setPushTokenOnServer(String fcmToken, String apnsToken) async {
+  Future<void> _setPushTokenOnServer(
+    String fcmToken,
+    String? apnsToken,
+  ) async {
     await Network.instance.enteDio.post(
       "/push/token",
       data: {

+ 40 - 36
lib/services/remote_sync_service.dart

@@ -1,5 +1,3 @@
-// @dart=2.9
-
 import 'dart:async';
 import 'dart:io';
 import 'dart:math';
@@ -44,8 +42,8 @@ class RemoteSyncService {
   final LocalFileUpdateService _localFileUpdateService =
       LocalFileUpdateService.instance;
   int _completedUploads = 0;
-  SharedPreferences _prefs;
-  Completer<void> _existingSync;
+  late SharedPreferences _prefs;
+  Completer<void>? _existingSync;
   bool _existingSyncSilent = false;
 
   static const kHasSyncedArchiveKey = "has_synced_archive";
@@ -89,7 +87,7 @@ class RemoteSyncService {
       if (_existingSyncSilent == true && silently == false) {
         _existingSyncSilent = false;
       }
-      return _existingSync.future;
+      return _existingSync?.future;
     }
     _existingSync = Completer<void>();
     _existingSyncSilent = silently;
@@ -99,8 +97,7 @@ class RemoteSyncService {
       // remote-sync is done. This is done to avoid adding existing files to
       // the same or different collection when user had already uploaded them
       // before.
-      final bool hasSyncedBefore =
-          _prefs.containsKey(_isFirstRemoteSyncDone) ?? false;
+      final bool hasSyncedBefore = _prefs.containsKey(_isFirstRemoteSyncDone);
       if (hasSyncedBefore) {
         await syncDeviceCollectionFilesForUpload();
       }
@@ -120,7 +117,7 @@ class RemoteSyncService {
       final hasUploadedFiles = await _uploadFiles(filesToBeUploaded);
       if (hasUploadedFiles) {
         await _pullDiff();
-        _existingSync.complete();
+        _existingSync?.complete();
         _existingSync = null;
         await syncDeviceCollectionFilesForUpload();
         final hasMoreFilesToBackup = (await _getFilesToBeUploaded()).isNotEmpty;
@@ -133,11 +130,11 @@ class RemoteSyncService {
           Bus.instance.fire(SyncStatusUpdate(SyncStatus.completedBackup));
         }
       } else {
-        _existingSync.complete();
+        _existingSync?.complete();
         _existingSync = null;
       }
     } catch (e, s) {
-      _existingSync.complete();
+      _existingSync?.complete();
       _existingSync = null;
       // rethrow whitelisted error so that UI status can be updated correctly.
       if (e is UnauthorizedError ||
@@ -202,7 +199,7 @@ class RemoteSyncService {
     final diff =
         await _diffFetcher.getEncryptedFilesDiff(collectionID, sinceTime);
     if (diff.deletedFiles.isNotEmpty) {
-      final fileIDs = diff.deletedFiles.map((f) => f.uploadedFileID).toList();
+      final fileIDs = diff.deletedFiles.map((f) => f.uploadedFileID!).toList();
       final deletedFiles = (await _db.getFilesFromIDs(fileIDs)).values.toList();
       await _db.deleteFilesFromCollection(collectionID, fileIDs);
       Bus.instance.fire(
@@ -259,7 +256,7 @@ class RemoteSyncService {
   }
 
   Future<void> syncDeviceCollectionFilesForUpload() async {
-    final int ownerID = _config.getUserID();
+    final int ownerID = _config.getUserID()!;
 
     final deviceCollections = await _db.getDeviceCollections();
     deviceCollections.removeWhere((element) => !element.shouldBackup);
@@ -290,7 +287,7 @@ class RemoteSyncService {
 
       moreFilesMarkedForBackup = true;
       await _db.setCollectionIDForUnMappedLocalFiles(
-        deviceCollection.collectionID,
+        deviceCollection.collectionID!,
         localIDsToSync,
       );
 
@@ -298,7 +295,9 @@ class RemoteSyncService {
       // the collection. This can happen when a user has marked a folder
       // for sync, then un-synced it and again tries to mark if for sync.
       final Set<String> existingMapping =
-          await _db.getLocalFileIDsForCollection(deviceCollection.collectionID);
+          await _db.getLocalFileIDsForCollection(
+        deviceCollection.collectionID!,
+      );
       final Set<String> commonElements =
           localIDsToSync.intersection(existingMapping);
       if (commonElements.isNotEmpty) {
@@ -322,7 +321,7 @@ class RemoteSyncService {
         final List<File> newFilesToInsert = [];
         final Set<String> fileFoundForLocalIDs = {};
         for (var existingFile in filesWithCollectionID) {
-          final String localID = existingFile.localID;
+          final String localID = existingFile.localID!;
           if (!fileFoundForLocalIDs.contains(localID)) {
             existingFile.generatedID = null;
             existingFile.collectionID = deviceCollection.collectionID;
@@ -400,7 +399,7 @@ class RemoteSyncService {
       final List<int> entriesToDelete = [];
       for (File pendingUpload in pendingUploads) {
         if (localIDsInOtherFileEntries.contains(pendingUpload.localID)) {
-          entriesToDelete.add(pendingUpload.generatedID);
+          entriesToDelete.add(pendingUpload.generatedID!);
         } else {
           pendingUpload.collectionID = null;
           entriesToUpdate.add(pendingUpload);
@@ -418,7 +417,7 @@ class RemoteSyncService {
   Future<void> _createCollectionForDevicePath(
     DeviceCollection deviceCollection,
   ) async {
-    int deviceCollectionID = deviceCollection.collectionID;
+    int deviceCollectionID = deviceCollection.collectionID ?? -1;
     if (deviceCollectionID != -1) {
       final collectionByID =
           _collectionsService.getCollectionByID(deviceCollectionID);
@@ -468,7 +467,7 @@ class RemoteSyncService {
   }
 
   Future<bool> _uploadFiles(List<File> filesToBeUploaded) async {
-    final int ownerID = _config.getUserID();
+    final int ownerID = _config.getUserID()!;
     final updatedFileIDs = await _db.getUploadedFileIDsToBeUpdated(ownerID);
     if (updatedFileIDs.isNotEmpty) {
       _logger.info("Identified ${updatedFileIDs.length} files for reupload");
@@ -492,7 +491,9 @@ class RemoteSyncService {
         break;
       }
       final file = await _db.getUploadedFileInAnyCollection(uploadedFileID);
-      _uploadFile(file, file.collectionID, futures);
+      if (file != null) {
+        _uploadFile(file, file.collectionID!, futures);
+      }
     }
 
     for (final file in filesToBeUploaded) {
@@ -504,7 +505,9 @@ class RemoteSyncService {
       // prefer existing collection ID for manually uploaded files.
       // See https://github.com/ente-io/photos-app/pull/187
       final collectionID = file.collectionID ??
-          (await _collectionsService.getOrCreateForPath(file.deviceFolder)).id;
+          (await _collectionsService
+                  .getOrCreateForPath(file.deviceFolder ?? 'Unknown Folder'))
+              .id;
       _uploadFile(file, collectionID, futures);
     }
 
@@ -587,7 +590,7 @@ class RemoteSyncService {
         localUploadedFromDevice = 0,
         localButUpdatedOnDevice = 0,
         remoteNewFile = 0;
-    final int userID = _config.getUserID();
+    final int userID = _config.getUserID()!;
     bool needsGalleryReload = false;
     // this is required when same file is uploaded twice in the same
     // collection. Without this check, if both remote files are part of same
@@ -599,13 +602,14 @@ class RemoteSyncService {
     for (File remoteDiff in diff) {
       // existingFile will be either set to existing collectionID+localID or
       // to the unclaimed aka not already linked to any uploaded file.
-      File existingFile;
+      File? existingFile;
       if (remoteDiff.generatedID != null) {
         // Case [1] Check and clear local cache when uploadedFile already exist
         // Note: Existing file can be null here if it's replaced by the time we
         // reach here
-        existingFile = await _db.getFile(remoteDiff.generatedID);
-        if (_shouldClearCache(remoteDiff, existingFile)) {
+        existingFile = await _db.getFile(remoteDiff.generatedID!);
+        if (existingFile != null &&
+            _shouldClearCache(remoteDiff, existingFile)) {
           needsGalleryReload = true;
           await clearCache(remoteDiff);
         }
@@ -636,9 +640,9 @@ class RemoteSyncService {
       if (existingFile == null && remoteDiff.localID != null) {
         final localFileEntries = await _db.getUnlinkedLocalMatchesForRemoteFile(
           userID,
-          remoteDiff.localID,
+          remoteDiff.localID!,
           remoteDiff.fileType,
-          title: remoteDiff.title,
+          title: remoteDiff.title ?? '',
           deviceFolder: remoteDiff.deviceFolder ?? '',
         );
         if (localFileEntries.isEmpty) {
@@ -653,18 +657,18 @@ class RemoteSyncService {
               )
               .reduce(max);
 
-          /* todo: In case of iOS, we will miss any asset modification in
+          /* Note: In case of iOS, we will miss any asset modification in
             between of two installation. This is done to avoid fetching assets
             from iCloud when modification time could have changed for number of
             reasons. To fix this, we need to identify a way to store version
             for the adjustments or just if the asset has been modified ever.
             https://stackoverflow.com/a/50093266/546896
             */
-          if (maxModificationTime > remoteDiff.modificationTime &&
+          if (maxModificationTime > remoteDiff.modificationTime! &&
               Platform.isAndroid) {
             localButUpdatedOnDevice++;
             await FileUpdationDB.instance.insertMultiple(
-              [remoteDiff.localID],
+              [remoteDiff.localID!],
               FileUpdationDB.modificationTimeUpdated,
             );
           }
@@ -680,7 +684,7 @@ class RemoteSyncService {
             // setting the generated ID of remoteFile to localFile generatedID
             existingFile = localFileEntries.first;
             localUploadedFromDevice++;
-            alreadyClaimedLocalFilesGenID.add(existingFile.generatedID);
+            alreadyClaimedLocalFilesGenID.add(existingFile.generatedID!);
             remoteDiff.generatedID = existingFile.generatedID;
           }
         }
@@ -716,17 +720,17 @@ class RemoteSyncService {
   }
 
   bool _shouldClearCache(File remoteFile, File existingFile) {
-    if (remoteFile.hash != null && existingFile?.hash != null) {
+    if (remoteFile.hash != null && existingFile.hash != null) {
       return remoteFile.hash != existingFile.hash;
     }
-    return remoteFile.updationTime != (existingFile?.updationTime ?? 0);
+    return remoteFile.updationTime != (existingFile.updationTime ?? 0);
   }
 
   bool _shouldReloadHomeGallery(File remoteFile, File existingFile) {
-    int remoteCreationTime = remoteFile.creationTime;
+    int remoteCreationTime = remoteFile.creationTime!;
     if (remoteFile.pubMmdVersion > 0 &&
-        (remoteFile.pubMagicMetadata.editedTime ?? 0) != 0) {
-      remoteCreationTime = remoteFile.pubMagicMetadata.editedTime;
+        (remoteFile.pubMagicMetadata?.editedTime ?? 0) != 0) {
+      remoteCreationTime = remoteFile.pubMagicMetadata!.editedTime!;
     }
     if (remoteCreationTime != existingFile.creationTime) {
       return true;
@@ -774,7 +778,7 @@ class RemoteSyncService {
   void _sortByTimeAndType(List<File> file) {
     file.sort((first, second) {
       if (first.fileType == second.fileType) {
-        return second.creationTime.compareTo(first.creationTime);
+        return second.creationTime!.compareTo(first.creationTime!);
       } else if (first.fileType == FileType.video) {
         return 1;
       } else {

+ 20 - 21
lib/services/search_service.dart

@@ -1,5 +1,3 @@
-// @dart=2.9
-
 import 'package:logging/logging.dart';
 import 'package:photos/core/event_bus.dart';
 import 'package:photos/core/network.dart';
@@ -21,7 +19,7 @@ import 'package:photos/utils/date_time_util.dart';
 import 'package:tuple/tuple.dart';
 
 class SearchService {
-  Future<List<File>> _cachedFilesFuture;
+  Future<List<File>>? _cachedFilesFuture;
   final _enteDio = Network.instance.enteDio;
   final _logger = Logger((SearchService).toString());
   final _collectionService = CollectionsService.instance;
@@ -44,12 +42,12 @@ class SearchService {
 
   Future<List<File>> _getAllFiles() async {
     if (_cachedFilesFuture != null) {
-      return _cachedFilesFuture;
+      return _cachedFilesFuture!;
     }
     _logger.fine("Reading all files from db");
     _cachedFilesFuture =
         FilesDB.instance.getAllFilesFromDB(ignoreCollections());
-    return _cachedFilesFuture;
+    return _cachedFilesFuture!;
   }
 
   void clearCache() {
@@ -75,12 +73,13 @@ class SearchService {
 
         for (var file in allFiles) {
           if (_isValidLocation(file.location) &&
-              _isLocationWithinBounds(file.location, locationData)) {
+              _isLocationWithinBounds(file.location!, locationData)) {
             filesInLocation.add(file);
           }
         }
         filesInLocation.sort(
-          (first, second) => second.creationTime.compareTo(first.creationTime),
+          (first, second) =>
+              second.creationTime!.compareTo(first.creationTime!),
         );
         if (filesInLocation.isNotEmpty) {
           searchResults.add(
@@ -116,7 +115,7 @@ class SearchService {
       }
 
       if (!c.collection.isHidden() &&
-          c.collection.name.toLowerCase().contains(
+          c.collection.name!.toLowerCase().contains(
                 query.toLowerCase(),
               )) {
         collectionSearchResults.add(AlbumSearchResult(c));
@@ -206,7 +205,7 @@ class SearchService {
     final List<File> captionMatch = <File>[];
     final List<File> displayNameMatch = <File>[];
     for (File eachFile in allFiles) {
-      if (eachFile.caption != null && pattern.hasMatch(eachFile.caption)) {
+      if (eachFile.caption != null && pattern.hasMatch(eachFile.caption!)) {
         captionMatch.add(eachFile);
       }
       if (pattern.hasMatch(eachFile.displayName)) {
@@ -252,7 +251,7 @@ class SearchService {
         if (!resultMap.containsKey(exnType)) {
           resultMap[exnType] = <File>[];
         }
-        resultMap[exnType].add(eachFile);
+        resultMap[exnType]!.add(eachFile);
       }
     }
     for (MapEntry<String, List<File>> entry in resultMap.entries) {
@@ -298,7 +297,7 @@ class SearchService {
     for (var potentialDate in potentialDates) {
       final int day = potentialDate.item1;
       final int month = potentialDate.item2.monthNumber;
-      final int year = potentialDate.item3; // nullable
+      final int? year = potentialDate.item3; // nullable
       final matchedFiles =
           await FilesDB.instance.getFilesCreatedWithinDurations(
         _getDurationsForCalendarDateInEveryYear(day, month, year: year),
@@ -338,7 +337,7 @@ class SearchService {
   List<List<int>> _getDurationsForCalendarDateInEveryYear(
     int day,
     int month, {
-    int year,
+    int? year,
   }) {
     final List<List<int>> durationsOfHolidayInEveryYear = [];
     final int startYear = year ?? searchStartYear;
@@ -367,7 +366,7 @@ class SearchService {
     return durationsOfMonthInEveryYear;
   }
 
-  bool _isValidLocation(Location location) {
+  bool _isValidLocation(Location? location) {
     return location != null &&
         location.latitude != null &&
         location.latitude != 0 &&
@@ -380,14 +379,14 @@ class SearchService {
     LocationDataFromResponse locationData,
   ) {
     //format returned by the api is [lng,lat,lng,lat] where indexes 0 & 1 are southwest and 2 & 3 northeast
-    return location.longitude > locationData.bbox[0] &&
-        location.latitude > locationData.bbox[1] &&
-        location.longitude < locationData.bbox[2] &&
-        location.latitude < locationData.bbox[3];
+    return location.longitude! > locationData.bbox[0] &&
+        location.latitude! > locationData.bbox[1] &&
+        location.longitude! < locationData.bbox[2] &&
+        location.latitude! < locationData.bbox[3];
   }
 
-  List<Tuple3<int, MonthData, int>> _getPossibleEventDate(String query) {
-    final List<Tuple3<int, MonthData, int>> possibleEvents = [];
+  List<Tuple3<int, MonthData, int?>> _getPossibleEventDate(String query) {
+    final List<Tuple3<int, MonthData, int?>> possibleEvents = [];
     if (query.trim().isEmpty) {
       return possibleEvents;
     }
@@ -402,13 +401,13 @@ class SearchService {
       return possibleEvents;
     }
 
-    final int day = int.tryParse(result[0]);
+    final int? day = int.tryParse(result[0]);
     if (day == null || day < 1 || day > 31) {
       return possibleEvents;
     }
     final List<MonthData> potentialMonth =
         resultCount > 1 ? _getMatchingMonths(result[1]) : allMonths;
-    final int parsedYear = resultCount >= 3 ? int.tryParse(result[2]) : null;
+    final int? parsedYear = resultCount >= 3 ? int.tryParse(result[2]) : null;
     final List<int> matchingYears = [];
     if (parsedYear != null) {
       bool foundMatch = false;

+ 14 - 14
lib/services/sync_service.dart

@@ -1,5 +1,3 @@
-// @dart=2.9
-
 import 'dart:async';
 import 'dart:io';
 
@@ -33,9 +31,9 @@ class SyncService {
   final _enteDio = Network.instance.enteDio;
   final _uploader = FileUploader.instance;
   bool _syncStopRequested = false;
-  Completer<bool> _existingSync;
-  SharedPreferences _prefs;
-  SyncStatusUpdate _lastSyncStatusEvent;
+  Completer<bool>? _existingSync;
+  late SharedPreferences _prefs;
+  SyncStatusUpdate? _lastSyncStatusEvent;
 
   static const kLastStorageLimitExceededNotificationPushTime =
       "last_storage_limit_exceeded_notification_push_time";
@@ -70,24 +68,26 @@ class SyncService {
     }
   }
 
+  // Note: Do not use this future for anything except log out.
+  // This is prone to bugs due to any potential race conditions
   Future<bool> existingSync() async {
-    return _existingSync.future;
+    return _existingSync?.future ?? Future.value(true);
   }
 
   Future<bool> sync() async {
     _syncStopRequested = false;
     if (_existingSync != null) {
       _logger.warning("Sync already in progress, skipping.");
-      return _existingSync.future;
+      return _existingSync!.future;
     }
     _existingSync = Completer<bool>();
     bool successful = false;
     try {
       await _doSync();
       if (_lastSyncStatusEvent != null &&
-          _lastSyncStatusEvent.status !=
+          _lastSyncStatusEvent!.status !=
               SyncStatus.completedFirstGalleryImport &&
-          _lastSyncStatusEvent.status != SyncStatus.completedBackup) {
+          _lastSyncStatusEvent!.status != SyncStatus.completedBackup) {
         Bus.instance.fire(SyncStatusUpdate(SyncStatus.completedBackup));
       }
       successful = true;
@@ -139,7 +139,7 @@ class SyncService {
       Bus.instance.fire(SyncStatusUpdate(SyncStatus.error));
       rethrow;
     } finally {
-      _existingSync.complete(successful);
+      _existingSync?.complete(successful);
       _existingSync = null;
       _lastSyncStatusEvent = null;
       _logger.info("Syncing completed");
@@ -160,7 +160,7 @@ class SyncService {
     return _existingSync != null;
   }
 
-  SyncStatusUpdate getLastSyncStatusEvent() {
+  SyncStatusUpdate? getLastSyncStatusEvent() {
     return _lastSyncStatusEvent;
   }
 
@@ -198,7 +198,7 @@ class SyncService {
     );
   }
 
-  Future<void> deleteFilesOnServer(List<int> fileIDs) async {
+  Future<Response> deleteFilesOnServer(List<int> fileIDs) async {
     return await _enteDio.post(
       "/files/delete",
       data: {
@@ -207,14 +207,14 @@ class SyncService {
     );
   }
 
-  Future<BackupStatus> getBackupStatus({String pathID}) async {
+  Future<BackupStatus> getBackupStatus({String? pathID}) async {
     BackedUpFileIDs ids;
     if (pathID == null) {
       ids = await FilesDB.instance.getBackedUpIDs();
     } else {
       ids = await FilesDB.instance.getBackedUpForDeviceCollection(
         pathID,
-        Configuration.instance.getUserID(),
+        Configuration.instance.getUserID()!,
       );
     }
     final size = await _getFileSize(ids.uploadedIDs);

+ 4 - 6
lib/services/trash_sync_service.dart

@@ -1,5 +1,3 @@
-// @dart=2.9
-
 import 'dart:async';
 
 import 'package:dio/dio.dart';
@@ -25,7 +23,7 @@ class TrashSyncService {
   final _diffFetcher = TrashDiffFetcher();
   final _trashDB = TrashDB.instance;
   static const kLastTrashSyncTime = "last_trash_sync_time";
-  SharedPreferences _prefs;
+  late SharedPreferences _prefs;
 
   TrashSyncService._privateConstructor();
 
@@ -55,7 +53,7 @@ class TrashSyncService {
     if (diff.restoredFiles.isNotEmpty) {
       _logger.fine("discard ${diff.restoredFiles.length} restored items");
       final itemsDeleted = await _trashDB
-          .delete(diff.restoredFiles.map((e) => e.uploadedFileID).toList());
+          .delete(diff.restoredFiles.map((e) => e.uploadedFileID!).toList());
       isLocalTrashUpdated = isLocalTrashUpdated || itemsDeleted > 0;
     }
 
@@ -97,7 +95,7 @@ class TrashSyncService {
     }
   }
 
-  Future<void> _setSyncTime(int time) async {
+  Future<bool> _setSyncTime(int time) async {
     return _prefs.setInt(kLastTrashSyncTime, time);
   }
 
@@ -136,7 +134,7 @@ class TrashSyncService {
 
   Future<void> deleteFromTrash(List<File> files) async {
     final params = <String, dynamic>{};
-    final uniqueFileIds = files.map((e) => e.uploadedFileID).toSet().toList();
+    final uniqueFileIds = files.map((e) => e.uploadedFileID!).toSet().toList();
     final batchedFileIDs = uniqueFileIds.chunks(batchSize);
     for (final batch in batchedFileIDs) {
       params["fileIDs"] = [];

+ 6 - 8
lib/services/update_service.dart

@@ -1,5 +1,3 @@
-// @dart=2.9
-
 import 'dart:io';
 
 import 'package:flutter/foundation.dart';
@@ -19,10 +17,10 @@ class UpdateService {
   static const changeLogVersionKey = "update_change_log_key";
   static const currentChangeLogVersion = 3;
 
-  LatestVersionInfo _latestVersion;
+  LatestVersionInfo? _latestVersion;
   final _logger = Logger("UpdateService");
-  PackageInfo _packageInfo;
-  SharedPreferences _prefs;
+  late PackageInfo _packageInfo;
+  late SharedPreferences _prefs;
 
   Future<void> init() async {
     _packageInfo = await PackageInfo.fromPlatform();
@@ -51,7 +49,7 @@ class UpdateService {
     try {
       _latestVersion = await _getLatestVersionInfo();
       final currentVersionCode = int.parse(_packageInfo.buildNumber);
-      return currentVersionCode < _latestVersion.code;
+      return currentVersionCode < _latestVersion!.code;
     } catch (e) {
       _logger.severe(e);
       return false;
@@ -71,7 +69,7 @@ class UpdateService {
     }
   }
 
-  LatestVersionInfo getLatestVersionInfo() {
+  LatestVersionInfo? getLatestVersionInfo() {
     return _latestVersion;
   }
 
@@ -87,7 +85,7 @@ class UpdateService {
         (now - lastNotificationShownTime) > (3 * microSecondsInDay);
     if (shouldUpdate &&
         hasBeen3DaysSinceLastNotification &&
-        _latestVersion.shouldNotify) {
+        _latestVersion!.shouldNotify) {
       NotificationService.instance.showNotification(
         "Update available",
         "Click to install our best version yet",

+ 36 - 37
lib/services/user_service.dart

@@ -1,5 +1,3 @@
-// @dart=2.9
-
 import 'dart:async';
 import 'dart:typed_data';
 
@@ -43,9 +41,9 @@ class UserService {
   final _enteDio = Network.instance.enteDio;
   final _logger = Logger((UserService).toString());
   final _config = Configuration.instance;
-  SharedPreferences _preferences;
+  late SharedPreferences _preferences;
 
-  ValueNotifier<String> emailValueNotifier;
+  late ValueNotifier<String?> emailValueNotifier;
 
   UserService._privateConstructor();
 
@@ -53,7 +51,7 @@ class UserService {
 
   Future<void> init() async {
     emailValueNotifier =
-        ValueNotifier<String>(Configuration.instance.getEmail());
+        ValueNotifier<String?>(Configuration.instance.getEmail());
     _preferences = await SharedPreferences.getInstance();
     if (Configuration.instance.isLoggedIn()) {
       // add artificial delay in refreshing 2FA status
@@ -81,7 +79,7 @@ class UserService {
         data: {"email": email, "purpose": isChangeEmail ? "change" : ""},
       );
       await dialog.hide();
-      if (response != null && response.statusCode == 200) {
+      if (response.statusCode == 200) {
         unawaited(
           Navigator.of(context).push(
             MaterialPageRoute(
@@ -101,7 +99,7 @@ class UserService {
     } on DioError catch (e) {
       await dialog.hide();
       _logger.info(e);
-      if (e.response != null && e.response.statusCode == 403) {
+      if (e.response != null && e.response!.statusCode == 403) {
         unawaited(
           showErrorDialog(
             context,
@@ -119,7 +117,7 @@ class UserService {
     }
   }
 
-  Future<String> getPublicKey(String email) async {
+  Future<String?> getPublicKey(String email) async {
     try {
       final response = await _enteDio.get(
         "/users/public-key",
@@ -134,9 +132,9 @@ class UserService {
     }
   }
 
-  UserDetails getCachedUserDetails() {
+  UserDetails? getCachedUserDetails() {
     if (_preferences.containsKey(keyUserDetails)) {
-      return UserDetails.fromJson(_preferences.getString(keyUserDetails));
+      return UserDetails.fromJson(_preferences.getString(keyUserDetails)!);
     }
     return null;
   }
@@ -203,7 +201,7 @@ class UserService {
     await dialog.show();
     try {
       final response = await _enteDio.post("/users/logout");
-      if (response != null && response.statusCode == 200) {
+      if (response.statusCode == 200) {
         await Configuration.instance.logout();
         await dialog.hide();
         Navigator.of(context).popUntil((route) => route.isFirst);
@@ -217,14 +215,14 @@ class UserService {
     }
   }
 
-  Future<DeleteChallengeResponse> getDeleteChallenge(
+  Future<DeleteChallengeResponse?> getDeleteChallenge(
     BuildContext context,
   ) async {
     final dialog = createProgressDialog(context, "Please wait...");
     await dialog.show();
     try {
       final response = await _enteDio.get("/users/delete-challenge");
-      if (response != null && response.statusCode == 200) {
+      if (response.statusCode == 200) {
         // clear data
         await dialog.hide();
         return DeleteChallengeResponse(
@@ -253,7 +251,7 @@ class UserService {
           "challenge": challengeResponse,
         },
       );
-      if (response != null && response.statusCode == 200) {
+      if (response.statusCode == 200) {
         // clear data
         await Configuration.instance.logout();
       } else {
@@ -277,10 +275,11 @@ class UserService {
         },
       );
       await dialog.hide();
-      if (response != null && response.statusCode == 200) {
+      if (response.statusCode == 200) {
         Widget page;
         final String twoFASessionID = response.data["twoFactorSessionID"];
-        if (twoFASessionID != null && twoFASessionID.isNotEmpty) {
+        if (twoFASessionID.isNotEmpty) {
+          setTwoFactor(value: true);
           page = TwoFactorAuthenticationPage(twoFASessionID);
         } else {
           await _saveConfiguration(response);
@@ -305,7 +304,7 @@ class UserService {
     } on DioError catch (e) {
       _logger.info(e);
       await dialog.hide();
-      if (e.response != null && e.response.statusCode == 410) {
+      if (e.response != null && e.response!.statusCode == 410) {
         await showErrorDialog(
           context,
           "Oops",
@@ -328,7 +327,7 @@ class UserService {
 
   Future<void> setEmail(String email) async {
     await _config.setEmail(email);
-    emailValueNotifier.value = email ?? "";
+    emailValueNotifier.value = email;
   }
 
   Future<void> changeEmail(
@@ -347,7 +346,7 @@ class UserService {
         },
       );
       await dialog.hide();
-      if (response != null && response.statusCode == 200) {
+      if (response.statusCode == 200) {
         showShortToast(context, "Email changed to " + email);
         await setEmail(email);
         Navigator.of(context).popUntil((route) => route.isFirst);
@@ -357,7 +356,7 @@ class UserService {
       showErrorDialog(context, "Oops", "Verification failed, please try again");
     } on DioError catch (e) {
       await dialog.hide();
-      if (e.response != null && e.response.statusCode == 403) {
+      if (e.response != null && e.response!.statusCode == 403) {
         showErrorDialog(context, "Oops", "This email is already in use");
       } else {
         showErrorDialog(
@@ -417,8 +416,8 @@ class UserService {
       final setRecoveryKeyRequest = SetRecoveryKeyRequest(
         keyAttributes.masterKeyEncryptedWithRecoveryKey,
         keyAttributes.masterKeyDecryptionNonce,
-        keyAttributes.recoveryKeyEncryptedWithMasterKey,
-        keyAttributes.recoveryKeyDecryptionNonce,
+        keyAttributes.recoveryKeyEncryptedWithMasterKey!,
+        keyAttributes.recoveryKeyDecryptionNonce!,
       );
       await _enteDio.put(
         "/users/recovery-key",
@@ -447,7 +446,7 @@ class UserService {
         },
       );
       await dialog.hide();
-      if (response != null && response.statusCode == 200) {
+      if (response.statusCode == 200) {
         showShortToast(context, "Authentication successful!");
         await _saveConfiguration(response);
         Navigator.of(context).pushAndRemoveUntil(
@@ -462,7 +461,7 @@ class UserService {
     } on DioError catch (e) {
       await dialog.hide();
       _logger.severe(e);
-      if (e.response != null && e.response.statusCode == 404) {
+      if (e.response != null && e.response!.statusCode == 404) {
         showToast(context, "Session expired");
         Navigator.of(context).pushAndRemoveUntil(
           MaterialPageRoute(
@@ -500,7 +499,7 @@ class UserService {
           "sessionID": sessionID,
         },
       );
-      if (response != null && response.statusCode == 200) {
+      if (response.statusCode == 200) {
         Navigator.of(context).pushAndRemoveUntil(
           MaterialPageRoute(
             builder: (BuildContext context) {
@@ -516,7 +515,7 @@ class UserService {
       }
     } on DioError catch (e) {
       _logger.severe(e);
-      if (e.response != null && e.response.statusCode == 404) {
+      if (e.response != null && e.response!.statusCode == 404) {
         showToast(context, "Session expired");
         Navigator.of(context).pushAndRemoveUntil(
           MaterialPageRoute(
@@ -588,7 +587,7 @@ class UserService {
           "secret": secret,
         },
       );
-      if (response != null && response.statusCode == 200) {
+      if (response.statusCode == 200) {
         showShortToast(context, "Two-factor authentication successfully reset");
         await _saveConfiguration(response);
         Navigator.of(context).pushAndRemoveUntil(
@@ -602,7 +601,7 @@ class UserService {
       }
     } on DioError catch (e) {
       _logger.severe(e);
-      if (e.response != null && e.response.statusCode == 404) {
+      if (e.response != null && e.response!.statusCode == 404) {
         showToast(context, "Session expired");
         Navigator.of(context).pushAndRemoveUntil(
           MaterialPageRoute(
@@ -677,9 +676,9 @@ class UserService {
         data: {
           "code": code,
           "encryptedTwoFactorSecret":
-              Sodium.bin2base64(encryptionResult.encryptedData),
+              Sodium.bin2base64(encryptionResult.encryptedData as Uint8List),
           "twoFactorSecretDecryptionNonce":
-              Sodium.bin2base64(encryptionResult.nonce),
+              Sodium.bin2base64(encryptionResult.nonce as Uint8List),
         },
       );
       await dialog.hide();
@@ -690,7 +689,7 @@ class UserService {
       await dialog.hide();
       _logger.severe(e, s);
       if (e is DioError) {
-        if (e.response != null && e.response.statusCode == 401) {
+        if (e.response != null && e.response!.statusCode == 401) {
           showErrorDialog(
             context,
             "Incorrect code",
@@ -747,8 +746,8 @@ class UserService {
   }
 
   Future<Uint8List> getOrCreateRecoveryKey(BuildContext context) async {
-    final encryptedRecoveryKey =
-        _config.getKeyAttributes().recoveryKeyEncryptedWithMasterKey;
+    final String? encryptedRecoveryKey =
+        _config.getKeyAttributes()!.recoveryKeyEncryptedWithMasterKey;
     if (encryptedRecoveryKey == null || encryptedRecoveryKey.isEmpty) {
       final dialog = createProgressDialog(context, "Please wait...");
       await dialog.show();
@@ -766,10 +765,10 @@ class UserService {
     return recoveryKey;
   }
 
-  Future<String> getPaymentToken() async {
+  Future<String?> getPaymentToken() async {
     try {
       final response = await _enteDio.get("/users/payment-token");
-      if (response != null && response.statusCode == 200) {
+      if (response.statusCode == 200) {
         return response.data["paymentToken"];
       } else {
         throw Exception("non 200 ok response");
@@ -783,7 +782,7 @@ class UserService {
   Future<String> getFamiliesToken() async {
     try {
       final response = await _enteDio.get("/users/families-token");
-      if (response != null && response.statusCode == 200) {
+      if (response.statusCode == 200) {
         return response.data["familiesToken"];
       } else {
         throw Exception("non 200 ok response");
@@ -818,6 +817,6 @@ class UserService {
   }
 
   bool hasEnabledTwoFactor() {
-    return _preferences.getBool(keyHasEnabledTwoFactor);
+    return _preferences.getBool(keyHasEnabledTwoFactor) ?? false;
   }
 }

+ 1 - 1
lib/ui/account/change_email_dialog.dart

@@ -71,7 +71,7 @@ class _ChangeEmailDialogState extends State<ChangeEmailDialog> {
               );
               return;
             }
-            UserService.instance.sendOtt(context, _email, isChangeEmail: true);
+            UserService.instance.sendOtt(context, _email!, isChangeEmail: true);
           },
         ),
       ],

+ 5 - 7
lib/ui/account/delete_account_page.dart

@@ -1,5 +1,3 @@
-// @dart=2.9
-
 import 'dart:convert';
 
 import 'package:flutter/material.dart';
@@ -17,7 +15,7 @@ import 'package:photos/utils/email_util.dart';
 
 class DeleteAccountPage extends StatelessWidget {
   const DeleteAccountPage({
-    Key key,
+    Key? key,
   }) : super(key: key);
 
   @override
@@ -53,7 +51,7 @@ class DeleteAccountPage extends StatelessWidget {
                   "We'll be sorry to see you go. Are you facing some issue?",
                   style: Theme.of(context)
                       .textTheme
-                      .subtitle1
+                      .subtitle1!
                       .copyWith(color: colorScheme.textMuted),
                 ),
               ),
@@ -75,7 +73,7 @@ class DeleteAccountPage extends StatelessWidget {
                   ],
                   style: Theme.of(context)
                       .textTheme
-                      .subtitle1
+                      .subtitle1!
                       .copyWith(color: colorScheme.textMuted),
                 ),
               ),
@@ -145,9 +143,9 @@ class DeleteAccountPage extends StatelessWidget {
           final decryptChallenge = CryptoUtil.openSealSync(
             Sodium.base642bin(response.encryptedChallenge),
             Sodium.base642bin(
-              Configuration.instance.getKeyAttributes().publicKey,
+              Configuration.instance.getKeyAttributes()!.publicKey,
             ),
-            Configuration.instance.getSecretKey(),
+            Configuration.instance.getSecretKey()!,
           );
           final challengeResponseStr = utf8.decode(decryptChallenge);
           await UserService.instance

+ 15 - 17
lib/ui/account/email_entry_page.dart

@@ -1,5 +1,3 @@
-// @dart=2.9
-
 import 'package:email_validator/email_validator.dart';
 import 'package:flutter/gestures.dart';
 import 'package:flutter/material.dart';
@@ -13,7 +11,7 @@ import 'package:photos/ui/common/web_page.dart';
 import 'package:step_progress_indicator/step_progress_indicator.dart';
 
 class EmailEntryPage extends StatefulWidget {
-  const EmailEntryPage({Key key}) : super(key: key);
+  const EmailEntryPage({Key? key}) : super(key: key);
 
   @override
   State<EmailEntryPage> createState() => _EmailEntryPageState();
@@ -28,8 +26,8 @@ class _EmailEntryPageState extends State<EmailEntryPage> {
   final _passwordController2 = TextEditingController();
   final Color _validFieldValueColor = const Color.fromRGBO(45, 194, 98, 0.2);
 
-  String _email;
-  String _password;
+  String? _email;
+  String? _password;
   String _cnfPassword = '';
   double _passwordStrength = 0.0;
   bool _emailIsValid = false;
@@ -65,7 +63,7 @@ class _EmailEntryPageState extends State<EmailEntryPage> {
   Widget build(BuildContext context) {
     final isKeypadOpen = MediaQuery.of(context).viewInsets.bottom > 100;
 
-    FloatingActionButtonLocation fabLocation() {
+    FloatingActionButtonLocation? fabLocation() {
       if (isKeypadOpen) {
         return null;
       } else {
@@ -104,9 +102,9 @@ class _EmailEntryPageState extends State<EmailEntryPage> {
         buttonText: 'Create account',
         onPressedFunction: () {
           _config.setVolatilePassword(_passwordController1.text);
-          UserService.instance.setEmail(_email);
+          UserService.instance.setEmail(_email!);
           UserService.instance
-              .sendOtt(context, _email, isCreateAccountScreen: true);
+              .sendOtt(context, _email!, isCreateAccountScreen: true);
           FocusScope.of(context).unfocus();
         },
       ),
@@ -162,7 +160,7 @@ class _EmailEntryPageState extends State<EmailEntryPage> {
                               size: 20,
                               color: Theme.of(context)
                                   .inputDecorationTheme
-                                  .focusedBorder
+                                  .focusedBorder!
                                   .borderSide
                                   .color,
                             )
@@ -170,9 +168,9 @@ class _EmailEntryPageState extends State<EmailEntryPage> {
                     ),
                     onChanged: (value) {
                       _email = value.trim();
-                      if (_emailIsValid != EmailValidator.validate(_email)) {
+                      if (_emailIsValid != EmailValidator.validate(_email!)) {
                         setState(() {
-                          _emailIsValid = EmailValidator.validate(_email);
+                          _emailIsValid = EmailValidator.validate(_email!);
                         });
                       }
                     },
@@ -220,7 +218,7 @@ class _EmailEntryPageState extends State<EmailEntryPage> {
                                   Icons.check,
                                   color: Theme.of(context)
                                       .inputDecorationTheme
-                                      .focusedBorder
+                                      .focusedBorder!
                                       .borderSide
                                       .color,
                                 )
@@ -287,7 +285,7 @@ class _EmailEntryPageState extends State<EmailEntryPage> {
                                   Icons.check,
                                   color: Theme.of(context)
                                       .inputDecorationTheme
-                                      .focusedBorder
+                                      .focusedBorder!
                                       .borderSide
                                       .color,
                                 )
@@ -363,7 +361,7 @@ class _EmailEntryPageState extends State<EmailEntryPage> {
             side: CheckboxTheme.of(context).side,
             onChanged: (value) {
               setState(() {
-                _hasAgreedToTOS = value;
+                _hasAgreedToTOS = value!;
               });
             },
           ),
@@ -416,7 +414,7 @@ class _EmailEntryPageState extends State<EmailEntryPage> {
                 ],
                 style: Theme.of(context)
                     .textTheme
-                    .subtitle1
+                    .subtitle1!
                     .copyWith(fontSize: 12),
               ),
               textAlign: TextAlign.left,
@@ -442,7 +440,7 @@ class _EmailEntryPageState extends State<EmailEntryPage> {
             side: CheckboxTheme.of(context).side,
             onChanged: (value) {
               setState(() {
-                _hasAgreedToE2E = value;
+                _hasAgreedToE2E = value!;
               });
             },
           ),
@@ -477,7 +475,7 @@ class _EmailEntryPageState extends State<EmailEntryPage> {
                 ],
                 style: Theme.of(context)
                     .textTheme
-                    .subtitle1
+                    .subtitle1!
                     .copyWith(fontSize: 12),
               ),
               textAlign: TextAlign.left,

+ 10 - 10
lib/ui/account/login_page.dart

@@ -1,4 +1,4 @@
-// @dart=2.9
+
 
 import 'package:email_validator/email_validator.dart';
 import 'package:flutter/gestures.dart';
@@ -9,7 +9,7 @@ import 'package:photos/ui/common/dynamic_fab.dart';
 import 'package:photos/ui/common/web_page.dart';
 
 class LoginPage extends StatefulWidget {
-  const LoginPage({Key key}) : super(key: key);
+  const LoginPage({Key? key}) : super(key: key);
 
   @override
   State<LoginPage> createState() => _LoginPageState();
@@ -18,8 +18,8 @@ class LoginPage extends StatefulWidget {
 class _LoginPageState extends State<LoginPage> {
   final _config = Configuration.instance;
   bool _emailIsValid = false;
-  String _email;
-  Color _emailInputFieldColor;
+  String? _email;
+  Color? _emailInputFieldColor;
 
   @override
   void initState() {
@@ -31,7 +31,7 @@ class _LoginPageState extends State<LoginPage> {
   Widget build(BuildContext context) {
     final isKeypadOpen = MediaQuery.of(context).viewInsets.bottom > 100;
 
-    FloatingActionButtonLocation fabLocation() {
+    FloatingActionButtonLocation? fabLocation() {
       if (isKeypadOpen) {
         return null;
       } else {
@@ -57,9 +57,9 @@ class _LoginPageState extends State<LoginPage> {
         isFormValid: _emailIsValid,
         buttonText: 'Log in',
         onPressedFunction: () {
-          UserService.instance.setEmail(_email);
+          UserService.instance.setEmail(_email!);
           UserService.instance
-              .sendOtt(context, _email, isCreateAccountScreen: false);
+              .sendOtt(context, _email!, isCreateAccountScreen: false);
           FocusScope.of(context).unfocus();
         },
       ),
@@ -105,7 +105,7 @@ class _LoginPageState extends State<LoginPage> {
                               size: 20,
                               color: Theme.of(context)
                                   .inputDecorationTheme
-                                  .focusedBorder
+                                  .focusedBorder!
                                   .borderSide
                                   .color,
                             )
@@ -114,7 +114,7 @@ class _LoginPageState extends State<LoginPage> {
                     onChanged: (value) {
                       setState(() {
                         _email = value.trim();
-                        _emailIsValid = EmailValidator.validate(_email);
+                        _emailIsValid = EmailValidator.validate(_email!);
                         if (_emailIsValid) {
                           _emailInputFieldColor =
                               const Color.fromRGBO(45, 194, 98, 0.2);
@@ -145,7 +145,7 @@ class _LoginPageState extends State<LoginPage> {
                           text: TextSpan(
                             style: Theme.of(context)
                                 .textTheme
-                                .subtitle1
+                                .subtitle1!
                                 .copyWith(fontSize: 12),
                             children: [
                               const TextSpan(

+ 6 - 9
lib/ui/account/ott_verification_page.dart

@@ -1,5 +1,3 @@
-// @dart=2.9
-
 import 'package:flutter/material.dart';
 import 'package:photos/ente_theme_data.dart';
 import 'package:photos/services/user_service.dart';
@@ -15,7 +13,7 @@ class OTTVerificationPage extends StatefulWidget {
     this.email, {
     this.isChangeEmail = false,
     this.isCreateAccountScreen = false,
-    Key key,
+    Key? key,
   }) : super(key: key);
 
   @override
@@ -29,7 +27,7 @@ class _OTTVerificationPageState extends State<OTTVerificationPage> {
   Widget build(BuildContext context) {
     final isKeypadOpen = MediaQuery.of(context).viewInsets.bottom > 100;
 
-    FloatingActionButtonLocation fabLocation() {
+    FloatingActionButtonLocation? fabLocation() {
       if (isKeypadOpen) {
         return null;
       } else {
@@ -65,8 +63,7 @@ class _OTTVerificationPageState extends State<OTTVerificationPage> {
       body: _getBody(),
       floatingActionButton: DynamicFAB(
         isKeypadOpen: isKeypadOpen,
-        isFormValid: !(_verificationCodeController.text == null ||
-            _verificationCodeController.text.isEmpty),
+        isFormValid: _verificationCodeController.text.isNotEmpty,
         buttonText: 'Verify',
         onPressedFunction: () {
           if (widget.isChangeEmail) {
@@ -114,7 +111,7 @@ class _OTTVerificationPageState extends State<OTTVerificationPage> {
                             text: TextSpan(
                               style: Theme.of(context)
                                   .textTheme
-                                  .subtitle1
+                                  .subtitle1!
                                   .copyWith(fontSize: 14),
                               children: [
                                 const TextSpan(text: "We've sent a mail to "),
@@ -134,7 +131,7 @@ class _OTTVerificationPageState extends State<OTTVerificationPage> {
                           'Please check your inbox (and spam) to complete verification',
                           style: Theme.of(context)
                               .textTheme
-                              .subtitle1
+                              .subtitle1!
                               .copyWith(fontSize: 14),
                         ),
                       ],
@@ -187,7 +184,7 @@ class _OTTVerificationPageState extends State<OTTVerificationPage> {
                     },
                     child: Text(
                       "Resend email",
-                      style: Theme.of(context).textTheme.subtitle1.copyWith(
+                      style: Theme.of(context).textTheme.subtitle1!.copyWith(
                             fontSize: 14,
                             decoration: TextDecoration.underline,
                           ),

+ 15 - 17
lib/ui/account/password_entry_page.dart

@@ -1,5 +1,3 @@
-// @dart=2.9
-
 import 'package:flutter/material.dart';
 import 'package:flutter/services.dart';
 import 'package:logging/logging.dart';
@@ -26,7 +24,7 @@ enum PasswordEntryMode {
 class PasswordEntryPage extends StatefulWidget {
   final PasswordEntryMode mode;
 
-  const PasswordEntryPage({this.mode = PasswordEntryMode.set, Key key})
+  const PasswordEntryPage({this.mode = PasswordEntryMode.set, Key? key})
       : super(key: key);
 
   @override
@@ -41,7 +39,7 @@ class _PasswordEntryPageState extends State<PasswordEntryPage> {
   final _passwordController1 = TextEditingController(),
       _passwordController2 = TextEditingController();
   final Color _validFieldValueColor = const Color.fromRGBO(45, 194, 98, 0.2);
-  String _volatilePassword;
+  String? _volatilePassword;
   String _passwordInInputBox = '';
   String _passwordInInputConfirmationBox = '';
   double _passwordStrength = 0.0;
@@ -62,7 +60,7 @@ class _PasswordEntryPageState extends State<PasswordEntryPage> {
     if (_volatilePassword != null) {
       Future.delayed(
         Duration.zero,
-        () => _showRecoveryCodeDialog(_volatilePassword),
+        () => _showRecoveryCodeDialog(_volatilePassword!),
       );
     }
     _password1FocusNode.addListener(() {
@@ -81,7 +79,7 @@ class _PasswordEntryPageState extends State<PasswordEntryPage> {
   Widget build(BuildContext context) {
     final isKeypadOpen = MediaQuery.of(context).viewInsets.bottom > 100;
 
-    FloatingActionButtonLocation fabLocation() {
+    FloatingActionButtonLocation? fabLocation() {
       if (isKeypadOpen) {
         return null;
       } else {
@@ -167,7 +165,7 @@ class _PasswordEntryPageState extends State<PasswordEntryPage> {
                     textAlign: TextAlign.start,
                     style: Theme.of(context)
                         .textTheme
-                        .subtitle1
+                        .subtitle1!
                         .copyWith(fontSize: 14),
                   ),
                 ),
@@ -178,7 +176,7 @@ class _PasswordEntryPageState extends State<PasswordEntryPage> {
                     text: TextSpan(
                       style: Theme.of(context)
                           .textTheme
-                          .subtitle1
+                          .subtitle1!
                           .copyWith(fontSize: 14),
                       children: [
                         const TextSpan(
@@ -187,10 +185,11 @@ class _PasswordEntryPageState extends State<PasswordEntryPage> {
                         ),
                         TextSpan(
                           text: "we cannot decrypt your data",
-                          style: Theme.of(context).textTheme.subtitle1.copyWith(
-                                fontSize: 14,
-                                decoration: TextDecoration.underline,
-                              ),
+                          style:
+                              Theme.of(context).textTheme.subtitle1!.copyWith(
+                                    fontSize: 14,
+                                    decoration: TextDecoration.underline,
+                                  ),
                         ),
                       ],
                     ),
@@ -245,7 +244,7 @@ class _PasswordEntryPageState extends State<PasswordEntryPage> {
                                   Icons.check,
                                   color: Theme.of(context)
                                       .inputDecorationTheme
-                                      .focusedBorder
+                                      .focusedBorder!
                                       .borderSide
                                       .color,
                                 )
@@ -307,7 +306,7 @@ class _PasswordEntryPageState extends State<PasswordEntryPage> {
                                   Icons.check,
                                   color: Theme.of(context)
                                       .inputDecorationTheme
-                                      .focusedBorder
+                                      .focusedBorder!
                                       .borderSide
                                       .color,
                                 )
@@ -321,8 +320,7 @@ class _PasswordEntryPageState extends State<PasswordEntryPage> {
                     onChanged: (cnfPassword) {
                       setState(() {
                         _passwordInInputConfirmationBox = cnfPassword;
-                        if (_passwordInInputBox != null ||
-                            _passwordInInputBox != '') {
+                        if (_passwordInInputBox != '') {
                           _passwordsMatch = _passwordInInputBox ==
                               _passwordInInputConfirmationBox;
                         }
@@ -364,7 +362,7 @@ class _PasswordEntryPageState extends State<PasswordEntryPage> {
                     child: RichText(
                       text: TextSpan(
                         text: "How it works",
-                        style: Theme.of(context).textTheme.subtitle1.copyWith(
+                        style: Theme.of(context).textTheme.subtitle1!.copyWith(
                               fontSize: 14,
                               decoration: TextDecoration.underline,
                             ),

+ 6 - 15
lib/ui/account/password_reentry_page.dart

@@ -1,5 +1,3 @@
-// @dart=2.9
-
 import 'dart:async';
 
 import 'package:flutter/material.dart';
@@ -8,7 +6,6 @@ import 'package:photos/core/configuration.dart';
 import 'package:photos/core/errors.dart';
 import 'package:photos/core/event_bus.dart';
 import 'package:photos/events/subscription_purchased_event.dart';
-import 'package:photos/models/key_attributes.dart';
 import 'package:photos/ui/account/recovery_page.dart';
 import 'package:photos/ui/common/dialogs.dart';
 import 'package:photos/ui/common/dynamic_fab.dart';
@@ -17,7 +14,7 @@ import 'package:photos/utils/dialog_util.dart';
 import 'package:photos/utils/email_util.dart';
 
 class PasswordReentryPage extends StatefulWidget {
-  const PasswordReentryPage({Key key}) : super(key: key);
+  const PasswordReentryPage({Key? key}) : super(key: key);
 
   @override
   State<PasswordReentryPage> createState() => _PasswordReentryPageState();
@@ -27,7 +24,7 @@ class _PasswordReentryPageState extends State<PasswordReentryPage> {
   final _logger = Logger((_PasswordReentryPageState).toString());
   final _passwordController = TextEditingController();
   final FocusNode _passwordFocusNode = FocusNode();
-  String email;
+  String? email;
   bool _passwordInFocus = false;
   bool _passwordVisible = false;
 
@@ -46,7 +43,7 @@ class _PasswordReentryPageState extends State<PasswordReentryPage> {
   Widget build(BuildContext context) {
     final isKeypadOpen = MediaQuery.of(context).viewInsets.bottom > 100;
 
-    FloatingActionButtonLocation fabLocation() {
+    FloatingActionButtonLocation? fabLocation() {
       if (isKeypadOpen) {
         return null;
       } else {
@@ -78,7 +75,7 @@ class _PasswordReentryPageState extends State<PasswordReentryPage> {
           try {
             await Configuration.instance.decryptAndSaveSecrets(
               _passwordController.text,
-              Configuration.instance.getKeyAttributes(),
+              Configuration.instance.getKeyAttributes()!,
             );
           } on KeyDerivationError catch (e, s) {
             _logger.severe("Password verification failed", e, s);
@@ -245,7 +242,7 @@ class _PasswordReentryPageState extends State<PasswordReentryPage> {
                           child: Text(
                             "Forgot password",
                             style:
-                                Theme.of(context).textTheme.subtitle1.copyWith(
+                                Theme.of(context).textTheme.subtitle1!.copyWith(
                                       fontSize: 14,
                                       decoration: TextDecoration.underline,
                                     ),
@@ -267,7 +264,7 @@ class _PasswordReentryPageState extends State<PasswordReentryPage> {
                           child: Text(
                             "Change email",
                             style:
-                                Theme.of(context).textTheme.subtitle1.copyWith(
+                                Theme.of(context).textTheme.subtitle1!.copyWith(
                                       fontSize: 14,
                                       decoration: TextDecoration.underline,
                                     ),
@@ -284,10 +281,4 @@ class _PasswordReentryPageState extends State<PasswordReentryPage> {
       ],
     );
   }
-
-  void validatePreVerificationState(KeyAttributes keyAttributes) {
-    if (keyAttributes == null) {
-      throw Exception("Key Attributes can not be null");
-    }
-  }
 }

+ 13 - 13
lib/ui/account/recovery_key_page.dart

@@ -1,4 +1,4 @@
-// @dart=2.9
+
 
 import 'dart:io' as io;
 
@@ -15,20 +15,20 @@ import 'package:share_plus/share_plus.dart';
 import 'package:step_progress_indicator/step_progress_indicator.dart';
 
 class RecoveryKeyPage extends StatefulWidget {
-  final bool showAppBar;
+  final bool? showAppBar;
   final String recoveryKey;
   final String doneText;
-  final Function() onDone;
-  final bool isDismissible;
-  final String title;
-  final String text;
-  final String subText;
+  final Function()? onDone;
+  final bool? isDismissible;
+  final String? title;
+  final String? text;
+  final String? subText;
   final bool showProgressBar;
 
   const RecoveryKeyPage(
     this.recoveryKey,
     this.doneText, {
-    Key key,
+    Key? key,
     this.showAppBar,
     this.onDone,
     this.isDismissible,
@@ -56,7 +56,7 @@ class _RecoveryKeyPageState extends State<RecoveryKeyPage> {
         'recovery code should have $mnemonicKeyWordCount words',
       );
     }
-    final double topPadding = widget.showAppBar
+    final double topPadding = widget.showAppBar!
         ? 40
         : widget.showProgressBar
             ? 32
@@ -79,7 +79,7 @@ class _RecoveryKeyPageState extends State<RecoveryKeyPage> {
                 ),
               ),
             )
-          : widget.showAppBar
+          : widget.showAppBar!
               ? AppBar(
                   elevation: 0,
                   title: Text(widget.title ?? "Recovery key"),
@@ -100,14 +100,14 @@ class _RecoveryKeyPageState extends State<RecoveryKeyPage> {
                     mainAxisSize: MainAxisSize.max,
                     crossAxisAlignment: CrossAxisAlignment.start,
                     children: [
-                      widget.showAppBar
+                      widget.showAppBar!
                           ? const SizedBox.shrink()
                           : Text(
                               widget.title ?? "Recovery key",
                               style: Theme.of(context).textTheme.headline4,
                             ),
                       Padding(
-                        padding: EdgeInsets.all(widget.showAppBar ? 0 : 12),
+                        padding: EdgeInsets.all(widget.showAppBar! ? 0 : 12),
                       ),
                       Text(
                         widget.text ??
@@ -263,6 +263,6 @@ class _RecoveryKeyPageState extends State<RecoveryKeyPage> {
     if (_recoveryKeyFile.existsSync()) {
       await _recoveryKeyFile.delete();
     }
-    widget.onDone();
+    widget.onDone!();
   }
 }

+ 4 - 4
lib/ui/account/recovery_page.dart

@@ -1,4 +1,4 @@
-// @dart=2.9
+
 
 import 'dart:ui';
 
@@ -10,7 +10,7 @@ import 'package:photos/utils/dialog_util.dart';
 import 'package:photos/utils/toast_util.dart';
 
 class RecoveryPage extends StatefulWidget {
-  const RecoveryPage({Key key}) : super(key: key);
+  const RecoveryPage({Key? key}) : super(key: key);
 
   @override
   State<RecoveryPage> createState() => _RecoveryPageState();
@@ -22,7 +22,7 @@ class _RecoveryPageState extends State<RecoveryPage> {
   @override
   Widget build(BuildContext context) {
     final isKeypadOpen = MediaQuery.of(context).viewInsets.bottom > 100;
-    FloatingActionButtonLocation fabLocation() {
+    FloatingActionButtonLocation? fabLocation() {
       if (isKeypadOpen) {
         return null;
       } else {
@@ -140,7 +140,7 @@ class _RecoveryPageState extends State<RecoveryPage> {
                           child: Text(
                             "No recovery key?",
                             style:
-                                Theme.of(context).textTheme.subtitle1.copyWith(
+                                Theme.of(context).textTheme.subtitle1!.copyWith(
                                       fontSize: 14,
                                       decoration: TextDecoration.underline,
                                     ),

+ 0 - 1
lib/ui/actions/collection/collection_file_actions.dart

@@ -1,5 +1,4 @@
 import 'package:flutter/cupertino.dart';
-import 'package:flutter/widgets.dart';
 import 'package:photos/db/files_db.dart';
 import 'package:photos/models/collection.dart';
 import 'package:photos/models/file.dart';

+ 1 - 2
lib/ui/actions/collection/collection_sharing_actions.dart

@@ -94,8 +94,7 @@ class CollectionActions {
         subType: subTypeSharedFilesCollection,
       );
       final collection = await collectionsService.createAndCacheCollection(
-        null,
-        createRequest: req,
+        req,
       );
       logger.finest("adding files to share to new album");
       await collectionsService.addToCollection(collection.id, files);

+ 18 - 20
lib/ui/backup_folder_selection_page.dart

@@ -1,5 +1,3 @@
-// @dart=2.9
-
 import 'dart:io';
 import 'dart:ui';
 
@@ -24,9 +22,9 @@ class BackupFolderSelectionPage extends StatefulWidget {
   final String buttonText;
 
   const BackupFolderSelectionPage({
-    @required this.buttonText,
+    required this.buttonText,
     this.isOnboarding = false,
-    Key key,
+    Key? key,
   }) : super(key: key);
 
   @override
@@ -38,8 +36,8 @@ class _BackupFolderSelectionPageState extends State<BackupFolderSelectionPage> {
   final Logger _logger = Logger((_BackupFolderSelectionPageState).toString());
   final Set<String> _allDevicePathIDs = <String>{};
   final Set<String> _selectedDevicePathIDs = <String>{};
-  List<DeviceCollection> _deviceCollections;
-  Map<String, int> _pathIDToItemCount;
+  List<DeviceCollection>? _deviceCollections;
+  Map<String, int>? _pathIDToItemCount;
 
   @override
   void initState() {
@@ -50,10 +48,10 @@ class _BackupFolderSelectionPageState extends State<BackupFolderSelectionPage> {
           await FilesDB.instance.getDevicePathIDToImportedFileCount();
       setState(() {
         _deviceCollections = files;
-        _deviceCollections.sort((first, second) {
+        _deviceCollections!.sort((first, second) {
           return first.name.toLowerCase().compareTo(second.name.toLowerCase());
         });
-        for (final file in _deviceCollections) {
+        for (final file in _deviceCollections!) {
           _allDevicePathIDs.add(file.id);
           if (file.shouldBackup) {
             _selectedDevicePathIDs.add(file.id);
@@ -103,7 +101,7 @@ class _BackupFolderSelectionPageState extends State<BackupFolderSelectionPage> {
             padding: const EdgeInsets.only(left: 24, right: 48),
             child: Text(
               "Selected folders will be encrypted and backed up",
-              style: Theme.of(context).textTheme.caption.copyWith(height: 1.3),
+              style: Theme.of(context).textTheme.caption!.copyWith(height: 1.3),
             ),
           ),
           const Padding(
@@ -139,7 +137,7 @@ class _BackupFolderSelectionPageState extends State<BackupFolderSelectionPage> {
                     } else {
                       _selectedDevicePathIDs.addAll(_allDevicePathIDs);
                     }
-                    _deviceCollections.sort((first, second) {
+                    _deviceCollections!.sort((first, second) {
                       return first.name
                           .toLowerCase()
                           .compareTo(second.name.toLowerCase());
@@ -191,7 +189,7 @@ class _BackupFolderSelectionPageState extends State<BackupFolderSelectionPage> {
                         },
                         child: Text(
                           "Skip",
-                          style: Theme.of(context).textTheme.caption.copyWith(
+                          style: Theme.of(context).textTheme.caption!.copyWith(
                                 decoration: TextDecoration.underline,
                               ),
                         ),
@@ -247,11 +245,11 @@ class _BackupFolderSelectionPageState extends State<BackupFolderSelectionPage> {
           padding: const EdgeInsets.only(right: 4),
           child: ImplicitlyAnimatedReorderableList<DeviceCollection>(
             controller: scrollController,
-            items: _deviceCollections,
+            items: _deviceCollections!,
             areItemsTheSame: (oldItem, newItem) => oldItem.id == newItem.id,
             onReorderFinished: (item, from, to, newItems) {
               setState(() {
-                _deviceCollections
+                _deviceCollections!
                   ..clear()
                   ..addAll(newItems);
               });
@@ -261,7 +259,7 @@ class _BackupFolderSelectionPageState extends State<BackupFolderSelectionPage> {
                 key: ValueKey(file),
                 builder: (context, dragAnimation, inDrag) {
                   final t = dragAnimation.value;
-                  final elevation = lerpDouble(0, 8, t);
+                  final elevation = lerpDouble(0, 8, t)!;
                   final themeColor = Theme.of(context).colorScheme.onSurface;
                   final color =
                       Color.lerp(themeColor, themeColor.withOpacity(0.8), t);
@@ -288,7 +286,7 @@ class _BackupFolderSelectionPageState extends State<BackupFolderSelectionPage> {
   Widget _getFileItem(DeviceCollection deviceCollection) {
     final isSelected = _selectedDevicePathIDs.contains(deviceCollection.id);
     final importedCount = _pathIDToItemCount != null
-        ? _pathIDToItemCount[deviceCollection.id] ?? 0
+        ? _pathIDToItemCount![deviceCollection.id] ?? 0
         : -1;
     return Padding(
       padding: const EdgeInsets.only(bottom: 1, right: 1),
@@ -326,7 +324,7 @@ class _BackupFolderSelectionPageState extends State<BackupFolderSelectionPage> {
                     activeColor: Colors.white,
                     value: isSelected,
                     onChanged: (value) {
-                      if (value) {
+                      if (value!) {
                         _selectedDevicePathIDs.add(deviceCollection.id);
                       } else {
                         _selectedDevicePathIDs.remove(deviceCollection.id);
@@ -360,9 +358,9 @@ class _BackupFolderSelectionPageState extends State<BackupFolderSelectionPage> {
                       const Padding(padding: EdgeInsets.only(top: 2)),
                       Text(
                         (kDebugMode ? 'inApp: $importedCount : device ' : '') +
-                            (deviceCollection.count ?? 0).toString() +
+                            (deviceCollection.count).toString() +
                             " item" +
-                            ((deviceCollection.count ?? 0) == 1 ? "" : "s"),
+                            ((deviceCollection.count) == 1 ? "" : "s"),
                         textAlign: TextAlign.left,
                         style: TextStyle(
                           fontSize: 12,
@@ -375,7 +373,7 @@ class _BackupFolderSelectionPageState extends State<BackupFolderSelectionPage> {
                   ),
                 ],
               ),
-              _getThumbnail(deviceCollection.thumbnail, isSelected),
+              _getThumbnail(deviceCollection.thumbnail!, isSelected),
             ],
           ),
           onTap: () {
@@ -393,7 +391,7 @@ class _BackupFolderSelectionPageState extends State<BackupFolderSelectionPage> {
   }
 
   void _sortFiles() {
-    _deviceCollections.sort((first, second) {
+    _deviceCollections!.sort((first, second) {
       if (_selectedDevicePathIDs.contains(first.id) &&
           _selectedDevicePathIDs.contains(second.id)) {
         return first.name.toLowerCase().compareTo(second.name.toLowerCase());

+ 1 - 1
lib/ui/collections/archived_collections_button_widget.dart

@@ -45,7 +45,7 @@ class ArchivedCollectionsButtonWidget extends StatelessWidget {
                   FutureBuilder<int>(
                     future: FilesDB.instance.fileCountWithVisibility(
                       visibilityArchive,
-                      Configuration.instance.getUserID(),
+                      Configuration.instance.getUserID()!,
                     ),
                     builder: (context, snapshot) {
                       if (snapshot.hasData && snapshot.data! > 0) {

+ 7 - 7
lib/ui/collections/device_folders_grid_view_widget.dart

@@ -1,4 +1,4 @@
-// @dart=2.9
+
 
 import 'dart:async';
 
@@ -17,7 +17,7 @@ import 'package:photos/ui/viewer/gallery/empty_state.dart';
 
 class DeviceFoldersGridViewWidget extends StatefulWidget {
   const DeviceFoldersGridViewWidget({
-    Key key,
+    Key? key,
   }) : super(key: key);
 
   @override
@@ -27,8 +27,8 @@ class DeviceFoldersGridViewWidget extends StatefulWidget {
 
 class _DeviceFoldersGridViewWidgetState
     extends State<DeviceFoldersGridViewWidget> {
-  StreamSubscription<BackupFoldersUpdatedEvent> _backupFoldersUpdatedEvent;
-  StreamSubscription<LocalPhotosUpdatedEvent> _localFilesSubscription;
+  StreamSubscription<BackupFoldersUpdatedEvent>? _backupFoldersUpdatedEvent;
+  StreamSubscription<LocalPhotosUpdatedEvent>? _localFilesSubscription;
   String _loadReason = "init";
 
   @override
@@ -65,7 +65,7 @@ class _DeviceFoldersGridViewWidgetState
                 .getDeviceCollections(includeCoverThumbnail: true),
             builder: (context, snapshot) {
               if (snapshot.hasData) {
-                return snapshot.data.isEmpty
+                return snapshot.data!.isEmpty
                     ? Padding(
                         padding: const EdgeInsets.all(22),
                         child: (isMigrationDone
@@ -81,10 +81,10 @@ class _DeviceFoldersGridViewWidgetState
                         physics: const ScrollPhysics(),
                         // to disable GridView's scrolling
                         itemBuilder: (context, index) {
-                          final deviceCollection = snapshot.data[index];
+                          final deviceCollection = snapshot.data![index];
                           return DeviceFolderIcon(deviceCollection);
                         },
-                        itemCount: snapshot.data.length,
+                        itemCount: snapshot.data!.length,
                       );
               } else if (snapshot.hasError) {
                 logger.severe("failed to load device gallery", snapshot.error);

+ 3 - 3
lib/ui/collections/hidden_collections_button_widget.dart

@@ -1,4 +1,4 @@
-// @dart=2.9
+
 
 import 'package:flutter/material.dart';
 import 'package:photos/services/local_authentication_service.dart';
@@ -10,7 +10,7 @@ class HiddenCollectionsButtonWidget extends StatelessWidget {
 
   const HiddenCollectionsButtonWidget(
     this.textStyle, {
-    Key key,
+    Key? key,
   }) : super(key: key);
 
   @override
@@ -24,7 +24,7 @@ class HiddenCollectionsButtonWidget extends StatelessWidget {
         padding: const EdgeInsets.all(0),
         side: BorderSide(
           width: 0.5,
-          color: Theme.of(context).iconTheme.color.withOpacity(0.24),
+          color: Theme.of(context).iconTheme.color!.withOpacity(0.24),
         ),
       ),
       child: SizedBox(

+ 6 - 6
lib/ui/collections/remote_collections_grid_view_widget.dart

@@ -1,4 +1,4 @@
-// @dart=2.9
+
 
 import 'dart:math';
 
@@ -17,11 +17,11 @@ class RemoteCollectionsGridViewWidget extends StatelessWidget {
   static const fixedGapBetweenAlbum = 8.0;
   static const minGapForHorizontalPadding = 8.0;
 
-  final List<CollectionWithThumbnail> collections;
+  final List<CollectionWithThumbnail>? collections;
 
   const RemoteCollectionsGridViewWidget(
     this.collections, {
-    Key key,
+    Key? key,
   }) : super(key: key);
 
   @override
@@ -46,13 +46,13 @@ class RemoteCollectionsGridViewWidget extends StatelessWidget {
         physics: const ScrollPhysics(),
         // to disable GridView's scrolling
         itemBuilder: (context, index) {
-          if (index < collections.length) {
-            return CollectionItem(collections[index], sideOfThumbnail);
+          if (index < collections!.length) {
+            return CollectionItem(collections![index], sideOfThumbnail);
           } else {
             return const CreateNewAlbumWidget();
           }
         },
-        itemCount: collections.length + 1,
+        itemCount: collections!.length + 1,
         // To include the + button
         gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
           crossAxisCount: albumsCountInOneRow,

+ 23 - 15
lib/ui/collections_gallery_widget.dart

@@ -1,5 +1,3 @@
-// @dart=2.9
-
 import 'dart:async';
 
 import 'package:collection/collection.dart';
@@ -22,11 +20,12 @@ import 'package:photos/ui/collections/remote_collections_grid_view_widget.dart';
 import 'package:photos/ui/collections/section_title.dart';
 import 'package:photos/ui/collections/trash_button_widget.dart';
 import 'package:photos/ui/common/loading_widget.dart';
+import 'package:photos/ui/viewer/actions/delete_empty_albums.dart';
 import 'package:photos/ui/viewer/gallery/empty_state.dart';
 import 'package:photos/utils/local_settings.dart';
 
 class CollectionsGalleryWidget extends StatefulWidget {
-  const CollectionsGalleryWidget({Key key}) : super(key: key);
+  const CollectionsGalleryWidget({Key? key}) : super(key: key);
 
   @override
   State<CollectionsGalleryWidget> createState() =>
@@ -36,10 +35,11 @@ class CollectionsGalleryWidget extends StatefulWidget {
 class _CollectionsGalleryWidgetState extends State<CollectionsGalleryWidget>
     with AutomaticKeepAliveClientMixin {
   final _logger = Logger((_CollectionsGalleryWidgetState).toString());
-  StreamSubscription<LocalPhotosUpdatedEvent> _localFilesSubscription;
-  StreamSubscription<CollectionUpdatedEvent> _collectionUpdatesSubscription;
-  StreamSubscription<UserLoggedOutEvent> _loggedOutEvent;
-  AlbumSortKey sortKey;
+  late StreamSubscription<LocalPhotosUpdatedEvent> _localFilesSubscription;
+  late StreamSubscription<CollectionUpdatedEvent>
+      _collectionUpdatesSubscription;
+  late StreamSubscription<UserLoggedOutEvent> _loggedOutEvent;
+  AlbumSortKey? sortKey;
   String _loadReason = "init";
 
   @override
@@ -98,8 +98,8 @@ class _CollectionsGalleryWidgetState extends State<CollectionsGalleryWidget>
       (first, second) {
         if (sortKey == AlbumSortKey.albumName) {
           return compareAsciiLowerCaseNatural(
-            first.collection.name,
-            second.collection.name,
+            first.collection.name!,
+            second.collection.name!,
           );
         } else if (sortKey == AlbumSortKey.newestPhoto) {
           return (second.thumbnail?.creationTime ?? -1 * intMaxValue)
@@ -121,13 +121,15 @@ class _CollectionsGalleryWidgetState extends State<CollectionsGalleryWidget>
   }
 
   Widget _getCollectionsGalleryWidget(
-    List<CollectionWithThumbnail> collections,
+    List<CollectionWithThumbnail>? collections,
   ) {
+    final bool showDeleteAlbumsButton =
+        collections!.where((c) => c.thumbnail == null).length >= 3;
     final TextStyle trashAndHiddenTextStyle = Theme.of(context)
         .textTheme
-        .subtitle1
+        .subtitle1!
         .copyWith(
-          color: Theme.of(context).textTheme.subtitle1.color.withOpacity(0.5),
+          color: Theme.of(context).textTheme.subtitle1!.color!.withOpacity(0.5),
         );
 
     return SingleChildScrollView(
@@ -149,6 +151,12 @@ class _CollectionsGalleryWidgetState extends State<CollectionsGalleryWidget>
                 _sortMenu(),
               ],
             ),
+            showDeleteAlbumsButton
+                ? const Padding(
+                    padding: EdgeInsets.only(top: 2, left: 8.5, right: 48),
+                    child: DeleteEmptyAlbums(),
+                  )
+                : const SizedBox.shrink(),
             const SizedBox(height: 12),
             Configuration.instance.hasConfiguredAccount()
                 ? RemoteCollectionsGridViewWidget(collections)
@@ -190,9 +198,9 @@ class _CollectionsGalleryWidgetState extends State<CollectionsGalleryWidget>
       }
       return Text(
         text,
-        style: Theme.of(context).textTheme.subtitle1.copyWith(
+        style: Theme.of(context).textTheme.subtitle1!.copyWith(
               fontSize: 14,
-              color: Theme.of(context).iconTheme.color.withOpacity(0.7),
+              color: Theme.of(context).iconTheme.color!.withOpacity(0.7),
             ),
       );
     }
@@ -228,7 +236,7 @@ class _CollectionsGalleryWidgetState extends State<CollectionsGalleryWidget>
         ),
         onSelected: (int index) async {
           sortKey = AlbumSortKey.values[index];
-          await LocalSettings.instance.setAlbumSortKey(sortKey);
+          await LocalSettings.instance.setAlbumSortKey(sortKey!);
           setState(() {});
         },
         itemBuilder: (context) {

+ 2 - 2
lib/ui/common/DividerWithPadding.dart

@@ -1,11 +1,11 @@
-// @dart=2.9
+
 
 import 'package:flutter/material.dart';
 
 class DividerWithPadding extends StatelessWidget {
   final double left, top, right, bottom, thinckness;
   const DividerWithPadding({
-    Key key,
+    Key? key,
     this.left = 0,
     this.top = 0,
     this.right = 0,

+ 3 - 3
lib/ui/common/bottom_shadow.dart

@@ -1,11 +1,11 @@
-// @dart=2.9
+
 
 import 'package:flutter/material.dart';
 
 class BottomShadowWidget extends StatelessWidget {
   final double offsetDy;
-  final Color shadowColor;
-  const BottomShadowWidget({this.offsetDy = 28, this.shadowColor, Key key})
+  final Color? shadowColor;
+  const BottomShadowWidget({this.offsetDy = 28, this.shadowColor, Key? key})
       : super(key: key);
 
   @override

+ 4 - 4
lib/ui/common/dialogs.dart

@@ -1,4 +1,4 @@
-// @dart=2.9
+
 
 import 'package:flutter/material.dart';
 import 'package:photos/ente_theme_data.dart';
@@ -11,14 +11,14 @@ enum ActionType {
 }
 
 // if dialog is dismissed by tapping outside, this will return null
-Future<DialogUserChoice> showChoiceDialog<T>(
+Future<DialogUserChoice?> showChoiceDialog<T>(
   BuildContext context,
   String title,
   String content, {
   String firstAction = 'Ok',
-  Color firstActionColor,
+  Color? firstActionColor,
   String secondAction = 'Cancel',
-  Color secondActionColor,
+  Color? secondActionColor,
   ActionType actionType = ActionType.confirm,
 }) {
   final AlertDialog alert = AlertDialog(

+ 15 - 15
lib/ui/common/dynamic_fab.dart

@@ -1,4 +1,4 @@
-// @dart=2.9
+
 
 import 'dart:math' as math;
 
@@ -6,13 +6,13 @@ import 'package:flutter/material.dart';
 import 'package:photos/ente_theme_data.dart';
 
 class DynamicFAB extends StatelessWidget {
-  final bool isKeypadOpen;
-  final bool isFormValid;
-  final String buttonText;
-  final Function onPressedFunction;
+  final bool? isKeypadOpen;
+  final bool? isFormValid;
+  final String? buttonText;
+  final Function? onPressedFunction;
 
   const DynamicFAB({
-    Key key,
+    Key? key,
     this.isKeypadOpen,
     this.buttonText,
     this.isFormValid,
@@ -21,7 +21,7 @@ class DynamicFAB extends StatelessWidget {
 
   @override
   Widget build(BuildContext context) {
-    if (isKeypadOpen) {
+    if (isKeypadOpen!) {
       return Container(
         decoration: BoxDecoration(
           boxShadow: [
@@ -43,13 +43,13 @@ class DynamicFAB extends StatelessWidget {
                   Theme.of(context).colorScheme.dynamicFABBackgroundColor,
               foregroundColor:
                   Theme.of(context).colorScheme.dynamicFABTextColor,
-              onPressed: isFormValid
-                  ? onPressedFunction
+              onPressed: isFormValid!
+                  ? onPressedFunction as void Function()?
                   : () {
                       FocusScope.of(context).unfocus();
                     },
               child: Transform.rotate(
-                angle: isFormValid ? 0 : math.pi / 2,
+                angle: isFormValid! ? 0 : math.pi / 2,
                 child: const Icon(
                   Icons.chevron_right,
                   size: 36,
@@ -65,8 +65,8 @@ class DynamicFAB extends StatelessWidget {
         height: 56,
         padding: const EdgeInsets.symmetric(horizontal: 20),
         child: OutlinedButton(
-          onPressed: isFormValid ? onPressedFunction : null,
-          child: Text(buttonText),
+          onPressed: isFormValid! ? onPressedFunction as void Function()? : null,
+          child: Text(buttonText!),
         ),
       );
     }
@@ -75,17 +75,17 @@ class DynamicFAB extends StatelessWidget {
 
 class NoScalingAnimation extends FloatingActionButtonAnimator {
   @override
-  Offset getOffset({Offset begin, Offset end, double progress}) {
+  Offset getOffset({Offset? begin, required Offset end, double? progress}) {
     return end;
   }
 
   @override
-  Animation<double> getRotationAnimation({Animation<double> parent}) {
+  Animation<double> getRotationAnimation({required Animation<double> parent}) {
     return Tween<double>(begin: 1.0, end: 1.0).animate(parent);
   }
 
   @override
-  Animation<double> getScaleAnimation({Animation<double> parent}) {
+  Animation<double> getScaleAnimation({required Animation<double> parent}) {
     return Tween<double>(begin: 1.0, end: 1.0).animate(parent);
   }
 }

+ 5 - 5
lib/ui/common/gradient_button.dart

@@ -1,23 +1,23 @@
-// @dart=2.9
+
 
 import 'package:flutter/material.dart';
 import 'package:photos/theme/ente_theme.dart';
 
 class GradientButton extends StatelessWidget {
   final List<Color> linearGradientColors;
-  final Function onTap;
+  final Function? onTap;
 
   // text is ignored if child is specified
   final String text;
 
   // nullable
-  final IconData iconData;
+  final IconData? iconData;
 
   // padding between the text and icon
   final double paddingValue;
 
   const GradientButton({
-    Key key,
+    Key? key,
     this.linearGradientColors = const [
       Color(0xFF2CD267),
       Color(0xFF1DB954),
@@ -65,7 +65,7 @@ class GradientButton extends StatelessWidget {
       );
     }
     return InkWell(
-      onTap: onTap,
+      onTap: onTap as void Function()?,
       child: Container(
         height: 56,
         decoration: BoxDecoration(

+ 3 - 3
lib/ui/common/linear_progress_dialog.dart

@@ -1,4 +1,4 @@
-// @dart=2.9
+
 
 import 'package:flutter/material.dart';
 import 'package:photos/ente_theme_data.dart';
@@ -6,14 +6,14 @@ import 'package:photos/ente_theme_data.dart';
 class LinearProgressDialog extends StatefulWidget {
   final String message;
 
-  const LinearProgressDialog(this.message, {Key key}) : super(key: key);
+  const LinearProgressDialog(this.message, {Key? key}) : super(key: key);
 
   @override
   LinearProgressDialogState createState() => LinearProgressDialogState();
 }
 
 class LinearProgressDialogState extends State<LinearProgressDialog> {
-  double _progress;
+  double? _progress;
 
   @override
   void initState() {

+ 36 - 36
lib/ui/common/progress_dialog.dart

@@ -1,5 +1,3 @@
-// @dart=2.9
-
 import 'package:flutter/material.dart';
 
 enum ProgressDialogType { normal, download }
@@ -7,7 +5,7 @@ enum ProgressDialogType { normal, download }
 String _dialogMessage = "Loading...";
 double _progress = 0.0, _maxProgress = 100.0;
 
-Widget _customBody;
+Widget? _customBody;
 
 TextAlign _textAlign = TextAlign.left;
 Alignment _progressWidgetAlignment = Alignment.centerLeft;
@@ -15,10 +13,10 @@ Alignment _progressWidgetAlignment = Alignment.centerLeft;
 TextDirection _direction = TextDirection.ltr;
 
 bool _isShowing = false;
-BuildContext _context, _dismissingContext;
-ProgressDialogType _progressDialogType;
+BuildContext? _context, _dismissingContext;
+ProgressDialogType? _progressDialogType;
 bool _barrierDismissible = true, _showLogs = false;
-Color _barrierColor;
+Color? _barrierColor;
 
 TextStyle _progressTextStyle = const TextStyle(
       color: Colors.black,
@@ -42,16 +40,16 @@ Widget _progressWidget = Image.asset(
 );
 
 class ProgressDialog {
-  _Body _dialog;
+  _Body? _dialog;
 
   ProgressDialog(
     BuildContext context, {
-    ProgressDialogType type,
-    bool isDismissible,
-    bool showLogs,
-    TextDirection textDirection,
-    Widget customBody,
-    Color barrierColor,
+    ProgressDialogType? type,
+    bool? isDismissible,
+    bool? showLogs,
+    TextDirection? textDirection,
+    Widget? customBody,
+    Color? barrierColor,
   }) {
     _context = context;
     _progressDialogType = type ?? ProgressDialogType.normal;
@@ -63,20 +61,20 @@ class ProgressDialog {
   }
 
   void style({
-    Widget child,
-    double progress,
-    double maxProgress,
-    String message,
-    Widget progressWidget,
-    Color backgroundColor,
-    TextStyle progressTextStyle,
-    TextStyle messageTextStyle,
-    double elevation,
-    TextAlign textAlign,
-    double borderRadius,
-    Curve insetAnimCurve,
-    EdgeInsets padding,
-    Alignment progressWidgetAlignment,
+    Widget? child,
+    double? progress,
+    double? maxProgress,
+    String? message,
+    Widget? progressWidget,
+    Color? backgroundColor,
+    TextStyle? progressTextStyle,
+    TextStyle? messageTextStyle,
+    double? elevation,
+    TextAlign? textAlign,
+    double? borderRadius,
+    Curve? insetAnimCurve,
+    EdgeInsets? padding,
+    Alignment? progressWidgetAlignment,
   }) {
     if (_isShowing) return;
     if (_progressDialogType == ProgressDialogType.download) {
@@ -100,12 +98,12 @@ class ProgressDialog {
   }
 
   void update({
-    double progress,
-    double maxProgress,
-    String message,
-    Widget progressWidget,
-    TextStyle progressTextStyle,
-    TextStyle messageTextStyle,
+    double? progress,
+    double? maxProgress,
+    String? message,
+    Widget? progressWidget,
+    TextStyle? progressTextStyle,
+    TextStyle? messageTextStyle,
   }) {
     if (_progressDialogType == ProgressDialogType.download) {
       _progress = progress ?? _progress;
@@ -117,7 +115,7 @@ class ProgressDialog {
     _messageStyle = messageTextStyle ?? _messageStyle;
     _progressTextStyle = progressTextStyle ?? _progressTextStyle;
 
-    if (_isShowing) _dialog.update();
+    if (_isShowing) _dialog!.update();
   }
 
   bool isShowing() {
@@ -128,7 +126,9 @@ class ProgressDialog {
     try {
       if (_isShowing) {
         _isShowing = false;
-        Navigator.of(_dismissingContext).pop();
+        if (_dismissingContext != null) {
+          Navigator.of(_dismissingContext!).pop();
+        }
         if (_showLogs) debugPrint('ProgressDialog dismissed');
         return Future.value(true);
       } else {
@@ -147,7 +147,7 @@ class ProgressDialog {
       if (!_isShowing) {
         _dialog = _Body();
         showDialog<dynamic>(
-          context: _context,
+          context: _context!,
           barrierDismissible: _barrierDismissible,
           barrierColor: _barrierColor,
           builder: (BuildContext context) {

+ 7 - 7
lib/ui/common/rename_dialog.dart

@@ -1,14 +1,14 @@
-// @dart=2.9
+
 
 import 'package:flutter/material.dart';
 import 'package:photos/utils/dialog_util.dart';
 
 class RenameDialog extends StatefulWidget {
-  final String name;
+  final String? name;
   final String type;
   final int maxLength;
 
-  const RenameDialog(this.name, this.type, {Key key, this.maxLength = 100})
+  const RenameDialog(this.name, this.type, {Key? key, this.maxLength = 100})
       : super(key: key);
 
   @override
@@ -16,7 +16,7 @@ class RenameDialog extends StatefulWidget {
 }
 
 class _RenameDialogState extends State<RenameDialog> {
-  String _newName;
+  String? _newName;
 
   @override
   void initState() {
@@ -74,7 +74,7 @@ class _RenameDialogState extends State<RenameDialog> {
             ),
           ),
           onPressed: () {
-            if (_newName.trim().isEmpty) {
+            if (_newName!.trim().isEmpty) {
               showErrorDialog(
                 context,
                 "Empty name",
@@ -82,7 +82,7 @@ class _RenameDialogState extends State<RenameDialog> {
               );
               return;
             }
-            if (_newName.trim().length > widget.maxLength) {
+            if (_newName!.trim().length > widget.maxLength) {
               showErrorDialog(
                 context,
                 "Name too large",
@@ -90,7 +90,7 @@ class _RenameDialogState extends State<RenameDialog> {
               );
               return;
             }
-            Navigator.of(context).pop(_newName.trim());
+            Navigator.of(context).pop(_newName!.trim());
           },
         ),
       ],

+ 2 - 2
lib/ui/common/web_page.dart

@@ -1,4 +1,4 @@
-// @dart=2.9
+
 
 import 'package:flutter/material.dart';
 import 'package:flutter_inappwebview/flutter_inappwebview.dart';
@@ -8,7 +8,7 @@ class WebPage extends StatefulWidget {
   final String title;
   final String url;
 
-  const WebPage(this.title, this.url, {Key key}) : super(key: key);
+  const WebPage(this.title, this.url, {Key? key}) : super(key: key);
 
   @override
   State<WebPage> createState() => _WebPageState();

+ 2 - 3
lib/ui/components/bottom_action_bar/bottom_action_bar_widget.dart

@@ -103,12 +103,11 @@ class BottomActionBarWidget extends StatelessWidget {
   }
 
   List<Widget> _iconButtons(BuildContext context) {
-    final iconButtonsWithExpansionIcon = <Widget?>[
+    final iconButtonsWithExpansionIcon = <Widget>[
       ...?iconButtons,
       ExpansionIconWidget(expandableController: _expandableController)
     ];
-    iconButtonsWithExpansionIcon.removeWhere((element) => element == null);
-    return iconButtonsWithExpansionIcon as List<Widget>;
+    return iconButtonsWithExpansionIcon;
   }
 
   ExpandableThemeData _getExpandableTheme() {

+ 33 - 0
lib/ui/components/button_widget.dart

@@ -60,6 +60,11 @@ class ButtonWidget extends StatelessWidget {
   ///This should be set to true if the alert which uses this button needs to
   ///return the Button's action.
   final bool isInAlert;
+
+  /// progressStatus can be used to display information about the action
+  /// progress when ExecutionState is in Progress.
+  final ValueNotifier<String>? progressStatus;
+
   const ButtonWidget({
     required this.buttonType,
     this.buttonSize = ButtonSize.large,
@@ -72,6 +77,7 @@ class ButtonWidget extends StatelessWidget {
     this.isInAlert = false,
     this.iconColor,
     this.shouldSurfaceExecutionStates = true,
+    this.progressStatus,
     super.key,
   });
 
@@ -137,6 +143,7 @@ class ButtonWidget extends StatelessWidget {
       icon: icon,
       buttonAction: buttonAction,
       shouldSurfaceExecutionStates: shouldSurfaceExecutionStates,
+      progressStatus: progressStatus,
     );
   }
 }
@@ -152,6 +159,8 @@ class ButtonChildWidget extends StatefulWidget {
   final ButtonAction? buttonAction;
   final bool isInAlert;
   final bool shouldSurfaceExecutionStates;
+  final ValueNotifier<String>? progressStatus;
+
   const ButtonChildWidget({
     required this.buttonStyle,
     required this.buttonType,
@@ -159,6 +168,7 @@ class ButtonChildWidget extends StatefulWidget {
     required this.buttonSize,
     required this.isInAlert,
     required this.shouldSurfaceExecutionStates,
+    this.progressStatus,
     this.onTap,
     this.labelText,
     this.icon,
@@ -177,14 +187,17 @@ class _ButtonChildWidgetState extends State<ButtonChildWidget> {
   late TextStyle labelStyle;
   late Color checkIconColor;
   late Color loadingIconColor;
+  ValueNotifier<String>? progressStatus;
 
   ///This is used to store the width of the button in idle state (small button)
   ///to be used as width for the button when the loading/succes states comes.
   double? widthOfButton;
   final _debouncer = Debouncer(const Duration(milliseconds: 300));
   ExecutionState executionState = ExecutionState.idle;
+
   @override
   void initState() {
+    progressStatus = widget.progressStatus;
     checkIconColor = widget.buttonStyle.checkIconColor ??
         widget.buttonStyle.defaultIconColor;
     loadingIconColor = widget.buttonStyle.defaultIconColor;
@@ -203,6 +216,7 @@ class _ButtonChildWidgetState extends State<ButtonChildWidget> {
       iconColor = widget.buttonStyle.defaultIconColor;
       labelStyle = widget.buttonStyle.defaultLabelStyle;
     }
+
     super.initState();
   }
 
@@ -315,6 +329,25 @@ class _ButtonChildWidgetState extends State<ButtonChildWidget> {
                             mainAxisAlignment: MainAxisAlignment.center,
                             mainAxisSize: MainAxisSize.min,
                             children: [
+                              progressStatus == null
+                                  ? const SizedBox.shrink()
+                                  : ValueListenableBuilder<String>(
+                                      valueListenable: progressStatus!,
+                                      builder: (
+                                        BuildContext context,
+                                        String value,
+                                        Widget? child,
+                                      ) {
+                                        return Padding(
+                                          padding:
+                                              const EdgeInsets.only(right: 8.0),
+                                          child: Text(
+                                            value,
+                                            style: lightTextTheme.smallBold,
+                                          ),
+                                        );
+                                      },
+                                    ),
                               EnteLoadingWidget(
                                 is20pts: true,
                                 color: loadingIconColor,

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

@@ -1,5 +1,4 @@
 import 'package:flutter/cupertino.dart';
-import 'package:flutter/material.dart';
 import 'package:photos/theme/ente_theme.dart';
 
 class KeyboardTopButton extends StatelessWidget {

+ 26 - 24
lib/ui/create_collection_page.dart

@@ -1,5 +1,3 @@
-// @dart=2.9
-
 import 'package:collection/collection.dart';
 import 'package:flutter/material.dart';
 import 'package:logging/logging.dart';
@@ -46,14 +44,14 @@ String _actionName(CollectionActionType type, bool plural) {
 }
 
 class CreateCollectionPage extends StatefulWidget {
-  final SelectedFiles selectedFiles;
-  final List<SharedMediaFile> sharedFiles;
+  final SelectedFiles? selectedFiles;
+  final List<SharedMediaFile>? sharedFiles;
   final CollectionActionType actionType;
 
   const CreateCollectionPage(
     this.selectedFiles,
     this.sharedFiles, {
-    Key key,
+    Key? key,
     this.actionType = CollectionActionType.addFiles,
   }) : super(key: key);
 
@@ -63,13 +61,13 @@ class CreateCollectionPage extends StatefulWidget {
 
 class _CreateCollectionPageState extends State<CreateCollectionPage> {
   final _logger = Logger((_CreateCollectionPageState).toString());
-  String _albumName;
+  late String _albumName;
 
   @override
   Widget build(BuildContext context) {
     final filesCount = widget.sharedFiles != null
-        ? widget.sharedFiles.length
-        : widget.selectedFiles.files.length;
+        ? widget.sharedFiles!.length
+        : widget.selectedFiles!.files.length;
     return Scaffold(
       appBar: AppBar(
         title: Text(_actionName(widget.actionType, filesCount > 1)),
@@ -134,9 +132,9 @@ class _CreateCollectionPageState extends State<CreateCollectionPage> {
         } else if (snapshot.hasData) {
           return ListView.builder(
             itemBuilder: (context, index) {
-              return _buildCollectionItem(snapshot.data[index]);
+              return _buildCollectionItem(snapshot.data![index]);
             },
-            itemCount: snapshot.data.length,
+            itemCount: snapshot.data!.length,
             shrinkWrap: true,
             physics: const NeverScrollableScrollPhysics(),
           );
@@ -171,7 +169,7 @@ class _CreateCollectionPageState extends State<CreateCollectionPage> {
             const Padding(padding: EdgeInsets.all(8)),
             Expanded(
               child: Text(
-                item.collection.name,
+                item.collection.name!,
                 style: const TextStyle(
                   fontSize: 16,
                 ),
@@ -184,8 +182,8 @@ class _CreateCollectionPageState extends State<CreateCollectionPage> {
             showShortToast(
               context,
               widget.actionType == CollectionActionType.addFiles
-                  ? "Added successfully to " + item.collection.name
-                  : "Moved successfully to " + item.collection.name,
+                  ? "Added successfully to " + item.collection.name!
+                  : "Moved successfully to " + item.collection.name!,
             );
             _navigateToCollection(item.collection);
           }
@@ -294,7 +292,6 @@ class _CreateCollectionPageState extends State<CreateCollectionPage> {
       case CollectionActionType.restoreFiles:
         return _restoreFilesToCollection(collectionID);
     }
-    throw AssertionError("unexpected actionType ${widget.actionType}");
   }
 
   Future<bool> _moveFilesToCollection(int toCollectionID) async {
@@ -305,11 +302,11 @@ class _CreateCollectionPageState extends State<CreateCollectionPage> {
     await dialog.show();
     try {
       final int fromCollectionID =
-          widget.selectedFiles.files.first?.collectionID;
+          widget.selectedFiles!.files.first.collectionID!;
       await CollectionsService.instance.move(
         toCollectionID,
         fromCollectionID,
-        widget.selectedFiles.files?.toList(),
+        widget.selectedFiles!.files.toList(),
       );
       await dialog.hide();
       RemoteSyncService.instance.sync(silently: true);
@@ -318,7 +315,7 @@ class _CreateCollectionPageState extends State<CreateCollectionPage> {
       return true;
     } on AssertionError catch (e) {
       await dialog.hide();
-      showErrorDialog(context, "Oops", e.message);
+      showErrorDialog(context, "Oops", e.message as String?);
       return false;
     } catch (e, s) {
       _logger.severe("Could not move to album", e, s);
@@ -333,14 +330,14 @@ class _CreateCollectionPageState extends State<CreateCollectionPage> {
     await dialog.show();
     try {
       await CollectionsService.instance
-          .restore(toCollectionID, widget.selectedFiles.files?.toList());
+          .restore(toCollectionID, widget.selectedFiles!.files.toList());
       RemoteSyncService.instance.sync(silently: true);
       widget.selectedFiles?.clearAll();
       await dialog.hide();
       return true;
     } on AssertionError catch (e) {
       await dialog.hide();
-      showErrorDialog(context, "Oops", e.message);
+      showErrorDialog(context, "Oops", e.message as String?);
       return false;
     } catch (e, s) {
       _logger.severe("Could not move to album", e, s);
@@ -359,13 +356,18 @@ class _CreateCollectionPageState extends State<CreateCollectionPage> {
       if (widget.sharedFiles != null) {
         filesPendingUpload.addAll(
           await convertIncomingSharedMediaToFile(
-            widget.sharedFiles,
+            widget.sharedFiles!,
             collectionID,
           ),
         );
       } else {
-        for (final file in widget.selectedFiles.files) {
-          final currentFile = await FilesDB.instance.getFile(file.generatedID);
+        for (final file in widget.selectedFiles!.files) {
+          final File? currentFile =
+              await (FilesDB.instance.getFile(file.generatedID!));
+          if (currentFile == null) {
+            _logger.severe("Failed to find fileBy genID");
+            continue;
+          }
           if (currentFile.uploadedFileID == null) {
             currentFile.collectionID = collectionID;
             filesPendingUpload.add(currentFile);
@@ -396,8 +398,8 @@ class _CreateCollectionPageState extends State<CreateCollectionPage> {
     return false;
   }
 
-  Future<Collection> _createAlbum(String albumName) async {
-    Collection collection;
+  Future<Collection?> _createAlbum(String albumName) async {
+    Collection? collection;
     final dialog = createProgressDialog(context, "Creating album...");
     await dialog.show();
     try {

+ 24 - 28
lib/ui/extents_page_view.dart

@@ -1,5 +1,3 @@
-// @dart=2.9
-
 import 'package:flutter/gestures.dart';
 import 'package:flutter/rendering.dart';
 import 'package:flutter/widgets.dart' hide PageView;
@@ -49,10 +47,10 @@ class ExtentsPageView extends StatefulWidget {
   /// child that could possibly be displayed in the page view, instead of just
   /// those children that are actually visible.
   ExtentsPageView({
-    Key key,
+    Key? key,
     this.scrollDirection = Axis.horizontal,
     this.reverse = false,
-    PageController controller,
+    PageController? controller,
     this.physics,
     this.pageSnapping = true,
     this.onPageChanged,
@@ -81,15 +79,15 @@ class ExtentsPageView extends StatefulWidget {
   /// you are planning to change child order at a later time, consider using
   /// [PageView] or [PageView.custom].
   ExtentsPageView.builder({
-    Key key,
+    Key? key,
     this.scrollDirection = Axis.horizontal,
     this.reverse = false,
-    PageController controller,
+    PageController? controller,
     this.physics,
     this.pageSnapping = true,
     this.onPageChanged,
-    @required IndexedWidgetBuilder itemBuilder,
-    int itemCount,
+    required IndexedWidgetBuilder itemBuilder,
+    int? itemCount,
     this.dragStartBehavior = DragStartBehavior.start,
     this.openDrawer,
   })  : controller = controller ?? _defaultPageController,
@@ -99,16 +97,16 @@ class ExtentsPageView extends StatefulWidget {
         super(key: key);
 
   ExtentsPageView.extents({
-    Key key,
+    Key? key,
     this.extents = 1,
     this.scrollDirection = Axis.horizontal,
     this.reverse = false,
-    PageController controller,
+    PageController? controller,
     this.physics,
     this.pageSnapping = true,
     this.onPageChanged,
-    @required IndexedWidgetBuilder itemBuilder,
-    int itemCount,
+    required IndexedWidgetBuilder itemBuilder,
+    int? itemCount,
     this.dragStartBehavior = DragStartBehavior.start,
     this.openDrawer,
   })  : controller = controller ?? _defaultPageController,
@@ -201,18 +199,17 @@ class ExtentsPageView extends StatefulWidget {
   /// ```
   /// {@end-tool}
   ExtentsPageView.custom({
-    Key key,
+    Key? key,
     this.scrollDirection = Axis.horizontal,
     this.reverse = false,
-    PageController controller,
+    PageController? controller,
     this.physics,
     this.pageSnapping = true,
     this.onPageChanged,
-    @required this.childrenDelegate,
+    required this.childrenDelegate,
     this.dragStartBehavior = DragStartBehavior.start,
     this.openDrawer,
-  })  : assert(childrenDelegate != null),
-        extents = 0,
+  })  : extents = 0,
         controller = controller ?? _defaultPageController,
         super(key: key);
 
@@ -257,13 +254,13 @@ class ExtentsPageView extends StatefulWidget {
   /// [PageScrollPhysics] prior to being used.
   ///
   /// Defaults to matching platform conventions.
-  final ScrollPhysics physics;
+  final ScrollPhysics? physics;
 
   /// Set to false to disable page snapping, useful for custom scroll behavior.
   final bool pageSnapping;
 
   /// Called whenever the page in the center of the viewport changes.
-  final ValueChanged<int> onPageChanged;
+  final ValueChanged<int>? onPageChanged;
 
   /// A delegate that provides the children for the [PageView].
   ///
@@ -276,7 +273,7 @@ class ExtentsPageView extends StatefulWidget {
   /// {@macro flutter.widgets.scrollable.dragStartBehavior}
   final DragStartBehavior dragStartBehavior;
 
-  final Function openDrawer; //nullable
+  final Function? openDrawer; //nullable
 
   @override
   State<ExtentsPageView> createState() => _PageViewState();
@@ -292,7 +289,7 @@ class _PageViewState extends State<ExtentsPageView> {
     widget.openDrawer != null
         ? widget.controller.addListener(() {
             if (widget.controller.offset < -45) {
-              widget.openDrawer();
+              widget.openDrawer!();
             }
           })
         : null;
@@ -304,7 +301,7 @@ class _PageViewState extends State<ExtentsPageView> {
     super.dispose();
   }
 
-  AxisDirection _getDirection(BuildContext context) {
+  AxisDirection? _getDirection(BuildContext context) {
     switch (widget.scrollDirection) {
       case Axis.horizontal:
         assert(debugCheckHasDirectionality(context));
@@ -317,13 +314,12 @@ class _PageViewState extends State<ExtentsPageView> {
       case Axis.vertical:
         return widget.reverse ? AxisDirection.up : AxisDirection.down;
     }
-    return null;
   }
 
   @override
   Widget build(BuildContext context) {
-    final AxisDirection axisDirection = _getDirection(context);
-    final ScrollPhysics physics = widget.pageSnapping
+    final AxisDirection axisDirection = _getDirection(context)!;
+    final ScrollPhysics? physics = widget.pageSnapping
         ? _kPagePhysics.applyTo(widget.physics)
         : widget.physics;
 
@@ -332,11 +328,11 @@ class _PageViewState extends State<ExtentsPageView> {
         if (notification.depth == 0 &&
             widget.onPageChanged != null &&
             notification is ScrollUpdateNotification) {
-          final PageMetrics metrics = notification.metrics;
-          final int currentPage = metrics.page.round();
+          final PageMetrics metrics = notification.metrics as PageMetrics;
+          final int currentPage = metrics.page!.round();
           if (currentPage != _lastReportedPage) {
             _lastReportedPage = currentPage;
-            widget.onPageChanged(currentPage);
+            widget.onPageChanged!(currentPage);
           }
         }
         return false;

+ 4 - 4
lib/ui/home/header_error_widget.dart

@@ -1,4 +1,4 @@
-// @dart=2.9
+
 
 import 'package:flutter/material.dart';
 import 'package:photos/core/errors.dart';
@@ -7,9 +7,9 @@ import 'package:photos/ui/payment/subscription.dart';
 import 'package:photos/utils/email_util.dart';
 
 class HeaderErrorWidget extends StatelessWidget {
-  final Error _error;
+  final Error? _error;
 
-  const HeaderErrorWidget({Key key, @required Error error})
+  const HeaderErrorWidget({Key? key, required Error? error})
       : _error = error,
         super(key: key);
 
@@ -123,7 +123,7 @@ class HeaderErrorWidget extends StatelessWidget {
                   padding: const EdgeInsets.fromLTRB(50, 16, 50, 16),
                   side: BorderSide(
                     width: 2,
-                    color: Colors.orange[600],
+                    color: Colors.orange[600]!,
                   ),
                 ),
                 child: Text(

+ 0 - 1
lib/ui/home/home_bottom_nav_bar.dart

@@ -2,7 +2,6 @@ import 'dart:async';
 import 'dart:ui';
 
 import 'package:flutter/material.dart';
-import 'package:flutter/widgets.dart';
 import 'package:photos/core/event_bus.dart';
 import 'package:photos/ente_theme_data.dart';
 import 'package:photos/events/tab_changed_event.dart';

+ 6 - 7
lib/ui/home/home_gallery_widget.dart

@@ -1,4 +1,3 @@
-// @dart=2.9
 import 'package:flutter/material.dart';
 import 'package:photos/core/configuration.dart';
 import 'package:photos/core/event_bus.dart';
@@ -16,15 +15,15 @@ import 'package:photos/ui/viewer/actions/file_selection_overlay_bar.dart';
 import 'package:photos/ui/viewer/gallery/gallery.dart';
 
 class HomeGalleryWidget extends StatelessWidget {
-  final Widget header;
-  final Widget footer;
+  final Widget? header;
+  final Widget? footer;
   final SelectedFiles selectedFiles;
 
   const HomeGalleryWidget({
-    Key key,
+    Key? key,
     this.header,
     this.footer,
-    this.selectedFiles,
+    required this.selectedFiles,
   }) : super(key: key);
 
   @override
@@ -42,7 +41,7 @@ class HomeGalleryWidget extends StatelessWidget {
           result = await FilesDB.instance.getAllLocalAndUploadedFiles(
             creationStartTime,
             creationEndTime,
-            ownerID,
+            ownerID!,
             limit: limit,
             asc: asc,
             ignoredCollectionIDs: collectionsToHide,
@@ -51,7 +50,7 @@ class HomeGalleryWidget extends StatelessWidget {
           result = await FilesDB.instance.getAllPendingOrUploadedFiles(
             creationStartTime,
             creationEndTime,
-            ownerID,
+            ownerID!,
             limit: limit,
             asc: asc,
             ignoredCollectionIDs: collectionsToHide,

+ 16 - 16
lib/ui/home/memories_widget.dart

@@ -1,4 +1,4 @@
-// @dart=2.9
+
 
 import 'package:flutter/material.dart';
 import 'package:photos/ente_theme_data.dart';
@@ -14,20 +14,20 @@ import 'package:photos/utils/share_util.dart';
 import 'package:step_progress_indicator/step_progress_indicator.dart';
 
 class MemoriesWidget extends StatelessWidget {
-  const MemoriesWidget({Key key}) : super(key: key);
+  const MemoriesWidget({Key? key}) : super(key: key);
 
   @override
   Widget build(BuildContext context) {
     return FutureBuilder<List<Memory>>(
       future: MemoriesService.instance.getMemories(),
       builder: (context, snapshot) {
-        if (snapshot.hasError || !snapshot.hasData || snapshot.data.isEmpty) {
+        if (snapshot.hasError || !snapshot.hasData || snapshot.data!.isEmpty) {
           return const SizedBox.shrink();
         } else {
           return Column(
             crossAxisAlignment: CrossAxisAlignment.start,
             children: [
-              _buildMemories(snapshot.data),
+              _buildMemories(snapshot.data!),
               const Divider(),
             ],
           );
@@ -69,17 +69,17 @@ class MemoriesWidget extends StatelessWidget {
 
   bool _areMemoriesFromSameYear(Memory first, Memory second) {
     final firstDate =
-        DateTime.fromMicrosecondsSinceEpoch(first.file.creationTime);
+        DateTime.fromMicrosecondsSinceEpoch(first.file.creationTime!);
     final secondDate =
-        DateTime.fromMicrosecondsSinceEpoch(second.file.creationTime);
+        DateTime.fromMicrosecondsSinceEpoch(second.file.creationTime!);
     return firstDate.year == secondDate.year;
   }
 }
 
 class MemoryWidget extends StatefulWidget {
   const MemoryWidget({
-    Key key,
-    @required this.memories,
+    Key? key,
+    required this.memories,
   }) : super(key: key);
 
   final List<Memory> memories;
@@ -119,7 +119,7 @@ class _MemoryWidgetState extends State<MemoryWidget> {
                     title,
                     style: Theme.of(context)
                         .textTheme
-                        .subtitle1
+                        .subtitle1!
                         .copyWith(fontSize: 12),
                     textAlign: TextAlign.center,
                   ),
@@ -186,7 +186,7 @@ class _MemoryWidgetState extends State<MemoryWidget> {
 
   String _getTitle(Memory memory) {
     final present = DateTime.now();
-    final then = DateTime.fromMicrosecondsSinceEpoch(memory.file.creationTime);
+    final then = DateTime.fromMicrosecondsSinceEpoch(memory.file.creationTime!);
     final diffInYears = present.year - then.year;
     if (diffInYears == 1) {
       return "1 year ago";
@@ -201,7 +201,7 @@ class FullScreenMemory extends StatefulWidget {
   final List<Memory> memories;
   final int index;
 
-  const FullScreenMemory(this.title, this.memories, this.index, {Key key})
+  const FullScreenMemory(this.title, this.memories, this.index, {Key? key})
       : super(key: key);
 
   @override
@@ -215,7 +215,7 @@ class _FullScreenMemoryState extends State<FullScreenMemory> {
   // when the top step indicator isn't visible.
   bool _showCounter = false;
   bool _showStepIndicator = true;
-  PageController _pageController;
+  PageController? _pageController;
   bool _shouldDisableScroll = false;
   final GlobalKey shareButtonKey = GlobalKey();
 
@@ -273,9 +273,9 @@ class _FullScreenMemoryState extends State<FullScreenMemory> {
                 ),
                 Text(
                   getFormattedDate(
-                    DateTime.fromMicrosecondsSinceEpoch(file.creationTime),
+                    DateTime.fromMicrosecondsSinceEpoch(file.creationTime!),
                   ),
-                  style: Theme.of(context).textTheme.subtitle1.copyWith(
+                  style: Theme.of(context).textTheme.subtitle1!.copyWith(
                         fontSize: 14,
                         color: Colors.white,
                       ), //same for both themes
@@ -328,7 +328,7 @@ class _FullScreenMemoryState extends State<FullScreenMemory> {
                 '${_index + 1}/${widget.memories.length}',
                 style: Theme.of(context)
                     .textTheme
-                    .bodyText1
+                    .bodyText1!
                     .copyWith(color: Colors.white.withOpacity(0.4)),
               )
             : AnimatedOpacity(
@@ -338,7 +338,7 @@ class _FullScreenMemoryState extends State<FullScreenMemory> {
                   widget.title,
                   style: Theme.of(context)
                       .textTheme
-                      .headline4
+                      .headline4!
                       .copyWith(color: Colors.white),
                 ),
               ),

+ 29 - 31
lib/ui/home/status_bar_widget.dart

@@ -1,5 +1,3 @@
-// @dart=2.9
-
 import 'dart:async';
 
 import 'package:flutter/material.dart';
@@ -19,18 +17,18 @@ import 'package:photos/utils/navigation_util.dart';
 const double kContainerHeight = 36;
 
 class StatusBarWidget extends StatefulWidget {
-  const StatusBarWidget({Key key}) : super(key: key);
+  const StatusBarWidget({Key? key}) : super(key: key);
 
   @override
   State<StatusBarWidget> createState() => _StatusBarWidgetState();
 }
 
 class _StatusBarWidgetState extends State<StatusBarWidget> {
-  StreamSubscription<SyncStatusUpdate> _subscription;
-  StreamSubscription<NotificationEvent> _notificationSubscription;
+  late StreamSubscription<SyncStatusUpdate> _subscription;
+  late StreamSubscription<NotificationEvent> _notificationSubscription;
   bool _showStatus = false;
   bool _showErrorBanner = false;
-  Error _syncError;
+  Error? _syncError;
 
   @override
   void initState() {
@@ -118,7 +116,7 @@ class _StatusBarWidgetState extends State<StatusBarWidget> {
 }
 
 class SyncStatusWidget extends StatefulWidget {
-  const SyncStatusWidget({Key key}) : super(key: key);
+  const SyncStatusWidget({Key? key}) : super(key: key);
 
   @override
   State<SyncStatusWidget> createState() => _SyncStatusWidgetState();
@@ -127,8 +125,8 @@ class SyncStatusWidget extends StatefulWidget {
 class _SyncStatusWidgetState extends State<SyncStatusWidget> {
   static const Duration kSleepDuration = Duration(milliseconds: 3000);
 
-  SyncStatusUpdate _event;
-  StreamSubscription<SyncStatusUpdate> _subscription;
+  SyncStatusUpdate? _event;
+  late StreamSubscription<SyncStatusUpdate> _subscription;
 
   @override
   void initState() {
@@ -150,17 +148,17 @@ class _SyncStatusWidgetState extends State<SyncStatusWidget> {
   @override
   Widget build(BuildContext context) {
     final bool isNotOutdatedEvent = _event != null &&
-        (_event.status == SyncStatus.completedBackup ||
-            _event.status == SyncStatus.completedFirstGalleryImport) &&
-        (DateTime.now().microsecondsSinceEpoch - _event.timestamp >
+        (_event!.status == SyncStatus.completedBackup ||
+            _event!.status == SyncStatus.completedFirstGalleryImport) &&
+        (DateTime.now().microsecondsSinceEpoch - _event!.timestamp >
             kSleepDuration.inMicroseconds);
     if (_event == null ||
         isNotOutdatedEvent ||
         //sync error cases are handled in StatusBarWidget
-        _event.status == SyncStatus.error) {
+        _event!.status == SyncStatus.error) {
       return const SizedBox.shrink();
     }
-    if (_event.status == SyncStatus.completedBackup) {
+    if (_event!.status == SyncStatus.completedBackup) {
       return const SyncStatusCompletedWidget();
     }
     return RefreshIndicatorWidget(_event);
@@ -173,9 +171,9 @@ class RefreshIndicatorWidget extends StatelessWidget {
     valueColor: AlwaysStoppedAnimation<Color>(Color.fromRGBO(45, 194, 98, 1.0)),
   );
 
-  final SyncStatusUpdate event;
+  final SyncStatusUpdate? event;
 
-  const RefreshIndicatorWidget(this.event, {Key key}) : super(key: key);
+  const RefreshIndicatorWidget(this.event, {Key? key}) : super(key: key);
 
   @override
   Widget build(BuildContext context) {
@@ -211,30 +209,30 @@ class RefreshIndicatorWidget extends StatelessWidget {
   }
 
   String _getRefreshingText() {
-    if (event.status == SyncStatus.startedFirstGalleryImport ||
-        event.status == SyncStatus.completedFirstGalleryImport) {
+    if (event!.status == SyncStatus.startedFirstGalleryImport ||
+        event!.status == SyncStatus.completedFirstGalleryImport) {
       return "Loading gallery...";
     }
-    if (event.status == SyncStatus.applyingRemoteDiff) {
+    if (event!.status == SyncStatus.applyingRemoteDiff) {
       return "Syncing...";
     }
-    if (event.status == SyncStatus.preparingForUpload) {
+    if (event!.status == SyncStatus.preparingForUpload) {
       return "Encrypting backup...";
     }
-    if (event.status == SyncStatus.inProgress) {
-      return event.completed.toString() +
+    if (event!.status == SyncStatus.inProgress) {
+      return event!.completed.toString() +
           "/" +
-          event.total.toString() +
+          event!.total.toString() +
           " memories preserved";
     }
-    if (event.status == SyncStatus.paused) {
-      return event.reason;
+    if (event!.status == SyncStatus.paused) {
+      return event!.reason;
     }
-    if (event.status == SyncStatus.error) {
-      return event.reason ?? "Upload failed";
+    if (event!.status == SyncStatus.error) {
+      return event!.reason;
     }
-    if (event.status == SyncStatus.completedBackup) {
-      if (event.wasStopped) {
+    if (event!.status == SyncStatus.completedBackup) {
+      if (event!.wasStopped) {
         return "Sync stopped";
       }
     }
@@ -243,7 +241,7 @@ class RefreshIndicatorWidget extends StatelessWidget {
 }
 
 class BrandingWidget extends StatelessWidget {
-  const BrandingWidget({Key key}) : super(key: key);
+  const BrandingWidget({Key? key}) : super(key: key);
 
   @override
   Widget build(BuildContext context) {
@@ -271,7 +269,7 @@ class BrandingWidget extends StatelessWidget {
 }
 
 class SyncStatusCompletedWidget extends StatelessWidget {
-  const SyncStatusCompletedWidget({Key key}) : super(key: key);
+  const SyncStatusCompletedWidget({Key? key}) : super(key: key);
 
   @override
   Widget build(BuildContext context) {

+ 18 - 18
lib/ui/home_widget.dart

@@ -1,4 +1,4 @@
-// @dart=2.9
+
 
 import 'dart:async';
 import 'dart:io';
@@ -51,7 +51,7 @@ import 'package:receive_sharing_intent/receive_sharing_intent.dart';
 import 'package:uni_links/uni_links.dart';
 
 class HomeWidget extends StatefulWidget {
-  const HomeWidget({Key key}) : super(key: key);
+  const HomeWidget({Key? key}) : super(key: key);
 
   @override
   State<StatefulWidget> createState() => _HomeWidgetState();
@@ -74,17 +74,17 @@ class _HomeWidgetState extends State<HomeWidget> {
 
   // for receiving media files
   // ignore: unused_field
-  StreamSubscription _intentDataStreamSubscription;
-  List<SharedMediaFile> _sharedFiles;
+  StreamSubscription? _intentDataStreamSubscription;
+  List<SharedMediaFile>? _sharedFiles;
 
-  StreamSubscription<TabChangedEvent> _tabChangedEventSubscription;
-  StreamSubscription<SubscriptionPurchasedEvent> _subscriptionPurchaseEvent;
-  StreamSubscription<TriggerLogoutEvent> _triggerLogoutEvent;
-  StreamSubscription<UserLoggedOutEvent> _loggedOutEvent;
-  StreamSubscription<PermissionGrantedEvent> _permissionGrantedEvent;
-  StreamSubscription<SyncStatusUpdate> _firstImportEvent;
-  StreamSubscription<BackupFoldersUpdatedEvent> _backupFoldersUpdatedEvent;
-  StreamSubscription<AccountConfiguredEvent> _accountConfiguredEvent;
+  late StreamSubscription<TabChangedEvent> _tabChangedEventSubscription;
+  late StreamSubscription<SubscriptionPurchasedEvent> _subscriptionPurchaseEvent;
+  late StreamSubscription<TriggerLogoutEvent> _triggerLogoutEvent;
+  late StreamSubscription<UserLoggedOutEvent> _loggedOutEvent;
+  late StreamSubscription<PermissionGrantedEvent> _permissionGrantedEvent;
+  late StreamSubscription<SyncStatusUpdate> _firstImportEvent;
+  late StreamSubscription<BackupFoldersUpdatedEvent> _backupFoldersUpdatedEvent;
+  late StreamSubscription<AccountConfiguredEvent> _accountConfiguredEvent;
 
   @override
   void initState() {
@@ -323,7 +323,7 @@ class _HomeWidgetState extends State<HomeWidget> {
     if (UserRemoteFlagService.instance.showPasswordReminder()) {
       return const PasswordReminder();
     }
-    if (_sharedFiles != null && _sharedFiles.isNotEmpty) {
+    if (_sharedFiles != null && _sharedFiles!.isNotEmpty) {
       ReceiveSharingIntent.reset();
       return CreateCollectionPage(null, _sharedFiles);
     }
@@ -392,7 +392,7 @@ class _HomeWidgetState extends State<HomeWidget> {
   Future<bool> _initDeepLinks() async {
     // Platform messages may fail, so we use a try/catch PlatformException.
     try {
-      final String initialLink = await getInitialLink();
+      final String? initialLink = await getInitialLink();
       // Parse the link and warn the user, if it is not correct,
       // but keep in mind it could be `null`.
       if (initialLink != null) {
@@ -410,8 +410,8 @@ class _HomeWidgetState extends State<HomeWidget> {
 
     // Attach a listener to the stream
     linkStream.listen(
-      (String link) {
-        _logger.info("Link received: " + link);
+      (String? link) {
+        _logger.info("Link received: " + link!);
         _getCredentials(context, link);
       },
       onError: (err) {
@@ -421,11 +421,11 @@ class _HomeWidgetState extends State<HomeWidget> {
     return false;
   }
 
-  void _getCredentials(BuildContext context, String link) {
+  void _getCredentials(BuildContext context, String? link) {
     if (Configuration.instance.hasConfiguredAccount()) {
       return;
     }
-    final ott = Uri.parse(link).queryParameters["ott"];
+    final ott = Uri.parse(link!).queryParameters["ott"]!;
     UserService.instance.verifyEmail(context, ott);
   }
 

+ 14 - 16
lib/ui/huge_listview/draggable_scrollbar.dart

@@ -1,5 +1,3 @@
-// @dart=2.9
-
 import 'dart:async';
 
 import 'package:flutter/foundation.dart';
@@ -12,18 +10,18 @@ class DraggableScrollbar extends StatefulWidget {
   final Color backgroundColor;
   final Color drawColor;
   final double heightScrollThumb;
-  final EdgeInsetsGeometry padding;
+  final EdgeInsetsGeometry? padding;
   final int totalCount;
   final int initialScrollIndex;
   final double bottomSafeArea;
   final int currentFirstIndex;
-  final ValueChanged<double> onChange;
+  final ValueChanged<double>? onChange;
   final String Function(int) labelTextBuilder;
   final bool isEnabled;
 
   const DraggableScrollbar({
-    Key key,
-    @required this.child,
+    Key? key,
+    required this.child,
     this.backgroundColor = Colors.white,
     this.drawColor = Colors.grey,
     this.heightScrollThumb = 80.0,
@@ -32,7 +30,7 @@ class DraggableScrollbar extends StatefulWidget {
     this.totalCount = 1,
     this.initialScrollIndex = 0,
     this.currentFirstIndex = 0,
-    @required this.labelTextBuilder,
+    required this.labelTextBuilder,
     this.onChange,
     this.isEnabled = true,
   }) : super(key: key);
@@ -47,18 +45,18 @@ class DraggableScrollbarState extends State<DraggableScrollbar>
   static const labelAnimationDuration = Duration(milliseconds: 1000);
   double thumbOffset = 0.0;
   bool isDragging = false;
-  int currentFirstIndex;
+  late int currentFirstIndex;
 
   double get thumbMin => 0.0;
 
   double get thumbMax =>
-      context.size.height - widget.heightScrollThumb - widget.bottomSafeArea;
+      context.size!.height - widget.heightScrollThumb - widget.bottomSafeArea;
 
-  AnimationController _thumbAnimationController;
-  Animation<double> _thumbAnimation;
-  AnimationController _labelAnimationController;
-  Animation<double> _labelAnimation;
-  Timer _fadeoutTimer;
+  late AnimationController _thumbAnimationController;
+  Animation<double>? _thumbAnimation;
+  late AnimationController _labelAnimationController;
+  Animation<double>? _labelAnimation;
+  Timer? _fadeoutTimer;
 
   @override
   void initState() {
@@ -66,7 +64,7 @@ class DraggableScrollbarState extends State<DraggableScrollbar>
     currentFirstIndex = widget.currentFirstIndex;
 
     if (widget.initialScrollIndex > 0 && widget.totalCount > 1) {
-      WidgetsBinding.instance?.addPostFrameCallback((_) {
+      WidgetsBinding.instance.addPostFrameCallback((_) {
         setState(
           () => thumbOffset = (widget.initialScrollIndex / widget.totalCount) *
               (thumbMax - thumbMin),
@@ -130,7 +128,7 @@ class DraggableScrollbarState extends State<DraggableScrollbar>
   }
 
   Widget buildThumb() => Padding(
-        padding: widget.padding,
+        padding: widget.padding!,
         child: Container(
           alignment: Alignment.topRight,
           margin: EdgeInsets.only(top: thumbOffset),

+ 15 - 15
lib/ui/huge_listview/huge_listview.dart

@@ -1,4 +1,4 @@
-// @dart=2.9
+
 
 import 'dart:math' show max;
 
@@ -17,7 +17,7 @@ typedef HugeListViewErrorBuilder = Widget Function(
 
 class HugeListView<T> extends StatefulWidget {
   /// A [ScrollablePositionedList] controller for jumping or scrolling to an item.
-  final ItemScrollController controller;
+  final ItemScrollController? controller;
 
   /// Index of an item to initially align within the viewport.
   final int startIndex;
@@ -46,29 +46,29 @@ class HugeListView<T> extends StatefulWidget {
   final HugeListViewItemBuilder<T> itemBuilder;
 
   /// Called to build a progress widget while the whole list is initialized.
-  final WidgetBuilder waitBuilder;
+  final WidgetBuilder? waitBuilder;
 
   /// Called to build a widget when the list is empty.
-  final WidgetBuilder emptyResultBuilder;
+  final WidgetBuilder? emptyResultBuilder;
 
   /// Called to build a widget when there is an error.
-  final HugeListViewErrorBuilder errorBuilder;
+  final HugeListViewErrorBuilder? errorBuilder;
 
   /// Event to call with the index of the topmost visible item in the viewport while scrolling.
   /// Can be used to display the current letter of an alphabetically sorted list, for instance.
-  final ValueChanged<int> firstShown;
+  final ValueChanged<int>? firstShown;
 
   final bool isDraggableScrollbarEnabled;
 
-  final EdgeInsetsGeometry thumbPadding;
+  final EdgeInsetsGeometry? thumbPadding;
 
   const HugeListView({
-    Key key,
+    Key? key,
     this.controller,
-    @required this.startIndex,
-    @required this.totalCount,
-    @required this.labelTextBuilder,
-    @required this.itemBuilder,
+    required this.startIndex,
+    required this.totalCount,
+    required this.labelTextBuilder,
+    required this.itemBuilder,
     this.waitBuilder,
     this.emptyResultBuilder,
     this.errorBuilder,
@@ -121,13 +121,13 @@ class HugeListViewState<T> extends State<HugeListView<T>> {
   @override
   Widget build(BuildContext context) {
     if (error != null && widget.errorBuilder != null) {
-      return widget.errorBuilder(context, error);
+      return widget.errorBuilder!(context, error);
     }
     if (widget.totalCount == -1 && widget.waitBuilder != null) {
-      return widget.waitBuilder(context);
+      return widget.waitBuilder!(context);
     }
     if (widget.totalCount == 0 && widget.emptyResultBuilder != null) {
-      return widget.emptyResultBuilder(context);
+      return widget.emptyResultBuilder!(context);
     }
 
     return LayoutBuilder(

+ 39 - 40
lib/ui/huge_listview/lazy_loading_gallery.dart

@@ -1,5 +1,3 @@
-// @dart=2.9
-
 import 'dart:async';
 import 'dart:math';
 
@@ -27,14 +25,14 @@ import 'package:visibility_detector/visibility_detector.dart';
 class LazyLoadingGallery extends StatefulWidget {
   final List<File> files;
   final int index;
-  final Stream<FilesUpdatedEvent> reloadEvent;
+  final Stream<FilesUpdatedEvent>? reloadEvent;
   final Set<EventType> removalEventTypes;
   final GalleryLoader asyncLoader;
   final SelectedFiles selectedFiles;
   final String tag;
-  final String logTag;
+  final String? logTag;
   final Stream<int> currentIndexStream;
-  final int photoGirdSize;
+  final int? photoGirdSize;
 
   LazyLoadingGallery(
     this.files,
@@ -47,7 +45,7 @@ class LazyLoadingGallery extends StatefulWidget {
     this.currentIndexStream, {
     this.logTag = "",
     this.photoGirdSize = photoGridSizeDefault,
-    Key key,
+    Key? key,
   }) : super(key: key ?? UniqueKey());
 
   @override
@@ -58,12 +56,12 @@ class _LazyLoadingGalleryState extends State<LazyLoadingGallery> {
   static const kRecycleLimit = 400;
   static const kNumberOfDaysToRenderBeforeAndAfter = 8;
 
-  Logger _logger;
+  late Logger _logger;
 
-  List<File> _files;
-  StreamSubscription<FilesUpdatedEvent> _reloadEventSubscription;
-  StreamSubscription<int> _currentIndexSubscription;
-  bool _shouldRender;
+  late List<File> _files;
+  late StreamSubscription<FilesUpdatedEvent> _reloadEventSubscription;
+  late StreamSubscription<int> _currentIndexSubscription;
+  bool? _shouldRender;
   final ValueNotifier<bool> _toggleSelectAllFromDay = ValueNotifier(false);
   final ValueNotifier<bool> _showSelectAllButton = ValueNotifier(false);
   final ValueNotifier<bool> _areAllFromDaySelected = ValueNotifier(false);
@@ -80,7 +78,7 @@ class _LazyLoadingGalleryState extends State<LazyLoadingGallery> {
     _logger = Logger("LazyLoading_${widget.logTag}");
     _shouldRender = true;
     _files = widget.files;
-    _reloadEventSubscription = widget.reloadEvent.listen((e) => _onReload(e));
+    _reloadEventSubscription = widget.reloadEvent!.listen((e) => _onReload(e));
 
     _currentIndexSubscription =
         widget.currentIndexStream.listen((currentIndex) {
@@ -96,9 +94,9 @@ class _LazyLoadingGalleryState extends State<LazyLoadingGallery> {
 
   Future _onReload(FilesUpdatedEvent event) async {
     final galleryDate =
-        DateTime.fromMicrosecondsSinceEpoch(_files[0].creationTime);
+        DateTime.fromMicrosecondsSinceEpoch(_files[0].creationTime!);
     final filesUpdatedThisDay = event.updatedFiles.where((file) {
-      final fileDate = DateTime.fromMicrosecondsSinceEpoch(file.creationTime);
+      final fileDate = DateTime.fromMicrosecondsSinceEpoch(file.creationTime!);
       return fileDate.year == galleryDate.year &&
           fileDate.month == galleryDate.month &&
           fileDate.day == galleryDate.day;
@@ -125,8 +123,8 @@ class _LazyLoadingGalleryState extends State<LazyLoadingGallery> {
         }
       } else if (widget.removalEventTypes.contains(event.type)) {
         // Files were removed
-        final generatedFileIDs = <int>{};
-        final uploadedFileIds = <int>{};
+        final generatedFileIDs = <int?>{};
+        final uploadedFileIds = <int?>{};
         for (final file in filesUpdatedThisDay) {
           if (file.generatedID != null) {
             generatedFileIDs.add(file.generatedID);
@@ -191,12 +189,12 @@ class _LazyLoadingGalleryState extends State<LazyLoadingGallery> {
           children: [
             getDayWidget(
               context,
-              _files[0].creationTime,
-              widget.photoGirdSize,
+              _files[0].creationTime!,
+              widget.photoGirdSize!,
             ),
             ValueListenableBuilder(
               valueListenable: _showSelectAllButton,
-              builder: (context, value, _) {
+              builder: (context, dynamic value, _) {
                 return !value
                     ? const SizedBox.shrink()
                     : GestureDetector(
@@ -206,7 +204,7 @@ class _LazyLoadingGalleryState extends State<LazyLoadingGallery> {
                           height: 44,
                           child: ValueListenableBuilder(
                             valueListenable: _areAllFromDaySelected,
-                            builder: (context, value, _) {
+                            builder: (context, dynamic value, _) {
                               return value
                                   ? const Icon(
                                       Icons.check_circle,
@@ -232,11 +230,11 @@ class _LazyLoadingGalleryState extends State<LazyLoadingGallery> {
             )
           ],
         ),
-        _shouldRender
+        _shouldRender!
             ? _getGallery()
             : PlaceHolderWidget(
                 _files.length,
-                widget.photoGirdSize,
+                widget.photoGirdSize!,
               ),
       ],
     );
@@ -244,7 +242,7 @@ class _LazyLoadingGalleryState extends State<LazyLoadingGallery> {
 
   Widget _getGallery() {
     final List<Widget> childGalleries = [];
-    final subGalleryItemLimit = widget.photoGirdSize < photoGridSizeDefault
+    final subGalleryItemLimit = widget.photoGirdSize! < photoGridSizeDefault
         ? subGalleryLimitMin
         : subGalleryLimitDefault;
     for (int index = 0; index < _files.length; index += subGalleryItemLimit) {
@@ -289,7 +287,7 @@ class LazyLoadingGridView extends StatefulWidget {
   final bool shouldRecycle;
   final ValueNotifier toggleSelectAllFromDay;
   final ValueNotifier areAllFilesSelected;
-  final int photoGridSize;
+  final int? photoGridSize;
 
   LazyLoadingGridView(
     this.tag,
@@ -301,7 +299,7 @@ class LazyLoadingGridView extends StatefulWidget {
     this.toggleSelectAllFromDay,
     this.areAllFilesSelected,
     this.photoGridSize, {
-    Key key,
+    Key? key,
   }) : super(key: key ?? UniqueKey());
 
   @override
@@ -309,9 +307,9 @@ class LazyLoadingGridView extends StatefulWidget {
 }
 
 class _LazyLoadingGridViewState extends State<LazyLoadingGridView> {
-  bool _shouldRender;
-  int _currentUserID;
-  StreamSubscription<ClearSelectionsEvent> _clearSelectionsEvent;
+  bool? _shouldRender;
+  int? _currentUserID;
+  late StreamSubscription<ClearSelectionsEvent> _clearSelectionsEvent;
 
   @override
   void initState() {
@@ -365,25 +363,25 @@ class _LazyLoadingGridViewState extends State<LazyLoadingGridView> {
           });
         }
       },
-      child: _shouldRender
+      child: _shouldRender!
           ? _getGridView()
-          : PlaceHolderWidget(widget.filesInDay.length, widget.photoGridSize),
+          : PlaceHolderWidget(widget.filesInDay.length, widget.photoGridSize!),
     );
   }
 
   Widget _getNonRecyclableView() {
-    if (!_shouldRender) {
+    if (!_shouldRender!) {
       return VisibilityDetector(
         key: UniqueKey(),
         onVisibilityChanged: (visibility) {
-          if (mounted && visibility.visibleFraction > 0 && !_shouldRender) {
+          if (mounted && visibility.visibleFraction > 0 && !_shouldRender!) {
             setState(() {
               _shouldRender = true;
             });
           }
         },
         child:
-            PlaceHolderWidget(widget.filesInDay.length, widget.photoGridSize),
+            PlaceHolderWidget(widget.filesInDay.length, widget.photoGridSize!),
       );
     } else {
       return _getGridView();
@@ -402,7 +400,7 @@ class _LazyLoadingGridViewState extends State<LazyLoadingGridView> {
       gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
         crossAxisSpacing: 2,
         mainAxisSpacing: 2,
-        crossAxisCount: widget.photoGridSize,
+        crossAxisCount: widget.photoGridSize!,
       ),
       padding: const EdgeInsets.all(0),
     );
@@ -414,11 +412,11 @@ class _LazyLoadingGridViewState extends State<LazyLoadingGridView> {
     if (isFileSelected &&
         file.isUploaded &&
         (file.ownerID != _currentUserID ||
-            file.pubMagicMetadata.uploaderName != null)) {
+            file.pubMagicMetadata!.uploaderName != null)) {
       final avatarColors = getEnteColorScheme(context).avatarColors;
       final int randomID = file.ownerID != _currentUserID
-          ? file.ownerID
-          : file.pubMagicMetadata.uploaderName.sumAsciiValues;
+          ? file.ownerID!
+          : file.pubMagicMetadata!.uploaderName.sumAsciiValues;
       selectionColor = avatarColors[(randomID).remainder(avatarColors.length)];
     }
     return GestureDetector(
@@ -452,7 +450,7 @@ class _LazyLoadingGridViewState extends State<LazyLoadingGridView> {
                   serverLoadDeferDuration: thumbnailServerLoadDeferDuration,
                   shouldShowLivePhotoOverlay: true,
                   key: Key(widget.tag + file.tag),
-                  thumbnailSize: widget.photoGridSize < photoGridSizeDefault
+                  thumbnailSize: widget.photoGridSize! < photoGridSizeDefault
                       ? thumbnailLargeSize
                       : thumbnailSmallSize,
                   shouldShowOwnerAvatar: !isFileSelected,
@@ -513,10 +511,11 @@ class _LazyLoadingGridViewState extends State<LazyLoadingGridView> {
   void _toggleSelectAllFromDayListener() {
     if (widget.selectedFiles.files.containsAll(widget.filesInDay.toSet())) {
       setState(() {
-        widget.selectedFiles.unSelectAll(widget.filesInDay.toSet());
+        widget.selectedFiles
+            .unSelectAll(widget.filesInDay.toSet() as Set<File>);
       });
     } else {
-      widget.selectedFiles.selectAll(widget.filesInDay.toSet());
+      widget.selectedFiles.selectAll(widget.filesInDay.toSet() as Set<File>);
     }
   }
 }

+ 14 - 14
lib/ui/huge_listview/scroll_bar_thumb.dart

@@ -1,4 +1,4 @@
-// @dart=2.9
+
 
 import 'package:flutter/material.dart';
 
@@ -7,8 +7,8 @@ class ScrollBarThumb extends StatelessWidget {
   final Color drawColor;
   final double height;
   final String title;
-  final Animation labelAnimation;
-  final Animation thumbAnimation;
+  final Animation? labelAnimation;
+  final Animation? thumbAnimation;
   final Function(DragStartDetails details) onDragStart;
   final Function(DragUpdateDetails details) onDragUpdate;
   final Function(DragEndDetails details) onDragEnd;
@@ -23,7 +23,7 @@ class ScrollBarThumb extends StatelessWidget {
     this.onDragStart,
     this.onDragUpdate,
     this.onDragEnd, {
-    Key key,
+    Key? key,
   }) : super(key: key);
 
   @override
@@ -33,7 +33,7 @@ class ScrollBarThumb extends StatelessWidget {
       children: [
         IgnorePointer(
           child: FadeTransition(
-            opacity: labelAnimation,
+            opacity: labelAnimation as Animation<double>,
             child: Container(
               padding: const EdgeInsets.fromLTRB(20, 12, 20, 12),
               decoration: BoxDecoration(
@@ -60,7 +60,7 @@ class ScrollBarThumb extends StatelessWidget {
           onVerticalDragUpdate: onDragUpdate,
           onVerticalDragEnd: onDragEnd,
           child: SlideFadeTransition(
-            animation: thumbAnimation,
+            animation: thumbAnimation as Animation<double>?,
             child: CustomPaint(
               foregroundPainter: _ArrowCustomPainter(drawColor),
               child: Material(
@@ -131,27 +131,27 @@ class _ArrowCustomPainter extends CustomPainter {
 }
 
 class SlideFadeTransition extends StatelessWidget {
-  final Animation<double> animation;
+  final Animation<double>? animation;
   final Widget child;
 
   const SlideFadeTransition({
-    Key key,
-    @required this.animation,
-    @required this.child,
+    Key? key,
+    required this.animation,
+    required this.child,
   }) : super(key: key);
 
   @override
   Widget build(BuildContext context) {
     return AnimatedBuilder(
-      animation: animation,
-      builder: (context, child) => animation.value == 0.0 ? Container() : child,
+      animation: animation!,
+      builder: (context, child) => animation!.value == 0.0 ? Container() : child!,
       child: SlideTransition(
         position: Tween(
           begin: const Offset(0.3, 0.0),
           end: const Offset(0.0, 0.0),
-        ).animate(animation),
+        ).animate(animation!),
         child: FadeTransition(
-          opacity: animation,
+          opacity: animation!,
           child: child,
         ),
       ),

+ 5 - 5
lib/ui/lifecycle_event_handler.dart

@@ -1,11 +1,11 @@
-// @dart=2.9
+
 
 import 'package:flutter/foundation.dart';
 import 'package:flutter/widgets.dart';
 
 class LifecycleEventHandler extends WidgetsBindingObserver {
-  final AsyncCallback resumeCallBack;
-  final AsyncCallback suspendingCallBack;
+  final AsyncCallback? resumeCallBack;
+  final AsyncCallback? suspendingCallBack;
 
   LifecycleEventHandler({
     this.resumeCallBack,
@@ -17,14 +17,14 @@ class LifecycleEventHandler extends WidgetsBindingObserver {
     switch (state) {
       case AppLifecycleState.resumed:
         if (resumeCallBack != null) {
-          await resumeCallBack();
+          await resumeCallBack!();
         }
         break;
       case AppLifecycleState.inactive:
       case AppLifecycleState.paused:
       case AppLifecycleState.detached:
         if (suspendingCallBack != null) {
-          await suspendingCallBack();
+          await suspendingCallBack!();
         }
         break;
     }

+ 6 - 6
lib/ui/loading_photos_widget.dart

@@ -1,4 +1,4 @@
-// @dart=2.9
+
 
 import 'dart:async';
 import 'dart:io';
@@ -15,15 +15,15 @@ import 'package:photos/ui/common/bottom_shadow.dart';
 import 'package:photos/utils/navigation_util.dart';
 
 class LoadingPhotosWidget extends StatefulWidget {
-  const LoadingPhotosWidget({Key key}) : super(key: key);
+  const LoadingPhotosWidget({Key? key}) : super(key: key);
 
   @override
   State<LoadingPhotosWidget> createState() => _LoadingPhotosWidgetState();
 }
 
 class _LoadingPhotosWidgetState extends State<LoadingPhotosWidget> {
-  StreamSubscription<SyncStatusUpdate> _firstImportEvent;
-  StreamSubscription<LocalImportProgressEvent> _imprortProgressEvent;
+  late StreamSubscription<SyncStatusUpdate> _firstImportEvent;
+  late StreamSubscription<LocalImportProgressEvent> _imprortProgressEvent;
   int _currentPage = 0;
   String _loadingMessage = "Loading your photos...";
   final PageController _pageController = PageController(
@@ -145,7 +145,7 @@ class _LoadingPhotosWidgetState extends State<LoadingPhotosWidget> {
                       children: [
                         Text(
                           "Did you know?",
-                          style: Theme.of(context).textTheme.headline6.copyWith(
+                          style: Theme.of(context).textTheme.headline6!.copyWith(
                                 color: Theme.of(context).colorScheme.greenText,
                               ),
                         ),
@@ -192,7 +192,7 @@ class _LoadingPhotosWidgetState extends State<LoadingPhotosWidget> {
       textAlign: TextAlign.start,
       style: Theme.of(context)
           .textTheme
-          .headline5
+          .headline5!
           .copyWith(color: Theme.of(context).colorScheme.defaultTextColor),
     );
   }

+ 93 - 93
lib/ui/nav_bar.dart

@@ -1,4 +1,4 @@
-// @dart=2.9
+
 
 library google_nav_bar;
 
@@ -7,7 +7,7 @@ import 'package:flutter/services.dart';
 
 class GNav extends StatefulWidget {
   const GNav({
-    Key key,
+    Key? key,
     this.tabs,
     this.selectedIndex = 0,
     this.onTabChange,
@@ -34,29 +34,29 @@ class GNav extends StatefulWidget {
     this.mainAxisAlignment = MainAxisAlignment.spaceBetween,
   }) : super(key: key);
 
-  final List<GButton> tabs;
+  final List<GButton>? tabs;
   final int selectedIndex;
-  final Function onTabChange;
-  final double gap;
-  final double tabBorderRadius;
-  final double iconSize;
-  final Color activeColor;
-  final Color backgroundColor;
-  final Color tabBackgroundColor;
-  final Color color;
-  final Color rippleColor;
-  final Color hoverColor;
-  final EdgeInsetsGeometry padding;
-  final EdgeInsetsGeometry tabMargin;
-  final TextStyle textStyle;
-  final Duration duration;
-  final Curve curve;
-  final bool debug;
-  final bool haptic;
-  final Border tabBorder;
-  final Border tabActiveBorder;
-  final List<BoxShadow> tabShadow;
-  final Gradient tabBackgroundGradient;
+  final Function? onTabChange;
+  final double? gap;
+  final double? tabBorderRadius;
+  final double? iconSize;
+  final Color? activeColor;
+  final Color? backgroundColor;
+  final Color? tabBackgroundColor;
+  final Color? color;
+  final Color? rippleColor;
+  final Color? hoverColor;
+  final EdgeInsetsGeometry? padding;
+  final EdgeInsetsGeometry? tabMargin;
+  final TextStyle? textStyle;
+  final Duration? duration;
+  final Curve? curve;
+  final bool? debug;
+  final bool? haptic;
+  final Border? tabBorder;
+  final Border? tabActiveBorder;
+  final List<BoxShadow>? tabShadow;
+  final Gradient? tabBackgroundGradient;
   final MainAxisAlignment mainAxisAlignment;
 
   @override
@@ -64,7 +64,7 @@ class GNav extends StatefulWidget {
 }
 
 class _GNavState extends State<GNav> {
-  int selectedIndex;
+  int? selectedIndex;
   bool clickable = true;
 
   @override
@@ -83,20 +83,20 @@ class _GNavState extends State<GNav> {
       color: widget.backgroundColor ?? Colors.transparent,
       child: Row(
         mainAxisAlignment: widget.mainAxisAlignment,
-        children: widget.tabs
+        children: widget.tabs!
             .map(
               (t) => GButton(
                 key: t.key,
                 border: t.border ?? widget.tabBorder,
                 activeBorder: t.activeBorder ?? widget.tabActiveBorder,
-                borderRadius: t.borderRadius ?? widget.tabBorderRadius != null
+                borderRadius: t.borderRadius as bool? ?? widget.tabBorderRadius != null
                     ? BorderRadius.all(
-                        Radius.circular(widget.tabBorderRadius),
+                        Radius.circular(widget.tabBorderRadius!),
                       )
                     : const BorderRadius.all(Radius.circular(100.0)),
                 debug: widget.debug ?? false,
                 margin: t.margin ?? widget.tabMargin,
-                active: selectedIndex == widget.tabs.indexOf(t),
+                active: selectedIndex == widget.tabs!.indexOf(t),
                 gap: t.gap ?? widget.gap,
                 iconActiveColor: t.iconActiveColor ?? widget.activeColor,
                 iconColor: t.iconColor ?? widget.color,
@@ -118,7 +118,7 @@ class _GNavState extends State<GNav> {
                     Colors.transparent,
                 duration: widget.duration ?? const Duration(milliseconds: 500),
                 onPressed: () {
-                  widget.onTabChange(widget.tabs.indexOf(t));
+                  widget.onTabChange!(widget.tabs!.indexOf(t));
                 },
               ),
             )
@@ -129,35 +129,35 @@ class _GNavState extends State<GNav> {
 }
 
 class GButton extends StatefulWidget {
-  final bool active;
-  final bool debug;
-  final bool haptic;
-  final double gap;
-  final Color iconColor;
-  final Color rippleColor;
-  final Color hoverColor;
-  final Color iconActiveColor;
-  final Color textColor;
-  final EdgeInsetsGeometry padding;
-  final EdgeInsetsGeometry margin;
-  final TextStyle textStyle;
-  final double iconSize;
-  final Function onPressed;
+  final bool? active;
+  final bool? debug;
+  final bool? haptic;
+  final double? gap;
+  final Color? iconColor;
+  final Color? rippleColor;
+  final Color? hoverColor;
+  final Color? iconActiveColor;
+  final Color? textColor;
+  final EdgeInsetsGeometry? padding;
+  final EdgeInsetsGeometry? margin;
+  final TextStyle? textStyle;
+  final double? iconSize;
+  final Function? onPressed;
   final String text;
-  final IconData icon;
-  final Color backgroundColor;
-  final Duration duration;
-  final Curve curve;
-  final Gradient backgroundGradient;
-  final Widget leading;
-  final BorderRadius borderRadius;
-  final Border border;
-  final Border activeBorder;
-  final List<BoxShadow> shadow;
-  final String semanticLabel;
+  final IconData? icon;
+  final Color? backgroundColor;
+  final Duration? duration;
+  final Curve? curve;
+  final Gradient? backgroundGradient;
+  final Widget? leading;
+  final BorderRadius? borderRadius;
+  final Border? border;
+  final Border? activeBorder;
+  final List<BoxShadow>? shadow;
+  final String? semanticLabel;
 
   const GButton({
-    Key key,
+    Key? key,
     this.active,
     this.haptic,
     this.backgroundColor,
@@ -205,8 +205,8 @@ class _GButtonState extends State<GButton> {
         iconSize: widget.iconSize,
         active: widget.active,
         onPressed: () {
-          if (widget.haptic) HapticFeedback.selectionClick();
-          widget.onPressed();
+          if (widget.haptic!) HapticFeedback.selectionClick();
+          widget.onPressed!();
         },
         padding: widget.padding,
         margin: widget.margin,
@@ -227,7 +227,7 @@ class _GButtonState extends State<GButton> {
 
 class Button extends StatefulWidget {
   const Button({
-    Key key,
+    Key? key,
     this.icon,
     this.iconSize,
     this.leading,
@@ -252,38 +252,38 @@ class Button extends StatefulWidget {
     this.shadow,
   }) : super(key: key);
 
-  final IconData icon;
-  final double iconSize;
-  final Text text;
-  final Widget leading;
-  final Color iconActiveColor;
-  final Color iconColor;
-  final Color color;
-  final Color rippleColor;
-  final Color hoverColor;
-  final double gap;
-  final bool active;
-  final bool debug;
-  final VoidCallback onPressed;
-  final EdgeInsetsGeometry padding;
-  final EdgeInsetsGeometry margin;
-  final Duration duration;
-  final Curve curve;
-  final Gradient gradient;
-  final BorderRadius borderRadius;
-  final Border border;
-  final Border activeBorder;
-  final List<BoxShadow> shadow;
+  final IconData? icon;
+  final double? iconSize;
+  final Text? text;
+  final Widget? leading;
+  final Color? iconActiveColor;
+  final Color? iconColor;
+  final Color? color;
+  final Color? rippleColor;
+  final Color? hoverColor;
+  final double? gap;
+  final bool? active;
+  final bool? debug;
+  final VoidCallback? onPressed;
+  final EdgeInsetsGeometry? padding;
+  final EdgeInsetsGeometry? margin;
+  final Duration? duration;
+  final Curve? curve;
+  final Gradient? gradient;
+  final BorderRadius? borderRadius;
+  final Border? border;
+  final Border? activeBorder;
+  final List<BoxShadow>? shadow;
 
   @override
   State<Button> createState() => _ButtonState();
 }
 
 class _ButtonState extends State<Button> with TickerProviderStateMixin {
-  bool _expanded;
+  bool? _expanded;
 
-  AnimationController expandController;
-  Animation<double> animation;
+  late AnimationController expandController;
+  Animation<double>? animation;
 
   @override
   void initState() {
@@ -302,8 +302,8 @@ class _ButtonState extends State<Button> with TickerProviderStateMixin {
 
   @override
   Widget build(BuildContext context) {
-    _expanded = !widget.active;
-    if (_expanded) {
+    _expanded = !widget.active!;
+    if (_expanded!) {
       expandController.reverse();
     } else {
       expandController.forward();
@@ -312,7 +312,7 @@ class _ButtonState extends State<Button> with TickerProviderStateMixin {
     final Widget icon = widget.leading ??
         Icon(
           widget.icon,
-          color: _expanded ? widget.iconColor : widget.iconActiveColor,
+          color: _expanded! ? widget.iconColor : widget.iconActiveColor,
           size: widget.iconSize,
         );
 
@@ -323,23 +323,23 @@ class _ButtonState extends State<Button> with TickerProviderStateMixin {
         splashColor: widget.rippleColor,
         borderRadius: BorderRadius.circular(100),
         onTap: () {
-          widget.onPressed();
+          widget.onPressed!();
         },
         child: Container(
           padding: widget.margin,
           child: AnimatedContainer(
             curve: Curves.easeOut,
             padding: widget.padding,
-            duration: widget.duration,
+            duration: widget.duration!,
             decoration: BoxDecoration(
               boxShadow: widget.shadow,
-              border: widget.active
+              border: widget.active!
                   ? (widget.activeBorder ?? widget.border)
                   : widget.border,
               gradient: widget.gradient,
-              color: _expanded
-                  ? widget.color.withOpacity(0)
-                  : widget.debug
+              color: _expanded!
+                  ? widget.color!.withOpacity(0)
+                  : widget.debug!
                       ? Colors.red
                       : widget.gradient != null
                           ? Colors.white

+ 2 - 2
lib/ui/notification/update/change_log_page.dart

@@ -1,4 +1,3 @@
-import 'package:flutter/foundation.dart';
 import 'package:flutter/material.dart';
 import 'package:photos/services/update_service.dart';
 import 'package:photos/theme/ente_theme.dart';
@@ -87,7 +86,8 @@ class _ChangeLogPageState extends State<ChangeLogPage> {
                       iconColor: enteColorScheme.primary500,
                       onTap: () async {
                         launchUrlString(
-                            UpdateService.instance.getRateDetails().item2);
+                          UpdateService.instance.getRateDetails().item2,
+                        );
                       },
                     ),
                     const SizedBox(height: 8),

+ 12 - 16
lib/ui/payment/billing_questions_widget.dart

@@ -1,5 +1,3 @@
-// @dart=2.9
-
 import 'dart:convert';
 
 import 'package:expansion_tile_card/expansion_tile_card.dart';
@@ -10,7 +8,7 @@ import 'package:photos/ui/common/loading_widget.dart';
 
 class BillingQuestionsWidget extends StatelessWidget {
   const BillingQuestionsWidget({
-    Key key,
+    Key? key,
   }) : super(key: key);
 
   @override
@@ -64,11 +62,11 @@ class BillingQuestionsWidget extends StatelessWidget {
 
 class FaqWidget extends StatelessWidget {
   const FaqWidget({
-    Key key,
-    @required this.faq,
+    Key? key,
+    required this.faq,
   }) : super(key: key);
 
-  final FaqItem faq;
+  final FaqItem? faq;
 
   @override
   Widget build(BuildContext context) {
@@ -76,7 +74,7 @@ class FaqWidget extends StatelessWidget {
       padding: const EdgeInsets.all(2),
       child: ExpansionTileCard(
         elevation: 0,
-        title: Text(faq.q),
+        title: Text(faq!.q!),
         expandedTextColor: Theme.of(context).colorScheme.greenAlternative,
         baseColor: Theme.of(context).cardColor,
         children: [
@@ -87,7 +85,7 @@ class FaqWidget extends StatelessWidget {
               bottom: 12,
             ),
             child: Text(
-              faq.a,
+              faq!.a!,
               style: const TextStyle(
                 height: 1.5,
               ),
@@ -100,16 +98,16 @@ class FaqWidget extends StatelessWidget {
 }
 
 class FaqItem {
-  final String q;
-  final String a;
+  final String? q;
+  final String? a;
   FaqItem({
     this.q,
     this.a,
   });
 
   FaqItem copyWith({
-    String q,
-    String a,
+    String? q,
+    String? a,
   }) {
     return FaqItem(
       q: q ?? this.q,
@@ -125,11 +123,9 @@ class FaqItem {
   }
 
   factory FaqItem.fromMap(Map<String, dynamic> map) {
-    if (map == null) return null;
-
     return FaqItem(
-      q: map['q'],
-      a: map['a'],
+      q: map['q'] ?? 'q',
+      a: map['a'] ?? 'a',
     );
   }
 

+ 27 - 28
lib/ui/payment/payment_web_page.dart

@@ -1,7 +1,6 @@
-// @dart=2.9
-
 import 'dart:io';
 
+import 'package:collection/collection.dart' show IterableNullableExtension;
 import 'package:flutter/foundation.dart';
 import 'package:flutter/material.dart';
 import 'package:flutter_inappwebview/flutter_inappwebview.dart';
@@ -15,10 +14,10 @@ import 'package:photos/ui/common/progress_dialog.dart';
 import 'package:photos/utils/dialog_util.dart';
 
 class PaymentWebPage extends StatefulWidget {
-  final String planId;
-  final String actionType;
+  final String? planId;
+  final String? actionType;
 
-  const PaymentWebPage({Key key, this.planId, this.actionType})
+  const PaymentWebPage({Key? key, this.planId, this.actionType})
       : super(key: key);
 
   @override
@@ -30,10 +29,10 @@ class _PaymentWebPageState extends State<PaymentWebPage> {
   final UserService userService = UserService.instance;
   final BillingService billingService = BillingService.instance;
   final String basePaymentUrl = kWebPaymentBaseEndpoint;
-  ProgressDialog _dialog;
-  InAppWebViewController webView;
+  late ProgressDialog _dialog;
+  InAppWebViewController? webView;
   double progress = 0;
-  Uri initPaymentUrl;
+  Uri? initPaymentUrl;
 
   @override
   void initState() {
@@ -54,7 +53,7 @@ class _PaymentWebPageState extends State<PaymentWebPage> {
       return const EnteLoadingWidget();
     }
     return WillPopScope(
-      onWillPop: () async => _buildPageExitWidget(context),
+      onWillPop: (() async => _buildPageExitWidget(context)),
       child: Scaffold(
         appBar: AppBar(
           title: const Text('Subscription'),
@@ -83,7 +82,7 @@ class _PaymentWebPageState extends State<PaymentWebPage> {
                   _logger.info("Loading url $loadingUri");
                   // handle the payment response
                   if (_isPaymentActionComplete(loadingUri)) {
-                    await _handlePaymentResponse(loadingUri);
+                    await _handlePaymentResponse(loadingUri!);
                     return NavigationActionPolicy.CANCEL;
                   }
                   return NavigationActionPolicy.ALLOW;
@@ -113,7 +112,7 @@ class _PaymentWebPageState extends State<PaymentWebPage> {
                 },
               ),
             ),
-          ].where((Object o) => o != null).toList(),
+          ].whereNotNull().toList(),
         ),
       ),
     );
@@ -125,7 +124,7 @@ class _PaymentWebPageState extends State<PaymentWebPage> {
     super.dispose();
   }
 
-  Uri _getPaymentUrl(String paymentToken) {
+  Uri _getPaymentUrl(String? paymentToken) {
     final queryParameters = {
       'productID': widget.planId,
       'paymentToken': paymentToken,
@@ -134,15 +133,15 @@ class _PaymentWebPageState extends State<PaymentWebPage> {
     };
     final tryParse = Uri.tryParse(kWebPaymentBaseEndpoint);
     if (kDebugMode && kWebPaymentBaseEndpoint.startsWith("http://")) {
-      return Uri.http(tryParse.authority, tryParse.path, queryParameters);
+      return Uri.http(tryParse!.authority, tryParse.path, queryParameters);
     } else {
-      return Uri.https(tryParse.authority, tryParse.path, queryParameters);
+      return Uri.https(tryParse!.authority, tryParse.path, queryParameters);
     }
   }
 
   // show dialog to handle accidental back press.
-  Future<bool> _buildPageExitWidget(BuildContext context) {
-    return showDialog(
+  Future<bool> _buildPageExitWidget(BuildContext context) async {
+    final result = await showDialog(
       context: context,
       builder: (context) => AlertDialog(
         title: const Text('Are you sure you want to exit?'),
@@ -168,9 +167,13 @@ class _PaymentWebPageState extends State<PaymentWebPage> {
         ],
       ),
     );
+    if (result != null) {
+      return result;
+    }
+    return false;
   }
 
-  bool _isPaymentActionComplete(Uri loadingUri) {
+  bool _isPaymentActionComplete(Uri? loadingUri) {
     return loadingUri.toString().startsWith(kWebPaymentRedirectUrl);
   }
 
@@ -221,14 +224,10 @@ class _PaymentWebPageState extends State<PaymentWebPage> {
         paymentProvider: stripe,
       );
       await _dialog.hide();
-      if (response != null) {
-        final content = widget.actionType == 'buy'
-            ? 'Your purchase was successful'
-            : 'Your subscription was updated successfully';
-        await _showExitPageDialog(title: 'Thank you', content: content);
-      } else {
-        throw Exception("verifySubscription api failed");
-      }
+      final content = widget.actionType == 'buy'
+          ? 'Your purchase was successful'
+          : 'Your subscription was updated successfully';
+      await _showExitPageDialog(title: 'Thank you', content: content);
     } catch (error) {
       _logger.severe(error);
       await _dialog.hide();
@@ -240,13 +239,13 @@ class _PaymentWebPageState extends State<PaymentWebPage> {
   }
 
   // warn the user to wait for sometime before trying another payment
-  Future<dynamic> _showExitPageDialog({String title, String content}) {
+  Future<dynamic> _showExitPageDialog({String? title, String? content}) {
     return showDialog(
       context: context,
       barrierDismissible: false,
       builder: (context) => AlertDialog(
-        title: Text(title),
-        content: Text(content),
+        title: Text(title!),
+        content: Text(content!),
         actions: <Widget>[
           TextButton(
             child: Text(

+ 30 - 32
lib/ui/payment/stripe_subscription_page.dart

@@ -1,5 +1,3 @@
-// @dart=2.9
-
 import 'dart:async';
 
 import 'package:flutter/material.dart';
@@ -30,7 +28,7 @@ class StripeSubscriptionPage extends StatefulWidget {
 
   const StripeSubscriptionPage({
     this.isOnboarding = false,
-    Key key,
+    Key? key,
   }) : super(key: key);
 
   @override
@@ -40,13 +38,13 @@ class StripeSubscriptionPage extends StatefulWidget {
 class _StripeSubscriptionPageState extends State<StripeSubscriptionPage> {
   final _billingService = BillingService.instance;
   final _userService = UserService.instance;
-  Subscription _currentSubscription;
-  ProgressDialog _dialog;
-  UserDetails _userDetails;
+  Subscription? _currentSubscription;
+  late ProgressDialog _dialog;
+  late UserDetails _userDetails;
 
   // indicates if user's subscription plan is still active
-  bool _hasActiveSubscription;
-  FreePlan _freePlan;
+  late bool _hasActiveSubscription;
+  late FreePlan _freePlan;
   List<BillingPlan> _plans = [];
   bool _hasLoadedData = false;
   bool _isLoading = false;
@@ -64,9 +62,9 @@ class _StripeSubscriptionPageState extends State<StripeSubscriptionPage> {
         .then((userDetails) async {
       _userDetails = userDetails;
       _currentSubscription = userDetails.subscription;
-      _showYearlyPlan = _currentSubscription.isYearlyPlan();
-      _hasActiveSubscription = _currentSubscription.isValid();
-      _isStripeSubscriber = _currentSubscription.paymentProvider == stripe;
+      _showYearlyPlan = _currentSubscription!.isYearlyPlan();
+      _hasActiveSubscription = _currentSubscription!.isValid();
+      _isStripeSubscriber = _currentSubscription!.paymentProvider == stripe;
       return _filterStripeForUI().then((value) {
         _hasLoadedData = true;
         setState(() {});
@@ -79,7 +77,7 @@ class _StripeSubscriptionPageState extends State<StripeSubscriptionPage> {
     final billingPlans = await _billingService.getBillingPlans();
     _freePlan = billingPlans.freePlan;
     _plans = billingPlans.plans.where((plan) {
-      if (plan.stripeID == null || plan.stripeID.isEmpty) {
+      if (plan.stripeID.isEmpty) {
         return false;
       }
       final isYearlyPlan = plan.period == 'year';
@@ -101,8 +99,8 @@ class _StripeSubscriptionPageState extends State<StripeSubscriptionPage> {
     // verify user has subscribed before redirecting to main page
     if (widget.isOnboarding &&
         _currentSubscription != null &&
-        _currentSubscription.isValid() &&
-        _currentSubscription.productID != freeProductID) {
+        _currentSubscription!.isValid() &&
+        _currentSubscription!.productID != freeProductID) {
       Navigator.of(context).popUntil((route) => route.isFirst);
     }
   }
@@ -203,7 +201,7 @@ class _StripeSubscriptionPageState extends State<StripeSubscriptionPage> {
       widgets.add(ValidityWidget(currentSubscription: _currentSubscription));
     }
 
-    if (_currentSubscription.productID == freeProductID) {
+    if (_currentSubscription!.productID == freeProductID) {
       if (widget.isOnboarding) {
         widgets.add(SkipSubscriptionWidget(freePlan: _freePlan));
       }
@@ -215,22 +213,22 @@ class _StripeSubscriptionPageState extends State<StripeSubscriptionPage> {
       widgets.add(_stripeRenewOrCancelButton());
     }
 
-    if (_currentSubscription.productID != freeProductID) {
+    if (_currentSubscription!.productID != freeProductID) {
       widgets.addAll([
         Align(
           alignment: Alignment.center,
           child: GestureDetector(
             onTap: () async {
               final String paymentProvider =
-                  _currentSubscription.paymentProvider;
-              switch (_currentSubscription.paymentProvider) {
+                  _currentSubscription!.paymentProvider;
+              switch (_currentSubscription!.paymentProvider) {
                 case stripe:
                   await _launchStripePortal();
                   break;
                 case playStore:
                   launchUrlString(
                     "https://play.google.com/store/account/subscriptions?sku=" +
-                        _currentSubscription.productID +
+                        _currentSubscription!.productID +
                         "&package=io.ente.photos",
                   );
                   break;
@@ -288,7 +286,7 @@ class _StripeSubscriptionPageState extends State<StripeSubscriptionPage> {
                   RichText(
                     text: TextSpan(
                       text: "Manage family",
-                      style: Theme.of(context).textTheme.bodyMedium.copyWith(
+                      style: Theme.of(context).textTheme.bodyMedium!.copyWith(
                             decoration: TextDecoration.underline,
                           ),
                     ),
@@ -330,7 +328,7 @@ class _StripeSubscriptionPageState extends State<StripeSubscriptionPage> {
 
   Widget _stripeRenewOrCancelButton() {
     final bool isRenewCancelled =
-        _currentSubscription.attributes?.isCancelled ?? false;
+        _currentSubscription!.attributes?.isCancelled ?? false;
     final String title =
         isRenewCancelled ? "Renew subscription" : "Cancel subscription";
     return TextButton(
@@ -393,11 +391,11 @@ class _StripeSubscriptionPageState extends State<StripeSubscriptionPage> {
     bool foundActivePlan = false;
     for (final plan in _plans) {
       final productID = plan.stripeID;
-      if (productID == null || productID.isEmpty) {
+      if (productID.isEmpty) {
         continue;
       }
-      final isActive =
-          _hasActiveSubscription && _currentSubscription.productID == productID;
+      final isActive = _hasActiveSubscription &&
+          _currentSubscription!.productID == productID;
       if (isActive) {
         foundActivePlan = true;
       }
@@ -412,12 +410,12 @@ class _StripeSubscriptionPageState extends State<StripeSubscriptionPage> {
               // payment providers
               if (!_isStripeSubscriber &&
                   _hasActiveSubscription &&
-                  _currentSubscription.productID != freeProductID) {
+                  _currentSubscription!.productID != freeProductID) {
                 showErrorDialog(
                   context,
                   "Sorry",
                   "Please cancel your existing subscription from "
-                      "${_currentSubscription.paymentProvider} first",
+                      "${_currentSubscription!.paymentProvider} first",
                 );
                 return;
               }
@@ -515,13 +513,13 @@ class _StripeSubscriptionPageState extends State<StripeSubscriptionPage> {
   void _addCurrentPlanWidget(List<Widget> planWidgets) {
     // don't add current plan if it's monthly plan but UI is showing yearly plans
     // and vice versa.
-    if (_showYearlyPlan != _currentSubscription.isYearlyPlan() &&
-        _currentSubscription.productID != freeProductID) {
+    if (_showYearlyPlan != _currentSubscription!.isYearlyPlan() &&
+        _currentSubscription!.productID != freeProductID) {
       return;
     }
     int activePlanIndex = 0;
     for (; activePlanIndex < _plans.length; activePlanIndex++) {
-      if (_plans[activePlanIndex].storage > _currentSubscription.storage) {
+      if (_plans[activePlanIndex].storage > _currentSubscription!.storage) {
         break;
       }
     }
@@ -531,9 +529,9 @@ class _StripeSubscriptionPageState extends State<StripeSubscriptionPage> {
         child: InkWell(
           onTap: () {},
           child: SubscriptionPlanWidget(
-            storage: _currentSubscription.storage,
-            price: _currentSubscription.price,
-            period: _currentSubscription.period,
+            storage: _currentSubscription!.storage,
+            price: _currentSubscription!.price,
+            period: _currentSubscription!.period,
             isActive: true,
           ),
         ),

+ 14 - 14
lib/ui/payment/subscription_common_widgets.dart

@@ -1,4 +1,4 @@
-// @dart=2.9
+
 
 import 'package:flutter/material.dart';
 import 'package:photos/ente_theme_data.dart';
@@ -8,11 +8,11 @@ import 'package:photos/utils/data_util.dart';
 import 'package:photos/utils/date_time_util.dart';
 
 class SubscriptionHeaderWidget extends StatefulWidget {
-  final bool isOnboarding;
-  final int currentUsage;
+  final bool? isOnboarding;
+  final int? currentUsage;
 
   const SubscriptionHeaderWidget({
-    Key key,
+    Key? key,
     this.isOnboarding,
     this.currentUsage,
   }) : super(key: key);
@@ -26,7 +26,7 @@ class SubscriptionHeaderWidget extends StatefulWidget {
 class _SubscriptionHeaderWidgetState extends State<SubscriptionHeaderWidget> {
   @override
   Widget build(BuildContext context) {
-    if (widget.isOnboarding) {
+    if (widget.isOnboarding!) {
       return Padding(
         padding: const EdgeInsets.fromLTRB(20, 20, 20, 24),
         child: Column(
@@ -64,10 +64,10 @@ class _SubscriptionHeaderWidgetState extends State<SubscriptionHeaderWidget> {
                   style: Theme.of(context).textTheme.subtitle1,
                 ),
                 TextSpan(
-                  text: formatBytes(widget.currentUsage),
+                  text: formatBytes(widget.currentUsage!),
                   style: Theme.of(context)
                       .textTheme
-                      .subtitle1
+                      .subtitle1!
                       .copyWith(fontWeight: FontWeight.bold),
                 )
               ],
@@ -80,9 +80,9 @@ class _SubscriptionHeaderWidgetState extends State<SubscriptionHeaderWidget> {
 }
 
 class ValidityWidget extends StatelessWidget {
-  final Subscription currentSubscription;
+  final Subscription? currentSubscription;
 
-  const ValidityWidget({Key key, this.currentSubscription}) : super(key: key);
+  const ValidityWidget({Key? key, this.currentSubscription}) : super(key: key);
 
   @override
   Widget build(BuildContext context) {
@@ -90,12 +90,12 @@ class ValidityWidget extends StatelessWidget {
       return const SizedBox.shrink();
     }
     final endDate = getDateAndMonthAndYear(
-      DateTime.fromMicrosecondsSinceEpoch(currentSubscription.expiryTime),
+      DateTime.fromMicrosecondsSinceEpoch(currentSubscription!.expiryTime),
     );
     var message = "Renews on $endDate";
-    if (currentSubscription.productID == freeProductID) {
+    if (currentSubscription!.productID == freeProductID) {
       message = "Free trial valid till $endDate";
-    } else if (currentSubscription.attributes?.isCancelled ?? false) {
+    } else if (currentSubscription!.attributes?.isCancelled ?? false) {
       message = "Your subscription will be cancelled on $endDate";
     }
     return Padding(
@@ -109,7 +109,7 @@ class ValidityWidget extends StatelessWidget {
 }
 
 class SubFaqWidget extends StatelessWidget {
-  const SubFaqWidget({Key key}) : super(key: key);
+  const SubFaqWidget({Key? key}) : super(key: key);
 
   @override
   Widget build(BuildContext context) {
@@ -132,7 +132,7 @@ class SubFaqWidget extends StatelessWidget {
           child: RichText(
             text: TextSpan(
               text: "Questions?",
-              style: Theme.of(context).textTheme.bodyMedium.copyWith(
+              style: Theme.of(context).textTheme.bodyMedium!.copyWith(
                     decoration: TextDecoration.underline,
                   ),
             ),

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

@@ -1,5 +1,3 @@
-// @dart=2.9
-
 import 'dart:async';
 import 'dart:io';
 
@@ -29,7 +27,7 @@ class SubscriptionPage extends StatefulWidget {
 
   const SubscriptionPage({
     this.isOnboarding = false,
-    Key key,
+    Key? key,
   }) : super(key: key);
 
   @override
@@ -40,16 +38,16 @@ class _SubscriptionPageState extends State<SubscriptionPage> {
   final _logger = Logger("SubscriptionPage");
   final _billingService = BillingService.instance;
   final _userService = UserService.instance;
-  Subscription _currentSubscription;
-  StreamSubscription _purchaseUpdateSubscription;
-  ProgressDialog _dialog;
-  UserDetails _userDetails;
-  bool _hasActiveSubscription;
-  FreePlan _freePlan;
-  List<BillingPlan> _plans;
+  Subscription? _currentSubscription;
+  late StreamSubscription _purchaseUpdateSubscription;
+  late ProgressDialog _dialog;
+  late UserDetails _userDetails;
+  late bool _hasActiveSubscription;
+  late FreePlan _freePlan;
+  late List<BillingPlan> _plans;
   bool _hasLoadedData = false;
   bool _isLoading = false;
-  bool _isActiveStripeSubscriber;
+  late bool _isActiveStripeSubscriber;
 
   @override
   void initState() {
@@ -76,9 +74,9 @@ class _SubscriptionPageState extends State<SubscriptionPage> {
             String text = "Thank you for subscribing!";
             if (!widget.isOnboarding) {
               final isUpgrade = _hasActiveSubscription &&
-                  newSubscription.storage > _currentSubscription.storage;
+                  newSubscription.storage > _currentSubscription!.storage;
               final isDowngrade = _hasActiveSubscription &&
-                  newSubscription.storage < _currentSubscription.storage;
+                  newSubscription.storage < _currentSubscription!.storage;
               if (isUpgrade) {
                 text = "Your plan was successfully upgraded";
               } else if (isDowngrade) {
@@ -87,7 +85,7 @@ class _SubscriptionPageState extends State<SubscriptionPage> {
             }
             showShortToast(context, text);
             _currentSubscription = newSubscription;
-            _hasActiveSubscription = _currentSubscription.isValid();
+            _hasActiveSubscription = _currentSubscription!.isValid();
             setState(() {});
             await _dialog.hide();
             Bus.instance.fire(SubscriptionPurchasedEvent());
@@ -155,18 +153,18 @@ class _SubscriptionPageState extends State<SubscriptionPage> {
     _userService.getUserDetailsV2(memoryCount: false).then((userDetails) async {
       _userDetails = userDetails;
       _currentSubscription = userDetails.subscription;
-      _hasActiveSubscription = _currentSubscription.isValid();
+      _hasActiveSubscription = _currentSubscription!.isValid();
       final billingPlans = await _billingService.getBillingPlans();
       _isActiveStripeSubscriber =
-          _currentSubscription.paymentProvider == stripe &&
-              _currentSubscription.isValid();
+          _currentSubscription!.paymentProvider == stripe &&
+              _currentSubscription!.isValid();
       _plans = billingPlans.plans.where((plan) {
         final productID = _isActiveStripeSubscriber
             ? plan.stripeID
             : Platform.isAndroid
                 ? plan.androidID
                 : plan.iosID;
-        return productID != null && productID.isNotEmpty;
+        return productID.isNotEmpty;
       }).toList();
       _freePlan = billingPlans.freePlan;
       _hasLoadedData = true;
@@ -210,7 +208,7 @@ class _SubscriptionPageState extends State<SubscriptionPage> {
       widgets.add(ValidityWidget(currentSubscription: _currentSubscription));
     }
 
-    if (_currentSubscription.productID == freeProductID) {
+    if (_currentSubscription!.productID == freeProductID) {
       if (widget.isOnboarding) {
         widgets.add(SkipSubscriptionWidget(freePlan: _freePlan));
       }
@@ -218,20 +216,20 @@ class _SubscriptionPageState extends State<SubscriptionPage> {
     }
 
     if (_hasActiveSubscription &&
-        _currentSubscription.productID != freeProductID) {
+        _currentSubscription!.productID != freeProductID) {
       widgets.addAll([
         Align(
           alignment: Alignment.center,
           child: GestureDetector(
             onTap: () {
               final String paymentProvider =
-                  _currentSubscription.paymentProvider;
+                  _currentSubscription!.paymentProvider;
               if (paymentProvider == appStore && !Platform.isAndroid) {
                 launchUrlString("https://apps.apple.com/account/billing");
               } else if (paymentProvider == playStore && Platform.isAndroid) {
                 launchUrlString(
                   "https://play.google.com/store/account/subscriptions?sku=" +
-                      _currentSubscription.productID +
+                      _currentSubscription!.productID +
                       "&package=io.ente.photos",
                 );
               } else if (paymentProvider == stripe) {
@@ -294,7 +292,7 @@ class _SubscriptionPageState extends State<SubscriptionPage> {
                   RichText(
                     text: TextSpan(
                       text: "Manage family",
-                      style: Theme.of(context).textTheme.bodyMedium.copyWith(
+                      style: Theme.of(context).textTheme.bodyMedium!.copyWith(
                             decoration: TextDecoration.underline,
                           ),
                     ),
@@ -320,11 +318,11 @@ class _SubscriptionPageState extends State<SubscriptionPage> {
     bool foundActivePlan = false;
     for (final plan in _plans) {
       final productID = plan.stripeID;
-      if (productID == null || productID.isEmpty) {
+      if (productID.isEmpty) {
         continue;
       }
-      final isActive =
-          _hasActiveSubscription && _currentSubscription.productID == productID;
+      final isActive = _hasActiveSubscription &&
+          _currentSubscription!.productID == productID;
       if (isActive) {
         foundActivePlan = true;
       }
@@ -362,7 +360,7 @@ class _SubscriptionPageState extends State<SubscriptionPage> {
     bool foundActivePlan = false;
     final List<Widget> planWidgets = [];
     if (_hasActiveSubscription &&
-        _currentSubscription.productID == freeProductID) {
+        _currentSubscription!.productID == freeProductID) {
       foundActivePlan = true;
       planWidgets.add(
         SubscriptionPlanWidget(
@@ -375,8 +373,8 @@ class _SubscriptionPageState extends State<SubscriptionPage> {
     }
     for (final plan in _plans) {
       final productID = Platform.isAndroid ? plan.androidID : plan.iosID;
-      final isActive =
-          _hasActiveSubscription && _currentSubscription.productID == productID;
+      final isActive = _hasActiveSubscription &&
+          _currentSubscription!.productID == productID;
       if (isActive) {
         foundActivePlan = true;
       }
@@ -408,8 +406,8 @@ class _SubscriptionPageState extends State<SubscriptionPage> {
               }
               final isCrossGradingOnAndroid = Platform.isAndroid &&
                   _hasActiveSubscription &&
-                  _currentSubscription.productID != freeProductID &&
-                  _currentSubscription.productID != plan.androidID;
+                  _currentSubscription!.productID != freeProductID &&
+                  _currentSubscription!.productID != plan.androidID;
               if (isCrossGradingOnAndroid) {
                 await _dialog.hide();
                 showErrorDialog(
@@ -445,7 +443,7 @@ class _SubscriptionPageState extends State<SubscriptionPage> {
   void _addCurrentPlanWidget(List<Widget> planWidgets) {
     int activePlanIndex = 0;
     for (; activePlanIndex < _plans.length; activePlanIndex++) {
-      if (_plans[activePlanIndex].storage > _currentSubscription.storage) {
+      if (_plans[activePlanIndex].storage > _currentSubscription!.storage) {
         break;
       }
     }
@@ -455,9 +453,9 @@ class _SubscriptionPageState extends State<SubscriptionPage> {
         child: InkWell(
           onTap: () {},
           child: SubscriptionPlanWidget(
-            storage: _currentSubscription.storage,
-            price: _currentSubscription.price,
-            period: _currentSubscription.period,
+            storage: _currentSubscription!.storage,
+            price: _currentSubscription!.price,
+            period: _currentSubscription!.period,
             isActive: true,
           ),
         ),

+ 7 - 7
lib/ui/payment/subscription_plan_widget.dart

@@ -1,14 +1,14 @@
-// @dart=2.9
+
 
 import 'package:flutter/material.dart';
 import 'package:photos/utils/data_util.dart';
 
 class SubscriptionPlanWidget extends StatelessWidget {
   const SubscriptionPlanWidget({
-    Key key,
-    @required this.storage,
-    @required this.price,
-    @required this.period,
+    Key? key,
+    required this.storage,
+    required this.price,
+    required this.period,
     this.isActive = false,
   }) : super(key: key);
 
@@ -57,12 +57,12 @@ class SubscriptionPlanWidget extends StatelessWidget {
                   convertBytesToReadableFormat(storage),
                   style: Theme.of(context)
                       .textTheme
-                      .headline6
+                      .headline6!
                       .copyWith(color: textColor),
                 ),
                 Text(
                   _displayPrice(),
-                  style: Theme.of(context).textTheme.headline6.copyWith(
+                  style: Theme.of(context).textTheme.headline6!.copyWith(
                         color: textColor,
                         fontWeight: FontWeight.normal,
                       ),

+ 3 - 1
lib/ui/settings/about_section_widget.dart

@@ -78,7 +78,9 @@ class AboutSectionWidget extends StatelessWidget {
                         );
                       } else {
                         showShortToast(
-                            context, "You are on the latest version");
+                          context,
+                          "You are on the latest version",
+                        );
                       }
                     },
                   ),

+ 15 - 15
lib/ui/settings/app_update_dialog.dart

@@ -1,4 +1,4 @@
-// @dart=2.9
+
 
 import 'package:flutter/material.dart';
 import 'package:logging/logging.dart';
@@ -11,9 +11,9 @@ import 'package:photos/theme/ente_theme.dart';
 import 'package:url_launcher/url_launcher_string.dart';
 
 class AppUpdateDialog extends StatefulWidget {
-  final LatestVersionInfo latestVersionInfo;
+  final LatestVersionInfo? latestVersionInfo;
 
-  const AppUpdateDialog(this.latestVersionInfo, {Key key}) : super(key: key);
+  const AppUpdateDialog(this.latestVersionInfo, {Key? key}) : super(key: key);
 
   @override
   State<AppUpdateDialog> createState() => _AppUpdateDialogState();
@@ -25,7 +25,7 @@ class _AppUpdateDialogState extends State<AppUpdateDialog> {
     final List<Widget> changelog = [];
     final enteTextTheme = getEnteTextTheme(context);
     final enteColor = getEnteColorScheme(context);
-    for (final log in widget.latestVersionInfo.changelog) {
+    for (final log in widget.latestVersionInfo!.changelog) {
       changelog.add(
         Padding(
           padding: const EdgeInsets.fromLTRB(0, 4, 0, 4),
@@ -68,7 +68,7 @@ class _AppUpdateDialogState extends State<AppUpdateDialog> {
           width: double.infinity,
           height: 56,
           child: OutlinedButton(
-            style: Theme.of(context).outlinedButtonTheme.style.copyWith(
+            style: Theme.of(context).outlinedButtonTheme.style!.copyWith(
               textStyle: MaterialStateProperty.resolveWith<TextStyle>(
                 (Set<MaterialState> states) {
                   return enteTextTheme.bodyBold;
@@ -97,11 +97,11 @@ class _AppUpdateDialogState extends State<AppUpdateDialog> {
               "Install manually",
               style: Theme.of(context)
                   .textTheme
-                  .caption
+                  .caption!
                   .copyWith(decoration: TextDecoration.underline),
             ),
             onTap: () => launchUrlString(
-              widget.latestVersionInfo.url,
+              widget.latestVersionInfo!.url,
               mode: LaunchMode.externalApplication,
             ),
           ),
@@ -109,7 +109,7 @@ class _AppUpdateDialogState extends State<AppUpdateDialog> {
       ],
     );
     final shouldForceUpdate =
-        UpdateService.instance.shouldForceUpdate(widget.latestVersionInfo);
+        UpdateService.instance.shouldForceUpdate(widget.latestVersionInfo!);
     return WillPopScope(
       onWillPop: () async => !shouldForceUpdate,
       child: AlertDialog(
@@ -139,24 +139,24 @@ class _AppUpdateDialogState extends State<AppUpdateDialog> {
 }
 
 class ApkDownloaderDialog extends StatefulWidget {
-  final LatestVersionInfo versionInfo;
+  final LatestVersionInfo? versionInfo;
 
-  const ApkDownloaderDialog(this.versionInfo, {Key key}) : super(key: key);
+  const ApkDownloaderDialog(this.versionInfo, {Key? key}) : super(key: key);
 
   @override
   State<ApkDownloaderDialog> createState() => _ApkDownloaderDialogState();
 }
 
 class _ApkDownloaderDialogState extends State<ApkDownloaderDialog> {
-  String _saveUrl;
-  double _downloadProgress;
+  String? _saveUrl;
+  double? _downloadProgress;
 
   @override
   void initState() {
     super.initState();
     _saveUrl = Configuration.instance.getTempDirectory() +
         "ente-" +
-        widget.versionInfo.name +
+        widget.versionInfo!.name +
         ".apk";
     _downloadApk();
   }
@@ -186,11 +186,11 @@ class _ApkDownloaderDialogState extends State<ApkDownloaderDialog> {
   Future<void> _downloadApk() async {
     try {
       await Network.instance.getDio().download(
-        widget.versionInfo.url,
+        widget.versionInfo!.url,
         _saveUrl,
         onReceiveProgress: (count, _) {
           setState(() {
-            _downloadProgress = count / widget.versionInfo.size;
+            _downloadProgress = count / widget.versionInfo!.size;
           });
         },
       );

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

@@ -1,4 +1,4 @@
-// @dart=2.9
+
 
 import 'dart:io';
 
@@ -26,7 +26,7 @@ import 'package:photos/utils/toast_util.dart';
 import 'package:url_launcher/url_launcher_string.dart';
 
 class BackupSectionWidget extends StatefulWidget {
-  const BackupSectionWidget({Key key}) : super(key: key);
+  const BackupSectionWidget({Key? key}) : super(key: key);
 
   @override
   BackupSectionWidgetState createState() => BackupSectionWidgetState();
@@ -108,7 +108,7 @@ class BackupSectionWidgetState extends State<BackupSectionWidget> {
                 "You've no files on this device that can be deleted",
               );
             } else {
-              final bool result =
+              final bool? result =
                   await routeToPage(context, FreeSpacePage(status));
               if (result == true) {
                 _showSpaceFreedDialog(status);
@@ -145,7 +145,7 @@ class BackupSectionWidgetState extends State<BackupSectionWidget> {
                 "You've no duplicate files that can be cleared",
               );
             } else {
-              final DeduplicationResult result =
+              final DeduplicationResult? result =
                   await routeToPage(context, DeduplicatePage(duplicates));
               if (result != null) {
                 _showDuplicateFilesDeletedDialog(result);

+ 0 - 2
lib/ui/settings/settings_title_bar_widget.dart

@@ -1,6 +1,5 @@
 import 'package:flutter/material.dart';
 import 'package:intl/intl.dart';
-import 'package:logging/logging.dart';
 import 'package:photos/models/user_details.dart';
 import 'package:photos/states/user_details_state.dart';
 import 'package:photos/theme/ente_theme.dart';
@@ -11,7 +10,6 @@ class SettingsTitleBarWidget extends StatelessWidget {
 
   @override
   Widget build(BuildContext context) {
-    final logger = Logger((SettingsTitleBarWidget).toString());
     final inheritedDetails = InheritedUserDetails.of(context);
     final userDetails = inheritedDetails?.userDetails;
     bool isCached = false;

+ 0 - 1
lib/ui/settings/social_section_widget.dart

@@ -32,7 +32,6 @@ class SocialSectionWidget extends StatelessWidget {
     );
     options.addAll(
       [
-        sectionOptionSpacing,
         const SocialsMenuItemWidget("Blog", "https://ente.io/blog"),
         sectionOptionSpacing,
         const SocialsMenuItemWidget("Twitter", "https://twitter.com/enteio"),

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

@@ -1,4 +1,4 @@
-// @dart=2.9
+
 
 import 'package:adaptive_theme/adaptive_theme.dart';
 import 'package:flutter/material.dart';
@@ -11,14 +11,14 @@ import 'package:photos/ui/components/menu_item_widget.dart';
 import 'package:photos/ui/settings/common_settings.dart';
 
 class ThemeSwitchWidget extends StatefulWidget {
-  const ThemeSwitchWidget({Key key}) : super(key: key);
+  const ThemeSwitchWidget({Key? key}) : super(key: key);
 
   @override
   State<ThemeSwitchWidget> createState() => _ThemeSwitchWidgetState();
 }
 
 class _ThemeSwitchWidgetState extends State<ThemeSwitchWidget> {
-  AdaptiveThemeMode currentThemeMode;
+  AdaptiveThemeMode? currentThemeMode;
 
   @override
   void initState() {
@@ -67,7 +67,7 @@ class _ThemeSwitchWidgetState extends State<ThemeSwitchWidget> {
   Widget _menuItem(BuildContext context, AdaptiveThemeMode themeMode) {
     return MenuItemWidget(
       captionedTextWidget: CaptionedTextWidget(
-        title: toBeginningOfSentenceCase(themeMode.name),
+        title: toBeginningOfSentenceCase(themeMode.name)!,
         textStyle: Theme.of(context).colorScheme.enteTheme.textTheme.body,
       ),
       pressedColor: getEnteColorScheme(context).fillFaint,

+ 5 - 5
lib/ui/settings_page.dart

@@ -1,4 +1,4 @@
-// @dart=2.9
+
 
 import 'dart:io';
 
@@ -24,8 +24,8 @@ import 'package:photos/ui/settings/support_section_widget.dart';
 import 'package:photos/ui/settings/theme_switch_widget.dart';
 
 class SettingsPage extends StatelessWidget {
-  final ValueNotifier<String> emailNotifier;
-  const SettingsPage({Key key, @required this.emailNotifier}) : super(key: key);
+  final ValueNotifier<String?> emailNotifier;
+  const SettingsPage({Key? key, required this.emailNotifier}) : super(key: key);
 
   @override
   Widget build(BuildContext context) {
@@ -54,9 +54,9 @@ class SettingsPage extends StatelessWidget {
           child: AnimatedBuilder(
             // [AnimatedBuilder] accepts any [Listenable] subtype.
             animation: emailNotifier,
-            builder: (BuildContext context, Widget child) {
+            builder: (BuildContext context, Widget? child) {
               return Text(
-                emailNotifier.value,
+                emailNotifier.value!,
                 style: enteTextTheme.body.copyWith(
                   color: colorScheme.textMuted,
                   overflow: TextOverflow.ellipsis,

+ 28 - 28
lib/ui/shared_collections_gallery.dart

@@ -1,4 +1,4 @@
-// @dart=2.9
+
 
 import 'dart:async';
 import 'dart:math';
@@ -28,7 +28,7 @@ import 'package:photos/utils/share_util.dart';
 import 'package:photos/utils/toast_util.dart';
 
 class SharedCollectionGallery extends StatefulWidget {
-  const SharedCollectionGallery({Key key}) : super(key: key);
+  const SharedCollectionGallery({Key? key}) : super(key: key);
 
   @override
   State<SharedCollectionGallery> createState() =>
@@ -38,9 +38,9 @@ class SharedCollectionGallery extends StatefulWidget {
 class _SharedCollectionGalleryState extends State<SharedCollectionGallery>
     with AutomaticKeepAliveClientMixin {
   final Logger _logger = Logger("SharedCollectionGallery");
-  StreamSubscription<LocalPhotosUpdatedEvent> _localFilesSubscription;
-  StreamSubscription<CollectionUpdatedEvent> _collectionUpdatesSubscription;
-  StreamSubscription<UserLoggedOutEvent> _loggedOutEvent;
+  late StreamSubscription<LocalPhotosUpdatedEvent> _localFilesSubscription;
+  late StreamSubscription<CollectionUpdatedEvent> _collectionUpdatesSubscription;
+  late StreamSubscription<UserLoggedOutEvent> _loggedOutEvent;
 
   @override
   void initState() {
@@ -71,10 +71,10 @@ class _SharedCollectionGalleryState extends State<SharedCollectionGallery>
         final List<CollectionWithThumbnail> incoming = [];
         for (final file in files) {
           final c =
-              CollectionsService.instance.getCollectionByID(file.collectionID);
-          if (c.owner.id == Configuration.instance.getUserID()) {
-            if (c.sharees.isNotEmpty ||
-                c.publicURLs.isNotEmpty ||
+              CollectionsService.instance.getCollectionByID(file.collectionID!)!;
+          if (c.owner!.id == Configuration.instance.getUserID()) {
+            if (c.sharees!.isNotEmpty ||
+                c.publicURLs!.isNotEmpty ||
                 c.isSharedFilesCollection()) {
               outgoing.add(
                 CollectionWithThumbnail(
@@ -112,7 +112,7 @@ class _SharedCollectionGalleryState extends State<SharedCollectionGallery>
       }),
       builder: (context, snapshot) {
         if (snapshot.hasData) {
-          return _getSharedCollectionsGallery(snapshot.data);
+          return _getSharedCollectionsGallery(snapshot.data!);
         } else if (snapshot.hasError) {
           _logger.shout(snapshot.error);
           return Center(child: Text(snapshot.error.toString()));
@@ -264,20 +264,20 @@ class OutgoingCollectionItem extends StatelessWidget {
 
   const OutgoingCollectionItem(
     this.c, {
-    Key key,
+    Key? key,
   }) : super(key: key);
 
   @override
   Widget build(BuildContext context) {
-    final sharees = <String>[];
-    for (int index = 0; index < c.collection.sharees.length; index++) {
-      final sharee = c.collection.sharees[index];
+    final sharees = <String?>[];
+    for (int index = 0; index < c.collection.sharees!.length; index++) {
+      final sharee = c.collection.sharees![index]!;
       final name =
           (sharee.name?.isNotEmpty ?? false) ? sharee.name : sharee.email;
       if (index < 2) {
         sharees.add(name);
       } else {
-        final remaining = c.collection.sharees.length - index;
+        final remaining = c.collection.sharees!.length - index;
         if (remaining == 1) {
           // If it's the last sharee
           sharees.add(name);
@@ -304,10 +304,10 @@ class OutgoingCollectionItem extends StatelessWidget {
                 height: 60,
                 width: 60,
                 child: Hero(
-                  tag: "outgoing_collection" + c.thumbnail.tag,
+                  tag: "outgoing_collection" + c.thumbnail!.tag,
                   child: ThumbnailWidget(
                     c.thumbnail,
-                    key: Key("outgoing_collection" + c.thumbnail.tag),
+                    key: Key("outgoing_collection" + c.thumbnail!.tag),
                   ),
                 ),
               ),
@@ -320,15 +320,15 @@ class OutgoingCollectionItem extends StatelessWidget {
                   Row(
                     children: [
                       Text(
-                        c.collection.name,
+                        c.collection.name!,
                         style: const TextStyle(
                           fontSize: 16,
                         ),
                       ),
                       const Padding(padding: EdgeInsets.all(2)),
-                      c.collection.publicURLs.isEmpty
+                      c.collection.publicURLs!.isEmpty
                           ? Container()
-                          : (c.collection.publicURLs.first.isExpired
+                          : (c.collection.publicURLs!.first!.isExpired
                               ? const Icon(
                                   Icons.link,
                                   color: warning500,
@@ -373,7 +373,7 @@ class IncomingCollectionItem extends StatelessWidget {
 
   const IncomingCollectionItem(
     this.c, {
-    Key key,
+    Key? key,
   }) : super(key: key);
 
   @override
@@ -381,7 +381,7 @@ class IncomingCollectionItem extends StatelessWidget {
     const double horizontalPaddingOfGridRow = 16;
     const double crossAxisSpacingOfGrid = 9;
     final TextStyle albumTitleTextStyle =
-        Theme.of(context).textTheme.subtitle1.copyWith(fontSize: 14);
+        Theme.of(context).textTheme.subtitle1!.copyWith(fontSize: 14);
     final Size size = MediaQuery.of(context).size;
     final int albumsCountInOneRow = max(size.width ~/ 220.0, 2);
     final double totalWhiteSpaceOfRow = (horizontalPaddingOfGridRow * 2) +
@@ -400,10 +400,10 @@ class IncomingCollectionItem extends StatelessWidget {
               child: Stack(
                 children: [
                   Hero(
-                    tag: "shared_collection" + c.thumbnail.tag,
+                    tag: "shared_collection" + c.thumbnail!.tag,
                     child: ThumbnailWidget(
                       c.thumbnail,
-                      key: Key("shared_collection" + c.thumbnail.tag),
+                      key: Key("shared_collection" + c.thumbnail!.tag),
                     ),
                   ),
                   Align(
@@ -411,7 +411,7 @@ class IncomingCollectionItem extends StatelessWidget {
                     child: Padding(
                       padding: const EdgeInsets.only(right: 8.0, bottom: 8.0),
                       child: UserAvatarWidget(
-                        c.collection.owner,
+                        c.collection.owner!,
                         thumbnailView: true,
                       ),
                     ),
@@ -426,7 +426,7 @@ class IncomingCollectionItem extends StatelessWidget {
               Container(
                 constraints: BoxConstraints(maxWidth: sideOfThumbnail - 40),
                 child: Text(
-                  c.collection.name,
+                  c.collection.name!,
                   style: albumTitleTextStyle,
                   overflow: TextOverflow.ellipsis,
                 ),
@@ -434,11 +434,11 @@ class IncomingCollectionItem extends StatelessWidget {
               FutureBuilder<int>(
                 future: FilesDB.instance.collectionFileCount(c.collection.id),
                 builder: (context, snapshot) {
-                  if (snapshot.hasData && snapshot.data > 0) {
+                  if (snapshot.hasData && snapshot.data! > 0) {
                     return RichText(
                       text: TextSpan(
                         style: albumTitleTextStyle.copyWith(
-                          color: albumTitleTextStyle.color.withOpacity(0.5),
+                          color: albumTitleTextStyle.color!.withOpacity(0.5),
                         ),
                         children: [
                           const TextSpan(text: "  \u2022  "),

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

@@ -313,7 +313,7 @@ class _AddParticipantPage extends State<AddParticipantPage> {
     }
     for (final c in CollectionsService.instance.getActiveCollections()) {
       if (c.owner?.id == ownerID) {
-        for (final User? u in c?.sharees ?? []) {
+        for (final User? u in c.sharees ?? []) {
           if (u != null && u.id != null && !existingUserIDs.contains(u.id)) {
             existingUserIDs.add(u.id!);
             suggestedUsers.add(u);

Algúns arquivos non se mostraron porque demasiados arquivos cambiaron neste cambio