Merge remote-tracking branch 'origin/master' into theme
This commit is contained in:
commit
2c03a09808
17 changed files with 331 additions and 60 deletions
|
@ -75,7 +75,7 @@ class Configuration {
|
||||||
String _volatilePassword;
|
String _volatilePassword;
|
||||||
|
|
||||||
final _secureStorageOptionsIOS =
|
final _secureStorageOptionsIOS =
|
||||||
IOSOptions(accessibility: IOSAccessibility.first_unlock_this_device);
|
IOSOptions(accessibility: IOSAccessibility.first_unlock);
|
||||||
|
|
||||||
Future<void> init() async {
|
Future<void> init() async {
|
||||||
_preferences = await SharedPreferences.getInstance();
|
_preferences = await SharedPreferences.getInstance();
|
||||||
|
|
|
@ -24,3 +24,6 @@ const kThumbnailServerLoadDeferDuration = Duration(milliseconds: 80);
|
||||||
// 256 bit key maps to 24 words
|
// 256 bit key maps to 24 words
|
||||||
// https://github.com/bitcoin/bips/blob/master/bip-0039.mediawiki#Generating_the_mnemonic
|
// https://github.com/bitcoin/bips/blob/master/bip-0039.mediawiki#Generating_the_mnemonic
|
||||||
const kMnemonicKeyWordCount = 24;
|
const kMnemonicKeyWordCount = 24;
|
||||||
|
|
||||||
|
// https://stackoverflow.com/a/61162219
|
||||||
|
const kDragSensitivity = 8;
|
||||||
|
|
|
@ -27,6 +27,9 @@ class CollectionsDB {
|
||||||
static final columnVersion = 'version';
|
static final columnVersion = 'version';
|
||||||
static final columnSharees = 'sharees';
|
static final columnSharees = 'sharees';
|
||||||
static final columnPublicURLs = 'public_urls';
|
static final columnPublicURLs = 'public_urls';
|
||||||
|
// MMD -> Magic Metadata
|
||||||
|
static final columnMMdEncodedJson = 'mmd_encoded_json';
|
||||||
|
static final columnMMdVersion = 'mmd_ver';
|
||||||
static final columnUpdationTime = 'updation_time';
|
static final columnUpdationTime = 'updation_time';
|
||||||
static final columnIsDeleted = 'is_deleted';
|
static final columnIsDeleted = 'is_deleted';
|
||||||
|
|
||||||
|
@ -37,6 +40,7 @@ class CollectionsDB {
|
||||||
...addVersion(),
|
...addVersion(),
|
||||||
...addIsDeleted(),
|
...addIsDeleted(),
|
||||||
...addPublicURLs(),
|
...addPublicURLs(),
|
||||||
|
...addPrivateMetadata(),
|
||||||
];
|
];
|
||||||
|
|
||||||
final dbConfig = MigrationConfig(
|
final dbConfig = MigrationConfig(
|
||||||
|
@ -138,6 +142,17 @@ class CollectionsDB {
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static List<String> addPrivateMetadata() {
|
||||||
|
return [
|
||||||
|
'''
|
||||||
|
ALTER TABLE $table ADD COLUMN $columnMMdEncodedJson TEXT DEFAULT '{}';
|
||||||
|
''',
|
||||||
|
'''
|
||||||
|
ALTER TABLE $table ADD COLUMN $columnMMdVersion INTEGER DEFAULT 0;
|
||||||
|
'''
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
Future<List<dynamic>> insert(List<Collection> collections) async {
|
Future<List<dynamic>> insert(List<Collection> collections) async {
|
||||||
final db = await instance.database;
|
final db = await instance.database;
|
||||||
var batch = db.batch();
|
var batch = db.batch();
|
||||||
|
@ -204,11 +219,13 @@ class CollectionsDB {
|
||||||
} else {
|
} else {
|
||||||
row[columnIsDeleted] = _sqlBoolFalse;
|
row[columnIsDeleted] = _sqlBoolFalse;
|
||||||
}
|
}
|
||||||
|
row[columnMMdVersion] = collection.mMdVersion ?? 0;
|
||||||
|
row[columnMMdEncodedJson] = collection.mMdEncodedJson ?? '{}';
|
||||||
return row;
|
return row;
|
||||||
}
|
}
|
||||||
|
|
||||||
Collection _convertToCollection(Map<String, dynamic> row) {
|
Collection _convertToCollection(Map<String, dynamic> row) {
|
||||||
return Collection(
|
Collection result = Collection(
|
||||||
row[columnID],
|
row[columnID],
|
||||||
User.fromJson(row[columnOwner]),
|
User.fromJson(row[columnOwner]),
|
||||||
row[columnEncryptedKey],
|
row[columnEncryptedKey],
|
||||||
|
@ -232,5 +249,8 @@ class CollectionsDB {
|
||||||
// default to False is columnIsDeleted is not set
|
// default to False is columnIsDeleted is not set
|
||||||
isDeleted: (row[columnIsDeleted] ?? _sqlBoolFalse) == _sqlBoolTrue,
|
isDeleted: (row[columnIsDeleted] ?? _sqlBoolFalse) == _sqlBoolTrue,
|
||||||
);
|
);
|
||||||
|
result.mMdVersion = row[columnMMdVersion] ?? 0;
|
||||||
|
result.mMdEncodedJson = row[columnMMdEncodedJson] ?? '{}';
|
||||||
|
return result;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -399,7 +399,10 @@ class FilesDB {
|
||||||
|
|
||||||
Future<FileLoadResult> getAllUploadedFiles(
|
Future<FileLoadResult> getAllUploadedFiles(
|
||||||
int startTime, int endTime, int ownerID,
|
int startTime, int endTime, int ownerID,
|
||||||
{int limit, bool asc, int visibility = kVisibilityVisible}) async {
|
{int limit,
|
||||||
|
bool asc,
|
||||||
|
int visibility = kVisibilityVisible,
|
||||||
|
Set<int> ignoredCollectionIDs}) async {
|
||||||
final db = await instance.database;
|
final db = await instance.database;
|
||||||
final order = (asc ?? false ? 'ASC' : 'DESC');
|
final order = (asc ?? false ? 'ASC' : 'DESC');
|
||||||
final results = await db.query(
|
final results = await db.query(
|
||||||
|
@ -413,13 +416,14 @@ class FilesDB {
|
||||||
limit: limit,
|
limit: limit,
|
||||||
);
|
);
|
||||||
final files = _convertToFiles(results);
|
final files = _convertToFiles(results);
|
||||||
List<File> deduplicatedFiles = _deduplicatedFiles(files);
|
List<File> deduplicatedFiles =
|
||||||
|
_deduplicatedAndFilterIgnoredFiles(files, ignoredCollectionIDs);
|
||||||
return FileLoadResult(deduplicatedFiles, files.length == limit);
|
return FileLoadResult(deduplicatedFiles, files.length == limit);
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<FileLoadResult> getAllLocalAndUploadedFiles(
|
Future<FileLoadResult> getAllLocalAndUploadedFiles(
|
||||||
int startTime, int endTime, int ownerID,
|
int startTime, int endTime, int ownerID,
|
||||||
{int limit, bool asc}) async {
|
{int limit, bool asc, Set<int> ignoredCollectionIDs}) async {
|
||||||
final db = await instance.database;
|
final db = await instance.database;
|
||||||
final order = (asc ?? false ? 'ASC' : 'DESC');
|
final order = (asc ?? false ? 'ASC' : 'DESC');
|
||||||
final results = await db.query(
|
final results = await db.query(
|
||||||
|
@ -433,13 +437,14 @@ class FilesDB {
|
||||||
limit: limit,
|
limit: limit,
|
||||||
);
|
);
|
||||||
final files = _convertToFiles(results);
|
final files = _convertToFiles(results);
|
||||||
List<File> deduplicatedFiles = _deduplicatedFiles(files);
|
List<File> deduplicatedFiles =
|
||||||
|
_deduplicatedAndFilterIgnoredFiles(files, ignoredCollectionIDs);
|
||||||
return FileLoadResult(deduplicatedFiles, files.length == limit);
|
return FileLoadResult(deduplicatedFiles, files.length == limit);
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<FileLoadResult> getImportantFiles(
|
Future<FileLoadResult> getImportantFiles(
|
||||||
int startTime, int endTime, int ownerID, List<String> paths,
|
int startTime, int endTime, int ownerID, List<String> paths,
|
||||||
{int limit, bool asc}) async {
|
{int limit, bool asc, Set<int> ignoredCollectionIDs}) async {
|
||||||
final db = await instance.database;
|
final db = await instance.database;
|
||||||
String inParam = "";
|
String inParam = "";
|
||||||
for (final path in paths) {
|
for (final path in paths) {
|
||||||
|
@ -458,15 +463,21 @@ class FilesDB {
|
||||||
limit: limit,
|
limit: limit,
|
||||||
);
|
);
|
||||||
final files = _convertToFiles(results);
|
final files = _convertToFiles(results);
|
||||||
List<File> deduplicatedFiles = _deduplicatedFiles(files);
|
List<File> deduplicatedFiles =
|
||||||
|
_deduplicatedAndFilterIgnoredFiles(files, ignoredCollectionIDs);
|
||||||
return FileLoadResult(deduplicatedFiles, files.length == limit);
|
return FileLoadResult(deduplicatedFiles, files.length == limit);
|
||||||
}
|
}
|
||||||
|
|
||||||
List<File> _deduplicatedFiles(List<File> files) {
|
List<File> _deduplicatedAndFilterIgnoredFiles(
|
||||||
|
List<File> files, Set<int> ignoredCollectionIDs) {
|
||||||
final uploadedFileIDs = <int>{};
|
final uploadedFileIDs = <int>{};
|
||||||
final List<File> deduplicatedFiles = [];
|
final List<File> deduplicatedFiles = [];
|
||||||
for (final file in files) {
|
for (final file in files) {
|
||||||
final id = file.uploadedFileID;
|
final id = file.uploadedFileID;
|
||||||
|
if (ignoredCollectionIDs != null &&
|
||||||
|
ignoredCollectionIDs.contains(file.collectionID)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
if (id != null && id != -1 && uploadedFileIDs.contains(id)) {
|
if (id != null && id != -1 && uploadedFileIDs.contains(id)) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,8 @@
|
||||||
import 'dart:convert';
|
import 'dart:convert';
|
||||||
|
import 'dart:core';
|
||||||
|
|
||||||
import 'package:flutter/foundation.dart';
|
import 'package:flutter/foundation.dart';
|
||||||
|
import 'package:photos/models/magic_metadata.dart';
|
||||||
|
|
||||||
class Collection {
|
class Collection {
|
||||||
final int id;
|
final int id;
|
||||||
|
@ -16,6 +18,14 @@ class Collection {
|
||||||
final List<PublicURL> publicURLs;
|
final List<PublicURL> publicURLs;
|
||||||
final int updationTime;
|
final int updationTime;
|
||||||
final bool isDeleted;
|
final bool isDeleted;
|
||||||
|
String mMdEncodedJson;
|
||||||
|
int mMdVersion = 0;
|
||||||
|
CollectionMagicMetadata _mmd;
|
||||||
|
|
||||||
|
CollectionMagicMetadata get magicMetadata =>
|
||||||
|
_mmd ?? CollectionMagicMetadata.fromEncodedJson(mMdEncodedJson ?? '{}');
|
||||||
|
|
||||||
|
set magicMetadata(val) => _mmd = val;
|
||||||
|
|
||||||
Collection(
|
Collection(
|
||||||
this.id,
|
this.id,
|
||||||
|
@ -33,6 +43,10 @@ class Collection {
|
||||||
this.isDeleted = false,
|
this.isDeleted = false,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
bool isArchived() {
|
||||||
|
return mMdVersion > 0 && magicMetadata.visibility == kVisibilityArchive;
|
||||||
|
}
|
||||||
|
|
||||||
static CollectionType typeFromString(String type) {
|
static CollectionType typeFromString(String type) {
|
||||||
switch (type) {
|
switch (type) {
|
||||||
case "folder":
|
case "folder":
|
||||||
|
@ -54,8 +68,8 @@ class Collection {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Collection copyWith({
|
Collection copyWith(
|
||||||
int id,
|
{int id,
|
||||||
User owner,
|
User owner,
|
||||||
String encryptedKey,
|
String encryptedKey,
|
||||||
String keyDecryptionNonce,
|
String keyDecryptionNonce,
|
||||||
|
@ -68,8 +82,9 @@ class Collection {
|
||||||
List<PublicURL> publicURLs,
|
List<PublicURL> publicURLs,
|
||||||
int updationTime,
|
int updationTime,
|
||||||
bool isDeleted,
|
bool isDeleted,
|
||||||
}) {
|
String mMdEncodedJson,
|
||||||
return Collection(
|
int mMdVersion}) {
|
||||||
|
Collection result = Collection(
|
||||||
id ?? this.id,
|
id ?? this.id,
|
||||||
owner ?? this.owner,
|
owner ?? this.owner,
|
||||||
encryptedKey ?? this.encryptedKey,
|
encryptedKey ?? this.encryptedKey,
|
||||||
|
@ -84,6 +99,9 @@ class Collection {
|
||||||
updationTime ?? this.updationTime,
|
updationTime ?? this.updationTime,
|
||||||
isDeleted: isDeleted ?? this.isDeleted,
|
isDeleted: isDeleted ?? this.isDeleted,
|
||||||
);
|
);
|
||||||
|
result.mMdVersion = mMdVersion ?? this.mMdVersion;
|
||||||
|
result.mMdEncodedJson = mMdEncodedJson ?? this.mMdEncodedJson;
|
||||||
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
Map<String, dynamic> toMap() {
|
Map<String, dynamic> toMap() {
|
||||||
|
|
|
@ -62,3 +62,30 @@ class PubMagicMetadata {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class CollectionMagicMetadata {
|
||||||
|
// 0 -> visible
|
||||||
|
// 1 -> archived
|
||||||
|
// 2 -> hidden etc?
|
||||||
|
int visibility;
|
||||||
|
|
||||||
|
CollectionMagicMetadata({this.visibility});
|
||||||
|
|
||||||
|
factory CollectionMagicMetadata.fromEncodedJson(String encodedJson) =>
|
||||||
|
CollectionMagicMetadata.fromJson(jsonDecode(encodedJson));
|
||||||
|
|
||||||
|
factory CollectionMagicMetadata.fromJson(dynamic json) => CollectionMagicMetadata.fromMap(json);
|
||||||
|
|
||||||
|
Map<String, dynamic> toJson() {
|
||||||
|
final map = <String, dynamic>{};
|
||||||
|
map[kMagicKeyVisibility] = visibility;
|
||||||
|
return map;
|
||||||
|
}
|
||||||
|
|
||||||
|
factory CollectionMagicMetadata.fromMap(Map<String, dynamic> map) {
|
||||||
|
if (map == null) return null;
|
||||||
|
return CollectionMagicMetadata(
|
||||||
|
visibility: map[kMagicKeyVisibility] ?? kVisibilityVisible,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
import 'dart:convert';
|
import 'dart:convert';
|
||||||
|
import 'dart:math';
|
||||||
import 'dart:typed_data';
|
import 'dart:typed_data';
|
||||||
|
|
||||||
import 'package:dio/dio.dart';
|
import 'package:dio/dio.dart';
|
||||||
|
@ -19,7 +20,9 @@ import 'package:photos/events/local_photos_updated_event.dart';
|
||||||
import 'package:photos/models/collection.dart';
|
import 'package:photos/models/collection.dart';
|
||||||
import 'package:photos/models/collection_file_item.dart';
|
import 'package:photos/models/collection_file_item.dart';
|
||||||
import 'package:photos/models/file.dart';
|
import 'package:photos/models/file.dart';
|
||||||
|
import 'package:photos/models/magic_metadata.dart';
|
||||||
import 'package:photos/services/app_lifecycle_service.dart';
|
import 'package:photos/services/app_lifecycle_service.dart';
|
||||||
|
import 'package:photos/services/file_magic_service.dart';
|
||||||
import 'package:photos/services/remote_sync_service.dart';
|
import 'package:photos/services/remote_sync_service.dart';
|
||||||
import 'package:photos/utils/crypto_util.dart';
|
import 'package:photos/utils/crypto_util.dart';
|
||||||
import 'package:photos/utils/file_download_util.dart';
|
import 'package:photos/utils/file_download_util.dart';
|
||||||
|
@ -27,7 +30,7 @@ import 'package:shared_preferences/shared_preferences.dart';
|
||||||
|
|
||||||
class CollectionsService {
|
class CollectionsService {
|
||||||
static final _collectionSyncTimeKeyPrefix = "collection_sync_time_";
|
static final _collectionSyncTimeKeyPrefix = "collection_sync_time_";
|
||||||
static final _collectionsSyncTimeKey = "collections_sync_time";
|
static final _collectionsSyncTimeKey = "collections_sync_time_x";
|
||||||
|
|
||||||
static const int kMaximumWriteAttempts = 5;
|
static const int kMaximumWriteAttempts = 5;
|
||||||
|
|
||||||
|
@ -55,6 +58,7 @@ class CollectionsService {
|
||||||
Future<void> init() async {
|
Future<void> init() async {
|
||||||
_prefs = await SharedPreferences.getInstance();
|
_prefs = await SharedPreferences.getInstance();
|
||||||
final collections = await _db.getAllCollections();
|
final collections = await _db.getAllCollections();
|
||||||
|
|
||||||
for (final collection in collections) {
|
for (final collection in collections) {
|
||||||
_cacheCollectionAttributes(collection);
|
_cacheCollectionAttributes(collection);
|
||||||
}
|
}
|
||||||
|
@ -74,8 +78,7 @@ class CollectionsService {
|
||||||
_prefs.getInt(_collectionsSyncTimeKey) ?? 0;
|
_prefs.getInt(_collectionsSyncTimeKey) ?? 0;
|
||||||
|
|
||||||
// Might not have synced the collection fully
|
// Might not have synced the collection fully
|
||||||
final fetchedCollections =
|
final fetchedCollections = await _fetchCollections(0);
|
||||||
await _fetchCollections(lastCollectionUpdationTime ?? 0);
|
|
||||||
final updatedCollections = <Collection>[];
|
final updatedCollections = <Collection>[];
|
||||||
int maxUpdationTime = lastCollectionUpdationTime;
|
int maxUpdationTime = lastCollectionUpdationTime;
|
||||||
final ownerID = _config.getUserID();
|
final ownerID = _config.getUserID();
|
||||||
|
@ -127,6 +130,14 @@ class CollectionsService {
|
||||||
return updatedCollections;
|
return updatedCollections;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Set<int> getArchivedCollections() {
|
||||||
|
return _collectionIDToCollections.values
|
||||||
|
.toList()
|
||||||
|
.where((element) => element.isArchived())
|
||||||
|
.map((e) => e.id)
|
||||||
|
.toSet();
|
||||||
|
}
|
||||||
|
|
||||||
int getCollectionSyncTime(int collectionID) {
|
int getCollectionSyncTime(int collectionID) {
|
||||||
return _prefs
|
return _prefs
|
||||||
.getInt(_collectionSyncTimeKeyPrefix + collectionID.toString()) ??
|
.getInt(_collectionSyncTimeKeyPrefix + collectionID.toString()) ??
|
||||||
|
@ -273,6 +284,64 @@ class CollectionsService {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<void> updateMagicMetadata(
|
||||||
|
Collection collection, Map<String, dynamic> newMetadataUpdate) async {
|
||||||
|
final int ownerID = Configuration.instance.getUserID();
|
||||||
|
try {
|
||||||
|
if (collection.owner.id != ownerID) {
|
||||||
|
throw AssertionError("cannot modify albums not owned by you");
|
||||||
|
}
|
||||||
|
// read the existing magic metadata and apply new updates to existing data
|
||||||
|
// current update is simple replace. This will be enhanced in the future,
|
||||||
|
// as required.
|
||||||
|
Map<String, dynamic> jsonToUpdate =
|
||||||
|
jsonDecode(collection.mMdEncodedJson ?? '{}');
|
||||||
|
newMetadataUpdate.forEach((key, value) {
|
||||||
|
jsonToUpdate[key] = value;
|
||||||
|
});
|
||||||
|
|
||||||
|
// update the local information so that it's reflected on UI
|
||||||
|
collection.mMdEncodedJson = jsonEncode(jsonToUpdate);
|
||||||
|
collection.magicMetadata = CollectionMagicMetadata.fromJson(jsonToUpdate);
|
||||||
|
|
||||||
|
final key = getCollectionKey(collection.id);
|
||||||
|
final encryptedMMd = await CryptoUtil.encryptChaCha(
|
||||||
|
utf8.encode(jsonEncode(jsonToUpdate)), key);
|
||||||
|
// for required field, the json validator on golang doesn't treat 0 as valid
|
||||||
|
// value. Instead of changing version to ptr, decided to start version with 1.
|
||||||
|
int currentVersion = max(collection.mMdVersion, 1);
|
||||||
|
final params = UpdateMagicMetadataRequest(
|
||||||
|
id: collection.id,
|
||||||
|
magicMetadata: MetadataRequest(
|
||||||
|
version: currentVersion,
|
||||||
|
count: jsonToUpdate.length,
|
||||||
|
data: Sodium.bin2base64(encryptedMMd.encryptedData),
|
||||||
|
header: Sodium.bin2base64(encryptedMMd.header),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
await _dio.put(
|
||||||
|
Configuration.instance.getHttpEndpoint() +
|
||||||
|
"/collections/magic-metadata",
|
||||||
|
data: params,
|
||||||
|
options: Options(
|
||||||
|
headers: {"X-Auth-Token": Configuration.instance.getToken()}),
|
||||||
|
);
|
||||||
|
collection.mMdVersion = currentVersion + 1;
|
||||||
|
_cacheCollectionAttributes(collection);
|
||||||
|
// trigger sync to fetch the latest collection state from server
|
||||||
|
sync();
|
||||||
|
} on DioError catch (e) {
|
||||||
|
if (e.response != null && e.response.statusCode == 409) {
|
||||||
|
_logger.severe('collection magic data out of sync');
|
||||||
|
sync();
|
||||||
|
}
|
||||||
|
rethrow;
|
||||||
|
} catch (e, s) {
|
||||||
|
_logger.severe("failed to sync magic metadata", e, s);
|
||||||
|
rethrow;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Future<void> createShareUrl(Collection collection) async {
|
Future<void> createShareUrl(Collection collection) async {
|
||||||
try {
|
try {
|
||||||
final response = await _dio.post(
|
final response = await _dio.post(
|
||||||
|
@ -361,8 +430,20 @@ class CollectionsService {
|
||||||
final List<Collection> collections = [];
|
final List<Collection> collections = [];
|
||||||
if (response != null) {
|
if (response != null) {
|
||||||
final c = response.data["collections"];
|
final c = response.data["collections"];
|
||||||
for (final collection in c) {
|
for (final collectionData in c) {
|
||||||
collections.add(Collection.fromMap(collection));
|
final collection = Collection.fromMap(collectionData);
|
||||||
|
if (collectionData['magicMetadata'] != null) {
|
||||||
|
final decryptionKey = _getDecryptedKey(collection);
|
||||||
|
final utfEncodedMmd = await CryptoUtil.decryptChaCha(
|
||||||
|
Sodium.base642bin(collectionData['magicMetadata']['data']),
|
||||||
|
decryptionKey,
|
||||||
|
Sodium.base642bin(collectionData['magicMetadata']['header']));
|
||||||
|
collection.mMdEncodedJson = utf8.decode(utfEncodedMmd);
|
||||||
|
collection.mMdVersion = collectionData['magicMetadata']['version'];
|
||||||
|
collection.magicMetadata = CollectionMagicMetadata.fromEncodedJson(
|
||||||
|
collection.mMdEncodedJson);
|
||||||
|
}
|
||||||
|
collections.add(collection);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return collections;
|
return collections;
|
||||||
|
@ -408,7 +489,19 @@ class CollectionsService {
|
||||||
headers: {"X-Auth-Token": Configuration.instance.getToken()}),
|
headers: {"X-Auth-Token": Configuration.instance.getToken()}),
|
||||||
);
|
);
|
||||||
assert(response != null && response.data != null);
|
assert(response != null && response.data != null);
|
||||||
final collection = Collection.fromMap(response.data["collection"]);
|
final collectionData = response.data["collection"];
|
||||||
|
final collection = Collection.fromMap(collectionData);
|
||||||
|
if (collectionData['magicMetadata'] != null) {
|
||||||
|
final decryptionKey = _getDecryptedKey(collection);
|
||||||
|
final utfEncodedMmd = await CryptoUtil.decryptChaCha(
|
||||||
|
Sodium.base642bin(collectionData['magicMetadata']['data']),
|
||||||
|
decryptionKey,
|
||||||
|
Sodium.base642bin(collectionData['magicMetadata']['header']));
|
||||||
|
collection.mMdEncodedJson = utf8.decode(utfEncodedMmd);
|
||||||
|
collection.mMdVersion = collectionData['magicMetadata']['version'];
|
||||||
|
collection.magicMetadata =
|
||||||
|
CollectionMagicMetadata.fromEncodedJson(collection.mMdEncodedJson);
|
||||||
|
}
|
||||||
await _db.insert(List.from([collection]));
|
await _db.insert(List.from([collection]));
|
||||||
_cacheCollectionAttributes(collection);
|
_cacheCollectionAttributes(collection);
|
||||||
return collection;
|
return collection;
|
||||||
|
|
|
@ -217,7 +217,7 @@ class RemoteSyncService {
|
||||||
// uploaded yet. These files should ignore video backup & ignored files filter
|
// uploaded yet. These files should ignore video backup & ignored files filter
|
||||||
filesToBeUploaded = await _db.getPendingManualUploads();
|
filesToBeUploaded = await _db.getPendingManualUploads();
|
||||||
}
|
}
|
||||||
_moveVideosToEnd(filesToBeUploaded);
|
_sortByTimeAndType(filesToBeUploaded);
|
||||||
_logger.info(
|
_logger.info(
|
||||||
filesToBeUploaded.length.toString() + " new files to be uploaded.");
|
filesToBeUploaded.length.toString() + " new files to be uploaded.");
|
||||||
return filesToBeUploaded;
|
return filesToBeUploaded;
|
||||||
|
@ -453,10 +453,12 @@ class RemoteSyncService {
|
||||||
return Platform.isIOS && !AppLifecycleService.instance.isForeground;
|
return Platform.isIOS && !AppLifecycleService.instance.isForeground;
|
||||||
}
|
}
|
||||||
|
|
||||||
void _moveVideosToEnd(List<File> file) {
|
// _sortByTimeAndType moves videos to end and sort by creation time (desc).
|
||||||
|
// This is done to upload most recent photo first.
|
||||||
|
void _sortByTimeAndType(List<File> file) {
|
||||||
file.sort((first, second) {
|
file.sort((first, second) {
|
||||||
if (first.fileType == second.fileType) {
|
if (first.fileType == second.fileType) {
|
||||||
return 0;
|
return second.creationTime.compareTo(first.creationTime);
|
||||||
} else if (first.fileType == FileType.video) {
|
} else if (first.fileType == FileType.video) {
|
||||||
return 1;
|
return 1;
|
||||||
} else {
|
} else {
|
||||||
|
|
|
@ -487,7 +487,8 @@ class CollectionItem extends StatelessWidget {
|
||||||
tag: "collection" + c.thumbnail.tag(),
|
tag: "collection" + c.thumbnail.tag(),
|
||||||
child: ThumbnailWidget(
|
child: ThumbnailWidget(
|
||||||
c.thumbnail,
|
c.thumbnail,
|
||||||
key: Key("collection" + c.thumbnail.tag()),
|
shouldShowArchiveStatus: c.collection.isArchived(),
|
||||||
|
key: Key("collection" + c.thumbnail.tag(),),
|
||||||
)),
|
)),
|
||||||
height: 140,
|
height: 140,
|
||||||
width: 140,
|
width: 140,
|
||||||
|
|
|
@ -166,11 +166,11 @@ class _GalleryAppBarWidgetState extends State<GalleryAppBarWidget> {
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
if (widget.type == GalleryAppBarType.owned_collection &&
|
if (widget.type == GalleryAppBarType.owned_collection) {
|
||||||
widget.collection.type == CollectionType.album) {
|
|
||||||
actions.add(PopupMenuButton(
|
actions.add(PopupMenuButton(
|
||||||
itemBuilder: (context) {
|
itemBuilder: (context) {
|
||||||
final List<PopupMenuItem> items = [];
|
final List<PopupMenuItem> items = [];
|
||||||
|
if (widget.collection.type == CollectionType.album) {
|
||||||
items.add(
|
items.add(
|
||||||
PopupMenuItem(
|
PopupMenuItem(
|
||||||
value: 1,
|
value: 1,
|
||||||
|
@ -185,12 +185,36 @@ class _GalleryAppBarWidgetState extends State<GalleryAppBarWidget> {
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
}
|
||||||
|
bool isArchived = widget.collection.isArchived();
|
||||||
|
items.add(
|
||||||
|
PopupMenuItem(
|
||||||
|
value: 2,
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
Icon(isArchived ? Icons.unarchive : Icons.archive),
|
||||||
|
Padding(
|
||||||
|
padding: EdgeInsets.all(8),
|
||||||
|
),
|
||||||
|
Text(isArchived ? "unarchive" : "archive"),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
return items;
|
return items;
|
||||||
},
|
},
|
||||||
onSelected: (value) async {
|
onSelected: (value) async {
|
||||||
if (value == 1) {
|
if (value == 1) {
|
||||||
await _renameAlbum(context);
|
await _renameAlbum(context);
|
||||||
}
|
}
|
||||||
|
if (value == 2) {
|
||||||
|
await changeCollectionVisibility(
|
||||||
|
context,
|
||||||
|
widget.collection,
|
||||||
|
widget.collection.isArchived()
|
||||||
|
? kVisibilityVisible
|
||||||
|
: kVisibilityArchive);
|
||||||
|
}
|
||||||
},
|
},
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
|
@ -343,20 +343,28 @@ class _HomeWidgetState extends State<HomeWidget> {
|
||||||
asyncLoader: (creationStartTime, creationEndTime, {limit, asc}) async {
|
asyncLoader: (creationStartTime, creationEndTime, {limit, asc}) async {
|
||||||
final importantPaths = Configuration.instance.getPathsToBackUp();
|
final importantPaths = Configuration.instance.getPathsToBackUp();
|
||||||
final ownerID = Configuration.instance.getUserID();
|
final ownerID = Configuration.instance.getUserID();
|
||||||
|
final archivedCollectionIds =
|
||||||
|
CollectionsService.instance.getArchivedCollections();
|
||||||
FileLoadResult result;
|
FileLoadResult result;
|
||||||
if (importantPaths.isNotEmpty) {
|
if (importantPaths.isNotEmpty) {
|
||||||
result = await FilesDB.instance.getImportantFiles(creationStartTime,
|
result = await FilesDB.instance.getImportantFiles(creationStartTime,
|
||||||
creationEndTime, ownerID, importantPaths.toList(),
|
creationEndTime, ownerID, importantPaths.toList(),
|
||||||
limit: limit, asc: asc);
|
limit: limit,
|
||||||
|
asc: asc,
|
||||||
|
ignoredCollectionIDs: archivedCollectionIds);
|
||||||
} else {
|
} else {
|
||||||
if (LocalSyncService.instance.hasGrantedLimitedPermissions()) {
|
if (LocalSyncService.instance.hasGrantedLimitedPermissions()) {
|
||||||
result = await FilesDB.instance.getAllLocalAndUploadedFiles(
|
result = await FilesDB.instance.getAllLocalAndUploadedFiles(
|
||||||
creationStartTime, creationEndTime, ownerID,
|
creationStartTime, creationEndTime, ownerID,
|
||||||
limit: limit, asc: asc);
|
limit: limit,
|
||||||
|
asc: asc,
|
||||||
|
ignoredCollectionIDs: archivedCollectionIds);
|
||||||
} else {
|
} else {
|
||||||
result = await FilesDB.instance.getAllUploadedFiles(
|
result = await FilesDB.instance.getAllUploadedFiles(
|
||||||
creationStartTime, creationEndTime, ownerID,
|
creationStartTime, creationEndTime, ownerID,
|
||||||
limit: limit, asc: asc);
|
limit: limit,
|
||||||
|
asc: asc,
|
||||||
|
ignoredCollectionIDs: archivedCollectionIds);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// hide ignored files from home page UI
|
// hide ignored files from home page UI
|
||||||
|
|
|
@ -35,7 +35,7 @@ class SocialSectionWidget extends StatelessWidget {
|
||||||
GestureDetector(
|
GestureDetector(
|
||||||
behavior: HitTestBehavior.translucent,
|
behavior: HitTestBehavior.translucent,
|
||||||
onTap: () {
|
onTap: () {
|
||||||
launch("https://discord.gg/uRqua3jSr5");
|
launch("https://ente.io/discord");
|
||||||
},
|
},
|
||||||
child: SettingsTextItem(text: "Discord", icon: Icons.navigate_next),
|
child: SettingsTextItem(text: "Discord", icon: Icons.navigate_next),
|
||||||
),
|
),
|
||||||
|
|
|
@ -21,6 +21,7 @@ class ThumbnailWidget extends StatefulWidget {
|
||||||
final File file;
|
final File file;
|
||||||
final BoxFit fit;
|
final BoxFit fit;
|
||||||
final bool shouldShowSyncStatus;
|
final bool shouldShowSyncStatus;
|
||||||
|
final bool shouldShowArchiveStatus;
|
||||||
final bool shouldShowLivePhotoOverlay;
|
final bool shouldShowLivePhotoOverlay;
|
||||||
final Duration diskLoadDeferDuration;
|
final Duration diskLoadDeferDuration;
|
||||||
final Duration serverLoadDeferDuration;
|
final Duration serverLoadDeferDuration;
|
||||||
|
@ -31,6 +32,7 @@ class ThumbnailWidget extends StatefulWidget {
|
||||||
this.fit = BoxFit.cover,
|
this.fit = BoxFit.cover,
|
||||||
this.shouldShowSyncStatus = true,
|
this.shouldShowSyncStatus = true,
|
||||||
this.shouldShowLivePhotoOverlay = false,
|
this.shouldShowLivePhotoOverlay = false,
|
||||||
|
this.shouldShowArchiveStatus = false,
|
||||||
this.diskLoadDeferDuration,
|
this.diskLoadDeferDuration,
|
||||||
this.serverLoadDeferDuration,
|
this.serverLoadDeferDuration,
|
||||||
}) : super(key: key ?? Key(file.tag()));
|
}) : super(key: key ?? Key(file.tag()));
|
||||||
|
@ -74,6 +76,18 @@ class _ThumbnailWidgetState extends State<ThumbnailWidget> {
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
static final kArchiveIconOverlay = Align(
|
||||||
|
alignment: Alignment.bottomRight,
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.only(right: 8, bottom: 8),
|
||||||
|
child: Icon(
|
||||||
|
Icons.archive_outlined,
|
||||||
|
size: 42,
|
||||||
|
color: Colors.white.withOpacity(0.9),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
static final kUnsyncedIconOverlay = Container(
|
static final kUnsyncedIconOverlay = Container(
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
gradient: LinearGradient(
|
gradient: LinearGradient(
|
||||||
|
@ -173,8 +187,7 @@ class _ThumbnailWidgetState extends State<ThumbnailWidget> {
|
||||||
content = image;
|
content = image;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return Stack(
|
List<Widget> viewChildrens = [
|
||||||
children: [
|
|
||||||
loadingWidget,
|
loadingWidget,
|
||||||
AnimatedOpacity(
|
AnimatedOpacity(
|
||||||
opacity: content == null ? 0 : 1.0,
|
opacity: content == null ? 0 : 1.0,
|
||||||
|
@ -184,7 +197,12 @@ class _ThumbnailWidgetState extends State<ThumbnailWidget> {
|
||||||
widget.shouldShowSyncStatus && widget.file.uploadedFileID == null
|
widget.shouldShowSyncStatus && widget.file.uploadedFileID == null
|
||||||
? kUnsyncedIconOverlay
|
? kUnsyncedIconOverlay
|
||||||
: getFileInfoContainer(widget.file),
|
: getFileInfoContainer(widget.file),
|
||||||
],
|
];
|
||||||
|
if (widget.shouldShowArchiveStatus) {
|
||||||
|
viewChildrens.add(kArchiveIconOverlay);
|
||||||
|
}
|
||||||
|
return Stack(
|
||||||
|
children: viewChildrens,
|
||||||
fit: StackFit.expand,
|
fit: StackFit.expand,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,6 +5,7 @@ import 'package:flutter/cupertino.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter/widgets.dart';
|
import 'package:flutter/widgets.dart';
|
||||||
import 'package:logging/logging.dart';
|
import 'package:logging/logging.dart';
|
||||||
|
import 'package:photos/core/constants.dart';
|
||||||
import 'package:photos/models/file.dart';
|
import 'package:photos/models/file.dart';
|
||||||
import 'package:photos/ui/thumbnail_widget.dart';
|
import 'package:photos/ui/thumbnail_widget.dart';
|
||||||
import 'package:photos/ui/video_controls.dart';
|
import 'package:photos/ui/video_controls.dart';
|
||||||
|
@ -118,6 +119,12 @@ class _VideoWidgetState extends State<VideoWidget> {
|
||||||
_videoPlayerController.value.isInitialized
|
_videoPlayerController.value.isInitialized
|
||||||
? _getVideoPlayer()
|
? _getVideoPlayer()
|
||||||
: _getLoadingWidget();
|
: _getLoadingWidget();
|
||||||
|
final contentWithDetector = GestureDetector(
|
||||||
|
child: content,
|
||||||
|
onVerticalDragUpdate: (d) => {
|
||||||
|
if (d.delta.dy > kDragSensitivity) {Navigator.of(context).pop()}
|
||||||
|
},
|
||||||
|
);
|
||||||
return VisibilityDetector(
|
return VisibilityDetector(
|
||||||
key: Key(widget.file.tag()),
|
key: Key(widget.file.tag()),
|
||||||
onVisibilityChanged: (info) {
|
onVisibilityChanged: (info) {
|
||||||
|
@ -129,7 +136,7 @@ class _VideoWidgetState extends State<VideoWidget> {
|
||||||
},
|
},
|
||||||
child: Hero(
|
child: Hero(
|
||||||
tag: widget.tagPrefix + widget.file.tag(),
|
tag: widget.tagPrefix + widget.file.tag(),
|
||||||
child: content,
|
child: contentWithDetector,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -43,6 +43,7 @@ class _ZoomableImageState extends State<ZoomableImage>
|
||||||
bool _loadingFinalImage = false;
|
bool _loadingFinalImage = false;
|
||||||
bool _loadedFinalImage = false;
|
bool _loadedFinalImage = false;
|
||||||
ValueChanged<PhotoViewScaleState> _scaleStateChangedCallback;
|
ValueChanged<PhotoViewScaleState> _scaleStateChangedCallback;
|
||||||
|
bool _isZooming = false;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
|
@ -51,6 +52,8 @@ class _ZoomableImageState extends State<ZoomableImage>
|
||||||
if (widget.shouldDisableScroll != null) {
|
if (widget.shouldDisableScroll != null) {
|
||||||
widget.shouldDisableScroll(value != PhotoViewScaleState.initial);
|
widget.shouldDisableScroll(value != PhotoViewScaleState.initial);
|
||||||
}
|
}
|
||||||
|
_isZooming = value != PhotoViewScaleState.initial;
|
||||||
|
// _logger.info('is reakky zooming $_isZooming with state $value');
|
||||||
};
|
};
|
||||||
super.initState();
|
super.initState();
|
||||||
}
|
}
|
||||||
|
@ -62,9 +65,10 @@ class _ZoomableImageState extends State<ZoomableImage>
|
||||||
} else {
|
} else {
|
||||||
_loadLocalImage(context);
|
_loadLocalImage(context);
|
||||||
}
|
}
|
||||||
|
Widget content;
|
||||||
|
|
||||||
if (_imageProvider != null) {
|
if (_imageProvider != null) {
|
||||||
return PhotoView(
|
content = PhotoView(
|
||||||
imageProvider: _imageProvider,
|
imageProvider: _imageProvider,
|
||||||
scaleStateChangedCallback: _scaleStateChangedCallback,
|
scaleStateChangedCallback: _scaleStateChangedCallback,
|
||||||
minScale: PhotoViewComputedScale.contained,
|
minScale: PhotoViewComputedScale.contained,
|
||||||
|
@ -75,8 +79,19 @@ class _ZoomableImageState extends State<ZoomableImage>
|
||||||
backgroundDecoration: widget.backgroundDecoration,
|
backgroundDecoration: widget.backgroundDecoration,
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
return loadWidget;
|
content = loadWidget;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
GestureDragUpdateCallback verticalDragCallback = _isZooming
|
||||||
|
? null
|
||||||
|
: (d) => {
|
||||||
|
if (!_isZooming && d.delta.dy > kDragSensitivity)
|
||||||
|
{Navigator.of(context).pop()}
|
||||||
|
};
|
||||||
|
return GestureDetector(
|
||||||
|
child: content,
|
||||||
|
onVerticalDragUpdate: verticalDragCallback,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
void _loadNetworkImage() {
|
void _loadNetworkImage() {
|
||||||
|
|
|
@ -4,8 +4,10 @@ import 'package:logging/logging.dart';
|
||||||
import 'package:path/path.dart';
|
import 'package:path/path.dart';
|
||||||
import 'package:photos/core/event_bus.dart';
|
import 'package:photos/core/event_bus.dart';
|
||||||
import 'package:photos/events/force_reload_home_gallery_event.dart';
|
import 'package:photos/events/force_reload_home_gallery_event.dart';
|
||||||
|
import 'package:photos/models/collection.dart';
|
||||||
import 'package:photos/models/file.dart';
|
import 'package:photos/models/file.dart';
|
||||||
import 'package:photos/models/magic_metadata.dart';
|
import 'package:photos/models/magic_metadata.dart';
|
||||||
|
import 'package:photos/services/collections_service.dart';
|
||||||
import 'package:photos/services/file_magic_service.dart';
|
import 'package:photos/services/file_magic_service.dart';
|
||||||
import 'package:photos/ui/rename_dialog.dart';
|
import 'package:photos/ui/rename_dialog.dart';
|
||||||
import 'package:photos/utils/dialog_util.dart';
|
import 'package:photos/utils/dialog_util.dart';
|
||||||
|
@ -32,6 +34,28 @@ Future<void> changeVisibility(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<void> changeCollectionVisibility(
|
||||||
|
BuildContext context, Collection collection, int newVisibility) async {
|
||||||
|
final dialog = createProgressDialog(context,
|
||||||
|
newVisibility == kVisibilityArchive ? "archiving..." : "unarchiving...");
|
||||||
|
await dialog.show();
|
||||||
|
try {
|
||||||
|
Map<String, dynamic> update = {kMagicKeyVisibility: newVisibility};
|
||||||
|
await CollectionsService.instance.updateMagicMetadata(collection, update);
|
||||||
|
// Force reload home gallery to pull in the now unarchived files
|
||||||
|
Bus.instance.fire(ForceReloadHomeGalleryEvent());
|
||||||
|
showShortToast(newVisibility == kVisibilityArchive
|
||||||
|
? "successfully archived"
|
||||||
|
: "successfully unarchived");
|
||||||
|
|
||||||
|
await dialog.hide();
|
||||||
|
} catch (e, s) {
|
||||||
|
_logger.severe("failed to update collection visibility", e, s);
|
||||||
|
await dialog.hide();
|
||||||
|
rethrow;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Future<bool> editTime(
|
Future<bool> editTime(
|
||||||
BuildContext context, List<File> files, int editedTime) async {
|
BuildContext context, List<File> files, int editedTime) async {
|
||||||
try {
|
try {
|
||||||
|
|
|
@ -11,7 +11,7 @@ description: ente photos application
|
||||||
# In iOS, build-name is used as CFBundleShortVersionString while build-number used as CFBundleVersion.
|
# In iOS, build-name is used as CFBundleShortVersionString while build-number used as CFBundleVersion.
|
||||||
# Read more about iOS versioning at
|
# Read more about iOS versioning at
|
||||||
# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
|
# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
|
||||||
version: 0.5.10+290
|
version: 0.5.11+291
|
||||||
|
|
||||||
environment:
|
environment:
|
||||||
sdk: ">=2.10.0 <3.0.0"
|
sdk: ">=2.10.0 <3.0.0"
|
||||||
|
|
Loading…
Add table
Reference in a new issue