diff --git a/README.md b/README.md index 98717f783..b3890d916 100644 --- a/README.md +++ b/README.md @@ -5,23 +5,31 @@ We have open-source apps across Android, iOS, web and desktop that automatically backup your photos and videos. This repository contains the code for our mobile apps, built with a lot of ❤️, and a little bit of [Flutter.](https://flutter.dev) -![App Screenshots](https://user-images.githubusercontent.com/1161789/154794909-c391f947-266f-4298-956b-a67b5eb9a169.png) + +![App Screenshots](https://user-images.githubusercontent.com/24503581/175218240-fe5a0703-82c1-4750-bfea-abfd9f409a97.png) + +
## ✨ Features - Client side encryption (only you can view your photos and videos) - Background sync +- Family plans - Shareable links for albums - Highlights of memories from previous years - Ability to detect and delete duplicate files +- Light and dark mode - Image editor - EXIF viewer - Ability to free up disk space by deleting backed up photos +- Support for Live Photos - Recycle bin - 2FA - Lockscreen - Zero third-party tracking / analytics +
+ ## 📲 Installation ### Android @@ -37,12 +45,14 @@ You can alternatively install the build from PlayStore or F-Droid. Get it on F-Droid - ### iOS + Download on AppStore +
+
## 🧑‍💻 Building from source @@ -52,22 +62,30 @@ You can alternatively install the build from PlayStore or F-Droid. 4. For Android, run `flutter build apk --release --flavor independent` 5. For iOS, run `flutter build ios` +
+ ## 🙋 Help We provide human support to our customers. Please write to [support@ente.io](mailto:support@ente.io) sharing as many details as possible about whatever it is that you need help with, and we will get back to you as soon as possible. +
+ ## 🧭 Roadmap We maintain a public roadmap, that's driven by our community @ [roadmap.ente.io](https://roadmap.ente.io). +
+ ## 🤗 Support If you like this project, please consider upgrading to a paid subscription. If you would like to motivate us to keep building, you can do so by [starring](https://github.com/ente-io/frame/stargazers) this project. +
+ ## ❤️ Join the Community -Follow us on [Twitter](https://twitter.com/enteio) and join [r/enteio](https://reddit.com/r/enteio) to get regular updates, connect with other customers, and discuss your ideas. +Follow us on [Twitter](https://twitter.com/enteio), join [r/enteio](https://reddit.com/r/enteio) or hang out on our [Discord](https://ente.io/discord) to get regular updates, connect with other customers, and discuss your ideas. An important part of our journey is to build better software by consistently listening to community feedback. Please feel free to [share your thoughts](mailto:feedback@ente.io) with us at any time. diff --git a/android/app/build.gradle b/android/app/build.gradle index 9a9942287..48c96b9f0 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -72,6 +72,11 @@ android { playstore { dimension "default" } + fdroid { + dimension "default" + applicationIdSuffix ".fdroid" + signingConfig null + } } buildTypes { @@ -88,6 +93,14 @@ android { } } } + + android.applicationVariants.all { variant -> + if (variant.flavorName == "fdroid") { + variant.outputs.all { output -> + output.outputFileName = "app-fdroid-release.apk" + } + } + } } rootProject.allprojects { diff --git a/android/app/src/fdroid/AndroidManifest.xml b/android/app/src/fdroid/AndroidManifest.xml new file mode 100644 index 000000000..2d9699753 --- /dev/null +++ b/android/app/src/fdroid/AndroidManifest.xml @@ -0,0 +1,4 @@ + + + diff --git a/android/permissions.md b/android/permissions.md new file mode 100644 index 000000000..4d3e1f424 --- /dev/null +++ b/android/permissions.md @@ -0,0 +1,37 @@ +## Android Permissions + +> android.permission.READ_EXTERNAL_STORAGE + + **Used to read photos and videos from the device.** + +> android.permission.READ_CONTACTS + +**Used when a customer tries to pick a contact in the phonebook to share an album with. This is an optional permission, which users can deny if their android version allows granting granular permission.** + +> android.permission.ACCESS_MEDIA_LOCATION + +**Used to extract the coordinates a photo/video was captured in. This information is encrypted with the customer's key before being sent to our servers.** + +> android.permission.WRITE_EXTERNAL_STORAGE + +**Used for downloading photos to the disk.** + +> android.permission.USE_BIOMETRIC + +**Used to optionally lock the app behind the default lock screen.** + +> android.permission.USE_FINGERPRINT + +Used to optionally lock the app behind the default lock screen. + +> android.permission.RECEIVE_BOOT_COMPLETED + +**Used to trigger background uploads for photos and videos that were clicked.** + +> android.permission.USE_FULL_SCREEN_INTENT + +**This is needed by the local notification library, to show notifications about your previous memories.** + +> android.permission.SET_WALLPAPER + +**This allows the user to set a particular photo as wallpaper.** \ No newline at end of file diff --git a/fastlane/metadata/android/en-US/changelogs/293.txt b/fastlane/metadata/android/en-US/changelogs/293.txt new file mode 100644 index 000000000..bbdcbdc37 --- /dev/null +++ b/fastlane/metadata/android/en-US/changelogs/293.txt @@ -0,0 +1 @@ +- Hello, FDroid! \ No newline at end of file diff --git a/fastlane/metadata/android/en-US/changelogs/317.txt b/fastlane/metadata/android/en-US/changelogs/317.txt new file mode 100644 index 000000000..db137699a --- /dev/null +++ b/fastlane/metadata/android/en-US/changelogs/317.txt @@ -0,0 +1 @@ +- Resync files with missing location data on Android 10+ \ No newline at end of file diff --git a/fastlane/metadata/android/en-US/changelogs/330.txt b/fastlane/metadata/android/en-US/changelogs/330.txt new file mode 100644 index 000000000..91dd765cd --- /dev/null +++ b/fastlane/metadata/android/en-US/changelogs/330.txt @@ -0,0 +1 @@ +New pixels! \ No newline at end of file diff --git a/fastlane/metadata/android/en-US/changelogs/331.txt b/fastlane/metadata/android/en-US/changelogs/331.txt new file mode 100644 index 000000000..16f5ad565 --- /dev/null +++ b/fastlane/metadata/android/en-US/changelogs/331.txt @@ -0,0 +1,5 @@ +Major release with fresh new pixels! ✨ + +- Redesigned app look with support for light and dark modes +- Performance improvements to speed up bulk file deletes +- New swipe gestures to quickly switch top level tabs and navigate between screens diff --git a/fastlane/metadata/android/en-US/changelogs/333.txt b/fastlane/metadata/android/en-US/changelogs/333.txt new file mode 100644 index 000000000..50c9efaa2 --- /dev/null +++ b/fastlane/metadata/android/en-US/changelogs/333.txt @@ -0,0 +1,5 @@ +Major release with fresh new pixels! ✨ + +- Redesigned app look with support for light and dark modes +- New swipe gestures to quickly switch between tabs and to navigate between screens +- Performance improvements to speed up bulk file deletes diff --git a/fastlane/metadata/android/en-US/full_description.txt b/fastlane/metadata/android/en-US/full_description.txt index 4de7387ca..71a133530 100644 --- a/fastlane/metadata/android/en-US/full_description.txt +++ b/fastlane/metadata/android/en-US/full_description.txt @@ -1,11 +1,33 @@ -ente provides a simple way to back up your memories. +Ente is a simple app to automatically backup and organize your photos and videos. -ente encrypts your photos and videos with your password before backing them up to the cloud, so only you can view them. +If you've been looking for a privacy-friendly alternative to preserve your memories, you've come to the right place. With Ente, they are stored end-to-end encrypted (e2ee). This means that only you can view them. -ente stores your encrypted data across multiple locations, including an underground fall out shelter, so your memories are preserved forever. +We have apps across Android, iOS, web and Desktop, and your photos will seamlessly sync between all your devices in an end-to-end encrypted (e2ee) manner. -ente lets you share your albums and folders with your loved ones, end-to-end encrypted. +Ente also makes it simple to share your albums with your loved ones. You can either share them directly with other Ente users, end-to-end encrypted; or with publicly viewable links. -ente has an open architecture and source code that has been peer-reviewed trusted. +Your encrypted data is stored across multiple locations, including a fall-out shelter in Paris. We take posterity seriously and make it easy to ensure that your memories outlive you. -ente is available on android, ios and the web. +We are here to make the safest photos app ever, come join our journey! + +FEATURES +- Original quality backups, because every pixel is important +- Family plans, so you can share storage with your family +- Shared folders, in case you want your partner to enjoy your "Camera" clicks +- Album links, that can be protected with a password and set to expire +- Ability to free up space, by removing files that have been safely backed up +- Image editor, to add finishing touches +- Favorite, hide and relive your memories, for they are precious +- One-click import from Google, Apple, your hard drive and more +- Dark theme, because your photos look good in it +- 2FA, 3FA, biometric auth +- and a LOT more! + +PERMISSIONS +Ente requests for certain permissions to serve the purpose of a photo storage provider, which can be reviewed here: https://github.com/ente-io/frame/blob/f-droid/android/permissions.md + +PRICING +We don't offer forever free plans, because it is important to us that we remain sustainable and withstand the test of time. Instead we offer affordable plans that you can freely share with your family. You can find more information at ente.io. + +SUPPORT +We take pride in offering human support. If you are our paid customer, you can reach out to team@ente.io and expect a response from our team within 24 hours. diff --git a/fastlane/metadata/android/en-US/images/icon.png b/fastlane/metadata/android/en-US/images/icon.png index dd45f4cd5..8b32155bb 100644 Binary files a/fastlane/metadata/android/en-US/images/icon.png and b/fastlane/metadata/android/en-US/images/icon.png differ diff --git a/fastlane/metadata/android/en-US/images/phoneScreenshots/1.png b/fastlane/metadata/android/en-US/images/phoneScreenshots/1.png index 2d78548ef..7cab0bfe4 100644 Binary files a/fastlane/metadata/android/en-US/images/phoneScreenshots/1.png and b/fastlane/metadata/android/en-US/images/phoneScreenshots/1.png differ diff --git a/fastlane/metadata/android/en-US/images/phoneScreenshots/2.png b/fastlane/metadata/android/en-US/images/phoneScreenshots/2.png index 464ee15d7..15e875465 100644 Binary files a/fastlane/metadata/android/en-US/images/phoneScreenshots/2.png and b/fastlane/metadata/android/en-US/images/phoneScreenshots/2.png differ diff --git a/fastlane/metadata/android/en-US/images/phoneScreenshots/3.png b/fastlane/metadata/android/en-US/images/phoneScreenshots/3.png index 77f216a31..91fb9da73 100644 Binary files a/fastlane/metadata/android/en-US/images/phoneScreenshots/3.png and b/fastlane/metadata/android/en-US/images/phoneScreenshots/3.png differ diff --git a/fastlane/metadata/android/en-US/images/phoneScreenshots/4.png b/fastlane/metadata/android/en-US/images/phoneScreenshots/4.png index 041457697..cd47572c9 100644 Binary files a/fastlane/metadata/android/en-US/images/phoneScreenshots/4.png and b/fastlane/metadata/android/en-US/images/phoneScreenshots/4.png differ diff --git a/fastlane/metadata/android/en-US/images/phoneScreenshots/5.png b/fastlane/metadata/android/en-US/images/phoneScreenshots/5.png index 84eb8117a..4e5ca0bad 100644 Binary files a/fastlane/metadata/android/en-US/images/phoneScreenshots/5.png and b/fastlane/metadata/android/en-US/images/phoneScreenshots/5.png differ diff --git a/fastlane/metadata/android/en-US/images/phoneScreenshots/6.png b/fastlane/metadata/android/en-US/images/phoneScreenshots/6.png new file mode 100644 index 000000000..39cfa966d Binary files /dev/null and b/fastlane/metadata/android/en-US/images/phoneScreenshots/6.png differ diff --git a/fastlane/metadata/android/en-US/images/phoneScreenshots/7.png b/fastlane/metadata/android/en-US/images/phoneScreenshots/7.png new file mode 100644 index 000000000..0b4422657 Binary files /dev/null and b/fastlane/metadata/android/en-US/images/phoneScreenshots/7.png differ diff --git a/lib/app.dart b/lib/app.dart index a2eebbc05..f12db02d0 100644 --- a/lib/app.dart +++ b/lib/app.dart @@ -2,7 +2,6 @@ import 'dart:io'; import 'package:adaptive_theme/adaptive_theme.dart'; import 'package:background_fetch/background_fetch.dart'; -import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter_easyloading/flutter_easyloading.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; @@ -15,221 +14,6 @@ import 'package:photos/services/app_lifecycle_service.dart'; import 'package:photos/services/sync_service.dart'; import 'package:photos/ui/home_widget.dart'; -final lightThemeData = ThemeData( - fontFamily: 'Inter', - brightness: Brightness.light, - hintColor: Colors.grey, - primaryColor: Colors.deepOrangeAccent, - primaryColorLight: Colors.black54, - iconTheme: IconThemeData(color: Colors.black), - primaryIconTheme: IconThemeData(color: Colors.red, opacity: 1.0, size: 50.0), - colorScheme: ColorScheme.light( - primary: Colors.black, - secondary: Color.fromARGB(255, 163, 163, 163), - ), - accentColor: Color.fromRGBO(0, 0, 0, 0.6), - buttonColor: Color.fromRGBO(45, 194, 98, 1.0), - outlinedButtonTheme: buildOutlinedButtonThemeData( - bgDisabled: Colors.grey.shade500, - bgEnabled: Colors.black, - fgDisabled: Colors.white, - fgEnabled: Colors.white, - ), - elevatedButtonTheme: buildElevatedButtonThemeData( - onPrimary: Colors.white, - primary: Colors.black, - ), - toggleableActiveColor: Colors.green[400], - scaffoldBackgroundColor: Colors.white, - backgroundColor: Colors.white, - appBarTheme: AppBarTheme().copyWith( - backgroundColor: Colors.white, - foregroundColor: Colors.black, - iconTheme: IconThemeData(color: Colors.black), - elevation: 0, - ), - //https://api.flutter.dev/flutter/material/TextTheme-class.html - textTheme: _buildTextTheme(Colors.black), - primaryTextTheme: TextTheme().copyWith( - bodyText2: TextStyle(color: Colors.yellow), - bodyText1: TextStyle(color: Colors.orange), - ), - cardColor: Color.fromRGBO(250, 250, 250, 1.0), - dialogTheme: DialogTheme().copyWith( - backgroundColor: Color.fromRGBO(250, 250, 250, 1.0), // - titleTextStyle: TextStyle( - color: Colors.black, - fontSize: 24, - fontWeight: FontWeight.w600, - ), - contentTextStyle: TextStyle( - fontFamily: 'Inter-Medium', - color: Colors.black, - fontSize: 16, - fontWeight: FontWeight.w500, - ), - shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)), - ), - inputDecorationTheme: InputDecorationTheme().copyWith( - focusedBorder: UnderlineInputBorder( - borderSide: BorderSide( - color: Color.fromRGBO(45, 194, 98, 1.0), - ), - ), - ), - checkboxTheme: CheckboxThemeData( - side: BorderSide( - color: Colors.black, - width: 2, - ), - fillColor: MaterialStateProperty.resolveWith((states) { - return states.contains(MaterialState.selected) - ? Colors.black - : Colors.white; - }), - checkColor: MaterialStateProperty.resolveWith((states) { - return states.contains(MaterialState.selected) - ? Colors.white - : Colors.black; - }), - ), -); - -final darkThemeData = ThemeData( - fontFamily: 'Inter', - brightness: Brightness.dark, - primaryColorLight: Colors.white70, - iconTheme: IconThemeData(color: Colors.white), - primaryIconTheme: IconThemeData(color: Colors.red, opacity: 1.0, size: 50.0), - hintColor: Colors.grey, - colorScheme: ColorScheme.dark(primary: Colors.white), - accentColor: Color.fromRGBO(45, 194, 98, 0.2), - buttonColor: Color.fromRGBO(45, 194, 98, 1.0), - buttonTheme: ButtonThemeData().copyWith( - buttonColor: Color.fromRGBO(45, 194, 98, 1.0), - ), - textTheme: _buildTextTheme(Colors.white), - toggleableActiveColor: Colors.green[400], - outlinedButtonTheme: buildOutlinedButtonThemeData( - bgDisabled: Colors.grey.shade500, - bgEnabled: Colors.white, - fgDisabled: Colors.white, - fgEnabled: Colors.black, - ), - elevatedButtonTheme: buildElevatedButtonThemeData( - onPrimary: Colors.black, - primary: Colors.white, - ), - scaffoldBackgroundColor: Colors.black, - backgroundColor: Colors.black, - appBarTheme: AppBarTheme().copyWith( - color: Colors.black, - elevation: 0, - ), - cardColor: Color.fromRGBO(10, 15, 15, 1.0), - dialogTheme: DialogTheme().copyWith( - backgroundColor: Color.fromRGBO(15, 15, 15, 1.0), - titleTextStyle: TextStyle( - color: Colors.white, - fontSize: 24, - fontWeight: FontWeight.w600, - ), - contentTextStyle: TextStyle( - fontFamily: 'Inter-Medium', - color: Colors.white, - fontSize: 16, - fontWeight: FontWeight.w500, - ), - shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)), - ), - inputDecorationTheme: InputDecorationTheme().copyWith( - focusedBorder: UnderlineInputBorder( - borderSide: BorderSide( - color: Color.fromRGBO(45, 194, 98, 1.0), - ), - ), - ), - checkboxTheme: CheckboxThemeData( - side: BorderSide( - color: Colors.grey, - width: 2, - ), - fillColor: MaterialStateProperty.resolveWith((states) { - if (states.contains(MaterialState.selected)) { - return Colors.grey; - } else { - return Colors.black; - } - }), - checkColor: MaterialStateProperty.resolveWith((states) { - if (states.contains(MaterialState.selected)) { - return Colors.black; - } else { - return Colors.grey; - } - }), - ), -); - -TextTheme _buildTextTheme(Color textColor) { - return TextTheme().copyWith( - headline4: TextStyle( - color: textColor, - fontSize: 32, - fontWeight: FontWeight.w600, - fontFamily: 'Inter', - ), - headline5: TextStyle( - color: textColor, - fontSize: 24, - fontWeight: FontWeight.w600, - fontFamily: 'Inter', - ), - headline6: TextStyle( - color: textColor, - fontSize: 18, - fontFamily: 'Inter', - fontWeight: FontWeight.w600, - ), - subtitle1: TextStyle( - color: textColor, - fontFamily: 'Inter', - fontSize: 16, - fontWeight: FontWeight.w500, - ), - subtitle2: TextStyle( - color: textColor, - fontFamily: 'Inter', - fontSize: 14, - fontWeight: FontWeight.w500, - ), - bodyText1: TextStyle( - fontFamily: 'Inter', - color: textColor, - fontSize: 16, - fontWeight: FontWeight.w500, - ), - bodyText2: TextStyle( - fontFamily: 'Inter', - color: textColor, - fontSize: 14, - fontWeight: FontWeight.w500, - ), - caption: TextStyle( - color: textColor.withOpacity(0.6), - fontSize: 14, - fontWeight: FontWeight.w500, - ), - overline: TextStyle( - fontFamily: 'Inter', - color: textColor, - fontSize: 14, - fontWeight: FontWeight.w500, - decoration: TextDecoration.underline, - ), - ); -} - class EnteApp extends StatefulWidget { static const _homeWidget = HomeWidget(); @@ -329,11 +113,11 @@ class _EnteAppState extends State with WidgetsBindingObserver { stopOnTerminate: false, startOnBoot: true, enableHeadless: true, - requiresBatteryNotLow: false, + requiresBatteryNotLow: true, requiresCharging: false, requiresStorageNotLow: false, requiresDeviceIdle: false, - requiredNetworkType: NetworkType.NONE, + requiredNetworkType: NetworkType.ANY, ), (String taskId) async { await widget.runBackgroundTask(taskId); }, (taskId) { diff --git a/lib/core/constants.dart b/lib/core/constants.dart index d644060f7..359fc317e 100644 --- a/lib/core/constants.dart +++ b/lib/core/constants.dart @@ -12,6 +12,7 @@ const String kRoadmapURL = "https://roadmap.ente.io"; const int kMicroSecondsInDay = 86400000000; const int kAndroid11SDKINT = 30; const int kGalleryLoadStartTime = -8000000000000000; // Wednesday, March 6, 1748 +const int kGalleryLoadEndTime = 9223372036854775807; // 2^63 -1 // used to identify which ente file are available in app cache const String kSharedMediaIdentifier = 'ente-shared://'; diff --git a/lib/core/error-reporting/super_logging.dart b/lib/core/error-reporting/super_logging.dart index 94bd638a9..fe5d29ff5 100644 --- a/lib/core/error-reporting/super_logging.dart +++ b/lib/core/error-reporting/super_logging.dart @@ -176,7 +176,7 @@ class SuperLogging { if (config.body == null) return; - if (enable) { + if (enable && sentryIsEnabled) { await SentryFlutter.init( (options) { options.dsn = config.sentryDsn; diff --git a/lib/db/files_db.dart b/lib/db/files_db.dart index 174ab5267..52e8d311a 100644 --- a/lib/db/files_db.dart +++ b/lib/db/files_db.dart @@ -580,9 +580,10 @@ class FilesDB { Future> getFilesCreatedWithinDurations( List> durations, + Set ignoredCollectionIDs, ) async { final db = await instance.database; - String whereClause = ""; + String whereClause = "( "; for (int index = 0; index < durations.length; index++) { whereClause += "($columnCreationTime > " + durations[index][0].toString() + @@ -593,12 +594,14 @@ class FilesDB { whereClause += " OR "; } } + whereClause += ") AND $columnMMdVisibility = $kVisibilityVisible"; final results = await db.query( table, where: whereClause, orderBy: '$columnCreationTime ASC', ); - return _convertToFiles(results); + final files = _convertToFiles(results); + return _deduplicatedAndFilterIgnoredFiles(files, ignoredCollectionIDs); } Future> getFilesToBeUploadedWithinFolders( diff --git a/lib/ente_theme_data.dart b/lib/ente_theme_data.dart index 56ac134e8..f84b8ce63 100644 --- a/lib/ente_theme_data.dart +++ b/lib/ente_theme_data.dart @@ -1,6 +1,221 @@ import 'package:flutter/material.dart'; import 'package:flutter_datetime_picker/flutter_datetime_picker.dart'; +final lightThemeData = ThemeData( + fontFamily: 'Inter', + brightness: Brightness.light, + hintColor: Colors.grey, + primaryColor: Colors.deepOrangeAccent, + primaryColorLight: Colors.black54, + iconTheme: IconThemeData(color: Colors.black), + primaryIconTheme: IconThemeData(color: Colors.red, opacity: 1.0, size: 50.0), + colorScheme: ColorScheme.light( + primary: Colors.black, + secondary: Color.fromARGB(255, 163, 163, 163), + ), + accentColor: Color.fromRGBO(0, 0, 0, 0.6), + buttonColor: Color.fromRGBO(45, 194, 98, 1.0), + outlinedButtonTheme: buildOutlinedButtonThemeData( + bgDisabled: Colors.grey.shade500, + bgEnabled: Colors.black, + fgDisabled: Colors.white, + fgEnabled: Colors.white, + ), + elevatedButtonTheme: buildElevatedButtonThemeData( + onPrimary: Colors.white, + primary: Colors.black, + ), + toggleableActiveColor: Colors.green[400], + scaffoldBackgroundColor: Colors.white, + backgroundColor: Colors.white, + appBarTheme: AppBarTheme().copyWith( + backgroundColor: Colors.white, + foregroundColor: Colors.black, + iconTheme: IconThemeData(color: Colors.black), + elevation: 0, + ), + //https://api.flutter.dev/flutter/material/TextTheme-class.html + textTheme: _buildTextTheme(Colors.black), + primaryTextTheme: TextTheme().copyWith( + bodyText2: TextStyle(color: Colors.yellow), + bodyText1: TextStyle(color: Colors.orange), + ), + cardColor: Color.fromRGBO(250, 250, 250, 1.0), + dialogTheme: DialogTheme().copyWith( + backgroundColor: Color.fromRGBO(250, 250, 250, 1.0), // + titleTextStyle: TextStyle( + color: Colors.black, + fontSize: 24, + fontWeight: FontWeight.w600, + ), + contentTextStyle: TextStyle( + fontFamily: 'Inter-Medium', + color: Colors.black, + fontSize: 16, + fontWeight: FontWeight.w500, + ), + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)), + ), + inputDecorationTheme: InputDecorationTheme().copyWith( + focusedBorder: UnderlineInputBorder( + borderSide: BorderSide( + color: Color.fromRGBO(45, 194, 98, 1.0), + ), + ), + ), + checkboxTheme: CheckboxThemeData( + side: BorderSide( + color: Colors.black, + width: 2, + ), + fillColor: MaterialStateProperty.resolveWith((states) { + return states.contains(MaterialState.selected) + ? Colors.black + : Colors.white; + }), + checkColor: MaterialStateProperty.resolveWith((states) { + return states.contains(MaterialState.selected) + ? Colors.white + : Colors.black; + }), + ), +); + +final darkThemeData = ThemeData( + fontFamily: 'Inter', + brightness: Brightness.dark, + primaryColorLight: Colors.white70, + iconTheme: IconThemeData(color: Colors.white), + primaryIconTheme: IconThemeData(color: Colors.red, opacity: 1.0, size: 50.0), + hintColor: Colors.grey, + colorScheme: ColorScheme.dark(primary: Colors.white), + accentColor: Color.fromRGBO(45, 194, 98, 0.2), + buttonColor: Color.fromRGBO(45, 194, 98, 1.0), + buttonTheme: ButtonThemeData().copyWith( + buttonColor: Color.fromRGBO(45, 194, 98, 1.0), + ), + textTheme: _buildTextTheme(Colors.white), + toggleableActiveColor: Colors.green[400], + outlinedButtonTheme: buildOutlinedButtonThemeData( + bgDisabled: Colors.grey.shade500, + bgEnabled: Colors.white, + fgDisabled: Colors.white, + fgEnabled: Colors.black, + ), + elevatedButtonTheme: buildElevatedButtonThemeData( + onPrimary: Colors.black, + primary: Colors.white, + ), + scaffoldBackgroundColor: Colors.black, + backgroundColor: Colors.black, + appBarTheme: AppBarTheme().copyWith( + color: Colors.black, + elevation: 0, + ), + cardColor: Color.fromRGBO(10, 15, 15, 1.0), + dialogTheme: DialogTheme().copyWith( + backgroundColor: Color.fromRGBO(15, 15, 15, 1.0), + titleTextStyle: TextStyle( + color: Colors.white, + fontSize: 24, + fontWeight: FontWeight.w600, + ), + contentTextStyle: TextStyle( + fontFamily: 'Inter-Medium', + color: Colors.white, + fontSize: 16, + fontWeight: FontWeight.w500, + ), + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)), + ), + inputDecorationTheme: InputDecorationTheme().copyWith( + focusedBorder: UnderlineInputBorder( + borderSide: BorderSide( + color: Color.fromRGBO(45, 194, 98, 1.0), + ), + ), + ), + checkboxTheme: CheckboxThemeData( + side: BorderSide( + color: Colors.grey, + width: 2, + ), + fillColor: MaterialStateProperty.resolveWith((states) { + if (states.contains(MaterialState.selected)) { + return Colors.grey; + } else { + return Colors.black; + } + }), + checkColor: MaterialStateProperty.resolveWith((states) { + if (states.contains(MaterialState.selected)) { + return Colors.black; + } else { + return Colors.grey; + } + }), + ), +); + +TextTheme _buildTextTheme(Color textColor) { + return TextTheme().copyWith( + headline4: TextStyle( + color: textColor, + fontSize: 32, + fontWeight: FontWeight.w600, + fontFamily: 'Inter', + ), + headline5: TextStyle( + color: textColor, + fontSize: 24, + fontWeight: FontWeight.w600, + fontFamily: 'Inter', + ), + headline6: TextStyle( + color: textColor, + fontSize: 18, + fontFamily: 'Inter', + fontWeight: FontWeight.w600, + ), + subtitle1: TextStyle( + color: textColor, + fontFamily: 'Inter', + fontSize: 16, + fontWeight: FontWeight.w500, + ), + subtitle2: TextStyle( + color: textColor, + fontFamily: 'Inter', + fontSize: 14, + fontWeight: FontWeight.w500, + ), + bodyText1: TextStyle( + fontFamily: 'Inter', + color: textColor, + fontSize: 16, + fontWeight: FontWeight.w500, + ), + bodyText2: TextStyle( + fontFamily: 'Inter', + color: textColor, + fontSize: 14, + fontWeight: FontWeight.w500, + ), + caption: TextStyle( + color: textColor.withOpacity(0.6), + fontSize: 14, + fontWeight: FontWeight.w500, + ), + overline: TextStyle( + fontFamily: 'Inter', + color: textColor, + fontSize: 14, + fontWeight: FontWeight.w500, + decoration: TextDecoration.underline, + ), + ); +} + extension CustomColorScheme on ColorScheme { Color get defaultBackgroundColor => brightness == Brightness.light ? Colors.white : Colors.black; @@ -121,6 +336,17 @@ extension CustomColorScheme on ColorScheme { Color get subTextColor => brightness == Brightness.light ? Color.fromRGBO(180, 180, 180, 1) : Color.fromRGBO(100, 100, 100, 1); + + Color get themeSwitchIndicatorColor => brightness == Brightness.light + ? Colors.black.withOpacity(0.75) + : Colors.white; + + Color get themeSwitchActiveIconColor => + brightness == Brightness.light ? Colors.white : Colors.black; + + Color get themeSwitchInactiveIconColor => brightness == Brightness.light + ? Colors.black.withOpacity(0.5) + : Colors.white.withOpacity(0.5); } OutlinedButtonThemeData buildOutlinedButtonThemeData({ diff --git a/lib/main.dart b/lib/main.dart index 1690c2461..d4544eba7 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -15,6 +15,7 @@ import 'package:photos/core/constants.dart'; import 'package:photos/core/error-reporting/super_logging.dart'; import 'package:photos/core/network.dart'; import 'package:photos/db/upload_locks_db.dart'; +import 'package:photos/ente_theme_data.dart'; import 'package:photos/services/app_lifecycle_service.dart'; import 'package:photos/services/billing_service.dart'; import 'package:photos/services/collections_service.dart'; @@ -28,6 +29,7 @@ import 'package:photos/services/remote_sync_service.dart'; import 'package:photos/services/sync_service.dart'; import 'package:photos/services/trash_sync_service.dart'; import 'package:photos/services/update_service.dart'; +import 'package:photos/services/user_service.dart'; import 'package:photos/ui/app_lock.dart'; import 'package:photos/ui/lock_screen.dart'; import 'package:photos/utils/crypto_util.dart'; @@ -130,6 +132,7 @@ Future _init(bool isBackground, {String via = ''}) async { await NotificationService.instance.init(); await Network.instance.init(); await Configuration.instance.init(); + await UserService.instance.init(); await UpdateService.instance.init(); await BillingService.instance.init(); await CollectionsService.instance.init(); @@ -169,7 +172,7 @@ Future _runWithLogs(Function() function, {String prefix = ""}) async { await SuperLogging.main( LogConfig( body: function, - logDirPath: (await getTemporaryDirectory()).path + "/logs", + logDirPath: (await getApplicationSupportDirectory()).path + "/logs", maxLogFiles: 5, sentryDsn: kDebugMode ? kSentryDebugDSN : kSentryDSN, tunnel: kSentryTunnel, diff --git a/lib/models/filters/important_items_filter.dart b/lib/models/filters/important_items_filter.dart index 4404f0928..ffd3b4dc4 100644 --- a/lib/models/filters/important_items_filter.dart +++ b/lib/models/filters/important_items_filter.dart @@ -10,25 +10,17 @@ class ImportantItemsFilter implements GalleryItemsFilter { @override bool shouldInclude(File file) { - if (_importantPaths.isEmpty) { - if (Platform.isAndroid) { - if (file.uploadedFileID != null) { - return true; - } - final String folder = basename(file.deviceFolder); - return folder == "Camera" || - folder == "Recents" || - folder == "DCIM" || - folder == "Download" || - folder == "Screenshot"; - } else { - return true; - } - } if (file.uploadedFileID != null) { return true; } - final folder = basename(file.deviceFolder); + final String folder = basename(file.deviceFolder); + if (_importantPaths.isEmpty && Platform.isAndroid) { + return folder == "Camera" || + folder == "Recents" || + folder == "DCIM" || + folder == "Download" || + folder == "Screenshot"; + } return _importantPaths.contains(folder); } } diff --git a/lib/services/local_sync_service.dart b/lib/services/local_sync_service.dart index 9c017dceb..63fa9e670 100644 --- a/lib/services/local_sync_service.dart +++ b/lib/services/local_sync_service.dart @@ -275,12 +275,18 @@ class LocalSyncService { } void _registerChangeCallback() { + // In case of iOS limit permission, this call back is fired immediately + // after file selection dialog is dismissed. PhotoManager.addChangeCallback((value) async { _logger.info("Something changed on disk"); if (_existingSync != null) { await _existingSync.future; } - sync(); + if (hasGrantedLimitedPermissions()) { + syncAll(); + } else { + sync(); + } }); PhotoManager.startChangeNotify(); } diff --git a/lib/services/memories_service.dart b/lib/services/memories_service.dart index 0179029bb..047f55249 100644 --- a/lib/services/memories_service.dart +++ b/lib/services/memories_service.dart @@ -5,6 +5,7 @@ import 'package:photos/db/files_db.dart'; import 'package:photos/db/memories_db.dart'; import 'package:photos/models/filters/important_items_filter.dart'; import 'package:photos/models/memory.dart'; +import 'package:photos/services/collections_service.dart'; class MemoriesService extends ChangeNotifier { final _logger = Logger("MemoryService"); @@ -70,7 +71,10 @@ class MemoriesService extends ChangeNotifier { date.add(Duration(days: daysAfter)).microsecondsSinceEpoch; durations.add([startCreationTime, endCreationTime]); } - final files = await _filesDB.getFilesCreatedWithinDurations(durations); + final archivedCollectionIds = + CollectionsService.instance.getArchivedCollections(); + final files = await _filesDB.getFilesCreatedWithinDurations( + durations, archivedCollectionIds); final seenTimes = await _memoriesDB.getSeenTimes(); final List memories = []; final filter = ImportantItemsFilter(); diff --git a/lib/services/remote_sync_service.dart b/lib/services/remote_sync_service.dart index 4a7509be6..c25aa4b41 100644 --- a/lib/services/remote_sync_service.dart +++ b/lib/services/remote_sync_service.dart @@ -105,7 +105,6 @@ class RemoteSyncService { _existingSync = null; } } catch (e, s) { - _logger.severe("Error executing remote sync ", e, s); _existingSync.complete(); _existingSync = null; // rethrow whitelisted error so that UI status can be updated correctly. @@ -114,7 +113,10 @@ class RemoteSyncService { e is WiFiUnavailableError || e is StorageLimitExceededError || e is SyncStopRequestedError) { + _logger.warning("Error executing remote sync", e); rethrow; + } else { + _logger.severe("Error executing remote sync ", e, s); } } } @@ -268,6 +270,10 @@ class RemoteSyncService { if (toBeUploaded > 0) { Bus.instance.fire(SyncStatusUpdate(SyncStatus.preparing_for_upload)); + // verify if files upload is allowed based on their subscription plan and + // storage limit. To avoid creating new endpoint, we are using + // fetchUploadUrls as alternative method. + await _uploader.fetchUploadURLs(toBeUploaded); } final List futures = []; for (final uploadedFileID in updatedFileIDs) { diff --git a/lib/services/user_service.dart b/lib/services/user_service.dart index 47b44c6ec..65c4f8d48 100644 --- a/lib/services/user_service.dart +++ b/lib/services/user_service.dart @@ -33,16 +33,21 @@ class UserService { final _dio = Network.instance.getDio(); final _logger = Logger((UserService).toString()); final _config = Configuration.instance; + ValueNotifier emailValueNotifier; UserService._privateConstructor(); - static final UserService instance = UserService._privateConstructor(); + Future init() async { + emailValueNotifier = + ValueNotifier(Configuration.instance.getEmail()); + } + Future getOtt( BuildContext context, String email, { bool isChangeEmail = false, - bool isCreateAccountScreen, + bool isCreateAccountScreen = false, }) async { final dialog = createProgressDialog(context, "Please wait..."); await dialog.show(); @@ -266,6 +271,11 @@ class UserService { } } + Future setEmail(String email) async { + await _config.setEmail(email); + emailValueNotifier.value = email ?? ""; + } + Future changeEmail( BuildContext context, String email, @@ -289,7 +299,7 @@ class UserService { await dialog.hide(); if (response != null && response.statusCode == 200) { showToast(context, "Email changed to " + email); - _config.setEmail(email); + await setEmail(email); Navigator.of(context).popUntil((route) => route.isFirst); Bus.instance.fire(UserDetailsChangedEvent()); return; diff --git a/lib/ui/email_entry_page.dart b/lib/ui/email_entry_page.dart index 3f0be876c..f6eddc227 100644 --- a/lib/ui/email_entry_page.dart +++ b/lib/ui/email_entry_page.dart @@ -108,7 +108,7 @@ class _EmailEntryPageState extends State { buttonText: 'Create account', onPressedFunction: () { _config.setVolatilePassword(_passwordController1.text); - _config.setEmail(_email); + UserService.instance.setEmail(_email); UserService.instance .getOtt(context, _email, isCreateAccountScreen: true); FocusScope.of(context).unfocus(); diff --git a/lib/ui/gallery.dart b/lib/ui/gallery.dart index d4faf70c3..adad4db34 100644 --- a/lib/ui/gallery.dart +++ b/lib/ui/gallery.dart @@ -114,7 +114,7 @@ class _GalleryState extends State { final startTime = DateTime.now().microsecondsSinceEpoch; final result = await widget.asyncLoader( kGalleryLoadStartTime, - DateTime.now().microsecondsSinceEpoch, + kGalleryLoadEndTime, limit: limit, ); final endTime = DateTime.now().microsecondsSinceEpoch; diff --git a/lib/ui/home_widget.dart b/lib/ui/home_widget.dart index f48949daf..cb84426f2 100644 --- a/lib/ui/home_widget.dart +++ b/lib/ui/home_widget.dart @@ -21,6 +21,7 @@ import 'package:photos/events/subscription_purchased_event.dart'; import 'package:photos/events/sync_status_update_event.dart'; import 'package:photos/events/tab_changed_event.dart'; import 'package:photos/events/trigger_logout_event.dart'; +import 'package:photos/events/user_details_changed_event.dart'; import 'package:photos/events/user_logged_out_event.dart'; import 'package:photos/models/file_load_result.dart'; import 'package:photos/models/galleryType.dart'; @@ -64,7 +65,9 @@ class HomeWidget extends StatefulWidget { class _HomeWidgetState extends State { static const _deviceFolderGalleryWidget = CollectionsGalleryWidget(); static const _sharedCollectionGallery = SharedCollectionGallery(); - static const _settingsPage = SettingsPage(); + static final _settingsPage = SettingsPage( + emailNotifier: UserService.instance.emailValueNotifier, + ); static const _headerWidget = HeaderWidget(); final _logger = Logger("HomeWidgetState"); @@ -566,7 +569,7 @@ class _HomeBottomNavigationBarState extends State { _tabChangedEventSubscription = Bus.instance.on().listen((event) { if (event.source != TabChangedEventSource.tab_bar) { - _logger.fine('index changed to ${event.selectedIndex}'); + debugPrint('index changed to ${event.selectedIndex}'); if (mounted) { setState(() { currentTabIndex = event.selectedIndex; diff --git a/lib/ui/login_page.dart b/lib/ui/login_page.dart index 7eec2faf0..7d62c7ad5 100644 --- a/lib/ui/login_page.dart +++ b/lib/ui/login_page.dart @@ -55,7 +55,7 @@ class _LoginPageState extends State { isFormValid: _emailIsValid, buttonText: 'Log in', onPressedFunction: () { - _config.setEmail(_email); + UserService.instance.setEmail(_email); UserService.instance .getOtt(context, _email, isCreateAccountScreen: false); FocusScope.of(context).unfocus(); diff --git a/lib/ui/memories_widget.dart b/lib/ui/memories_widget.dart index e63fc7f92..34d225dd9 100644 --- a/lib/ui/memories_widget.dart +++ b/lib/ui/memories_widget.dart @@ -334,8 +334,8 @@ class _FullScreenMemoryState extends State { child: IconButton( icon: Icon( Icons.adaptive.share, - color: Colors.white, - ), //same for both themes + color: Colors.white, //same for both themes + ), onPressed: () { share(context, [file]); }, diff --git a/lib/ui/ott_verification_page.dart b/lib/ui/ott_verification_page.dart index 6892d3468..2dd66aa19 100644 --- a/lib/ui/ott_verification_page.dart +++ b/lib/ui/ott_verification_page.dart @@ -12,7 +12,7 @@ class OTTVerificationPage extends StatefulWidget { OTTVerificationPage( this.email, { this.isChangeEmail = false, - this.isCreateAccountScreen, + this.isCreateAccountScreen = false, Key key, }) : super(key: key); diff --git a/lib/ui/settings/account_section_widget.dart b/lib/ui/settings/account_section_widget.dart index 601ef3972..2575fd851 100644 --- a/lib/ui/settings/account_section_widget.dart +++ b/lib/ui/settings/account_section_widget.dart @@ -70,7 +70,7 @@ class AccountSectionWidgetState extends State { child: SettingsTextItem(text: "Recovery key", icon: Icons.navigate_next), ), - SectionOptionDivider, + sectionOptionDivider, GestureDetector( behavior: HitTestBehavior.translucent, onTap: () async { @@ -95,7 +95,7 @@ class AccountSectionWidgetState extends State { child: SettingsTextItem(text: "Change email", icon: Icons.navigate_next), ), - SectionOptionDivider, + sectionOptionDivider, GestureDetector( behavior: HitTestBehavior.translucent, onTap: () async { diff --git a/lib/ui/settings/backup_section_widget.dart b/lib/ui/settings/backup_section_widget.dart index e6c59fa25..998fe120f 100644 --- a/lib/ui/settings/backup_section_widget.dart +++ b/lib/ui/settings/backup_section_widget.dart @@ -55,7 +55,7 @@ class BackupSectionWidgetState extends State { icon: Icons.navigate_next, ), ), - SectionOptionDivider, + sectionOptionDivider, SizedBox( height: 48, child: Row( @@ -75,7 +75,7 @@ class BackupSectionWidgetState extends State { ], ), ), - SectionOptionDivider, + sectionOptionDivider, SizedBox( height: 48, child: Row( @@ -98,7 +98,7 @@ class BackupSectionWidgetState extends State { ]; if (Platform.isIOS) { sectionOptions.addAll([ - SectionOptionDivider, + sectionOptionDivider, SizedBox( height: 48, child: Row( @@ -134,7 +134,7 @@ class BackupSectionWidgetState extends State { } sectionOptions.addAll( [ - SectionOptionDivider, + sectionOptionDivider, GestureDetector( behavior: HitTestBehavior.translucent, onTap: () async { @@ -168,7 +168,7 @@ class BackupSectionWidgetState extends State { icon: Icons.navigate_next, ), ), - SectionOptionDivider, + sectionOptionDivider, GestureDetector( behavior: HitTestBehavior.translucent, onTap: () async { diff --git a/lib/ui/settings/common_settings.dart b/lib/ui/settings/common_settings.dart index 2512834eb..09fdeae3c 100644 --- a/lib/ui/settings/common_settings.dart +++ b/lib/ui/settings/common_settings.dart @@ -4,7 +4,7 @@ import 'package:expandable/expandable.dart'; import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; -Widget SectionOptionDivider = Padding( +Widget sectionOptionDivider = Padding( padding: EdgeInsets.all(Platform.isIOS ? 4 : 2), ); diff --git a/lib/ui/settings/danger_section_widget.dart b/lib/ui/settings/danger_section_widget.dart index 4fa53c01e..4c35878cc 100644 --- a/lib/ui/settings/danger_section_widget.dart +++ b/lib/ui/settings/danger_section_widget.dart @@ -35,7 +35,7 @@ class _DangerSectionWidgetState extends State { }, child: SettingsTextItem(text: "Logout", icon: Icons.navigate_next), ), - SectionOptionDivider, + sectionOptionDivider, GestureDetector( behavior: HitTestBehavior.translucent, onTap: () { diff --git a/lib/ui/settings/info_section_widget.dart b/lib/ui/settings/info_section_widget.dart index 401eef8d3..13bf1414a 100644 --- a/lib/ui/settings/info_section_widget.dart +++ b/lib/ui/settings/info_section_widget.dart @@ -39,7 +39,7 @@ class InfoSectionWidget extends StatelessWidget { }, child: SettingsTextItem(text: "FAQ", icon: Icons.navigate_next), ), - SectionOptionDivider, + sectionOptionDivider, GestureDetector( behavior: HitTestBehavior.translucent, onTap: () { @@ -53,7 +53,7 @@ class InfoSectionWidget extends StatelessWidget { }, child: SettingsTextItem(text: "Terms", icon: Icons.navigate_next), ), - SectionOptionDivider, + sectionOptionDivider, GestureDetector( behavior: HitTestBehavior.translucent, onTap: () { @@ -67,19 +67,19 @@ class InfoSectionWidget extends StatelessWidget { }, child: SettingsTextItem(text: "Privacy", icon: Icons.navigate_next), ), - SectionOptionDivider, + sectionOptionDivider, GestureDetector( behavior: HitTestBehavior.translucent, onTap: () async { - launch("https://github.com/ente-io/frame"); + launchUrl(Uri.parse("https://github.com/ente-io/frame")); }, child: SettingsTextItem(text: "Source code", icon: Icons.navigate_next), ), + sectionOptionDivider, UpdateService.instance.isIndependent() ? Column( children: [ - Divider(height: 4), GestureDetector( behavior: HitTestBehavior.translucent, onTap: () async { @@ -110,7 +110,7 @@ class InfoSectionWidget extends StatelessWidget { ), ], ) - : Container(), + : const SizedBox.shrink(), ], ); } diff --git a/lib/ui/settings/security_section_widget.dart b/lib/ui/settings/security_section_widget.dart index 62139d4d8..aab9ede10 100644 --- a/lib/ui/settings/security_section_widget.dart +++ b/lib/ui/settings/security_section_widget.dart @@ -116,7 +116,7 @@ class _SecuritySectionWidgetState extends State { ); } children.addAll([ - SectionOptionDivider, + sectionOptionDivider, SizedBox( height: 48, child: Row( @@ -150,7 +150,7 @@ class _SecuritySectionWidgetState extends State { if (Platform.isAndroid) { children.addAll( [ - SectionOptionDivider, + sectionOptionDivider, SizedBox( height: 48, child: Row( @@ -246,7 +246,7 @@ class _SecuritySectionWidgetState extends State { ); } children.addAll([ - SectionOptionDivider, + sectionOptionDivider, GestureDetector( behavior: HitTestBehavior.translucent, onTap: () async { diff --git a/lib/ui/settings/social_section_widget.dart b/lib/ui/settings/social_section_widget.dart index bb8e0aeb7..d56336918 100644 --- a/lib/ui/settings/social_section_widget.dart +++ b/lib/ui/settings/social_section_widget.dart @@ -30,7 +30,7 @@ class SocialSectionWidget extends StatelessWidget { }, child: SettingsTextItem(text: "Twitter", icon: Icons.navigate_next), ), - SectionOptionDivider, + sectionOptionDivider, GestureDetector( behavior: HitTestBehavior.translucent, onTap: () { @@ -38,7 +38,7 @@ class SocialSectionWidget extends StatelessWidget { }, child: SettingsTextItem(text: "Discord", icon: Icons.navigate_next), ), - SectionOptionDivider, + sectionOptionDivider, GestureDetector( behavior: HitTestBehavior.translucent, onTap: () { @@ -50,7 +50,7 @@ class SocialSectionWidget extends StatelessWidget { if (!UpdateService.instance.isIndependent()) { options.addAll( [ - SectionOptionDivider, + sectionOptionDivider, GestureDetector( behavior: HitTestBehavior.translucent, onTap: () { diff --git a/lib/ui/settings/support_section_widget.dart b/lib/ui/settings/support_section_widget.dart index afebbbe83..beb6fc6a1 100644 --- a/lib/ui/settings/support_section_widget.dart +++ b/lib/ui/settings/support_section_widget.dart @@ -43,7 +43,7 @@ class SupportSectionWidget extends StatelessWidget { }, child: SettingsTextItem(text: "Email", icon: Icons.navigate_next), ), - SectionOptionDivider, + sectionOptionDivider, GestureDetector( behavior: HitTestBehavior.translucent, onTap: () { @@ -63,7 +63,7 @@ class SupportSectionWidget extends StatelessWidget { }, child: SettingsTextItem(text: "Roadmap", icon: Icons.navigate_next), ), - SectionOptionDivider, + sectionOptionDivider, GestureDetector( behavior: HitTestBehavior.translucent, onTap: () async { diff --git a/lib/ui/settings/theme_switch_widget.dart b/lib/ui/settings/theme_switch_widget.dart index ed669bfe4..d5466f925 100644 --- a/lib/ui/settings/theme_switch_widget.dart +++ b/lib/ui/settings/theme_switch_widget.dart @@ -1,30 +1,52 @@ import 'package:adaptive_theme/adaptive_theme.dart'; +import 'package:animated_toggle_switch/animated_toggle_switch.dart'; import 'package:flutter/material.dart'; -import 'package:flutter/widgets.dart'; +import 'package:photos/ente_theme_data.dart'; class ThemeSwitchWidget extends StatelessWidget { const ThemeSwitchWidget({Key key}) : super(key: key); @override Widget build(BuildContext context) { - return Switch.adaptive( - value: Theme.of(context).colorScheme.onSurface == Colors.black, - activeColor: Theme.of(context).colorScheme.onSurface, - activeTrackColor: Theme.of(context).colorScheme.onBackground, - inactiveTrackColor: Theme.of(context).colorScheme.onSurface, - inactiveThumbColor: Theme.of(context).colorScheme.primary, - hoverColor: Theme.of(context).colorScheme.onSurface, - onChanged: (bool value) { - if (value) { + var selectedTheme = 0; + if (Theme.of(context).brightness == Brightness.dark) { + selectedTheme = 1; + } + return AnimatedToggleSwitch.rolling( + current: selectedTheme, + values: const [0, 1], + onChanged: (i) { + print("Changed to ${i}, selectedTheme is ${selectedTheme} "); + if (i == 0) { AdaptiveTheme.of(context).setLight(); } else { AdaptiveTheme.of(context).setDark(); } }, - // activeThumbImage: new NetworkImage( - // 'https://cdn0.iconfinder.com/data/icons/multimedia-solid-30px/30/moon_dark_mode_night-512.png'), - // inactiveThumbImage: new NetworkImage( - // 'https://cdn0.iconfinder.com/data/icons/multimedia-solid-30px/30/moon_dark_mode_night-512.png'), + iconBuilder: (i, size, foreground) { + final color = selectedTheme == i + ? Theme.of(context).colorScheme.themeSwitchActiveIconColor + : Theme.of(context).colorScheme.themeSwitchInactiveIconColor; + if (i == 0) { + return Icon( + Icons.light_mode, + color: color, + ); + } else { + return Icon( + Icons.dark_mode, + color: color, + ); + } + }, + height: 36, + indicatorSize: Size(36, 36), + indicatorColor: Theme.of(context).colorScheme.themeSwitchIndicatorColor, + borderColor: Theme.of(context) + .colorScheme + .themeSwitchInactiveIconColor + .withOpacity(0.1), + borderWidth: 1, ); } } diff --git a/lib/ui/settings_page.dart b/lib/ui/settings_page.dart index 85c85ddd3..9dace5b0f 100644 --- a/lib/ui/settings_page.dart +++ b/lib/ui/settings_page.dart @@ -16,7 +16,8 @@ import 'package:photos/ui/settings/support_section_widget.dart'; import 'package:photos/ui/settings/theme_switch_widget.dart'; class SettingsPage extends StatelessWidget { - const SettingsPage({Key key}) : super(key: key); + final ValueNotifier emailNotifier; + const SettingsPage({Key key, @required this.emailNotifier}) : super(key: key); @override Widget build(BuildContext context) { @@ -27,7 +28,6 @@ class SettingsPage extends StatelessWidget { Widget _getBody(BuildContext context) { final hasLoggedIn = Configuration.instance.getToken() != null; - final String email = Configuration.instance.getEmail(); final List contents = []; contents.add( Container( @@ -36,13 +36,23 @@ class SettingsPage extends StatelessWidget { mainAxisAlignment: MainAxisAlignment.spaceBetween, crossAxisAlignment: CrossAxisAlignment.center, children: [ - Text( - email, - style: Theme.of(context) - .textTheme - .subtitle1 - .copyWith(overflow: TextOverflow.ellipsis), + // // Thanks to the [AnimatedBuilder], only the widget displaying the + // // current email is rebuilt when `emailNotifier` notifies its + // // listeners. + AnimatedBuilder( + // [AnimatedBuilder] accepts any [Listenable] subtype. + animation: emailNotifier, + builder: (BuildContext context, Widget child) { + return Text( + emailNotifier.value, + style: Theme.of(context) + .textTheme + .subtitle1 + .copyWith(overflow: TextOverflow.ellipsis), + ); + }, ), + (Platform.isAndroid) ? ThemeSwitchWidget() : const SizedBox.shrink(), diff --git a/lib/utils/email_util.dart b/lib/utils/email_util.dart index e237e883f..070344e94 100644 --- a/lib/utils/email_util.dart +++ b/lib/utils/email_util.dart @@ -7,6 +7,7 @@ import 'package:flutter/services.dart'; import 'package:flutter_email_sender/flutter_email_sender.dart'; import 'package:logging/logging.dart'; import 'package:path_provider/path_provider.dart'; +import 'package:photos/core/configuration.dart'; import 'package:photos/core/error-reporting/super_logging.dart'; import 'package:photos/ente_theme_data.dart'; import 'package:photos/ui/common/dialogs.dart'; @@ -80,7 +81,7 @@ Future sendLogs( content.addAll( [ Text( - "This will send across logs and metrics that will help us debug your issue better", + "This will send across logs to help us debug your issue. Please note that file names will be included to help track issues with specific files.", style: TextStyle( height: 1.5, fontSize: 16, @@ -139,9 +140,11 @@ Future _sendLogs( Future getZippedLogsFile(BuildContext context) async { final dialog = createProgressDialog(context, "Preparing logs..."); await dialog.show(); + final logsPath = (await getApplicationSupportDirectory()).path; + final logsDirectory = Directory(logsPath + "/logs"); final tempPath = (await getTemporaryDirectory()).path; - final zipFilePath = tempPath + "/logs.zip"; - final logsDirectory = Directory(tempPath + "/logs"); + final zipFilePath = + tempPath + "/logs-${Configuration.instance.getUserID() ?? 0}.zip"; var encoder = ZipFileEncoder(); encoder.create(zipFilePath); encoder.addDirectory(logsDirectory); diff --git a/lib/utils/file_uploader.dart b/lib/utils/file_uploader.dart index 8082ca80b..1ba235dae 100644 --- a/lib/utils/file_uploader.dart +++ b/lib/utils/file_uploader.dart @@ -467,6 +467,8 @@ class FileUploader { } catch (e, s) { if (!(e is NoActiveSubscriptionError || e is StorageLimitExceededError || + e is WiFiUnavailableError || + e is SilentlyCancelUploadsError || e is FileTooLargeForPlanError)) { _logger.severe("File upload failed for " + file.toString(), e, s); } @@ -650,20 +652,20 @@ class FileUploader { Future _getUploadURL() async { if (_uploadURLs.isEmpty) { - await _fetchUploadURLs(); + await fetchUploadURLs(_queue.length); } return _uploadURLs.removeFirst(); } Future _uploadURLFetchInProgress; - Future _fetchUploadURLs() async { + Future fetchUploadURLs(int fileCount) async { _uploadURLFetchInProgress ??= Future(() async { try { final response = await _dio.get( Configuration.instance.getHttpEndpoint() + "/files/upload-urls", queryParameters: { - "count": min(42, 2 * _queue.length), // m4gic number + "count": min(42, fileCount * 2), // m4gic number }, options: Options( headers: {"X-Auth-Token": Configuration.instance.getToken()}, diff --git a/pubspec.lock b/pubspec.lock index e6d3f4fee..77d3c1b8a 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -24,6 +24,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "2.1.0" + animated_toggle_switch: + dependency: "direct main" + description: + name: animated_toggle_switch + url: "https://pub.dartlang.org" + source: hosted + version: "0.5.2" archive: dependency: "direct main" description: diff --git a/pubspec.yaml b/pubspec.yaml index cf045ad8f..3030ed0ca 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -21,6 +21,7 @@ dependencies: alice: git: "https://github.com/jhomlala/alice.git" animate_do: ^2.0.0 + animated_toggle_switch: ^0.5.2 archive: ^3.1.2 background_fetch: ^1.0.1 bip39: ^1.0.6