Merge pull request #46 from ente-io/archive_unarchive
Add support for archiving memories
This commit is contained in:
commit
82a9e88cd7
8 changed files with 421 additions and 13 deletions
|
@ -6,6 +6,7 @@ import 'package:path_provider/path_provider.dart';
|
|||
import 'package:photos/models/backup_status.dart';
|
||||
import 'package:photos/models/file.dart';
|
||||
import 'package:photos/models/file_load_result.dart';
|
||||
import 'package:photos/models/magic_metadata.dart';
|
||||
import 'package:photos/models/file_type.dart';
|
||||
import 'package:photos/models/location.dart';
|
||||
import 'package:sqflite/sqflite.dart';
|
||||
|
@ -51,12 +52,22 @@ class FilesDB {
|
|||
static final columnThumbnailDecryptionHeader = 'thumbnail_decryption_header';
|
||||
static final columnMetadataDecryptionHeader = 'metadata_decryption_header';
|
||||
|
||||
// MMD -> Magic Metadata
|
||||
static final columnMMdEncodedJson = 'mmd_encoded_json';
|
||||
static final columnMMdVersion = 'mmd_ver';
|
||||
|
||||
// part of magic metadata
|
||||
// Only parse & store selected fields from JSON in separate columns if
|
||||
// we need to write query based on that field
|
||||
static final columnMMdVisibility = 'mmd_visibility';
|
||||
|
||||
static final initializationScript = [...createTable(table)];
|
||||
static final migrationScripts = [
|
||||
...alterDeviceFolderToAllowNULL(),
|
||||
...alterTimestampColumnTypes(),
|
||||
...addIndices(),
|
||||
...addMetadataColumns(),
|
||||
...addMagicMetadataColumns(),
|
||||
];
|
||||
|
||||
final dbConfig = MigrationConfig(
|
||||
|
@ -227,6 +238,20 @@ class FilesDB {
|
|||
];
|
||||
}
|
||||
|
||||
static List<String> addMagicMetadataColumns() {
|
||||
return [
|
||||
'''
|
||||
ALTER TABLE $table ADD COLUMN $columnMMdEncodedJson TEXT DEFAULT '{}';
|
||||
''',
|
||||
'''
|
||||
ALTER TABLE $table ADD COLUMN $columnMMdVersion INTEGER DEFAULT 0;
|
||||
''',
|
||||
'''
|
||||
ALTER TABLE $table ADD COLUMN $columnMMdVisibility INTEGER DEFAULT $kVisibilityVisible;
|
||||
'''
|
||||
];
|
||||
}
|
||||
|
||||
Future<void> clearTable() async {
|
||||
final db = await instance.database;
|
||||
await db.delete(table);
|
||||
|
@ -332,20 +357,22 @@ class FilesDB {
|
|||
}
|
||||
|
||||
Future<FileLoadResult> getAllUploadedFiles(int startTime, int endTime,
|
||||
int ownerID, {int limit, bool asc}) async {
|
||||
int ownerID, {int limit, bool asc, int visibility = kVisibilityVisible}) async {
|
||||
final db = await instance.database;
|
||||
final order = (asc ?? false ? 'ASC' : 'DESC');
|
||||
final results = await db.query(
|
||||
table,
|
||||
where:
|
||||
'$columnCreationTime >= ? AND $columnCreationTime <= ? AND $columnOwnerID = ? AND ($columnCollectionID IS NOT NULL AND $columnCollectionID IS NOT -1)',
|
||||
whereArgs: [startTime, endTime, ownerID],
|
||||
'$columnCreationTime >= ? AND $columnCreationTime <= ? AND $columnOwnerID = ? AND ($columnCollectionID IS NOT NULL AND $columnCollectionID IS NOT -1)'
|
||||
' AND $columnMMdVisibility = ?',
|
||||
whereArgs: [startTime, endTime, ownerID, visibility],
|
||||
orderBy:
|
||||
'$columnCreationTime ' + order + ', $columnModificationTime ' + order,
|
||||
limit: limit,
|
||||
);
|
||||
final files = _convertToFiles(results);
|
||||
return FileLoadResult(files, files.length == limit);
|
||||
List<File> deduplicatedFiles = _deduplicatedFiles(files);
|
||||
return FileLoadResult(deduplicatedFiles, files.length == limit);
|
||||
}
|
||||
|
||||
Future<FileLoadResult> getAllLocalAndUploadedFiles(int startTime, int endTime, int ownerID,
|
||||
|
@ -355,8 +382,9 @@ class FilesDB {
|
|||
final results = await db.query(
|
||||
table,
|
||||
where:
|
||||
'$columnCreationTime >= ? AND $columnCreationTime <= ? AND ($columnOwnerID IS NULL OR $columnOwnerID = ?) AND ($columnLocalID IS NOT NULL OR ($columnCollectionID IS NOT NULL AND $columnCollectionID IS NOT -1))',
|
||||
whereArgs: [startTime, endTime, ownerID],
|
||||
'$columnCreationTime >= ? AND $columnCreationTime <= ? AND ($columnOwnerID IS NULL OR $columnOwnerID = ?) AND ($columnMMdVisibility IS NULL OR $columnMMdVisibility = ?)'
|
||||
' AND ($columnLocalID IS NOT NULL OR ($columnCollectionID IS NOT NULL AND $columnCollectionID IS NOT -1))',
|
||||
whereArgs: [startTime, endTime, ownerID, kVisibilityVisible],
|
||||
orderBy:
|
||||
'$columnCreationTime ' + order + ', $columnModificationTime ' + order,
|
||||
limit: limit,
|
||||
|
@ -378,14 +406,20 @@ class FilesDB {
|
|||
final results = await db.query(
|
||||
table,
|
||||
where:
|
||||
'$columnCreationTime >= ? AND $columnCreationTime <= ? AND ($columnOwnerID IS NULL OR $columnOwnerID = ?) AND (($columnLocalID IS NOT NULL AND $columnDeviceFolder IN ($inParam)) OR ($columnCollectionID IS NOT NULL AND $columnCollectionID IS NOT -1))',
|
||||
whereArgs: [startTime, endTime, ownerID],
|
||||
'$columnCreationTime >= ? AND $columnCreationTime <= ? AND ($columnOwnerID IS NULL OR $columnOwnerID = ?) AND ($columnMMdVisibility IS NULL OR $columnMMdVisibility = ?)'
|
||||
'AND (($columnLocalID IS NOT NULL AND $columnDeviceFolder IN ($inParam)) OR ($columnCollectionID IS NOT NULL AND $columnCollectionID IS NOT -1))',
|
||||
whereArgs: [startTime, endTime, ownerID, kVisibilityVisible],
|
||||
orderBy:
|
||||
'$columnCreationTime ' + order + ', $columnModificationTime ' + order,
|
||||
limit: limit,
|
||||
);
|
||||
final uploadedFileIDs = <int>{};
|
||||
final files = _convertToFiles(results);
|
||||
List<File> deduplicatedFiles = _deduplicatedFiles(files);
|
||||
return FileLoadResult(deduplicatedFiles, files.length == limit);
|
||||
}
|
||||
|
||||
List<File> _deduplicatedFiles(List<File> files) {
|
||||
final uploadedFileIDs = <int>{};
|
||||
final List<File> deduplicatedFiles = [];
|
||||
for (final file in files) {
|
||||
final id = file.uploadedFileID;
|
||||
|
@ -395,7 +429,7 @@ class FilesDB {
|
|||
uploadedFileIDs.add(id);
|
||||
deduplicatedFiles.add(file);
|
||||
}
|
||||
return FileLoadResult(deduplicatedFiles, files.length == limit);
|
||||
return deduplicatedFiles;
|
||||
}
|
||||
|
||||
Future<FileLoadResult> getFilesInCollection(
|
||||
|
@ -877,6 +911,10 @@ class FilesDB {
|
|||
row[columnExif] = file.exif;
|
||||
row[columnHash] = file.hash;
|
||||
row[columnMetadataVersion] = file.metadataVersion;
|
||||
row[columnMMdVersion] = file.mMdVersion ?? 0;
|
||||
row[columnMMdEncodedJson] = file.mMdEncodedJson ?? '{}';
|
||||
row[columnMMdVisibility] =
|
||||
file.magicMetadata?.visibility ?? kVisibilityVisible;
|
||||
return row;
|
||||
}
|
||||
|
||||
|
@ -903,6 +941,11 @@ class FilesDB {
|
|||
row[columnExif] = file.exif;
|
||||
row[columnHash] = file.hash;
|
||||
row[columnMetadataVersion] = file.metadataVersion;
|
||||
|
||||
row[columnMMdVersion] = file.mMdVersion ?? 0;
|
||||
row[columnMMdEncodedJson] == file.mMdEncodedJson ?? '{}';
|
||||
row[columnMMdVisibility] =
|
||||
file.magicMetadata?.visibility ?? kVisibilityVisible;
|
||||
return row;
|
||||
}
|
||||
|
||||
|
@ -934,6 +977,9 @@ class FilesDB {
|
|||
file.exif = row[columnExif];
|
||||
file.hash = row[columnHash];
|
||||
file.metadataVersion = row[columnMetadataVersion] ?? 0;
|
||||
|
||||
file.mMdVersion = row[columnMMdVersion] ?? 0 ;
|
||||
file.mMdEncodedJson = row[columnMMdEncodedJson] ?? '{}';
|
||||
return file;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,15 +1,16 @@
|
|||
import 'package:exif/exif.dart';
|
||||
import 'dart:io' as io;
|
||||
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter_sodium/flutter_sodium.dart';
|
||||
import 'package:path/path.dart';
|
||||
import 'package:photo_manager/photo_manager.dart';
|
||||
import 'package:photos/core/configuration.dart';
|
||||
import 'package:photos/core/constants.dart';
|
||||
import 'package:photos/models/magic_metadata.dart';
|
||||
import 'package:photos/models/file_type.dart';
|
||||
import 'package:photos/models/location.dart';
|
||||
import 'package:photos/services/feature_flag_service.dart';
|
||||
import 'package:photos/utils/crypto_util.dart';
|
||||
import 'dart:io' as io;
|
||||
|
||||
class File {
|
||||
int generatedID;
|
||||
|
@ -35,6 +36,13 @@ class File {
|
|||
String thumbnailDecryptionHeader;
|
||||
String metadataDecryptionHeader;
|
||||
|
||||
String mMdEncodedJson;
|
||||
int mMdVersion = 0;
|
||||
MagicMetadata _mmd;
|
||||
MagicMetadata get magicMetadata =>
|
||||
_mmd ?? MagicMetadata.fromEncodedJson(mMdEncodedJson ?? '{}');
|
||||
set magicMetadata (val) => _mmd = val;
|
||||
|
||||
static const kCurrentMetadataVersion = 1;
|
||||
|
||||
File();
|
||||
|
|
35
lib/models/magic_metadata.dart
Normal file
35
lib/models/magic_metadata.dart
Normal file
|
@ -0,0 +1,35 @@
|
|||
|
||||
import 'dart:convert';
|
||||
|
||||
const kVisibilityVisible = 0;
|
||||
const kVisibilityArchive = 1;
|
||||
|
||||
const kMagicKeyVisibility = 'visibility';
|
||||
|
||||
class MagicMetadata {
|
||||
// 0 -> visible
|
||||
// 1 -> archived
|
||||
// 2 -> hidden etc?
|
||||
int visibility;
|
||||
|
||||
MagicMetadata({this.visibility});
|
||||
|
||||
factory MagicMetadata.fromEncodedJson(String encodedJson) =>
|
||||
MagicMetadata.fromJson(jsonDecode(encodedJson));
|
||||
|
||||
factory MagicMetadata.fromJson(dynamic json) =>
|
||||
MagicMetadata.fromMap(json);
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
final map = <String, dynamic>{};
|
||||
map[kMagicKeyVisibility] = visibility;
|
||||
return map;
|
||||
}
|
||||
|
||||
factory MagicMetadata.fromMap(Map<String, dynamic> map) {
|
||||
if (map == null) return null;
|
||||
return MagicMetadata(
|
||||
visibility: map[kMagicKeyVisibility] ?? kVisibilityVisible,
|
||||
);
|
||||
}
|
||||
}
|
145
lib/services/file_magic_service.dart
Normal file
145
lib/services/file_magic_service.dart
Normal file
|
@ -0,0 +1,145 @@
|
|||
import 'dart:convert';
|
||||
|
||||
import 'package:dio/dio.dart';
|
||||
import 'package:flutter_sodium/flutter_sodium.dart';
|
||||
import 'package:logging/logging.dart';
|
||||
import 'package:photos/core/event_bus.dart';
|
||||
import 'package:photos/core/network.dart';
|
||||
import 'package:photos/db/files_db.dart';
|
||||
import 'package:photos/events/files_updated_event.dart';
|
||||
import 'package:photos/models/file.dart';
|
||||
import 'package:photos/core/configuration.dart';
|
||||
import 'package:photos/models/magic_metadata.dart';
|
||||
import 'package:photos/services/remote_sync_service.dart';
|
||||
import 'package:photos/utils/crypto_util.dart';
|
||||
import 'package:photos/utils/file_download_util.dart';
|
||||
|
||||
class FileMagicService {
|
||||
final _logger = Logger("FileMagicService");
|
||||
Dio _dio;
|
||||
FilesDB _filesDB;
|
||||
|
||||
FileMagicService._privateConstructor() {
|
||||
_filesDB = FilesDB.instance;
|
||||
_dio = Network.instance.getDio();
|
||||
}
|
||||
|
||||
static final FileMagicService instance =
|
||||
FileMagicService._privateConstructor();
|
||||
|
||||
Future<void> changeVisibility(List<File> files, int visibility) async {
|
||||
Map<String, dynamic> update = {};
|
||||
update[kMagicKeyVisibility] = visibility;
|
||||
return _updateMagicData(files, update);
|
||||
}
|
||||
|
||||
Future<void> _updateMagicData(
|
||||
List<File> files, Map<String, dynamic> newMetadataUpdate) async {
|
||||
final params = <String, dynamic>{};
|
||||
params['metadataList'] = [];
|
||||
final int ownerID = Configuration.instance.getUserID();
|
||||
try {
|
||||
for (final file in files) {
|
||||
if (file.uploadedFileID == null) {
|
||||
throw AssertionError(
|
||||
"operation is only supported on backed up files");
|
||||
} else if (file.ownerID != ownerID) {
|
||||
throw AssertionError("cannot modify memories 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(file.mMdEncodedJson);
|
||||
newMetadataUpdate.forEach((key, value) {
|
||||
jsonToUpdate[key] = value;
|
||||
});
|
||||
|
||||
// update the local information so that it's reflected on UI
|
||||
file.mMdEncodedJson = jsonEncode(jsonToUpdate);
|
||||
file.magicMetadata = MagicMetadata.fromJson(jsonToUpdate);
|
||||
|
||||
final fileKey = decryptFileKey(file);
|
||||
final encryptedMMd = await CryptoUtil.encryptChaCha(
|
||||
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),
|
||||
)));
|
||||
}
|
||||
|
||||
await _dio.put(
|
||||
Configuration.instance.getHttpEndpoint() +
|
||||
"/files/magic-metadata",
|
||||
data: params,
|
||||
options: Options(
|
||||
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
|
||||
await _filesDB.insertMultiple(files);
|
||||
Bus.instance.fire(FilesUpdatedEvent(files));
|
||||
RemoteSyncService.instance.sync(silently: true);
|
||||
} on DioError catch (e) {
|
||||
if (e.response != null && e.response.statusCode == 409) {
|
||||
RemoteSyncService.instance.sync(silently: true);
|
||||
}
|
||||
rethrow;
|
||||
} catch (e, s) {
|
||||
_logger.severe("failed to sync magic metadata", e, s);
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class UpdateMagicMetadataRequest {
|
||||
final int id;
|
||||
final MetadataRequest magicMetadata;
|
||||
|
||||
UpdateMagicMetadataRequest({this.id, this.magicMetadata});
|
||||
|
||||
factory UpdateMagicMetadataRequest.fromJson(dynamic json) {
|
||||
return UpdateMagicMetadataRequest(
|
||||
id: json['id'],
|
||||
magicMetadata: json['magicMetadata'] != null
|
||||
? MetadataRequest.fromJson(json['magicMetadata'])
|
||||
: null);
|
||||
}
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
final map = <String, dynamic>{};
|
||||
map['id'] = id;
|
||||
if (magicMetadata != null) {
|
||||
map['magicMetadata'] = magicMetadata.toJson();
|
||||
}
|
||||
return map;
|
||||
}
|
||||
}
|
||||
|
||||
class MetadataRequest {
|
||||
int version;
|
||||
int count;
|
||||
String data;
|
||||
String header;
|
||||
|
||||
MetadataRequest({this.version, this.count, this.data, this.header});
|
||||
|
||||
MetadataRequest.fromJson(dynamic json) {
|
||||
version = json['version'];
|
||||
count = json['count'];
|
||||
data = json['data'];
|
||||
header = json['header'];
|
||||
}
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
var map = <String, dynamic>{};
|
||||
map['version'] = version;
|
||||
map['count'] = count;
|
||||
map['data'] = data;
|
||||
map['header'] = header;
|
||||
return map;
|
||||
}
|
||||
}
|
63
lib/ui/archive_page.dart
Normal file
63
lib/ui/archive_page.dart
Normal file
|
@ -0,0 +1,63 @@
|
|||
import 'dart:io';
|
||||
|
||||
import 'package:flutter/cupertino.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:photos/core/configuration.dart';
|
||||
import 'package:photos/core/event_bus.dart';
|
||||
import 'package:photos/db/files_db.dart';
|
||||
import 'package:photos/events/files_updated_event.dart';
|
||||
import 'package:photos/models/magic_metadata.dart';
|
||||
import 'package:photos/models/selected_files.dart';
|
||||
|
||||
import 'gallery.dart';
|
||||
import 'gallery_app_bar_widget.dart';
|
||||
|
||||
class ArchivePage extends StatelessWidget {
|
||||
final String tagPrefix;
|
||||
final GalleryAppBarType appBarType;
|
||||
final _selectedFiles = SelectedFiles();
|
||||
|
||||
ArchivePage(
|
||||
{this.tagPrefix = "archived_page",
|
||||
this.appBarType = GalleryAppBarType.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);
|
||||
},
|
||||
reloadEvent: Bus.instance.on<FilesUpdatedEvent>().where((event) =>
|
||||
event.updatedFiles
|
||||
.firstWhere((element) => element.uploadedFileID != null) !=
|
||||
null),
|
||||
forceReloadEvent: Bus.instance.on<FilesUpdatedEvent>().where((event) =>
|
||||
event.updatedFiles
|
||||
.firstWhere((element) => element.uploadedFileID != null) !=
|
||||
null),
|
||||
tagPrefix: tagPrefix,
|
||||
selectedFiles: _selectedFiles,
|
||||
initialFiles: null,
|
||||
);
|
||||
return Scaffold(
|
||||
body: Stack(children: [
|
||||
Padding(
|
||||
padding: EdgeInsets.only(top: Platform.isAndroid ? 80 : 100),
|
||||
child: gallery,
|
||||
),
|
||||
SizedBox(
|
||||
height: Platform.isAndroid ? 80 : 100,
|
||||
child: GalleryAppBarWidget(
|
||||
appBarType,
|
||||
"archived memories",
|
||||
_selectedFiles,
|
||||
),
|
||||
)
|
||||
]),
|
||||
);
|
||||
}
|
||||
}
|
|
@ -23,6 +23,7 @@ import 'package:photos/ui/thumbnail_widget.dart';
|
|||
import 'package:photos/utils/local_settings.dart';
|
||||
import 'package:photos/utils/navigation_util.dart';
|
||||
import 'package:photos/utils/toast_util.dart';
|
||||
import 'package:photos/ui/archive_page.dart';
|
||||
|
||||
class CollectionsGalleryWidget extends StatefulWidget {
|
||||
const CollectionsGalleryWidget({Key key}) : super(key: key);
|
||||
|
@ -178,6 +179,44 @@ class _CollectionsGalleryWidgetState extends State<CollectionsGalleryWidget>
|
|||
),
|
||||
)
|
||||
: nothingToSeeHere,
|
||||
Divider(),
|
||||
OutlinedButton(
|
||||
style: OutlinedButton.styleFrom(
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(5),
|
||||
),
|
||||
padding: EdgeInsets.fromLTRB(20, 10,20, 10),
|
||||
side: BorderSide(
|
||||
width: 2,
|
||||
color: Colors.white12,
|
||||
),
|
||||
),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Icon(
|
||||
Icons.archive_outlined,
|
||||
color: Colors.white,
|
||||
),
|
||||
Padding(padding: EdgeInsets.all(6)),
|
||||
Text(
|
||||
"archived",
|
||||
style: TextStyle(
|
||||
color: Colors.white,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
onPressed: () async {
|
||||
routeToPage(
|
||||
context,
|
||||
ArchivePage(),
|
||||
);
|
||||
}
|
||||
),
|
||||
Padding(padding: EdgeInsets.all(12)),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
|
|
@ -1,24 +1,29 @@
|
|||
import 'dart:async';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:flutter/cupertino.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:fluttertoast/fluttertoast.dart';
|
||||
import 'package:logging/logging.dart';
|
||||
import 'package:page_transition/page_transition.dart';
|
||||
import 'package:photos/core/configuration.dart';
|
||||
import 'package:photos/core/event_bus.dart';
|
||||
import 'package:photos/events/subscription_purchased_event.dart';
|
||||
import 'package:photos/models/collection.dart';
|
||||
import 'package:photos/models/magic_metadata.dart';
|
||||
import 'package:photos/models/selected_files.dart';
|
||||
import 'package:photos/services/collections_service.dart';
|
||||
import 'package:photos/ui/create_collection_page.dart';
|
||||
import 'package:photos/ui/share_collection_widget.dart';
|
||||
import 'package:photos/utils/delete_file_util.dart';
|
||||
import 'package:photos/utils/dialog_util.dart';
|
||||
import 'package:photos/services/file_magic_service.dart';
|
||||
import 'package:photos/utils/share_util.dart';
|
||||
import 'package:photos/utils/toast_util.dart';
|
||||
|
||||
enum GalleryAppBarType {
|
||||
homepage,
|
||||
archive,
|
||||
local_folder,
|
||||
// indicator for gallery view of collections shared with the user
|
||||
shared_collection,
|
||||
|
@ -208,6 +213,7 @@ class _GalleryAppBarWidgetState extends State<GalleryAppBarWidget> {
|
|||
},
|
||||
));
|
||||
if (widget.type == GalleryAppBarType.homepage ||
|
||||
widget.type == GalleryAppBarType.archive ||
|
||||
widget.type == GalleryAppBarType.local_folder) {
|
||||
actions.add(IconButton(
|
||||
icon: Icon(
|
||||
|
@ -235,9 +241,64 @@ class _GalleryAppBarWidgetState extends State<GalleryAppBarWidget> {
|
|||
));
|
||||
}
|
||||
}
|
||||
|
||||
if (widget.type == GalleryAppBarType.homepage ||
|
||||
widget.type == GalleryAppBarType.archive) {
|
||||
bool showArchive = widget.type == GalleryAppBarType.homepage;
|
||||
actions.add(PopupMenuButton(
|
||||
itemBuilder: (context) {
|
||||
final List<PopupMenuItem> items = [];
|
||||
items.add(
|
||||
PopupMenuItem(
|
||||
value: 1,
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(Platform.isAndroid
|
||||
? Icons.archive_outlined
|
||||
: CupertinoIcons.archivebox),
|
||||
Padding(
|
||||
padding: EdgeInsets.all(8),
|
||||
),
|
||||
Text(showArchive ? "archive" : "unarchive"),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
return items;
|
||||
},
|
||||
onSelected: (value) async {
|
||||
if (value == 1) {
|
||||
await _handleVisibilityChangeRequest(context, showArchive ? kVisibilityArchive : kVisibilityVisible);
|
||||
}
|
||||
},
|
||||
));
|
||||
}
|
||||
return actions;
|
||||
}
|
||||
|
||||
Future<void> _handleVisibilityChangeRequest(BuildContext context,
|
||||
int newVisibility) async {
|
||||
final dialog = createProgressDialog(context, "please wait...");
|
||||
await dialog.show();
|
||||
try {
|
||||
await FileMagicService.instance.changeVisibility(
|
||||
widget.selectedFiles.files.toList(), newVisibility);
|
||||
showToast(
|
||||
newVisibility == kVisibilityArchive
|
||||
? "successfully archived"
|
||||
: "successfully unarchived",
|
||||
toastLength: Toast.LENGTH_SHORT);
|
||||
|
||||
await dialog.hide();
|
||||
} catch (e, s) {
|
||||
_logger.severe("failed to update file visibility", e, s);
|
||||
await dialog.hide();
|
||||
await showGenericErrorDialog(context);
|
||||
} finally {
|
||||
_clearSelectedFiles();
|
||||
}
|
||||
}
|
||||
|
||||
void _shareSelected(BuildContext context) {
|
||||
share(context, widget.selectedFiles.files.toList());
|
||||
}
|
||||
|
|
|
@ -11,6 +11,7 @@ import 'package:photos/events/collection_updated_event.dart';
|
|||
import 'package:photos/events/local_photos_updated_event.dart';
|
||||
import 'package:photos/events/remote_sync_event.dart';
|
||||
import 'package:photos/models/file.dart';
|
||||
import 'package:photos/models/magic_metadata.dart';
|
||||
import 'package:photos/utils/crypto_util.dart';
|
||||
import 'package:photos/utils/file_download_util.dart';
|
||||
|
||||
|
@ -71,14 +72,24 @@ class DiffFetcher {
|
|||
item["thumbnail"]["decryptionHeader"];
|
||||
file.metadataDecryptionHeader =
|
||||
item["metadata"]["decryptionHeader"];
|
||||
|
||||
final fileDecryptionKey = decryptFileKey(file);
|
||||
final encodedMetadata = await CryptoUtil.decryptChaCha(
|
||||
Sodium.base642bin(item["metadata"]["encryptedData"]),
|
||||
decryptFileKey(file),
|
||||
fileDecryptionKey,
|
||||
Sodium.base642bin(file.metadataDecryptionHeader),
|
||||
);
|
||||
Map<String, dynamic> metadata =
|
||||
jsonDecode(utf8.decode(encodedMetadata));
|
||||
file.applyMetadata(metadata);
|
||||
if (item['magicMetadata'] != null) {
|
||||
final utfEncodedMmd = await CryptoUtil.decryptChaCha(
|
||||
Sodium.base642bin(item['magicMetadata']['data']), fileDecryptionKey,
|
||||
Sodium.base642bin(item['magicMetadata']['header']));
|
||||
file.mMdEncodedJson = utf8.decode(utfEncodedMmd);
|
||||
file.mMdVersion = item['magicMetadata']['version'];
|
||||
file.magicMetadata = MagicMetadata.fromEncodedJson(file.mMdEncodedJson);
|
||||
}
|
||||
files.add(file);
|
||||
}
|
||||
|
||||
|
|
Loading…
Add table
Reference in a new issue