浏览代码

Merge branch 'theme' of github.com:ente-io/frame into theme

Neeraj Gupta 3 年之前
父节点
当前提交
112948ee44

+ 128 - 14
lib/app.dart

@@ -1,5 +1,8 @@
+import 'dart:io';
+
 import 'package:adaptive_theme/adaptive_theme.dart';
 import 'package:adaptive_theme/adaptive_theme.dart';
 import 'package:background_fetch/background_fetch.dart';
 import 'package:background_fetch/background_fetch.dart';
+import 'package:flutter/foundation.dart';
 import 'package:flutter/material.dart';
 import 'package:flutter/material.dart';
 import 'package:flutter_easyloading/flutter_easyloading.dart';
 import 'package:flutter_easyloading/flutter_easyloading.dart';
 import 'package:flutter_gen/gen_l10n/app_localizations.dart';
 import 'package:flutter_gen/gen_l10n/app_localizations.dart';
@@ -15,25 +18,70 @@ final lightThemeData = ThemeData(
   fontFamily: 'Inter',
   fontFamily: 'Inter',
   brightness: Brightness.light,
   brightness: Brightness.light,
   hintColor: Colors.grey,
   hintColor: Colors.grey,
+  primaryColor: Colors.deepOrangeAccent,
   iconTheme: IconThemeData(color: Colors.black),
   iconTheme: IconThemeData(color: Colors.black),
   primaryIconTheme: IconThemeData(color: Colors.red, opacity: 1.0, size: 50.0),
   primaryIconTheme: IconThemeData(color: Colors.red, opacity: 1.0, size: 50.0),
   colorScheme: ColorScheme.light(primary: Colors.black),
   colorScheme: ColorScheme.light(primary: Colors.black),
   accentColor: Color.fromRGBO(45, 194, 98, 0.2),
   accentColor: Color.fromRGBO(45, 194, 98, 0.2),
   buttonColor: Color.fromRGBO(45, 194, 98, 1.0),
   buttonColor: Color.fromRGBO(45, 194, 98, 1.0),
-  buttonTheme: ButtonThemeData().copyWith(
-    buttonColor: Color.fromRGBO(45, 194, 98, 1.0),
+  outlinedButtonTheme: OutlinedButtonThemeData(
+    style: OutlinedButton.styleFrom(
+      shape: RoundedRectangleBorder(
+        borderRadius: BorderRadius.circular(8),
+      ),
+      padding: EdgeInsets.fromLTRB(50, 16, 50, 16),
+      alignment: Alignment.center,
+      textStyle: TextStyle(
+        fontWeight: FontWeight.w600,
+        fontFamily: 'Inter-SemiBold',
+        fontSize: 18,
+      ),
+    ).copyWith(
+      backgroundColor: MaterialStateProperty.resolveWith<Color>(
+        (Set<MaterialState> states) {
+          if (states.contains(MaterialState.disabled)) {
+            return Colors.grey.shade500;
+          }
+          return Colors.black;
+        },
+      ),
+      foregroundColor: MaterialStateProperty.resolveWith<Color>(
+        (Set<MaterialState> states) {
+          if (states.contains(MaterialState.disabled)) {
+            return Colors.white;
+          }
+          return Colors.white;
+        },
+      ),
+      alignment: Alignment.center,
+    ),
   ),
   ),
   toggleableActiveColor: Colors.red[400],
   toggleableActiveColor: Colors.red[400],
   scaffoldBackgroundColor: Colors.white,
   scaffoldBackgroundColor: Colors.white,
   bottomAppBarColor: Color.fromRGBO(196, 196, 196, 1.0),
   bottomAppBarColor: Color.fromRGBO(196, 196, 196, 1.0),
   backgroundColor: Colors.white,
   backgroundColor: Colors.white,
-  appBarTheme: AppBarTheme().copyWith(color: Colors.blue),
+  appBarTheme: AppBarTheme().copyWith(
+      backgroundColor: Colors.white,
+      iconTheme: IconThemeData(color: Colors.black)),
   //https://api.flutter.dev/flutter/material/TextTheme-class.html
   //https://api.flutter.dev/flutter/material/TextTheme-class.html
   textTheme: TextTheme().copyWith(
   textTheme: TextTheme().copyWith(
+      headline4: TextStyle(
+          fontFamily: 'Inter-SemiBold',
+          color: Colors.black,
+          fontSize: 32,
+          fontWeight: FontWeight.w600),
       headline6: TextStyle(
       headline6: TextStyle(
           color: Colors.black, fontSize: 18, fontWeight: FontWeight.w600),
           color: Colors.black, fontSize: 18, fontWeight: FontWeight.w600),
       subtitle1: TextStyle(
       subtitle1: TextStyle(
-          color: Colors.black, fontSize: 15, fontWeight: FontWeight.w500),
+          fontFamily: 'Inter-Medium',
+          color: Colors.black,
+          fontSize: 16,
+          fontWeight: FontWeight.w500),
+      bodyText1: TextStyle(
+          fontFamily: 'Inter-Medium',
+          color: Colors.black,
+          fontSize: 16,
+          fontWeight: FontWeight.w400),
       caption: TextStyle(color: Colors.black.withOpacity(0.7), fontSize: 14),
       caption: TextStyle(color: Colors.black.withOpacity(0.7), fontSize: 14),
       overline: TextStyle(color: Colors.black.withOpacity(0.8), fontSize: 12)),
       overline: TextStyle(color: Colors.black.withOpacity(0.8), fontSize: 12)),
 
 
@@ -41,7 +89,6 @@ final lightThemeData = ThemeData(
       bodyText2: TextStyle(color: Colors.yellow),
       bodyText2: TextStyle(color: Colors.yellow),
       bodyText1: TextStyle(color: Colors.orange)),
       bodyText1: TextStyle(color: Colors.orange)),
   cardColor: Color.fromRGBO(250, 250, 250, 1.0),
   cardColor: Color.fromRGBO(250, 250, 250, 1.0),
-  //
   dialogTheme: DialogTheme().copyWith(
   dialogTheme: DialogTheme().copyWith(
     backgroundColor: Color.fromRGBO(250, 250, 250, 1.0), //
     backgroundColor: Color.fromRGBO(250, 250, 250, 1.0), //
   ),
   ),
@@ -73,10 +120,23 @@ final darkThemeData = ThemeData(
   ),
   ),
   // primaryColor: Colors.red,
   // primaryColor: Colors.red,
   textTheme: TextTheme().copyWith(
   textTheme: TextTheme().copyWith(
+      headline4: TextStyle(
+          fontFamily: 'Inter-SemiBold',
+          color: Colors.white,
+          fontSize: 32,
+          fontWeight: FontWeight.w600),
       headline6: TextStyle(
       headline6: TextStyle(
           color: Colors.white, fontSize: 18, fontWeight: FontWeight.w600),
           color: Colors.white, fontSize: 18, fontWeight: FontWeight.w600),
       subtitle1: TextStyle(
       subtitle1: TextStyle(
-          color: Colors.white, fontSize: 16, fontWeight: FontWeight.w500),
+          color: Colors.white,
+          fontFamily: 'Inter-Medium',
+          fontSize: 16,
+          fontWeight: FontWeight.w500),
+      bodyText1: TextStyle(
+          fontFamily: 'Inter-Medium',
+          color: Colors.white,
+          fontSize: 16,
+          fontWeight: FontWeight.w400),
       caption: TextStyle(
       caption: TextStyle(
         color: Colors.white.withOpacity(0.6),
         color: Colors.white.withOpacity(0.6),
         fontSize: 14,
         fontSize: 14,
@@ -86,6 +146,38 @@ final darkThemeData = ThemeData(
         fontSize: 12,
         fontSize: 12,
       )),
       )),
   toggleableActiveColor: Colors.green[400],
   toggleableActiveColor: Colors.green[400],
+  outlinedButtonTheme: OutlinedButtonThemeData(
+    style: OutlinedButton.styleFrom(
+      shape: RoundedRectangleBorder(
+        borderRadius: BorderRadius.circular(8),
+      ),
+      alignment: Alignment.center,
+      padding: EdgeInsets.fromLTRB(50, 16, 50, 16),
+      textStyle: TextStyle(
+        fontWeight: FontWeight.w600,
+        fontFamily: 'Inter-SemiBold',
+        fontSize: 18,
+      ),
+    ).copyWith(
+      backgroundColor: MaterialStateProperty.resolveWith<Color>(
+        (Set<MaterialState> states) {
+          if (states.contains(MaterialState.disabled)) {
+            return Colors.grey.shade500;
+          }
+          return Colors.white;
+        },
+      ),
+      foregroundColor: MaterialStateProperty.resolveWith<Color>(
+        (Set<MaterialState> states) {
+          if (states.contains(MaterialState.disabled)) {
+            return Colors.white;
+          }
+          return Colors.black;
+        },
+      ),
+      alignment: Alignment.center,
+    ),
+  ),
   scaffoldBackgroundColor: Colors.black,
   scaffoldBackgroundColor: Colors.black,
   backgroundColor: Colors.black,
   backgroundColor: Colors.black,
   appBarTheme: AppBarTheme().copyWith(
   appBarTheme: AppBarTheme().copyWith(
@@ -107,11 +199,6 @@ final darkThemeData = ThemeData(
   ),
   ),
 );
 );
 
 
-extension CustomColorScheme on ColorScheme {
-  Color get defaultTextColor =>
-      brightness == Brightness.light ? Colors.black : Colors.white;
-}
-
 class EnteApp extends StatefulWidget {
 class EnteApp extends StatefulWidget {
   static const _homeWidget = HomeWidget();
   static const _homeWidget = HomeWidget();
 
 
@@ -133,19 +220,44 @@ class _EnteAppState extends State<EnteApp> with WidgetsBindingObserver {
 
 
   @override
   @override
   void initState() {
   void initState() {
+    _logger.info('init App');
     super.initState();
     super.initState();
     WidgetsBinding.instance.addObserver(this);
     WidgetsBinding.instance.addObserver(this);
     _configureBackgroundFetch();
     _configureBackgroundFetch();
   }
   }
 
 
+  Widget debugBuild(BuildContext context) {
+    return MaterialApp(
+      title: "ente",
+      themeMode: ThemeMode.system,
+      theme: lightThemeData,
+      darkTheme: darkThemeData,
+      home: EnteApp._homeWidget,
+      debugShowCheckedModeBanner: false,
+      navigatorKey: Network.instance.getAlice().getNavigatorKey(),
+      builder: EasyLoading.init(),
+      supportedLocales: L10n.all,
+      localizationsDelegates: const [
+        AppLocalizations.delegate,
+        GlobalMaterialLocalizations.delegate,
+        GlobalCupertinoLocalizations.delegate,
+        GlobalWidgetsLocalizations.delegate,
+      ],
+    );
+  }
+
   @override
   @override
   Widget build(BuildContext context) {
   Widget build(BuildContext context) {
+    if (kDebugMode && Platform.isIOS) {
+      return debugBuild(context);
+    }
     return AdaptiveTheme(
     return AdaptiveTheme(
         light: lightThemeData,
         light: lightThemeData,
         dark: darkThemeData,
         dark: darkThemeData,
         initial: AdaptiveThemeMode.dark,
         initial: AdaptiveThemeMode.dark,
         builder: (lightTheme, dartTheme) => MaterialApp(
         builder: (lightTheme, dartTheme) => MaterialApp(
               title: "ente",
               title: "ente",
+              themeMode: ThemeMode.system,
               theme: lightTheme,
               theme: lightTheme,
               darkTheme: dartTheme,
               darkTheme: dartTheme,
               home: EnteApp._homeWidget,
               home: EnteApp._homeWidget,
@@ -170,11 +282,13 @@ class _EnteAppState extends State<EnteApp> with WidgetsBindingObserver {
 
 
   @override
   @override
   void didChangeAppLifecycleState(AppLifecycleState state) {
   void didChangeAppLifecycleState(AppLifecycleState state) {
+    final String stateChangeReason = 'app -> $state';
     if (state == AppLifecycleState.resumed) {
     if (state == AppLifecycleState.resumed) {
-      AppLifecycleService.instance.onAppInForeground();
+      AppLifecycleService.instance
+          .onAppInForeground(stateChangeReason + ': sync now');
       SyncService.instance.sync();
       SyncService.instance.sync();
     } else {
     } else {
-      AppLifecycleService.instance.onAppInBackground();
+      AppLifecycleService.instance.onAppInBackground(stateChangeReason);
     }
     }
   }
   }
 
 
@@ -194,7 +308,7 @@ class _EnteAppState extends State<EnteApp> with WidgetsBindingObserver {
         ), (String taskId) async {
         ), (String taskId) async {
       await widget.runBackgroundTask(taskId);
       await widget.runBackgroundTask(taskId);
     }, (taskId) {
     }, (taskId) {
-      _logger.info("BG task timeout");
+      _logger.info("BG task timeout taskID: $taskId");
       widget.killBackgroundTask(taskId);
       widget.killBackgroundTask(taskId);
     }).then((int status) {
     }).then((int status) {
       _logger.info('[BackgroundFetch] configure success: $status');
       _logger.info('[BackgroundFetch] configure success: $status');

+ 1 - 1
lib/core/configuration.dart

@@ -75,7 +75,7 @@ class Configuration {
   String _volatilePassword;
   String _volatilePassword;
 
 
   final _secureStorageOptionsIOS =
   final _secureStorageOptionsIOS =
-      IOSOptions(accessibility: IOSAccessibility.first_unlock_this_device);
+      IOSOptions(accessibility: IOSAccessibility.first_unlock);
 
 
   Future<void> init() async {
   Future<void> init() async {
     _preferences = await SharedPreferences.getInstance();
     _preferences = await SharedPreferences.getInstance();

+ 3 - 0
lib/core/constants.dart

@@ -24,3 +24,6 @@ const kThumbnailServerLoadDeferDuration = Duration(milliseconds: 80);
 // 256 bit key maps to 24 words
 // 256 bit key maps to 24 words
 // https://github.com/bitcoin/bips/blob/master/bip-0039.mediawiki#Generating_the_mnemonic
 // https://github.com/bitcoin/bips/blob/master/bip-0039.mediawiki#Generating_the_mnemonic
 const kMnemonicKeyWordCount = 24;
 const kMnemonicKeyWordCount = 24;
+
+// https://stackoverflow.com/a/61162219
+const kDragSensitivity = 8;

+ 21 - 1
lib/db/collections_db.dart

@@ -27,6 +27,9 @@ class CollectionsDB {
   static final columnVersion = 'version';
   static final columnVersion = 'version';
   static final columnSharees = 'sharees';
   static final columnSharees = 'sharees';
   static final columnPublicURLs = 'public_urls';
   static final columnPublicURLs = 'public_urls';
+  // MMD -> Magic Metadata
+  static final columnMMdEncodedJson = 'mmd_encoded_json';
+  static final columnMMdVersion = 'mmd_ver';
   static final columnUpdationTime = 'updation_time';
   static final columnUpdationTime = 'updation_time';
   static final columnIsDeleted = 'is_deleted';
   static final columnIsDeleted = 'is_deleted';
 
 
@@ -37,6 +40,7 @@ class CollectionsDB {
     ...addVersion(),
     ...addVersion(),
     ...addIsDeleted(),
     ...addIsDeleted(),
     ...addPublicURLs(),
     ...addPublicURLs(),
+    ...addPrivateMetadata(),
   ];
   ];
 
 
   final dbConfig = MigrationConfig(
   final dbConfig = MigrationConfig(
@@ -138,6 +142,17 @@ class CollectionsDB {
     ];
     ];
   }
   }
 
 
+  static List<String> addPrivateMetadata() {
+    return [
+      '''
+        ALTER TABLE $table ADD COLUMN $columnMMdEncodedJson TEXT DEFAULT '{}';
+      ''',
+      '''
+        ALTER TABLE $table ADD COLUMN $columnMMdVersion INTEGER DEFAULT 0;
+      '''
+    ];
+  }
+
   Future<List<dynamic>> insert(List<Collection> collections) async {
   Future<List<dynamic>> insert(List<Collection> collections) async {
     final db = await instance.database;
     final db = await instance.database;
     var batch = db.batch();
     var batch = db.batch();
@@ -204,11 +219,13 @@ class CollectionsDB {
     } else {
     } else {
       row[columnIsDeleted] = _sqlBoolFalse;
       row[columnIsDeleted] = _sqlBoolFalse;
     }
     }
+    row[columnMMdVersion] = collection.mMdVersion ?? 0;
+    row[columnMMdEncodedJson] = collection.mMdEncodedJson ?? '{}';
     return row;
     return row;
   }
   }
 
 
   Collection _convertToCollection(Map<String, dynamic> row) {
   Collection _convertToCollection(Map<String, dynamic> row) {
-    return Collection(
+    Collection result = Collection(
       row[columnID],
       row[columnID],
       User.fromJson(row[columnOwner]),
       User.fromJson(row[columnOwner]),
       row[columnEncryptedKey],
       row[columnEncryptedKey],
@@ -232,5 +249,8 @@ class CollectionsDB {
       // default to False is columnIsDeleted is not set
       // default to False is columnIsDeleted is not set
       isDeleted: (row[columnIsDeleted] ?? _sqlBoolFalse) == _sqlBoolTrue,
       isDeleted: (row[columnIsDeleted] ?? _sqlBoolFalse) == _sqlBoolTrue,
     );
     );
+    result.mMdVersion = row[columnMMdVersion] ?? 0;
+    result.mMdEncodedJson = row[columnMMdEncodedJson] ?? '{}';
+    return result;
   }
   }
 }
 }

+ 18 - 7
lib/db/files_db.dart

@@ -399,7 +399,10 @@ class FilesDB {
 
 
   Future<FileLoadResult> getAllUploadedFiles(
   Future<FileLoadResult> getAllUploadedFiles(
       int startTime, int endTime, int ownerID,
       int startTime, int endTime, int ownerID,
-      {int limit, bool asc, int visibility = kVisibilityVisible}) async {
+      {int limit,
+      bool asc,
+      int visibility = kVisibilityVisible,
+      Set<int> ignoredCollectionIDs}) async {
     final db = await instance.database;
     final db = await instance.database;
     final order = (asc ?? false ? 'ASC' : 'DESC');
     final order = (asc ?? false ? 'ASC' : 'DESC');
     final results = await db.query(
     final results = await db.query(
@@ -413,13 +416,14 @@ class FilesDB {
       limit: limit,
       limit: limit,
     );
     );
     final files = _convertToFiles(results);
     final files = _convertToFiles(results);
-    List<File> deduplicatedFiles = _deduplicatedFiles(files);
+    List<File> deduplicatedFiles =
+        _deduplicatedAndFilterIgnoredFiles(files, ignoredCollectionIDs);
     return FileLoadResult(deduplicatedFiles, files.length == limit);
     return FileLoadResult(deduplicatedFiles, files.length == limit);
   }
   }
 
 
   Future<FileLoadResult> getAllLocalAndUploadedFiles(
   Future<FileLoadResult> getAllLocalAndUploadedFiles(
       int startTime, int endTime, int ownerID,
       int startTime, int endTime, int ownerID,
-      {int limit, bool asc}) async {
+      {int limit, bool asc, Set<int> ignoredCollectionIDs}) async {
     final db = await instance.database;
     final db = await instance.database;
     final order = (asc ?? false ? 'ASC' : 'DESC');
     final order = (asc ?? false ? 'ASC' : 'DESC');
     final results = await db.query(
     final results = await db.query(
@@ -433,13 +437,14 @@ class FilesDB {
       limit: limit,
       limit: limit,
     );
     );
     final files = _convertToFiles(results);
     final files = _convertToFiles(results);
-    List<File> deduplicatedFiles = _deduplicatedFiles(files);
+    List<File> deduplicatedFiles =
+        _deduplicatedAndFilterIgnoredFiles(files, ignoredCollectionIDs);
     return FileLoadResult(deduplicatedFiles, files.length == limit);
     return FileLoadResult(deduplicatedFiles, files.length == limit);
   }
   }
 
 
   Future<FileLoadResult> getImportantFiles(
   Future<FileLoadResult> getImportantFiles(
       int startTime, int endTime, int ownerID, List<String> paths,
       int startTime, int endTime, int ownerID, List<String> paths,
-      {int limit, bool asc}) async {
+      {int limit, bool asc, Set<int> ignoredCollectionIDs}) async {
     final db = await instance.database;
     final db = await instance.database;
     String inParam = "";
     String inParam = "";
     for (final path in paths) {
     for (final path in paths) {
@@ -458,15 +463,21 @@ class FilesDB {
       limit: limit,
       limit: limit,
     );
     );
     final files = _convertToFiles(results);
     final files = _convertToFiles(results);
-    List<File> deduplicatedFiles = _deduplicatedFiles(files);
+    List<File> deduplicatedFiles =
+        _deduplicatedAndFilterIgnoredFiles(files, ignoredCollectionIDs);
     return FileLoadResult(deduplicatedFiles, files.length == limit);
     return FileLoadResult(deduplicatedFiles, files.length == limit);
   }
   }
 
 
-  List<File> _deduplicatedFiles(List<File> files) {
+  List<File> _deduplicatedAndFilterIgnoredFiles(
+      List<File> files, Set<int> ignoredCollectionIDs) {
     final uploadedFileIDs = <int>{};
     final uploadedFileIDs = <int>{};
     final List<File> deduplicatedFiles = [];
     final List<File> deduplicatedFiles = [];
     for (final file in files) {
     for (final file in files) {
       final id = file.uploadedFileID;
       final id = file.uploadedFileID;
+      if (ignoredCollectionIDs != null &&
+          ignoredCollectionIDs.contains(file.collectionID)) {
+        continue;
+      }
       if (id != null && id != -1 && uploadedFileIDs.contains(id)) {
       if (id != null && id != -1 && uploadedFileIDs.contains(id)) {
         continue;
         continue;
       }
       }

+ 43 - 25
lib/main.dart

@@ -1,6 +1,5 @@
 import 'dart:async';
 import 'dart:async';
 import 'dart:io';
 import 'dart:io';
-import 'dart:isolate';
 
 
 import 'package:adaptive_theme/adaptive_theme.dart';
 import 'package:adaptive_theme/adaptive_theme.dart';
 import 'package:background_fetch/background_fetch.dart';
 import 'package:background_fetch/background_fetch.dart';
@@ -56,8 +55,8 @@ void main() async {
 Future<void> _runInForeground() async {
 Future<void> _runInForeground() async {
   return await _runWithLogs(() async {
   return await _runWithLogs(() async {
     _logger.info("Starting app in foreground");
     _logger.info("Starting app in foreground");
-    await _init(false);
-    _scheduleFGSync();
+    await _init(false, via: 'mainMethod');
+    _scheduleFGSync('appStart in FG');
     runApp(AppLock(
     runApp(AppLock(
       builder: (args) => EnteApp(_runBackgroundTask, _killBGTask),
       builder: (args) => EnteApp(_runBackgroundTask, _killBGTask),
       lockScreen: LockScreen(),
       lockScreen: LockScreen(),
@@ -70,10 +69,11 @@ Future<void> _runInForeground() async {
 Future<void> _runBackgroundTask(String taskId) async {
 Future<void> _runBackgroundTask(String taskId) async {
   if (Platform.isIOS && _isProcessRunning) {
   if (Platform.isIOS && _isProcessRunning) {
     _logger.info("Background task triggered when process was already running");
     _logger.info("Background task triggered when process was already running");
-    await _sync();
+    await _sync('bgTaskActiveProcess');
     BackgroundFetch.finish(taskId);
     BackgroundFetch.finish(taskId);
   } else {
   } else {
     _runWithLogs(() async {
     _runWithLogs(() async {
+      _logger.info("run background task");
       _runInBackground(taskId);
       _runInBackground(taskId);
     }, prefix: "[bg]");
     }, prefix: "[bg]");
   }
   }
@@ -82,7 +82,7 @@ Future<void> _runBackgroundTask(String taskId) async {
 Future<void> _runInBackground(String taskId) async {
 Future<void> _runInBackground(String taskId) async {
   await Future.delayed(Duration(seconds: 3));
   await Future.delayed(Duration(seconds: 3));
   if (await _isRunningInForeground()) {
   if (await _isRunningInForeground()) {
-    _logger.info("FG task running, skipping BG task");
+    _logger.info("FG task running, skipping BG taskID: $taskId");
     BackgroundFetch.finish(taskId);
     BackgroundFetch.finish(taskId);
     return;
     return;
   } else {
   } else {
@@ -91,11 +91,11 @@ Future<void> _runInBackground(String taskId) async {
   _logger.info("[BackgroundFetch] Event received: $taskId");
   _logger.info("[BackgroundFetch] Event received: $taskId");
   _scheduleBGTaskKill(taskId);
   _scheduleBGTaskKill(taskId);
   if (Platform.isIOS) {
   if (Platform.isIOS) {
-    _scheduleSuicide(kBGTaskTimeout); // To prevent OS from punishing us
+    _scheduleSuicide(kBGTaskTimeout, taskId); // To prevent OS from punishing us
   }
   }
-  await _init(true);
+  await _init(true, via: 'runViaBackgroundTask');
   UpdateService.instance.showUpdateNotification();
   UpdateService.instance.showUpdateNotification();
-  await _sync();
+  await _sync('bgSync');
   BackgroundFetch.finish(taskId);
   BackgroundFetch.finish(taskId);
 }
 }
 
 
@@ -107,15 +107,16 @@ void _headlessTaskHandler(HeadlessTask task) {
   }
   }
 }
 }
 
 
-Future<void> _init(bool isBackground) async {
+Future<void> _init(bool isBackground, {String via = ''}) async {
   _isProcessRunning = true;
   _isProcessRunning = true;
-  _logger.info("Initializing...");
+  _logger.info("Initializing...  inBG =$isBackground via: $via");
+  await _logFGHeartBeatInfo();
   _savedThemeMode = await AdaptiveTheme.getThemeMode();
   _savedThemeMode = await AdaptiveTheme.getThemeMode();
   _scheduleHeartBeat(isBackground);
   _scheduleHeartBeat(isBackground);
   if (isBackground) {
   if (isBackground) {
-    AppLifecycleService.instance.onAppInBackground();
+    AppLifecycleService.instance.onAppInBackground('init via: $via');
   } else {
   } else {
-    AppLifecycleService.instance.onAppInForeground();
+    AppLifecycleService.instance.onAppInForeground('init via: $via');
   }
   }
   InAppPurchaseConnection.enablePendingPurchases();
   InAppPurchaseConnection.enablePendingPurchases();
   CryptoUtil.init();
   CryptoUtil.init();
@@ -142,9 +143,11 @@ Future<void> _init(bool isBackground) async {
   _logger.info("Initialization done");
   _logger.info("Initialization done");
 }
 }
 
 
-Future<void> _sync() async {
+Future<void> _sync(String caller) async {
   if (!AppLifecycleService.instance.isForeground) {
   if (!AppLifecycleService.instance.isForeground) {
-    _logger.info("Syncing in background");
+    _logger.info("Syncing in background caller $caller");
+  } else {
+    _logger.info("Syncing in foreground caller $caller");
   }
   }
   try {
   try {
     await SyncService.instance.sync();
     await SyncService.instance.sync();
@@ -174,16 +177,16 @@ Future<void> _scheduleHeartBeat(bool isBackground) async {
   });
   });
 }
 }
 
 
-Future<void> _scheduleFGSync() async {
-  await _sync();
+Future<void> _scheduleFGSync(String caller) async {
+  await _sync(caller);
   Future.delayed(kFGSyncFrequency, () async {
   Future.delayed(kFGSyncFrequency, () async {
-    _scheduleFGSync();
+    _scheduleFGSync('fgSyncCron');
   });
   });
 }
 }
 
 
 void _scheduleBGTaskKill(String taskId) async {
 void _scheduleBGTaskKill(String taskId) async {
   if (await _isRunningInForeground()) {
   if (await _isRunningInForeground()) {
-    _logger.info("Found app in FG, committing seppuku.");
+    _logger.info("Found app in FG, committing seppuku. $taskId");
     await _killBGTask(taskId);
     await _killBGTask(taskId);
     return;
     return;
   }
   }
@@ -208,14 +211,16 @@ Future<void> _killBGTask([String taskId]) async {
   if (taskId != null) {
   if (taskId != null) {
     BackgroundFetch.finish(taskId);
     BackgroundFetch.finish(taskId);
   }
   }
-  Isolate.current.kill(priority: Isolate.immediate);
 }
 }
 
 
 Future<void> _firebaseMessagingBackgroundHandler(RemoteMessage message) async {
 Future<void> _firebaseMessagingBackgroundHandler(RemoteMessage message) async {
+  bool isRunningInFG = await _isRunningInForeground(); // hb
+  bool isInForeground = AppLifecycleService.instance.isForeground;
   if (_isProcessRunning) {
   if (_isProcessRunning) {
-    _logger.info("Background push received when app is alive");
+    _logger.info(
+        "Background push received when app is alive and runningInFS: $isRunningInFG inForeground: $isInForeground");
     if (PushService.shouldSync(message)) {
     if (PushService.shouldSync(message)) {
-      await _sync();
+      await _sync('firebaseBgSyncActiveProcess');
     }
     }
   } else {
   } else {
     // App is dead
     // App is dead
@@ -224,17 +229,30 @@ Future<void> _firebaseMessagingBackgroundHandler(RemoteMessage message) async {
       if (Platform.isIOS) {
       if (Platform.isIOS) {
         _scheduleSuicide(kBGPushTimeout); // To prevent OS from punishing us
         _scheduleSuicide(kBGPushTimeout); // To prevent OS from punishing us
       }
       }
-      await _init(true);
+      await _init(true, via: 'firebasePush');
       if (PushService.shouldSync(message)) {
       if (PushService.shouldSync(message)) {
-        await _sync();
+        await _sync('firebaseBgSyncNoActiveProcess');
       }
       }
-    }, prefix: "[bg]");
+    }, prefix: "[fbg]");
   }
   }
 }
 }
 
 
+Future<void> _logFGHeartBeatInfo() async {
+  bool isRunningInFG = await _isRunningInForeground();
+  final prefs = await SharedPreferences.getInstance();
+  await prefs.reload();
+  var lastFGTaskHeartBeatTime = prefs.getInt(kLastFGTaskHeartBeatTime) ?? 0;
+  String lastRun = lastFGTaskHeartBeatTime == 0
+      ? 'never'
+      : DateTime.fromMicrosecondsSinceEpoch(lastFGTaskHeartBeatTime).toString();
+  _logger.info('isAlreaduunningFG: $isRunningInFG, last Beat: $lastRun');
+}
+
 void _scheduleSuicide(Duration duration, [String taskID]) {
 void _scheduleSuicide(Duration duration, [String taskID]) {
+  var taskIDVal = taskID ?? 'no taskID';
+  _logger.warning("Schedule seppuku taskID: $taskIDVal");
   Future.delayed(duration, () {
   Future.delayed(duration, () {
-    _logger.warning("TLE, committing seppuku");
+    _logger.warning("TLE, committing seppuku for taskID: $taskIDVal");
     _killBGTask(taskID);
     _killBGTask(taskID);
   });
   });
 }
 }

+ 34 - 16
lib/models/collection.dart

@@ -1,6 +1,8 @@
 import 'dart:convert';
 import 'dart:convert';
+import 'dart:core';
 
 
 import 'package:flutter/foundation.dart';
 import 'package:flutter/foundation.dart';
+import 'package:photos/models/magic_metadata.dart';
 
 
 class Collection {
 class Collection {
   final int id;
   final int id;
@@ -16,6 +18,14 @@ class Collection {
   final List<PublicURL> publicURLs;
   final List<PublicURL> publicURLs;
   final int updationTime;
   final int updationTime;
   final bool isDeleted;
   final bool isDeleted;
+  String mMdEncodedJson;
+  int mMdVersion = 0;
+  CollectionMagicMetadata _mmd;
+
+  CollectionMagicMetadata get magicMetadata =>
+      _mmd ?? CollectionMagicMetadata.fromEncodedJson(mMdEncodedJson ?? '{}');
+
+  set magicMetadata(val) => _mmd = val;
 
 
   Collection(
   Collection(
     this.id,
     this.id,
@@ -33,6 +43,10 @@ class Collection {
     this.isDeleted = false,
     this.isDeleted = false,
   });
   });
 
 
+  bool isArchived() {
+    return mMdVersion > 0 && magicMetadata.visibility == kVisibilityArchive;
+  }
+
   static CollectionType typeFromString(String type) {
   static CollectionType typeFromString(String type) {
     switch (type) {
     switch (type) {
       case "folder":
       case "folder":
@@ -54,22 +68,23 @@ class Collection {
     }
     }
   }
   }
 
 
-  Collection copyWith({
-    int id,
-    User owner,
-    String encryptedKey,
-    String keyDecryptionNonce,
-    String name,
-    String encryptedName,
-    String nameDecryptionNonce,
-    CollectionType type,
-    CollectionAttributes attributes,
-    List<User> sharees,
-    List<PublicURL> publicURLs,
-    int updationTime,
-    bool isDeleted,
-  }) {
-    return Collection(
+  Collection copyWith(
+      {int id,
+      User owner,
+      String encryptedKey,
+      String keyDecryptionNonce,
+      String name,
+      String encryptedName,
+      String nameDecryptionNonce,
+      CollectionType type,
+      CollectionAttributes attributes,
+      List<User> sharees,
+      List<PublicURL> publicURLs,
+      int updationTime,
+      bool isDeleted,
+      String mMdEncodedJson,
+      int mMdVersion}) {
+    Collection result = Collection(
       id ?? this.id,
       id ?? this.id,
       owner ?? this.owner,
       owner ?? this.owner,
       encryptedKey ?? this.encryptedKey,
       encryptedKey ?? this.encryptedKey,
@@ -84,6 +99,9 @@ class Collection {
       updationTime ?? this.updationTime,
       updationTime ?? this.updationTime,
       isDeleted: isDeleted ?? this.isDeleted,
       isDeleted: isDeleted ?? this.isDeleted,
     );
     );
+    result.mMdVersion = mMdVersion ?? this.mMdVersion;
+    result.mMdEncodedJson = mMdEncodedJson ?? this.mMdEncodedJson;
+    return result;
   }
   }
 
 
   Map<String, dynamic> toMap() {
   Map<String, dynamic> toMap() {

+ 27 - 0
lib/models/magic_metadata.dart

@@ -62,3 +62,30 @@ class PubMagicMetadata {
     );
     );
   }
   }
 }
 }
+
+class CollectionMagicMetadata {
+  // 0 -> visible
+  // 1 -> archived
+  // 2 -> hidden etc?
+  int visibility;
+
+  CollectionMagicMetadata({this.visibility});
+
+  factory CollectionMagicMetadata.fromEncodedJson(String encodedJson) =>
+      CollectionMagicMetadata.fromJson(jsonDecode(encodedJson));
+
+  factory CollectionMagicMetadata.fromJson(dynamic json) => CollectionMagicMetadata.fromMap(json);
+
+  Map<String, dynamic> toJson() {
+    final map = <String, dynamic>{};
+    map[kMagicKeyVisibility] = visibility;
+    return map;
+  }
+
+  factory CollectionMagicMetadata.fromMap(Map<String, dynamic> map) {
+    if (map == null) return null;
+    return CollectionMagicMetadata(
+      visibility: map[kMagicKeyVisibility] ?? kVisibilityVisible,
+    );
+  }
+}

+ 4 - 4
lib/services/app_lifecycle_service.dart

@@ -10,13 +10,13 @@ class AppLifecycleService {
 
 
   AppLifecycleService._privateConstructor();
   AppLifecycleService._privateConstructor();
 
 
-  void onAppInForeground() {
-    _logger.info("App in foreground");
+  void onAppInForeground(String reason) {
+    _logger.info("App in foreground via $reason");
     isForeground = true;
     isForeground = true;
   }
   }
 
 
-  void onAppInBackground() {
-    _logger.info("App in background");
+  void onAppInBackground(String reason) {
+    _logger.info("App in background $reason");
     isForeground = false;
     isForeground = false;
   }
   }
 }
 }

+ 99 - 6
lib/services/collections_service.dart

@@ -1,4 +1,5 @@
 import 'dart:convert';
 import 'dart:convert';
+import 'dart:math';
 import 'dart:typed_data';
 import 'dart:typed_data';
 
 
 import 'package:dio/dio.dart';
 import 'package:dio/dio.dart';
@@ -19,7 +20,9 @@ import 'package:photos/events/local_photos_updated_event.dart';
 import 'package:photos/models/collection.dart';
 import 'package:photos/models/collection.dart';
 import 'package:photos/models/collection_file_item.dart';
 import 'package:photos/models/collection_file_item.dart';
 import 'package:photos/models/file.dart';
 import 'package:photos/models/file.dart';
+import 'package:photos/models/magic_metadata.dart';
 import 'package:photos/services/app_lifecycle_service.dart';
 import 'package:photos/services/app_lifecycle_service.dart';
+import 'package:photos/services/file_magic_service.dart';
 import 'package:photos/services/remote_sync_service.dart';
 import 'package:photos/services/remote_sync_service.dart';
 import 'package:photos/utils/crypto_util.dart';
 import 'package:photos/utils/crypto_util.dart';
 import 'package:photos/utils/file_download_util.dart';
 import 'package:photos/utils/file_download_util.dart';
@@ -27,7 +30,7 @@ import 'package:shared_preferences/shared_preferences.dart';
 
 
 class CollectionsService {
 class CollectionsService {
   static final _collectionSyncTimeKeyPrefix = "collection_sync_time_";
   static final _collectionSyncTimeKeyPrefix = "collection_sync_time_";
-  static final _collectionsSyncTimeKey = "collections_sync_time";
+  static final _collectionsSyncTimeKey = "collections_sync_time_x";
 
 
   static const int kMaximumWriteAttempts = 5;
   static const int kMaximumWriteAttempts = 5;
 
 
@@ -55,6 +58,7 @@ class CollectionsService {
   Future<void> init() async {
   Future<void> init() async {
     _prefs = await SharedPreferences.getInstance();
     _prefs = await SharedPreferences.getInstance();
     final collections = await _db.getAllCollections();
     final collections = await _db.getAllCollections();
+
     for (final collection in collections) {
     for (final collection in collections) {
       _cacheCollectionAttributes(collection);
       _cacheCollectionAttributes(collection);
     }
     }
@@ -74,8 +78,7 @@ class CollectionsService {
         _prefs.getInt(_collectionsSyncTimeKey) ?? 0;
         _prefs.getInt(_collectionsSyncTimeKey) ?? 0;
 
 
     // Might not have synced the collection fully
     // Might not have synced the collection fully
-    final fetchedCollections =
-        await _fetchCollections(lastCollectionUpdationTime ?? 0);
+    final fetchedCollections = await _fetchCollections(0);
     final updatedCollections = <Collection>[];
     final updatedCollections = <Collection>[];
     int maxUpdationTime = lastCollectionUpdationTime;
     int maxUpdationTime = lastCollectionUpdationTime;
     final ownerID = _config.getUserID();
     final ownerID = _config.getUserID();
@@ -127,6 +130,14 @@ class CollectionsService {
     return updatedCollections;
     return updatedCollections;
   }
   }
 
 
+  Set<int> getArchivedCollections() {
+    return _collectionIDToCollections.values
+        .toList()
+        .where((element) => element.isArchived())
+        .map((e) => e.id)
+        .toSet();
+  }
+
   int getCollectionSyncTime(int collectionID) {
   int getCollectionSyncTime(int collectionID) {
     return _prefs
     return _prefs
             .getInt(_collectionSyncTimeKeyPrefix + collectionID.toString()) ??
             .getInt(_collectionSyncTimeKeyPrefix + collectionID.toString()) ??
@@ -273,6 +284,64 @@ class CollectionsService {
     }
     }
   }
   }
 
 
+  Future<void> updateMagicMetadata(
+      Collection collection, Map<String, dynamic> newMetadataUpdate) async {
+    final int ownerID = Configuration.instance.getUserID();
+    try {
+      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
+      // current update is simple replace. This will be enhanced in the future,
+      // as required.
+      Map<String, dynamic> jsonToUpdate =
+          jsonDecode(collection.mMdEncodedJson ?? '{}');
+      newMetadataUpdate.forEach((key, value) {
+        jsonToUpdate[key] = value;
+      });
+
+      // update the local information so that it's reflected on UI
+      collection.mMdEncodedJson = jsonEncode(jsonToUpdate);
+      collection.magicMetadata = CollectionMagicMetadata.fromJson(jsonToUpdate);
+
+      final key = getCollectionKey(collection.id);
+      final encryptedMMd = await CryptoUtil.encryptChaCha(
+          utf8.encode(jsonEncode(jsonToUpdate)), key);
+      // for required field, the json validator on golang doesn't treat 0 as valid
+      // value. Instead of changing version to ptr, decided to start version with 1.
+      int currentVersion = max(collection.mMdVersion, 1);
+      final params = UpdateMagicMetadataRequest(
+        id: collection.id,
+        magicMetadata: MetadataRequest(
+          version: currentVersion,
+          count: jsonToUpdate.length,
+          data: Sodium.bin2base64(encryptedMMd.encryptedData),
+          header: Sodium.bin2base64(encryptedMMd.header),
+        ),
+      );
+      await _dio.put(
+        Configuration.instance.getHttpEndpoint() +
+            "/collections/magic-metadata",
+        data: params,
+        options: Options(
+            headers: {"X-Auth-Token": Configuration.instance.getToken()}),
+      );
+      collection.mMdVersion = currentVersion + 1;
+      _cacheCollectionAttributes(collection);
+      // trigger sync to fetch the latest collection state from server
+      sync();
+    } on DioError catch (e) {
+      if (e.response != null && e.response.statusCode == 409) {
+        _logger.severe('collection magic data out of sync');
+        sync();
+      }
+      rethrow;
+    } catch (e, s) {
+      _logger.severe("failed to sync magic metadata", e, s);
+      rethrow;
+    }
+  }
+
   Future<void> createShareUrl(Collection collection) async {
   Future<void> createShareUrl(Collection collection) async {
     try {
     try {
       final response = await _dio.post(
       final response = await _dio.post(
@@ -361,8 +430,20 @@ class CollectionsService {
       final List<Collection> collections = [];
       final List<Collection> collections = [];
       if (response != null) {
       if (response != null) {
         final c = response.data["collections"];
         final c = response.data["collections"];
-        for (final collection in c) {
-          collections.add(Collection.fromMap(collection));
+        for (final collectionData in c) {
+          final collection = Collection.fromMap(collectionData);
+          if (collectionData['magicMetadata'] != null) {
+            final decryptionKey = _getDecryptedKey(collection);
+            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;
       return collections;
@@ -408,7 +489,19 @@ class CollectionsService {
             headers: {"X-Auth-Token": Configuration.instance.getToken()}),
             headers: {"X-Auth-Token": Configuration.instance.getToken()}),
       );
       );
       assert(response != null && response.data != null);
       assert(response != null && response.data != null);
-      final collection = Collection.fromMap(response.data["collection"]);
+      final collectionData = response.data["collection"];
+      final collection = Collection.fromMap(collectionData);
+      if (collectionData['magicMetadata'] != null) {
+        final decryptionKey = _getDecryptedKey(collection);
+        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);
+      }
       await _db.insert(List.from([collection]));
       await _db.insert(List.from([collection]));
       _cacheCollectionAttributes(collection);
       _cacheCollectionAttributes(collection);
       return collection;
       return collection;

+ 5 - 3
lib/services/remote_sync_service.dart

@@ -217,7 +217,7 @@ class RemoteSyncService {
       // uploaded yet. These files should ignore video backup & ignored files filter
       // uploaded yet. These files should ignore video backup & ignored files filter
       filesToBeUploaded = await _db.getPendingManualUploads();
       filesToBeUploaded = await _db.getPendingManualUploads();
     }
     }
-    _moveVideosToEnd(filesToBeUploaded);
+    _sortByTimeAndType(filesToBeUploaded);
     _logger.info(
     _logger.info(
         filesToBeUploaded.length.toString() + " new files to be uploaded.");
         filesToBeUploaded.length.toString() + " new files to be uploaded.");
     return filesToBeUploaded;
     return filesToBeUploaded;
@@ -453,10 +453,12 @@ class RemoteSyncService {
     return Platform.isIOS && !AppLifecycleService.instance.isForeground;
     return Platform.isIOS && !AppLifecycleService.instance.isForeground;
   }
   }
 
 
-  void _moveVideosToEnd(List<File> file) {
+  // _sortByTimeAndType moves videos to end and sort by creation time (desc).
+  // This is done to upload most recent photo first.
+  void _sortByTimeAndType(List<File> file) {
     file.sort((first, second) {
     file.sort((first, second) {
       if (first.fileType == second.fileType) {
       if (first.fileType == second.fileType) {
-        return 0;
+        return second.creationTime.compareTo(first.creationTime);
       } else if (first.fileType == FileType.video) {
       } else if (first.fileType == FileType.video) {
         return 1;
         return 1;
       } else {
       } else {

+ 65 - 45
lib/ui/backup_folder_selection_page.dart

@@ -9,6 +9,7 @@ import 'package:photos/core/event_bus.dart';
 import 'package:photos/db/files_db.dart';
 import 'package:photos/db/files_db.dart';
 import 'package:photos/events/backup_folders_updated_event.dart';
 import 'package:photos/events/backup_folders_updated_event.dart';
 import 'package:photos/models/file.dart';
 import 'package:photos/models/file.dart';
+import 'package:photos/ui/common/custom_color_scheme.dart';
 import 'package:photos/ui/common_elements.dart';
 import 'package:photos/ui/common_elements.dart';
 import 'package:photos/ui/loading_widget.dart';
 import 'package:photos/ui/loading_widget.dart';
 import 'package:photos/ui/thumbnail_widget.dart';
 import 'package:photos/ui/thumbnail_widget.dart';
@@ -61,19 +62,29 @@ class _BackupFolderSelectionPageState extends State<BackupFolderSelectionPage> {
   @override
   @override
   Widget build(BuildContext context) {
   Widget build(BuildContext context) {
     return Scaffold(
     return Scaffold(
-      appBar: AppBar(title: Text("select folders to backup")),
+      appBar: AppBar(
+        title: Text(""),
+      ),
       body: Column(
       body: Column(
         mainAxisAlignment: MainAxisAlignment.center,
         mainAxisAlignment: MainAxisAlignment.center,
         children: [
         children: [
-          Padding(
-            padding: EdgeInsets.all(12),
+          Container(
+            padding: EdgeInsets.all(24),
+            child: Text(
+              'Select folders for backup',
+              textAlign: TextAlign.left,
+              style: TextStyle(
+                  color: Theme.of(context).colorScheme.onSurface,
+                  fontFamily: 'Inter-Bold',
+                  fontSize: 32,
+                  fontWeight: FontWeight.bold),
+            ),
           ),
           ),
           Padding(
           Padding(
-            padding: const EdgeInsets.only(left: 32, right: 32),
+            padding: const EdgeInsets.only(left: 24, right: 48),
             child: Text(
             child: Text(
-              "the selected folders will be end-to-end encrypted and backed up",
+              "Selected folders will be end-to-end encrypted and backed up",
               style: Theme.of(context).textTheme.caption.copyWith(height: 1.3),
               style: Theme.of(context).textTheme.caption.copyWith(height: 1.3),
-              textAlign: TextAlign.center,
             ),
             ),
           ),
           ),
           Padding(
           Padding(
@@ -84,15 +95,18 @@ class _BackupFolderSelectionPageState extends State<BackupFolderSelectionPage> {
               : GestureDetector(
               : GestureDetector(
                   behavior: HitTestBehavior.translucent,
                   behavior: HitTestBehavior.translucent,
                   child: Padding(
                   child: Padding(
-                    padding: const EdgeInsets.fromLTRB(6, 6, 64, 6),
+                    padding: const EdgeInsets.fromLTRB(24, 6, 64, 12),
                     child: Align(
                     child: Align(
-                      alignment: Alignment.centerRight,
+                      alignment: Alignment.centerLeft,
                       child: Text(
                       child: Text(
                         _selectedFolders.length == _allFolders.length
                         _selectedFolders.length == _allFolders.length
-                            ? "unselect all"
-                            : "select all",
+                            ? "Unselect all"
+                            : "Select all",
                         textAlign: TextAlign.right,
                         textAlign: TextAlign.right,
-                        style: Theme.of(context).textTheme.overline,
+                        style: Theme.of(context)
+                            .textTheme
+                            .overline
+                            .copyWith(decoration: TextDecoration.underline),
                       ),
                       ),
                     ),
                     ),
                   ),
                   ),
@@ -119,11 +133,11 @@ class _BackupFolderSelectionPageState extends State<BackupFolderSelectionPage> {
           Hero(
           Hero(
             tag: "select_folders",
             tag: "select_folders",
             child: Container(
             child: Container(
+              width: double.infinity,
               padding: EdgeInsets.only(
               padding: EdgeInsets.only(
-                  left: 60, right: 60, bottom: Platform.isIOS ? 60 : 32),
-              child: button(
-                widget.buttonText,
-                fontSize: 18,
+                  left: 24, right: 24, bottom: Platform.isIOS ? 60 : 32),
+              child: OutlinedButton(
+                child: Text(widget.buttonText),
                 onPressed: _selectedFolders.isEmpty
                 onPressed: _selectedFolders.isEmpty
                     ? null
                     ? null
                     : () async {
                     : () async {
@@ -132,7 +146,7 @@ class _BackupFolderSelectionPageState extends State<BackupFolderSelectionPage> {
                         Bus.instance.fire(BackupFoldersUpdatedEvent());
                         Bus.instance.fire(BackupFoldersUpdatedEvent());
                         Navigator.of(context).pop();
                         Navigator.of(context).pop();
                       },
                       },
-                padding: EdgeInsets.fromLTRB(60, 20, 60, 20),
+                // padding: EdgeInsets.fromLTRB(12, 20, 12, 20),
               ),
               ),
             ),
             ),
           ),
           ),
@@ -148,7 +162,7 @@ class _BackupFolderSelectionPageState extends State<BackupFolderSelectionPage> {
     _sortFiles();
     _sortFiles();
     final scrollController = ScrollController();
     final scrollController = ScrollController();
     return Container(
     return Container(
-      padding: EdgeInsets.only(left: 40, right: 40),
+      padding: EdgeInsets.only(left: 20, right: 24),
       child: Scrollbar(
       child: Scrollbar(
         controller: scrollController,
         controller: scrollController,
         isAlwaysShown: true,
         isAlwaysShown: true,
@@ -198,47 +212,63 @@ class _BackupFolderSelectionPageState extends State<BackupFolderSelectionPage> {
   Widget _getFileItem(File file) {
   Widget _getFileItem(File file) {
     final isSelected = _selectedFolders.contains(file.deviceFolder);
     final isSelected = _selectedFolders.contains(file.deviceFolder);
     return Padding(
     return Padding(
-      padding: const EdgeInsets.only(bottom: 1, right: 4),
+      padding: const EdgeInsets.only(bottom: 1, right: 1),
       child: Container(
       child: Container(
         decoration: BoxDecoration(
         decoration: BoxDecoration(
           border: Border.all(
           border: Border.all(
-            color: Theme.of(context).primaryColor,
+            color: Theme.of(context).colorScheme.boxUnSelectColor,
           ),
           ),
           borderRadius: BorderRadius.all(
           borderRadius: BorderRadius.all(
-            Radius.circular(10),
+            Radius.circular(12),
           ),
           ),
           color: isSelected
           color: isSelected
-              ? Color.fromRGBO(16, 32, 32, 1)
-              : Color.fromRGBO(8, 18, 18, 0.4),
+              ? Theme.of(context).colorScheme.boxSelectColor
+              : Theme.of(context).colorScheme.boxUnSelectColor,
         ),
         ),
-        padding: EdgeInsets.fromLTRB(20, 16, 20, 16),
+        padding: EdgeInsets.fromLTRB(8, 4, 4, 4),
         child: InkWell(
         child: InkWell(
           child: Row(
           child: Row(
             mainAxisAlignment: MainAxisAlignment.spaceBetween,
             mainAxisAlignment: MainAxisAlignment.spaceBetween,
             children: [
             children: [
               Row(
               Row(
                 children: [
                 children: [
-                  _getThumbnail(file),
-                  Padding(padding: EdgeInsets.all(10)),
+                  Checkbox(
+                    checkColor: Colors.green,
+                    activeColor: Colors.white,
+                    value: isSelected,
+                    onChanged: (value) {
+                      if (value) {
+                        _selectedFolders.add(file.deviceFolder);
+                      } else {
+                        _selectedFolders.remove(file.deviceFolder);
+                      }
+                      setState(() {});
+                    },
+                  ),
                   Column(
                   Column(
                     crossAxisAlignment: CrossAxisAlignment.start,
                     crossAxisAlignment: CrossAxisAlignment.start,
                     children: [
                     children: [
                       Container(
                       Container(
-                        constraints: BoxConstraints(maxWidth: 140),
+                        constraints: BoxConstraints(maxWidth: 180),
                         child: Text(
                         child: Text(
                           file.deviceFolder,
                           file.deviceFolder,
+                          textAlign: TextAlign.left,
                           style: TextStyle(
                           style: TextStyle(
-                            fontSize: 14,
-                            height: 1.5,
+                            fontFamily: 'Inter-Medium',
+                            fontSize: 18,
+                            fontWeight: FontWeight.w600,
                             color: isSelected
                             color: isSelected
                                 ? Colors.white
                                 ? Colors.white
-                                : Colors.white.withOpacity(0.7),
+                                : Theme.of(context)
+                                    .colorScheme
+                                    .onSurface
+                                    .withOpacity(0.7),
                           ),
                           ),
                           overflow: TextOverflow.ellipsis,
                           overflow: TextOverflow.ellipsis,
                           maxLines: 2,
                           maxLines: 2,
                         ),
                         ),
                       ),
                       ),
-                      Padding(padding: EdgeInsets.all(2)),
+                      Padding(padding: EdgeInsets.only(top: 2)),
                       Text(
                       Text(
                         _itemCount[file.deviceFolder].toString() +
                         _itemCount[file.deviceFolder].toString() +
                             " item" +
                             " item" +
@@ -247,25 +277,15 @@ class _BackupFolderSelectionPageState extends State<BackupFolderSelectionPage> {
                         style: TextStyle(
                         style: TextStyle(
                           fontSize: 12,
                           fontSize: 12,
                           color: isSelected
                           color: isSelected
-                              ? Colors.white.withOpacity(0.65)
-                              : Colors.white.withOpacity(0.4),
+                              ? Colors.white
+                              : Theme.of(context).colorScheme.onSurface,
                         ),
                         ),
                       ),
                       ),
                     ],
                     ],
                   ),
                   ),
                 ],
                 ],
               ),
               ),
-              Checkbox(
-                value: isSelected,
-                onChanged: (value) {
-                  if (value) {
-                    _selectedFolders.add(file.deviceFolder);
-                  } else {
-                    _selectedFolders.remove(file.deviceFolder);
-                  }
-                  setState(() {});
-                },
-              ),
+              _getThumbnail(file),
             ],
             ],
           ),
           ),
           onTap: () {
           onTap: () {
@@ -309,8 +329,8 @@ class _BackupFolderSelectionPageState extends State<BackupFolderSelectionPage> {
           shouldShowSyncStatus: false,
           shouldShowSyncStatus: false,
           key: Key("backup_selection_widget" + file.tag()),
           key: Key("backup_selection_widget" + file.tag()),
         ),
         ),
-        height: 60,
-        width: 60,
+        height: 88,
+        width: 88,
       ),
       ),
     );
     );
   }
   }

+ 2 - 1
lib/ui/collections_gallery_widget.dart

@@ -487,7 +487,8 @@ class CollectionItem extends StatelessWidget {
                   tag: "collection" + c.thumbnail.tag(),
                   tag: "collection" + c.thumbnail.tag(),
                   child: ThumbnailWidget(
                   child: ThumbnailWidget(
                     c.thumbnail,
                     c.thumbnail,
-                    key: Key("collection" + c.thumbnail.tag()),
+                    shouldShowArchiveStatus: c.collection.isArchived(),
+                    key: Key("collection" + c.thumbnail.tag(),),
                   )),
                   )),
               height: 140,
               height: 140,
               width: 140,
               width: 140,

+ 16 - 0
lib/ui/common/custom_color_scheme.dart

@@ -0,0 +1,16 @@
+import 'dart:ui';
+
+import 'package:flutter/material.dart';
+
+extension CustomColorScheme on ColorScheme {
+  Color get defaultTextColor =>
+      brightness == Brightness.light ? Colors.black : Colors.white;
+
+  Color get boxSelectColor => brightness == Brightness.light
+      ? Color.fromRGBO(67, 186, 108, 1)
+      : Color.fromRGBO(16, 32, 32, 1);
+
+  Color get boxUnSelectColor => brightness == Brightness.light
+      ? Color.fromRGBO(240, 240, 240, 1)
+      : Color.fromRGBO(8, 18, 18, 0.4);
+}

+ 24 - 14
lib/ui/common_elements.dart

@@ -24,26 +24,36 @@ Widget button(
     child: OutlinedButton(
     child: OutlinedButton(
       style: OutlinedButton.styleFrom(
       style: OutlinedButton.styleFrom(
         shape: RoundedRectangleBorder(
         shape: RoundedRectangleBorder(
-          borderRadius: BorderRadius.circular(10),
+          borderRadius: BorderRadius.circular(8),
         ),
         ),
         padding: padding ?? EdgeInsets.fromLTRB(50, 16, 50, 16),
         padding: padding ?? EdgeInsets.fromLTRB(50, 16, 50, 16),
-        side: BorderSide(
-          width: onPressed == null ? 1 : 2,
-          color: onPressed == null
-              ? Colors.grey
-              : Color.fromRGBO(45, 194, 98, 1.0),
-        ),
-      ),
-      child: Text(
-        text,
-        style: TextStyle(
-          fontWeight: FontWeight.bold,
+        textStyle: TextStyle(
+          fontWeight: FontWeight.w600,
+          fontFamily: 'Inter-SemiBold',
           fontSize: fontSize,
           fontSize: fontSize,
-          color: onPressed == null ? Colors.grey : Colors.white,
           height: lineHeight,
           height: lineHeight,
         ),
         ),
-        textAlign: TextAlign.center,
+      ).copyWith(
+        backgroundColor: MaterialStateProperty.resolveWith<Color>(
+          (Set<MaterialState> states) {
+            if (states.contains(MaterialState.disabled)) {
+              return Colors.grey;
+            }
+            // return Color.fromRGBO(29, 184, 80, 1);
+            return Colors.white;
+          },
+        ),
+        foregroundColor: MaterialStateProperty.resolveWith<Color>(
+          (Set<MaterialState> states) {
+            if (states.contains(MaterialState.disabled)) {
+              return Colors.white;
+            }
+            return Colors.black;
+          },
+        ),
+        alignment: Alignment.center,
       ),
       ),
+      child: Text(text),
       onPressed: onPressed,
       onPressed: onPressed,
     ),
     ),
   );
   );

+ 30 - 6
lib/ui/gallery_app_bar_widget.dart

@@ -166,21 +166,37 @@ class _GalleryAppBarWidgetState extends State<GalleryAppBarWidget> {
         ),
         ),
       );
       );
     }
     }
-    if (widget.type == GalleryAppBarType.owned_collection &&
-        widget.collection.type == CollectionType.album) {
+    if (widget.type == GalleryAppBarType.owned_collection) {
       actions.add(PopupMenuButton(
       actions.add(PopupMenuButton(
         itemBuilder: (context) {
         itemBuilder: (context) {
           final List<PopupMenuItem> items = [];
           final List<PopupMenuItem> items = [];
+          if (widget.collection.type == CollectionType.album) {
+            items.add(
+              PopupMenuItem(
+                value: 1,
+                child: Row(
+                  children: const [
+                    Icon(Icons.edit),
+                    Padding(
+                      padding: EdgeInsets.all(8),
+                    ),
+                    Text("rename"),
+                  ],
+                ),
+              ),
+            );
+          }
+          bool isArchived = widget.collection.isArchived();
           items.add(
           items.add(
             PopupMenuItem(
             PopupMenuItem(
-              value: 1,
+              value: 2,
               child: Row(
               child: Row(
-                children: const [
-                  Icon(Icons.edit),
+                children: [
+                  Icon(isArchived ? Icons.unarchive : Icons.archive),
                   Padding(
                   Padding(
                     padding: EdgeInsets.all(8),
                     padding: EdgeInsets.all(8),
                   ),
                   ),
-                  Text("rename"),
+                  Text(isArchived ? "unarchive" : "archive"),
                 ],
                 ],
               ),
               ),
             ),
             ),
@@ -191,6 +207,14 @@ class _GalleryAppBarWidgetState extends State<GalleryAppBarWidget> {
           if (value == 1) {
           if (value == 1) {
             await _renameAlbum(context);
             await _renameAlbum(context);
           }
           }
+          if (value == 2) {
+            await changeCollectionVisibility(
+                context,
+                widget.collection,
+                widget.collection.isArchived()
+                    ? kVisibilityVisible
+                    : kVisibilityArchive);
+          }
         },
         },
       ));
       ));
     }
     }

+ 11 - 3
lib/ui/home_widget.dart

@@ -343,20 +343,28 @@ class _HomeWidgetState extends State<HomeWidget> {
       asyncLoader: (creationStartTime, creationEndTime, {limit, asc}) async {
       asyncLoader: (creationStartTime, creationEndTime, {limit, asc}) async {
         final importantPaths = Configuration.instance.getPathsToBackUp();
         final importantPaths = Configuration.instance.getPathsToBackUp();
         final ownerID = Configuration.instance.getUserID();
         final ownerID = Configuration.instance.getUserID();
+        final archivedCollectionIds =
+        CollectionsService.instance.getArchivedCollections();
         FileLoadResult result;
         FileLoadResult result;
         if (importantPaths.isNotEmpty) {
         if (importantPaths.isNotEmpty) {
           result = await FilesDB.instance.getImportantFiles(creationStartTime,
           result = await FilesDB.instance.getImportantFiles(creationStartTime,
               creationEndTime, ownerID, importantPaths.toList(),
               creationEndTime, ownerID, importantPaths.toList(),
-              limit: limit, asc: asc);
+              limit: limit,
+              asc: asc,
+              ignoredCollectionIDs: archivedCollectionIds);
         } else {
         } else {
           if (LocalSyncService.instance.hasGrantedLimitedPermissions()) {
           if (LocalSyncService.instance.hasGrantedLimitedPermissions()) {
             result = await FilesDB.instance.getAllLocalAndUploadedFiles(
             result = await FilesDB.instance.getAllLocalAndUploadedFiles(
                 creationStartTime, creationEndTime, ownerID,
                 creationStartTime, creationEndTime, ownerID,
-                limit: limit, asc: asc);
+                limit: limit,
+                asc: asc,
+                ignoredCollectionIDs: archivedCollectionIds);
           } else {
           } else {
             result = await FilesDB.instance.getAllUploadedFiles(
             result = await FilesDB.instance.getAllUploadedFiles(
                 creationStartTime, creationEndTime, ownerID,
                 creationStartTime, creationEndTime, ownerID,
-                limit: limit, asc: asc);
+                limit: limit,
+                asc: asc,
+                ignoredCollectionIDs: archivedCollectionIds);
           }
           }
         }
         }
         // hide ignored files from home page UI
         // hide ignored files from home page UI

+ 4 - 4
lib/ui/password_entry_page.dart

@@ -69,13 +69,13 @@ class _PasswordEntryPageState extends State<PasswordEntryPage> {
 
 
   @override
   @override
   Widget build(BuildContext context) {
   Widget build(BuildContext context) {
-    String title = "set password";
+    String title = "Set password";
     if (widget.mode == PasswordEntryMode.update) {
     if (widget.mode == PasswordEntryMode.update) {
-      title = "change password";
+      title = "Change password";
     } else if (widget.mode == PasswordEntryMode.reset) {
     } else if (widget.mode == PasswordEntryMode.reset) {
-      title = "reset password";
+      title = "Reset password";
     } else if (_password != null) {
     } else if (_password != null) {
-      title = "encryption keys";
+      title = "Encryption keys";
     }
     }
     return Scaffold(
     return Scaffold(
       appBar: AppBar(
       appBar: AppBar(

+ 79 - 0
lib/ui/recovery_key_page.dart

@@ -0,0 +1,79 @@
+import 'package:flutter/foundation.dart';
+import 'package:flutter/material.dart';
+import 'package:flutter/widgets.dart';
+import 'package:photos/core/configuration.dart';
+import 'package:photos/ui/settings/account_section_widget.dart';
+import 'package:photos/ui/settings/app_version_widget.dart';
+import 'package:photos/ui/settings/backup_section_widget.dart';
+import 'package:photos/ui/settings/danger_section_widget.dart';
+import 'package:photos/ui/settings/debug_section_widget.dart';
+import 'package:photos/ui/settings/details_section_widget.dart';
+import 'package:photos/ui/settings/info_section_widget.dart';
+import 'package:photos/ui/settings/security_section_widget.dart';
+import 'package:photos/ui/settings/social_section_widget.dart';
+import 'package:photos/ui/settings/support_section_widget.dart';
+import 'package:photos/ui/settings/theme_switch_widget.dart';
+import 'package:photos/utils/dialog_util.dart';
+
+class RecoveryKeyPage extends StatelessWidget {
+  const RecoveryKeyPage({Key key}) : super(key: key);
+
+  @override
+  Widget build(BuildContext context) {
+    return Scaffold(
+      body: _getBody(context),
+    );
+  }
+
+  Widget _getBody(BuildContext context) {
+    final hasLoggedIn = Configuration.instance.getToken() != null;
+    final List<Widget> contents = [];
+    contents.add(Row(
+        mainAxisAlignment: MainAxisAlignment.spaceAround,
+        crossAxisAlignment: CrossAxisAlignment.center,
+        children: const [ThemeSwitchWidget()]));
+    final sectionDivider = Divider(
+      height: 10,
+      color: Theme.of(context).colorScheme.onSurface.withOpacity(0.4),
+    );
+    if (hasLoggedIn) {
+      contents.addAll([
+        DetailsSectionWidget(),
+        sectionDivider,
+        BackupSectionWidget(),
+        sectionDivider,
+        AccountSectionWidget(),
+        sectionDivider,
+      ]);
+    }
+    contents.addAll([
+      SecuritySectionWidget(),
+      sectionDivider,
+      test(),
+      sectionDivider,
+      SupportSectionWidget(),
+      sectionDivider,
+      SocialSectionWidget(),
+      sectionDivider,
+      InfoSectionWidget(),
+    ]);
+    if (hasLoggedIn) {
+      contents.addAll([
+        sectionDivider,
+        DangerSectionWidget(),
+      ]);
+    }
+    contents.add(AppVersionWidget());
+    if (kDebugMode && hasLoggedIn) {
+      contents.add(DebugSectionWidget());
+    }
+    return SingleChildScrollView(
+      child: Padding(
+        padding: const EdgeInsets.all(12.0),
+        child: Column(
+          children: contents,
+        ),
+      ),
+    );
+  }
+}

+ 64 - 99
lib/ui/recovery_page.dart

@@ -6,9 +6,11 @@ import 'package:photos/ui/common_elements.dart';
 import 'package:photos/ui/password_entry_page.dart';
 import 'package:photos/ui/password_entry_page.dart';
 import 'package:photos/utils/dialog_util.dart';
 import 'package:photos/utils/dialog_util.dart';
 import 'package:photos/utils/toast_util.dart';
 import 'package:photos/utils/toast_util.dart';
+import 'package:dotted_border/dotted_border.dart';
 
 
 class RecoveryPage extends StatefulWidget {
 class RecoveryPage extends StatefulWidget {
-  const RecoveryPage({Key key}) : super(key: key);
+  final bool showAppBar;
+  const RecoveryPage({Key key, @required this.showAppBar}) : super(key: key);
 
 
   @override
   @override
   _RecoveryPageState createState() => _RecoveryPageState();
   _RecoveryPageState createState() => _RecoveryPageState();
@@ -20,108 +22,71 @@ class _RecoveryPageState extends State<RecoveryPage> {
   @override
   @override
   Widget build(BuildContext context) {
   Widget build(BuildContext context) {
     return Scaffold(
     return Scaffold(
-      appBar: AppBar(
-        title: Text(
-          "recover account",
-          style: TextStyle(
-            fontSize: 18,
-          ),
-        ),
-      ),
-      body: Column(
-        crossAxisAlignment: CrossAxisAlignment.stretch,
-        mainAxisAlignment: MainAxisAlignment.center,
-        mainAxisSize: MainAxisSize.max,
-        children: [
-          Padding(
-            padding: const EdgeInsets.fromLTRB(60, 0, 60, 0),
-            child: TextFormField(
-              decoration: InputDecoration(
-                hintText: "enter your recovery key",
-                contentPadding: EdgeInsets.all(20),
-              ),
-              style: TextStyle(
-                fontSize: 14,
-                fontFeatures: [FontFeature.tabularFigures()],
-              ),
-              controller: _recoveryKey,
-              autofocus: false,
-              autocorrect: false,
-              keyboardType: TextInputType.multiline,
-              maxLines: null,
-              onChanged: (_) {
-                setState(() {});
-              },
+      appBar: widget.showAppBar
+          ? AppBar(
+              title: Text(""),
+            )
+          : null,
+      body: Padding(
+        padding: const EdgeInsets.fromLTRB(20, 40, 20, 20),
+        child: Column(
+          crossAxisAlignment: CrossAxisAlignment.stretch,
+          // mainAxisAlignment: MainAxisAlignment.center,
+
+          mainAxisSize: MainAxisSize.max,
+          children: [
+            Text("Recovery Key", style: Theme.of(context).textTheme.headline4),
+            Padding(padding: EdgeInsets.all(12)),
+            Text(
+              "If you forget your password, the only way you can recover your data is with this key.",
+              style: Theme.of(context).textTheme.subtitle1,
             ),
             ),
-          ),
-          Padding(padding: EdgeInsets.all(12)),
-          Container(
-            padding: const EdgeInsets.fromLTRB(80, 0, 80, 0),
-            width: double.infinity,
-            height: 64,
-            child: button(
-              "recover",
-              fontSize: 18,
-              onPressed: _recoveryKey.text.isNotEmpty
-                  ? () async {
-                      final dialog =
-                          createProgressDialog(context, "decrypting...");
-                      await dialog.show();
-                      try {
-                        await Configuration.instance
-                            .recover(_recoveryKey.text.trim());
-                        await dialog.hide();
-                        showToast("recovery successful!");
-                        Navigator.of(context).pushReplacement(
-                          MaterialPageRoute(
-                            builder: (BuildContext context) {
-                              return WillPopScope(
-                                onWillPop: () async => false,
-                                child: PasswordEntryPage(
-                                  mode: PasswordEntryMode.reset,
-                                ),
-                              );
-                            },
+            Padding(padding: EdgeInsets.only(top: 24)),
+            DottedBorder(
+              color: Color.fromRGBO(17, 127, 56, 1), //color of dotted/dash line
+              strokeWidth: 1, //thickness of dash/dots
+              dashPattern: const [6, 6],
+              radius: Radius.circular(8),
+              //dash patterns, 10 is dash width, 6 is space width
+              child: SizedBox(
+                //inner container
+                height: 200, //height of inner container
+                width:
+                    double.infinity, //width to 100% match to parent container.
+                // ignore: prefer_const_literals_to_create_immutables
+                child: Column(
+                  children: [
+                    Container(
+                      decoration: BoxDecoration(
+                        border: Border.all(
+                          color: Color.fromRGBO(49, 155, 86, .2),
+                        ),
+                        borderRadius: BorderRadius.all(
+                          Radius.circular(12),
+                        ),
+                        color: Color.fromRGBO(49, 155, 86, .2),
+                      ),
+                      // color: Color.fromRGBO(49, 155, 86, .2),
+                      height: 120,
+                      width: double.infinity,
+                      child: const Text('1'),
+                    ),
+                    SizedBox(
+                      height: 80,
+                      width: double.infinity,
+                      child: Padding(
+                          child: Text(
+                            "we don’t store this key, please save this in a safe place",
+                            style: Theme.of(context).textTheme.bodyText1,
                           ),
                           ),
-                        );
-                      } catch (e) {
-                        await dialog.hide();
-                        String errMessage =
-                            'the recovery key you entered is incorrect';
-                        if (e is AssertionError) {
-                          errMessage = '$errMessage : ${e.message}';
-                        }
-                        showErrorDialog(
-                            context, "incorrect recovery key", errMessage);
-                      }
-                    }
-                  : null,
-            ),
-          ),
-          GestureDetector(
-            behavior: HitTestBehavior.translucent,
-            onTap: () {
-              showErrorDialog(
-                context,
-                "sorry",
-                "due to the nature of our end-to-end encryption protocol, your data cannot be decrypted without your password or recovery key",
-              );
-            },
-            child: Container(
-              padding: EdgeInsets.all(40),
-              child: Center(
-                child: Text(
-                  "no recovery key?",
-                  style: TextStyle(
-                    decoration: TextDecoration.underline,
-                    fontSize: 12,
-                    color: Colors.white.withOpacity(0.9),
-                  ),
+                          padding: EdgeInsets.all(20)),
+                    ),
+                  ],
                 ),
                 ),
               ),
               ),
-            ),
-          ),
-        ],
+            )
+          ],
+        ),
       ),
       ),
     );
     );
   }
   }

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

@@ -133,7 +133,7 @@ class AccountSectionWidgetState extends State<AccountSectionWidget> {
             );
             );
           },
           },
           child: SettingsTextItem(
           child: SettingsTextItem(
-              text: "change password", icon: Icons.navigate_next),
+              text: "Change password", icon: Icons.navigate_next),
         ),
         ),
       ],
       ],
     );
     );

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

@@ -10,6 +10,7 @@ import 'package:photos/services/sync_service.dart';
 import 'package:photos/ui/backup_folder_selection_page.dart';
 import 'package:photos/ui/backup_folder_selection_page.dart';
 import 'package:photos/ui/deduplicate_page.dart';
 import 'package:photos/ui/deduplicate_page.dart';
 import 'package:photos/ui/free_space_page.dart';
 import 'package:photos/ui/free_space_page.dart';
+import 'package:photos/ui/recovery_page.dart';
 import 'package:photos/ui/settings/common_settings.dart';
 import 'package:photos/ui/settings/common_settings.dart';
 import 'package:photos/ui/settings/settings_section_title.dart';
 import 'package:photos/ui/settings/settings_section_title.dart';
 import 'package:photos/ui/settings/settings_text_item.dart';
 import 'package:photos/ui/settings/settings_text_item.dart';
@@ -44,11 +45,14 @@ class BackupSectionWidgetState extends State<BackupSectionWidget> {
           behavior: HitTestBehavior.translucent,
           behavior: HitTestBehavior.translucent,
           onTap: () async {
           onTap: () async {
             routeToPage(
             routeToPage(
-              context,
-              BackupFolderSelectionPage(
-                buttonText: "Backup",
-              ),
-            );
+                context,
+                RecoveryPage(
+                  showAppBar: true,
+                )
+                // BackupFolderSelectionPage(
+                //   buttonText: "Backup",
+                // ),
+                );
           },
           },
           child: SettingsTextItem(
           child: SettingsTextItem(
               text: "Backed up folders", icon: Icons.navigate_next),
               text: "Backed up folders", icon: Icons.navigate_next),
@@ -59,7 +63,10 @@ class BackupSectionWidgetState extends State<BackupSectionWidget> {
           child: Row(
           child: Row(
             mainAxisAlignment: MainAxisAlignment.spaceBetween,
             mainAxisAlignment: MainAxisAlignment.spaceBetween,
             children: [
             children: [
-              Text("Backup over mobile data"),
+              Text(
+                "Backup over mobile data",
+                style: Theme.of(context).textTheme.subtitle1,
+              ),
               Switch.adaptive(
               Switch.adaptive(
                 value: Configuration.instance.shouldBackupOverMobileData(),
                 value: Configuration.instance.shouldBackupOverMobileData(),
                 onChanged: (value) async {
                 onChanged: (value) async {
@@ -76,7 +83,10 @@ class BackupSectionWidgetState extends State<BackupSectionWidget> {
           child: Row(
           child: Row(
             mainAxisAlignment: MainAxisAlignment.spaceBetween,
             mainAxisAlignment: MainAxisAlignment.spaceBetween,
             children: [
             children: [
-              Text("Backup videos"),
+              Text(
+                "Backup videos",
+                style: Theme.of(context).textTheme.subtitle1,
+              ),
               Switch.adaptive(
               Switch.adaptive(
                 value: Configuration.instance.shouldBackupVideos(),
                 value: Configuration.instance.shouldBackupVideos(),
                 onChanged: (value) async {
                 onChanged: (value) async {

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

@@ -70,7 +70,7 @@ class _SecuritySectionWidgetState extends State<SecuritySectionWidget> {
             child: Row(
             child: Row(
               mainAxisAlignment: MainAxisAlignment.spaceBetween,
               mainAxisAlignment: MainAxisAlignment.spaceBetween,
               children: [
               children: [
-                Text("Two-factor"),
+                Text("Two-factor", style: Theme.of(context).textTheme.subtitle1,),
                 FutureBuilder(
                 FutureBuilder(
                   future: UserService.instance.fetchTwoFactorStatus(),
                   future: UserService.instance.fetchTwoFactorStatus(),
                   builder: (_, snapshot) {
                   builder: (_, snapshot) {
@@ -117,7 +117,7 @@ class _SecuritySectionWidgetState extends State<SecuritySectionWidget> {
         child: Row(
         child: Row(
           mainAxisAlignment: MainAxisAlignment.spaceBetween,
           mainAxisAlignment: MainAxisAlignment.spaceBetween,
           children: [
           children: [
-            Text("Lockscreen"),
+            Text("Lockscreen", style: Theme.of(context).textTheme.subtitle1,),
             Switch.adaptive(
             Switch.adaptive(
               value: _config.shouldShowLockScreen(),
               value: _config.shouldShowLockScreen(),
               onChanged: (value) async {
               onChanged: (value) async {
@@ -147,7 +147,7 @@ class _SecuritySectionWidgetState extends State<SecuritySectionWidget> {
             child: Row(
             child: Row(
               mainAxisAlignment: MainAxisAlignment.spaceBetween,
               mainAxisAlignment: MainAxisAlignment.spaceBetween,
               children: [
               children: [
-                Text("Hide from recents"),
+                Text("Hide from recents", style: Theme.of(context).textTheme.subtitle1),
                 Switch.adaptive(
                 Switch.adaptive(
                   value: _config.shouldHideFromRecents(),
                   value: _config.shouldHideFromRecents(),
                   onChanged: (value) async {
                   onChanged: (value) async {

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

@@ -35,7 +35,7 @@ class SocialSectionWidget extends StatelessWidget {
         GestureDetector(
         GestureDetector(
           behavior: HitTestBehavior.translucent,
           behavior: HitTestBehavior.translucent,
           onTap: () {
           onTap: () {
-            launch("https://discord.gg/uRqua3jSr5");
+            launch("https://ente.io/discord");
           },
           },
           child: SettingsTextItem(text: "Discord", icon: Icons.navigate_next),
           child: SettingsTextItem(text: "Discord", icon: Icons.navigate_next),
         ),
         ),

+ 0 - 1
lib/ui/settings_page.dart

@@ -1,6 +1,5 @@
 import 'package:flutter/foundation.dart';
 import 'package:flutter/foundation.dart';
 import 'package:flutter/material.dart';
 import 'package:flutter/material.dart';
-import 'package:flutter/widgets.dart';
 import 'package:photos/core/configuration.dart';
 import 'package:photos/core/configuration.dart';
 import 'package:photos/ui/settings/account_section_widget.dart';
 import 'package:photos/ui/settings/account_section_widget.dart';
 import 'package:photos/ui/settings/app_version_widget.dart';
 import 'package:photos/ui/settings/app_version_widget.dart';

+ 1 - 0
lib/ui/share_collection_widget.dart

@@ -138,6 +138,7 @@ class _SharingDialogState extends State<SharingDialog> {
             ],
             ],
           ),
           ),
         ),
         ),
+        Padding(padding: EdgeInsets.all(8)),
       ]);
       ]);
       if (widget.collection.publicURLs?.isNotEmpty ?? false) {
       if (widget.collection.publicURLs?.isNotEmpty ?? false) {
         children.add(Padding(
         children.add(Padding(

+ 29 - 11
lib/ui/thumbnail_widget.dart

@@ -21,6 +21,7 @@ class ThumbnailWidget extends StatefulWidget {
   final File file;
   final File file;
   final BoxFit fit;
   final BoxFit fit;
   final bool shouldShowSyncStatus;
   final bool shouldShowSyncStatus;
+  final bool shouldShowArchiveStatus;
   final bool shouldShowLivePhotoOverlay;
   final bool shouldShowLivePhotoOverlay;
   final Duration diskLoadDeferDuration;
   final Duration diskLoadDeferDuration;
   final Duration serverLoadDeferDuration;
   final Duration serverLoadDeferDuration;
@@ -31,6 +32,7 @@ class ThumbnailWidget extends StatefulWidget {
     this.fit = BoxFit.cover,
     this.fit = BoxFit.cover,
     this.shouldShowSyncStatus = true,
     this.shouldShowSyncStatus = true,
     this.shouldShowLivePhotoOverlay = false,
     this.shouldShowLivePhotoOverlay = false,
+    this.shouldShowArchiveStatus = false,
     this.diskLoadDeferDuration,
     this.diskLoadDeferDuration,
     this.serverLoadDeferDuration,
     this.serverLoadDeferDuration,
   }) : super(key: key ?? Key(file.tag()));
   }) : super(key: key ?? Key(file.tag()));
@@ -74,6 +76,18 @@ class _ThumbnailWidgetState extends State<ThumbnailWidget> {
     ),
     ),
   );
   );
 
 
+  static final kArchiveIconOverlay = Align(
+    alignment: Alignment.bottomRight,
+    child: Padding(
+      padding: const EdgeInsets.only(right: 8, bottom: 8),
+      child: Icon(
+        Icons.archive_outlined,
+        size: 42,
+        color: Colors.white.withOpacity(0.9),
+      ),
+    ),
+  );
+
   static final kUnsyncedIconOverlay = Container(
   static final kUnsyncedIconOverlay = Container(
     decoration: BoxDecoration(
     decoration: BoxDecoration(
       gradient: LinearGradient(
       gradient: LinearGradient(
@@ -173,18 +187,22 @@ class _ThumbnailWidgetState extends State<ThumbnailWidget> {
         content = image;
         content = image;
       }
       }
     }
     }
+    List<Widget> viewChildrens = [
+      loadingWidget,
+      AnimatedOpacity(
+        opacity: content == null ? 0 : 1.0,
+        duration: Duration(milliseconds: 200),
+        child: content,
+      ),
+      widget.shouldShowSyncStatus && widget.file.uploadedFileID == null
+          ? kUnsyncedIconOverlay
+          : getFileInfoContainer(widget.file),
+    ];
+    if (widget.shouldShowArchiveStatus) {
+      viewChildrens.add(kArchiveIconOverlay);
+    }
     return Stack(
     return Stack(
-      children: [
-        loadingWidget,
-        AnimatedOpacity(
-          opacity: content == null ? 0 : 1.0,
-          duration: Duration(milliseconds: 200),
-          child: content,
-        ),
-        widget.shouldShowSyncStatus && widget.file.uploadedFileID == null
-            ? kUnsyncedIconOverlay
-            : getFileInfoContainer(widget.file),
-      ],
+      children: viewChildrens,
       fit: StackFit.expand,
       fit: StackFit.expand,
     );
     );
   }
   }

+ 8 - 1
lib/ui/video_widget.dart

@@ -5,6 +5,7 @@ import 'package:flutter/cupertino.dart';
 import 'package:flutter/material.dart';
 import 'package:flutter/material.dart';
 import 'package:flutter/widgets.dart';
 import 'package:flutter/widgets.dart';
 import 'package:logging/logging.dart';
 import 'package:logging/logging.dart';
+import 'package:photos/core/constants.dart';
 import 'package:photos/models/file.dart';
 import 'package:photos/models/file.dart';
 import 'package:photos/ui/thumbnail_widget.dart';
 import 'package:photos/ui/thumbnail_widget.dart';
 import 'package:photos/ui/video_controls.dart';
 import 'package:photos/ui/video_controls.dart';
@@ -118,6 +119,12 @@ class _VideoWidgetState extends State<VideoWidget> {
             _videoPlayerController.value.isInitialized
             _videoPlayerController.value.isInitialized
         ? _getVideoPlayer()
         ? _getVideoPlayer()
         : _getLoadingWidget();
         : _getLoadingWidget();
+    final contentWithDetector = GestureDetector(
+      child: content,
+      onVerticalDragUpdate: (d) => {
+        if (d.delta.dy > kDragSensitivity) {Navigator.of(context).pop()}
+      },
+    );
     return VisibilityDetector(
     return VisibilityDetector(
       key: Key(widget.file.tag()),
       key: Key(widget.file.tag()),
       onVisibilityChanged: (info) {
       onVisibilityChanged: (info) {
@@ -129,7 +136,7 @@ class _VideoWidgetState extends State<VideoWidget> {
       },
       },
       child: Hero(
       child: Hero(
         tag: widget.tagPrefix + widget.file.tag(),
         tag: widget.tagPrefix + widget.file.tag(),
-        child: content,
+        child: contentWithDetector,
       ),
       ),
     );
     );
   }
   }

+ 17 - 2
lib/ui/zoomable_image.dart

@@ -43,6 +43,7 @@ class _ZoomableImageState extends State<ZoomableImage>
   bool _loadingFinalImage = false;
   bool _loadingFinalImage = false;
   bool _loadedFinalImage = false;
   bool _loadedFinalImage = false;
   ValueChanged<PhotoViewScaleState> _scaleStateChangedCallback;
   ValueChanged<PhotoViewScaleState> _scaleStateChangedCallback;
+  bool _isZooming = false;
 
 
   @override
   @override
   void initState() {
   void initState() {
@@ -51,6 +52,8 @@ class _ZoomableImageState extends State<ZoomableImage>
       if (widget.shouldDisableScroll != null) {
       if (widget.shouldDisableScroll != null) {
         widget.shouldDisableScroll(value != PhotoViewScaleState.initial);
         widget.shouldDisableScroll(value != PhotoViewScaleState.initial);
       }
       }
+      _isZooming = value != PhotoViewScaleState.initial;
+      // _logger.info('is reakky zooming $_isZooming with state $value');
     };
     };
     super.initState();
     super.initState();
   }
   }
@@ -62,9 +65,10 @@ class _ZoomableImageState extends State<ZoomableImage>
     } else {
     } else {
       _loadLocalImage(context);
       _loadLocalImage(context);
     }
     }
+    Widget content;
 
 
     if (_imageProvider != null) {
     if (_imageProvider != null) {
-      return PhotoView(
+      content = PhotoView(
         imageProvider: _imageProvider,
         imageProvider: _imageProvider,
         scaleStateChangedCallback: _scaleStateChangedCallback,
         scaleStateChangedCallback: _scaleStateChangedCallback,
         minScale: PhotoViewComputedScale.contained,
         minScale: PhotoViewComputedScale.contained,
@@ -75,8 +79,19 @@ class _ZoomableImageState extends State<ZoomableImage>
         backgroundDecoration: widget.backgroundDecoration,
         backgroundDecoration: widget.backgroundDecoration,
       );
       );
     } else {
     } else {
-      return loadWidget;
+      content = loadWidget;
     }
     }
+
+    GestureDragUpdateCallback verticalDragCallback = _isZooming
+        ? null
+        : (d) => {
+              if (!_isZooming && d.delta.dy > kDragSensitivity)
+                {Navigator.of(context).pop()}
+            };
+    return GestureDetector(
+      child: content,
+      onVerticalDragUpdate: verticalDragCallback,
+    );
   }
   }
 
 
   void _loadNetworkImage() {
   void _loadNetworkImage() {

+ 125 - 0
lib/utils/dialog_util.dart

@@ -101,3 +101,128 @@ Future<T> showConfettiDialog<T>({
     routeSettings: routeSettings,
     routeSettings: routeSettings,
   );
   );
 }
 }
+
+Widget test() {
+  return Container(
+    width: 355,
+    height: 236,
+    child: Row(
+      mainAxisSize: MainAxisSize.min,
+      mainAxisAlignment: MainAxisAlignment.center,
+      crossAxisAlignment: CrossAxisAlignment.center,
+      children: [
+        Container(
+          width: 355,
+          height: 236,
+          decoration: BoxDecoration(
+            borderRadius: BorderRadius.circular(8),
+            color: Color(0x4c000000),
+          ),
+          padding: const EdgeInsets.only(
+            top: 20,
+            bottom: 16,
+          ),
+          child: Column(
+            mainAxisSize: MainAxisSize.min,
+            mainAxisAlignment: MainAxisAlignment.end,
+            crossAxisAlignment: CrossAxisAlignment.center,
+            children: [
+              SizedBox(
+                width: 234,
+                child: Text(
+                  "Are you sure you want to logout?",
+                  style: TextStyle(
+                    color: Colors.white,
+                    fontSize: 24,
+                    fontFamily: "SF Pro Display",
+                    fontWeight: FontWeight.w600,
+                  ),
+                ),
+              ),
+              SizedBox(height: 16),
+              Container(
+                width: 323,
+                height: 48,
+                child: Row(
+                  mainAxisSize: MainAxisSize.min,
+                  mainAxisAlignment: MainAxisAlignment.center,
+                  crossAxisAlignment: CrossAxisAlignment.center,
+                  children: [
+                    Container(
+                      width: 323,
+                      height: 48,
+                      decoration: BoxDecoration(
+                        borderRadius: BorderRadius.circular(8),
+                        color: Color(0x4c000000),
+                      ),
+                      padding: const EdgeInsets.only(
+                        left: 135,
+                        right: 136,
+                      ),
+                      child: Row(
+                        mainAxisSize: MainAxisSize.min,
+                        mainAxisAlignment: MainAxisAlignment.center,
+                        crossAxisAlignment: CrossAxisAlignment.center,
+                        children: [
+                          Text(
+                            "Cancel",
+                            style: TextStyle(
+                              color: Colors.white,
+                              fontSize: 16,
+                              fontFamily: "SF Pro Text",
+                              fontWeight: FontWeight.w500,
+                            ),
+                          ),
+                        ],
+                      ),
+                    ),
+                  ],
+                ),
+              ),
+              SizedBox(height: 16),
+              Container(
+                width: 323,
+                height: 48,
+                child: Row(
+                  mainAxisSize: MainAxisSize.min,
+                  mainAxisAlignment: MainAxisAlignment.center,
+                  crossAxisAlignment: CrossAxisAlignment.center,
+                  children: [
+                    Container(
+                      width: 323,
+                      height: 48,
+                      decoration: BoxDecoration(
+                        borderRadius: BorderRadius.circular(8),
+                        color: Colors.white,
+                      ),
+                      padding: const EdgeInsets.symmetric(
+                        horizontal: 120,
+                        vertical: 12,
+                      ),
+                      child: Row(
+                        mainAxisSize: MainAxisSize.min,
+                        mainAxisAlignment: MainAxisAlignment.center,
+                        crossAxisAlignment: CrossAxisAlignment.center,
+                        children: [
+                          Text(
+                            "Yes Logout",
+                            style: TextStyle(
+                              color: Colors.black,
+                              fontSize: 16,
+                              fontFamily: "SF Pro Text",
+                              fontWeight: FontWeight.w500,
+                            ),
+                          ),
+                        ],
+                      ),
+                    ),
+                  ],
+                ),
+              ),
+            ],
+          ),
+        ),
+      ],
+    ),
+  );
+}

+ 24 - 0
lib/utils/magic_util.dart

@@ -4,8 +4,10 @@ import 'package:logging/logging.dart';
 import 'package:path/path.dart';
 import 'package:path/path.dart';
 import 'package:photos/core/event_bus.dart';
 import 'package:photos/core/event_bus.dart';
 import 'package:photos/events/force_reload_home_gallery_event.dart';
 import 'package:photos/events/force_reload_home_gallery_event.dart';
+import 'package:photos/models/collection.dart';
 import 'package:photos/models/file.dart';
 import 'package:photos/models/file.dart';
 import 'package:photos/models/magic_metadata.dart';
 import 'package:photos/models/magic_metadata.dart';
+import 'package:photos/services/collections_service.dart';
 import 'package:photos/services/file_magic_service.dart';
 import 'package:photos/services/file_magic_service.dart';
 import 'package:photos/ui/rename_dialog.dart';
 import 'package:photos/ui/rename_dialog.dart';
 import 'package:photos/utils/dialog_util.dart';
 import 'package:photos/utils/dialog_util.dart';
@@ -32,6 +34,28 @@ Future<void> changeVisibility(
   }
   }
 }
 }
 
 
+Future<void> changeCollectionVisibility(
+    BuildContext context, Collection collection, int newVisibility) async {
+  final dialog = createProgressDialog(context,
+      newVisibility == kVisibilityArchive ? "archiving..." : "unarchiving...");
+  await dialog.show();
+  try {
+    Map<String, dynamic> update = {kMagicKeyVisibility: newVisibility};
+    await CollectionsService.instance.updateMagicMetadata(collection, update);
+    // Force reload home gallery to pull in the now unarchived files
+    Bus.instance.fire(ForceReloadHomeGalleryEvent());
+    showShortToast(newVisibility == kVisibilityArchive
+        ? "successfully archived"
+        : "successfully unarchived");
+
+    await dialog.hide();
+  } catch (e, s) {
+    _logger.severe("failed to update collection visibility", e, s);
+    await dialog.hide();
+    rethrow;
+  }
+}
+
 Future<bool> editTime(
 Future<bool> editTime(
     BuildContext context, List<File> files, int editedTime) async {
     BuildContext context, List<File> files, int editedTime) async {
   try {
   try {

+ 22 - 1
pubspec.lock

@@ -232,6 +232,13 @@ packages:
       url: "https://pub.dartlang.org"
       url: "https://pub.dartlang.org"
     source: hosted
     source: hosted
     version: "2.0.0"
     version: "2.0.0"
+  dotted_border:
+    dependency: "direct main"
+    description:
+      name: dotted_border
+      url: "https://pub.dartlang.org"
+    source: hosted
+    version: "2.0.0+2"
   email_validator:
   email_validator:
     dependency: "direct main"
     dependency: "direct main"
     description:
     description:
@@ -397,7 +404,7 @@ packages:
       name: flutter_email_sender
       name: flutter_email_sender
       url: "https://pub.dartlang.org"
       url: "https://pub.dartlang.org"
     source: hosted
     source: hosted
-    version: "5.1.0"
+    version: "5.0.2"
   flutter_image_compress:
   flutter_image_compress:
     dependency: "direct main"
     dependency: "direct main"
     description:
     description:
@@ -786,6 +793,20 @@ packages:
       url: "https://pub.dartlang.org"
       url: "https://pub.dartlang.org"
     source: hosted
     source: hosted
     version: "1.8.0"
     version: "1.8.0"
+  path_drawing:
+    dependency: transitive
+    description:
+      name: path_drawing
+      url: "https://pub.dartlang.org"
+    source: hosted
+    version: "1.0.0"
+  path_parsing:
+    dependency: transitive
+    description:
+      name: path_parsing
+      url: "https://pub.dartlang.org"
+    source: hosted
+    version: "1.0.0"
   path_provider:
   path_provider:
     dependency: "direct main"
     dependency: "direct main"
     description:
     description:

+ 4 - 2
pubspec.yaml

@@ -11,7 +11,7 @@ description: ente photos application
 # In iOS, build-name is used as CFBundleShortVersionString while build-number used as CFBundleVersion.
 # In iOS, build-name is used as CFBundleShortVersionString while build-number used as CFBundleVersion.
 # Read more about iOS versioning at
 # Read more about iOS versioning at
 # https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
 # https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
-version: 0.5.6+285
+version: 0.5.11+291
 
 
 environment:
 environment:
   sdk: ">=2.10.0 <3.0.0"
   sdk: ">=2.10.0 <3.0.0"
@@ -33,6 +33,7 @@ dependencies:
   device_info: ^2.0.2
   device_info: ^2.0.2
   dio: ^4.0.0
   dio: ^4.0.0
   dots_indicator: ^2.0.0
   dots_indicator: ^2.0.0
+  dotted_border: ^2.0.0+2
   email_validator: ^2.0.1
   email_validator: ^2.0.1
   event_bus: ^2.0.0
   event_bus: ^2.0.0
   exif: ^3.0.0
   exif: ^3.0.0
@@ -49,7 +50,7 @@ dependencies:
   flutter_cache_manager: ^3.3.0
   flutter_cache_manager: ^3.3.0
   flutter_datetime_picker: ^1.5.1
   flutter_datetime_picker: ^1.5.1
   flutter_easyloading: ^3.0.0
   flutter_easyloading: ^3.0.0
-  flutter_email_sender: ^5.1.0
+  flutter_email_sender: 5.0.2
   flutter_image_compress: ^1.1.0
   flutter_image_compress: ^1.1.0
   flutter_inappwebview: ^5.3.2
   flutter_inappwebview: ^5.3.2
   flutter_local_notifications: ^5.0.0+4
   flutter_local_notifications: ^5.0.0+4
@@ -151,6 +152,7 @@ flutter:
   - family: Inter
   - family: Inter
     fonts:
     fonts:
       - asset: fonts/Inter-Regular.ttf
       - asset: fonts/Inter-Regular.ttf
+      - asset: fonts/Inter-Medium.ttf
       - asset: fonts/Inter-Light.ttf
       - asset: fonts/Inter-Light.ttf
       - asset: fonts/Inter-SemiBold.ttf
       - asset: fonts/Inter-SemiBold.ttf
       - asset: fonts/Inter-Bold.ttf
       - asset: fonts/Inter-Bold.ttf

+ 1 - 0
thirdparty/extended_image

@@ -0,0 +1 @@
+Subproject commit 112cc8be0c2f07129edd7575003a3f9ddb62ae3f

+ 1 - 1
thirdparty/super_logging/lib/super_logging.dart

@@ -162,7 +162,7 @@ class SuperLogging {
       $.info("detected debug mode; sentry & file logging disabled.");
       $.info("detected debug mode; sentry & file logging disabled.");
     }
     }
     if (fileIsEnabled) {
     if (fileIsEnabled) {
-      $.info("using this log file for today: $logFile");
+      $.info("log file for today: $logFile with prefix ${config.prefix}");
     }
     }
     if (sentryIsEnabled) {
     if (sentryIsEnabled) {
       $.info("sentry uploader started");
       $.info("sentry uploader started");