commit
2f3321cdd8
142 changed files with 4006 additions and 2612 deletions
|
@ -14,6 +14,7 @@ linter:
|
|||
- prefer_const_constructors_in_immutables
|
||||
- prefer_const_declarations
|
||||
- prefer_const_literals_to_create_immutables
|
||||
- require_trailing_commas
|
||||
- sized_box_for_whitespace
|
||||
- use_full_hex_values_for_flutter_colors
|
||||
- use_key_in_widget_constructors
|
||||
|
|
49
lib/app.dart
49
lib/app.dart
|
@ -24,7 +24,9 @@ final lightThemeData = ThemeData(
|
|||
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)),
|
||||
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(
|
||||
|
@ -34,7 +36,9 @@ final lightThemeData = ThemeData(
|
|||
fgEnabled: Colors.white,
|
||||
),
|
||||
elevatedButtonTheme: buildElevatedButtonThemeData(
|
||||
onPrimary: Colors.white, primary: Colors.black),
|
||||
onPrimary: Colors.white,
|
||||
primary: Colors.black,
|
||||
),
|
||||
toggleableActiveColor: Colors.green[400],
|
||||
scaffoldBackgroundColor: Colors.white,
|
||||
backgroundColor: Colors.white,
|
||||
|
@ -47,19 +51,25 @@ final lightThemeData = ThemeData(
|
|||
//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)),
|
||||
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))),
|
||||
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(
|
||||
|
@ -101,12 +111,15 @@ final darkThemeData = ThemeData(
|
|||
textTheme: _buildTextTheme(Colors.white),
|
||||
toggleableActiveColor: Colors.green[400],
|
||||
outlinedButtonTheme: buildOutlinedButtonThemeData(
|
||||
bgDisabled: Colors.grey.shade500,
|
||||
bgEnabled: Colors.white,
|
||||
fgDisabled: Colors.white,
|
||||
fgEnabled: Colors.black),
|
||||
bgDisabled: Colors.grey.shade500,
|
||||
bgEnabled: Colors.white,
|
||||
fgDisabled: Colors.white,
|
||||
fgEnabled: Colors.black,
|
||||
),
|
||||
elevatedButtonTheme: buildElevatedButtonThemeData(
|
||||
onPrimary: Colors.black, primary: Colors.white),
|
||||
onPrimary: Colors.black,
|
||||
primary: Colors.white,
|
||||
),
|
||||
scaffoldBackgroundColor: Colors.black,
|
||||
backgroundColor: Colors.black,
|
||||
appBarTheme: AppBarTheme().copyWith(
|
||||
|
|
23
lib/core/cache/thumbnail_cache.dart
vendored
23
lib/core/cache/thumbnail_cache.dart
vendored
|
@ -8,9 +8,11 @@ class ThumbnailLruCache {
|
|||
static final LRUMap<String, Uint8List> _map = LRUMap(1000);
|
||||
|
||||
static Uint8List get(File photo, [int size]) {
|
||||
return _map.get(photo.generatedID.toString() +
|
||||
"_" +
|
||||
(size != null ? size.toString() : kThumbnailLargeSize.toString()));
|
||||
return _map.get(
|
||||
photo.generatedID.toString() +
|
||||
"_" +
|
||||
(size != null ? size.toString() : kThumbnailLargeSize.toString()),
|
||||
);
|
||||
}
|
||||
|
||||
static void put(
|
||||
|
@ -19,16 +21,19 @@ class ThumbnailLruCache {
|
|||
int size,
|
||||
]) {
|
||||
_map.put(
|
||||
photo.generatedID.toString() +
|
||||
"_" +
|
||||
(size != null ? size.toString() : kThumbnailLargeSize.toString()),
|
||||
imageData);
|
||||
photo.generatedID.toString() +
|
||||
"_" +
|
||||
(size != null ? size.toString() : kThumbnailLargeSize.toString()),
|
||||
imageData,
|
||||
);
|
||||
}
|
||||
|
||||
static void clearCache(File file) {
|
||||
_map.remove(
|
||||
file.generatedID.toString() + "_" + kThumbnailLargeSize.toString());
|
||||
file.generatedID.toString() + "_" + kThumbnailLargeSize.toString(),
|
||||
);
|
||||
_map.remove(
|
||||
file.generatedID.toString() + "_" + kThumbnailSmallSize.toString());
|
||||
file.generatedID.toString() + "_" + kThumbnailSmallSize.toString(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -187,8 +187,11 @@ class Configuration {
|
|||
Sodium.bin2base64(encryptedRecoveryKey.encryptedData),
|
||||
Sodium.bin2base64(encryptedRecoveryKey.nonce),
|
||||
);
|
||||
final privateAttributes = PrivateKeyAttributes(Sodium.bin2base64(masterKey),
|
||||
Sodium.bin2hex(recoveryKey), Sodium.bin2base64(keyPair.sk));
|
||||
final privateAttributes = PrivateKeyAttributes(
|
||||
Sodium.bin2base64(masterKey),
|
||||
Sodium.bin2hex(recoveryKey),
|
||||
Sodium.bin2base64(keyPair.sk),
|
||||
);
|
||||
return KeyGenResult(attributes, privateAttributes);
|
||||
}
|
||||
|
||||
|
@ -218,7 +221,9 @@ class Configuration {
|
|||
}
|
||||
|
||||
Future<void> decryptAndSaveSecrets(
|
||||
String password, KeyAttributes attributes) async {
|
||||
String password,
|
||||
KeyAttributes attributes,
|
||||
) async {
|
||||
final kek = await CryptoUtil.deriveKey(
|
||||
utf8.encode(password),
|
||||
Sodium.base642bin(attributes.kekSalt),
|
||||
|
@ -227,23 +232,29 @@ class Configuration {
|
|||
);
|
||||
Uint8List key;
|
||||
try {
|
||||
key = CryptoUtil.decryptSync(Sodium.base642bin(attributes.encryptedKey),
|
||||
kek, Sodium.base642bin(attributes.keyDecryptionNonce));
|
||||
key = CryptoUtil.decryptSync(
|
||||
Sodium.base642bin(attributes.encryptedKey),
|
||||
kek,
|
||||
Sodium.base642bin(attributes.keyDecryptionNonce),
|
||||
);
|
||||
} catch (e) {
|
||||
throw Exception("Incorrect password");
|
||||
}
|
||||
await setKey(Sodium.bin2base64(key));
|
||||
final secretKey = CryptoUtil.decryptSync(
|
||||
Sodium.base642bin(attributes.encryptedSecretKey),
|
||||
key,
|
||||
Sodium.base642bin(attributes.secretKeyDecryptionNonce));
|
||||
Sodium.base642bin(attributes.encryptedSecretKey),
|
||||
key,
|
||||
Sodium.base642bin(attributes.secretKeyDecryptionNonce),
|
||||
);
|
||||
await setSecretKey(Sodium.bin2base64(secretKey));
|
||||
final token = CryptoUtil.openSealSync(
|
||||
Sodium.base642bin(getEncryptedToken()),
|
||||
Sodium.base642bin(attributes.publicKey),
|
||||
secretKey);
|
||||
Sodium.base642bin(getEncryptedToken()),
|
||||
Sodium.base642bin(attributes.publicKey),
|
||||
secretKey,
|
||||
);
|
||||
await setToken(
|
||||
Sodium.bin2base64(token, variant: Sodium.base64VariantUrlsafe));
|
||||
Sodium.bin2base64(token, variant: Sodium.base64VariantUrlsafe),
|
||||
);
|
||||
}
|
||||
|
||||
Future<KeyAttributes> createNewRecoveryKey() async {
|
||||
|
@ -272,7 +283,8 @@ class Configuration {
|
|||
if (recoveryKey.contains(' ')) {
|
||||
if (recoveryKey.split(' ').length != kMnemonicKeyWordCount) {
|
||||
throw AssertionError(
|
||||
'recovery code should have $kMnemonicKeyWordCount words');
|
||||
'recovery code should have $kMnemonicKeyWordCount words',
|
||||
);
|
||||
}
|
||||
recoveryKey = bip39.mnemonicToEntropy(recoveryKey);
|
||||
}
|
||||
|
@ -280,25 +292,29 @@ class Configuration {
|
|||
Uint8List masterKey;
|
||||
try {
|
||||
masterKey = await CryptoUtil.decrypt(
|
||||
Sodium.base642bin(attributes.masterKeyEncryptedWithRecoveryKey),
|
||||
Sodium.hex2bin(recoveryKey),
|
||||
Sodium.base642bin(attributes.masterKeyDecryptionNonce));
|
||||
Sodium.base642bin(attributes.masterKeyEncryptedWithRecoveryKey),
|
||||
Sodium.hex2bin(recoveryKey),
|
||||
Sodium.base642bin(attributes.masterKeyDecryptionNonce),
|
||||
);
|
||||
} catch (e) {
|
||||
_logger.severe(e);
|
||||
rethrow;
|
||||
}
|
||||
await setKey(Sodium.bin2base64(masterKey));
|
||||
final secretKey = CryptoUtil.decryptSync(
|
||||
Sodium.base642bin(attributes.encryptedSecretKey),
|
||||
masterKey,
|
||||
Sodium.base642bin(attributes.secretKeyDecryptionNonce));
|
||||
Sodium.base642bin(attributes.encryptedSecretKey),
|
||||
masterKey,
|
||||
Sodium.base642bin(attributes.secretKeyDecryptionNonce),
|
||||
);
|
||||
await setSecretKey(Sodium.bin2base64(secretKey));
|
||||
final token = CryptoUtil.openSealSync(
|
||||
Sodium.base642bin(getEncryptedToken()),
|
||||
Sodium.base642bin(attributes.publicKey),
|
||||
secretKey);
|
||||
Sodium.base642bin(getEncryptedToken()),
|
||||
Sodium.base642bin(attributes.publicKey),
|
||||
secretKey,
|
||||
);
|
||||
await setToken(
|
||||
Sodium.bin2base64(token, variant: Sodium.base64VariantUrlsafe));
|
||||
Sodium.bin2base64(token, variant: Sodium.base64VariantUrlsafe),
|
||||
);
|
||||
}
|
||||
|
||||
String getHttpEndpoint() {
|
||||
|
@ -428,9 +444,10 @@ class Configuration {
|
|||
Uint8List getRecoveryKey() {
|
||||
final keyAttributes = getKeyAttributes();
|
||||
return CryptoUtil.decryptSync(
|
||||
Sodium.base642bin(keyAttributes.recoveryKeyEncryptedWithMasterKey),
|
||||
getKey(),
|
||||
Sodium.base642bin(keyAttributes.recoveryKeyDecryptionNonce));
|
||||
Sodium.base642bin(keyAttributes.recoveryKeyEncryptedWithMasterKey),
|
||||
getKey(),
|
||||
Sodium.base642bin(keyAttributes.recoveryKeyDecryptionNonce),
|
||||
);
|
||||
}
|
||||
|
||||
String getDocumentsDirectory() {
|
||||
|
@ -551,7 +568,9 @@ class Configuration {
|
|||
iOptions: _secureStorageOptionsIOS,
|
||||
);
|
||||
await _preferences.setBool(
|
||||
hasMigratedSecureStorageToFirstUnlockKey, true);
|
||||
hasMigratedSecureStorageToFirstUnlockKey,
|
||||
true,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -63,7 +63,8 @@ class TunneledTransport implements Transport {
|
|||
}
|
||||
|
||||
Future<StreamedRequest> _createStreamedRequest(
|
||||
SentryEnvelope envelope) async {
|
||||
SentryEnvelope envelope,
|
||||
) async {
|
||||
final streamedRequest = StreamedRequest('POST', _tunnel);
|
||||
envelope
|
||||
.envelopeStream(_options)
|
||||
|
@ -88,7 +89,10 @@ class _CredentialBuilder {
|
|||
_clock = clock;
|
||||
|
||||
factory _CredentialBuilder(
|
||||
Dsn dsn, String sdkIdentifier, ClockProvider clock) {
|
||||
Dsn dsn,
|
||||
String sdkIdentifier,
|
||||
ClockProvider clock,
|
||||
) {
|
||||
final authHeader = _buildAuthHeader(
|
||||
publicKey: dsn.publicKey,
|
||||
secretKey: dsn.secretKey,
|
||||
|
|
|
@ -17,11 +17,16 @@ class Network {
|
|||
_alice = Alice(darkTheme: true, showNotification: kDebugMode);
|
||||
await FkUserAgent.init();
|
||||
final packageInfo = await PackageInfo.fromPlatform();
|
||||
_dio = Dio(BaseOptions(connectTimeout: kConnectTimeout, headers: {
|
||||
HttpHeaders.userAgentHeader: FkUserAgent.userAgent,
|
||||
'X-Client-Version': packageInfo.version,
|
||||
'X-Client-Package': packageInfo.packageName,
|
||||
}));
|
||||
_dio = Dio(
|
||||
BaseOptions(
|
||||
connectTimeout: kConnectTimeout,
|
||||
headers: {
|
||||
HttpHeaders.userAgentHeader: FkUserAgent.userAgent,
|
||||
'X-Client-Version': packageInfo.version,
|
||||
'X-Client-Package': packageInfo.packageName,
|
||||
},
|
||||
),
|
||||
);
|
||||
_dio.interceptors.add(RequestIdInterceptor());
|
||||
_dio.interceptors.add(_alice.getDioInterceptor());
|
||||
}
|
||||
|
|
|
@ -44,7 +44,9 @@ class CollectionsDB {
|
|||
];
|
||||
|
||||
final dbConfig = MigrationConfig(
|
||||
initializationScript: intitialScript, migrationScripts: migrationScripts);
|
||||
initializationScript: intitialScript,
|
||||
migrationScripts: migrationScripts,
|
||||
);
|
||||
|
||||
CollectionsDB._privateConstructor();
|
||||
|
||||
|
@ -157,8 +159,11 @@ class CollectionsDB {
|
|||
final db = await instance.database;
|
||||
var batch = db.batch();
|
||||
for (final collection in collections) {
|
||||
batch.insert(table, _getRowForCollection(collection),
|
||||
conflictAlgorithm: ConflictAlgorithm.replace);
|
||||
batch.insert(
|
||||
table,
|
||||
_getRowForCollection(collection),
|
||||
conflictAlgorithm: ConflictAlgorithm.replace,
|
||||
);
|
||||
}
|
||||
return await batch.commit();
|
||||
}
|
||||
|
@ -239,12 +244,15 @@ class CollectionsDB {
|
|||
pathDecryptionNonce: row[columnPathDecryptionNonce],
|
||||
version: row[columnVersion],
|
||||
),
|
||||
List<User>.from((json.decode(row[columnSharees]) as List)
|
||||
.map((x) => User.fromMap(x))),
|
||||
List<User>.from(
|
||||
(json.decode(row[columnSharees]) as List).map((x) => User.fromMap(x)),
|
||||
),
|
||||
row[columnPublicURLs] == null
|
||||
? []
|
||||
: List<PublicURL>.from((json.decode(row[columnPublicURLs]) as List)
|
||||
.map((x) => PublicURL.fromMap(x))),
|
||||
: List<PublicURL>.from(
|
||||
(json.decode(row[columnPublicURLs]) as List)
|
||||
.map((x) => PublicURL.fromMap(x)),
|
||||
),
|
||||
int.parse(row[columnUpdationTime]),
|
||||
// default to False is columnIsDeleted is not set
|
||||
isDeleted: (row[columnIsDeleted] ?? _sqlBoolFalse) == _sqlBoolTrue,
|
||||
|
|
|
@ -14,12 +14,14 @@ class FilesMigrationDB {
|
|||
static final columnLocalID = 'local_id';
|
||||
|
||||
Future _onCreate(Database db, int version) async {
|
||||
await db.execute('''
|
||||
await db.execute(
|
||||
'''
|
||||
CREATE TABLE $tableName (
|
||||
$columnLocalID TEXT NOT NULL,
|
||||
UNIQUE($columnLocalID)
|
||||
);
|
||||
''');
|
||||
''',
|
||||
);
|
||||
}
|
||||
|
||||
FilesMigrationDB._privateConstructor();
|
||||
|
@ -73,10 +75,13 @@ class FilesMigrationDB {
|
|||
await batch.commit(noResult: true);
|
||||
final endTime = DateTime.now();
|
||||
final duration = Duration(
|
||||
microseconds:
|
||||
endTime.microsecondsSinceEpoch - startTime.microsecondsSinceEpoch);
|
||||
_logger.info("Batch insert of ${fileLocalIDs.length} "
|
||||
"took ${duration.inMilliseconds} ms.");
|
||||
microseconds:
|
||||
endTime.microsecondsSinceEpoch - startTime.microsecondsSinceEpoch,
|
||||
);
|
||||
_logger.info(
|
||||
"Batch insert of ${fileLocalIDs.length} "
|
||||
"took ${duration.inMilliseconds} ms.",
|
||||
);
|
||||
}
|
||||
|
||||
Future<int> deleteByLocalIDs(List<String> localIDs) async {
|
||||
|
|
|
@ -76,8 +76,9 @@ class FilesDB {
|
|||
];
|
||||
|
||||
final dbConfig = MigrationConfig(
|
||||
initializationScript: initializationScript,
|
||||
migrationScripts: migrationScripts);
|
||||
initializationScript: initializationScript,
|
||||
migrationScripts: migrationScripts,
|
||||
);
|
||||
|
||||
// make this a singleton class
|
||||
FilesDB._privateConstructor();
|
||||
|
@ -319,13 +320,16 @@ class FilesDB {
|
|||
await batch.commit(noResult: true);
|
||||
final endTime = DateTime.now();
|
||||
final duration = Duration(
|
||||
microseconds:
|
||||
endTime.microsecondsSinceEpoch - startTime.microsecondsSinceEpoch);
|
||||
_logger.info("Batch insert of " +
|
||||
files.length.toString() +
|
||||
" took " +
|
||||
duration.inMilliseconds.toString() +
|
||||
"ms.");
|
||||
microseconds:
|
||||
endTime.microsecondsSinceEpoch - startTime.microsecondsSinceEpoch,
|
||||
);
|
||||
_logger.info(
|
||||
"Batch insert of " +
|
||||
files.length.toString() +
|
||||
" took " +
|
||||
duration.inMilliseconds.toString() +
|
||||
"ms.",
|
||||
);
|
||||
}
|
||||
|
||||
Future<int> insert(File file) async {
|
||||
|
@ -339,8 +343,11 @@ class FilesDB {
|
|||
|
||||
Future<File> getFile(int generatedID) async {
|
||||
final db = await instance.database;
|
||||
final results = await db.query(table,
|
||||
where: '$columnGeneratedID = ?', whereArgs: [generatedID]);
|
||||
final results = await db.query(
|
||||
table,
|
||||
where: '$columnGeneratedID = ?',
|
||||
whereArgs: [generatedID],
|
||||
);
|
||||
if (results.isEmpty) {
|
||||
return null;
|
||||
}
|
||||
|
@ -398,11 +405,14 @@ class FilesDB {
|
|||
}
|
||||
|
||||
Future<FileLoadResult> getAllUploadedFiles(
|
||||
int startTime, int endTime, int ownerID,
|
||||
{int limit,
|
||||
bool asc,
|
||||
int visibility = kVisibilityVisible,
|
||||
Set<int> ignoredCollectionIDs}) async {
|
||||
int startTime,
|
||||
int endTime,
|
||||
int ownerID, {
|
||||
int limit,
|
||||
bool asc,
|
||||
int visibility = kVisibilityVisible,
|
||||
Set<int> ignoredCollectionIDs,
|
||||
}) async {
|
||||
final db = await instance.database;
|
||||
final order = (asc ?? false ? 'ASC' : 'DESC');
|
||||
final results = await db.query(
|
||||
|
@ -422,8 +432,13 @@ class FilesDB {
|
|||
}
|
||||
|
||||
Future<FileLoadResult> getAllLocalAndUploadedFiles(
|
||||
int startTime, int endTime, int ownerID,
|
||||
{int limit, bool asc, Set<int> ignoredCollectionIDs}) async {
|
||||
int startTime,
|
||||
int endTime,
|
||||
int ownerID, {
|
||||
int limit,
|
||||
bool asc,
|
||||
Set<int> ignoredCollectionIDs,
|
||||
}) async {
|
||||
final db = await instance.database;
|
||||
final order = (asc ?? false ? 'ASC' : 'DESC');
|
||||
final results = await db.query(
|
||||
|
@ -443,8 +458,14 @@ class FilesDB {
|
|||
}
|
||||
|
||||
Future<FileLoadResult> getImportantFiles(
|
||||
int startTime, int endTime, int ownerID, List<String> paths,
|
||||
{int limit, bool asc, Set<int> ignoredCollectionIDs}) async {
|
||||
int startTime,
|
||||
int endTime,
|
||||
int ownerID,
|
||||
List<String> paths, {
|
||||
int limit,
|
||||
bool asc,
|
||||
Set<int> ignoredCollectionIDs,
|
||||
}) async {
|
||||
final db = await instance.database;
|
||||
String inParam = "";
|
||||
for (final path in paths) {
|
||||
|
@ -469,7 +490,9 @@ class FilesDB {
|
|||
}
|
||||
|
||||
List<File> _deduplicatedAndFilterIgnoredFiles(
|
||||
List<File> files, Set<int> ignoredCollectionIDs) {
|
||||
List<File> files,
|
||||
Set<int> ignoredCollectionIDs,
|
||||
) {
|
||||
final uploadedFileIDs = <int>{};
|
||||
final List<File> deduplicatedFiles = [];
|
||||
for (final file in files) {
|
||||
|
@ -488,8 +511,12 @@ class FilesDB {
|
|||
}
|
||||
|
||||
Future<FileLoadResult> getFilesInCollection(
|
||||
int collectionID, int startTime, int endTime,
|
||||
{int limit, bool asc}) async {
|
||||
int collectionID,
|
||||
int startTime,
|
||||
int endTime, {
|
||||
int limit,
|
||||
bool asc,
|
||||
}) async {
|
||||
final db = await instance.database;
|
||||
final order = (asc ?? false ? 'ASC' : 'DESC');
|
||||
final results = await db.query(
|
||||
|
@ -506,8 +533,13 @@ class FilesDB {
|
|||
return FileLoadResult(files, files.length == limit);
|
||||
}
|
||||
|
||||
Future<FileLoadResult> getFilesInPath(String path, int startTime, int endTime,
|
||||
{int limit, bool asc}) async {
|
||||
Future<FileLoadResult> getFilesInPath(
|
||||
String path,
|
||||
int startTime,
|
||||
int endTime, {
|
||||
int limit,
|
||||
bool asc,
|
||||
}) async {
|
||||
final db = await instance.database;
|
||||
final order = (asc ?? false ? 'ASC' : 'DESC');
|
||||
final results = await db.query(
|
||||
|
@ -547,7 +579,8 @@ class FilesDB {
|
|||
}
|
||||
|
||||
Future<List<File>> getFilesCreatedWithinDurations(
|
||||
List<List<int>> durations) async {
|
||||
List<List<int>> durations,
|
||||
) async {
|
||||
final db = await instance.database;
|
||||
String whereClause = "";
|
||||
for (int index = 0; index < durations.length; index++) {
|
||||
|
@ -569,7 +602,8 @@ class FilesDB {
|
|||
}
|
||||
|
||||
Future<List<File>> getFilesToBeUploadedWithinFolders(
|
||||
Set<String> folders) async {
|
||||
Set<String> folders,
|
||||
) async {
|
||||
if (folders.isEmpty) {
|
||||
return [];
|
||||
}
|
||||
|
@ -604,10 +638,12 @@ class FilesDB {
|
|||
var files = _convertToFiles(results);
|
||||
// future-safe filter just to ensure that the query doesn't end up returning files
|
||||
// which should not be backed up
|
||||
files.removeWhere((e) =>
|
||||
e.collectionID == null ||
|
||||
e.localID == null ||
|
||||
e.uploadedFileID != null);
|
||||
files.removeWhere(
|
||||
(e) =>
|
||||
e.collectionID == null ||
|
||||
e.localID == null ||
|
||||
e.uploadedFileID != null,
|
||||
);
|
||||
return files;
|
||||
}
|
||||
|
||||
|
@ -801,11 +837,13 @@ class FilesDB {
|
|||
}
|
||||
inParam = inParam.substring(0, inParam.length - 1);
|
||||
final db = await instance.database;
|
||||
await db.rawQuery('''
|
||||
await db.rawQuery(
|
||||
'''
|
||||
UPDATE $table
|
||||
SET $columnLocalID = NULL
|
||||
WHERE $columnLocalID IN ($inParam);
|
||||
''');
|
||||
''',
|
||||
);
|
||||
}
|
||||
|
||||
Future<List<File>> getLocalFiles(List<String> localIDs) async {
|
||||
|
@ -846,7 +884,9 @@ class FilesDB {
|
|||
}
|
||||
|
||||
Future<int> deleteFilesFromCollection(
|
||||
int collectionID, List<int> uploadedFileIDs) async {
|
||||
int collectionID,
|
||||
List<int> uploadedFileIDs,
|
||||
) async {
|
||||
final db = await instance.database;
|
||||
return db.delete(
|
||||
table,
|
||||
|
@ -858,15 +898,21 @@ class FilesDB {
|
|||
|
||||
Future<int> collectionFileCount(int collectionID) async {
|
||||
final db = await instance.database;
|
||||
var count = Sqflite.firstIntValue(await db.rawQuery(
|
||||
'SELECT COUNT(*) FROM $table where $columnCollectionID = $collectionID'));
|
||||
var count = Sqflite.firstIntValue(
|
||||
await db.rawQuery(
|
||||
'SELECT COUNT(*) FROM $table where $columnCollectionID = $collectionID',
|
||||
),
|
||||
);
|
||||
return count;
|
||||
}
|
||||
|
||||
Future<int> fileCountWithVisibility(int visibility, int ownerID) async {
|
||||
final db = await instance.database;
|
||||
var count = Sqflite.firstIntValue(await db.rawQuery(
|
||||
'SELECT COUNT(*) FROM $table where $columnMMdVisibility = $visibility AND $columnOwnerID = $ownerID'));
|
||||
var count = Sqflite.firstIntValue(
|
||||
await db.rawQuery(
|
||||
'SELECT COUNT(*) FROM $table where $columnMMdVisibility = $visibility AND $columnOwnerID = $ownerID',
|
||||
),
|
||||
);
|
||||
return count;
|
||||
}
|
||||
|
||||
|
@ -891,7 +937,8 @@ class FilesDB {
|
|||
|
||||
Future<List<File>> getLatestLocalFiles() async {
|
||||
final db = await instance.database;
|
||||
final rows = await db.rawQuery('''
|
||||
final rows = await db.rawQuery(
|
||||
'''
|
||||
SELECT $table.*
|
||||
FROM $table
|
||||
INNER JOIN
|
||||
|
@ -903,7 +950,8 @@ class FilesDB {
|
|||
) latest_files
|
||||
ON $table.$columnDeviceFolder = latest_files.$columnDeviceFolder
|
||||
AND $table.$columnCreationTime = latest_files.max_creation_time;
|
||||
''');
|
||||
''',
|
||||
);
|
||||
final files = _convertToFiles(rows);
|
||||
// TODO: Do this de-duplication within the SQL Query
|
||||
final folderMap = <String, File>{};
|
||||
|
@ -920,7 +968,8 @@ class FilesDB {
|
|||
|
||||
Future<List<File>> getLatestCollectionFiles() async {
|
||||
final db = await instance.database;
|
||||
final rows = await db.rawQuery('''
|
||||
final rows = await db.rawQuery(
|
||||
'''
|
||||
SELECT $table.*
|
||||
FROM $table
|
||||
INNER JOIN
|
||||
|
@ -932,7 +981,8 @@ class FilesDB {
|
|||
) latest_files
|
||||
ON $table.$columnCollectionID = latest_files.$columnCollectionID
|
||||
AND $table.$columnCreationTime = latest_files.max_creation_time;
|
||||
''');
|
||||
''',
|
||||
);
|
||||
final files = _convertToFiles(rows);
|
||||
// TODO: Do this de-duplication within the SQL Query
|
||||
final collectionMap = <int, File>{};
|
||||
|
@ -965,12 +1015,14 @@ class FilesDB {
|
|||
|
||||
Future<Map<String, int>> getFileCountInDeviceFolders() async {
|
||||
final db = await instance.database;
|
||||
final rows = await db.rawQuery('''
|
||||
final rows = await db.rawQuery(
|
||||
'''
|
||||
SELECT COUNT($columnGeneratedID) as count, $columnDeviceFolder
|
||||
FROM $table
|
||||
WHERE $columnLocalID IS NOT NULL
|
||||
GROUP BY $columnDeviceFolder
|
||||
''');
|
||||
''',
|
||||
);
|
||||
final result = <String, int>{};
|
||||
for (final row in rows) {
|
||||
result[row[columnDeviceFolder]] = row["count"];
|
||||
|
@ -1005,16 +1057,20 @@ class FilesDB {
|
|||
}
|
||||
inParam = inParam.substring(0, inParam.length - 1);
|
||||
final db = await instance.database;
|
||||
await db.rawUpdate('''
|
||||
await db.rawUpdate(
|
||||
'''
|
||||
UPDATE $table
|
||||
SET $columnUpdationTime = NULL
|
||||
WHERE $columnLocalID IN ($inParam)
|
||||
AND ($columnLatitude IS NULL OR $columnLongitude IS NULL OR $columnLongitude = 0.0 or $columnLongitude = 0.0);
|
||||
''');
|
||||
''',
|
||||
);
|
||||
}
|
||||
|
||||
Future<bool> doesFileExistInCollection(
|
||||
int uploadedFileID, int collectionID) async {
|
||||
int uploadedFileID,
|
||||
int collectionID,
|
||||
) async {
|
||||
final db = await instance.database;
|
||||
final rows = await db.query(
|
||||
table,
|
||||
|
|
|
@ -22,7 +22,8 @@ class IgnoredFilesDB {
|
|||
static final columnReason = 'reason';
|
||||
|
||||
Future _onCreate(Database db, int version) async {
|
||||
await db.execute('''
|
||||
await db.execute(
|
||||
'''
|
||||
CREATE TABLE $tableName (
|
||||
$columnLocalID TEXT NOT NULL,
|
||||
$columnTitle TEXT NOT NULL,
|
||||
|
@ -32,7 +33,8 @@ class IgnoredFilesDB {
|
|||
);
|
||||
CREATE INDEX IF NOT EXISTS local_id_index ON $tableName($columnLocalID);
|
||||
CREATE INDEX IF NOT EXISTS device_folder_index ON $tableName($columnDeviceFolder);
|
||||
''');
|
||||
''',
|
||||
);
|
||||
}
|
||||
|
||||
IgnoredFilesDB._privateConstructor();
|
||||
|
@ -85,10 +87,13 @@ class IgnoredFilesDB {
|
|||
await batch.commit(noResult: true);
|
||||
final endTime = DateTime.now();
|
||||
final duration = Duration(
|
||||
microseconds:
|
||||
endTime.microsecondsSinceEpoch - startTime.microsecondsSinceEpoch);
|
||||
_logger.info("Batch insert of ${ignoredFiles.length} "
|
||||
"took ${duration.inMilliseconds} ms.");
|
||||
microseconds:
|
||||
endTime.microsecondsSinceEpoch - startTime.microsecondsSinceEpoch,
|
||||
);
|
||||
_logger.info(
|
||||
"Batch insert of ${ignoredFiles.length} "
|
||||
"took ${duration.inMilliseconds} ms.",
|
||||
);
|
||||
}
|
||||
|
||||
Future<List<IgnoredFile>> getAll() async {
|
||||
|
@ -102,8 +107,12 @@ class IgnoredFilesDB {
|
|||
}
|
||||
|
||||
IgnoredFile _getIgnoredFileFromRow(Map<String, dynamic> row) {
|
||||
return IgnoredFile(row[columnLocalID], row[columnTitle],
|
||||
row[columnDeviceFolder], row[columnReason]);
|
||||
return IgnoredFile(
|
||||
row[columnLocalID],
|
||||
row[columnTitle],
|
||||
row[columnDeviceFolder],
|
||||
row[columnReason],
|
||||
);
|
||||
}
|
||||
|
||||
Map<String, dynamic> _getRowForIgnoredFile(IgnoredFile ignoredFile) {
|
||||
|
|
|
@ -35,12 +35,14 @@ class MemoriesDB {
|
|||
}
|
||||
|
||||
Future _onCreate(Database db, int version) async {
|
||||
await db.execute('''
|
||||
await db.execute(
|
||||
'''
|
||||
CREATE TABLE $table (
|
||||
$columnFileID INTEGER PRIMARY KEY NOT NULL,
|
||||
$columnSeenTime TEXT NOT NULL
|
||||
)
|
||||
''');
|
||||
''',
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> clearTable() async {
|
||||
|
@ -59,8 +61,11 @@ class MemoriesDB {
|
|||
|
||||
Future<int> markMemoryAsSeen(Memory memory, int timestamp) async {
|
||||
final db = await instance.database;
|
||||
return await db.insert(table, _getRowForSeenMemory(memory, timestamp),
|
||||
conflictAlgorithm: ConflictAlgorithm.replace);
|
||||
return await db.insert(
|
||||
table,
|
||||
_getRowForSeenMemory(memory, timestamp),
|
||||
conflictAlgorithm: ConflictAlgorithm.replace,
|
||||
);
|
||||
}
|
||||
|
||||
Future<Map<int, int>> getSeenTimes() async {
|
||||
|
|
|
@ -36,12 +36,14 @@ class PublicKeysDB {
|
|||
}
|
||||
|
||||
Future _onCreate(Database db, int version) async {
|
||||
await db.execute('''
|
||||
await db.execute(
|
||||
'''
|
||||
CREATE TABLE $table (
|
||||
$columnEmail TEXT PRIMARY KEY NOT NULL,
|
||||
$columnPublicKey TEXT NOT NULL
|
||||
)
|
||||
''');
|
||||
''',
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> clearTable() async {
|
||||
|
@ -51,17 +53,22 @@ class PublicKeysDB {
|
|||
|
||||
Future<int> setKey(PublicKey key) async {
|
||||
final db = await instance.database;
|
||||
return db.insert(table, _getRow(key),
|
||||
conflictAlgorithm: ConflictAlgorithm.replace);
|
||||
return db.insert(
|
||||
table,
|
||||
_getRow(key),
|
||||
conflictAlgorithm: ConflictAlgorithm.replace,
|
||||
);
|
||||
}
|
||||
|
||||
Future<List<PublicKey>> searchByEmail(String email) async {
|
||||
final db = await instance.database;
|
||||
return _convertRows(await db.query(
|
||||
table,
|
||||
where: '$columnEmail LIKE ?',
|
||||
whereArgs: ['%$email%'],
|
||||
));
|
||||
return _convertRows(
|
||||
await db.query(
|
||||
table,
|
||||
where: '$columnEmail LIKE ?',
|
||||
whereArgs: ['%$email%'],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Map<String, dynamic> _getRow(PublicKey key) {
|
||||
|
|
|
@ -42,7 +42,8 @@ class TrashDB {
|
|||
static final columnPubMMdVersion = 'pub_mmd_ver';
|
||||
|
||||
Future _onCreate(Database db, int version) async {
|
||||
await db.execute('''
|
||||
await db.execute(
|
||||
'''
|
||||
CREATE TABLE $tableName (
|
||||
$columnUploadedFileID INTEGER PRIMARY KEY NOT NULL,
|
||||
$columnCollectionID INTEGER NOT NULL,
|
||||
|
@ -65,7 +66,8 @@ class TrashDB {
|
|||
CREATE INDEX IF NOT EXISTS creation_time_index ON $tableName($columnCreationTime);
|
||||
CREATE INDEX IF NOT EXISTS delete_by_time_index ON $tableName($columnTrashDeleteBy);
|
||||
CREATE INDEX IF NOT EXISTS updated_at_time_index ON $tableName($columnTrashUpdatedAt);
|
||||
''');
|
||||
''',
|
||||
);
|
||||
}
|
||||
|
||||
TrashDB._privateConstructor();
|
||||
|
@ -101,8 +103,11 @@ class TrashDB {
|
|||
// getRecentlyTrashedFile returns the file which was trashed recently
|
||||
Future<TrashFile> getRecentlyTrashedFile() async {
|
||||
final db = await instance.database;
|
||||
var rows = await db.query(tableName,
|
||||
orderBy: '$columnTrashDeleteBy DESC', limit: 1);
|
||||
var rows = await db.query(
|
||||
tableName,
|
||||
orderBy: '$columnTrashDeleteBy DESC',
|
||||
limit: 1,
|
||||
);
|
||||
if (rows == null || rows.isEmpty) {
|
||||
return null;
|
||||
}
|
||||
|
@ -112,7 +117,8 @@ class TrashDB {
|
|||
Future<int> count() async {
|
||||
final db = await instance.database;
|
||||
var count = Sqflite.firstIntValue(
|
||||
await db.rawQuery('SELECT COUNT(*) FROM $tableName'));
|
||||
await db.rawQuery('SELECT COUNT(*) FROM $tableName'),
|
||||
);
|
||||
return count;
|
||||
}
|
||||
|
||||
|
@ -137,13 +143,16 @@ class TrashDB {
|
|||
await batch.commit(noResult: true);
|
||||
final endTime = DateTime.now();
|
||||
final duration = Duration(
|
||||
microseconds:
|
||||
endTime.microsecondsSinceEpoch - startTime.microsecondsSinceEpoch);
|
||||
_logger.info("Batch insert of " +
|
||||
trashFiles.length.toString() +
|
||||
" took " +
|
||||
duration.inMilliseconds.toString() +
|
||||
"ms.");
|
||||
microseconds:
|
||||
endTime.microsecondsSinceEpoch - startTime.microsecondsSinceEpoch,
|
||||
);
|
||||
_logger.info(
|
||||
"Batch insert of " +
|
||||
trashFiles.length.toString() +
|
||||
" took " +
|
||||
duration.inMilliseconds.toString() +
|
||||
"ms.",
|
||||
);
|
||||
}
|
||||
|
||||
Future<int> insert(TrashFile trash) async {
|
||||
|
@ -173,8 +182,12 @@ class TrashDB {
|
|||
);
|
||||
}
|
||||
|
||||
Future<FileLoadResult> getTrashedFiles(int startTime, int endTime,
|
||||
{int limit, bool asc}) async {
|
||||
Future<FileLoadResult> getTrashedFiles(
|
||||
int startTime,
|
||||
int endTime, {
|
||||
int limit,
|
||||
bool asc,
|
||||
}) async {
|
||||
final db = await instance.database;
|
||||
final order = (asc ?? false ? 'ASC' : 'DESC');
|
||||
final results = await db.query(
|
||||
|
|
|
@ -34,13 +34,15 @@ class UploadLocksDB {
|
|||
}
|
||||
|
||||
Future _onCreate(Database db, int version) async {
|
||||
await db.execute('''
|
||||
await db.execute(
|
||||
'''
|
||||
CREATE TABLE $_table (
|
||||
$_columnID TEXT PRIMARY KEY NOT NULL,
|
||||
$_columnOwner TEXT NOT NULL,
|
||||
$_columnTime TEXT NOT NULL
|
||||
)
|
||||
''');
|
||||
''',
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> clearTable() async {
|
||||
|
|
|
@ -31,10 +31,10 @@ extension CustomColorScheme on ColorScheme {
|
|||
|
||||
// todo: use brightness == Brightness.light for changing color for dark/light theme
|
||||
ButtonStyle get optionalActionButtonStyle => buildElevatedButtonThemeData(
|
||||
onPrimary: Color(0xFF777777),
|
||||
primary: Color(0xFFF0F0F0),
|
||||
elevation: 0)
|
||||
.style;
|
||||
onPrimary: Color(0xFF777777),
|
||||
primary: Color(0xFFF0F0F0),
|
||||
elevation: 0,
|
||||
).style;
|
||||
|
||||
Color get recoveryKeyBoxColor => brightness == Brightness.light
|
||||
? Color.fromRGBO(49, 155, 86, 0.2)
|
||||
|
@ -65,11 +65,13 @@ extension CustomColorScheme on ColorScheme {
|
|||
? DatePickerTheme(
|
||||
backgroundColor: Colors.white,
|
||||
itemStyle: TextStyle(color: Colors.black),
|
||||
cancelStyle: TextStyle(color: Colors.black))
|
||||
cancelStyle: TextStyle(color: Colors.black),
|
||||
)
|
||||
: DatePickerTheme(
|
||||
backgroundColor: Colors.black,
|
||||
itemStyle: TextStyle(color: Colors.white),
|
||||
cancelStyle: TextStyle(color: Colors.white));
|
||||
cancelStyle: TextStyle(color: Colors.white),
|
||||
);
|
||||
|
||||
Color get stepProgressUnselectedColor => brightness == Brightness.light
|
||||
? Color.fromRGBO(196, 196, 196, 0.6)
|
||||
|
@ -123,8 +125,12 @@ extension CustomColorScheme on ColorScheme {
|
|||
: Color.fromRGBO(100, 100, 100, 1);
|
||||
}
|
||||
|
||||
OutlinedButtonThemeData buildOutlinedButtonThemeData(
|
||||
{Color bgDisabled, Color bgEnabled, Color fgDisabled, Color fgEnabled}) {
|
||||
OutlinedButtonThemeData buildOutlinedButtonThemeData({
|
||||
Color bgDisabled,
|
||||
Color bgEnabled,
|
||||
Color fgDisabled,
|
||||
Color fgEnabled,
|
||||
}) {
|
||||
return OutlinedButtonThemeData(
|
||||
style: OutlinedButton.styleFrom(
|
||||
shape: RoundedRectangleBorder(
|
||||
|
@ -159,27 +165,28 @@ OutlinedButtonThemeData buildOutlinedButtonThemeData(
|
|||
);
|
||||
}
|
||||
|
||||
ElevatedButtonThemeData buildElevatedButtonThemeData(
|
||||
{@required Color onPrimary, // text button color
|
||||
@required Color primary,
|
||||
double elevation = 2 // background color of button
|
||||
}) {
|
||||
ElevatedButtonThemeData buildElevatedButtonThemeData({
|
||||
@required Color onPrimary, // text button color
|
||||
@required Color primary,
|
||||
double elevation = 2, // background color of button
|
||||
}) {
|
||||
return ElevatedButtonThemeData(
|
||||
style: ElevatedButton.styleFrom(
|
||||
elevation: elevation,
|
||||
onPrimary: onPrimary,
|
||||
primary: primary,
|
||||
alignment: Alignment.center,
|
||||
textStyle: TextStyle(
|
||||
fontWeight: FontWeight.w600,
|
||||
fontFamily: 'Inter-SemiBold',
|
||||
fontSize: 18,
|
||||
style: ElevatedButton.styleFrom(
|
||||
elevation: elevation,
|
||||
onPrimary: onPrimary,
|
||||
primary: primary,
|
||||
alignment: Alignment.center,
|
||||
textStyle: TextStyle(
|
||||
fontWeight: FontWeight.w600,
|
||||
fontFamily: 'Inter-SemiBold',
|
||||
fontSize: 18,
|
||||
),
|
||||
padding: EdgeInsets.symmetric(vertical: 18),
|
||||
shape: const RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.all(Radius.circular(8)),
|
||||
),
|
||||
),
|
||||
padding: EdgeInsets.symmetric(vertical: 18),
|
||||
shape: const RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.all(Radius.circular(8)),
|
||||
),
|
||||
));
|
||||
);
|
||||
}
|
||||
|
||||
TextStyle gradientButtonTextTheme() {
|
||||
|
|
|
@ -58,13 +58,15 @@ Future<void> _runInForeground() async {
|
|||
_logger.info("Starting app in foreground");
|
||||
await _init(false, via: 'mainMethod');
|
||||
_scheduleFGSync('appStart in FG');
|
||||
runApp(AppLock(
|
||||
builder: (args) => EnteApp(_runBackgroundTask, _killBGTask),
|
||||
lockScreen: LockScreen(),
|
||||
enabled: Configuration.instance.shouldShowLockScreen(),
|
||||
lightTheme: lightThemeData,
|
||||
darkTheme: darkThemeData,
|
||||
));
|
||||
runApp(
|
||||
AppLock(
|
||||
builder: (args) => EnteApp(_runBackgroundTask, _killBGTask),
|
||||
lockScreen: LockScreen(),
|
||||
enabled: Configuration.instance.shouldShowLockScreen(),
|
||||
lightTheme: lightThemeData,
|
||||
darkTheme: darkThemeData,
|
||||
),
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -74,10 +76,13 @@ Future<void> _runBackgroundTask(String taskId) async {
|
|||
await _sync('bgTaskActiveProcess');
|
||||
BackgroundFetch.finish(taskId);
|
||||
} else {
|
||||
_runWithLogs(() async {
|
||||
_logger.info("run background task");
|
||||
_runInBackground(taskId);
|
||||
}, prefix: "[bg]");
|
||||
_runWithLogs(
|
||||
() async {
|
||||
_logger.info("run background task");
|
||||
_runInBackground(taskId);
|
||||
},
|
||||
prefix: "[bg]",
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -139,7 +144,8 @@ Future<void> _init(bool isBackground, {String via = ''}) async {
|
|||
if (Platform.isIOS) {
|
||||
PushService.instance.init().then((_) {
|
||||
FirebaseMessaging.onBackgroundMessage(
|
||||
_firebaseMessagingBackgroundHandler);
|
||||
_firebaseMessagingBackgroundHandler,
|
||||
);
|
||||
});
|
||||
}
|
||||
FeatureFlagService.instance.init();
|
||||
|
@ -160,22 +166,25 @@ Future<void> _sync(String caller) async {
|
|||
}
|
||||
|
||||
Future _runWithLogs(Function() function, {String prefix = ""}) async {
|
||||
await SuperLogging.main(LogConfig(
|
||||
body: function,
|
||||
logDirPath: (await getTemporaryDirectory()).path + "/logs",
|
||||
maxLogFiles: 5,
|
||||
sentryDsn: kDebugMode ? kSentryDebugDSN : kSentryDSN,
|
||||
tunnel: kSentryTunnel,
|
||||
enableInDebugMode: true,
|
||||
prefix: prefix,
|
||||
));
|
||||
await SuperLogging.main(
|
||||
LogConfig(
|
||||
body: function,
|
||||
logDirPath: (await getTemporaryDirectory()).path + "/logs",
|
||||
maxLogFiles: 5,
|
||||
sentryDsn: kDebugMode ? kSentryDebugDSN : kSentryDSN,
|
||||
tunnel: kSentryTunnel,
|
||||
enableInDebugMode: true,
|
||||
prefix: prefix,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _scheduleHeartBeat(bool isBackground) async {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
await prefs.setInt(
|
||||
isBackground ? kLastBGTaskHeartBeatTime : kLastFGTaskHeartBeatTime,
|
||||
DateTime.now().microsecondsSinceEpoch);
|
||||
isBackground ? kLastBGTaskHeartBeatTime : kLastFGTaskHeartBeatTime,
|
||||
DateTime.now().microsecondsSinceEpoch,
|
||||
);
|
||||
Future.delayed(kHeartBeatFrequency, () async {
|
||||
_scheduleHeartBeat(isBackground);
|
||||
});
|
||||
|
@ -209,7 +218,9 @@ Future<bool> _isRunningInForeground() async {
|
|||
|
||||
Future<void> _killBGTask([String taskId]) async {
|
||||
await UploadLocksDB.instance.releaseLocksAcquiredByOwnerBefore(
|
||||
ProcessType.background.toString(), DateTime.now().microsecondsSinceEpoch);
|
||||
ProcessType.background.toString(),
|
||||
DateTime.now().microsecondsSinceEpoch,
|
||||
);
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
prefs.remove(kLastBGTaskHeartBeatTime);
|
||||
if (taskId != null) {
|
||||
|
@ -222,22 +233,26 @@ Future<void> _firebaseMessagingBackgroundHandler(RemoteMessage message) async {
|
|||
bool isInForeground = AppLifecycleService.instance.isForeground;
|
||||
if (_isProcessRunning) {
|
||||
_logger.info(
|
||||
"Background push received when app is alive and runningInFS: $isRunningInFG inForeground: $isInForeground");
|
||||
"Background push received when app is alive and runningInFS: $isRunningInFG inForeground: $isInForeground",
|
||||
);
|
||||
if (PushService.shouldSync(message)) {
|
||||
await _sync('firebaseBgSyncActiveProcess');
|
||||
}
|
||||
} else {
|
||||
// App is dead
|
||||
_runWithLogs(() async {
|
||||
_logger.info("Background push received");
|
||||
if (Platform.isIOS) {
|
||||
_scheduleSuicide(kBGPushTimeout); // To prevent OS from punishing us
|
||||
}
|
||||
await _init(true, via: 'firebasePush');
|
||||
if (PushService.shouldSync(message)) {
|
||||
await _sync('firebaseBgSyncNoActiveProcess');
|
||||
}
|
||||
}, prefix: "[fbg]");
|
||||
_runWithLogs(
|
||||
() async {
|
||||
_logger.info("Background push received");
|
||||
if (Platform.isIOS) {
|
||||
_scheduleSuicide(kBGPushTimeout); // To prevent OS from punishing us
|
||||
}
|
||||
await _init(true, via: 'firebasePush');
|
||||
if (PushService.shouldSync(message)) {
|
||||
await _sync('firebaseBgSyncNoActiveProcess');
|
||||
}
|
||||
},
|
||||
prefix: "[fbg]",
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -33,7 +33,8 @@ class BillingPlans {
|
|||
|
||||
return BillingPlans(
|
||||
plans: List<BillingPlan>.from(
|
||||
map['plans']?.map((x) => BillingPlan.fromMap(x))),
|
||||
map['plans']?.map((x) => BillingPlan.fromMap(x)),
|
||||
),
|
||||
freePlan: FreePlan.fromMap(map['freePlan']),
|
||||
);
|
||||
}
|
||||
|
|
|
@ -68,22 +68,23 @@ class Collection {
|
|||
}
|
||||
}
|
||||
|
||||
Collection copyWith(
|
||||
{int id,
|
||||
User owner,
|
||||
String encryptedKey,
|
||||
String keyDecryptionNonce,
|
||||
String name,
|
||||
String encryptedName,
|
||||
String nameDecryptionNonce,
|
||||
CollectionType type,
|
||||
CollectionAttributes attributes,
|
||||
List<User> sharees,
|
||||
List<PublicURL> publicURLs,
|
||||
int updationTime,
|
||||
bool isDeleted,
|
||||
String mMdEncodedJson,
|
||||
int mMdVersion}) {
|
||||
Collection copyWith({
|
||||
int id,
|
||||
User owner,
|
||||
String encryptedKey,
|
||||
String keyDecryptionNonce,
|
||||
String name,
|
||||
String encryptedName,
|
||||
String nameDecryptionNonce,
|
||||
CollectionType type,
|
||||
CollectionAttributes attributes,
|
||||
List<User> sharees,
|
||||
List<PublicURL> publicURLs,
|
||||
int updationTime,
|
||||
bool isDeleted,
|
||||
String mMdEncodedJson,
|
||||
int mMdVersion,
|
||||
}) {
|
||||
Collection result = Collection(
|
||||
id ?? this.id,
|
||||
owner ?? this.owner,
|
||||
|
@ -131,7 +132,8 @@ class Collection {
|
|||
(map['publicURLs'] == null || map['publicURLs'].length == 0)
|
||||
? <PublicURL>[]
|
||||
: List<PublicURL>.from(
|
||||
map['publicURLs'].map((x) => PublicURL.fromMap(x)));
|
||||
map['publicURLs'].map((x) => PublicURL.fromMap(x)),
|
||||
);
|
||||
return Collection(
|
||||
map['id'],
|
||||
User.fromMap(map['owner']),
|
||||
|
@ -338,12 +340,13 @@ class PublicURL {
|
|||
bool enableDownload = true;
|
||||
bool passwordEnabled = false;
|
||||
|
||||
PublicURL(
|
||||
{this.url,
|
||||
this.deviceLimit,
|
||||
this.validTill,
|
||||
this.enableDownload,
|
||||
this.passwordEnabled});
|
||||
PublicURL({
|
||||
this.url,
|
||||
this.deviceLimit,
|
||||
this.validTill,
|
||||
this.enableDownload,
|
||||
this.passwordEnabled,
|
||||
});
|
||||
|
||||
Map<String, dynamic> toMap() {
|
||||
return {
|
||||
|
|
|
@ -9,7 +9,8 @@ class DuplicateFilesResponse {
|
|||
factory DuplicateFilesResponse.fromMap(Map<String, dynamic> map) {
|
||||
return DuplicateFilesResponse(
|
||||
List<DuplicateItems>.from(
|
||||
map['duplicates']?.map((x) => DuplicateItems.fromMap(x))),
|
||||
map['duplicates']?.map((x) => DuplicateItems.fromMap(x)),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
@ -71,11 +71,12 @@ class File {
|
|||
if (file.creationTime == 0) {
|
||||
try {
|
||||
final parsedDateTime = DateTime.parse(
|
||||
basenameWithoutExtension(file.title)
|
||||
.replaceAll("IMG_", "")
|
||||
.replaceAll("VID_", "")
|
||||
.replaceAll("DCIM_", "")
|
||||
.replaceAll("_", " "));
|
||||
basenameWithoutExtension(file.title)
|
||||
.replaceAll("IMG_", "")
|
||||
.replaceAll("VID_", "")
|
||||
.replaceAll("DCIM_", "")
|
||||
.replaceAll("_", " "),
|
||||
);
|
||||
file.creationTime = parsedDateTime.microsecondsSinceEpoch;
|
||||
} catch (e) {
|
||||
file.creationTime = asset.modifiedDateTime.microsecondsSinceEpoch;
|
||||
|
|
|
@ -22,7 +22,11 @@ class IgnoredFile {
|
|||
return null;
|
||||
}
|
||||
|
||||
return IgnoredFile(trashFile.localID, trashFile.title,
|
||||
trashFile.deviceFolder, kIgnoreReasonTrash);
|
||||
return IgnoredFile(
|
||||
trashFile.localID,
|
||||
trashFile.title,
|
||||
trashFile.deviceFolder,
|
||||
kIgnoreReasonTrash,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -74,7 +74,8 @@ class CollectionMagicMetadata {
|
|||
factory CollectionMagicMetadata.fromEncodedJson(String encodedJson) =>
|
||||
CollectionMagicMetadata.fromJson(jsonDecode(encodedJson));
|
||||
|
||||
factory CollectionMagicMetadata.fromJson(dynamic json) => CollectionMagicMetadata.fromMap(json);
|
||||
factory CollectionMagicMetadata.fromJson(dynamic json) =>
|
||||
CollectionMagicMetadata.fromMap(json);
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
final map = <String, dynamic>{};
|
||||
|
|
|
@ -135,7 +135,8 @@ class Attributes {
|
|||
|
||||
Attributes({
|
||||
this.isCancelled,
|
||||
this.customerID});
|
||||
this.customerID,
|
||||
});
|
||||
|
||||
Attributes.fromJson(dynamic json) {
|
||||
isCancelled = json["isCancelled"];
|
||||
|
@ -153,5 +154,4 @@ class Attributes {
|
|||
String toString() {
|
||||
return 'Attributes{isCancelled: $isCancelled, customerID: $customerID}';
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
import 'package:photos/models/file.dart';
|
||||
|
||||
class TrashFile extends File {
|
||||
|
||||
// time when file was put in the trash for first time
|
||||
int createdAt;
|
||||
|
||||
|
|
|
@ -0,0 +1 @@
|
|||
|
|
@ -40,10 +40,11 @@ class UserDetails {
|
|||
|
||||
int getFreeStorage() {
|
||||
return max(
|
||||
isPartOfFamily()
|
||||
? (familyData.storage - familyData.getTotalUsage())
|
||||
: (subscription.storage - (usage)),
|
||||
0);
|
||||
isPartOfFamily()
|
||||
? (familyData.storage - familyData.getTotalUsage())
|
||||
: (subscription.storage - (usage)),
|
||||
0,
|
||||
);
|
||||
}
|
||||
|
||||
int getTotalStorage() {
|
||||
|
@ -118,7 +119,8 @@ class FamilyData {
|
|||
}
|
||||
assert(map['members'] != null && map['members'].length >= 0);
|
||||
final members = List<FamilyMember>.from(
|
||||
map['members'].map((x) => FamilyMember.fromMap(x)));
|
||||
map['members'].map((x) => FamilyMember.fromMap(x)),
|
||||
);
|
||||
return FamilyData(
|
||||
members,
|
||||
map['storage'] as int,
|
||||
|
|
|
@ -11,11 +11,15 @@ import 'package:photos/models/billing_plan.dart';
|
|||
import 'package:photos/models/subscription.dart';
|
||||
|
||||
const kWebPaymentRedirectUrl = "https://payments.ente.io/frameRedirect";
|
||||
const kWebPaymentBaseEndpoint = String.fromEnvironment("web-payment",
|
||||
defaultValue: "https://payments.ente.io");
|
||||
const kWebPaymentBaseEndpoint = String.fromEnvironment(
|
||||
"web-payment",
|
||||
defaultValue: "https://payments.ente.io",
|
||||
);
|
||||
|
||||
const kFamilyPlanManagementUrl = String.fromEnvironment("web-family",
|
||||
defaultValue: "https://family.ente.io");
|
||||
const kFamilyPlanManagementUrl = String.fromEnvironment(
|
||||
"web-family",
|
||||
defaultValue: "https://family.ente.io",
|
||||
);
|
||||
|
||||
class BillingService {
|
||||
BillingService._privateConstructor();
|
||||
|
@ -42,9 +46,10 @@ class BillingService {
|
|||
}
|
||||
for (final purchase in purchases) {
|
||||
if (purchase.status == PurchaseStatus.purchased) {
|
||||
verifySubscription(purchase.productID,
|
||||
purchase.verificationData.serverVerificationData)
|
||||
.then((response) {
|
||||
verifySubscription(
|
||||
purchase.productID,
|
||||
purchase.verificationData.serverVerificationData,
|
||||
).then((response) {
|
||||
if (response != null) {
|
||||
InAppPurchaseConnection.instance.completePurchase(purchase);
|
||||
}
|
||||
|
@ -166,8 +171,9 @@ class BillingService {
|
|||
}
|
||||
}
|
||||
|
||||
Future<String> getStripeCustomerPortalUrl(
|
||||
{String endpoint = kWebPaymentRedirectUrl}) async {
|
||||
Future<String> getStripeCustomerPortalUrl({
|
||||
String endpoint = kWebPaymentRedirectUrl,
|
||||
}) async {
|
||||
try {
|
||||
final response = await _dio.get(
|
||||
_config.getHttpEndpoint() + "/billing/stripe/customer-portal",
|
||||
|
|
|
@ -192,7 +192,9 @@ class CollectionsService {
|
|||
|
||||
Future<void> share(int collectionID, String email, String publicKey) async {
|
||||
final encryptedKey = CryptoUtil.sealSync(
|
||||
getCollectionKey(collectionID), Sodium.base642bin(publicKey));
|
||||
getCollectionKey(collectionID),
|
||||
Sodium.base642bin(publicKey),
|
||||
);
|
||||
try {
|
||||
await _dio.post(
|
||||
Configuration.instance.getHttpEndpoint() + "/collections/share",
|
||||
|
@ -202,7 +204,8 @@ class CollectionsService {
|
|||
"encryptedKey": Sodium.bin2base64(encryptedKey),
|
||||
},
|
||||
options: Options(
|
||||
headers: {"X-Auth-Token": Configuration.instance.getToken()}),
|
||||
headers: {"X-Auth-Token": Configuration.instance.getToken()},
|
||||
),
|
||||
);
|
||||
} on DioError catch (e) {
|
||||
if (e.response.statusCode == 402) {
|
||||
|
@ -222,7 +225,8 @@ class CollectionsService {
|
|||
"email": email,
|
||||
},
|
||||
options: Options(
|
||||
headers: {"X-Auth-Token": Configuration.instance.getToken()}),
|
||||
headers: {"X-Auth-Token": Configuration.instance.getToken()},
|
||||
),
|
||||
);
|
||||
_collectionIDToCollections[collectionID]
|
||||
.sharees
|
||||
|
@ -253,20 +257,26 @@ class CollectionsService {
|
|||
Uint8List _getDecryptedKey(Collection collection) {
|
||||
final encryptedKey = Sodium.base642bin(collection.encryptedKey);
|
||||
if (collection.owner.id == _config.getUserID()) {
|
||||
return CryptoUtil.decryptSync(encryptedKey, _config.getKey(),
|
||||
Sodium.base642bin(collection.keyDecryptionNonce));
|
||||
return CryptoUtil.decryptSync(
|
||||
encryptedKey,
|
||||
_config.getKey(),
|
||||
Sodium.base642bin(collection.keyDecryptionNonce),
|
||||
);
|
||||
} else {
|
||||
return CryptoUtil.openSealSync(
|
||||
encryptedKey,
|
||||
Sodium.base642bin(_config.getKeyAttributes().publicKey),
|
||||
_config.getSecretKey());
|
||||
encryptedKey,
|
||||
Sodium.base642bin(_config.getKeyAttributes().publicKey),
|
||||
_config.getSecretKey(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> rename(Collection collection, String newName) async {
|
||||
try {
|
||||
final encryptedName = CryptoUtil.encryptSync(
|
||||
utf8.encode(newName), getCollectionKey(collection.id));
|
||||
utf8.encode(newName),
|
||||
getCollectionKey(collection.id),
|
||||
);
|
||||
await _dio.post(
|
||||
Configuration.instance.getHttpEndpoint() + "/collections/rename",
|
||||
data: {
|
||||
|
@ -275,7 +285,8 @@ class CollectionsService {
|
|||
"nameDecryptionNonce": Sodium.bin2base64(encryptedName.nonce)
|
||||
},
|
||||
options: Options(
|
||||
headers: {"X-Auth-Token": Configuration.instance.getToken()}),
|
||||
headers: {"X-Auth-Token": Configuration.instance.getToken()},
|
||||
),
|
||||
);
|
||||
// trigger sync to fetch the latest name from server
|
||||
sync();
|
||||
|
@ -286,7 +297,9 @@ class CollectionsService {
|
|||
}
|
||||
|
||||
Future<void> updateMagicMetadata(
|
||||
Collection collection, Map<String, dynamic> newMetadataUpdate) async {
|
||||
Collection collection,
|
||||
Map<String, dynamic> newMetadataUpdate,
|
||||
) async {
|
||||
final int ownerID = Configuration.instance.getUserID();
|
||||
try {
|
||||
if (collection.owner.id != ownerID) {
|
||||
|
@ -307,7 +320,9 @@ class CollectionsService {
|
|||
|
||||
final key = getCollectionKey(collection.id);
|
||||
final encryptedMMd = await CryptoUtil.encryptChaCha(
|
||||
utf8.encode(jsonEncode(jsonToUpdate)), key);
|
||||
utf8.encode(jsonEncode(jsonToUpdate)),
|
||||
key,
|
||||
);
|
||||
// for required field, the json validator on golang doesn't treat 0 as valid
|
||||
// value. Instead of changing version to ptr, decided to start version with 1.
|
||||
int currentVersion = max(collection.mMdVersion, 1);
|
||||
|
@ -325,7 +340,8 @@ class CollectionsService {
|
|||
"/collections/magic-metadata",
|
||||
data: params,
|
||||
options: Options(
|
||||
headers: {"X-Auth-Token": Configuration.instance.getToken()}),
|
||||
headers: {"X-Auth-Token": Configuration.instance.getToken()},
|
||||
),
|
||||
);
|
||||
collection.mMdVersion = currentVersion + 1;
|
||||
_cacheCollectionAttributes(collection);
|
||||
|
@ -351,7 +367,8 @@ class CollectionsService {
|
|||
"collectionID": collection.id,
|
||||
},
|
||||
options: Options(
|
||||
headers: {"X-Auth-Token": Configuration.instance.getToken()}),
|
||||
headers: {"X-Auth-Token": Configuration.instance.getToken()},
|
||||
),
|
||||
);
|
||||
collection.publicURLs?.add(PublicURL.fromMap(response.data["result"]));
|
||||
await _db.insert(List.from([collection]));
|
||||
|
@ -369,14 +386,17 @@ class CollectionsService {
|
|||
}
|
||||
|
||||
Future<void> updateShareUrl(
|
||||
Collection collection, Map<String, dynamic> prop) async {
|
||||
Collection collection,
|
||||
Map<String, dynamic> prop,
|
||||
) async {
|
||||
prop.putIfAbsent('collectionID', () => collection.id);
|
||||
try {
|
||||
final response = await _dio.put(
|
||||
Configuration.instance.getHttpEndpoint() + "/collections/share-url",
|
||||
data: json.encode(prop),
|
||||
options: Options(
|
||||
headers: {"X-Auth-Token": Configuration.instance.getToken()}),
|
||||
headers: {"X-Auth-Token": Configuration.instance.getToken()},
|
||||
),
|
||||
);
|
||||
// remove existing url information
|
||||
collection.publicURLs?.clear();
|
||||
|
@ -426,7 +446,8 @@ class CollectionsService {
|
|||
"source": AppLifecycleService.instance.isForeground ? "fg" : "bg",
|
||||
},
|
||||
options: Options(
|
||||
headers: {"X-Auth-Token": Configuration.instance.getToken()}),
|
||||
headers: {"X-Auth-Token": Configuration.instance.getToken()},
|
||||
),
|
||||
);
|
||||
final List<Collection> collections = [];
|
||||
if (response != null) {
|
||||
|
@ -436,13 +457,15 @@ class CollectionsService {
|
|||
if (collectionData['magicMetadata'] != null) {
|
||||
final decryptionKey = _getDecryptedKey(collection);
|
||||
final utfEncodedMmd = await CryptoUtil.decryptChaCha(
|
||||
Sodium.base642bin(collectionData['magicMetadata']['data']),
|
||||
decryptionKey,
|
||||
Sodium.base642bin(collectionData['magicMetadata']['header']));
|
||||
Sodium.base642bin(collectionData['magicMetadata']['data']),
|
||||
decryptionKey,
|
||||
Sodium.base642bin(collectionData['magicMetadata']['header']),
|
||||
);
|
||||
collection.mMdEncodedJson = utf8.decode(utfEncodedMmd);
|
||||
collection.mMdVersion = collectionData['magicMetadata']['version'];
|
||||
collection.magicMetadata = CollectionMagicMetadata.fromEncodedJson(
|
||||
collection.mMdEncodedJson);
|
||||
collection.mMdEncodedJson,
|
||||
);
|
||||
}
|
||||
collections.add(collection);
|
||||
}
|
||||
|
@ -464,20 +487,22 @@ class CollectionsService {
|
|||
final key = CryptoUtil.generateKey();
|
||||
final encryptedKeyData = CryptoUtil.encryptSync(key, _config.getKey());
|
||||
final encryptedName = CryptoUtil.encryptSync(utf8.encode(albumName), key);
|
||||
final collection = await createAndCacheCollection(Collection(
|
||||
null,
|
||||
null,
|
||||
Sodium.bin2base64(encryptedKeyData.encryptedData),
|
||||
Sodium.bin2base64(encryptedKeyData.nonce),
|
||||
null,
|
||||
Sodium.bin2base64(encryptedName.encryptedData),
|
||||
Sodium.bin2base64(encryptedName.nonce),
|
||||
CollectionType.album,
|
||||
CollectionAttributes(),
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
));
|
||||
final collection = await createAndCacheCollection(
|
||||
Collection(
|
||||
null,
|
||||
null,
|
||||
Sodium.bin2base64(encryptedKeyData.encryptedData),
|
||||
Sodium.bin2base64(encryptedKeyData.nonce),
|
||||
null,
|
||||
Sodium.bin2base64(encryptedName.encryptedData),
|
||||
Sodium.bin2base64(encryptedName.nonce),
|
||||
CollectionType.album,
|
||||
CollectionAttributes(),
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
),
|
||||
);
|
||||
return collection;
|
||||
}
|
||||
|
||||
|
@ -487,7 +512,8 @@ class CollectionsService {
|
|||
final response = await _dio.get(
|
||||
Configuration.instance.getHttpEndpoint() + "/collections/$collectionID",
|
||||
options: Options(
|
||||
headers: {"X-Auth-Token": Configuration.instance.getToken()}),
|
||||
headers: {"X-Auth-Token": Configuration.instance.getToken()},
|
||||
),
|
||||
);
|
||||
assert(response != null && response.data != null);
|
||||
final collectionData = response.data["collection"];
|
||||
|
@ -495,9 +521,10 @@ class CollectionsService {
|
|||
if (collectionData['magicMetadata'] != null) {
|
||||
final decryptionKey = _getDecryptedKey(collection);
|
||||
final utfEncodedMmd = await CryptoUtil.decryptChaCha(
|
||||
Sodium.base642bin(collectionData['magicMetadata']['data']),
|
||||
decryptionKey,
|
||||
Sodium.base642bin(collectionData['magicMetadata']['header']));
|
||||
Sodium.base642bin(collectionData['magicMetadata']['data']),
|
||||
decryptionKey,
|
||||
Sodium.base642bin(collectionData['magicMetadata']['header']),
|
||||
);
|
||||
collection.mMdEncodedJson = utf8.decode(utfEncodedMmd);
|
||||
collection.mMdVersion = collectionData['magicMetadata']['version'];
|
||||
collection.magicMetadata =
|
||||
|
@ -523,38 +550,43 @@ class CollectionsService {
|
|||
final key = CryptoUtil.generateKey();
|
||||
final encryptedKeyData = CryptoUtil.encryptSync(key, _config.getKey());
|
||||
final encryptedPath = CryptoUtil.encryptSync(utf8.encode(path), key);
|
||||
final collection = await createAndCacheCollection(Collection(
|
||||
null,
|
||||
null,
|
||||
Sodium.bin2base64(encryptedKeyData.encryptedData),
|
||||
Sodium.bin2base64(encryptedKeyData.nonce),
|
||||
null,
|
||||
Sodium.bin2base64(encryptedPath.encryptedData),
|
||||
Sodium.bin2base64(encryptedPath.nonce),
|
||||
CollectionType.folder,
|
||||
CollectionAttributes(
|
||||
encryptedPath: Sodium.bin2base64(encryptedPath.encryptedData),
|
||||
pathDecryptionNonce: Sodium.bin2base64(encryptedPath.nonce),
|
||||
version: 1,
|
||||
final collection = await createAndCacheCollection(
|
||||
Collection(
|
||||
null,
|
||||
null,
|
||||
Sodium.bin2base64(encryptedKeyData.encryptedData),
|
||||
Sodium.bin2base64(encryptedKeyData.nonce),
|
||||
null,
|
||||
Sodium.bin2base64(encryptedPath.encryptedData),
|
||||
Sodium.bin2base64(encryptedPath.nonce),
|
||||
CollectionType.folder,
|
||||
CollectionAttributes(
|
||||
encryptedPath: Sodium.bin2base64(encryptedPath.encryptedData),
|
||||
pathDecryptionNonce: Sodium.bin2base64(encryptedPath.nonce),
|
||||
version: 1,
|
||||
),
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
),
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
));
|
||||
);
|
||||
return collection;
|
||||
}
|
||||
|
||||
Future<void> addToCollection(int collectionID, List<File> files) async {
|
||||
final containsUploadedFile = files.firstWhere(
|
||||
(element) => element.uploadedFileID != null,
|
||||
orElse: () => null) !=
|
||||
(element) => element.uploadedFileID != null,
|
||||
orElse: () => null,
|
||||
) !=
|
||||
null;
|
||||
if (containsUploadedFile) {
|
||||
final existingFileIDsInCollection =
|
||||
await FilesDB.instance.getUploadedFileIDs(collectionID);
|
||||
files.removeWhere((element) =>
|
||||
element.uploadedFileID != null &&
|
||||
existingFileIDsInCollection.contains(element.uploadedFileID));
|
||||
files.removeWhere(
|
||||
(element) =>
|
||||
element.uploadedFileID != null &&
|
||||
existingFileIDsInCollection.contains(element.uploadedFileID),
|
||||
);
|
||||
}
|
||||
if (files.isEmpty || !containsUploadedFile) {
|
||||
_logger.info("nothing to add to the collection");
|
||||
|
@ -574,9 +606,13 @@ class CollectionsService {
|
|||
if (params["files"] == null) {
|
||||
params["files"] = [];
|
||||
}
|
||||
params["files"].add(CollectionFileItem(
|
||||
file.uploadedFileID, file.encryptedKey, file.keyDecryptionNonce)
|
||||
.toMap());
|
||||
params["files"].add(
|
||||
CollectionFileItem(
|
||||
file.uploadedFileID,
|
||||
file.encryptedKey,
|
||||
file.keyDecryptionNonce,
|
||||
).toMap(),
|
||||
);
|
||||
}
|
||||
|
||||
try {
|
||||
|
@ -584,7 +620,8 @@ class CollectionsService {
|
|||
Configuration.instance.getHttpEndpoint() + "/collections/add-files",
|
||||
data: params,
|
||||
options: Options(
|
||||
headers: {"X-Auth-Token": Configuration.instance.getToken()}),
|
||||
headers: {"X-Auth-Token": Configuration.instance.getToken()},
|
||||
),
|
||||
);
|
||||
await _filesDB.insertMultiple(files);
|
||||
Bus.instance.fire(CollectionUpdatedEvent(collectionID, files));
|
||||
|
@ -605,16 +642,21 @@ class CollectionsService {
|
|||
final encryptedKeyData = CryptoUtil.encryptSync(key, toCollectionKey);
|
||||
file.encryptedKey = Sodium.bin2base64(encryptedKeyData.encryptedData);
|
||||
file.keyDecryptionNonce = Sodium.bin2base64(encryptedKeyData.nonce);
|
||||
params["files"].add(CollectionFileItem(
|
||||
file.uploadedFileID, file.encryptedKey, file.keyDecryptionNonce)
|
||||
.toMap());
|
||||
params["files"].add(
|
||||
CollectionFileItem(
|
||||
file.uploadedFileID,
|
||||
file.encryptedKey,
|
||||
file.keyDecryptionNonce,
|
||||
).toMap(),
|
||||
);
|
||||
}
|
||||
try {
|
||||
await _dio.post(
|
||||
Configuration.instance.getHttpEndpoint() + "/collections/restore-files",
|
||||
data: params,
|
||||
options: Options(
|
||||
headers: {"X-Auth-Token": Configuration.instance.getToken()}),
|
||||
headers: {"X-Auth-Token": Configuration.instance.getToken()},
|
||||
),
|
||||
);
|
||||
await _filesDB.insertMultiple(files);
|
||||
await TrashDB.instance
|
||||
|
@ -641,7 +683,10 @@ class CollectionsService {
|
|||
}
|
||||
|
||||
Future<void> move(
|
||||
int toCollectionID, int fromCollectionID, List<File> files) async {
|
||||
int toCollectionID,
|
||||
int fromCollectionID,
|
||||
List<File> files,
|
||||
) async {
|
||||
_validateMoveRequest(toCollectionID, fromCollectionID, files);
|
||||
files.removeWhere((element) => element.uploadedFileID == null);
|
||||
if (files.isEmpty) {
|
||||
|
@ -660,9 +705,13 @@ class CollectionsService {
|
|||
CryptoUtil.encryptSync(fileKey, getCollectionKey(toCollectionID));
|
||||
file.encryptedKey = Sodium.bin2base64(encryptedKeyData.encryptedData);
|
||||
file.keyDecryptionNonce = Sodium.bin2base64(encryptedKeyData.nonce);
|
||||
params["files"].add(CollectionFileItem(
|
||||
file.uploadedFileID, file.encryptedKey, file.keyDecryptionNonce)
|
||||
.toMap());
|
||||
params["files"].add(
|
||||
CollectionFileItem(
|
||||
file.uploadedFileID,
|
||||
file.encryptedKey,
|
||||
file.keyDecryptionNonce,
|
||||
).toMap(),
|
||||
);
|
||||
}
|
||||
await _dio.post(
|
||||
Configuration.instance.getHttpEndpoint() + "/collections/move-files",
|
||||
|
@ -673,20 +722,31 @@ class CollectionsService {
|
|||
|
||||
// remove files from old collection
|
||||
await _filesDB.removeFromCollection(
|
||||
fromCollectionID, files.map((e) => e.uploadedFileID).toList());
|
||||
Bus.instance.fire(CollectionUpdatedEvent(fromCollectionID, files,
|
||||
type: EventType.deletedFromRemote));
|
||||
fromCollectionID,
|
||||
files.map((e) => e.uploadedFileID).toList(),
|
||||
);
|
||||
Bus.instance.fire(
|
||||
CollectionUpdatedEvent(
|
||||
fromCollectionID,
|
||||
files,
|
||||
type: EventType.deletedFromRemote,
|
||||
),
|
||||
);
|
||||
// insert new files in the toCollection which are not part of the toCollection
|
||||
final existingUploadedIDs =
|
||||
await FilesDB.instance.getUploadedFileIDs(toCollectionID);
|
||||
files.removeWhere(
|
||||
(element) => existingUploadedIDs.contains(element.uploadedFileID));
|
||||
(element) => existingUploadedIDs.contains(element.uploadedFileID),
|
||||
);
|
||||
await _filesDB.insertMultiple(files);
|
||||
Bus.instance.fire(CollectionUpdatedEvent(toCollectionID, files));
|
||||
}
|
||||
|
||||
void _validateMoveRequest(
|
||||
int toCollectionID, int fromCollectionID, List<File> files) {
|
||||
int toCollectionID,
|
||||
int fromCollectionID,
|
||||
List<File> files,
|
||||
) {
|
||||
if (toCollectionID == fromCollectionID) {
|
||||
throw AssertionError("can't move to same album");
|
||||
}
|
||||
|
@ -754,10 +814,13 @@ class CollectionsService {
|
|||
final key = collection.attributes.version == 1
|
||||
? _getDecryptedKey(collection)
|
||||
: _config.getKey();
|
||||
return utf8.decode(CryptoUtil.decryptSync(
|
||||
return utf8.decode(
|
||||
CryptoUtil.decryptSync(
|
||||
Sodium.base642bin(collection.attributes.encryptedPath),
|
||||
key,
|
||||
Sodium.base642bin(collection.attributes.pathDecryptionNonce)));
|
||||
Sodium.base642bin(collection.attributes.pathDecryptionNonce),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
bool hasSyncedCollections() {
|
||||
|
@ -770,13 +833,17 @@ class CollectionsService {
|
|||
String name;
|
||||
try {
|
||||
final result = CryptoUtil.decryptSync(
|
||||
Sodium.base642bin(collection.encryptedName),
|
||||
_getDecryptedKey(collection),
|
||||
Sodium.base642bin(collection.nameDecryptionNonce));
|
||||
Sodium.base642bin(collection.encryptedName),
|
||||
_getDecryptedKey(collection),
|
||||
Sodium.base642bin(collection.nameDecryptionNonce),
|
||||
);
|
||||
name = utf8.decode(result);
|
||||
} catch (e, s) {
|
||||
_logger.severe(
|
||||
"failed to decrypt collection name: ${collection.id}", e, s);
|
||||
"failed to decrypt collection name: ${collection.id}",
|
||||
e,
|
||||
s,
|
||||
);
|
||||
name = "Unknown Album";
|
||||
}
|
||||
return collection.copyWith(name: name);
|
||||
|
@ -830,7 +897,8 @@ class AddFilesRequest {
|
|||
return AddFilesRequest(
|
||||
map['collectionID'],
|
||||
List<CollectionFileItem>.from(
|
||||
map['files']?.map((x) => CollectionFileItem.fromMap(x))),
|
||||
map['files']?.map((x) => CollectionFileItem.fromMap(x)),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
@ -52,11 +52,14 @@ class DeduplicationService {
|
|||
}
|
||||
if (missingFileIDs.isNotEmpty) {
|
||||
_logger.severe(
|
||||
"Missing files",
|
||||
InvalidStateError("Could not find " +
|
||||
"Missing files",
|
||||
InvalidStateError(
|
||||
"Could not find " +
|
||||
missingFileIDs.length.toString() +
|
||||
" files in local DB: " +
|
||||
missingFileIDs.toString()));
|
||||
missingFileIDs.toString(),
|
||||
),
|
||||
);
|
||||
}
|
||||
return result;
|
||||
} catch (e) {
|
||||
|
|
|
@ -37,7 +37,9 @@ class FavoritesService {
|
|||
return false;
|
||||
}
|
||||
return _filesDB.doesFileExistInCollection(
|
||||
file.uploadedFileID, collection.id);
|
||||
file.uploadedFileID,
|
||||
collection.id,
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> addToFavorites(File file) async {
|
||||
|
@ -84,21 +86,22 @@ class FavoritesService {
|
|||
final key = CryptoUtil.generateKey();
|
||||
final encryptedKeyData = CryptoUtil.encryptSync(key, _config.getKey());
|
||||
final encryptedName = CryptoUtil.encryptSync(utf8.encode("Favorites"), key);
|
||||
final collection =
|
||||
await _collectionsService.createAndCacheCollection(Collection(
|
||||
null,
|
||||
null,
|
||||
Sodium.bin2base64(encryptedKeyData.encryptedData),
|
||||
Sodium.bin2base64(encryptedKeyData.nonce),
|
||||
null,
|
||||
Sodium.bin2base64(encryptedName.encryptedData),
|
||||
Sodium.bin2base64(encryptedName.nonce),
|
||||
CollectionType.favorites,
|
||||
CollectionAttributes(),
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
));
|
||||
final collection = await _collectionsService.createAndCacheCollection(
|
||||
Collection(
|
||||
null,
|
||||
null,
|
||||
Sodium.bin2base64(encryptedKeyData.encryptedData),
|
||||
Sodium.bin2base64(encryptedKeyData.nonce),
|
||||
null,
|
||||
Sodium.bin2base64(encryptedName.encryptedData),
|
||||
Sodium.bin2base64(encryptedName.nonce),
|
||||
CollectionType.favorites,
|
||||
CollectionAttributes(),
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
),
|
||||
);
|
||||
_cachedFavoritesCollectionID = collection.id;
|
||||
return collection.id;
|
||||
}
|
||||
|
|
|
@ -101,21 +101,23 @@ class FeatureFlagService {
|
|||
|
||||
class FeatureFlags {
|
||||
static FeatureFlags defaultFlags = FeatureFlags(
|
||||
disableCFWorker: FFDefault.disableCFWorker,
|
||||
disableUrlSharing: FFDefault.disableUrlSharing,
|
||||
enableStripe: FFDefault.enableStripe,
|
||||
enableMissingLocationMigration: FFDefault.enableMissingLocationMigration);
|
||||
disableCFWorker: FFDefault.disableCFWorker,
|
||||
disableUrlSharing: FFDefault.disableUrlSharing,
|
||||
enableStripe: FFDefault.enableStripe,
|
||||
enableMissingLocationMigration: FFDefault.enableMissingLocationMigration,
|
||||
);
|
||||
|
||||
final bool disableCFWorker;
|
||||
final bool disableUrlSharing;
|
||||
final bool enableStripe;
|
||||
final bool enableMissingLocationMigration;
|
||||
|
||||
FeatureFlags(
|
||||
{@required this.disableCFWorker,
|
||||
@required this.disableUrlSharing,
|
||||
@required this.enableStripe,
|
||||
@required this.enableMissingLocationMigration});
|
||||
FeatureFlags({
|
||||
@required this.disableCFWorker,
|
||||
@required this.disableUrlSharing,
|
||||
@required this.enableStripe,
|
||||
@required this.enableMissingLocationMigration,
|
||||
});
|
||||
|
||||
Map<String, dynamic> toMap() {
|
||||
return {
|
||||
|
|
|
@ -44,7 +44,9 @@ class FileMagicService {
|
|||
}
|
||||
|
||||
Future<void> updatePublicMagicMetadata(
|
||||
List<File> files, Map<String, dynamic> newMetadataUpdate) async {
|
||||
List<File> files,
|
||||
Map<String, dynamic> newMetadataUpdate,
|
||||
) async {
|
||||
final params = <String, dynamic>{};
|
||||
params['metadataList'] = [];
|
||||
final int ownerID = Configuration.instance.getUserID();
|
||||
|
@ -52,7 +54,8 @@ class FileMagicService {
|
|||
for (final file in files) {
|
||||
if (file.uploadedFileID == null) {
|
||||
throw AssertionError(
|
||||
"operation is only supported on backed up files");
|
||||
"operation is only supported on backed up files",
|
||||
);
|
||||
} else if (file.ownerID != ownerID) {
|
||||
throw AssertionError("cannot modify memories not owned by you");
|
||||
}
|
||||
|
@ -70,15 +73,20 @@ class FileMagicService {
|
|||
|
||||
final fileKey = decryptFileKey(file);
|
||||
final encryptedMMd = await CryptoUtil.encryptChaCha(
|
||||
utf8.encode(jsonEncode(jsonToUpdate)), fileKey);
|
||||
params['metadataList'].add(UpdateMagicMetadataRequest(
|
||||
utf8.encode(jsonEncode(jsonToUpdate)),
|
||||
fileKey,
|
||||
);
|
||||
params['metadataList'].add(
|
||||
UpdateMagicMetadataRequest(
|
||||
id: file.uploadedFileID,
|
||||
magicMetadata: MetadataRequest(
|
||||
version: file.pubMmdVersion,
|
||||
count: jsonToUpdate.length,
|
||||
data: Sodium.bin2base64(encryptedMMd.encryptedData),
|
||||
header: Sodium.bin2base64(encryptedMMd.header),
|
||||
)));
|
||||
),
|
||||
),
|
||||
);
|
||||
file.pubMmdVersion = file.pubMmdVersion + 1;
|
||||
}
|
||||
|
||||
|
@ -87,7 +95,8 @@ class FileMagicService {
|
|||
"/files/public-magic-metadata",
|
||||
data: params,
|
||||
options: Options(
|
||||
headers: {"X-Auth-Token": Configuration.instance.getToken()}),
|
||||
headers: {"X-Auth-Token": Configuration.instance.getToken()},
|
||||
),
|
||||
);
|
||||
// update the state of the selected file. Same file in other collection
|
||||
// should be eventually synced after remote sync has completed
|
||||
|
@ -105,7 +114,9 @@ class FileMagicService {
|
|||
}
|
||||
|
||||
Future<void> _updateMagicData(
|
||||
List<File> files, Map<String, dynamic> newMetadataUpdate) async {
|
||||
List<File> files,
|
||||
Map<String, dynamic> newMetadataUpdate,
|
||||
) async {
|
||||
final params = <String, dynamic>{};
|
||||
params['metadataList'] = [];
|
||||
final int ownerID = Configuration.instance.getUserID();
|
||||
|
@ -113,7 +124,8 @@ class FileMagicService {
|
|||
for (final file in files) {
|
||||
if (file.uploadedFileID == null) {
|
||||
throw AssertionError(
|
||||
"operation is only supported on backed up files");
|
||||
"operation is only supported on backed up files",
|
||||
);
|
||||
} else if (file.ownerID != ownerID) {
|
||||
throw AssertionError("cannot modify memories not owned by you");
|
||||
}
|
||||
|
@ -131,15 +143,20 @@ class FileMagicService {
|
|||
|
||||
final fileKey = decryptFileKey(file);
|
||||
final encryptedMMd = await CryptoUtil.encryptChaCha(
|
||||
utf8.encode(jsonEncode(jsonToUpdate)), fileKey);
|
||||
params['metadataList'].add(UpdateMagicMetadataRequest(
|
||||
utf8.encode(jsonEncode(jsonToUpdate)),
|
||||
fileKey,
|
||||
);
|
||||
params['metadataList'].add(
|
||||
UpdateMagicMetadataRequest(
|
||||
id: file.uploadedFileID,
|
||||
magicMetadata: MetadataRequest(
|
||||
version: file.mMdVersion,
|
||||
count: jsonToUpdate.length,
|
||||
data: Sodium.bin2base64(encryptedMMd.encryptedData),
|
||||
header: Sodium.bin2base64(encryptedMMd.header),
|
||||
)));
|
||||
),
|
||||
),
|
||||
);
|
||||
file.mMdVersion = file.mMdVersion + 1;
|
||||
}
|
||||
|
||||
|
@ -147,7 +164,8 @@ class FileMagicService {
|
|||
Configuration.instance.getHttpEndpoint() + "/files/magic-metadata",
|
||||
data: params,
|
||||
options: Options(
|
||||
headers: {"X-Auth-Token": Configuration.instance.getToken()}),
|
||||
headers: {"X-Auth-Token": Configuration.instance.getToken()},
|
||||
),
|
||||
);
|
||||
// update the state of the selected file. Same file in other collection
|
||||
// should be eventually synced after remote sync has completed
|
||||
|
@ -173,10 +191,11 @@ class UpdateMagicMetadataRequest {
|
|||
|
||||
factory UpdateMagicMetadataRequest.fromJson(dynamic json) {
|
||||
return UpdateMagicMetadataRequest(
|
||||
id: json['id'],
|
||||
magicMetadata: json['magicMetadata'] != null
|
||||
? MetadataRequest.fromJson(json['magicMetadata'])
|
||||
: null);
|
||||
id: json['id'],
|
||||
magicMetadata: json['magicMetadata'] != null
|
||||
? MetadataRequest.fromJson(json['magicMetadata'])
|
||||
: null,
|
||||
);
|
||||
}
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
|
|
|
@ -81,13 +81,15 @@ class FileMigrationService {
|
|||
final eTime = DateTime.now().microsecondsSinceEpoch;
|
||||
final d = Duration(microseconds: eTime - sTime);
|
||||
_logger.info(
|
||||
'filesWithMissingLocation migration completed in ${d.inSeconds.toString()} seconds');
|
||||
'filesWithMissingLocation migration completed in ${d.inSeconds.toString()} seconds',
|
||||
);
|
||||
}
|
||||
await _markLocationMigrationAsCompleted();
|
||||
}
|
||||
|
||||
Future<void> _checkAndMarkFilesForReUpload(
|
||||
List<String> localIDsToProcess) async {
|
||||
List<String> localIDsToProcess,
|
||||
) async {
|
||||
_logger.info("files to process ${localIDsToProcess.length}");
|
||||
var localIDsWithLocation = <String>[];
|
||||
for (var localID in localIDsToProcess) {
|
||||
|
@ -101,7 +103,8 @@ class FileMigrationService {
|
|||
if ((latLng.longitude ?? 0.0) != 0.0 ||
|
||||
(latLng.longitude ?? 0.0) != 0.0) {
|
||||
_logger.finest(
|
||||
'found lat/long ${latLng.longitude}/${latLng.longitude} for ${assetEntity.title} ${assetEntity.relativePath} with id : $localID');
|
||||
'found lat/long ${latLng.longitude}/${latLng.longitude} for ${assetEntity.title} ${assetEntity.relativePath} with id : $localID',
|
||||
);
|
||||
hasLocation = true;
|
||||
}
|
||||
} catch (e, s) {
|
||||
|
@ -127,7 +130,8 @@ class FileMigrationService {
|
|||
final eTime = DateTime.now().microsecondsSinceEpoch;
|
||||
final d = Duration(microseconds: eTime - sTime);
|
||||
_logger.info(
|
||||
'importing completed, total files count ${fileLocalIDs.length} and took ${d.inSeconds.toString()} seconds');
|
||||
'importing completed, total files count ${fileLocalIDs.length} and took ${d.inSeconds.toString()} seconds',
|
||||
);
|
||||
_prefs.setBool(isLocalImportDone, true);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -25,10 +25,12 @@ class IgnoredFilesService {
|
|||
|
||||
Future<void> cacheAndInsert(List<IgnoredFile> ignoredFiles) async {
|
||||
final existingIDs = await ignoredIDs;
|
||||
existingIDs.addAll(ignoredFiles
|
||||
.map((e) => _idForIgnoredFile(e))
|
||||
.where((id) => id != null)
|
||||
.toSet());
|
||||
existingIDs.addAll(
|
||||
ignoredFiles
|
||||
.map((e) => _idForIgnoredFile(e))
|
||||
.where((id) => id != null)
|
||||
.toSet(),
|
||||
);
|
||||
return _db.insertMultiple(ignoredFiles);
|
||||
}
|
||||
|
||||
|
@ -56,7 +58,10 @@ class IgnoredFilesService {
|
|||
|
||||
String _idForIgnoredFile(IgnoredFile ignoredFile) {
|
||||
return _getIgnoreID(
|
||||
ignoredFile.localID, ignoredFile.deviceFolder, ignoredFile.title);
|
||||
ignoredFile.localID,
|
||||
ignoredFile.deviceFolder,
|
||||
ignoredFile.title,
|
||||
);
|
||||
}
|
||||
|
||||
// _computeIgnoreID will return null if don't have sufficient information
|
||||
|
|
|
@ -55,8 +55,10 @@ class LocalSyncService {
|
|||
if (Platform.isAndroid && AppLifecycleService.instance.isForeground) {
|
||||
final permissionState = await PhotoManager.requestPermissionExtend();
|
||||
if (permissionState != PermissionState.authorized) {
|
||||
_logger.severe("sync requested with invalid permission",
|
||||
permissionState.toString());
|
||||
_logger.severe(
|
||||
"sync requested with invalid permission",
|
||||
permissionState.toString(),
|
||||
);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
@ -67,7 +69,8 @@ class LocalSyncService {
|
|||
_existingSync = Completer<void>();
|
||||
final existingLocalFileIDs = await _db.getExistingLocalFileIDs();
|
||||
_logger.info(
|
||||
existingLocalFileIDs.length.toString() + " localIDs were discovered");
|
||||
existingLocalFileIDs.length.toString() + " localIDs were discovered",
|
||||
);
|
||||
final editedFileIDs = getEditedFileIDs().toSet();
|
||||
final downloadedFileIDs = getDownloadedFileIDs().toSet();
|
||||
final syncStartTime = DateTime.now().microsecondsSinceEpoch;
|
||||
|
@ -127,11 +130,13 @@ class LocalSyncService {
|
|||
final localAssets = await getAllLocalAssets();
|
||||
final eTime = DateTime.now().microsecondsSinceEpoch;
|
||||
final d = Duration(microseconds: eTime - sTime);
|
||||
_logger.info("Loading from the beginning returned " +
|
||||
localAssets.length.toString() +
|
||||
" assets and took " +
|
||||
d.inMilliseconds.toString() +
|
||||
"ms");
|
||||
_logger.info(
|
||||
"Loading from the beginning returned " +
|
||||
localAssets.length.toString() +
|
||||
" assets and took " +
|
||||
d.inMilliseconds.toString() +
|
||||
"ms",
|
||||
);
|
||||
final existingIDs = await _db.getExistingLocalFileIDs();
|
||||
final invalidIDs = getInvalidFileIDs().toSet();
|
||||
final unsyncedFiles =
|
||||
|
@ -139,7 +144,8 @@ class LocalSyncService {
|
|||
if (unsyncedFiles.isNotEmpty) {
|
||||
await _db.insertMultiple(unsyncedFiles);
|
||||
_logger.info(
|
||||
"Inserted " + unsyncedFiles.length.toString() + " unsynced files.");
|
||||
"Inserted " + unsyncedFiles.length.toString() + " unsynced files.",
|
||||
);
|
||||
_updatePathsToBackup(unsyncedFiles);
|
||||
Bus.instance.fire(LocalPhotosUpdatedEvent(unsyncedFiles));
|
||||
return true;
|
||||
|
@ -218,10 +224,12 @@ class LocalSyncService {
|
|||
Set<String> editedFileIDs,
|
||||
Set<String> downloadedFileIDs,
|
||||
) async {
|
||||
_logger.info("Loading photos from " +
|
||||
DateTime.fromMicrosecondsSinceEpoch(fromTime).toString() +
|
||||
" to " +
|
||||
DateTime.fromMicrosecondsSinceEpoch(toTime).toString());
|
||||
_logger.info(
|
||||
"Loading photos from " +
|
||||
DateTime.fromMicrosecondsSinceEpoch(fromTime).toString() +
|
||||
" to " +
|
||||
DateTime.fromMicrosecondsSinceEpoch(toTime).toString(),
|
||||
);
|
||||
final files = await getDeviceFiles(fromTime, toTime, _computer);
|
||||
if (files.isNotEmpty) {
|
||||
_logger.info("Fetched " + files.length.toString() + " files.");
|
||||
|
@ -233,7 +241,8 @@ class LocalSyncService {
|
|||
.removeWhere((file) => downloadedFileIDs.contains(file.localID));
|
||||
if (updatedFiles.isNotEmpty) {
|
||||
_logger.info(
|
||||
updatedFiles.length.toString() + " local files were updated.");
|
||||
updatedFiles.length.toString() + " local files were updated.",
|
||||
);
|
||||
}
|
||||
for (final file in updatedFiles) {
|
||||
await _db.updateUploadedFile(
|
||||
|
|
|
@ -30,7 +30,8 @@ class MemoriesService extends ChangeNotifier {
|
|||
// Intention of delay is to give more CPU cycles to other tasks
|
||||
Future.delayed(const Duration(seconds: 5), () {
|
||||
_memoriesDB.clearMemoriesSeenBeforeTime(
|
||||
DateTime.now().microsecondsSinceEpoch - (7 * kMicroSecondsInDay));
|
||||
DateTime.now().microsecondsSinceEpoch - (7 * kMicroSecondsInDay),
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -53,10 +54,13 @@ class MemoriesService extends ChangeNotifier {
|
|||
Future<List<Memory>> _fetchMemories() async {
|
||||
_logger.info("Fetching memories");
|
||||
final presentTime = DateTime.now();
|
||||
final present = presentTime.subtract(Duration(
|
||||
final present = presentTime.subtract(
|
||||
Duration(
|
||||
hours: presentTime.hour,
|
||||
minutes: presentTime.minute,
|
||||
seconds: presentTime.second));
|
||||
seconds: presentTime.second,
|
||||
),
|
||||
);
|
||||
final List<List<int>> durations = [];
|
||||
for (var yearAgo = 1; yearAgo <= yearsBefore; yearAgo++) {
|
||||
final date = _getDate(present, yearAgo);
|
||||
|
@ -94,7 +98,9 @@ class MemoriesService extends ChangeNotifier {
|
|||
Future markMemoryAsSeen(Memory memory) async {
|
||||
memory.markSeen();
|
||||
await _memoriesDB.markMemoryAsSeen(
|
||||
memory, DateTime.now().microsecondsSinceEpoch);
|
||||
memory,
|
||||
DateTime.now().microsecondsSinceEpoch,
|
||||
);
|
||||
notifyListeners();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -20,8 +20,10 @@ class NotificationService {
|
|||
InitializationSettings(
|
||||
android: initializationSettingsAndroid,
|
||||
);
|
||||
await _flutterLocalNotificationsPlugin.initialize(initializationSettings,
|
||||
onSelectNotification: selectNotification);
|
||||
await _flutterLocalNotificationsPlugin.initialize(
|
||||
initializationSettings,
|
||||
onSelectNotification: selectNotification,
|
||||
);
|
||||
}
|
||||
|
||||
Future selectNotification(String payload) async {}
|
||||
|
@ -42,6 +44,10 @@ class NotificationService {
|
|||
const NotificationDetails platformChannelSpecifics =
|
||||
NotificationDetails(android: androidPlatformChannelSpecifics);
|
||||
await _flutterLocalNotificationsPlugin.show(
|
||||
0, title, message, platformChannelSpecifics);
|
||||
0,
|
||||
title,
|
||||
message,
|
||||
platformChannelSpecifics,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -61,7 +61,9 @@ class PushService {
|
|||
await _setPushTokenOnServer(fcmToken, apnsToken);
|
||||
await _prefs.setString(kFCMPushToken, fcmToken);
|
||||
await _prefs.setInt(
|
||||
kLastFCMTokenUpdationTime, DateTime.now().microsecondsSinceEpoch);
|
||||
kLastFCMTokenUpdationTime,
|
||||
DateTime.now().microsecondsSinceEpoch,
|
||||
);
|
||||
_logger.info("Push token updated on server");
|
||||
} catch (e) {
|
||||
_logger.severe("Could not set push token", e, StackTrace.current);
|
||||
|
@ -88,7 +90,8 @@ class PushService {
|
|||
_logger.info("Message data: ${message.data}");
|
||||
if (message.notification != null) {
|
||||
_logger.info(
|
||||
"Message also contained a notification: ${message.notification}");
|
||||
"Message also contained a notification: ${message.notification}",
|
||||
);
|
||||
}
|
||||
if (shouldSync(message)) {
|
||||
SyncService.instance.sync();
|
||||
|
|
|
@ -147,7 +147,9 @@ class RemoteSyncService {
|
|||
}
|
||||
for (final c in updatedCollections) {
|
||||
await _syncCollectionDiff(
|
||||
c.id, _collectionsService.getCollectionSyncTime(c.id));
|
||||
c.id,
|
||||
_collectionsService.getCollectionSyncTime(c.id),
|
||||
);
|
||||
await _collectionsService.setCollectionSyncTime(c.id, c.updationTime);
|
||||
}
|
||||
}
|
||||
|
@ -156,8 +158,10 @@ class RemoteSyncService {
|
|||
_logger.info('re-sync collections sinceTime: $sinceTime');
|
||||
final collections = _collectionsService.getActiveCollections();
|
||||
for (final c in collections) {
|
||||
await _syncCollectionDiff(c.id,
|
||||
min(_collectionsService.getCollectionSyncTime(c.id), sinceTime));
|
||||
await _syncCollectionDiff(
|
||||
c.id,
|
||||
min(_collectionsService.getCollectionSyncTime(c.id), sinceTime),
|
||||
);
|
||||
await _collectionsService.setCollectionSyncTime(c.id, c.updationTime);
|
||||
}
|
||||
}
|
||||
|
@ -170,17 +174,28 @@ class RemoteSyncService {
|
|||
final deletedFiles =
|
||||
(await FilesDB.instance.getFilesFromIDs(fileIDs)).values.toList();
|
||||
await FilesDB.instance.deleteFilesFromCollection(collectionID, fileIDs);
|
||||
Bus.instance.fire(CollectionUpdatedEvent(collectionID, deletedFiles,
|
||||
type: EventType.deletedFromRemote));
|
||||
Bus.instance.fire(LocalPhotosUpdatedEvent(deletedFiles,
|
||||
type: EventType.deletedFromRemote));
|
||||
Bus.instance.fire(
|
||||
CollectionUpdatedEvent(
|
||||
collectionID,
|
||||
deletedFiles,
|
||||
type: EventType.deletedFromRemote,
|
||||
),
|
||||
);
|
||||
Bus.instance.fire(
|
||||
LocalPhotosUpdatedEvent(
|
||||
deletedFiles,
|
||||
type: EventType.deletedFromRemote,
|
||||
),
|
||||
);
|
||||
}
|
||||
if (diff.updatedFiles.isNotEmpty) {
|
||||
await _storeDiff(diff.updatedFiles, collectionID);
|
||||
_logger.info("Updated " +
|
||||
diff.updatedFiles.length.toString() +
|
||||
" files in collection " +
|
||||
collectionID.toString());
|
||||
_logger.info(
|
||||
"Updated " +
|
||||
diff.updatedFiles.length.toString() +
|
||||
" files in collection " +
|
||||
collectionID.toString(),
|
||||
);
|
||||
Bus.instance.fire(LocalPhotosUpdatedEvent(diff.updatedFiles));
|
||||
Bus.instance
|
||||
.fire(CollectionUpdatedEvent(collectionID, diff.updatedFiles));
|
||||
|
@ -188,11 +203,15 @@ class RemoteSyncService {
|
|||
|
||||
if (diff.latestUpdatedAtTime > 0) {
|
||||
await _collectionsService.setCollectionSyncTime(
|
||||
collectionID, diff.latestUpdatedAtTime);
|
||||
collectionID,
|
||||
diff.latestUpdatedAtTime,
|
||||
);
|
||||
}
|
||||
if (diff.hasMore) {
|
||||
return await _syncCollectionDiff(collectionID,
|
||||
_collectionsService.getCollectionSyncTime(collectionID));
|
||||
return await _syncCollectionDiff(
|
||||
collectionID,
|
||||
_collectionsService.getCollectionSyncTime(collectionID),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -213,11 +232,15 @@ class RemoteSyncService {
|
|||
if (filesToBeUploaded.isNotEmpty) {
|
||||
final int prevCount = filesToBeUploaded.length;
|
||||
final ignoredIDs = await IgnoredFilesService.instance.ignoredIDs;
|
||||
filesToBeUploaded.removeWhere((file) =>
|
||||
IgnoredFilesService.instance.shouldSkipUpload(ignoredIDs, file));
|
||||
filesToBeUploaded.removeWhere(
|
||||
(file) =>
|
||||
IgnoredFilesService.instance.shouldSkipUpload(ignoredIDs, file),
|
||||
);
|
||||
if (prevCount != filesToBeUploaded.length) {
|
||||
_logger.info((prevCount - filesToBeUploaded.length).toString() +
|
||||
" files were ignored for upload");
|
||||
_logger.info(
|
||||
(prevCount - filesToBeUploaded.length).toString() +
|
||||
" files were ignored for upload",
|
||||
);
|
||||
}
|
||||
}
|
||||
if (filesToBeUploaded.isEmpty) {
|
||||
|
@ -227,7 +250,8 @@ class RemoteSyncService {
|
|||
}
|
||||
_sortByTimeAndType(filesToBeUploaded);
|
||||
_logger.info(
|
||||
filesToBeUploaded.length.toString() + " new files to be uploaded.");
|
||||
filesToBeUploaded.length.toString() + " new files to be uploaded.",
|
||||
);
|
||||
return filesToBeUploaded;
|
||||
}
|
||||
|
||||
|
@ -320,13 +344,21 @@ class RemoteSyncService {
|
|||
_completedUploads < 0 ||
|
||||
toBeUploadedInThisSession < 0) {
|
||||
_logger.info(
|
||||
"Incorrect sync status",
|
||||
InvalidSyncStatusError("Tried to report $_completedUploads as "
|
||||
"uploaded out of $toBeUploadedInThisSession"));
|
||||
"Incorrect sync status",
|
||||
InvalidSyncStatusError(
|
||||
"Tried to report $_completedUploads as "
|
||||
"uploaded out of $toBeUploadedInThisSession",
|
||||
),
|
||||
);
|
||||
return;
|
||||
}
|
||||
Bus.instance.fire(SyncStatusUpdate(SyncStatus.in_progress,
|
||||
completed: _completedUploads, total: toBeUploadedInThisSession));
|
||||
Bus.instance.fire(
|
||||
SyncStatusUpdate(
|
||||
SyncStatus.in_progress,
|
||||
completed: _completedUploads,
|
||||
total: toBeUploadedInThisSession,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Future _storeDiff(List<File> diff, int collectionID) async {
|
||||
|
@ -362,17 +394,21 @@ class RemoteSyncService {
|
|||
// case when localID for a file changes and the file is uploaded again in
|
||||
// the same collection
|
||||
final fileWithLocalID = existingFiles.firstWhere(
|
||||
(e) =>
|
||||
file.localID != null &&
|
||||
e.localID != null &&
|
||||
e.localID == file.localID,
|
||||
orElse: () => existingFiles.firstWhere((e) => e.localID != null,
|
||||
orElse: () => null));
|
||||
(e) =>
|
||||
file.localID != null &&
|
||||
e.localID != null &&
|
||||
e.localID == file.localID,
|
||||
orElse: () => existingFiles.firstWhere(
|
||||
(e) => e.localID != null,
|
||||
orElse: () => null,
|
||||
),
|
||||
);
|
||||
if (fileWithLocalID != null) {
|
||||
// File should ideally have the same localID
|
||||
if (file.localID != null && file.localID != fileWithLocalID.localID) {
|
||||
_logger.severe(
|
||||
"unexpected mismatch in localIDs remote: ${file.toString()} and existing: ${fileWithLocalID.toString()}");
|
||||
"unexpected mismatch in localIDs remote: ${file.toString()} and existing: ${fileWithLocalID.toString()}",
|
||||
);
|
||||
}
|
||||
file.localID = fileWithLocalID.localID;
|
||||
} else {
|
||||
|
@ -384,8 +420,10 @@ class RemoteSyncService {
|
|||
file.generatedID = existingFiles[0].generatedID;
|
||||
if (file.modificationTime != existingFiles[0].modificationTime) {
|
||||
// File was updated since the app was uninstalled
|
||||
_logger.info("Updated since last installation: " +
|
||||
file.uploadedFileID.toString());
|
||||
_logger.info(
|
||||
"Updated since last installation: " +
|
||||
file.uploadedFileID.toString(),
|
||||
);
|
||||
file.modificationTime = existingFiles[0].modificationTime;
|
||||
file.updationTime = null;
|
||||
updated++;
|
||||
|
|
|
@ -91,18 +91,28 @@ class SyncService {
|
|||
} on WiFiUnavailableError {
|
||||
_logger.warning("Not uploading over mobile data");
|
||||
Bus.instance.fire(
|
||||
SyncStatusUpdate(SyncStatus.paused, reason: "waiting for WiFi..."));
|
||||
SyncStatusUpdate(SyncStatus.paused, reason: "waiting for WiFi..."),
|
||||
);
|
||||
} on SyncStopRequestedError {
|
||||
_syncStopRequested = false;
|
||||
Bus.instance.fire(
|
||||
SyncStatusUpdate(SyncStatus.completed_backup, wasStopped: true));
|
||||
SyncStatusUpdate(SyncStatus.completed_backup, wasStopped: true),
|
||||
);
|
||||
} on NoActiveSubscriptionError {
|
||||
Bus.instance.fire(SyncStatusUpdate(SyncStatus.error,
|
||||
error: NoActiveSubscriptionError()));
|
||||
Bus.instance.fire(
|
||||
SyncStatusUpdate(
|
||||
SyncStatus.error,
|
||||
error: NoActiveSubscriptionError(),
|
||||
),
|
||||
);
|
||||
} on StorageLimitExceededError {
|
||||
_showStorageLimitExceededNotification();
|
||||
Bus.instance.fire(SyncStatusUpdate(SyncStatus.error,
|
||||
error: StorageLimitExceededError()));
|
||||
Bus.instance.fire(
|
||||
SyncStatusUpdate(
|
||||
SyncStatus.error,
|
||||
error: StorageLimitExceededError(),
|
||||
),
|
||||
);
|
||||
} on UnauthorizedError {
|
||||
_logger.info("Logging user out");
|
||||
Bus.instance.fire(TriggerLogoutEvent());
|
||||
|
@ -112,8 +122,12 @@ class SyncService {
|
|||
e.type == DioErrorType.sendTimeout ||
|
||||
e.type == DioErrorType.receiveTimeout ||
|
||||
e.type == DioErrorType.other) {
|
||||
Bus.instance.fire(SyncStatusUpdate(SyncStatus.paused,
|
||||
reason: "waiting for network..."));
|
||||
Bus.instance.fire(
|
||||
SyncStatusUpdate(
|
||||
SyncStatus.paused,
|
||||
reason: "waiting for network...",
|
||||
),
|
||||
);
|
||||
_logger.severe("unable to connect", e, StackTrace.current);
|
||||
return false;
|
||||
}
|
||||
|
@ -155,15 +169,21 @@ class SyncService {
|
|||
}
|
||||
|
||||
void onFoldersSet(Set<String> paths) {
|
||||
_uploader.removeFromQueueWhere((file) {
|
||||
return !paths.contains(file.deviceFolder);
|
||||
}, UserCancelledUploadError());
|
||||
_uploader.removeFromQueueWhere(
|
||||
(file) {
|
||||
return !paths.contains(file.deviceFolder);
|
||||
},
|
||||
UserCancelledUploadError(),
|
||||
);
|
||||
}
|
||||
|
||||
void onVideoBackupPaused() {
|
||||
_uploader.removeFromQueueWhere((file) {
|
||||
return file.fileType == FileType.video;
|
||||
}, UserCancelledUploadError());
|
||||
_uploader.removeFromQueueWhere(
|
||||
(file) {
|
||||
return file.fileType == FileType.video;
|
||||
},
|
||||
UserCancelledUploadError(),
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> deleteFilesOnServer(List<int> fileIDs) async {
|
||||
|
@ -224,7 +244,9 @@ class SyncService {
|
|||
if ((now - lastNotificationShownTime) > kMicroSecondsInDay) {
|
||||
await _prefs.setInt(kLastStorageLimitExceededNotificationPushTime, now);
|
||||
NotificationService.instance.showNotification(
|
||||
"storage limit exceeded", "sorry, we had to pause your backups");
|
||||
"storage limit exceeded",
|
||||
"sorry, we had to pause your backups",
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -113,7 +113,8 @@ class TrashSyncService {
|
|||
}
|
||||
|
||||
Future<Response<dynamic>> _trashFiles(
|
||||
Map<String, dynamic> requestData) async {
|
||||
Map<String, dynamic> requestData,
|
||||
) async {
|
||||
return _dio.post(
|
||||
Configuration.instance.getHttpEndpoint() + "/files/trash",
|
||||
options: Options(
|
||||
|
|
|
@ -69,7 +69,9 @@ class UpdateService {
|
|||
hasBeen3DaysSinceLastNotification &&
|
||||
_latestVersion.shouldNotify) {
|
||||
NotificationService.instance.showNotification(
|
||||
"update available", "click to install our best version yet");
|
||||
"update available",
|
||||
"click to install our best version yet",
|
||||
);
|
||||
await _prefs.setInt(kUpdateAvailableShownTimeKey, now);
|
||||
} else {
|
||||
_logger.info("Debouncing notification");
|
||||
|
|
|
@ -59,9 +59,11 @@ class UserService {
|
|||
Navigator.of(context).push(
|
||||
MaterialPageRoute(
|
||||
builder: (BuildContext context) {
|
||||
return OTTVerificationPage(email,
|
||||
isChangeEmail: isChangeEmail,
|
||||
isCreateAccountScreen: isCreateAccountScreen);
|
||||
return OTTVerificationPage(
|
||||
email,
|
||||
isChangeEmail: isChangeEmail,
|
||||
isCreateAccountScreen: isCreateAccountScreen,
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
|
@ -143,15 +145,17 @@ class UserService {
|
|||
|
||||
Future<void> terminateSession(String token) async {
|
||||
try {
|
||||
await _dio.delete(_config.getHttpEndpoint() + "/users/session",
|
||||
options: Options(
|
||||
headers: {
|
||||
"X-Auth-Token": _config.getToken(),
|
||||
},
|
||||
),
|
||||
queryParameters: {
|
||||
"token": token,
|
||||
});
|
||||
await _dio.delete(
|
||||
_config.getHttpEndpoint() + "/users/session",
|
||||
options: Options(
|
||||
headers: {
|
||||
"X-Auth-Token": _config.getToken(),
|
||||
},
|
||||
),
|
||||
queryParameters: {
|
||||
"token": token,
|
||||
},
|
||||
);
|
||||
} on DioError catch (e) {
|
||||
_logger.info(e);
|
||||
rethrow;
|
||||
|
@ -160,12 +164,14 @@ class UserService {
|
|||
|
||||
Future<void> leaveFamilyPlan() async {
|
||||
try {
|
||||
await _dio.delete(_config.getHttpEndpoint() + "/family/leave",
|
||||
options: Options(
|
||||
headers: {
|
||||
"X-Auth-Token": _config.getToken(),
|
||||
},
|
||||
));
|
||||
await _dio.delete(
|
||||
_config.getHttpEndpoint() + "/family/leave",
|
||||
options: Options(
|
||||
headers: {
|
||||
"X-Auth-Token": _config.getToken(),
|
||||
},
|
||||
),
|
||||
);
|
||||
} on DioError catch (e) {
|
||||
_logger.warning('failed to leave family plan', e);
|
||||
rethrow;
|
||||
|
@ -176,13 +182,14 @@ class UserService {
|
|||
final dialog = createProgressDialog(context, "Logging out...");
|
||||
await dialog.show();
|
||||
try {
|
||||
final response =
|
||||
await _dio.post(_config.getHttpEndpoint() + "/users/logout",
|
||||
options: Options(
|
||||
headers: {
|
||||
"X-Auth-Token": _config.getToken(),
|
||||
},
|
||||
));
|
||||
final response = await _dio.post(
|
||||
_config.getHttpEndpoint() + "/users/logout",
|
||||
options: Options(
|
||||
headers: {
|
||||
"X-Auth-Token": _config.getToken(),
|
||||
},
|
||||
),
|
||||
);
|
||||
if (response != null && response.statusCode == 200) {
|
||||
await Configuration.instance.logout();
|
||||
await dialog.hide();
|
||||
|
@ -240,11 +247,17 @@ class UserService {
|
|||
await dialog.hide();
|
||||
if (e.response != null && e.response.statusCode == 410) {
|
||||
await showErrorDialog(
|
||||
context, "Oops", "Your verification code has expired");
|
||||
context,
|
||||
"Oops",
|
||||
"Your verification code has expired",
|
||||
);
|
||||
Navigator.of(context).pop();
|
||||
} else {
|
||||
showErrorDialog(context, "Incorrect code",
|
||||
"Sorry, the code you've entered is incorrect");
|
||||
showErrorDialog(
|
||||
context,
|
||||
"Incorrect code",
|
||||
"Sorry, the code you've entered is incorrect",
|
||||
);
|
||||
}
|
||||
} catch (e) {
|
||||
await dialog.hide();
|
||||
|
@ -287,8 +300,11 @@ class UserService {
|
|||
if (e.response != null && e.response.statusCode == 403) {
|
||||
showErrorDialog(context, "Oops", "This email is already in use");
|
||||
} else {
|
||||
showErrorDialog(context, "Incorrect code",
|
||||
"Authentication failed, please try again");
|
||||
showErrorDialog(
|
||||
context,
|
||||
"Incorrect code",
|
||||
"Authentication failed, please try again",
|
||||
);
|
||||
}
|
||||
} catch (e) {
|
||||
await dialog.hide();
|
||||
|
@ -371,7 +387,10 @@ class UserService {
|
|||
}
|
||||
|
||||
Future<void> verifyTwoFactor(
|
||||
BuildContext context, String sessionID, String code) async {
|
||||
BuildContext context,
|
||||
String sessionID,
|
||||
String code,
|
||||
) async {
|
||||
final dialog = createProgressDialog(context, "Authenticating...");
|
||||
await dialog.show();
|
||||
try {
|
||||
|
@ -409,14 +428,20 @@ class UserService {
|
|||
(route) => route.isFirst,
|
||||
);
|
||||
} else {
|
||||
showErrorDialog(context, "Incorrect code",
|
||||
"Authentication failed, please try again");
|
||||
showErrorDialog(
|
||||
context,
|
||||
"Incorrect code",
|
||||
"Authentication failed, please try again",
|
||||
);
|
||||
}
|
||||
} catch (e) {
|
||||
await dialog.hide();
|
||||
_logger.severe(e);
|
||||
showErrorDialog(
|
||||
context, "Oops", "Authentication failed, please try again");
|
||||
context,
|
||||
"Oops",
|
||||
"Authentication failed, please try again",
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -435,9 +460,10 @@ class UserService {
|
|||
MaterialPageRoute(
|
||||
builder: (BuildContext context) {
|
||||
return TwoFactorRecoveryPage(
|
||||
sessionID,
|
||||
response.data["encryptedSecret"],
|
||||
response.data["secretDecryptionNonce"]);
|
||||
sessionID,
|
||||
response.data["encryptedSecret"],
|
||||
response.data["secretDecryptionNonce"],
|
||||
);
|
||||
},
|
||||
),
|
||||
(route) => route.isFirst,
|
||||
|
@ -457,12 +483,18 @@ class UserService {
|
|||
);
|
||||
} else {
|
||||
showErrorDialog(
|
||||
context, "Oops", "Something went wrong, please try again");
|
||||
context,
|
||||
"Oops",
|
||||
"Something went wrong, please try again",
|
||||
);
|
||||
}
|
||||
} catch (e) {
|
||||
_logger.severe(e);
|
||||
showErrorDialog(
|
||||
context, "Oops", "Something went wrong, please try again");
|
||||
context,
|
||||
"Oops",
|
||||
"Something went wrong, please try again",
|
||||
);
|
||||
} finally {
|
||||
await dialog.hide();
|
||||
}
|
||||
|
@ -479,14 +511,20 @@ class UserService {
|
|||
await dialog.show();
|
||||
String secret;
|
||||
try {
|
||||
secret = Sodium.bin2base64(await CryptoUtil.decrypt(
|
||||
secret = Sodium.bin2base64(
|
||||
await CryptoUtil.decrypt(
|
||||
Sodium.base642bin(encryptedSecret),
|
||||
Sodium.hex2bin(recoveryKey.trim()),
|
||||
Sodium.base642bin(secretDecryptionNonce)));
|
||||
Sodium.base642bin(secretDecryptionNonce),
|
||||
),
|
||||
);
|
||||
} catch (e) {
|
||||
await dialog.hide();
|
||||
showErrorDialog(context, "Incorrect recovery key",
|
||||
"The recovery key you entered is incorrect");
|
||||
showErrorDialog(
|
||||
context,
|
||||
"Incorrect recovery key",
|
||||
"The recovery key you entered is incorrect",
|
||||
);
|
||||
return;
|
||||
}
|
||||
try {
|
||||
|
@ -523,12 +561,18 @@ class UserService {
|
|||
);
|
||||
} else {
|
||||
showErrorDialog(
|
||||
context, "Oops", "Something went wrong, please try again");
|
||||
context,
|
||||
"Oops",
|
||||
"Something went wrong, please try again",
|
||||
);
|
||||
}
|
||||
} catch (e) {
|
||||
_logger.severe(e);
|
||||
showErrorDialog(
|
||||
context, "Oops", "Something went wrong, please try again");
|
||||
context,
|
||||
"Oops",
|
||||
"Something went wrong, please try again",
|
||||
);
|
||||
} finally {
|
||||
await dialog.hide();
|
||||
}
|
||||
|
@ -548,9 +592,12 @@ class UserService {
|
|||
);
|
||||
await dialog.hide();
|
||||
routeToPage(
|
||||
context,
|
||||
TwoFactorSetupPage(
|
||||
response.data["secretCode"], response.data["qrCode"]));
|
||||
context,
|
||||
TwoFactorSetupPage(
|
||||
response.data["secretCode"],
|
||||
response.data["qrCode"],
|
||||
),
|
||||
);
|
||||
} catch (e, s) {
|
||||
await dialog.hide();
|
||||
_logger.severe(e, s);
|
||||
|
@ -559,7 +606,10 @@ class UserService {
|
|||
}
|
||||
|
||||
Future<bool> enableTwoFactor(
|
||||
BuildContext context, String secret, String code) async {
|
||||
BuildContext context,
|
||||
String secret,
|
||||
String code,
|
||||
) async {
|
||||
Uint8List recoveryKey;
|
||||
try {
|
||||
recoveryKey = await getOrCreateRecoveryKey(context);
|
||||
|
@ -596,13 +646,19 @@ class UserService {
|
|||
_logger.severe(e, s);
|
||||
if (e is DioError) {
|
||||
if (e.response != null && e.response.statusCode == 401) {
|
||||
showErrorDialog(context, "Incorrect code",
|
||||
"Please verify the code you have entered");
|
||||
showErrorDialog(
|
||||
context,
|
||||
"Incorrect code",
|
||||
"Please verify the code you have entered",
|
||||
);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
showErrorDialog(context, "Something went wrong",
|
||||
"Please contact support if the problem persists");
|
||||
showErrorDialog(
|
||||
context,
|
||||
"Something went wrong",
|
||||
"Please contact support if the problem persists",
|
||||
);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
@ -626,8 +682,11 @@ class UserService {
|
|||
} catch (e, s) {
|
||||
await dialog.hide();
|
||||
_logger.severe(e, s);
|
||||
showErrorDialog(context, "Something went wrong",
|
||||
"Please contact support if the problem persists");
|
||||
showErrorDialog(
|
||||
context,
|
||||
"Something went wrong",
|
||||
"Please contact support if the problem persists",
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -716,7 +775,8 @@ class UserService {
|
|||
await Configuration.instance
|
||||
.setEncryptedToken(response.data["encryptedToken"]);
|
||||
await Configuration.instance.setKeyAttributes(
|
||||
KeyAttributes.fromMap(response.data["keyAttributes"]));
|
||||
KeyAttributes.fromMap(response.data["keyAttributes"]),
|
||||
);
|
||||
} else {
|
||||
await Configuration.instance.setToken(response.data["token"]);
|
||||
}
|
||||
|
|
|
@ -106,11 +106,13 @@ class _AppLockState extends State<AppLock> with WidgetsBindingObserver {
|
|||
switch (settings.name) {
|
||||
case '/lock-screen':
|
||||
return PageRouteBuilder(
|
||||
pageBuilder: (_, __, ___) => this._lockScreen);
|
||||
pageBuilder: (_, __, ___) => this._lockScreen,
|
||||
);
|
||||
case '/unlocked':
|
||||
return PageRouteBuilder(
|
||||
pageBuilder: (_, __, ___) =>
|
||||
this.widget.builder(settings.arguments));
|
||||
pageBuilder: (_, __, ___) =>
|
||||
this.widget.builder(settings.arguments),
|
||||
);
|
||||
}
|
||||
return PageRouteBuilder(pageBuilder: (_, __, ___) => this._lockScreen);
|
||||
},
|
||||
|
|
|
@ -19,10 +19,12 @@ class _AppUpdateDialogState extends State<AppUpdateDialog> {
|
|||
Widget build(BuildContext context) {
|
||||
final List<Widget> changelog = [];
|
||||
for (final log in widget.latestVersionInfo.changelog) {
|
||||
changelog.add(Padding(
|
||||
padding: const EdgeInsets.fromLTRB(8, 4, 0, 4),
|
||||
child: Text("- " + log, style: Theme.of(context).textTheme.caption),
|
||||
));
|
||||
changelog.add(
|
||||
Padding(
|
||||
padding: const EdgeInsets.fromLTRB(8, 4, 0, 4),
|
||||
child: Text("- " + log, style: Theme.of(context).textTheme.caption),
|
||||
),
|
||||
);
|
||||
}
|
||||
final content = Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
|
@ -36,10 +38,12 @@ class _AppUpdateDialogState extends State<AppUpdateDialog> {
|
|||
),
|
||||
),
|
||||
Padding(padding: EdgeInsets.all(8)),
|
||||
Text("Changelog",
|
||||
style: TextStyle(
|
||||
fontSize: 18,
|
||||
)),
|
||||
Text(
|
||||
"Changelog",
|
||||
style: TextStyle(
|
||||
fontSize: 18,
|
||||
),
|
||||
),
|
||||
Padding(padding: EdgeInsets.all(4)),
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
|
@ -79,9 +83,9 @@ class _AppUpdateDialogState extends State<AppUpdateDialog> {
|
|||
return WillPopScope(
|
||||
onWillPop: () async => !shouldForceUpdate,
|
||||
child: AlertDialog(
|
||||
title: Text(shouldForceUpdate
|
||||
? "Critical update available"
|
||||
: "Update available"),
|
||||
title: Text(
|
||||
shouldForceUpdate ? "Critical update available" : "Update available",
|
||||
),
|
||||
content: content,
|
||||
),
|
||||
);
|
||||
|
@ -134,12 +138,15 @@ class _ApkDownloaderDialogState extends State<ApkDownloaderDialog> {
|
|||
|
||||
Future<void> _downloadApk() async {
|
||||
try {
|
||||
await Network.instance.getDio().download(widget.versionInfo.url, _saveUrl,
|
||||
onReceiveProgress: (count, _) {
|
||||
setState(() {
|
||||
_downloadProgress = count / widget.versionInfo.size;
|
||||
});
|
||||
});
|
||||
await Network.instance.getDio().download(
|
||||
widget.versionInfo.url,
|
||||
_saveUrl,
|
||||
onReceiveProgress: (count, _) {
|
||||
setState(() {
|
||||
_downloadProgress = count / widget.versionInfo.size;
|
||||
});
|
||||
},
|
||||
);
|
||||
Navigator.of(context, rootNavigator: true).pop('dialog');
|
||||
OpenFile.open(_saveUrl);
|
||||
} catch (e) {
|
||||
|
|
|
@ -16,26 +16,32 @@ class ArchivePage extends StatelessWidget {
|
|||
final GalleryType overlayType;
|
||||
final _selectedFiles = SelectedFiles();
|
||||
|
||||
ArchivePage(
|
||||
{this.tagPrefix = "archived_page",
|
||||
this.appBarType = GalleryType.archive,
|
||||
this.overlayType = GalleryType.archive,
|
||||
Key key})
|
||||
: super(key: key);
|
||||
ArchivePage({
|
||||
this.tagPrefix = "archived_page",
|
||||
this.appBarType = GalleryType.archive,
|
||||
this.overlayType = GalleryType.archive,
|
||||
Key key,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(Object context) {
|
||||
final gallery = Gallery(
|
||||
asyncLoader: (creationStartTime, creationEndTime, {limit, asc}) {
|
||||
return FilesDB.instance.getAllUploadedFiles(creationStartTime,
|
||||
creationEndTime, Configuration.instance.getUserID(),
|
||||
visibility: kVisibilityArchive, limit: limit, asc: asc);
|
||||
return FilesDB.instance.getAllUploadedFiles(
|
||||
creationStartTime,
|
||||
creationEndTime,
|
||||
Configuration.instance.getUserID(),
|
||||
visibility: kVisibilityArchive,
|
||||
limit: limit,
|
||||
asc: asc,
|
||||
);
|
||||
},
|
||||
reloadEvent: Bus.instance.on<FilesUpdatedEvent>().where(
|
||||
(event) =>
|
||||
event.updatedFiles.firstWhere(
|
||||
(element) => element.uploadedFileID != null,
|
||||
orElse: () => null) !=
|
||||
(element) => element.uploadedFileID != null,
|
||||
orElse: () => null,
|
||||
) !=
|
||||
null,
|
||||
),
|
||||
removalEventTypes: const {EventType.unarchived},
|
||||
|
@ -43,8 +49,9 @@ class ArchivePage extends StatelessWidget {
|
|||
Bus.instance.on<FilesUpdatedEvent>().where(
|
||||
(event) =>
|
||||
event.updatedFiles.firstWhere(
|
||||
(element) => element.uploadedFileID != null,
|
||||
orElse: () => null) !=
|
||||
(element) => element.uploadedFileID != null,
|
||||
orElse: () => null,
|
||||
) !=
|
||||
null,
|
||||
),
|
||||
],
|
||||
|
|
|
@ -133,21 +133,28 @@ class _BackupFolderSelectionPageState extends State<BackupFolderSelectionPage> {
|
|||
.compareTo(second.deviceFolder.toLowerCase());
|
||||
});
|
||||
setState(() {});
|
||||
}),
|
||||
},
|
||||
),
|
||||
Expanded(child: _getFolders()),
|
||||
Hero(
|
||||
tag: "select_folders",
|
||||
child: Container(
|
||||
width: double.infinity,
|
||||
decoration: BoxDecoration(boxShadow: [
|
||||
BoxShadow(
|
||||
decoration: BoxDecoration(
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Theme.of(context).backgroundColor,
|
||||
blurRadius: 24,
|
||||
offset: Offset(0, -8),
|
||||
spreadRadius: 4)
|
||||
]),
|
||||
spreadRadius: 4,
|
||||
)
|
||||
],
|
||||
),
|
||||
padding: EdgeInsets.only(
|
||||
left: 20, right: 20, bottom: Platform.isIOS ? 60 : 32),
|
||||
left: 20,
|
||||
right: 20,
|
||||
bottom: Platform.isIOS ? 60 : 32,
|
||||
),
|
||||
child: OutlinedButton(
|
||||
child: Text(widget.buttonText),
|
||||
onPressed: _selectedFolders.isEmpty
|
||||
|
@ -226,24 +233,26 @@ class _BackupFolderSelectionPageState extends State<BackupFolderSelectionPage> {
|
|||
padding: const EdgeInsets.only(bottom: 1, right: 1),
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
border: Border.all(
|
||||
color: Theme.of(context).colorScheme.boxUnSelectColor,
|
||||
),
|
||||
borderRadius: BorderRadius.all(
|
||||
Radius.circular(12),
|
||||
),
|
||||
// color: isSelected
|
||||
// ? Theme.of(context).colorScheme.boxSelectColor
|
||||
// : Theme.of(context).colorScheme.boxUnSelectColor,
|
||||
gradient: isSelected
|
||||
? LinearGradient(colors: const [
|
||||
Color(0xFF00DD4D),
|
||||
Color(0xFF43BA6C)
|
||||
]) //same for both themes
|
||||
: LinearGradient(colors: [
|
||||
border: Border.all(
|
||||
color: Theme.of(context).colorScheme.boxUnSelectColor,
|
||||
),
|
||||
borderRadius: BorderRadius.all(
|
||||
Radius.circular(12),
|
||||
),
|
||||
// color: isSelected
|
||||
// ? Theme.of(context).colorScheme.boxSelectColor
|
||||
// : Theme.of(context).colorScheme.boxUnSelectColor,
|
||||
gradient: isSelected
|
||||
? LinearGradient(
|
||||
colors: const [Color(0xFF00DD4D), Color(0xFF43BA6C)],
|
||||
) //same for both themes
|
||||
: LinearGradient(
|
||||
colors: [
|
||||
Theme.of(context).colorScheme.boxUnSelectColor,
|
||||
Theme.of(context).colorScheme.boxUnSelectColor
|
||||
])),
|
||||
],
|
||||
),
|
||||
),
|
||||
padding: EdgeInsets.fromLTRB(8, 4, 4, 4),
|
||||
child: InkWell(
|
||||
child: Row(
|
||||
|
@ -343,21 +352,25 @@ class _BackupFolderSelectionPageState extends State<BackupFolderSelectionPage> {
|
|||
return ClipRRect(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
child: SizedBox(
|
||||
child: Stack(alignment: AlignmentDirectional.bottomEnd, children: [
|
||||
ThumbnailWidget(
|
||||
file,
|
||||
shouldShowSyncStatus: false,
|
||||
key: Key("backup_selection_widget" + file.tag()),
|
||||
),
|
||||
Padding(
|
||||
child: Stack(
|
||||
alignment: AlignmentDirectional.bottomEnd,
|
||||
children: [
|
||||
ThumbnailWidget(
|
||||
file,
|
||||
shouldShowSyncStatus: false,
|
||||
key: Key("backup_selection_widget" + file.tag()),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(9),
|
||||
child: isSelected
|
||||
? Icon(
|
||||
Icons.local_police,
|
||||
color: Colors.white,
|
||||
)
|
||||
: null),
|
||||
]),
|
||||
: null,
|
||||
),
|
||||
],
|
||||
),
|
||||
height: 88,
|
||||
width: 88,
|
||||
),
|
||||
|
|
|
@ -26,22 +26,26 @@ class BillingQuestionsWidget extends StatelessWidget {
|
|||
builder: (BuildContext context, AsyncSnapshot snapshot) {
|
||||
if (snapshot.hasData) {
|
||||
final faqs = <Widget>[];
|
||||
faqs.add(Padding(
|
||||
padding: const EdgeInsets.all(24),
|
||||
child: Text(
|
||||
"FAQs",
|
||||
style: TextStyle(
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.bold,
|
||||
faqs.add(
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(24),
|
||||
child: Text(
|
||||
"FAQs",
|
||||
style: TextStyle(
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
),
|
||||
));
|
||||
);
|
||||
for (final faq in snapshot.data) {
|
||||
faqs.add(FaqWidget(faq: faq));
|
||||
}
|
||||
faqs.add(Padding(
|
||||
padding: EdgeInsets.all(16),
|
||||
));
|
||||
faqs.add(
|
||||
Padding(
|
||||
padding: EdgeInsets.all(16),
|
||||
),
|
||||
);
|
||||
return SingleChildScrollView(
|
||||
child: Column(
|
||||
children: faqs,
|
||||
|
|
|
@ -64,8 +64,11 @@ class _ChangeEmailDialogState extends State<ChangeEmailDialog> {
|
|||
),
|
||||
onPressed: () {
|
||||
if (!isValidEmail(_email)) {
|
||||
showErrorDialog(context, "Invalid email address",
|
||||
"Please enter a valid email address.");
|
||||
showErrorDialog(
|
||||
context,
|
||||
"Invalid email address",
|
||||
"Please enter a valid email address.",
|
||||
);
|
||||
return;
|
||||
}
|
||||
UserService.instance.getOtt(context, _email, isChangeEmail: true);
|
||||
|
|
|
@ -17,12 +17,13 @@ class CollectionPage extends StatelessWidget {
|
|||
final GalleryType overlayType;
|
||||
final _selectedFiles = SelectedFiles();
|
||||
|
||||
CollectionPage(this.c,
|
||||
{this.tagPrefix = "collection",
|
||||
this.appBarType = GalleryType.owned_collection,
|
||||
this.overlayType = GalleryType.owned_collection,
|
||||
Key key})
|
||||
: super(key: key);
|
||||
CollectionPage(
|
||||
this.c, {
|
||||
this.tagPrefix = "collection",
|
||||
this.appBarType = GalleryType.owned_collection,
|
||||
this.overlayType = GalleryType.owned_collection,
|
||||
Key key,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(Object context) {
|
||||
|
@ -30,8 +31,12 @@ class CollectionPage extends StatelessWidget {
|
|||
final gallery = Gallery(
|
||||
asyncLoader: (creationStartTime, creationEndTime, {limit, asc}) {
|
||||
return FilesDB.instance.getFilesInCollection(
|
||||
c.collection.id, creationStartTime, creationEndTime,
|
||||
limit: limit, asc: asc);
|
||||
c.collection.id,
|
||||
creationStartTime,
|
||||
creationEndTime,
|
||||
limit: limit,
|
||||
asc: asc,
|
||||
);
|
||||
},
|
||||
reloadEvent: Bus.instance
|
||||
.on<CollectionUpdatedEvent>()
|
||||
|
|
|
@ -100,8 +100,10 @@ class _CollectionsGalleryWidgetState extends State<CollectionsGalleryWidget>
|
|||
for (final file in latestLocalFiles) {
|
||||
folders.add(DeviceFolder(file.deviceFolder, file.deviceFolder, file));
|
||||
}
|
||||
folders.sort((first, second) =>
|
||||
second.thumbnail.creationTime.compareTo(first.thumbnail.creationTime));
|
||||
folders.sort(
|
||||
(first, second) =>
|
||||
second.thumbnail.creationTime.compareTo(first.thumbnail.creationTime),
|
||||
);
|
||||
|
||||
final List<CollectionWithThumbnail> collectionsWithThumbnail = [];
|
||||
final latestCollectionFiles =
|
||||
|
@ -136,8 +138,8 @@ class _CollectionsGalleryWidgetState extends State<CollectionsGalleryWidget>
|
|||
.textTheme
|
||||
.subtitle1
|
||||
.copyWith(
|
||||
color:
|
||||
Theme.of(context).textTheme.subtitle1.color.withOpacity(0.5));
|
||||
color: Theme.of(context).textTheme.subtitle1.color.withOpacity(0.5),
|
||||
);
|
||||
Size size = MediaQuery.of(context).size;
|
||||
int albumsCountInOneRow = max(size.width ~/ 220.0, 2);
|
||||
final double sideOfThumbnail = (size.width / 2) -
|
||||
|
@ -155,8 +157,8 @@ class _CollectionsGalleryWidgetState extends State<CollectionsGalleryWidget>
|
|||
? Padding(
|
||||
padding: const EdgeInsets.all(22),
|
||||
child: nothingToSeeHere(
|
||||
textColor:
|
||||
Theme.of(context).colorScheme.defaultTextColor),
|
||||
textColor: Theme.of(context).colorScheme.defaultTextColor,
|
||||
),
|
||||
)
|
||||
: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8),
|
||||
|
@ -168,7 +170,8 @@ class _CollectionsGalleryWidgetState extends State<CollectionsGalleryWidget>
|
|||
? nothingToSeeHere(
|
||||
textColor: Theme.of(context)
|
||||
.colorScheme
|
||||
.defaultTextColor)
|
||||
.defaultTextColor,
|
||||
)
|
||||
: ListView.builder(
|
||||
shrinkWrap: true,
|
||||
scrollDirection: Axis.horizontal,
|
||||
|
@ -203,21 +206,25 @@ class _CollectionsGalleryWidgetState extends State<CollectionsGalleryWidget>
|
|||
// to disable GridView's scrolling
|
||||
itemBuilder: (context, index) {
|
||||
return _buildCollection(
|
||||
context, items.collections, index);
|
||||
context,
|
||||
items.collections,
|
||||
index,
|
||||
);
|
||||
},
|
||||
itemCount: items.collections.length + 1,
|
||||
// To include the + button
|
||||
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
|
||||
crossAxisCount: albumsCountInOneRow,
|
||||
mainAxisSpacing: 12,
|
||||
crossAxisSpacing: crossAxisSpacingOfGrid,
|
||||
childAspectRatio: sideOfThumbnail /
|
||||
(sideOfThumbnail +
|
||||
24)), //24 is height of album title
|
||||
crossAxisCount: albumsCountInOneRow,
|
||||
mainAxisSpacing: 12,
|
||||
crossAxisSpacing: crossAxisSpacingOfGrid,
|
||||
childAspectRatio:
|
||||
sideOfThumbnail / (sideOfThumbnail + 24),
|
||||
), //24 is height of album title
|
||||
),
|
||||
)
|
||||
: nothingToSeeHere(
|
||||
textColor: Theme.of(context).colorScheme.defaultTextColor),
|
||||
textColor: Theme.of(context).colorScheme.defaultTextColor,
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
const Divider(),
|
||||
const Padding(padding: EdgeInsets.all(8)),
|
||||
|
@ -258,31 +265,38 @@ class _CollectionsGalleryWidgetState extends State<CollectionsGalleryWidget>
|
|||
builder: (context, snapshot) {
|
||||
if (snapshot.hasData && snapshot.data > 0) {
|
||||
return RichText(
|
||||
text: TextSpan(
|
||||
style: trashAndHiddenTextStyle,
|
||||
children: [
|
||||
text: TextSpan(
|
||||
style: trashAndHiddenTextStyle,
|
||||
children: [
|
||||
TextSpan(
|
||||
text: "Trash",
|
||||
style: Theme.of(context)
|
||||
.textTheme
|
||||
.subtitle1),
|
||||
text: "Trash",
|
||||
style: Theme.of(context)
|
||||
.textTheme
|
||||
.subtitle1,
|
||||
),
|
||||
TextSpan(text: " \u2022 "),
|
||||
TextSpan(
|
||||
text: snapshot.data.toString()),
|
||||
text: snapshot.data.toString(),
|
||||
),
|
||||
//need to query in db and bring this value
|
||||
]));
|
||||
],
|
||||
),
|
||||
);
|
||||
} else {
|
||||
return RichText(
|
||||
text: TextSpan(
|
||||
style: trashAndHiddenTextStyle,
|
||||
children: [
|
||||
text: TextSpan(
|
||||
style: trashAndHiddenTextStyle,
|
||||
children: [
|
||||
TextSpan(
|
||||
text: "Trash",
|
||||
style: Theme.of(context)
|
||||
.textTheme
|
||||
.subtitle1),
|
||||
text: "Trash",
|
||||
style: Theme.of(context)
|
||||
.textTheme
|
||||
.subtitle1,
|
||||
),
|
||||
//need to query in db and bring this value
|
||||
]));
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
},
|
||||
),
|
||||
|
@ -333,38 +347,46 @@ class _CollectionsGalleryWidgetState extends State<CollectionsGalleryWidget>
|
|||
),
|
||||
Padding(padding: EdgeInsets.all(6)),
|
||||
FutureBuilder<int>(
|
||||
future: FilesDB.instance
|
||||
.fileCountWithVisibility(
|
||||
kVisibilityArchive,
|
||||
Configuration.instance.getUserID()),
|
||||
future:
|
||||
FilesDB.instance.fileCountWithVisibility(
|
||||
kVisibilityArchive,
|
||||
Configuration.instance.getUserID(),
|
||||
),
|
||||
builder: (context, snapshot) {
|
||||
if (snapshot.hasData && snapshot.data > 0) {
|
||||
return RichText(
|
||||
text: TextSpan(
|
||||
style: trashAndHiddenTextStyle,
|
||||
children: [
|
||||
text: TextSpan(
|
||||
style: trashAndHiddenTextStyle,
|
||||
children: [
|
||||
TextSpan(
|
||||
text: "Hidden",
|
||||
style: Theme.of(context)
|
||||
.textTheme
|
||||
.subtitle1),
|
||||
text: "Hidden",
|
||||
style: Theme.of(context)
|
||||
.textTheme
|
||||
.subtitle1,
|
||||
),
|
||||
TextSpan(text: " \u2022 "),
|
||||
TextSpan(
|
||||
text: snapshot.data.toString()),
|
||||
text: snapshot.data.toString(),
|
||||
),
|
||||
//need to query in db and bring this value
|
||||
]));
|
||||
],
|
||||
),
|
||||
);
|
||||
} else {
|
||||
return RichText(
|
||||
text: TextSpan(
|
||||
style: trashAndHiddenTextStyle,
|
||||
children: [
|
||||
text: TextSpan(
|
||||
style: trashAndHiddenTextStyle,
|
||||
children: [
|
||||
TextSpan(
|
||||
text: "Hidden",
|
||||
style: Theme.of(context)
|
||||
.textTheme
|
||||
.subtitle1),
|
||||
text: "Hidden",
|
||||
style: Theme.of(context)
|
||||
.textTheme
|
||||
.subtitle1,
|
||||
),
|
||||
//need to query in db and bring this value
|
||||
]));
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
},
|
||||
),
|
||||
|
@ -408,10 +430,13 @@ class _CollectionsGalleryWidgetState extends State<CollectionsGalleryWidget>
|
|||
case AlbumSortKey.lastUpdated:
|
||||
text = "Last updated";
|
||||
}
|
||||
return Text(text,
|
||||
style: Theme.of(context).textTheme.subtitle1.copyWith(
|
||||
return Text(
|
||||
text,
|
||||
style: Theme.of(context).textTheme.subtitle1.copyWith(
|
||||
fontSize: 14,
|
||||
color: Theme.of(context).iconTheme.color.withOpacity(0.7)));
|
||||
color: Theme.of(context).iconTheme.color.withOpacity(0.7),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return Padding(
|
||||
|
@ -460,8 +485,11 @@ class _CollectionsGalleryWidgetState extends State<CollectionsGalleryWidget>
|
|||
);
|
||||
}
|
||||
|
||||
Widget _buildCollection(BuildContext context,
|
||||
List<CollectionWithThumbnail> collections, int index) {
|
||||
Widget _buildCollection(
|
||||
BuildContext context,
|
||||
List<CollectionWithThumbnail> collections,
|
||||
int index,
|
||||
) {
|
||||
if (index < collections.length) {
|
||||
final c = collections[index];
|
||||
return CollectionItem(c);
|
||||
|
@ -473,10 +501,11 @@ class _CollectionsGalleryWidgetState extends State<CollectionsGalleryWidget>
|
|||
color: Theme.of(context).backgroundColor,
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
blurRadius: 2,
|
||||
spreadRadius: 0,
|
||||
offset: Offset(0, 0),
|
||||
color: Theme.of(context).iconTheme.color.withOpacity(0.3))
|
||||
blurRadius: 2,
|
||||
spreadRadius: 0,
|
||||
offset: Offset(0, 0),
|
||||
color: Theme.of(context).iconTheme.color.withOpacity(0.3),
|
||||
)
|
||||
],
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
),
|
||||
|
@ -486,9 +515,11 @@ class _CollectionsGalleryWidgetState extends State<CollectionsGalleryWidget>
|
|||
),
|
||||
),
|
||||
onTap: () async {
|
||||
await showToast(context,
|
||||
"long press to select photos and click + to create an album",
|
||||
toastLength: Toast.LENGTH_LONG);
|
||||
await showToast(
|
||||
context,
|
||||
"long press to select photos and click + to create an album",
|
||||
toastLength: Toast.LENGTH_LONG,
|
||||
);
|
||||
Bus.instance
|
||||
.fire(TabChangedEvent(0, TabChangedEventSource.collections_page));
|
||||
},
|
||||
|
@ -566,9 +597,11 @@ class DeviceFolderIcon extends StatelessWidget {
|
|||
ThumbnailWidget(
|
||||
folder.thumbnail,
|
||||
shouldShowSyncStatus: false,
|
||||
key: Key("device_folder:" +
|
||||
folder.path +
|
||||
folder.thumbnail.tag()),
|
||||
key: Key(
|
||||
"device_folder:" +
|
||||
folder.path +
|
||||
folder.thumbnail.tag(),
|
||||
),
|
||||
),
|
||||
isBackedUp ? Container() : kUnsyncedIconOverlay,
|
||||
],
|
||||
|
@ -623,17 +656,19 @@ class CollectionItem extends StatelessWidget {
|
|||
ClipRRect(
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
child: SizedBox(
|
||||
child: Hero(
|
||||
tag: "collection" + c.thumbnail.tag(),
|
||||
child: ThumbnailWidget(
|
||||
c.thumbnail,
|
||||
shouldShowArchiveStatus: c.collection.isArchived(),
|
||||
key: Key(
|
||||
"collection" + c.thumbnail.tag(),
|
||||
),
|
||||
)),
|
||||
height: sideOfThumbnail,
|
||||
width: sideOfThumbnail),
|
||||
child: Hero(
|
||||
tag: "collection" + c.thumbnail.tag(),
|
||||
child: ThumbnailWidget(
|
||||
c.thumbnail,
|
||||
shouldShowArchiveStatus: c.collection.isArchived(),
|
||||
key: Key(
|
||||
"collection" + c.thumbnail.tag(),
|
||||
),
|
||||
),
|
||||
),
|
||||
height: sideOfThumbnail,
|
||||
width: sideOfThumbnail,
|
||||
),
|
||||
),
|
||||
SizedBox(height: 4),
|
||||
Row(
|
||||
|
@ -651,15 +686,17 @@ class CollectionItem extends StatelessWidget {
|
|||
builder: (context, snapshot) {
|
||||
if (snapshot.hasData && snapshot.data > 0) {
|
||||
return RichText(
|
||||
text: TextSpan(
|
||||
style: albumTitleTextStyle.copyWith(
|
||||
color:
|
||||
albumTitleTextStyle.color.withOpacity(0.5)),
|
||||
children: [
|
||||
text: TextSpan(
|
||||
style: albumTitleTextStyle.copyWith(
|
||||
color: albumTitleTextStyle.color.withOpacity(0.5),
|
||||
),
|
||||
children: [
|
||||
TextSpan(text: " \u2022 "),
|
||||
TextSpan(text: snapshot.data.toString()),
|
||||
//need to query in db and bring this value
|
||||
]));
|
||||
],
|
||||
),
|
||||
);
|
||||
} else {
|
||||
return Container();
|
||||
}
|
||||
|
|
|
@ -14,13 +14,13 @@ class BottomShadowWidget extends StatelessWidget {
|
|||
color: Colors.transparent,
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: shadowColor == null
|
||||
? Theme.of(context).backgroundColor
|
||||
: shadowColor,
|
||||
spreadRadius: 42,
|
||||
blurRadius: 42,
|
||||
offset: Offset(0, offsetDy) // changes position of shadow
|
||||
),
|
||||
color: shadowColor == null
|
||||
? Theme.of(context).backgroundColor
|
||||
: shadowColor,
|
||||
spreadRadius: 42,
|
||||
blurRadius: 42,
|
||||
offset: Offset(0, offsetDy), // changes position of shadow
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
|
|
|
@ -9,13 +9,13 @@ class DynamicFAB extends StatelessWidget {
|
|||
final String buttonText;
|
||||
final Function onPressedFunction;
|
||||
|
||||
DynamicFAB(
|
||||
{Key key,
|
||||
this.isKeypadOpen,
|
||||
this.buttonText,
|
||||
this.isFormValid,
|
||||
this.onPressedFunction})
|
||||
: super(key: key);
|
||||
DynamicFAB({
|
||||
Key key,
|
||||
this.isKeypadOpen,
|
||||
this.buttonText,
|
||||
this.isFormValid,
|
||||
this.onPressedFunction,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
|
@ -36,24 +36,24 @@ class DynamicFAB extends StatelessWidget {
|
|||
mainAxisAlignment: MainAxisAlignment.end,
|
||||
children: [
|
||||
FloatingActionButton(
|
||||
heroTag: 'FAB',
|
||||
backgroundColor:
|
||||
Theme.of(context).colorScheme.dynamicFABBackgroundColor,
|
||||
foregroundColor:
|
||||
Theme.of(context).colorScheme.dynamicFABTextColor,
|
||||
child: Transform.rotate(
|
||||
angle: isFormValid ? 0 : math.pi / 2,
|
||||
child: Icon(
|
||||
Icons.chevron_right,
|
||||
size: 36,
|
||||
),
|
||||
),
|
||||
onPressed: isFormValid
|
||||
? onPressedFunction
|
||||
: () {
|
||||
FocusScope.of(context).unfocus();
|
||||
} //keypad down here
|
||||
heroTag: 'FAB',
|
||||
backgroundColor:
|
||||
Theme.of(context).colorScheme.dynamicFABBackgroundColor,
|
||||
foregroundColor:
|
||||
Theme.of(context).colorScheme.dynamicFABTextColor,
|
||||
child: Transform.rotate(
|
||||
angle: isFormValid ? 0 : math.pi / 2,
|
||||
child: Icon(
|
||||
Icons.chevron_right,
|
||||
size: 36,
|
||||
),
|
||||
),
|
||||
onPressed: isFormValid
|
||||
? onPressedFunction
|
||||
: () {
|
||||
FocusScope.of(context).unfocus();
|
||||
}, //keypad down here
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
|
|
|
@ -10,20 +10,23 @@ class onlyOuterShadow extends BoxShadow {
|
|||
double spreadRadius = 0.0,
|
||||
this.blurStyle = BlurStyle.normal,
|
||||
}) : super(
|
||||
color: color,
|
||||
offset: offset,
|
||||
blurRadius: blurRadius,
|
||||
spreadRadius: spreadRadius);
|
||||
color: color,
|
||||
offset: offset,
|
||||
blurRadius: blurRadius,
|
||||
spreadRadius: spreadRadius,
|
||||
);
|
||||
|
||||
@override
|
||||
Paint toPaint() {
|
||||
final Paint result = Paint()
|
||||
..color = color
|
||||
..maskFilter = MaskFilter.blur(this.blurStyle, blurSigma);
|
||||
assert(() {
|
||||
if (debugDisableShadows) result.maskFilter = null;
|
||||
return true;
|
||||
}());
|
||||
assert(
|
||||
() {
|
||||
if (debugDisableShadows) result.maskFilter = null;
|
||||
return true;
|
||||
}(),
|
||||
);
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -19,8 +19,12 @@ PopupMenuButton<dynamic> reportBugPopupMenu(BuildContext context) {
|
|||
},
|
||||
onSelected: (value) async {
|
||||
if (value == 1) {
|
||||
await sendLogs(context, "Contact support", "support@ente.io",
|
||||
postShare: () {});
|
||||
await sendLogs(
|
||||
context,
|
||||
"Contact support",
|
||||
"support@ente.io",
|
||||
postShare: () {},
|
||||
);
|
||||
}
|
||||
},
|
||||
);
|
||||
|
|
|
@ -43,9 +43,12 @@ class CreateCollectionPage extends StatefulWidget {
|
|||
final List<SharedMediaFile> sharedFiles;
|
||||
final CollectionActionType actionType;
|
||||
|
||||
const CreateCollectionPage(this.selectedFiles, this.sharedFiles,
|
||||
{Key key, this.actionType = CollectionActionType.addFiles})
|
||||
: super(key: key);
|
||||
const CreateCollectionPage(
|
||||
this.selectedFiles,
|
||||
this.sharedFiles, {
|
||||
Key key,
|
||||
this.actionType = CollectionActionType.addFiles,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
_CreateCollectionPageState createState() => _CreateCollectionPageState();
|
||||
|
@ -78,7 +81,11 @@ class _CreateCollectionPageState extends State<CreateCollectionPage> {
|
|||
Expanded(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.only(
|
||||
top: 30, bottom: 12, left: 40, right: 40),
|
||||
top: 30,
|
||||
bottom: 12,
|
||||
left: 40,
|
||||
right: 40,
|
||||
),
|
||||
child: GradientButton(
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
|
@ -182,10 +189,11 @@ class _CreateCollectionPageState extends State<CreateCollectionPage> {
|
|||
onTap: () async {
|
||||
if (await _runCollectionAction(item.collection.id)) {
|
||||
showShortToast(
|
||||
context,
|
||||
widget.actionType == CollectionActionType.addFiles
|
||||
? "Added successfully to " + item.collection.name
|
||||
: "Moved successfully to " + item.collection.name);
|
||||
context,
|
||||
widget.actionType == CollectionActionType.addFiles
|
||||
? "Added successfully to " + item.collection.name
|
||||
: "Moved successfully to " + item.collection.name,
|
||||
);
|
||||
_navigateToCollection(item.collection);
|
||||
}
|
||||
},
|
||||
|
@ -243,10 +251,14 @@ class _CreateCollectionPageState extends State<CreateCollectionPage> {
|
|||
if (await _runCollectionAction(collection.id)) {
|
||||
if (widget.actionType == CollectionActionType.restoreFiles) {
|
||||
showShortToast(
|
||||
context, 'Restored files to album ' + _albumName);
|
||||
context,
|
||||
'Restored files to album ' + _albumName,
|
||||
);
|
||||
} else {
|
||||
showShortToast(
|
||||
context, "Album '" + _albumName + "' created.");
|
||||
context,
|
||||
"Album '" + _albumName + "' created.",
|
||||
);
|
||||
}
|
||||
_navigateToCollection(collection);
|
||||
}
|
||||
|
@ -267,12 +279,14 @@ class _CreateCollectionPageState extends State<CreateCollectionPage> {
|
|||
void _navigateToCollection(Collection collection) {
|
||||
Navigator.pop(context);
|
||||
Navigator.push(
|
||||
context,
|
||||
PageTransition(
|
||||
type: PageTransitionType.bottomToTop,
|
||||
child: CollectionPage(
|
||||
CollectionWithThumbnail(collection, null),
|
||||
)));
|
||||
context,
|
||||
PageTransition(
|
||||
type: PageTransitionType.bottomToTop,
|
||||
child: CollectionPage(
|
||||
CollectionWithThumbnail(collection, null),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Future<bool> _runCollectionAction(int collectionID) async {
|
||||
|
@ -292,8 +306,11 @@ class _CreateCollectionPageState extends State<CreateCollectionPage> {
|
|||
await dialog.show();
|
||||
try {
|
||||
int fromCollectionID = widget.selectedFiles.files?.first?.collectionID;
|
||||
await CollectionsService.instance.move(toCollectionID, fromCollectionID,
|
||||
widget.selectedFiles.files?.toList());
|
||||
await CollectionsService.instance.move(
|
||||
toCollectionID,
|
||||
fromCollectionID,
|
||||
widget.selectedFiles.files?.toList(),
|
||||
);
|
||||
RemoteSyncService.instance.sync(silently: true);
|
||||
widget.selectedFiles?.clearAll();
|
||||
await dialog.hide();
|
||||
|
@ -339,8 +356,12 @@ class _CreateCollectionPageState extends State<CreateCollectionPage> {
|
|||
final List<File> files = [];
|
||||
final List<File> filesPendingUpload = [];
|
||||
if (widget.sharedFiles != null) {
|
||||
filesPendingUpload.addAll(await convertIncomingSharedMediaToFile(
|
||||
widget.sharedFiles, collectionID));
|
||||
filesPendingUpload.addAll(
|
||||
await convertIncomingSharedMediaToFile(
|
||||
widget.sharedFiles,
|
||||
collectionID,
|
||||
),
|
||||
);
|
||||
} else {
|
||||
final List<File> filesPendingUpload = [];
|
||||
for (final file in widget.selectedFiles.files) {
|
||||
|
|
|
@ -126,15 +126,17 @@ class _DeduplicatePageState extends State<DeduplicatePage> {
|
|||
return Padding(
|
||||
padding: EdgeInsets.only(top: 32),
|
||||
child: nothingToSeeHere(
|
||||
textColor:
|
||||
Theme.of(context).colorScheme.defaultTextColor),
|
||||
textColor: Theme.of(context).colorScheme.defaultTextColor,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(top: 10, bottom: 10),
|
||||
child: _getGridView(_duplicates[index - kHeaderRowCount],
|
||||
index - kHeaderRowCount),
|
||||
child: _getGridView(
|
||||
_duplicates[index - kHeaderRowCount],
|
||||
index - kHeaderRowCount,
|
||||
),
|
||||
);
|
||||
},
|
||||
itemCount: _duplicates.length + kHeaderRowCount,
|
||||
|
@ -153,10 +155,11 @@ class _DeduplicatePageState extends State<DeduplicatePage> {
|
|||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
Text(
|
||||
"Following files were clubbed based on their sizes" +
|
||||
((_shouldClubByCaptureTime ? " and capture times." : ".") +
|
||||
", please review and delete the items you believe are duplicates."),
|
||||
style: Theme.of(context).textTheme.subtitle2),
|
||||
"Following files were clubbed based on their sizes" +
|
||||
((_shouldClubByCaptureTime ? " and capture times." : ".") +
|
||||
", please review and delete the items you believe are duplicates."),
|
||||
style: Theme.of(context).textTheme.subtitle2,
|
||||
),
|
||||
Padding(
|
||||
padding: EdgeInsets.all(12),
|
||||
),
|
||||
|
@ -210,8 +213,9 @@ class _DeduplicatePageState extends State<DeduplicatePage> {
|
|||
return Text(
|
||||
text,
|
||||
style: Theme.of(context).textTheme.subtitle1.copyWith(
|
||||
fontSize: 14,
|
||||
color: Theme.of(context).iconTheme.color.withOpacity(0.7)),
|
||||
fontSize: 14,
|
||||
color: Theme.of(context).iconTheme.color.withOpacity(0.7),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -377,19 +381,21 @@ class _DeduplicatePageState extends State<DeduplicatePage> {
|
|||
)
|
||||
: null,
|
||||
),
|
||||
child: Stack(children: [
|
||||
Hero(
|
||||
tag: "deduplicate_" + file.tag(),
|
||||
child: ThumbnailWidget(
|
||||
file,
|
||||
diskLoadDeferDuration: kThumbnailDiskLoadDeferDuration,
|
||||
serverLoadDeferDuration: kThumbnailServerLoadDeferDuration,
|
||||
shouldShowLivePhotoOverlay: true,
|
||||
key: Key("deduplicate_" + file.tag()),
|
||||
child: Stack(
|
||||
children: [
|
||||
Hero(
|
||||
tag: "deduplicate_" + file.tag(),
|
||||
child: ThumbnailWidget(
|
||||
file,
|
||||
diskLoadDeferDuration: kThumbnailDiskLoadDeferDuration,
|
||||
serverLoadDeferDuration: kThumbnailServerLoadDeferDuration,
|
||||
shouldShowLivePhotoOverlay: true,
|
||||
key: Key("deduplicate_" + file.tag()),
|
||||
),
|
||||
),
|
||||
),
|
||||
_selectedFiles.contains(file) ? kDeleteIconOverlay : Container(),
|
||||
]),
|
||||
_selectedFiles.contains(file) ? kDeleteIconOverlay : Container(),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
|
|
@ -91,13 +91,15 @@ class _DetailPageState extends State<DetailPage> {
|
|||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
_logger.info("Opening " +
|
||||
_files[_selectedIndex].toString() +
|
||||
". " +
|
||||
(_selectedIndex + 1).toString() +
|
||||
" / " +
|
||||
_files.length.toString() +
|
||||
" files .");
|
||||
_logger.info(
|
||||
"Opening " +
|
||||
_files[_selectedIndex].toString() +
|
||||
". " +
|
||||
(_selectedIndex + 1).toString() +
|
||||
" / " +
|
||||
_files.length.toString() +
|
||||
" files .",
|
||||
);
|
||||
_appBarKey = GlobalKey<FadingAppBarState>();
|
||||
_bottomBarKey = GlobalKey<FadingBottomBarState>();
|
||||
return Scaffold(
|
||||
|
@ -198,10 +200,11 @@ class _DetailPageState extends State<DetailPage> {
|
|||
}
|
||||
if (_selectedIndex == 0 && !_hasLoadedTillStart) {
|
||||
final result = await widget.config.asyncLoader(
|
||||
_files[_selectedIndex].creationTime + 1,
|
||||
DateTime.now().microsecondsSinceEpoch,
|
||||
limit: kLoadLimit,
|
||||
asc: true);
|
||||
_files[_selectedIndex].creationTime + 1,
|
||||
DateTime.now().microsecondsSinceEpoch,
|
||||
limit: kLoadLimit,
|
||||
asc: true,
|
||||
);
|
||||
setState(() {
|
||||
// Returned result could be a subtype of File
|
||||
// ignore: unnecessary_cast
|
||||
|
@ -218,8 +221,10 @@ class _DetailPageState extends State<DetailPage> {
|
|||
}
|
||||
if (_selectedIndex == _files.length - 1 && !_hasLoadedTillEnd) {
|
||||
final result = await widget.config.asyncLoader(
|
||||
kGalleryLoadStartTime, _files[_selectedIndex].creationTime - 1,
|
||||
limit: kLoadLimit);
|
||||
kGalleryLoadStartTime,
|
||||
_files[_selectedIndex].creationTime - 1,
|
||||
limit: kLoadLimit,
|
||||
);
|
||||
setState(() {
|
||||
if (!result.hasMore) {
|
||||
_hasLoadedTillEnd = true;
|
||||
|
@ -248,13 +253,17 @@ class _DetailPageState extends State<DetailPage> {
|
|||
if (_selectedIndex == totalFiles - 1) {
|
||||
// Deleted the last file
|
||||
await _pageController.previousPage(
|
||||
duration: Duration(milliseconds: 200), curve: Curves.easeInOut);
|
||||
duration: Duration(milliseconds: 200),
|
||||
curve: Curves.easeInOut,
|
||||
);
|
||||
setState(() {
|
||||
_files.remove(file);
|
||||
});
|
||||
} else {
|
||||
await _pageController.nextPage(
|
||||
duration: Duration(milliseconds: 200), curve: Curves.easeInOut);
|
||||
duration: Duration(milliseconds: 200),
|
||||
curve: Curves.easeInOut,
|
||||
);
|
||||
setState(() {
|
||||
_selectedIndex--;
|
||||
_files.remove(file);
|
||||
|
@ -265,10 +274,16 @@ class _DetailPageState extends State<DetailPage> {
|
|||
Future<void> _onEditFileRequested(File file) async {
|
||||
if (file.uploadedFileID != null &&
|
||||
file.ownerID != Configuration.instance.getUserID()) {
|
||||
_logger.severe("Attempt to edit unowned file", UnauthorizedEditError(),
|
||||
StackTrace.current);
|
||||
showErrorDialog(context, "Sorry",
|
||||
"We don't support editing photos and albums that you don't own yet");
|
||||
_logger.severe(
|
||||
"Attempt to edit unowned file",
|
||||
UnauthorizedEditError(),
|
||||
StackTrace.current,
|
||||
);
|
||||
showErrorDialog(
|
||||
context,
|
||||
"Sorry",
|
||||
"We don't support editing photos and albums that you don't own yet",
|
||||
);
|
||||
return;
|
||||
}
|
||||
final dialog = createProgressDialog(context, "Please wait...");
|
||||
|
|
|
@ -24,8 +24,12 @@ class DeviceFolderPage extends StatelessWidget {
|
|||
final gallery = Gallery(
|
||||
asyncLoader: (creationStartTime, creationEndTime, {limit, asc}) {
|
||||
return FilesDB.instance.getFilesInPath(
|
||||
folder.path, creationStartTime, creationEndTime,
|
||||
limit: limit, asc: asc);
|
||||
folder.path,
|
||||
creationStartTime,
|
||||
creationEndTime,
|
||||
limit: limit,
|
||||
asc: asc,
|
||||
);
|
||||
},
|
||||
reloadEvent: Bus.instance.on<LocalPhotosUpdatedEvent>(),
|
||||
removalEventTypes: const {
|
||||
|
@ -96,10 +100,11 @@ class _BackupConfigurationHeaderWidgetState
|
|||
: Text(
|
||||
"Backup disabled",
|
||||
style: TextStyle(
|
||||
color: Theme.of(context)
|
||||
.colorScheme
|
||||
.defaultTextColor
|
||||
.withOpacity(0.7)),
|
||||
color: Theme.of(context)
|
||||
.colorScheme
|
||||
.defaultTextColor
|
||||
.withOpacity(0.7),
|
||||
),
|
||||
),
|
||||
Switch(
|
||||
value: isBackedUp,
|
||||
|
|
|
@ -18,19 +18,23 @@ class FilteredImage extends StatelessWidget {
|
|||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return ColorFiltered(
|
||||
colorFilter:
|
||||
ColorFilter.matrix(ColorFilterGenerator.brightnessAdjustMatrix(
|
||||
value: brightness ?? 1,
|
||||
)),
|
||||
colorFilter: ColorFilter.matrix(
|
||||
ColorFilterGenerator.brightnessAdjustMatrix(
|
||||
value: brightness ?? 1,
|
||||
),
|
||||
),
|
||||
child: ColorFiltered(
|
||||
colorFilter:
|
||||
ColorFilter.matrix(ColorFilterGenerator.saturationAdjustMatrix(
|
||||
value: saturation ?? 1,
|
||||
)),
|
||||
colorFilter: ColorFilter.matrix(
|
||||
ColorFilterGenerator.saturationAdjustMatrix(
|
||||
value: saturation ?? 1,
|
||||
),
|
||||
),
|
||||
child: ColorFiltered(
|
||||
colorFilter: ColorFilter.matrix(ColorFilterGenerator.hueAdjustMatrix(
|
||||
value: hue ?? 0,
|
||||
)),
|
||||
colorFilter: ColorFilter.matrix(
|
||||
ColorFilterGenerator.hueAdjustMatrix(
|
||||
value: hue ?? 0,
|
||||
),
|
||||
),
|
||||
child: child,
|
||||
),
|
||||
),
|
||||
|
|
|
@ -88,15 +88,16 @@ class _EmailEntryPageState extends State<EmailEntryPage> {
|
|||
},
|
||||
),
|
||||
title: Material(
|
||||
type: MaterialType.transparency,
|
||||
child: StepProgressIndicator(
|
||||
totalSteps: 4,
|
||||
currentStep: 1,
|
||||
selectedColor: Theme.of(context).buttonColor,
|
||||
roundedEdges: Radius.circular(10),
|
||||
unselectedColor:
|
||||
Theme.of(context).colorScheme.stepProgressUnselectedColor,
|
||||
)),
|
||||
type: MaterialType.transparency,
|
||||
child: StepProgressIndicator(
|
||||
totalSteps: 4,
|
||||
currentStep: 1,
|
||||
selectedColor: Theme.of(context).buttonColor,
|
||||
roundedEdges: Radius.circular(10),
|
||||
unselectedColor:
|
||||
Theme.of(context).colorScheme.stepProgressUnselectedColor,
|
||||
),
|
||||
),
|
||||
);
|
||||
return Scaffold(
|
||||
appBar: appBar,
|
||||
|
@ -136,8 +137,10 @@ class _EmailEntryPageState extends State<EmailEntryPage> {
|
|||
Padding(
|
||||
padding:
|
||||
const EdgeInsets.symmetric(vertical: 30, horizontal: 20),
|
||||
child: Text('Create new account',
|
||||
style: Theme.of(context).textTheme.headline4),
|
||||
child: Text(
|
||||
'Create new account',
|
||||
style: Theme.of(context).textTheme.headline4,
|
||||
),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.fromLTRB(20, 0, 20, 0),
|
||||
|
@ -151,8 +154,9 @@ class _EmailEntryPageState extends State<EmailEntryPage> {
|
|||
contentPadding:
|
||||
EdgeInsets.symmetric(horizontal: 16, vertical: 14),
|
||||
border: UnderlineInputBorder(
|
||||
borderSide: BorderSide.none,
|
||||
borderRadius: BorderRadius.circular(6)),
|
||||
borderSide: BorderSide.none,
|
||||
borderRadius: BorderRadius.circular(6),
|
||||
),
|
||||
suffixIcon: _emailIsValid
|
||||
? Icon(
|
||||
Icons.check,
|
||||
|
@ -221,8 +225,9 @@ class _EmailEntryPageState extends State<EmailEntryPage> {
|
|||
)
|
||||
: null,
|
||||
border: UnderlineInputBorder(
|
||||
borderSide: BorderSide.none,
|
||||
borderRadius: BorderRadius.circular(6)),
|
||||
borderSide: BorderSide.none,
|
||||
borderRadius: BorderRadius.circular(6),
|
||||
),
|
||||
),
|
||||
focusNode: _password1FocusNode,
|
||||
onChanged: (password) {
|
||||
|
@ -284,8 +289,9 @@ class _EmailEntryPageState extends State<EmailEntryPage> {
|
|||
)
|
||||
: null,
|
||||
border: UnderlineInputBorder(
|
||||
borderSide: BorderSide.none,
|
||||
borderRadius: BorderRadius.circular(6)),
|
||||
borderSide: BorderSide.none,
|
||||
borderRadius: BorderRadius.circular(6),
|
||||
),
|
||||
),
|
||||
focusNode: _password2FocusNode,
|
||||
onChanged: (cnfPassword) {
|
||||
|
@ -348,13 +354,14 @@ class _EmailEntryPageState extends State<EmailEntryPage> {
|
|||
child: Row(
|
||||
children: [
|
||||
Checkbox(
|
||||
value: _hasAgreedToTOS,
|
||||
side: CheckboxTheme.of(context).side,
|
||||
onChanged: (value) {
|
||||
setState(() {
|
||||
_hasAgreedToTOS = value;
|
||||
});
|
||||
}),
|
||||
value: _hasAgreedToTOS,
|
||||
side: CheckboxTheme.of(context).side,
|
||||
onChanged: (value) {
|
||||
setState(() {
|
||||
_hasAgreedToTOS = value;
|
||||
});
|
||||
},
|
||||
),
|
||||
Expanded(
|
||||
child: RichText(
|
||||
text: TextSpan(
|
||||
|
@ -390,7 +397,9 @@ class _EmailEntryPageState extends State<EmailEntryPage> {
|
|||
MaterialPageRoute(
|
||||
builder: (BuildContext context) {
|
||||
return WebPage(
|
||||
"Privacy", "https://ente.io/privacy");
|
||||
"Privacy",
|
||||
"https://ente.io/privacy",
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
|
@ -448,7 +457,9 @@ class _EmailEntryPageState extends State<EmailEntryPage> {
|
|||
MaterialPageRoute(
|
||||
builder: (BuildContext context) {
|
||||
return WebPage(
|
||||
"encryption", "https://ente.io/architecture");
|
||||
"encryption",
|
||||
"https://ente.io/architecture",
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
|
@ -526,12 +537,14 @@ class PricingWidget extends StatelessWidget {
|
|||
children: planWidgets,
|
||||
),
|
||||
),
|
||||
Text("We offer a free trial of " +
|
||||
convertBytesToReadableFormat(freePlan.storage) +
|
||||
" for " +
|
||||
freePlan.duration.toString() +
|
||||
" " +
|
||||
freePlan.period),
|
||||
Text(
|
||||
"We offer a free trial of " +
|
||||
convertBytesToReadableFormat(freePlan.storage) +
|
||||
" for " +
|
||||
freePlan.duration.toString() +
|
||||
" " +
|
||||
freePlan.period,
|
||||
),
|
||||
GestureDetector(
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
|
|
|
@ -178,21 +178,22 @@ class _ExpansionTileState extends State<ExpansionCard>
|
|||
mainAxisSize: MainAxisSize.min,
|
||||
children: <Widget>[
|
||||
ListTileTheme.merge(
|
||||
iconColor: _iconColor.value,
|
||||
textColor: _headerColor.value,
|
||||
child: Container(
|
||||
margin: widget.margin,
|
||||
child: ListTile(
|
||||
onTap: _handleTap,
|
||||
leading: widget.leading,
|
||||
title: widget.title,
|
||||
trailing: widget.trailing ??
|
||||
RotationTransition(
|
||||
turns: _iconTurns,
|
||||
child: const Icon(Icons.expand_more),
|
||||
),
|
||||
),
|
||||
)),
|
||||
iconColor: _iconColor.value,
|
||||
textColor: _headerColor.value,
|
||||
child: Container(
|
||||
margin: widget.margin,
|
||||
child: ListTile(
|
||||
onTap: _handleTap,
|
||||
leading: widget.leading,
|
||||
title: widget.title,
|
||||
trailing: widget.trailing ??
|
||||
RotationTransition(
|
||||
turns: _iconTurns,
|
||||
child: const Icon(Icons.expand_more),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
ClipRect(
|
||||
child: Align(
|
||||
heightFactor: _heightFactor.value,
|
||||
|
|
|
@ -368,14 +368,28 @@ class _PageViewState extends State<ExtentsPageView> {
|
|||
description
|
||||
.add(EnumProperty<Axis>('scrollDirection', widget.scrollDirection));
|
||||
description.add(
|
||||
FlagProperty('reverse', value: widget.reverse, ifTrue: 'reversed'));
|
||||
description.add(DiagnosticsProperty<PageController>(
|
||||
'controller', widget.controller,
|
||||
showName: false));
|
||||
description.add(DiagnosticsProperty<ScrollPhysics>(
|
||||
'physics', widget.physics,
|
||||
showName: false));
|
||||
description.add(FlagProperty('pageSnapping',
|
||||
value: widget.pageSnapping, ifFalse: 'snapping disabled'));
|
||||
FlagProperty('reverse', value: widget.reverse, ifTrue: 'reversed'),
|
||||
);
|
||||
description.add(
|
||||
DiagnosticsProperty<PageController>(
|
||||
'controller',
|
||||
widget.controller,
|
||||
showName: false,
|
||||
),
|
||||
);
|
||||
description.add(
|
||||
DiagnosticsProperty<ScrollPhysics>(
|
||||
'physics',
|
||||
widget.physics,
|
||||
showName: false,
|
||||
),
|
||||
);
|
||||
description.add(
|
||||
FlagProperty(
|
||||
'pageSnapping',
|
||||
value: widget.pageSnapping,
|
||||
ifFalse: 'snapping disabled',
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -99,87 +99,89 @@ class FadingAppBarState extends State<FadingAppBar> {
|
|||
if (widget.file.ownerID == null || widget.file.ownerID == widget.userID) {
|
||||
actions.add(_getFavoriteButton());
|
||||
}
|
||||
actions.add(PopupMenuButton(
|
||||
itemBuilder: (context) {
|
||||
final List<PopupMenuItem> items = [];
|
||||
if (widget.file.isRemoteFile()) {
|
||||
items.add(
|
||||
PopupMenuItem(
|
||||
value: 1,
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(
|
||||
Platform.isAndroid
|
||||
? Icons.download
|
||||
: CupertinoIcons.cloud_download,
|
||||
color: Theme.of(context).iconTheme.color,
|
||||
),
|
||||
Padding(
|
||||
padding: EdgeInsets.all(8),
|
||||
),
|
||||
Text("Download"),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
// options for files owned by the user
|
||||
if (widget.file.ownerID == null ||
|
||||
widget.file.ownerID == widget.userID) {
|
||||
if (widget.file.uploadedFileID != null) {
|
||||
actions.add(
|
||||
PopupMenuButton(
|
||||
itemBuilder: (context) {
|
||||
final List<PopupMenuItem> items = [];
|
||||
if (widget.file.isRemoteFile()) {
|
||||
items.add(
|
||||
PopupMenuItem(
|
||||
value: 2,
|
||||
value: 1,
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(
|
||||
Platform.isAndroid
|
||||
? Icons.access_time_rounded
|
||||
: CupertinoIcons.time,
|
||||
? Icons.download
|
||||
: CupertinoIcons.cloud_download,
|
||||
color: Theme.of(context).iconTheme.color,
|
||||
),
|
||||
Padding(
|
||||
padding: EdgeInsets.all(8),
|
||||
),
|
||||
Text("Edit time"),
|
||||
Text("Download"),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
// options for files owned by the user
|
||||
if (widget.file.ownerID == null ||
|
||||
widget.file.ownerID == widget.userID) {
|
||||
if (widget.file.uploadedFileID != null) {
|
||||
items.add(
|
||||
PopupMenuItem(
|
||||
value: 2,
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(
|
||||
Platform.isAndroid
|
||||
? Icons.access_time_rounded
|
||||
: CupertinoIcons.time,
|
||||
color: Theme.of(context).iconTheme.color,
|
||||
),
|
||||
Padding(
|
||||
padding: EdgeInsets.all(8),
|
||||
),
|
||||
Text("Edit time"),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
items.add(
|
||||
PopupMenuItem(
|
||||
value: 3,
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(
|
||||
Platform.isAndroid
|
||||
? Icons.delete_outline
|
||||
: CupertinoIcons.delete,
|
||||
color: Theme.of(context).iconTheme.color,
|
||||
),
|
||||
Padding(
|
||||
padding: EdgeInsets.all(8),
|
||||
),
|
||||
Text("Delete"),
|
||||
],
|
||||
items.add(
|
||||
PopupMenuItem(
|
||||
value: 3,
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(
|
||||
Platform.isAndroid
|
||||
? Icons.delete_outline
|
||||
: CupertinoIcons.delete,
|
||||
color: Theme.of(context).iconTheme.color,
|
||||
),
|
||||
Padding(
|
||||
padding: EdgeInsets.all(8),
|
||||
),
|
||||
Text("Delete"),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
return items;
|
||||
},
|
||||
onSelected: (value) {
|
||||
if (value == 1) {
|
||||
_download(widget.file);
|
||||
} else if (value == 2) {
|
||||
_showDateTimePicker(widget.file);
|
||||
} else if (value == 3) {
|
||||
_showDeleteSheet(widget.file);
|
||||
}
|
||||
},
|
||||
));
|
||||
);
|
||||
}
|
||||
return items;
|
||||
},
|
||||
onSelected: (value) {
|
||||
if (value == 1) {
|
||||
_download(widget.file);
|
||||
} else if (value == 2) {
|
||||
_showDateTimePicker(widget.file);
|
||||
} else if (value == 3) {
|
||||
_showDeleteSheet(widget.file);
|
||||
}
|
||||
},
|
||||
),
|
||||
);
|
||||
return AppBar(
|
||||
iconTheme: IconThemeData(color: Colors.white), //same for both themes
|
||||
actions: shouldShowActions ? actions : [],
|
||||
|
@ -267,8 +269,11 @@ class FadingAppBarState extends State<FadingAppBar> {
|
|||
theme: Theme.of(context).colorScheme.dateTimePickertheme,
|
||||
);
|
||||
if (dateWithTimeResult != null) {
|
||||
if (await editTime(context, List.of([widget.file]),
|
||||
dateWithTimeResult.microsecondsSinceEpoch)) {
|
||||
if (await editTime(
|
||||
context,
|
||||
List.of([widget.file]),
|
||||
dateWithTimeResult.microsecondsSinceEpoch,
|
||||
)) {
|
||||
widget.file.creationTime = dateWithTimeResult.microsecondsSinceEpoch;
|
||||
setState(() {});
|
||||
}
|
||||
|
@ -278,48 +283,56 @@ class FadingAppBarState extends State<FadingAppBar> {
|
|||
void _showDeleteSheet(File file) {
|
||||
final List<Widget> actions = [];
|
||||
if (file.uploadedFileID == null || file.localID == null) {
|
||||
actions.add(CupertinoActionSheetAction(
|
||||
child: Text("Everywhere"),
|
||||
isDestructiveAction: true,
|
||||
onPressed: () async {
|
||||
await deleteFilesFromEverywhere(context, [file]);
|
||||
Navigator.of(context, rootNavigator: true).pop();
|
||||
widget.onFileDeleted(file);
|
||||
},
|
||||
));
|
||||
actions.add(
|
||||
CupertinoActionSheetAction(
|
||||
child: Text("Everywhere"),
|
||||
isDestructiveAction: true,
|
||||
onPressed: () async {
|
||||
await deleteFilesFromEverywhere(context, [file]);
|
||||
Navigator.of(context, rootNavigator: true).pop();
|
||||
widget.onFileDeleted(file);
|
||||
},
|
||||
),
|
||||
);
|
||||
} else {
|
||||
// uploaded file which is present locally too
|
||||
actions.add(CupertinoActionSheetAction(
|
||||
child: Text("Device"),
|
||||
isDestructiveAction: true,
|
||||
onPressed: () async {
|
||||
await deleteFilesOnDeviceOnly(context, [file]);
|
||||
showToast(context, "File deleted from device");
|
||||
Navigator.of(context, rootNavigator: true).pop();
|
||||
// TODO: Fix behavior when inside a device folder
|
||||
},
|
||||
));
|
||||
actions.add(
|
||||
CupertinoActionSheetAction(
|
||||
child: Text("Device"),
|
||||
isDestructiveAction: true,
|
||||
onPressed: () async {
|
||||
await deleteFilesOnDeviceOnly(context, [file]);
|
||||
showToast(context, "File deleted from device");
|
||||
Navigator.of(context, rootNavigator: true).pop();
|
||||
// TODO: Fix behavior when inside a device folder
|
||||
},
|
||||
),
|
||||
);
|
||||
|
||||
actions.add(CupertinoActionSheetAction(
|
||||
child: Text("ente"),
|
||||
isDestructiveAction: true,
|
||||
onPressed: () async {
|
||||
await deleteFilesFromRemoteOnly(context, [file]);
|
||||
showShortToast(context, "Moved to trash");
|
||||
Navigator.of(context, rootNavigator: true).pop();
|
||||
// TODO: Fix behavior when inside a collection
|
||||
},
|
||||
));
|
||||
actions.add(
|
||||
CupertinoActionSheetAction(
|
||||
child: Text("ente"),
|
||||
isDestructiveAction: true,
|
||||
onPressed: () async {
|
||||
await deleteFilesFromRemoteOnly(context, [file]);
|
||||
showShortToast(context, "Moved to trash");
|
||||
Navigator.of(context, rootNavigator: true).pop();
|
||||
// TODO: Fix behavior when inside a collection
|
||||
},
|
||||
),
|
||||
);
|
||||
|
||||
actions.add(CupertinoActionSheetAction(
|
||||
child: Text("Everywhere"),
|
||||
isDestructiveAction: true,
|
||||
onPressed: () async {
|
||||
await deleteFilesFromEverywhere(context, [file]);
|
||||
Navigator.of(context, rootNavigator: true).pop();
|
||||
widget.onFileDeleted(file);
|
||||
},
|
||||
));
|
||||
actions.add(
|
||||
CupertinoActionSheetAction(
|
||||
child: Text("Everywhere"),
|
||||
isDestructiveAction: true,
|
||||
onPressed: () async {
|
||||
await deleteFilesFromEverywhere(context, [file]);
|
||||
Navigator.of(context, rootNavigator: true).pop();
|
||||
widget.onFileDeleted(file);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
final action = CupertinoActionSheet(
|
||||
title: Text("Delete file?"),
|
||||
|
|
|
@ -192,14 +192,16 @@ class FadingBottomBarState extends State<FadingBottomBar> {
|
|||
final selectedFiles = SelectedFiles();
|
||||
selectedFiles.toggleSelection(widget.file);
|
||||
Navigator.push(
|
||||
context,
|
||||
PageTransition(
|
||||
type: PageTransitionType.bottomToTop,
|
||||
child: CreateCollectionPage(
|
||||
selectedFiles,
|
||||
null,
|
||||
actionType: CollectionActionType.restoreFiles,
|
||||
)));
|
||||
context,
|
||||
PageTransition(
|
||||
type: PageTransitionType.bottomToTop,
|
||||
child: CreateCollectionPage(
|
||||
selectedFiles,
|
||||
null,
|
||||
actionType: CollectionActionType.restoreFiles,
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
|
|
|
@ -134,7 +134,8 @@ class _FileInfoWidgetState extends State<FileInfoWidget> {
|
|||
Padding(padding: EdgeInsets.all(4)),
|
||||
Text(
|
||||
getFormattedTime(
|
||||
DateTime.fromMicrosecondsSinceEpoch(file.updationTime)),
|
||||
DateTime.fromMicrosecondsSinceEpoch(file.updationTime),
|
||||
),
|
||||
style: TextStyle(color: infoColor),
|
||||
),
|
||||
],
|
||||
|
@ -385,8 +386,10 @@ class _FileInfoWidgetState extends State<FileInfoWidget> {
|
|||
children: [
|
||||
Icon(Icons.center_focus_strong_outlined, color: infoColor),
|
||||
Padding(padding: EdgeInsets.all(4)),
|
||||
Text(focalLength.toString() + " mm",
|
||||
style: TextStyle(color: infoColor)),
|
||||
Text(
|
||||
focalLength.toString() + " mm",
|
||||
style: TextStyle(color: infoColor),
|
||||
),
|
||||
],
|
||||
),
|
||||
Padding(padding: EdgeInsets.all(6)),
|
||||
|
|
|
@ -30,8 +30,9 @@ class _FreeSpacePageState extends State<FreeSpacePage> {
|
|||
}
|
||||
|
||||
Widget _getBody() {
|
||||
Logger("FreeSpacePage").info("Number of uploaded files: " +
|
||||
widget.status.localIDs.length.toString());
|
||||
Logger("FreeSpacePage").info(
|
||||
"Number of uploaded files: " + widget.status.localIDs.length.toString(),
|
||||
);
|
||||
Logger("FreeSpacePage")
|
||||
.info("Space consumed: " + widget.status.size.toString());
|
||||
return _getWidget(widget.status);
|
||||
|
|
|
@ -79,11 +79,13 @@ class _GalleryState extends State<Gallery> {
|
|||
}
|
||||
if (widget.forceReloadEvents != null) {
|
||||
for (final event in widget.forceReloadEvents) {
|
||||
_forceReloadEventSubscriptions.add(event.listen((event) async {
|
||||
_logger.info("Force reload triggered");
|
||||
final result = await _loadFiles();
|
||||
_setFilesAndReload(result.files);
|
||||
}));
|
||||
_forceReloadEventSubscriptions.add(
|
||||
event.listen((event) async {
|
||||
_logger.info("Force reload triggered");
|
||||
final result = await _loadFiles();
|
||||
_setFilesAndReload(result.files);
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
if (widget.initialFiles != null) {
|
||||
|
@ -111,15 +113,19 @@ class _GalleryState extends State<Gallery> {
|
|||
try {
|
||||
final startTime = DateTime.now().microsecondsSinceEpoch;
|
||||
final result = await widget.asyncLoader(
|
||||
kGalleryLoadStartTime, DateTime.now().microsecondsSinceEpoch,
|
||||
limit: limit);
|
||||
kGalleryLoadStartTime,
|
||||
DateTime.now().microsecondsSinceEpoch,
|
||||
limit: limit,
|
||||
);
|
||||
final endTime = DateTime.now().microsecondsSinceEpoch;
|
||||
final duration = Duration(microseconds: endTime - startTime);
|
||||
_logger.info("Time taken to load " +
|
||||
result.files.length.toString() +
|
||||
" files :" +
|
||||
duration.inMilliseconds.toString() +
|
||||
"ms");
|
||||
_logger.info(
|
||||
"Time taken to load " +
|
||||
result.files.length.toString() +
|
||||
" files :" +
|
||||
duration.inMilliseconds.toString() +
|
||||
"ms",
|
||||
);
|
||||
return result;
|
||||
} catch (e, s) {
|
||||
_logger.severe("failed to load files", e, s);
|
||||
|
@ -178,10 +184,13 @@ class _GalleryState extends State<Gallery> {
|
|||
if (widget.header != null) {
|
||||
children.add(widget.header);
|
||||
}
|
||||
children.add(Expanded(
|
||||
child: nothingToSeeHere(
|
||||
textColor: Theme.of(context).colorScheme.defaultTextColor),
|
||||
));
|
||||
children.add(
|
||||
Expanded(
|
||||
child: nothingToSeeHere(
|
||||
textColor: Theme.of(context).colorScheme.defaultTextColor,
|
||||
),
|
||||
),
|
||||
);
|
||||
if (widget.footer != null) {
|
||||
children.add(widget.footer);
|
||||
}
|
||||
|
@ -215,8 +224,11 @@ class _GalleryState extends State<Gallery> {
|
|||
return gallery;
|
||||
},
|
||||
labelTextBuilder: (int index) {
|
||||
return getMonthAndYear(DateTime.fromMicrosecondsSinceEpoch(
|
||||
_collatedFiles[index][0].creationTime));
|
||||
return getMonthAndYear(
|
||||
DateTime.fromMicrosecondsSinceEpoch(
|
||||
_collatedFiles[index][0].creationTime,
|
||||
),
|
||||
);
|
||||
},
|
||||
thumbBackgroundColor:
|
||||
Theme.of(context).colorScheme.galleryThumbBackgroundColor,
|
||||
|
@ -234,7 +246,9 @@ class _GalleryState extends State<Gallery> {
|
|||
for (int index = 0; index < files.length; index++) {
|
||||
if (index > 0 &&
|
||||
!_areFromSameDay(
|
||||
files[index - 1].creationTime, files[index].creationTime)) {
|
||||
files[index - 1].creationTime,
|
||||
files[index].creationTime,
|
||||
)) {
|
||||
final List<File> collatedDailyFiles = [];
|
||||
collatedDailyFiles.addAll(dailyFiles);
|
||||
collatedFiles.add(collatedDailyFiles);
|
||||
|
|
|
@ -135,56 +135,59 @@ class _GalleryAppBarWidgetState extends State<GalleryAppBarWidget> {
|
|||
);
|
||||
}
|
||||
if (widget.type == GalleryType.owned_collection) {
|
||||
actions.add(PopupMenuButton(
|
||||
itemBuilder: (context) {
|
||||
final List<PopupMenuItem> items = [];
|
||||
if (widget.collection.type == CollectionType.album) {
|
||||
actions.add(
|
||||
PopupMenuButton(
|
||||
itemBuilder: (context) {
|
||||
final List<PopupMenuItem> items = [];
|
||||
if (widget.collection.type == CollectionType.album) {
|
||||
items.add(
|
||||
PopupMenuItem(
|
||||
value: 1,
|
||||
child: Row(
|
||||
children: const [
|
||||
Icon(Icons.edit),
|
||||
Padding(
|
||||
padding: EdgeInsets.all(8),
|
||||
),
|
||||
Text("Rename"),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
bool isArchived = widget.collection.isArchived();
|
||||
items.add(
|
||||
PopupMenuItem(
|
||||
value: 1,
|
||||
value: 2,
|
||||
child: Row(
|
||||
children: const [
|
||||
Icon(Icons.edit),
|
||||
children: [
|
||||
Icon(isArchived ? Icons.visibility : Icons.visibility_off),
|
||||
Padding(
|
||||
padding: EdgeInsets.all(8),
|
||||
),
|
||||
Text("Rename"),
|
||||
Text(isArchived ? "Unhide" : "Hide"),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
bool isArchived = widget.collection.isArchived();
|
||||
items.add(
|
||||
PopupMenuItem(
|
||||
value: 2,
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(isArchived ? Icons.visibility : Icons.visibility_off),
|
||||
Padding(
|
||||
padding: EdgeInsets.all(8),
|
||||
),
|
||||
Text(isArchived ? "Unhide" : "Hide"),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
return items;
|
||||
},
|
||||
onSelected: (value) async {
|
||||
if (value == 1) {
|
||||
await _renameAlbum(context);
|
||||
}
|
||||
if (value == 2) {
|
||||
await changeCollectionVisibility(
|
||||
return items;
|
||||
},
|
||||
onSelected: (value) async {
|
||||
if (value == 1) {
|
||||
await _renameAlbum(context);
|
||||
}
|
||||
if (value == 2) {
|
||||
await changeCollectionVisibility(
|
||||
context,
|
||||
widget.collection,
|
||||
widget.collection.isArchived()
|
||||
? kVisibilityVisible
|
||||
: kVisibilityArchive);
|
||||
}
|
||||
},
|
||||
));
|
||||
: kVisibilityArchive,
|
||||
);
|
||||
}
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
return actions;
|
||||
}
|
||||
|
@ -200,7 +203,8 @@ class _GalleryAppBarWidgetState extends State<GalleryAppBarWidget> {
|
|||
await CollectionsService.instance.getOrCreateForPath(widget.path);
|
||||
} else {
|
||||
throw Exception(
|
||||
"Cannot create a collection of type" + widget.type.toString());
|
||||
"Cannot create a collection of type" + widget.type.toString(),
|
||||
);
|
||||
}
|
||||
} else {
|
||||
final sharees =
|
||||
|
|
|
@ -27,9 +27,13 @@ class GalleryOverlayWidget extends StatefulWidget {
|
|||
final SelectedFiles selectedFiles;
|
||||
final String path;
|
||||
final Collection collection;
|
||||
const GalleryOverlayWidget(this.type, this.selectedFiles,
|
||||
{this.path, this.collection, Key key})
|
||||
: super(key: key);
|
||||
const GalleryOverlayWidget(
|
||||
this.type,
|
||||
this.selectedFiles, {
|
||||
this.path,
|
||||
this.collection,
|
||||
Key key,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
State<GalleryOverlayWidget> createState() => _GalleryOverlayWidgetState();
|
||||
|
@ -216,25 +220,29 @@ class _OverlayWidgetState extends State<OverlayWidget> {
|
|||
|
||||
Future<void> _createAlbum() async {
|
||||
Navigator.push(
|
||||
context,
|
||||
PageTransition(
|
||||
type: PageTransitionType.bottomToTop,
|
||||
child: CreateCollectionPage(
|
||||
widget.selectedFiles,
|
||||
null,
|
||||
)));
|
||||
context,
|
||||
PageTransition(
|
||||
type: PageTransitionType.bottomToTop,
|
||||
child: CreateCollectionPage(
|
||||
widget.selectedFiles,
|
||||
null,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _moveFiles() async {
|
||||
Navigator.push(
|
||||
context,
|
||||
PageTransition(
|
||||
type: PageTransitionType.bottomToTop,
|
||||
child: CreateCollectionPage(
|
||||
widget.selectedFiles,
|
||||
null,
|
||||
actionType: CollectionActionType.moveFiles,
|
||||
)));
|
||||
context,
|
||||
PageTransition(
|
||||
type: PageTransitionType.bottomToTop,
|
||||
child: CreateCollectionPage(
|
||||
widget.selectedFiles,
|
||||
null,
|
||||
actionType: CollectionActionType.moveFiles,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
List<Widget> _getActions(BuildContext context) {
|
||||
|
@ -276,9 +284,11 @@ class _OverlayWidgetState extends State<OverlayWidget> {
|
|||
message: "Move",
|
||||
child: IconButton(
|
||||
color: Theme.of(context).colorScheme.iconColor,
|
||||
icon: Icon(Platform.isAndroid
|
||||
? Icons.arrow_forward
|
||||
: CupertinoIcons.arrow_right),
|
||||
icon: Icon(
|
||||
Platform.isAndroid
|
||||
? Icons.arrow_forward
|
||||
: CupertinoIcons.arrow_right,
|
||||
),
|
||||
onPressed: () {
|
||||
_moveFiles();
|
||||
},
|
||||
|
@ -352,44 +362,52 @@ class _OverlayWidgetState extends State<OverlayWidget> {
|
|||
if (widget.type == GalleryType.homepage ||
|
||||
widget.type == GalleryType.archive) {
|
||||
bool showArchive = widget.type == GalleryType.homepage;
|
||||
actions.add(Tooltip(
|
||||
message: showArchive ? "Hide" : "Unhide",
|
||||
child: IconButton(
|
||||
color: Theme.of(context).colorScheme.iconColor,
|
||||
icon: Icon(
|
||||
showArchive ? Icons.visibility_off : Icons.visibility,
|
||||
actions.add(
|
||||
Tooltip(
|
||||
message: showArchive ? "Hide" : "Unhide",
|
||||
child: IconButton(
|
||||
color: Theme.of(context).colorScheme.iconColor,
|
||||
icon: Icon(
|
||||
showArchive ? Icons.visibility_off : Icons.visibility,
|
||||
),
|
||||
onPressed: () {
|
||||
_handleVisibilityChangeRequest(
|
||||
context,
|
||||
showArchive ? kVisibilityArchive : kVisibilityVisible,
|
||||
);
|
||||
},
|
||||
),
|
||||
onPressed: () {
|
||||
_handleVisibilityChangeRequest(
|
||||
context, showArchive ? kVisibilityArchive : kVisibilityVisible);
|
||||
},
|
||||
),
|
||||
));
|
||||
);
|
||||
}
|
||||
return actions;
|
||||
}
|
||||
|
||||
void _addTrashAction(List<Widget> actions) {
|
||||
actions.add(Tooltip(
|
||||
message: "Restore",
|
||||
child: IconButton(
|
||||
color: Theme.of(context).colorScheme.iconColor,
|
||||
icon: Icon(
|
||||
Icons.restore,
|
||||
),
|
||||
onPressed: () {
|
||||
Navigator.push(
|
||||
actions.add(
|
||||
Tooltip(
|
||||
message: "Restore",
|
||||
child: IconButton(
|
||||
color: Theme.of(context).colorScheme.iconColor,
|
||||
icon: Icon(
|
||||
Icons.restore,
|
||||
),
|
||||
onPressed: () {
|
||||
Navigator.push(
|
||||
context,
|
||||
PageTransition(
|
||||
type: PageTransitionType.bottomToTop,
|
||||
child: CreateCollectionPage(
|
||||
widget.selectedFiles,
|
||||
null,
|
||||
actionType: CollectionActionType.restoreFiles,
|
||||
)));
|
||||
},
|
||||
type: PageTransitionType.bottomToTop,
|
||||
child: CreateCollectionPage(
|
||||
widget.selectedFiles,
|
||||
null,
|
||||
actionType: CollectionActionType.restoreFiles,
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
));
|
||||
);
|
||||
actions.add(
|
||||
Tooltip(
|
||||
message: "Delete permanently",
|
||||
|
@ -400,7 +418,9 @@ class _OverlayWidgetState extends State<OverlayWidget> {
|
|||
),
|
||||
onPressed: () async {
|
||||
if (await deleteFromTrash(
|
||||
context, widget.selectedFiles.files.toList())) {
|
||||
context,
|
||||
widget.selectedFiles.files.toList(),
|
||||
)) {
|
||||
_clearSelectedFiles();
|
||||
}
|
||||
},
|
||||
|
@ -410,10 +430,15 @@ class _OverlayWidgetState extends State<OverlayWidget> {
|
|||
}
|
||||
|
||||
Future<void> _handleVisibilityChangeRequest(
|
||||
BuildContext context, int newVisibility) async {
|
||||
BuildContext context,
|
||||
int newVisibility,
|
||||
) async {
|
||||
try {
|
||||
await changeVisibility(
|
||||
context, widget.selectedFiles.files.toList(), newVisibility);
|
||||
context,
|
||||
widget.selectedFiles.files.toList(),
|
||||
newVisibility,
|
||||
);
|
||||
} catch (e, s) {
|
||||
_logger.severe("failed to update file visibility", e, s);
|
||||
await showGenericErrorDialog(context);
|
||||
|
@ -423,8 +448,11 @@ class _OverlayWidgetState extends State<OverlayWidget> {
|
|||
}
|
||||
|
||||
void _shareSelected(BuildContext context) {
|
||||
share(context, widget.selectedFiles.files.toList(),
|
||||
shareButtonKey: shareButtonKey);
|
||||
share(
|
||||
context,
|
||||
widget.selectedFiles.files.toList(),
|
||||
shareButtonKey: shareButtonKey,
|
||||
);
|
||||
}
|
||||
|
||||
void _showDeleteSheet(BuildContext context) {
|
||||
|
@ -440,56 +468,74 @@ class _OverlayWidgetState extends State<OverlayWidget> {
|
|||
}
|
||||
final actions = <Widget>[];
|
||||
if (containsUploadedFile && containsLocalFile) {
|
||||
actions.add(CupertinoActionSheetAction(
|
||||
child: Text("Device"),
|
||||
isDestructiveAction: true,
|
||||
onPressed: () async {
|
||||
Navigator.of(context, rootNavigator: true).pop();
|
||||
await deleteFilesOnDeviceOnly(
|
||||
context, widget.selectedFiles.files.toList());
|
||||
_clearSelectedFiles();
|
||||
showToast(context, "Files deleted from device");
|
||||
},
|
||||
));
|
||||
actions.add(CupertinoActionSheetAction(
|
||||
child: Text("ente"),
|
||||
isDestructiveAction: true,
|
||||
onPressed: () async {
|
||||
Navigator.of(context, rootNavigator: true).pop();
|
||||
await deleteFilesFromRemoteOnly(
|
||||
context, widget.selectedFiles.files.toList());
|
||||
_clearSelectedFiles();
|
||||
showShortToast(context, "Moved to trash");
|
||||
},
|
||||
));
|
||||
actions.add(CupertinoActionSheetAction(
|
||||
child: Text("Everywhere"),
|
||||
isDestructiveAction: true,
|
||||
onPressed: () async {
|
||||
Navigator.of(context, rootNavigator: true).pop();
|
||||
await deleteFilesFromEverywhere(
|
||||
context, widget.selectedFiles.files.toList());
|
||||
_clearSelectedFiles();
|
||||
},
|
||||
));
|
||||
actions.add(
|
||||
CupertinoActionSheetAction(
|
||||
child: Text("Device"),
|
||||
isDestructiveAction: true,
|
||||
onPressed: () async {
|
||||
Navigator.of(context, rootNavigator: true).pop();
|
||||
await deleteFilesOnDeviceOnly(
|
||||
context,
|
||||
widget.selectedFiles.files.toList(),
|
||||
);
|
||||
_clearSelectedFiles();
|
||||
showToast(context, "Files deleted from device");
|
||||
},
|
||||
),
|
||||
);
|
||||
actions.add(
|
||||
CupertinoActionSheetAction(
|
||||
child: Text("ente"),
|
||||
isDestructiveAction: true,
|
||||
onPressed: () async {
|
||||
Navigator.of(context, rootNavigator: true).pop();
|
||||
await deleteFilesFromRemoteOnly(
|
||||
context,
|
||||
widget.selectedFiles.files.toList(),
|
||||
);
|
||||
_clearSelectedFiles();
|
||||
showShortToast(context, "Moved to trash");
|
||||
},
|
||||
),
|
||||
);
|
||||
actions.add(
|
||||
CupertinoActionSheetAction(
|
||||
child: Text("Everywhere"),
|
||||
isDestructiveAction: true,
|
||||
onPressed: () async {
|
||||
Navigator.of(context, rootNavigator: true).pop();
|
||||
await deleteFilesFromEverywhere(
|
||||
context,
|
||||
widget.selectedFiles.files.toList(),
|
||||
);
|
||||
_clearSelectedFiles();
|
||||
},
|
||||
),
|
||||
);
|
||||
} else {
|
||||
actions.add(CupertinoActionSheetAction(
|
||||
child: Text("Delete"),
|
||||
isDestructiveAction: true,
|
||||
onPressed: () async {
|
||||
Navigator.of(context, rootNavigator: true).pop();
|
||||
await deleteFilesFromEverywhere(
|
||||
context, widget.selectedFiles.files.toList());
|
||||
_clearSelectedFiles();
|
||||
},
|
||||
));
|
||||
actions.add(
|
||||
CupertinoActionSheetAction(
|
||||
child: Text("Delete"),
|
||||
isDestructiveAction: true,
|
||||
onPressed: () async {
|
||||
Navigator.of(context, rootNavigator: true).pop();
|
||||
await deleteFilesFromEverywhere(
|
||||
context,
|
||||
widget.selectedFiles.files.toList(),
|
||||
);
|
||||
_clearSelectedFiles();
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
final action = CupertinoActionSheet(
|
||||
title: Text("Delete " +
|
||||
count.toString() +
|
||||
" file" +
|
||||
(count == 1 ? "" : "s") +
|
||||
(containsUploadedFile && containsLocalFile ? " from" : "?")),
|
||||
title: Text(
|
||||
"Delete " +
|
||||
count.toString() +
|
||||
" file" +
|
||||
(count == 1 ? "" : "s") +
|
||||
(containsUploadedFile && containsLocalFile ? " from" : "?"),
|
||||
),
|
||||
actions: actions,
|
||||
cancelButton: CupertinoActionSheetAction(
|
||||
child: Text("Cancel"),
|
||||
|
@ -508,13 +554,15 @@ class _OverlayWidgetState extends State<OverlayWidget> {
|
|||
void _showRemoveFromCollectionSheet(BuildContext context) {
|
||||
final count = widget.selectedFiles.files.length;
|
||||
final action = CupertinoActionSheet(
|
||||
title: Text("Remove " +
|
||||
count.toString() +
|
||||
" file" +
|
||||
(count == 1 ? "" : "s") +
|
||||
" from " +
|
||||
widget.collection.name +
|
||||
"?"),
|
||||
title: Text(
|
||||
"Remove " +
|
||||
count.toString() +
|
||||
" file" +
|
||||
(count == 1 ? "" : "s") +
|
||||
" from " +
|
||||
widget.collection.name +
|
||||
"?",
|
||||
),
|
||||
actions: <Widget>[
|
||||
CupertinoActionSheetAction(
|
||||
child: Text("Remove"),
|
||||
|
@ -525,7 +573,9 @@ class _OverlayWidgetState extends State<OverlayWidget> {
|
|||
await dialog.show();
|
||||
try {
|
||||
await CollectionsService.instance.removeFromCollection(
|
||||
widget.collection.id, widget.selectedFiles.files.toList());
|
||||
widget.collection.id,
|
||||
widget.selectedFiles.files.toList(),
|
||||
);
|
||||
await dialog.hide();
|
||||
widget.selectedFiles.clearAll();
|
||||
} catch (e, s) {
|
||||
|
|
|
@ -47,28 +47,34 @@ class GrantPermissionsWidget extends StatelessWidget {
|
|||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 40),
|
||||
child: RichText(
|
||||
text: TextSpan(
|
||||
text: TextSpan(
|
||||
style: Theme.of(context)
|
||||
.textTheme
|
||||
.headline5
|
||||
.copyWith(fontWeight: FontWeight.w700),
|
||||
children: [
|
||||
TextSpan(text: 'ente '),
|
||||
TextSpan(
|
||||
text: "needs permission to ",
|
||||
style: Theme.of(context)
|
||||
.textTheme
|
||||
.headline5
|
||||
.copyWith(fontWeight: FontWeight.w700),
|
||||
children: [
|
||||
TextSpan(text: 'ente '),
|
||||
TextSpan(
|
||||
text: "needs permission to ",
|
||||
style: Theme.of(context)
|
||||
.textTheme
|
||||
.headline5
|
||||
.copyWith(fontWeight: FontWeight.w400)),
|
||||
.copyWith(fontWeight: FontWeight.w400),
|
||||
),
|
||||
TextSpan(text: 'preserve your photos')
|
||||
])),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
Container(
|
||||
width: double.infinity,
|
||||
padding: EdgeInsets.only(
|
||||
left: 20, right: 20, bottom: Platform.isIOS ? 84 : 60),
|
||||
left: 20,
|
||||
right: 20,
|
||||
bottom: Platform.isIOS ? 84 : 60,
|
||||
),
|
||||
child: OutlinedButton(
|
||||
child: Text("Grant permission"),
|
||||
onPressed: () async {
|
||||
|
@ -80,7 +86,8 @@ class GrantPermissionsWidget extends StatelessWidget {
|
|||
AlertDialog alert = AlertDialog(
|
||||
title: Text("Please grant permissions"),
|
||||
content: Text(
|
||||
"ente can encrypt and preserve files only if you grant access to them"),
|
||||
"ente can encrypt and preserve files only if you grant access to them",
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
child: Text(
|
||||
|
@ -89,8 +96,9 @@ class GrantPermissionsWidget extends StatelessWidget {
|
|||
.textTheme
|
||||
.subtitle1
|
||||
.copyWith(
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w700),
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w700,
|
||||
),
|
||||
),
|
||||
onPressed: () {
|
||||
Navigator.of(context, rootNavigator: true)
|
||||
|
|
|
@ -183,7 +183,8 @@ class _HomeWidgetState extends State<HomeWidget> {
|
|||
context: context,
|
||||
builder: (BuildContext context) {
|
||||
return AppUpdateDialog(
|
||||
UpdateService.instance.getLatestVersionInfo());
|
||||
UpdateService.instance.getLatestVersionInfo(),
|
||||
);
|
||||
},
|
||||
barrierColor: Colors.black.withOpacity(0.85),
|
||||
);
|
||||
|
@ -209,14 +210,17 @@ class _HomeWidgetState extends State<HomeWidget> {
|
|||
}
|
||||
|
||||
void _initMediaShareSubscription() {
|
||||
_intentDataStreamSubscription = ReceiveSharingIntent.getMediaStream()
|
||||
.listen((List<SharedMediaFile> value) {
|
||||
setState(() {
|
||||
_sharedFiles = value;
|
||||
});
|
||||
}, onError: (err) {
|
||||
_logger.severe("getIntentDataStream error: $err");
|
||||
});
|
||||
_intentDataStreamSubscription =
|
||||
ReceiveSharingIntent.getMediaStream().listen(
|
||||
(List<SharedMediaFile> value) {
|
||||
setState(() {
|
||||
_sharedFiles = value;
|
||||
});
|
||||
},
|
||||
onError: (err) {
|
||||
_logger.severe("getIntentDataStream error: $err");
|
||||
},
|
||||
);
|
||||
// For sharing images coming from outside the app while the app is closed
|
||||
ReceiveSharingIntent.getInitialMedia().then((List<SharedMediaFile> value) {
|
||||
setState(() {
|
||||
|
@ -283,10 +287,12 @@ class _HomeWidgetState extends State<HomeWidget> {
|
|||
_settingsPage,
|
||||
],
|
||||
onPageChanged: (page) {
|
||||
Bus.instance.fire(TabChangedEvent(
|
||||
page,
|
||||
TabChangedEventSource.page_view,
|
||||
));
|
||||
Bus.instance.fire(
|
||||
TabChangedEvent(
|
||||
page,
|
||||
TabChangedEventSource.page_view,
|
||||
),
|
||||
);
|
||||
},
|
||||
physics: NeverScrollableScrollPhysics(),
|
||||
controller: _pageController,
|
||||
|
@ -330,12 +336,15 @@ class _HomeWidgetState extends State<HomeWidget> {
|
|||
}
|
||||
|
||||
// Attach a listener to the stream
|
||||
linkStream.listen((String link) {
|
||||
_logger.info("Link received: " + link);
|
||||
_getCredentials(context, link);
|
||||
}, onError: (err) {
|
||||
_logger.severe(err);
|
||||
});
|
||||
linkStream.listen(
|
||||
(String link) {
|
||||
_logger.info("Link received: " + link);
|
||||
_getCredentials(context, link);
|
||||
},
|
||||
onError: (err) {
|
||||
_logger.severe(err);
|
||||
},
|
||||
);
|
||||
return false;
|
||||
}
|
||||
|
||||
|
@ -362,31 +371,43 @@ class _HomeWidgetState extends State<HomeWidget> {
|
|||
CollectionsService.instance.getArchivedCollections();
|
||||
FileLoadResult result;
|
||||
if (importantPaths.isNotEmpty) {
|
||||
result = await FilesDB.instance.getImportantFiles(creationStartTime,
|
||||
creationEndTime, ownerID, importantPaths.toList(),
|
||||
limit: limit,
|
||||
asc: asc,
|
||||
ignoredCollectionIDs: archivedCollectionIds);
|
||||
result = await FilesDB.instance.getImportantFiles(
|
||||
creationStartTime,
|
||||
creationEndTime,
|
||||
ownerID,
|
||||
importantPaths.toList(),
|
||||
limit: limit,
|
||||
asc: asc,
|
||||
ignoredCollectionIDs: archivedCollectionIds,
|
||||
);
|
||||
} else {
|
||||
if (LocalSyncService.instance.hasGrantedLimitedPermissions()) {
|
||||
result = await FilesDB.instance.getAllLocalAndUploadedFiles(
|
||||
creationStartTime, creationEndTime, ownerID,
|
||||
limit: limit,
|
||||
asc: asc,
|
||||
ignoredCollectionIDs: archivedCollectionIds);
|
||||
creationStartTime,
|
||||
creationEndTime,
|
||||
ownerID,
|
||||
limit: limit,
|
||||
asc: asc,
|
||||
ignoredCollectionIDs: archivedCollectionIds,
|
||||
);
|
||||
} else {
|
||||
result = await FilesDB.instance.getAllUploadedFiles(
|
||||
creationStartTime, creationEndTime, ownerID,
|
||||
limit: limit,
|
||||
asc: asc,
|
||||
ignoredCollectionIDs: archivedCollectionIds);
|
||||
creationStartTime,
|
||||
creationEndTime,
|
||||
ownerID,
|
||||
limit: limit,
|
||||
asc: asc,
|
||||
ignoredCollectionIDs: archivedCollectionIds,
|
||||
);
|
||||
}
|
||||
}
|
||||
// hide ignored files from home page UI
|
||||
final ignoredIDs = await IgnoredFilesService.instance.ignoredIDs;
|
||||
result.files.removeWhere((f) =>
|
||||
f.uploadedFileID == null &&
|
||||
IgnoredFilesService.instance.shouldSkipUpload(ignoredIDs, f));
|
||||
result.files.removeWhere(
|
||||
(f) =>
|
||||
f.uploadedFileID == null &&
|
||||
IgnoredFilesService.instance.shouldSkipUpload(ignoredIDs, f),
|
||||
);
|
||||
return result;
|
||||
},
|
||||
reloadEvent: Bus.instance.on<LocalPhotosUpdatedEvent>(),
|
||||
|
@ -426,11 +447,13 @@ class _HomeWidgetState extends State<HomeWidget> {
|
|||
height: 206,
|
||||
),
|
||||
),
|
||||
Text('No photos are being backed up right now',
|
||||
style: Theme.of(context)
|
||||
.textTheme
|
||||
.caption
|
||||
.copyWith(fontFamily: 'Inter-Medium', fontSize: 16)),
|
||||
Text(
|
||||
'No photos are being backed up right now',
|
||||
style: Theme.of(context)
|
||||
.textTheme
|
||||
.caption
|
||||
.copyWith(fontFamily: 'Inter-Medium', fontSize: 16),
|
||||
),
|
||||
Center(
|
||||
child: Hero(
|
||||
tag: "select_folders",
|
||||
|
@ -560,10 +583,12 @@ class _HomeBottomNavigationBarState extends State<HomeBottomNavigationBar> {
|
|||
}
|
||||
|
||||
void _onTabChange(int index) {
|
||||
Bus.instance.fire(TabChangedEvent(
|
||||
index,
|
||||
TabChangedEventSource.tab_bar,
|
||||
));
|
||||
Bus.instance.fire(
|
||||
TabChangedEvent(
|
||||
index,
|
||||
TabChangedEventSource.tab_bar,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
|
@ -580,111 +605,112 @@ class _HomeBottomNavigationBarState extends State<HomeBottomNavigationBar> {
|
|||
child: IgnorePointer(
|
||||
ignoring: filesAreSelected,
|
||||
child: ListView(
|
||||
physics: const NeverScrollableScrollPhysics(),
|
||||
children: [
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
ClipRRect(
|
||||
borderRadius: BorderRadius.circular(36),
|
||||
child: Container(
|
||||
alignment: Alignment.bottomCenter,
|
||||
height: 52,
|
||||
width: 240,
|
||||
child: ClipRect(
|
||||
child: BackdropFilter(
|
||||
filter: ImageFilter.blur(sigmaX: 20, sigmaY: 20),
|
||||
child: GNav(
|
||||
curve: Curves.easeOutExpo,
|
||||
backgroundColor: Theme.of(context)
|
||||
.colorScheme
|
||||
.gNavBackgroundColor,
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
rippleColor: Colors.white.withOpacity(0.1),
|
||||
activeColor: Theme.of(context)
|
||||
.colorScheme
|
||||
.gNavBarActiveColor,
|
||||
iconSize: 24,
|
||||
padding: EdgeInsets.fromLTRB(16, 8, 16, 8),
|
||||
duration: Duration(milliseconds: 200),
|
||||
gap: 0,
|
||||
tabBorderRadius: 24,
|
||||
tabBackgroundColor: Theme.of(context)
|
||||
.colorScheme
|
||||
.gNavBarActiveColor,
|
||||
haptic: false,
|
||||
tabs: [
|
||||
GButton(
|
||||
margin: EdgeInsets.fromLTRB(6, 6, 0, 6),
|
||||
icon: Icons.home,
|
||||
iconColor: Theme.of(context)
|
||||
.colorScheme
|
||||
.gNavIconColor,
|
||||
iconActiveColor: Theme.of(context)
|
||||
.colorScheme
|
||||
.gNavActiveIconColor,
|
||||
text: '',
|
||||
onPressed: () {
|
||||
_onTabChange(
|
||||
0); // To take care of occasional missing events
|
||||
},
|
||||
),
|
||||
GButton(
|
||||
margin: EdgeInsets.fromLTRB(0, 6, 0, 6),
|
||||
icon: Icons.photo_library,
|
||||
iconColor: Theme.of(context)
|
||||
.colorScheme
|
||||
.gNavIconColor,
|
||||
iconActiveColor: Theme.of(context)
|
||||
.colorScheme
|
||||
.gNavActiveIconColor,
|
||||
text: '',
|
||||
onPressed: () {
|
||||
_onTabChange(
|
||||
1); // To take care of occasional missing events
|
||||
},
|
||||
),
|
||||
GButton(
|
||||
margin: EdgeInsets.fromLTRB(0, 6, 0, 6),
|
||||
icon: Icons.folder_shared,
|
||||
iconColor: Theme.of(context)
|
||||
.colorScheme
|
||||
.gNavIconColor,
|
||||
iconActiveColor: Theme.of(context)
|
||||
.colorScheme
|
||||
.gNavActiveIconColor,
|
||||
text: '',
|
||||
onPressed: () {
|
||||
_onTabChange(
|
||||
2); // To take care of occasional missing events
|
||||
},
|
||||
),
|
||||
GButton(
|
||||
margin: EdgeInsets.fromLTRB(0, 6, 6, 6),
|
||||
icon: Icons.person,
|
||||
iconColor: Theme.of(context)
|
||||
.colorScheme
|
||||
.gNavIconColor,
|
||||
iconActiveColor: Theme.of(context)
|
||||
.colorScheme
|
||||
.gNavActiveIconColor,
|
||||
text: '',
|
||||
onPressed: () {
|
||||
_onTabChange(
|
||||
3); // To take care of occasional missing events
|
||||
},
|
||||
)
|
||||
],
|
||||
selectedIndex: currentTabIndex,
|
||||
onTabChange: _onTabChange,
|
||||
),
|
||||
physics: const NeverScrollableScrollPhysics(),
|
||||
children: [
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
ClipRRect(
|
||||
borderRadius: BorderRadius.circular(36),
|
||||
child: Container(
|
||||
alignment: Alignment.bottomCenter,
|
||||
height: 52,
|
||||
width: 240,
|
||||
child: ClipRect(
|
||||
child: BackdropFilter(
|
||||
filter: ImageFilter.blur(sigmaX: 20, sigmaY: 20),
|
||||
child: GNav(
|
||||
curve: Curves.easeOutExpo,
|
||||
backgroundColor: Theme.of(context)
|
||||
.colorScheme
|
||||
.gNavBackgroundColor,
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
rippleColor: Colors.white.withOpacity(0.1),
|
||||
activeColor: Theme.of(context)
|
||||
.colorScheme
|
||||
.gNavBarActiveColor,
|
||||
iconSize: 24,
|
||||
padding: EdgeInsets.fromLTRB(16, 8, 16, 8),
|
||||
duration: Duration(milliseconds: 200),
|
||||
gap: 0,
|
||||
tabBorderRadius: 24,
|
||||
tabBackgroundColor: Theme.of(context)
|
||||
.colorScheme
|
||||
.gNavBarActiveColor,
|
||||
haptic: false,
|
||||
tabs: [
|
||||
GButton(
|
||||
margin: EdgeInsets.fromLTRB(6, 6, 0, 6),
|
||||
icon: Icons.home,
|
||||
iconColor:
|
||||
Theme.of(context).colorScheme.gNavIconColor,
|
||||
iconActiveColor: Theme.of(context)
|
||||
.colorScheme
|
||||
.gNavActiveIconColor,
|
||||
text: '',
|
||||
onPressed: () {
|
||||
_onTabChange(
|
||||
0,
|
||||
); // To take care of occasional missing events
|
||||
},
|
||||
),
|
||||
GButton(
|
||||
margin: EdgeInsets.fromLTRB(0, 6, 0, 6),
|
||||
icon: Icons.photo_library,
|
||||
iconColor:
|
||||
Theme.of(context).colorScheme.gNavIconColor,
|
||||
iconActiveColor: Theme.of(context)
|
||||
.colorScheme
|
||||
.gNavActiveIconColor,
|
||||
text: '',
|
||||
onPressed: () {
|
||||
_onTabChange(
|
||||
1,
|
||||
); // To take care of occasional missing events
|
||||
},
|
||||
),
|
||||
GButton(
|
||||
margin: EdgeInsets.fromLTRB(0, 6, 0, 6),
|
||||
icon: Icons.folder_shared,
|
||||
iconColor:
|
||||
Theme.of(context).colorScheme.gNavIconColor,
|
||||
iconActiveColor: Theme.of(context)
|
||||
.colorScheme
|
||||
.gNavActiveIconColor,
|
||||
text: '',
|
||||
onPressed: () {
|
||||
_onTabChange(
|
||||
2,
|
||||
); // To take care of occasional missing events
|
||||
},
|
||||
),
|
||||
GButton(
|
||||
margin: EdgeInsets.fromLTRB(0, 6, 6, 6),
|
||||
icon: Icons.person,
|
||||
iconColor:
|
||||
Theme.of(context).colorScheme.gNavIconColor,
|
||||
iconActiveColor: Theme.of(context)
|
||||
.colorScheme
|
||||
.gNavActiveIconColor,
|
||||
text: '',
|
||||
onPressed: () {
|
||||
_onTabChange(
|
||||
3,
|
||||
); // To take care of occasional missing events
|
||||
},
|
||||
)
|
||||
],
|
||||
selectedIndex: currentTabIndex,
|
||||
onTabChange: _onTabChange,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
]),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
|
|
@ -62,9 +62,10 @@ class DraggableScrollbarState extends State<DraggableScrollbar>
|
|||
|
||||
if (widget.initialScrollIndex > 0 && widget.totalCount > 1) {
|
||||
WidgetsBinding.instance?.addPostFrameCallback((_) {
|
||||
setState(() => thumbOffset =
|
||||
(widget.initialScrollIndex / widget.totalCount) *
|
||||
(thumbMax - thumbMin));
|
||||
setState(
|
||||
() => thumbOffset = (widget.initialScrollIndex / widget.totalCount) *
|
||||
(thumbMax - thumbMin),
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -121,7 +122,6 @@ class DraggableScrollbarState extends State<DraggableScrollbar>
|
|||
} else {
|
||||
return buildThumb();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Widget buildThumb() => Container(
|
||||
|
@ -191,25 +191,33 @@ class DraggableScrollbarState extends State<DraggableScrollbar>
|
|||
void keyHandler(RawKeyEvent value) {
|
||||
if (value.runtimeType == RawKeyDownEvent) {
|
||||
if (value.logicalKey == LogicalKeyboardKey.arrowDown) {
|
||||
onDragUpdate(DragUpdateDetails(
|
||||
globalPosition: Offset.zero,
|
||||
delta: Offset(0, 2),
|
||||
));
|
||||
onDragUpdate(
|
||||
DragUpdateDetails(
|
||||
globalPosition: Offset.zero,
|
||||
delta: Offset(0, 2),
|
||||
),
|
||||
);
|
||||
} else if (value.logicalKey == LogicalKeyboardKey.arrowUp) {
|
||||
onDragUpdate(DragUpdateDetails(
|
||||
globalPosition: Offset.zero,
|
||||
delta: Offset(0, -2),
|
||||
));
|
||||
onDragUpdate(
|
||||
DragUpdateDetails(
|
||||
globalPosition: Offset.zero,
|
||||
delta: Offset(0, -2),
|
||||
),
|
||||
);
|
||||
} else if (value.logicalKey == LogicalKeyboardKey.pageDown) {
|
||||
onDragUpdate(DragUpdateDetails(
|
||||
globalPosition: Offset.zero,
|
||||
delta: Offset(0, 25),
|
||||
));
|
||||
onDragUpdate(
|
||||
DragUpdateDetails(
|
||||
globalPosition: Offset.zero,
|
||||
delta: Offset(0, 25),
|
||||
),
|
||||
);
|
||||
} else if (value.logicalKey == LogicalKeyboardKey.pageUp) {
|
||||
onDragUpdate(DragUpdateDetails(
|
||||
globalPosition: Offset.zero,
|
||||
delta: Offset(0, -25),
|
||||
));
|
||||
onDragUpdate(
|
||||
DragUpdateDetails(
|
||||
globalPosition: Offset.zero,
|
||||
delta: Offset(0, -25),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -5,9 +5,13 @@ import 'package:photos/ui/huge_listview/draggable_scrollbar.dart';
|
|||
import 'package:scrollable_positioned_list/scrollable_positioned_list.dart';
|
||||
|
||||
typedef HugeListViewItemBuilder<T> = Widget Function(
|
||||
BuildContext context, int index);
|
||||
BuildContext context,
|
||||
int index,
|
||||
);
|
||||
typedef HugeListViewErrorBuilder = Widget Function(
|
||||
BuildContext context, dynamic error);
|
||||
BuildContext context,
|
||||
dynamic error,
|
||||
);
|
||||
|
||||
class HugeListView<T> extends StatefulWidget {
|
||||
/// A [ScrollablePositionedList] controller for jumping or scrolling to an item.
|
||||
|
@ -61,11 +65,11 @@ class HugeListView<T> extends StatefulWidget {
|
|||
this.emptyResultBuilder,
|
||||
this.errorBuilder,
|
||||
this.firstShown,
|
||||
this.thumbBackgroundColor = Colors.red,// Colors.white,
|
||||
this.thumbBackgroundColor = Colors.red, // Colors.white,
|
||||
this.thumbDrawColor = Colors.yellow, //Colors.grey,
|
||||
this.thumbHeight = 48.0,
|
||||
this.isDraggableScrollbarEnabled = true,
|
||||
}) : super(key: key);
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
HugeListViewState<T> createState() => HugeListViewState<T>();
|
||||
|
|
|
@ -101,8 +101,9 @@ class _LazyLoadingGalleryState extends State<LazyLoadingGallery> {
|
|||
final dayStartTime =
|
||||
DateTime(galleryDate.year, galleryDate.month, galleryDate.day);
|
||||
final result = await widget.asyncLoader(
|
||||
dayStartTime.microsecondsSinceEpoch,
|
||||
dayStartTime.microsecondsSinceEpoch + kMicroSecondsInDay - 1);
|
||||
dayStartTime.microsecondsSinceEpoch,
|
||||
dayStartTime.microsecondsSinceEpoch + kMicroSecondsInDay - 1,
|
||||
);
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
_files = result.files;
|
||||
|
@ -152,7 +153,10 @@ class _LazyLoadingGalleryState extends State<LazyLoadingGallery> {
|
|||
child: Column(
|
||||
children: [
|
||||
getDayWidget(
|
||||
context, _files[0].creationTime, widget.smallerTodayFont),
|
||||
context,
|
||||
_files[0].creationTime,
|
||||
widget.smallerTodayFont,
|
||||
),
|
||||
_shouldRender ? _getGallery() : PlaceHolderWidget(_files.length),
|
||||
],
|
||||
),
|
||||
|
@ -162,14 +166,17 @@ class _LazyLoadingGalleryState extends State<LazyLoadingGallery> {
|
|||
Widget _getGallery() {
|
||||
List<Widget> childGalleries = [];
|
||||
for (int index = 0; index < _files.length; index += kSubGalleryItemLimit) {
|
||||
childGalleries.add(LazyLoadingGridView(
|
||||
widget.tag,
|
||||
_files.sublist(index, min(index + kSubGalleryItemLimit, _files.length)),
|
||||
widget.asyncLoader,
|
||||
widget.selectedFiles,
|
||||
index == 0,
|
||||
_files.length > kRecycleLimit,
|
||||
));
|
||||
childGalleries.add(
|
||||
LazyLoadingGridView(
|
||||
widget.tag,
|
||||
_files.sublist(
|
||||
index, min(index + kSubGalleryItemLimit, _files.length)),
|
||||
widget.asyncLoader,
|
||||
widget.selectedFiles,
|
||||
index == 0,
|
||||
_files.length > kRecycleLimit,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return Column(
|
||||
|
@ -314,9 +321,11 @@ class _LazyLoadingGridViewState extends State<LazyLoadingGridView> {
|
|||
tag: widget.tag + file.tag(),
|
||||
child: ColorFiltered(
|
||||
colorFilter: ColorFilter.mode(
|
||||
Colors.black.withOpacity(
|
||||
widget.selectedFiles.files.contains(file) ? 0.4 : 0),
|
||||
BlendMode.darken),
|
||||
Colors.black.withOpacity(
|
||||
widget.selectedFiles.files.contains(file) ? 0.4 : 0,
|
||||
),
|
||||
BlendMode.darken,
|
||||
),
|
||||
child: ThumbnailWidget(
|
||||
file,
|
||||
diskLoadDeferDuration: kThumbnailDiskLoadDeferDuration,
|
||||
|
@ -350,12 +359,14 @@ class _LazyLoadingGridViewState extends State<LazyLoadingGridView> {
|
|||
}
|
||||
|
||||
void _routeToDetailPage(File file, BuildContext context) {
|
||||
final page = DetailPage(DetailPageConfiguration(
|
||||
List.unmodifiable(widget.files),
|
||||
widget.asyncLoader,
|
||||
widget.files.indexOf(file),
|
||||
widget.tag,
|
||||
));
|
||||
final page = DetailPage(
|
||||
DetailPageConfiguration(
|
||||
List.unmodifiable(widget.files),
|
||||
widget.asyncLoader,
|
||||
widget.files.indexOf(file),
|
||||
widget.tag,
|
||||
),
|
||||
);
|
||||
routeToPage(context, page);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -62,8 +62,8 @@ class ScrollBarThumb extends StatelessWidget {
|
|||
child: Material(
|
||||
elevation: 4.0,
|
||||
child: Container(
|
||||
constraints:
|
||||
BoxConstraints.tight(Size(height * 0.6, height))),
|
||||
constraints: BoxConstraints.tight(Size(height * 0.6, height)),
|
||||
),
|
||||
color: backgroundColor,
|
||||
borderRadius: BorderRadius.only(
|
||||
topLeft: Radius.circular(height),
|
||||
|
@ -100,20 +100,28 @@ class _ArrowCustomPainter extends CustomPainter {
|
|||
final baseY = size.height / 2;
|
||||
|
||||
canvas.drawPath(
|
||||
trianglePath(Offset(baseX - 2.0, baseY - 2.0), width, height, true),
|
||||
paint);
|
||||
trianglePath(Offset(baseX - 2.0, baseY - 2.0), width, height, true),
|
||||
paint,
|
||||
);
|
||||
canvas.drawPath(
|
||||
trianglePath(Offset(baseX - 2.0, baseY + 2.0), width, height, false),
|
||||
paint);
|
||||
trianglePath(Offset(baseX - 2.0, baseY + 2.0), width, height, false),
|
||||
paint,
|
||||
);
|
||||
}
|
||||
|
||||
static Path trianglePath(
|
||||
Offset offset, double width, double height, bool isUp) {
|
||||
Offset offset,
|
||||
double width,
|
||||
double height,
|
||||
bool isUp,
|
||||
) {
|
||||
return Path()
|
||||
..moveTo(offset.dx, offset.dy)
|
||||
..lineTo(offset.dx + width, offset.dy)
|
||||
..lineTo(offset.dx + (width / 2),
|
||||
isUp ? offset.dy - height : offset.dy + height)
|
||||
..lineTo(
|
||||
offset.dx + (width / 2),
|
||||
isUp ? offset.dy - height : offset.dy + height,
|
||||
)
|
||||
..close();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -308,7 +308,8 @@ class _ImageEditorPageState extends State<ImageEditorPage> {
|
|||
|
||||
option.addOption(ClipOption.fromRect(rect));
|
||||
option.addOption(
|
||||
FlipOption(horizontal: flipHorizontal, vertical: flipVertical));
|
||||
FlipOption(horizontal: flipHorizontal, vertical: flipVertical),
|
||||
);
|
||||
if (action.hasRotateAngle) {
|
||||
option.addOption(RotateOption(radian.toInt()));
|
||||
}
|
||||
|
@ -363,8 +364,9 @@ class _ImageEditorPageState extends State<ImageEditorPage> {
|
|||
_logger.info("Saved edits to file " + newFile.toString());
|
||||
final existingFiles = widget.detailPageConfig.files;
|
||||
final files = (await widget.detailPageConfig.asyncLoader(
|
||||
existingFiles[existingFiles.length - 1].creationTime,
|
||||
existingFiles[0].creationTime))
|
||||
existingFiles[existingFiles.length - 1].creationTime,
|
||||
existingFiles[0].creationTime,
|
||||
))
|
||||
.files;
|
||||
replacePage(
|
||||
context,
|
||||
|
|
|
@ -55,9 +55,11 @@ class _LandingPageWidgetState extends State<LandingPageWidget> {
|
|||
Theme.of(context).colorScheme.dotsIndicatorActiveColor,
|
||||
color: Theme.of(context).colorScheme.dotsIndicatorInactiveColor,
|
||||
activeShape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(3)),
|
||||
borderRadius: BorderRadius.circular(3),
|
||||
),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(3)),
|
||||
borderRadius: BorderRadius.circular(3),
|
||||
),
|
||||
size: Size(100, 5),
|
||||
activeSize: Size(100, 5),
|
||||
spacing: EdgeInsets.all(3),
|
||||
|
|
|
@ -45,11 +45,12 @@ class _LoadingPhotosWidgetState extends State<LoadingPhotosWidget> {
|
|||
// Do nothing, let HomeWidget refresh
|
||||
} else {
|
||||
routeToPage(
|
||||
context,
|
||||
BackupFolderSelectionPage(
|
||||
shouldSelectAll: true,
|
||||
buttonText: "Start backup",
|
||||
));
|
||||
context,
|
||||
BackupFolderSelectionPage(
|
||||
shouldSelectAll: true,
|
||||
buttonText: "Start backup",
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
@ -134,23 +135,25 @@ class _LoadingPhotosWidgetState extends State<LoadingPhotosWidget> {
|
|||
),
|
||||
SizedBox(
|
||||
height: 175,
|
||||
child: Stack(children: [
|
||||
PageView.builder(
|
||||
scrollDirection: Axis.vertical,
|
||||
controller: _pageController,
|
||||
itemBuilder: (context, index) {
|
||||
return _getMessage(_messages[index]);
|
||||
},
|
||||
itemCount: _messages.length,
|
||||
physics: NeverScrollableScrollPhysics(),
|
||||
),
|
||||
Positioned(
|
||||
bottom: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
child: BottomShadowWidget(),
|
||||
)
|
||||
]),
|
||||
child: Stack(
|
||||
children: [
|
||||
PageView.builder(
|
||||
scrollDirection: Axis.vertical,
|
||||
controller: _pageController,
|
||||
itemBuilder: (context, index) {
|
||||
return _getMessage(_messages[index]);
|
||||
},
|
||||
itemCount: _messages.length,
|
||||
physics: NeverScrollableScrollPhysics(),
|
||||
),
|
||||
Positioned(
|
||||
bottom: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
child: BottomShadowWidget(),
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
|
|
@ -48,7 +48,8 @@ class _LocationSearchWidgetState extends State<LocationSearchWidget> {
|
|||
"query": pattern,
|
||||
},
|
||||
options: Options(
|
||||
headers: {"X-Auth-Token": Configuration.instance.getToken()}),
|
||||
headers: {"X-Auth-Token": Configuration.instance.getToken()},
|
||||
),
|
||||
)
|
||||
.then((response) {
|
||||
if (_searchString == pattern) {
|
||||
|
@ -63,21 +64,23 @@ class _LocationSearchWidgetState extends State<LocationSearchWidget> {
|
|||
},
|
||||
onSuggestionSelected: (suggestion) {
|
||||
Navigator.pop(context);
|
||||
Navigator.of(context).push(MaterialPageRoute(
|
||||
Navigator.of(context).push(
|
||||
MaterialPageRoute(
|
||||
builder: (context) => LocationSearchResultsPage(
|
||||
ViewPort(
|
||||
Location(
|
||||
suggestion['geometry']['viewport']['northeast']
|
||||
['lat'],
|
||||
suggestion['geometry']['viewport']['northeast']
|
||||
['lng']),
|
||||
Location(
|
||||
suggestion['geometry']['viewport']['southwest']
|
||||
['lat'],
|
||||
suggestion['geometry']['viewport']['southwest']
|
||||
['lng'])),
|
||||
suggestion['name'],
|
||||
)));
|
||||
ViewPort(
|
||||
Location(
|
||||
suggestion['geometry']['viewport']['northeast']['lat'],
|
||||
suggestion['geometry']['viewport']['northeast']['lng'],
|
||||
),
|
||||
Location(
|
||||
suggestion['geometry']['viewport']['southwest']['lat'],
|
||||
suggestion['geometry']['viewport']['southwest']['lng'],
|
||||
),
|
||||
),
|
||||
suggestion['name'],
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
@ -95,25 +98,27 @@ class LocationSearchResultWidget extends StatelessWidget {
|
|||
return Container(
|
||||
padding: new EdgeInsets.symmetric(vertical: 6.0, horizontal: 6.0),
|
||||
margin: EdgeInsets.symmetric(vertical: 6.0),
|
||||
child: Column(children: <Widget>[
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.start,
|
||||
children: <Widget>[
|
||||
Icon(
|
||||
Icons.location_on,
|
||||
),
|
||||
Padding(padding: EdgeInsets.only(left: 20.0)),
|
||||
Flexible(
|
||||
child: Container(
|
||||
child: Text(
|
||||
name,
|
||||
overflow: TextOverflow.clip,
|
||||
child: Column(
|
||||
children: <Widget>[
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.start,
|
||||
children: <Widget>[
|
||||
Icon(
|
||||
Icons.location_on,
|
||||
),
|
||||
Padding(padding: EdgeInsets.only(left: 20.0)),
|
||||
Flexible(
|
||||
child: Container(
|
||||
child: Text(
|
||||
name,
|
||||
overflow: TextOverflow.clip,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
]),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -26,35 +26,37 @@ class _LockScreenState extends State<LockScreen> {
|
|||
return Scaffold(
|
||||
body: Center(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
|
||||
children: [
|
||||
Stack(
|
||||
alignment: Alignment.center,
|
||||
children: [
|
||||
Image.asset(MediaQuery.of(context).platformBrightness ==
|
||||
Brightness.light
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
|
||||
children: [
|
||||
Stack(
|
||||
alignment: Alignment.center,
|
||||
children: [
|
||||
Image.asset(
|
||||
MediaQuery.of(context).platformBrightness == Brightness.light
|
||||
? 'assets/loading_photos_light.png'
|
||||
: 'assets/loading_photos_dark.png'),
|
||||
SizedBox(
|
||||
width: 172,
|
||||
child: GradientButton(
|
||||
child: Text(
|
||||
'Unlock',
|
||||
style: gradientButtonTextTheme(),
|
||||
),
|
||||
linearGradientColors: const [
|
||||
Color(0xFF2CD267),
|
||||
Color(0xFF1DB954),
|
||||
],
|
||||
onTap: () async {
|
||||
_showLockScreen();
|
||||
},
|
||||
: 'assets/loading_photos_dark.png',
|
||||
),
|
||||
SizedBox(
|
||||
width: 172,
|
||||
child: GradientButton(
|
||||
child: Text(
|
||||
'Unlock',
|
||||
style: gradientButtonTextTheme(),
|
||||
),
|
||||
linearGradientColors: const [
|
||||
Color(0xFF2CD267),
|
||||
Color(0xFF1DB954),
|
||||
],
|
||||
onTap: () async {
|
||||
_showLockScreen();
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
]),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
@ -63,7 +65,8 @@ class _LockScreenState extends State<LockScreen> {
|
|||
_logger.info("Showing lockscreen");
|
||||
try {
|
||||
final result = await requestAuthentication(
|
||||
"Please authenticate to view your memories");
|
||||
"Please authenticate to view your memories",
|
||||
);
|
||||
if (result) {
|
||||
AppLock.of(context).didUnlock();
|
||||
}
|
||||
|
|
|
@ -75,8 +75,10 @@ class _LoginPageState extends State<LoginPage> {
|
|||
Padding(
|
||||
padding:
|
||||
const EdgeInsets.symmetric(vertical: 30, horizontal: 20),
|
||||
child: Text('Welcome back!',
|
||||
style: Theme.of(context).textTheme.headline4),
|
||||
child: Text(
|
||||
'Welcome back!',
|
||||
style: Theme.of(context).textTheme.headline4,
|
||||
),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.fromLTRB(20, 24, 20, 0),
|
||||
|
@ -89,8 +91,9 @@ class _LoginPageState extends State<LoginPage> {
|
|||
contentPadding:
|
||||
EdgeInsets.symmetric(horizontal: 15, vertical: 15),
|
||||
border: UnderlineInputBorder(
|
||||
borderSide: BorderSide.none,
|
||||
borderRadius: BorderRadius.circular(6)),
|
||||
borderSide: BorderSide.none,
|
||||
borderRadius: BorderRadius.circular(6),
|
||||
),
|
||||
suffixIcon: _emailIsValid
|
||||
? Icon(
|
||||
Icons.check,
|
||||
|
@ -141,7 +144,8 @@ class _LoginPageState extends State<LoginPage> {
|
|||
.copyWith(fontSize: 12),
|
||||
children: [
|
||||
TextSpan(
|
||||
text: "By clicking log in, I agree to the "),
|
||||
text: "By clicking log in, I agree to the ",
|
||||
),
|
||||
TextSpan(
|
||||
text: "terms of service",
|
||||
style: TextStyle(
|
||||
|
@ -153,7 +157,9 @@ class _LoginPageState extends State<LoginPage> {
|
|||
MaterialPageRoute(
|
||||
builder: (BuildContext context) {
|
||||
return WebPage(
|
||||
"terms", "https://ente.io/terms");
|
||||
"terms",
|
||||
"https://ente.io/terms",
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
|
@ -170,8 +176,10 @@ class _LoginPageState extends State<LoginPage> {
|
|||
Navigator.of(context).push(
|
||||
MaterialPageRoute(
|
||||
builder: (BuildContext context) {
|
||||
return WebPage("privacy",
|
||||
"https://ente.io/privacy");
|
||||
return WebPage(
|
||||
"privacy",
|
||||
"https://ente.io/privacy",
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
|
|
|
@ -137,12 +137,15 @@ class _ManageSharedLinkWidgetState extends State<ManageSharedLinkWidget> {
|
|||
inputResult == 'ok' &&
|
||||
_textFieldController.text.trim().isNotEmpty) {
|
||||
var propToUpdate = await _getEncryptedPassword(
|
||||
_textFieldController.text);
|
||||
_textFieldController.text,
|
||||
);
|
||||
await _updateUrlSettings(context, propToUpdate);
|
||||
}
|
||||
} else {
|
||||
await _updateUrlSettings(
|
||||
context, {'disablePassword': true});
|
||||
context,
|
||||
{'disablePassword': true},
|
||||
);
|
||||
}
|
||||
setState(() {});
|
||||
},
|
||||
|
@ -166,25 +169,31 @@ class _ManageSharedLinkWidgetState extends State<ManageSharedLinkWidget> {
|
|||
onChanged: (value) async {
|
||||
if (!value) {
|
||||
final choice = await showChoiceDialog(
|
||||
context,
|
||||
'Disable downloads',
|
||||
'Are you sure that you want to disable the download button for files?',
|
||||
firstAction: 'No',
|
||||
secondAction: 'Yes',
|
||||
firstActionColor:
|
||||
Theme.of(context).colorScheme.greenText,
|
||||
secondActionColor: Theme.of(context)
|
||||
.colorScheme
|
||||
.inverseBackgroundColor);
|
||||
context,
|
||||
'Disable downloads',
|
||||
'Are you sure that you want to disable the download button for files?',
|
||||
firstAction: 'No',
|
||||
secondAction: 'Yes',
|
||||
firstActionColor:
|
||||
Theme.of(context).colorScheme.greenText,
|
||||
secondActionColor: Theme.of(context)
|
||||
.colorScheme
|
||||
.inverseBackgroundColor,
|
||||
);
|
||||
if (choice != DialogUserChoice.secondChoice) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
await _updateUrlSettings(
|
||||
context, {'enableDownload': value});
|
||||
context,
|
||||
{'enableDownload': value},
|
||||
);
|
||||
if (!value) {
|
||||
showErrorDialog(context, "Please note",
|
||||
"Viewers can still take screenshots or save a copy of your photos using external tools");
|
||||
showErrorDialog(
|
||||
context,
|
||||
"Please note",
|
||||
"Viewers can still take screenshots or save a copy of your photos using external tools",
|
||||
);
|
||||
}
|
||||
setState(() {});
|
||||
},
|
||||
|
@ -262,7 +271,9 @@ class _ManageSharedLinkWidgetState extends State<ManageSharedLinkWidget> {
|
|||
}
|
||||
if (newValidTill >= 0) {
|
||||
await _updateUrlSettings(
|
||||
context, {'validTill': newValidTill});
|
||||
context,
|
||||
{'validTill': newValidTill},
|
||||
);
|
||||
setState(() {});
|
||||
}
|
||||
Navigator.of(context).pop('');
|
||||
|
@ -333,10 +344,11 @@ class _ManageSharedLinkWidgetState extends State<ManageSharedLinkWidget> {
|
|||
Future<String> _displayLinkPasswordInput(BuildContext context) async {
|
||||
_textFieldController.clear();
|
||||
return showDialog<String>(
|
||||
context: context,
|
||||
builder: (context) {
|
||||
bool _passwordVisible = false;
|
||||
return StatefulBuilder(builder: (context, setState) {
|
||||
context: context,
|
||||
builder: (context) {
|
||||
bool _passwordVisible = false;
|
||||
return StatefulBuilder(
|
||||
builder: (context, setState) {
|
||||
return AlertDialog(
|
||||
title: Text('Enter password'),
|
||||
content: TextFormField(
|
||||
|
@ -369,8 +381,10 @@ class _ManageSharedLinkWidgetState extends State<ManageSharedLinkWidget> {
|
|||
),
|
||||
actions: <Widget>[
|
||||
TextButton(
|
||||
child: Text('Cancel',
|
||||
style: Theme.of(context).textTheme.subtitle2),
|
||||
child: Text(
|
||||
'Cancel',
|
||||
style: Theme.of(context).textTheme.subtitle2,
|
||||
),
|
||||
onPressed: () {
|
||||
Navigator.pop(context, 'cancel');
|
||||
},
|
||||
|
@ -387,18 +401,26 @@ class _ManageSharedLinkWidgetState extends State<ManageSharedLinkWidget> {
|
|||
),
|
||||
],
|
||||
);
|
||||
});
|
||||
});
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Future<Map<String, dynamic>> _getEncryptedPassword(String pass) async {
|
||||
assert(Sodium.cryptoPwhashAlgArgon2id13 == Sodium.cryptoPwhashAlgDefault,
|
||||
"mismatch in expected default pw hashing algo");
|
||||
assert(
|
||||
Sodium.cryptoPwhashAlgArgon2id13 == Sodium.cryptoPwhashAlgDefault,
|
||||
"mismatch in expected default pw hashing algo",
|
||||
);
|
||||
int memLimit = Sodium.cryptoPwhashMemlimitInteractive;
|
||||
int opsLimit = Sodium.cryptoPwhashOpslimitInteractive;
|
||||
final kekSalt = CryptoUtil.getSaltToDeriveKey();
|
||||
final result = await CryptoUtil.deriveKey(
|
||||
utf8.encode(pass), kekSalt, memLimit, opsLimit);
|
||||
utf8.encode(pass),
|
||||
kekSalt,
|
||||
memLimit,
|
||||
opsLimit,
|
||||
);
|
||||
return {
|
||||
'passHash': Sodium.bin2base64(result),
|
||||
'nonce': Sodium.bin2base64(kekSalt),
|
||||
|
@ -408,7 +430,9 @@ class _ManageSharedLinkWidgetState extends State<ManageSharedLinkWidget> {
|
|||
}
|
||||
|
||||
Future<void> _updateUrlSettings(
|
||||
BuildContext context, Map<String, dynamic> prop) async {
|
||||
BuildContext context,
|
||||
Map<String, dynamic> prop,
|
||||
) async {
|
||||
final dialog = createProgressDialog(context, "Please wait...");
|
||||
await dialog.show();
|
||||
try {
|
||||
|
@ -451,7 +475,8 @@ class _ManageSharedLinkWidgetState extends State<ManageSharedLinkWidget> {
|
|||
List<Text> options = [];
|
||||
for (int i = 50; i > 0; i--) {
|
||||
options.add(
|
||||
Text(i.toString(), style: Theme.of(context).textTheme.subtitle1));
|
||||
Text(i.toString(), style: Theme.of(context).textTheme.subtitle1),
|
||||
);
|
||||
}
|
||||
return showCupertinoModalPopup(
|
||||
context: context,
|
||||
|
@ -493,7 +518,8 @@ class _ManageSharedLinkWidgetState extends State<ManageSharedLinkWidget> {
|
|||
onPressed: () async {
|
||||
await _updateUrlSettings(context, {
|
||||
'deviceLimit': int.tryParse(
|
||||
options[_selectedDeviceLimitIndex].data),
|
||||
options[_selectedDeviceLimitIndex].data,
|
||||
),
|
||||
});
|
||||
setState(() {});
|
||||
Navigator.of(context).pop('');
|
||||
|
|
|
@ -95,7 +95,9 @@ class _MemoryWidgetState extends State<MemoryWidget> {
|
|||
return GestureDetector(
|
||||
onTap: () async {
|
||||
await routeToPage(
|
||||
context, FullScreenMemory(title, widget.memories, index));
|
||||
context,
|
||||
FullScreenMemory(title, widget.memories, index),
|
||||
);
|
||||
setState(() {});
|
||||
},
|
||||
child: SizedBox(
|
||||
|
@ -261,9 +263,12 @@ class _FullScreenMemoryState extends State<FullScreenMemory> {
|
|||
),
|
||||
Text(
|
||||
getFormattedDate(
|
||||
DateTime.fromMicrosecondsSinceEpoch(file.creationTime)),
|
||||
DateTime.fromMicrosecondsSinceEpoch(file.creationTime),
|
||||
),
|
||||
style: Theme.of(context).textTheme.subtitle1.copyWith(
|
||||
fontSize: 14, color: Colors.white), //same for both themes
|
||||
fontSize: 14,
|
||||
color: Colors.white,
|
||||
), //same for both themes
|
||||
),
|
||||
],
|
||||
),
|
||||
|
@ -271,16 +276,17 @@ class _FullScreenMemoryState extends State<FullScreenMemory> {
|
|||
),
|
||||
flexibleSpace: Container(
|
||||
decoration: BoxDecoration(
|
||||
gradient: LinearGradient(
|
||||
begin: Alignment.topCenter,
|
||||
end: Alignment.bottomCenter,
|
||||
colors: [
|
||||
Colors.black.withOpacity(0.6),
|
||||
Colors.black.withOpacity(0.5),
|
||||
Colors.transparent,
|
||||
],
|
||||
stops: const [0, 0.6, 1],
|
||||
)),
|
||||
gradient: LinearGradient(
|
||||
begin: Alignment.topCenter,
|
||||
end: Alignment.bottomCenter,
|
||||
colors: [
|
||||
Colors.black.withOpacity(0.6),
|
||||
Colors.black.withOpacity(0.5),
|
||||
Colors.transparent,
|
||||
],
|
||||
stops: const [0, 0.6, 1],
|
||||
),
|
||||
),
|
||||
),
|
||||
backgroundColor: Color(0x00000000),
|
||||
elevation: 0,
|
||||
|
@ -288,12 +294,15 @@ class _FullScreenMemoryState extends State<FullScreenMemory> {
|
|||
extendBodyBehindAppBar: true,
|
||||
body: Container(
|
||||
color: Colors.black,
|
||||
child: Stack(alignment: Alignment.bottomCenter, children: [
|
||||
_buildSwiper(),
|
||||
bottomGradient(),
|
||||
_buildTitleText(),
|
||||
_buildBottomIcons(),
|
||||
]),
|
||||
child: Stack(
|
||||
alignment: Alignment.bottomCenter,
|
||||
children: [
|
||||
_buildSwiper(),
|
||||
bottomGradient(),
|
||||
_buildTitleText(),
|
||||
_buildBottomIcons(),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
@ -307,11 +316,13 @@ class _FullScreenMemoryState extends State<FullScreenMemory> {
|
|||
child: AnimatedOpacity(
|
||||
opacity: _opacity,
|
||||
duration: Duration(milliseconds: 500),
|
||||
child: Text(widget.title,
|
||||
style: Theme.of(context)
|
||||
.textTheme
|
||||
.headline4
|
||||
.copyWith(color: Colors.white)),
|
||||
child: Text(
|
||||
widget.title,
|
||||
style: Theme.of(context)
|
||||
.textTheme
|
||||
.headline4
|
||||
.copyWith(color: Colors.white),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
@ -320,17 +331,18 @@ class _FullScreenMemoryState extends State<FullScreenMemory> {
|
|||
Widget _buildBottomIcons() {
|
||||
final file = widget.memories[_index].file;
|
||||
return Container(
|
||||
alignment: Alignment.bottomRight,
|
||||
padding: EdgeInsets.fromLTRB(0, 0, 26, 20),
|
||||
child: IconButton(
|
||||
icon: Icon(
|
||||
Icons.adaptive.share,
|
||||
color: Colors.white,
|
||||
), //same for both themes
|
||||
onPressed: () {
|
||||
share(context, [file]);
|
||||
},
|
||||
));
|
||||
alignment: Alignment.bottomRight,
|
||||
padding: EdgeInsets.fromLTRB(0, 0, 26, 20),
|
||||
child: IconButton(
|
||||
icon: Icon(
|
||||
Icons.adaptive.share,
|
||||
color: Colors.white,
|
||||
), //same for both themes
|
||||
onPressed: () {
|
||||
share(context, [file]);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget bottomGradient() {
|
||||
|
@ -338,15 +350,16 @@ class _FullScreenMemoryState extends State<FullScreenMemory> {
|
|||
height: 124,
|
||||
width: double.infinity,
|
||||
decoration: BoxDecoration(
|
||||
gradient: LinearGradient(
|
||||
begin: Alignment.bottomCenter,
|
||||
end: Alignment.topCenter,
|
||||
colors: [
|
||||
Colors.black.withOpacity(0.5), //same for both themes
|
||||
Colors.transparent,
|
||||
],
|
||||
stops: const [0, 0.8],
|
||||
)),
|
||||
gradient: LinearGradient(
|
||||
begin: Alignment.bottomCenter,
|
||||
end: Alignment.topCenter,
|
||||
colors: [
|
||||
Colors.black.withOpacity(0.5), //same for both themes
|
||||
Colors.transparent,
|
||||
],
|
||||
stops: const [0, 0.8],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
@ -77,65 +77,65 @@ class _GNavState extends State<GNav> {
|
|||
selectedIndex = widget.selectedIndex;
|
||||
|
||||
return Container(
|
||||
color: widget.backgroundColor ?? Colors.transparent,
|
||||
// padding: EdgeInsets.all(12),
|
||||
// alignment: Alignment.center,
|
||||
child: Row(
|
||||
mainAxisAlignment: widget.mainAxisAlignment,
|
||||
children: widget.tabs
|
||||
.map((t) => GButton(
|
||||
key: t.key,
|
||||
border: t.border ?? widget.tabBorder,
|
||||
activeBorder: t.activeBorder ?? widget.tabActiveBorder,
|
||||
borderRadius:
|
||||
t.borderRadius ?? widget.tabBorderRadius != null
|
||||
? BorderRadius.all(
|
||||
Radius.circular(widget.tabBorderRadius))
|
||||
: const BorderRadius.all(Radius.circular(100.0)),
|
||||
debug: widget.debug ?? false,
|
||||
margin: t.margin ?? widget.tabMargin,
|
||||
active: selectedIndex == widget.tabs.indexOf(t),
|
||||
gap: t.gap ?? widget.gap,
|
||||
iconActiveColor: t.iconActiveColor ?? widget.activeColor,
|
||||
iconColor: t.iconColor ?? widget.color,
|
||||
iconSize: t.iconSize ?? widget.iconSize,
|
||||
textColor: t.textColor ?? widget.activeColor,
|
||||
rippleColor: t.rippleColor ??
|
||||
widget.rippleColor ??
|
||||
Colors.transparent,
|
||||
hoverColor: t.hoverColor ??
|
||||
widget.hoverColor ??
|
||||
Colors.transparent,
|
||||
padding: t.padding ?? widget.padding,
|
||||
icon: t.icon,
|
||||
haptic: widget.haptic ?? true,
|
||||
leading: t.leading,
|
||||
curve: widget.curve ?? Curves.easeInCubic,
|
||||
backgroundGradient:
|
||||
t.backgroundGradient ?? widget.tabBackgroundGradient,
|
||||
backgroundColor: t.backgroundColor ??
|
||||
widget.tabBackgroundColor ??
|
||||
Colors.transparent,
|
||||
duration:
|
||||
widget.duration ?? const Duration(milliseconds: 500),
|
||||
onPressed: () {
|
||||
if (!clickable) return;
|
||||
setState(() {
|
||||
selectedIndex = widget.tabs.indexOf(t);
|
||||
clickable = false;
|
||||
});
|
||||
widget.onTabChange(selectedIndex);
|
||||
color: widget.backgroundColor ?? Colors.transparent,
|
||||
// padding: EdgeInsets.all(12),
|
||||
// alignment: Alignment.center,
|
||||
child: Row(
|
||||
mainAxisAlignment: widget.mainAxisAlignment,
|
||||
children: widget.tabs
|
||||
.map(
|
||||
(t) => GButton(
|
||||
key: t.key,
|
||||
border: t.border ?? widget.tabBorder,
|
||||
activeBorder: t.activeBorder ?? widget.tabActiveBorder,
|
||||
borderRadius: t.borderRadius ?? widget.tabBorderRadius != null
|
||||
? BorderRadius.all(
|
||||
Radius.circular(widget.tabBorderRadius),
|
||||
)
|
||||
: const BorderRadius.all(Radius.circular(100.0)),
|
||||
debug: widget.debug ?? false,
|
||||
margin: t.margin ?? widget.tabMargin,
|
||||
active: selectedIndex == widget.tabs.indexOf(t),
|
||||
gap: t.gap ?? widget.gap,
|
||||
iconActiveColor: t.iconActiveColor ?? widget.activeColor,
|
||||
iconColor: t.iconColor ?? widget.color,
|
||||
iconSize: t.iconSize ?? widget.iconSize,
|
||||
textColor: t.textColor ?? widget.activeColor,
|
||||
rippleColor:
|
||||
t.rippleColor ?? widget.rippleColor ?? Colors.transparent,
|
||||
hoverColor:
|
||||
t.hoverColor ?? widget.hoverColor ?? Colors.transparent,
|
||||
padding: t.padding ?? widget.padding,
|
||||
icon: t.icon,
|
||||
haptic: widget.haptic ?? true,
|
||||
leading: t.leading,
|
||||
curve: widget.curve ?? Curves.easeInCubic,
|
||||
backgroundGradient:
|
||||
t.backgroundGradient ?? widget.tabBackgroundGradient,
|
||||
backgroundColor: t.backgroundColor ??
|
||||
widget.tabBackgroundColor ??
|
||||
Colors.transparent,
|
||||
duration: widget.duration ?? const Duration(milliseconds: 500),
|
||||
onPressed: () {
|
||||
if (!clickable) return;
|
||||
setState(() {
|
||||
selectedIndex = widget.tabs.indexOf(t);
|
||||
clickable = false;
|
||||
});
|
||||
widget.onTabChange(selectedIndex);
|
||||
|
||||
Future.delayed(
|
||||
widget.duration ??
|
||||
const Duration(milliseconds: 500), () {
|
||||
setState(() {
|
||||
clickable = true;
|
||||
});
|
||||
});
|
||||
},
|
||||
))
|
||||
.toList()));
|
||||
Future.delayed(
|
||||
widget.duration ?? const Duration(milliseconds: 500), () {
|
||||
setState(() {
|
||||
clickable = true;
|
||||
});
|
||||
});
|
||||
},
|
||||
),
|
||||
)
|
||||
.toList(),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -237,31 +237,31 @@ class _GButtonState extends State<GButton> {
|
|||
}
|
||||
|
||||
class Button extends StatefulWidget {
|
||||
const Button(
|
||||
{Key key,
|
||||
this.icon,
|
||||
this.iconSize,
|
||||
this.leading,
|
||||
this.iconActiveColor,
|
||||
this.iconColor,
|
||||
this.text,
|
||||
this.gap = 0,
|
||||
this.color,
|
||||
this.rippleColor,
|
||||
this.hoverColor,
|
||||
this.onPressed,
|
||||
this.duration,
|
||||
this.curve,
|
||||
this.padding = const EdgeInsets.all(25),
|
||||
this.margin = const EdgeInsets.all(0),
|
||||
this.active = false,
|
||||
this.debug,
|
||||
this.gradient,
|
||||
this.borderRadius = const BorderRadius.all(Radius.circular(100.0)),
|
||||
this.border,
|
||||
this.activeBorder,
|
||||
this.shadow})
|
||||
: super(key: key);
|
||||
const Button({
|
||||
Key key,
|
||||
this.icon,
|
||||
this.iconSize,
|
||||
this.leading,
|
||||
this.iconActiveColor,
|
||||
this.iconColor,
|
||||
this.text,
|
||||
this.gap = 0,
|
||||
this.color,
|
||||
this.rippleColor,
|
||||
this.hoverColor,
|
||||
this.onPressed,
|
||||
this.duration,
|
||||
this.curve,
|
||||
this.padding = const EdgeInsets.all(25),
|
||||
this.margin = const EdgeInsets.all(0),
|
||||
this.active = false,
|
||||
this.debug,
|
||||
this.gradient,
|
||||
this.borderRadius = const BorderRadius.all(Radius.circular(100.0)),
|
||||
this.border,
|
||||
this.activeBorder,
|
||||
this.shadow,
|
||||
}) : super(key: key);
|
||||
|
||||
final IconData icon;
|
||||
final double iconSize;
|
||||
|
@ -317,7 +317,8 @@ class _ButtonState extends State<Button> with TickerProviderStateMixin {
|
|||
Widget build(BuildContext context) {
|
||||
var curveValue = expandController
|
||||
.drive(
|
||||
CurveTween(curve: _expanded ? widget.curve : widget.curve.flipped))
|
||||
CurveTween(curve: _expanded ? widget.curve : widget.curve.flipped),
|
||||
)
|
||||
.value;
|
||||
|
||||
_expanded = !widget.active;
|
||||
|
@ -370,9 +371,11 @@ class _ButtonState extends State<Button> with TickerProviderStateMixin {
|
|||
),
|
||||
child: FittedBox(
|
||||
fit: BoxFit.fitHeight,
|
||||
child: Stack(children: [
|
||||
Align(alignment: Alignment.centerLeft, child: icon),
|
||||
]),
|
||||
child: Stack(
|
||||
children: [
|
||||
Align(alignment: Alignment.centerLeft, child: icon),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
|
|
@ -69,7 +69,10 @@ class _OTTVerificationPageState extends State<OTTVerificationPage> {
|
|||
onPressedFunction: () {
|
||||
if (widget.isChangeEmail) {
|
||||
UserService.instance.changeEmail(
|
||||
context, widget.email, _verificationCodeController.text);
|
||||
context,
|
||||
widget.email,
|
||||
_verificationCodeController.text,
|
||||
);
|
||||
} else {
|
||||
UserService.instance
|
||||
.verifyEmail(context, _verificationCodeController.text);
|
||||
|
@ -89,8 +92,10 @@ class _OTTVerificationPageState extends State<OTTVerificationPage> {
|
|||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.fromLTRB(20, 30, 20, 15),
|
||||
child: Text('Verify email',
|
||||
style: Theme.of(context).textTheme.headline4),
|
||||
child: Text(
|
||||
'Verify email',
|
||||
style: Theme.of(context).textTheme.headline4,
|
||||
),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 10),
|
||||
|
@ -111,9 +116,11 @@ class _OTTVerificationPageState extends State<OTTVerificationPage> {
|
|||
children: [
|
||||
TextSpan(text: "We've sent a mail to "),
|
||||
TextSpan(
|
||||
text: widget.email,
|
||||
style: TextStyle(
|
||||
color: Theme.of(context).buttonColor))
|
||||
text: widget.email,
|
||||
style: TextStyle(
|
||||
color: Theme.of(context).buttonColor,
|
||||
),
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
|
@ -144,8 +151,9 @@ class _OTTVerificationPageState extends State<OTTVerificationPage> {
|
|||
hintText: 'Tap to enter code',
|
||||
contentPadding: EdgeInsets.all(15),
|
||||
border: UnderlineInputBorder(
|
||||
borderSide: BorderSide.none,
|
||||
borderRadius: BorderRadius.circular(6)),
|
||||
borderSide: BorderSide.none,
|
||||
borderRadius: BorderRadius.circular(6),
|
||||
),
|
||||
),
|
||||
controller: _verificationCodeController,
|
||||
autofocus: false,
|
||||
|
@ -166,13 +174,18 @@ class _OTTVerificationPageState extends State<OTTVerificationPage> {
|
|||
children: [
|
||||
TextButton(
|
||||
onPressed: () {
|
||||
UserService.instance.getOtt(context, widget.email,
|
||||
isCreateAccountScreen: widget.isCreateAccountScreen);
|
||||
UserService.instance.getOtt(
|
||||
context,
|
||||
widget.email,
|
||||
isCreateAccountScreen: widget.isCreateAccountScreen,
|
||||
);
|
||||
},
|
||||
child: Text(
|
||||
"Resend email",
|
||||
style: Theme.of(context).textTheme.subtitle1.copyWith(
|
||||
fontSize: 14, decoration: TextDecoration.underline),
|
||||
fontSize: 14,
|
||||
decoration: TextDecoration.underline,
|
||||
),
|
||||
),
|
||||
)
|
||||
],
|
||||
|
|
|
@ -59,7 +59,9 @@ class _PasswordEntryPageState extends State<PasswordEntryPage> {
|
|||
_volatilePassword = Configuration.instance.getVolatilePassword();
|
||||
if (_volatilePassword != null) {
|
||||
Future.delayed(
|
||||
Duration.zero, () => _showRecoveryCodeDialog(_volatilePassword));
|
||||
Duration.zero,
|
||||
() => _showRecoveryCodeDialog(_volatilePassword),
|
||||
);
|
||||
}
|
||||
_password1FocusNode.addListener(() {
|
||||
setState(() {
|
||||
|
@ -108,16 +110,17 @@ class _PasswordEntryPageState extends State<PasswordEntryPage> {
|
|||
),
|
||||
body: _getBody(title),
|
||||
floatingActionButton: DynamicFAB(
|
||||
isKeypadOpen: isKeypadOpen,
|
||||
isFormValid: _passwordsMatch,
|
||||
buttonText: title,
|
||||
onPressedFunction: () {
|
||||
if (widget.mode == PasswordEntryMode.set) {
|
||||
_showRecoveryCodeDialog(_passwordController1.text);
|
||||
} else {
|
||||
_updatePassword();
|
||||
}
|
||||
}),
|
||||
isKeypadOpen: isKeypadOpen,
|
||||
isFormValid: _passwordsMatch,
|
||||
buttonText: title,
|
||||
onPressedFunction: () {
|
||||
if (widget.mode == PasswordEntryMode.set) {
|
||||
_showRecoveryCodeDialog(_passwordController1.text);
|
||||
} else {
|
||||
_updatePassword();
|
||||
}
|
||||
},
|
||||
),
|
||||
floatingActionButtonLocation: fabLocation(),
|
||||
floatingActionButtonAnimator: NoScalingAnimation(),
|
||||
);
|
||||
|
@ -146,8 +149,10 @@ class _PasswordEntryPageState extends State<PasswordEntryPage> {
|
|||
Padding(
|
||||
padding:
|
||||
const EdgeInsets.symmetric(vertical: 30, horizontal: 20),
|
||||
child: Text(buttonTextAndHeading,
|
||||
style: Theme.of(context).textTheme.headline4),
|
||||
child: Text(
|
||||
buttonTextAndHeading,
|
||||
style: Theme.of(context).textTheme.headline4,
|
||||
),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 20),
|
||||
|
@ -166,22 +171,26 @@ class _PasswordEntryPageState extends State<PasswordEntryPage> {
|
|||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 20),
|
||||
child: RichText(
|
||||
text: TextSpan(
|
||||
style: Theme.of(context)
|
||||
.textTheme
|
||||
.subtitle1
|
||||
.copyWith(fontSize: 14),
|
||||
children: [
|
||||
text: TextSpan(
|
||||
style: Theme.of(context)
|
||||
.textTheme
|
||||
.subtitle1
|
||||
.copyWith(fontSize: 14),
|
||||
children: [
|
||||
TextSpan(
|
||||
text:
|
||||
"We don't store this password, so if you forget, "),
|
||||
text:
|
||||
"We don't store this password, so if you forget, ",
|
||||
),
|
||||
TextSpan(
|
||||
text: "we cannot decrypt your data",
|
||||
style: Theme.of(context).textTheme.subtitle1.copyWith(
|
||||
fontSize: 14,
|
||||
decoration: TextDecoration.underline),
|
||||
fontSize: 14,
|
||||
decoration: TextDecoration.underline,
|
||||
),
|
||||
),
|
||||
])),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
Padding(padding: EdgeInsets.all(12)),
|
||||
Visibility(
|
||||
|
@ -209,8 +218,9 @@ class _PasswordEntryPageState extends State<PasswordEntryPage> {
|
|||
hintText: "Password",
|
||||
contentPadding: EdgeInsets.all(20),
|
||||
border: UnderlineInputBorder(
|
||||
borderSide: BorderSide.none,
|
||||
borderRadius: BorderRadius.circular(6)),
|
||||
borderSide: BorderSide.none,
|
||||
borderRadius: BorderRadius.circular(6),
|
||||
),
|
||||
suffixIcon: _password1InFocus
|
||||
? IconButton(
|
||||
icon: Icon(
|
||||
|
@ -274,7 +284,9 @@ class _PasswordEntryPageState extends State<PasswordEntryPage> {
|
|||
filled: true,
|
||||
hintText: "Confirm password",
|
||||
contentPadding: EdgeInsets.symmetric(
|
||||
horizontal: 20, vertical: 20),
|
||||
horizontal: 20,
|
||||
vertical: 20,
|
||||
),
|
||||
suffixIcon: _password2InFocus
|
||||
? IconButton(
|
||||
icon: Icon(
|
||||
|
@ -301,8 +313,9 @@ class _PasswordEntryPageState extends State<PasswordEntryPage> {
|
|||
)
|
||||
: null,
|
||||
border: UnderlineInputBorder(
|
||||
borderSide: BorderSide.none,
|
||||
borderRadius: BorderRadius.circular(6)),
|
||||
borderSide: BorderSide.none,
|
||||
borderRadius: BorderRadius.circular(6),
|
||||
),
|
||||
),
|
||||
focusNode: _password2FocusNode,
|
||||
onChanged: (cnfPassword) {
|
||||
|
@ -321,66 +334,74 @@ class _PasswordEntryPageState extends State<PasswordEntryPage> {
|
|||
visible:
|
||||
((_passwordInInputBox != '') && _password1InFocus),
|
||||
child: Positioned(
|
||||
bottom: 24,
|
||||
child: Row(
|
||||
children: [
|
||||
SizedBox(
|
||||
width: MediaQuery.of(context).size.width,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 20),
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
spreadRadius: 0.5,
|
||||
color: Theme.of(context).hintColor,
|
||||
offset: Offset(0, -0.325),
|
||||
),
|
||||
],
|
||||
borderRadius: BorderRadius.only(
|
||||
topLeft: Radius.zero,
|
||||
topRight: Radius.zero,
|
||||
bottomLeft: Radius.circular(5),
|
||||
bottomRight: Radius.circular(5),
|
||||
bottom: 24,
|
||||
child: Row(
|
||||
children: [
|
||||
SizedBox(
|
||||
width: MediaQuery.of(context).size.width,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 20,
|
||||
),
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
spreadRadius: 0.5,
|
||||
color: Theme.of(context).hintColor,
|
||||
offset: Offset(0, -0.325),
|
||||
),
|
||||
color: Theme.of(context)
|
||||
.dialogTheme
|
||||
.backgroundColor,
|
||||
],
|
||||
borderRadius: BorderRadius.only(
|
||||
topLeft: Radius.zero,
|
||||
topRight: Radius.zero,
|
||||
bottomLeft: Radius.circular(5),
|
||||
bottomRight: Radius.circular(5),
|
||||
),
|
||||
width: double.infinity,
|
||||
child: Column(
|
||||
crossAxisAlignment:
|
||||
CrossAxisAlignment.start,
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.fromLTRB(
|
||||
4.0, 4, 4.0, 4.0),
|
||||
child: Row(
|
||||
children: [
|
||||
Padding(
|
||||
padding:
|
||||
EdgeInsets.symmetric(
|
||||
horizontal: 10,
|
||||
vertical: 5),
|
||||
child: Text(
|
||||
'Password Strength: $passwordStrengthText',
|
||||
style: TextStyle(
|
||||
color:
|
||||
passwordStrengthColor),
|
||||
)),
|
||||
],
|
||||
color: Theme.of(context)
|
||||
.dialogTheme
|
||||
.backgroundColor,
|
||||
),
|
||||
width: double.infinity,
|
||||
child: Column(
|
||||
crossAxisAlignment:
|
||||
CrossAxisAlignment.start,
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.fromLTRB(
|
||||
4.0,
|
||||
4,
|
||||
4.0,
|
||||
4.0,
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
Padding(
|
||||
padding: EdgeInsets.symmetric(
|
||||
horizontal: 10,
|
||||
vertical: 5,
|
||||
),
|
||||
child: Text(
|
||||
'Password Strength: $passwordStrengthText',
|
||||
style: TextStyle(
|
||||
color: passwordStrengthColor,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
]),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
SizedBox(
|
||||
width: 20,
|
||||
),
|
||||
],
|
||||
)),
|
||||
),
|
||||
SizedBox(
|
||||
width: 20,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
clipBehavior: Clip.none,
|
||||
|
@ -395,7 +416,9 @@ class _PasswordEntryPageState extends State<PasswordEntryPage> {
|
|||
MaterialPageRoute(
|
||||
builder: (BuildContext context) {
|
||||
return WebPage(
|
||||
"How it works", "https://ente.io/architecture");
|
||||
"How it works",
|
||||
"https://ente.io/architecture",
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
|
@ -404,10 +427,12 @@ class _PasswordEntryPageState extends State<PasswordEntryPage> {
|
|||
padding: EdgeInsets.symmetric(horizontal: 20),
|
||||
child: RichText(
|
||||
text: TextSpan(
|
||||
text: "How it works",
|
||||
style: Theme.of(context).textTheme.subtitle1.copyWith(
|
||||
text: "How it works",
|
||||
style: Theme.of(context).textTheme.subtitle1.copyWith(
|
||||
fontSize: 14,
|
||||
decoration: TextDecoration.underline)),
|
||||
decoration: TextDecoration.underline,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
@ -473,21 +498,25 @@ class _PasswordEntryPageState extends State<PasswordEntryPage> {
|
|||
}
|
||||
|
||||
routeToPage(
|
||||
context,
|
||||
RecoveryKeyPage(
|
||||
result.privateKeyAttributes.recoveryKey,
|
||||
"Continue",
|
||||
showAppBar: false,
|
||||
isDismissible: false,
|
||||
onDone: onDone,
|
||||
showProgressBar: true,
|
||||
));
|
||||
context,
|
||||
RecoveryKeyPage(
|
||||
result.privateKeyAttributes.recoveryKey,
|
||||
"Continue",
|
||||
showAppBar: false,
|
||||
isDismissible: false,
|
||||
onDone: onDone,
|
||||
showProgressBar: true,
|
||||
),
|
||||
);
|
||||
} catch (e) {
|
||||
_logger.severe(e);
|
||||
await dialog.hide();
|
||||
if (e is UnsupportedError) {
|
||||
showErrorDialog(context, "Insecure device",
|
||||
"Sorry, we could not generate secure keys on this device.\n\nplease sign up from a different device.");
|
||||
showErrorDialog(
|
||||
context,
|
||||
"Insecure device",
|
||||
"Sorry, we could not generate secure keys on this device.\n\nplease sign up from a different device.",
|
||||
);
|
||||
} else {
|
||||
showGenericErrorDialog(context);
|
||||
}
|
||||
|
|
|
@ -63,8 +63,9 @@ class _PasswordReentryPageState extends State<PasswordReentryPage> {
|
|||
await dialog.show();
|
||||
try {
|
||||
await Configuration.instance.decryptAndSaveSecrets(
|
||||
_passwordController.text,
|
||||
Configuration.instance.getKeyAttributes());
|
||||
_passwordController.text,
|
||||
Configuration.instance.getKeyAttributes(),
|
||||
);
|
||||
} catch (e) {
|
||||
Logger("PRP").warning(e);
|
||||
await dialog.hide();
|
||||
|
@ -90,8 +91,10 @@ class _PasswordReentryPageState extends State<PasswordReentryPage> {
|
|||
Padding(
|
||||
padding:
|
||||
const EdgeInsets.symmetric(vertical: 30, horizontal: 20),
|
||||
child: Text('Welcome back!',
|
||||
style: Theme.of(context).textTheme.headline4),
|
||||
child: Text(
|
||||
'Welcome back!',
|
||||
style: Theme.of(context).textTheme.headline4,
|
||||
),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.fromLTRB(20, 24, 20, 0),
|
||||
|
@ -102,8 +105,9 @@ class _PasswordReentryPageState extends State<PasswordReentryPage> {
|
|||
filled: true,
|
||||
contentPadding: EdgeInsets.all(20),
|
||||
border: UnderlineInputBorder(
|
||||
borderSide: BorderSide.none,
|
||||
borderRadius: BorderRadius.circular(6)),
|
||||
borderSide: BorderSide.none,
|
||||
borderRadius: BorderRadius.circular(6),
|
||||
),
|
||||
suffixIcon: _passwordInFocus
|
||||
? IconButton(
|
||||
icon: Icon(
|
||||
|
@ -159,13 +163,14 @@ class _PasswordReentryPageState extends State<PasswordReentryPage> {
|
|||
},
|
||||
child: Container(
|
||||
child: Center(
|
||||
child: Text("Forgot password",
|
||||
style: Theme.of(context)
|
||||
.textTheme
|
||||
.subtitle1
|
||||
.copyWith(
|
||||
child: Text(
|
||||
"Forgot password",
|
||||
style:
|
||||
Theme.of(context).textTheme.subtitle1.copyWith(
|
||||
fontSize: 14,
|
||||
decoration: TextDecoration.underline)),
|
||||
decoration: TextDecoration.underline,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
@ -182,13 +187,14 @@ class _PasswordReentryPageState extends State<PasswordReentryPage> {
|
|||
},
|
||||
child: Container(
|
||||
child: Center(
|
||||
child: Text("Change email",
|
||||
style: Theme.of(context)
|
||||
.textTheme
|
||||
.subtitle1
|
||||
.copyWith(
|
||||
child: Text(
|
||||
"Change email",
|
||||
style:
|
||||
Theme.of(context).textTheme.subtitle1.copyWith(
|
||||
fontSize: 14,
|
||||
decoration: TextDecoration.underline)),
|
||||
decoration: TextDecoration.underline,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
|
|
@ -52,69 +52,69 @@ class _PaymentWebPageState extends State<PaymentWebPage> {
|
|||
return loadWidget;
|
||||
}
|
||||
return WillPopScope(
|
||||
onWillPop: () async => _buildPageExitWidget(context),
|
||||
child: Scaffold(
|
||||
appBar: AppBar(
|
||||
title: const Text('Subscription'),
|
||||
),
|
||||
body: Column(
|
||||
children: <Widget>[
|
||||
(progress != 1.0)
|
||||
? LinearProgressIndicator(value: progress)
|
||||
: Container(),
|
||||
Expanded(
|
||||
child: InAppWebView(
|
||||
initialUrlRequest: URLRequest(url: initPaymentUrl),
|
||||
onProgressChanged:
|
||||
(InAppWebViewController controller, int progress) {
|
||||
setState(() {
|
||||
this.progress = progress / 100;
|
||||
});
|
||||
},
|
||||
initialOptions: InAppWebViewGroupOptions(
|
||||
crossPlatform: InAppWebViewOptions(
|
||||
useShouldOverrideUrlLoading: true,
|
||||
),
|
||||
onWillPop: () async => _buildPageExitWidget(context),
|
||||
child: Scaffold(
|
||||
appBar: AppBar(
|
||||
title: const Text('Subscription'),
|
||||
),
|
||||
body: Column(
|
||||
children: <Widget>[
|
||||
(progress != 1.0)
|
||||
? LinearProgressIndicator(value: progress)
|
||||
: Container(),
|
||||
Expanded(
|
||||
child: InAppWebView(
|
||||
initialUrlRequest: URLRequest(url: initPaymentUrl),
|
||||
onProgressChanged:
|
||||
(InAppWebViewController controller, int progress) {
|
||||
setState(() {
|
||||
this.progress = progress / 100;
|
||||
});
|
||||
},
|
||||
initialOptions: InAppWebViewGroupOptions(
|
||||
crossPlatform: InAppWebViewOptions(
|
||||
useShouldOverrideUrlLoading: true,
|
||||
),
|
||||
shouldOverrideUrlLoading:
|
||||
(controller, navigationAction) async {
|
||||
var loadingUri = navigationAction.request.url;
|
||||
_logger.info("Loading url $loadingUri");
|
||||
// handle the payment response
|
||||
if (_isPaymentActionComplete(loadingUri)) {
|
||||
await _handlePaymentResponse(loadingUri);
|
||||
return NavigationActionPolicy.CANCEL;
|
||||
}
|
||||
return NavigationActionPolicy.ALLOW;
|
||||
},
|
||||
onConsoleMessage: (controller, consoleMessage) {
|
||||
_logger.info(consoleMessage);
|
||||
},
|
||||
onLoadStart: (controller, navigationAction) async {
|
||||
if (!_dialog.isShowing()) {
|
||||
await _dialog.show();
|
||||
}
|
||||
},
|
||||
onLoadError: (controller, navigationAction, code, msg) async {
|
||||
if (_dialog.isShowing()) {
|
||||
await _dialog.hide();
|
||||
}
|
||||
},
|
||||
onLoadHttpError:
|
||||
(controller, navigationAction, code, msg) async {
|
||||
_logger.info("onHttpError with $code and msg = $msg");
|
||||
},
|
||||
onLoadStop: (controller, navigationAction) async {
|
||||
_logger.info("loadStart" + navigationAction.toString());
|
||||
if (_dialog.isShowing()) {
|
||||
await _dialog.hide();
|
||||
}
|
||||
},
|
||||
),
|
||||
shouldOverrideUrlLoading: (controller, navigationAction) async {
|
||||
var loadingUri = navigationAction.request.url;
|
||||
_logger.info("Loading url $loadingUri");
|
||||
// handle the payment response
|
||||
if (_isPaymentActionComplete(loadingUri)) {
|
||||
await _handlePaymentResponse(loadingUri);
|
||||
return NavigationActionPolicy.CANCEL;
|
||||
}
|
||||
return NavigationActionPolicy.ALLOW;
|
||||
},
|
||||
onConsoleMessage: (controller, consoleMessage) {
|
||||
_logger.info(consoleMessage);
|
||||
},
|
||||
onLoadStart: (controller, navigationAction) async {
|
||||
if (!_dialog.isShowing()) {
|
||||
await _dialog.show();
|
||||
}
|
||||
},
|
||||
onLoadError: (controller, navigationAction, code, msg) async {
|
||||
if (_dialog.isShowing()) {
|
||||
await _dialog.hide();
|
||||
}
|
||||
},
|
||||
onLoadHttpError:
|
||||
(controller, navigationAction, code, msg) async {
|
||||
_logger.info("onHttpError with $code and msg = $msg");
|
||||
},
|
||||
onLoadStop: (controller, navigationAction) async {
|
||||
_logger.info("loadStart" + navigationAction.toString());
|
||||
if (_dialog.isShowing()) {
|
||||
await _dialog.hide();
|
||||
}
|
||||
},
|
||||
),
|
||||
].where((Object o) => o != null).toList(),
|
||||
),
|
||||
));
|
||||
),
|
||||
].where((Object o) => o != null).toList(),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
|
@ -146,10 +146,12 @@ class _PaymentWebPageState extends State<PaymentWebPage> {
|
|||
title: Text('Are you sure you want to exit?'),
|
||||
actions: <Widget>[
|
||||
TextButton(
|
||||
child: Text('Yes',
|
||||
style: TextStyle(
|
||||
color: Colors.redAccent,
|
||||
)),
|
||||
child: Text(
|
||||
'Yes',
|
||||
style: TextStyle(
|
||||
color: Colors.redAccent,
|
||||
),
|
||||
),
|
||||
onPressed: () => Navigator.of(context).pop(true),
|
||||
),
|
||||
TextButton(
|
||||
|
@ -188,19 +190,21 @@ class _PaymentWebPageState extends State<PaymentWebPage> {
|
|||
|
||||
Future<void> _handlePaymentFailure(String reason) async {
|
||||
await showDialog(
|
||||
context: context,
|
||||
barrierDismissible: false,
|
||||
builder: (context) => AlertDialog(
|
||||
title: Text('Payment failed'),
|
||||
content:
|
||||
Text("Unfortunately your payment failed due to $reason"),
|
||||
actions: <Widget>[
|
||||
TextButton(
|
||||
child: Text('Ok'),
|
||||
onPressed: () {
|
||||
Navigator.of(context).pop('dialog');
|
||||
}),
|
||||
]));
|
||||
context: context,
|
||||
barrierDismissible: false,
|
||||
builder: (context) => AlertDialog(
|
||||
title: Text('Payment failed'),
|
||||
content: Text("Unfortunately your payment failed due to $reason"),
|
||||
actions: <Widget>[
|
||||
TextButton(
|
||||
child: Text('Ok'),
|
||||
onPressed: () {
|
||||
Navigator.of(context).pop('dialog');
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
Navigator.of(context).pop(true);
|
||||
}
|
||||
|
||||
|
@ -210,8 +214,10 @@ class _PaymentWebPageState extends State<PaymentWebPage> {
|
|||
await _dialog.show();
|
||||
try {
|
||||
var response = await billingService.verifySubscription(
|
||||
widget.planId, checkoutSessionID,
|
||||
paymentProvider: kStripe);
|
||||
widget.planId,
|
||||
checkoutSessionID,
|
||||
paymentProvider: kStripe,
|
||||
);
|
||||
await _dialog.hide();
|
||||
if (response != null) {
|
||||
var content = widget.actionType == 'buy'
|
||||
|
@ -241,13 +247,14 @@ class _PaymentWebPageState extends State<PaymentWebPage> {
|
|||
content: Text(content),
|
||||
actions: <Widget>[
|
||||
TextButton(
|
||||
child: Text(
|
||||
'Ok',
|
||||
style: TextStyle(color: Theme.of(context).buttonColor),
|
||||
),
|
||||
onPressed: () {
|
||||
Navigator.of(context).pop('dialog');
|
||||
}),
|
||||
child: Text(
|
||||
'Ok',
|
||||
style: TextStyle(color: Theme.of(context).buttonColor),
|
||||
),
|
||||
onPressed: () {
|
||||
Navigator.of(context).pop('dialog');
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
).then((val) => Navigator.pop(context, true));
|
||||
|
|
|
@ -117,31 +117,36 @@ class _StripeSubscriptionPageState extends State<StripeSubscriptionPage> {
|
|||
final appBar = PreferredSize(
|
||||
preferredSize: Size(double.infinity, 60),
|
||||
child: Container(
|
||||
decoration: BoxDecoration(boxShadow: [
|
||||
decoration: BoxDecoration(
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Theme.of(context).backgroundColor,
|
||||
blurRadius: 16,
|
||||
offset: Offset(0, 8))
|
||||
]),
|
||||
child: widget.isOnboarding
|
||||
? AppBar(
|
||||
elevation: 0,
|
||||
title: Hero(
|
||||
tag: "subscription",
|
||||
child: StepProgressIndicator(
|
||||
totalSteps: 4,
|
||||
currentStep: 4,
|
||||
selectedColor: Theme.of(context).buttonColor,
|
||||
roundedEdges: Radius.circular(10),
|
||||
unselectedColor: Theme.of(context)
|
||||
.colorScheme
|
||||
.stepProgressUnselectedColor,
|
||||
)),
|
||||
)
|
||||
: AppBar(
|
||||
elevation: 0,
|
||||
title: Text("Subscription"),
|
||||
)),
|
||||
color: Theme.of(context).backgroundColor,
|
||||
blurRadius: 16,
|
||||
offset: Offset(0, 8),
|
||||
)
|
||||
],
|
||||
),
|
||||
child: widget.isOnboarding
|
||||
? AppBar(
|
||||
elevation: 0,
|
||||
title: Hero(
|
||||
tag: "subscription",
|
||||
child: StepProgressIndicator(
|
||||
totalSteps: 4,
|
||||
currentStep: 4,
|
||||
selectedColor: Theme.of(context).buttonColor,
|
||||
roundedEdges: Radius.circular(10),
|
||||
unselectedColor: Theme.of(context)
|
||||
.colorScheme
|
||||
.stepProgressUnselectedColor,
|
||||
),
|
||||
),
|
||||
)
|
||||
: AppBar(
|
||||
elevation: 0,
|
||||
title: Text("Subscription"),
|
||||
),
|
||||
),
|
||||
);
|
||||
return Scaffold(
|
||||
appBar: appBar,
|
||||
|
@ -176,15 +181,18 @@ class _StripeSubscriptionPageState extends State<StripeSubscriptionPage> {
|
|||
Widget _buildPlans() {
|
||||
final widgets = <Widget>[];
|
||||
|
||||
widgets.add(SubscriptionHeaderWidget(
|
||||
isOnboarding: widget.isOnboarding,
|
||||
currentUsage: _userDetails.getFamilyOrPersonalUsage(),
|
||||
));
|
||||
widgets.add(
|
||||
SubscriptionHeaderWidget(
|
||||
isOnboarding: widget.isOnboarding,
|
||||
currentUsage: _userDetails.getFamilyOrPersonalUsage(),
|
||||
),
|
||||
);
|
||||
|
||||
widgets.addAll([
|
||||
Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: _getStripePlanWidgets()),
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: _getStripePlanWidgets(),
|
||||
),
|
||||
Padding(padding: EdgeInsets.all(4)),
|
||||
]);
|
||||
|
||||
|
@ -218,16 +226,19 @@ class _StripeSubscriptionPageState extends State<StripeSubscriptionPage> {
|
|||
break;
|
||||
case kPlayStore:
|
||||
launch(
|
||||
"https://play.google.com/store/account/subscriptions?sku=" +
|
||||
_currentSubscription.productID +
|
||||
"&package=io.ente.photos");
|
||||
"https://play.google.com/store/account/subscriptions?sku=" +
|
||||
_currentSubscription.productID +
|
||||
"&package=io.ente.photos",
|
||||
);
|
||||
break;
|
||||
case kAppStore:
|
||||
launch("https://apps.apple.com/account/billing");
|
||||
break;
|
||||
default:
|
||||
_logger.severe(
|
||||
"unexpected payment provider ", _currentSubscription);
|
||||
"unexpected payment provider ",
|
||||
_currentSubscription,
|
||||
);
|
||||
}
|
||||
},
|
||||
child: Container(
|
||||
|
@ -236,17 +247,18 @@ class _StripeSubscriptionPageState extends State<StripeSubscriptionPage> {
|
|||
children: [
|
||||
RichText(
|
||||
text: TextSpan(
|
||||
text: !_isStripeSubscriber
|
||||
? "visit ${_currentSubscription.paymentProvider} to manage your subscription"
|
||||
: "Payment details",
|
||||
style: TextStyle(
|
||||
color: Theme.of(context).colorScheme.onSurface,
|
||||
fontFamily: 'Inter-Medium',
|
||||
fontSize: 14,
|
||||
decoration: _isStripeSubscriber
|
||||
? TextDecoration.underline
|
||||
: TextDecoration.none,
|
||||
)),
|
||||
text: !_isStripeSubscriber
|
||||
? "visit ${_currentSubscription.paymentProvider} to manage your subscription"
|
||||
: "Payment details",
|
||||
style: TextStyle(
|
||||
color: Theme.of(context).colorScheme.onSurface,
|
||||
fontFamily: 'Inter-Medium',
|
||||
fontSize: 14,
|
||||
decoration: _isStripeSubscriber
|
||||
? TextDecoration.underline
|
||||
: TextDecoration.none,
|
||||
),
|
||||
),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
],
|
||||
|
@ -316,21 +328,26 @@ class _StripeSubscriptionPageState extends State<StripeSubscriptionPage> {
|
|||
Future<void> _launchFamilyPortal() async {
|
||||
if (_userDetails.subscription.productID == kFreeProductID) {
|
||||
await showErrorDialog(
|
||||
context,
|
||||
"Now you can share your storage plan with your family members!",
|
||||
"Customers on paid plans can add up to 5 family members without paying extra. Each member gets their own private space.");
|
||||
context,
|
||||
"Now you can share your storage plan with your family members!",
|
||||
"Customers on paid plans can add up to 5 family members without paying extra. Each member gets their own private space.",
|
||||
);
|
||||
return;
|
||||
}
|
||||
await _dialog.show();
|
||||
try {
|
||||
final String jwtToken = await _userService.getFamiliesToken();
|
||||
final bool familyExist = _userDetails.isPartOfFamily();
|
||||
Navigator.of(context).push(MaterialPageRoute(
|
||||
builder: (BuildContext context) {
|
||||
return WebPage("Family",
|
||||
'$kFamilyPlanManagementUrl?token=$jwtToken&isFamilyCreated=$familyExist');
|
||||
},
|
||||
)).then((value) => onWebPaymentGoBack);
|
||||
Navigator.of(context).push(
|
||||
MaterialPageRoute(
|
||||
builder: (BuildContext context) {
|
||||
return WebPage(
|
||||
"Family",
|
||||
'$kFamilyPlanManagementUrl?token=$jwtToken&isFamilyCreated=$familyExist',
|
||||
);
|
||||
},
|
||||
),
|
||||
).then((value) => onWebPaymentGoBack);
|
||||
} catch (e) {
|
||||
await _dialog.hide();
|
||||
showGenericErrorDialog(context);
|
||||
|
@ -357,15 +374,22 @@ class _StripeSubscriptionPageState extends State<StripeSubscriptionPage> {
|
|||
bool confirmAction = false;
|
||||
if (isRenewCancelled) {
|
||||
var choice = await showChoiceDialog(
|
||||
context, title, "are you sure you want to renew?",
|
||||
firstAction: "no", secondAction: "yes");
|
||||
context,
|
||||
title,
|
||||
"are you sure you want to renew?",
|
||||
firstAction: "no",
|
||||
secondAction: "yes",
|
||||
);
|
||||
confirmAction = choice == DialogUserChoice.secondChoice;
|
||||
} else {
|
||||
var choice = await showChoiceDialog(
|
||||
context, title, 'are you sure you want to cancel?',
|
||||
firstAction: 'yes, cancel',
|
||||
secondAction: 'no',
|
||||
actionType: ActionType.critical);
|
||||
context,
|
||||
title,
|
||||
'are you sure you want to cancel?',
|
||||
firstAction: 'yes, cancel',
|
||||
secondAction: 'no',
|
||||
actionType: ActionType.critical,
|
||||
);
|
||||
confirmAction = choice == DialogUserChoice.firstChoice;
|
||||
}
|
||||
if (confirmAction) {
|
||||
|
@ -384,7 +408,9 @@ class _StripeSubscriptionPageState extends State<StripeSubscriptionPage> {
|
|||
await _fetchSub();
|
||||
} catch (e) {
|
||||
showToast(
|
||||
context, isRenewCancelled ? 'failed to renew' : 'failed to cancel');
|
||||
context,
|
||||
isRenewCancelled ? 'failed to renew' : 'failed to cancel',
|
||||
);
|
||||
}
|
||||
await _dialog.hide();
|
||||
}
|
||||
|
@ -414,24 +440,31 @@ class _StripeSubscriptionPageState extends State<StripeSubscriptionPage> {
|
|||
if (!_isStripeSubscriber &&
|
||||
_hasActiveSubscription &&
|
||||
_currentSubscription.productID != kFreeProductID) {
|
||||
showErrorDialog(context, "Sorry",
|
||||
"please cancel your existing subscription from ${_currentSubscription.paymentProvider} first");
|
||||
showErrorDialog(
|
||||
context,
|
||||
"Sorry",
|
||||
"please cancel your existing subscription from ${_currentSubscription.paymentProvider} first",
|
||||
);
|
||||
return;
|
||||
}
|
||||
if (_userDetails.getFamilyOrPersonalUsage() > plan.storage) {
|
||||
showErrorDialog(
|
||||
context, "Sorry", "you cannot downgrade to this plan");
|
||||
context,
|
||||
"Sorry",
|
||||
"you cannot downgrade to this plan",
|
||||
);
|
||||
return;
|
||||
}
|
||||
String stripPurChaseAction = 'buy';
|
||||
if (_isStripeSubscriber && _hasActiveSubscription) {
|
||||
// confirm if user wants to change plan or not
|
||||
var result = await showChoiceDialog(
|
||||
context,
|
||||
"Confirm plan change",
|
||||
"Are you sure you want to change your plan?",
|
||||
firstAction: "No",
|
||||
secondAction: 'Yes');
|
||||
context,
|
||||
"Confirm plan change",
|
||||
"Are you sure you want to change your plan?",
|
||||
firstAction: "No",
|
||||
secondAction: 'Yes',
|
||||
);
|
||||
if (result != DialogUserChoice.secondChoice) {
|
||||
return;
|
||||
}
|
||||
|
|
|
@ -10,9 +10,11 @@ class SubscriptionHeaderWidget extends StatefulWidget {
|
|||
final bool isOnboarding;
|
||||
final int currentUsage;
|
||||
|
||||
const SubscriptionHeaderWidget(
|
||||
{Key key, this.isOnboarding, this.currentUsage})
|
||||
: super(key: key);
|
||||
const SubscriptionHeaderWidget({
|
||||
Key key,
|
||||
this.isOnboarding,
|
||||
this.currentUsage,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
State<StatefulWidget> createState() {
|
||||
|
@ -57,8 +59,9 @@ class _SubscriptionHeaderWidgetState extends State<SubscriptionHeaderWidget> {
|
|||
text: TextSpan(
|
||||
children: [
|
||||
TextSpan(
|
||||
text: "Current usage is ",
|
||||
style: Theme.of(context).textTheme.subtitle1),
|
||||
text: "Current usage is ",
|
||||
style: Theme.of(context).textTheme.subtitle1,
|
||||
),
|
||||
TextSpan(
|
||||
text: formatBytes(widget.currentUsage),
|
||||
style: Theme.of(context)
|
||||
|
@ -86,7 +89,8 @@ class ValidityWidget extends StatelessWidget {
|
|||
return Container();
|
||||
}
|
||||
var endDate = getDateAndMonthAndYear(
|
||||
DateTime.fromMicrosecondsSinceEpoch(currentSubscription.expiryTime));
|
||||
DateTime.fromMicrosecondsSinceEpoch(currentSubscription.expiryTime),
|
||||
);
|
||||
var message = "Renews on $endDate";
|
||||
if (currentSubscription.productID == kFreeProductID) {
|
||||
message = "Free plan valid till $endDate";
|
||||
|
|
|
@ -99,11 +99,12 @@ class _SubscriptionPageState extends State<SubscriptionPage> {
|
|||
_logger.warning("Could not complete payment ", e);
|
||||
await _dialog.hide();
|
||||
showErrorDialog(
|
||||
context,
|
||||
"payment failed",
|
||||
"please talk to " +
|
||||
(Platform.isAndroid ? "PlayStore" : "AppStore") +
|
||||
" support if you were charged");
|
||||
context,
|
||||
"payment failed",
|
||||
"please talk to " +
|
||||
(Platform.isAndroid ? "PlayStore" : "AppStore") +
|
||||
" support if you were charged",
|
||||
);
|
||||
return;
|
||||
}
|
||||
} else if (Platform.isIOS && purchase.pendingCompletePurchase) {
|
||||
|
@ -175,10 +176,12 @@ class _SubscriptionPageState extends State<SubscriptionPage> {
|
|||
|
||||
Widget _buildPlans() {
|
||||
final widgets = <Widget>[];
|
||||
widgets.add(SubscriptionHeaderWidget(
|
||||
isOnboarding: widget.isOnboarding,
|
||||
currentUsage: _userDetails.getFamilyOrPersonalUsage(),
|
||||
));
|
||||
widgets.add(
|
||||
SubscriptionHeaderWidget(
|
||||
isOnboarding: widget.isOnboarding,
|
||||
currentUsage: _userDetails.getFamilyOrPersonalUsage(),
|
||||
),
|
||||
);
|
||||
|
||||
widgets.addAll([
|
||||
Column(
|
||||
|
@ -213,9 +216,10 @@ class _SubscriptionPageState extends State<SubscriptionPage> {
|
|||
}
|
||||
if (Platform.isAndroid) {
|
||||
launch(
|
||||
"https://play.google.com/store/account/subscriptions?sku=" +
|
||||
_currentSubscription.productID +
|
||||
"&package=io.ente.photos");
|
||||
"https://play.google.com/store/account/subscriptions?sku=" +
|
||||
_currentSubscription.productID +
|
||||
"&package=io.ente.photos",
|
||||
);
|
||||
} else {
|
||||
launch("https://apps.apple.com/account/billing");
|
||||
}
|
||||
|
@ -307,8 +311,11 @@ class _SubscriptionPageState extends State<SubscriptionPage> {
|
|||
if (isActive) {
|
||||
return;
|
||||
}
|
||||
showErrorDialog(context, "Sorry",
|
||||
"Please visit web.ente.io to manage your subscription");
|
||||
showErrorDialog(
|
||||
context,
|
||||
"Sorry",
|
||||
"Please visit web.ente.io to manage your subscription",
|
||||
);
|
||||
},
|
||||
child: SubscriptionPlanWidget(
|
||||
storage: plan.storage,
|
||||
|
@ -357,7 +364,10 @@ class _SubscriptionPageState extends State<SubscriptionPage> {
|
|||
}
|
||||
if (_userDetails.getFamilyOrPersonalUsage() > plan.storage) {
|
||||
showErrorDialog(
|
||||
context, "Sorry", "you cannot downgrade to this plan");
|
||||
context,
|
||||
"Sorry",
|
||||
"you cannot downgrade to this plan",
|
||||
);
|
||||
return;
|
||||
}
|
||||
await _dialog.show();
|
||||
|
@ -365,8 +375,9 @@ class _SubscriptionPageState extends State<SubscriptionPage> {
|
|||
await InAppPurchaseConnection.instance
|
||||
.queryProductDetails({productID});
|
||||
if (response.notFoundIDs.isNotEmpty) {
|
||||
_logger.severe("Could not find products: " +
|
||||
response.notFoundIDs.toString());
|
||||
_logger.severe(
|
||||
"Could not find products: " + response.notFoundIDs.toString(),
|
||||
);
|
||||
await _dialog.hide();
|
||||
showGenericErrorDialog(context);
|
||||
return;
|
||||
|
@ -380,8 +391,10 @@ class _SubscriptionPageState extends State<SubscriptionPage> {
|
|||
await InAppPurchaseConnection.instance
|
||||
.queryProductDetails({_currentSubscription.productID});
|
||||
if (existingProductDetailsResponse.notFoundIDs.isNotEmpty) {
|
||||
_logger.severe("Could not find existing products: " +
|
||||
response.notFoundIDs.toString());
|
||||
_logger.severe(
|
||||
"Could not find existing products: " +
|
||||
response.notFoundIDs.toString(),
|
||||
);
|
||||
await _dialog.hide();
|
||||
showGenericErrorDialog(context);
|
||||
return;
|
||||
|
@ -451,21 +464,26 @@ class _SubscriptionPageState extends State<SubscriptionPage> {
|
|||
Future<void> _launchFamilyPortal() async {
|
||||
if (_userDetails.subscription.productID == kFreeProductID) {
|
||||
await showErrorDialog(
|
||||
context,
|
||||
"Now you can share your storage plan with your family members!",
|
||||
"Customers on paid plans can add up to 5 family members without paying extra. Each member gets their own private space.");
|
||||
context,
|
||||
"Now you can share your storage plan with your family members!",
|
||||
"Customers on paid plans can add up to 5 family members without paying extra. Each member gets their own private space.",
|
||||
);
|
||||
return;
|
||||
}
|
||||
await _dialog.show();
|
||||
try {
|
||||
final String jwtToken = await _userService.getFamiliesToken();
|
||||
final bool familyExist = _userDetails.isPartOfFamily();
|
||||
Navigator.of(context).push(MaterialPageRoute(
|
||||
builder: (BuildContext context) {
|
||||
return WebPage("Family",
|
||||
'$kFamilyPlanManagementUrl?token=$jwtToken&isFamilyCreated=$familyExist');
|
||||
},
|
||||
));
|
||||
Navigator.of(context).push(
|
||||
MaterialPageRoute(
|
||||
builder: (BuildContext context) {
|
||||
return WebPage(
|
||||
"Family",
|
||||
'$kFamilyPlanManagementUrl?token=$jwtToken&isFamilyCreated=$familyExist',
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
} catch (e) {
|
||||
await _dialog.hide();
|
||||
showGenericErrorDialog(context);
|
||||
|
|
|
@ -58,9 +58,13 @@ class SubscriptionPlanWidget extends StatelessWidget {
|
|||
.headline6
|
||||
.copyWith(color: textColor),
|
||||
),
|
||||
Text(_displayPrice(),
|
||||
style: Theme.of(context).textTheme.headline6.copyWith(
|
||||
color: textColor, fontWeight: FontWeight.normal)),
|
||||
Text(
|
||||
_displayPrice(),
|
||||
style: Theme.of(context).textTheme.headline6.copyWith(
|
||||
color: textColor,
|
||||
fontWeight: FontWeight.normal,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
|
|
|
@ -21,9 +21,15 @@ bool _barrierDismissible = true, _showLogs = false;
|
|||
Color _barrierColor;
|
||||
|
||||
TextStyle _progressTextStyle = TextStyle(
|
||||
color: Colors.black, fontSize: 12.0, fontWeight: FontWeight.w400),
|
||||
color: Colors.black,
|
||||
fontSize: 12.0,
|
||||
fontWeight: FontWeight.w400,
|
||||
),
|
||||
_messageStyle = TextStyle(
|
||||
color: Colors.black, fontSize: 18.0, fontWeight: FontWeight.w600);
|
||||
color: Colors.black,
|
||||
fontSize: 18.0,
|
||||
fontWeight: FontWeight.w600,
|
||||
);
|
||||
|
||||
double _dialogElevation = 8.0, _borderRadius = 8.0;
|
||||
Color _backgroundColor = Colors.white;
|
||||
|
@ -56,21 +62,22 @@ class ProgressDialog {
|
|||
_barrierColor = barrierColor ?? barrierColor;
|
||||
}
|
||||
|
||||
void style(
|
||||
{Widget child,
|
||||
double progress,
|
||||
double maxProgress,
|
||||
String message,
|
||||
Widget progressWidget,
|
||||
Color backgroundColor,
|
||||
TextStyle progressTextStyle,
|
||||
TextStyle messageTextStyle,
|
||||
double elevation,
|
||||
TextAlign textAlign,
|
||||
double borderRadius,
|
||||
Curve insetAnimCurve,
|
||||
EdgeInsets padding,
|
||||
Alignment progressWidgetAlignment}) {
|
||||
void style({
|
||||
Widget child,
|
||||
double progress,
|
||||
double maxProgress,
|
||||
String message,
|
||||
Widget progressWidget,
|
||||
Color backgroundColor,
|
||||
TextStyle progressTextStyle,
|
||||
TextStyle messageTextStyle,
|
||||
double elevation,
|
||||
TextAlign textAlign,
|
||||
double borderRadius,
|
||||
Curve insetAnimCurve,
|
||||
EdgeInsets padding,
|
||||
Alignment progressWidgetAlignment,
|
||||
}) {
|
||||
if (_isShowing) return;
|
||||
if (_progressDialogType == ProgressDialogType.Download) {
|
||||
_progress = progress ?? _progress;
|
||||
|
@ -92,13 +99,14 @@ class ProgressDialog {
|
|||
progressWidgetAlignment ?? _progressWidgetAlignment;
|
||||
}
|
||||
|
||||
void update(
|
||||
{double progress,
|
||||
double maxProgress,
|
||||
String message,
|
||||
Widget progressWidget,
|
||||
TextStyle progressTextStyle,
|
||||
TextStyle messageTextStyle}) {
|
||||
void update({
|
||||
double progress,
|
||||
double maxProgress,
|
||||
String message,
|
||||
Widget progressWidget,
|
||||
TextStyle progressTextStyle,
|
||||
TextStyle messageTextStyle,
|
||||
}) {
|
||||
if (_progressDialogType == ProgressDialogType.Download) {
|
||||
_progress = progress ?? _progress;
|
||||
}
|
||||
|
@ -152,8 +160,9 @@ class ProgressDialog {
|
|||
insetAnimationDuration: Duration(milliseconds: 100),
|
||||
elevation: _dialogElevation,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius:
|
||||
BorderRadius.all(Radius.circular(_borderRadius))),
|
||||
borderRadius:
|
||||
BorderRadius.all(Radius.circular(_borderRadius)),
|
||||
),
|
||||
child: _dialog,
|
||||
),
|
||||
);
|
||||
|
@ -232,11 +241,12 @@ class _BodyState extends State<_Body> {
|
|||
Row(
|
||||
children: <Widget>[
|
||||
Expanded(
|
||||
child: Text(
|
||||
_dialogMessage,
|
||||
style: _messageStyle,
|
||||
textDirection: _direction,
|
||||
)),
|
||||
child: Text(
|
||||
_dialogMessage,
|
||||
style: _messageStyle,
|
||||
textDirection: _direction,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
SizedBox(height: 4.0),
|
||||
|
|
|
@ -22,16 +22,18 @@ class RecoveryKeyPage extends StatefulWidget {
|
|||
final String subText;
|
||||
final bool showProgressBar;
|
||||
|
||||
const RecoveryKeyPage(this.recoveryKey, this.doneText,
|
||||
{Key key,
|
||||
this.showAppBar,
|
||||
this.onDone,
|
||||
this.isDismissible,
|
||||
this.title,
|
||||
this.text,
|
||||
this.subText,
|
||||
this.showProgressBar = false})
|
||||
: super(key: key);
|
||||
const RecoveryKeyPage(
|
||||
this.recoveryKey,
|
||||
this.doneText, {
|
||||
Key key,
|
||||
this.showAppBar,
|
||||
this.onDone,
|
||||
this.isDismissible,
|
||||
this.title,
|
||||
this.text,
|
||||
this.subText,
|
||||
this.showProgressBar = false,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
_RecoveryKeyPageState createState() => _RecoveryKeyPageState();
|
||||
|
@ -40,14 +42,16 @@ class RecoveryKeyPage extends StatefulWidget {
|
|||
class _RecoveryKeyPageState extends State<RecoveryKeyPage> {
|
||||
bool _hasTriedToSave = false;
|
||||
final _recoveryKeyFile = io.File(
|
||||
Configuration.instance.getTempDirectory() + "ente-recovery-key.txt");
|
||||
Configuration.instance.getTempDirectory() + "ente-recovery-key.txt",
|
||||
);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final String recoveryKey = bip39.entropyToMnemonic(widget.recoveryKey);
|
||||
if (recoveryKey.split(' ').length != kMnemonicKeyWordCount) {
|
||||
throw AssertionError(
|
||||
'recovery code should have $kMnemonicKeyWordCount words');
|
||||
'recovery code should have $kMnemonicKeyWordCount words',
|
||||
);
|
||||
}
|
||||
|
||||
return Scaffold(
|
||||
|
@ -74,22 +78,25 @@ class _RecoveryKeyPageState extends State<RecoveryKeyPage> {
|
|||
: null,
|
||||
body: Padding(
|
||||
padding: EdgeInsets.fromLTRB(
|
||||
20,
|
||||
widget.showAppBar
|
||||
? 40
|
||||
: widget.showProgressBar
|
||||
? 32
|
||||
: 120,
|
||||
20,
|
||||
20),
|
||||
20,
|
||||
widget.showAppBar
|
||||
? 40
|
||||
: widget.showProgressBar
|
||||
? 32
|
||||
: 120,
|
||||
20,
|
||||
20,
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
mainAxisSize: MainAxisSize.max,
|
||||
children: [
|
||||
widget.showAppBar
|
||||
? const SizedBox.shrink()
|
||||
: Text(widget.title ?? "Recovery key",
|
||||
style: Theme.of(context).textTheme.headline4),
|
||||
: Text(
|
||||
widget.title ?? "Recovery key",
|
||||
style: Theme.of(context).textTheme.headline4,
|
||||
),
|
||||
Padding(padding: EdgeInsets.all(widget.showAppBar ? 0 : 12)),
|
||||
Text(
|
||||
widget.text ??
|
||||
|
@ -116,7 +123,8 @@ class _RecoveryKeyPageState extends State<RecoveryKeyPage> {
|
|||
GestureDetector(
|
||||
onTap: () async {
|
||||
await Clipboard.setData(
|
||||
ClipboardData(text: recoveryKey));
|
||||
ClipboardData(text: recoveryKey),
|
||||
);
|
||||
showToast(context, "Recovery key copied to clipboard");
|
||||
setState(() {
|
||||
_hasTriedToSave = true;
|
||||
|
@ -150,12 +158,13 @@ class _RecoveryKeyPageState extends State<RecoveryKeyPage> {
|
|||
height: 80,
|
||||
width: double.infinity,
|
||||
child: Padding(
|
||||
child: Text(
|
||||
widget.subText ??
|
||||
"We don’t store this key, please save this in a safe place.",
|
||||
style: Theme.of(context).textTheme.bodyText1,
|
||||
),
|
||||
padding: EdgeInsets.symmetric(vertical: 20)),
|
||||
child: Text(
|
||||
widget.subText ??
|
||||
"We don’t store this key, please save this in a safe place.",
|
||||
style: Theme.of(context).textTheme.bodyText1,
|
||||
),
|
||||
padding: EdgeInsets.symmetric(vertical: 20),
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
child: Container(
|
||||
|
@ -163,9 +172,10 @@ class _RecoveryKeyPageState extends State<RecoveryKeyPage> {
|
|||
width: double.infinity,
|
||||
padding: EdgeInsets.fromLTRB(10, 10, 10, 24),
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.end,
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: _saveOptions(context, recoveryKey)),
|
||||
mainAxisAlignment: MainAxisAlignment.end,
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: _saveOptions(context, recoveryKey),
|
||||
),
|
||||
),
|
||||
)
|
||||
],
|
||||
|
@ -177,37 +187,43 @@ class _RecoveryKeyPageState extends State<RecoveryKeyPage> {
|
|||
List<Widget> _saveOptions(BuildContext context, String recoveryKey) {
|
||||
List<Widget> childrens = [];
|
||||
if (!_hasTriedToSave) {
|
||||
childrens.add(ElevatedButton(
|
||||
child: Text('Do this later'),
|
||||
style: Theme.of(context).colorScheme.optionalActionButtonStyle,
|
||||
onPressed: () async {
|
||||
await _saveKeys();
|
||||
},
|
||||
));
|
||||
childrens.add(
|
||||
ElevatedButton(
|
||||
child: Text('Do this later'),
|
||||
style: Theme.of(context).colorScheme.optionalActionButtonStyle,
|
||||
onPressed: () async {
|
||||
await _saveKeys();
|
||||
},
|
||||
),
|
||||
);
|
||||
childrens.add(SizedBox(height: 10));
|
||||
}
|
||||
|
||||
childrens.add(GradientButton(
|
||||
child: Text(
|
||||
'Save key',
|
||||
style: gradientButtonTextTheme(),
|
||||
childrens.add(
|
||||
GradientButton(
|
||||
child: Text(
|
||||
'Save key',
|
||||
style: gradientButtonTextTheme(),
|
||||
),
|
||||
linearGradientColors: const [
|
||||
Color(0xFF2CD267),
|
||||
Color(0xFF1DB954),
|
||||
],
|
||||
onTap: () async {
|
||||
await _shareRecoveryKey(recoveryKey);
|
||||
},
|
||||
),
|
||||
linearGradientColors: const [
|
||||
Color(0xFF2CD267),
|
||||
Color(0xFF1DB954),
|
||||
],
|
||||
onTap: () async {
|
||||
await _shareRecoveryKey(recoveryKey);
|
||||
},
|
||||
));
|
||||
);
|
||||
if (_hasTriedToSave) {
|
||||
childrens.add(SizedBox(height: 10));
|
||||
childrens.add(ElevatedButton(
|
||||
child: Text(widget.doneText),
|
||||
onPressed: () async {
|
||||
await _saveKeys();
|
||||
},
|
||||
));
|
||||
childrens.add(
|
||||
ElevatedButton(
|
||||
child: Text(widget.doneText),
|
||||
onPressed: () async {
|
||||
await _saveKeys();
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
childrens.add(SizedBox(height: 12));
|
||||
return childrens;
|
||||
|
|
|
@ -31,37 +31,38 @@ class _RecoveryPageState extends State<RecoveryPage> {
|
|||
),
|
||||
),
|
||||
floatingActionButton: DynamicFAB(
|
||||
isKeypadOpen: false,
|
||||
isFormValid: _recoveryKey.text.isNotEmpty,
|
||||
buttonText: 'Recover',
|
||||
onPressedFunction: () async {
|
||||
final dialog = createProgressDialog(context, "Decrypting...");
|
||||
await dialog.show();
|
||||
try {
|
||||
await Configuration.instance.recover(_recoveryKey.text.trim());
|
||||
await dialog.hide();
|
||||
showToast(context, "Recovery successful!");
|
||||
Navigator.of(context).pushReplacement(
|
||||
MaterialPageRoute(
|
||||
builder: (BuildContext context) {
|
||||
return WillPopScope(
|
||||
onWillPop: () async => false,
|
||||
child: PasswordEntryPage(
|
||||
mode: PasswordEntryMode.reset,
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
} catch (e) {
|
||||
await dialog.hide();
|
||||
String errMessage = 'the recovery key you entered is incorrect';
|
||||
if (e is AssertionError) {
|
||||
errMessage = '$errMessage : ${e.message}';
|
||||
}
|
||||
showErrorDialog(context, "incorrect recovery key", errMessage);
|
||||
isKeypadOpen: false,
|
||||
isFormValid: _recoveryKey.text.isNotEmpty,
|
||||
buttonText: 'Recover',
|
||||
onPressedFunction: () async {
|
||||
final dialog = createProgressDialog(context, "Decrypting...");
|
||||
await dialog.show();
|
||||
try {
|
||||
await Configuration.instance.recover(_recoveryKey.text.trim());
|
||||
await dialog.hide();
|
||||
showToast(context, "Recovery successful!");
|
||||
Navigator.of(context).pushReplacement(
|
||||
MaterialPageRoute(
|
||||
builder: (BuildContext context) {
|
||||
return WillPopScope(
|
||||
onWillPop: () async => false,
|
||||
child: PasswordEntryPage(
|
||||
mode: PasswordEntryMode.reset,
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
} catch (e) {
|
||||
await dialog.hide();
|
||||
String errMessage = 'the recovery key you entered is incorrect';
|
||||
if (e is AssertionError) {
|
||||
errMessage = '$errMessage : ${e.message}';
|
||||
}
|
||||
}),
|
||||
showErrorDialog(context, "incorrect recovery key", errMessage);
|
||||
}
|
||||
},
|
||||
),
|
||||
floatingActionButtonLocation: FloatingActionButtonLocation.centerFloat,
|
||||
floatingActionButtonAnimator: NoScalingAnimation(),
|
||||
body: Column(
|
||||
|
@ -72,8 +73,10 @@ class _RecoveryPageState extends State<RecoveryPage> {
|
|||
Padding(
|
||||
padding:
|
||||
const EdgeInsets.symmetric(vertical: 30, horizontal: 20),
|
||||
child: Text('Forgot password',
|
||||
style: Theme.of(context).textTheme.headline4),
|
||||
child: Text(
|
||||
'Forgot password',
|
||||
style: Theme.of(context).textTheme.headline4,
|
||||
),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.fromLTRB(20, 24, 20, 0),
|
||||
|
@ -83,8 +86,9 @@ class _RecoveryPageState extends State<RecoveryPage> {
|
|||
hintText: "Enter your recovery key",
|
||||
contentPadding: EdgeInsets.all(20),
|
||||
border: UnderlineInputBorder(
|
||||
borderSide: BorderSide.none,
|
||||
borderRadius: BorderRadius.circular(6)),
|
||||
borderSide: BorderSide.none,
|
||||
borderRadius: BorderRadius.circular(6),
|
||||
),
|
||||
),
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
|
@ -122,12 +126,11 @@ class _RecoveryPageState extends State<RecoveryPage> {
|
|||
child: Center(
|
||||
child: Text(
|
||||
"No recovery key?",
|
||||
style: Theme.of(context)
|
||||
.textTheme
|
||||
.subtitle1
|
||||
.copyWith(
|
||||
fontSize: 14,
|
||||
decoration: TextDecoration.underline),
|
||||
style:
|
||||
Theme.of(context).textTheme.subtitle1.copyWith(
|
||||
fontSize: 14,
|
||||
decoration: TextDecoration.underline,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
|
|
@ -74,12 +74,18 @@ class _RenameDialogState extends State<RenameDialog> {
|
|||
onPressed: () {
|
||||
if (_newName.trim().isEmpty) {
|
||||
showErrorDialog(
|
||||
context, "Empty name", "${widget.type} name cannot be empty");
|
||||
context,
|
||||
"Empty name",
|
||||
"${widget.type} name cannot be empty",
|
||||
);
|
||||
return;
|
||||
}
|
||||
if (_newName.trim().length > widget.maxLength) {
|
||||
showErrorDialog(context, "Name too large",
|
||||
"${widget.type} name should be less than ${widget.maxLength} characters");
|
||||
showErrorDialog(
|
||||
context,
|
||||
"Name too large",
|
||||
"${widget.type} name should be less than ${widget.maxLength} characters",
|
||||
);
|
||||
return;
|
||||
}
|
||||
Navigator.of(context).pop(_newName.trim());
|
||||
|
|
|
@ -120,7 +120,10 @@ class _SessionsPageState extends State<SessionsPage> {
|
|||
await dialog.hide();
|
||||
_logger.severe('failed to terminate', e, s);
|
||||
showErrorDialog(
|
||||
context, 'Oops', "Something went wrong, please try again");
|
||||
context,
|
||||
'Oops',
|
||||
"Something went wrong, please try again",
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -73,7 +73,9 @@ class _SetWallpaperDialogState extends State<SetWallpaperDialog> {
|
|||
await dialog.show();
|
||||
try {
|
||||
await WallpaperManagerFlutter().setwallpaperfromFile(
|
||||
await getFile(widget.file), _lockscreenValue);
|
||||
await getFile(widget.file),
|
||||
_lockscreenValue,
|
||||
);
|
||||
await dialog.hide();
|
||||
showToast(context, "Wallpaper set successfully");
|
||||
} catch (e, s) {
|
||||
|
|
|
@ -50,7 +50,9 @@ class AccountSectionWidgetState extends State<AccountSectionWidget> {
|
|||
);
|
||||
},
|
||||
child: SettingsTextItem(
|
||||
text: "Subscription plan", icon: Icons.navigate_next),
|
||||
text: "Subscription plan",
|
||||
icon: Icons.navigate_next,
|
||||
),
|
||||
),
|
||||
SectionOptionDivider,
|
||||
GestureDetector(
|
||||
|
@ -74,9 +76,14 @@ class AccountSectionWidgetState extends State<AccountSectionWidget> {
|
|||
return;
|
||||
}
|
||||
routeToPage(
|
||||
context,
|
||||
RecoveryKeyPage(recoveryKey, "OK",
|
||||
showAppBar: true, onDone: () {}));
|
||||
context,
|
||||
RecoveryKeyPage(
|
||||
recoveryKey,
|
||||
"OK",
|
||||
showAppBar: true,
|
||||
onDone: () {},
|
||||
),
|
||||
);
|
||||
},
|
||||
child:
|
||||
SettingsTextItem(text: "Recovery key", icon: Icons.navigate_next),
|
||||
|
@ -130,7 +137,9 @@ class AccountSectionWidgetState extends State<AccountSectionWidget> {
|
|||
);
|
||||
},
|
||||
child: SettingsTextItem(
|
||||
text: "Change password", icon: Icons.navigate_next),
|
||||
text: "Change password",
|
||||
icon: Icons.navigate_next,
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
|
@ -138,6 +147,7 @@ class AccountSectionWidgetState extends State<AccountSectionWidget> {
|
|||
|
||||
Future<String> _getOrCreateRecoveryKey() async {
|
||||
return Sodium.bin2hex(
|
||||
await UserService.instance.getOrCreateRecoveryKey(context));
|
||||
await UserService.instance.getOrCreateRecoveryKey(context),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Reference in a new issue