Merge remote-tracking branch 'origin/master' into set_sane_defaults_for_firebase
24
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)
|
||||

|
||||
|
||||

|
||||
|
||||
<br/>
|
||||
|
||||
## ✨ 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
|
||||
|
||||
<br/>
|
||||
|
||||
## 📲 Installation
|
||||
|
||||
### Android
|
||||
|
@ -37,12 +45,14 @@ You can alternatively install the build from PlayStore or F-Droid.
|
|||
<img width="197" alt="Get it on F-Droid" src="https://fdroid.gitlab.io/artwork/badge/get-it-on.png">
|
||||
</a>
|
||||
|
||||
|
||||
### iOS
|
||||
|
||||
<a href="https://apps.apple.com/in/app/ente-photos/id1542026904">
|
||||
<img width="197" alt="Download on AppStore" src="https://user-images.githubusercontent.com/1161789/154795157-c4468ff9-97fd-46f3-87fe-dca789d8733a.png">
|
||||
</a>
|
||||
|
||||
<br/>
|
||||
<br/>
|
||||
|
||||
## 🧑💻 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`
|
||||
|
||||
<br/>
|
||||
|
||||
## 🙋 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.
|
||||
|
||||
<br/>
|
||||
|
||||
## 🧭 Roadmap
|
||||
|
||||
We maintain a public roadmap, that's driven by our community @ [roadmap.ente.io](https://roadmap.ente.io).
|
||||
|
||||
<br/>
|
||||
|
||||
## 🤗 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.
|
||||
|
||||
<br/>
|
||||
|
||||
## ❤️ 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.
|
||||
|
|
|
@ -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 {
|
||||
|
|
4
android/app/src/fdroid/AndroidManifest.xml
Normal file
|
@ -0,0 +1,4 @@
|
|||
<manifest xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools"
|
||||
package="io.ente.photos">
|
||||
<uses-permission android:name="com.android.vending.BILLING" tools:node="remove"/>
|
||||
</manifest>
|
37
android/permissions.md
Normal file
|
@ -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.**
|
1
fastlane/metadata/android/en-US/changelogs/293.txt
Normal file
|
@ -0,0 +1 @@
|
|||
- Hello, FDroid!
|
1
fastlane/metadata/android/en-US/changelogs/317.txt
Normal file
|
@ -0,0 +1 @@
|
|||
- Resync files with missing location data on Android 10+
|
1
fastlane/metadata/android/en-US/changelogs/330.txt
Normal file
|
@ -0,0 +1 @@
|
|||
New pixels!
|
5
fastlane/metadata/android/en-US/changelogs/331.txt
Normal file
|
@ -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
|
5
fastlane/metadata/android/en-US/changelogs/333.txt
Normal file
|
@ -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
|
|
@ -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.
|
||||
|
|
Before Width: | Height: | Size: 8.3 KiB After Width: | Height: | Size: 8.3 KiB |
Before Width: | Height: | Size: 1.3 MiB After Width: | Height: | Size: 1.5 MiB |
Before Width: | Height: | Size: 347 KiB After Width: | Height: | Size: 690 KiB |
Before Width: | Height: | Size: 1.2 MiB After Width: | Height: | Size: 1.9 MiB |
Before Width: | Height: | Size: 687 KiB After Width: | Height: | Size: 1.6 MiB |
Before Width: | Height: | Size: 734 KiB After Width: | Height: | Size: 853 KiB |
BIN
fastlane/metadata/android/en-US/images/phoneScreenshots/6.png
Normal file
After Width: | Height: | Size: 76 KiB |
BIN
fastlane/metadata/android/en-US/images/phoneScreenshots/7.png
Normal file
After Width: | Height: | Size: 1.1 MiB |
220
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<EnteApp> 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) {
|
||||
|
|
|
@ -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://';
|
||||
|
|
|
@ -176,7 +176,7 @@ class SuperLogging {
|
|||
|
||||
if (config.body == null) return;
|
||||
|
||||
if (enable) {
|
||||
if (enable && sentryIsEnabled) {
|
||||
await SentryFlutter.init(
|
||||
(options) {
|
||||
options.dsn = config.sentryDsn;
|
||||
|
|
|
@ -580,9 +580,10 @@ class FilesDB {
|
|||
|
||||
Future<List<File>> getFilesCreatedWithinDurations(
|
||||
List<List<int>> durations,
|
||||
Set<int> 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<List<File>> getFilesToBeUploadedWithinFolders(
|
||||
|
|
|
@ -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({
|
||||
|
|
|
@ -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<void> _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,
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
|
|
|
@ -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<Memory> memories = [];
|
||||
final filter = ImportantItemsFilter();
|
||||
|
|
|
@ -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<Future> futures = [];
|
||||
for (final uploadedFileID in updatedFileIDs) {
|
||||
|
|
|
@ -33,16 +33,21 @@ class UserService {
|
|||
final _dio = Network.instance.getDio();
|
||||
final _logger = Logger((UserService).toString());
|
||||
final _config = Configuration.instance;
|
||||
ValueNotifier<String> emailValueNotifier;
|
||||
|
||||
UserService._privateConstructor();
|
||||
|
||||
static final UserService instance = UserService._privateConstructor();
|
||||
|
||||
Future<void> init() async {
|
||||
emailValueNotifier =
|
||||
ValueNotifier<String>(Configuration.instance.getEmail());
|
||||
}
|
||||
|
||||
Future<void> 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<void> setEmail(String email) async {
|
||||
await _config.setEmail(email);
|
||||
emailValueNotifier.value = email ?? "";
|
||||
}
|
||||
|
||||
Future<void> 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;
|
||||
|
|
|
@ -108,7 +108,7 @@ class _EmailEntryPageState extends State<EmailEntryPage> {
|
|||
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();
|
||||
|
|
|
@ -114,7 +114,7 @@ class _GalleryState extends State<Gallery> {
|
|||
final startTime = DateTime.now().microsecondsSinceEpoch;
|
||||
final result = await widget.asyncLoader(
|
||||
kGalleryLoadStartTime,
|
||||
DateTime.now().microsecondsSinceEpoch,
|
||||
kGalleryLoadEndTime,
|
||||
limit: limit,
|
||||
);
|
||||
final endTime = DateTime.now().microsecondsSinceEpoch;
|
||||
|
|
|
@ -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<HomeWidget> {
|
||||
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<HomeBottomNavigationBar> {
|
|||
_tabChangedEventSubscription =
|
||||
Bus.instance.on<TabChangedEvent>().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;
|
||||
|
|
|
@ -55,7 +55,7 @@ class _LoginPageState extends State<LoginPage> {
|
|||
isFormValid: _emailIsValid,
|
||||
buttonText: 'Log in',
|
||||
onPressedFunction: () {
|
||||
_config.setEmail(_email);
|
||||
UserService.instance.setEmail(_email);
|
||||
UserService.instance
|
||||
.getOtt(context, _email, isCreateAccountScreen: false);
|
||||
FocusScope.of(context).unfocus();
|
||||
|
|
|
@ -334,8 +334,8 @@ class _FullScreenMemoryState extends State<FullScreenMemory> {
|
|||
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]);
|
||||
},
|
||||
|
|
|
@ -12,7 +12,7 @@ class OTTVerificationPage extends StatefulWidget {
|
|||
OTTVerificationPage(
|
||||
this.email, {
|
||||
this.isChangeEmail = false,
|
||||
this.isCreateAccountScreen,
|
||||
this.isCreateAccountScreen = false,
|
||||
Key key,
|
||||
}) : super(key: key);
|
||||
|
||||
|
|
|
@ -70,7 +70,7 @@ class AccountSectionWidgetState extends State<AccountSectionWidget> {
|
|||
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<AccountSectionWidget> {
|
|||
child:
|
||||
SettingsTextItem(text: "Change email", icon: Icons.navigate_next),
|
||||
),
|
||||
SectionOptionDivider,
|
||||
sectionOptionDivider,
|
||||
GestureDetector(
|
||||
behavior: HitTestBehavior.translucent,
|
||||
onTap: () async {
|
||||
|
|
|
@ -55,7 +55,7 @@ class BackupSectionWidgetState extends State<BackupSectionWidget> {
|
|||
icon: Icons.navigate_next,
|
||||
),
|
||||
),
|
||||
SectionOptionDivider,
|
||||
sectionOptionDivider,
|
||||
SizedBox(
|
||||
height: 48,
|
||||
child: Row(
|
||||
|
@ -75,7 +75,7 @@ class BackupSectionWidgetState extends State<BackupSectionWidget> {
|
|||
],
|
||||
),
|
||||
),
|
||||
SectionOptionDivider,
|
||||
sectionOptionDivider,
|
||||
SizedBox(
|
||||
height: 48,
|
||||
child: Row(
|
||||
|
@ -98,7 +98,7 @@ class BackupSectionWidgetState extends State<BackupSectionWidget> {
|
|||
];
|
||||
if (Platform.isIOS) {
|
||||
sectionOptions.addAll([
|
||||
SectionOptionDivider,
|
||||
sectionOptionDivider,
|
||||
SizedBox(
|
||||
height: 48,
|
||||
child: Row(
|
||||
|
@ -134,7 +134,7 @@ class BackupSectionWidgetState extends State<BackupSectionWidget> {
|
|||
}
|
||||
sectionOptions.addAll(
|
||||
[
|
||||
SectionOptionDivider,
|
||||
sectionOptionDivider,
|
||||
GestureDetector(
|
||||
behavior: HitTestBehavior.translucent,
|
||||
onTap: () async {
|
||||
|
@ -168,7 +168,7 @@ class BackupSectionWidgetState extends State<BackupSectionWidget> {
|
|||
icon: Icons.navigate_next,
|
||||
),
|
||||
),
|
||||
SectionOptionDivider,
|
||||
sectionOptionDivider,
|
||||
GestureDetector(
|
||||
behavior: HitTestBehavior.translucent,
|
||||
onTap: () async {
|
||||
|
|
|
@ -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),
|
||||
);
|
||||
|
||||
|
|
|
@ -35,7 +35,7 @@ class _DangerSectionWidgetState extends State<DangerSectionWidget> {
|
|||
},
|
||||
child: SettingsTextItem(text: "Logout", icon: Icons.navigate_next),
|
||||
),
|
||||
SectionOptionDivider,
|
||||
sectionOptionDivider,
|
||||
GestureDetector(
|
||||
behavior: HitTestBehavior.translucent,
|
||||
onTap: () {
|
||||
|
|
|
@ -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(),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
|
|
@ -116,7 +116,7 @@ class _SecuritySectionWidgetState extends State<SecuritySectionWidget> {
|
|||
);
|
||||
}
|
||||
children.addAll([
|
||||
SectionOptionDivider,
|
||||
sectionOptionDivider,
|
||||
SizedBox(
|
||||
height: 48,
|
||||
child: Row(
|
||||
|
@ -150,7 +150,7 @@ class _SecuritySectionWidgetState extends State<SecuritySectionWidget> {
|
|||
if (Platform.isAndroid) {
|
||||
children.addAll(
|
||||
[
|
||||
SectionOptionDivider,
|
||||
sectionOptionDivider,
|
||||
SizedBox(
|
||||
height: 48,
|
||||
child: Row(
|
||||
|
@ -246,7 +246,7 @@ class _SecuritySectionWidgetState extends State<SecuritySectionWidget> {
|
|||
);
|
||||
}
|
||||
children.addAll([
|
||||
SectionOptionDivider,
|
||||
sectionOptionDivider,
|
||||
GestureDetector(
|
||||
behavior: HitTestBehavior.translucent,
|
||||
onTap: () async {
|
||||
|
|
|
@ -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: () {
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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<int>.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,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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<String> 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<Widget> 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(),
|
||||
|
|
|
@ -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<void> 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<void> _sendLogs(
|
|||
Future<String> 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);
|
||||
|
|
|
@ -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<UploadURL> _getUploadURL() async {
|
||||
if (_uploadURLs.isEmpty) {
|
||||
await _fetchUploadURLs();
|
||||
await fetchUploadURLs(_queue.length);
|
||||
}
|
||||
return _uploadURLs.removeFirst();
|
||||
}
|
||||
|
||||
Future<void> _uploadURLFetchInProgress;
|
||||
|
||||
Future<void> _fetchUploadURLs() async {
|
||||
Future<void> fetchUploadURLs(int fileCount) async {
|
||||
_uploadURLFetchInProgress ??= Future<void>(() 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()},
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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
|
||||
|
|