Sort option in album (#1149)

This commit is contained in:
Neeraj Gupta 2023-05-26 11:45:06 +05:30 committed by GitHub
commit 68392fa8c6
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
12 changed files with 278 additions and 10 deletions

View file

@ -30,6 +30,9 @@ class CollectionsDB {
// MMD -> Magic Metadata
static const columnMMdEncodedJson = 'mmd_encoded_json';
static const columnMMdVersion = 'mmd_ver';
static const columnPubMMdEncodedJson = 'pub_mmd_encoded_json';
static const columnPubMMdVersion = 'pub_mmd_ver';
static const columnUpdationTime = 'updation_time';
static const columnIsDeleted = 'is_deleted';
@ -41,6 +44,7 @@ class CollectionsDB {
...addIsDeleted(),
...addPublicURLs(),
...addPrivateMetadata(),
...addPublicMetadata(),
];
final dbConfig = MigrationConfig(
@ -156,6 +160,18 @@ class CollectionsDB {
];
}
static List<String> addPublicMetadata() {
return [
'''
ALTER TABLE $table ADD COLUMN $columnPubMMdEncodedJson TEXT DEFAULT '
{}';
''',
'''
ALTER TABLE $table ADD COLUMN $columnPubMMdVersion INTEGER DEFAULT 0;
'''
];
}
Future<void> insert(List<Collection> collections) async {
final db = await instance.database;
var batch = db.batch();
@ -238,6 +254,8 @@ class CollectionsDB {
}
row[columnMMdVersion] = collection.mMdVersion;
row[columnMMdEncodedJson] = collection.mMdEncodedJson ?? '{}';
row[columnPubMMdVersion] = collection.mMbPubVersion;
row[columnPubMMdEncodedJson] = collection.mMdPubEncodedJson ?? '{}';
return row;
}
@ -271,6 +289,8 @@ class CollectionsDB {
);
result.mMdVersion = row[columnMMdVersion] ?? 0;
result.mMdEncodedJson = row[columnMMdEncodedJson] ?? '{}';
result.mMbPubVersion = row[columnPubMMdVersion] ?? 0;
result.mMdPubEncodedJson = row[columnPubMMdEncodedJson] ?? '{}';
return result;
}
}

View file

@ -0,0 +1,16 @@
import 'package:photos/events/event.dart';
class CollectionMetaEvent extends Event {
final int id;
final CollectionMetaEventType type;
CollectionMetaEvent(this.id, this.type);
}
enum CollectionMetaEventType {
created,
deleted,
archived,
sortChanged,
thumbnailChanged,
}

View file

@ -1096,6 +1096,9 @@ class MessageLookup extends MessageLookupByLibrary {
"sorryWeCouldNotGenerateSecureKeysOnThisDevicennplease":
MessageLookupByLibrary.simpleMessage(
"Sorry, we could not generate secure keys on this device.\n\nplease sign up from a different device."),
"sortAlbumsBy": MessageLookupByLibrary.simpleMessage("Sort by"),
"sortNewestFirst": MessageLookupByLibrary.simpleMessage("Newest first"),
"sortOldestFirst": MessageLookupByLibrary.simpleMessage("Oldest first"),
"sparkleSuccess": MessageLookupByLibrary.simpleMessage("✨ Success"),
"startBackup": MessageLookupByLibrary.simpleMessage("Start backup"),
"storage": MessageLookupByLibrary.simpleMessage("Storage"),

View file

@ -5233,6 +5233,36 @@ class S {
);
}
/// `Sort by`
String get sortAlbumsBy {
return Intl.message(
'Sort by',
name: 'sortAlbumsBy',
desc: '',
args: [],
);
}
/// `Newest first`
String get sortNewestFirst {
return Intl.message(
'Newest first',
name: 'sortNewestFirst',
desc: '',
args: [],
);
}
/// `Oldest first`
String get sortOldestFirst {
return Intl.message(
'Oldest first',
name: 'sortOldestFirst',
desc: '',
args: [],
);
}
/// `Rename`
String get rename {
return Intl.message(

View file

@ -748,6 +748,9 @@
},
"deleteAll": "Delete All",
"renameAlbum": "Rename album",
"sortAlbumsBy": "Sort by",
"sortNewestFirst": "Newest first",
"sortOldestFirst": "Oldest first",
"rename": "Rename",
"leaveSharedAlbum": "Leave shared album?",
"leaveAlbum": "Leave album",

View file

@ -22,13 +22,22 @@ class Collection {
final int updationTime;
final bool isDeleted;
String? mMdEncodedJson;
String? mMdPubEncodedJson;
int mMdVersion = 0;
int mMbPubVersion = 0;
CollectionMagicMetadata? _mmd;
CollectionPubMagicMetadata? _pubMmd;
CollectionMagicMetadata get magicMetadata =>
_mmd ?? CollectionMagicMetadata.fromEncodedJson(mMdEncodedJson ?? '{}');
set magicMetadata(val) => _mmd = val;
CollectionPubMagicMetadata get pubMagicMetadata =>
_pubMmd ??
CollectionPubMagicMetadata.fromEncodedJson(mMdPubEncodedJson ?? '{}');
set magicMetadata(CollectionMagicMetadata? val) => _mmd = val;
set pubMagicMetadata(CollectionPubMagicMetadata? val) => _pubMmd = val;
Collection(
this.id,
@ -162,6 +171,8 @@ class Collection {
);
result.mMdVersion = mMdVersion ?? this.mMdVersion;
result.mMdEncodedJson = mMdEncodedJson ?? this.mMdEncodedJson;
result.mMbPubVersion = mMbPubVersion;
result.mMdPubEncodedJson = mMdPubEncodedJson;
return result;
}

View file

@ -44,3 +44,35 @@ class CollectionMagicMetadata {
);
}
}
class CollectionPubMagicMetadata {
// sort order while showing collection
bool? asc;
// cover photo id for the collection
int? coverID;
CollectionPubMagicMetadata({this.asc, this.coverID});
Map<String, dynamic> toJson() {
final Map<String, dynamic> result = {"asc": asc ?? false};
if (coverID != null) {
result["coverID"] = coverID!;
}
return result;
}
factory CollectionPubMagicMetadata.fromEncodedJson(String encodedJson) =>
CollectionPubMagicMetadata.fromJson(jsonDecode(encodedJson));
factory CollectionPubMagicMetadata.fromJson(dynamic json) =>
CollectionPubMagicMetadata.fromMap(json);
static fromMap(Map<String, dynamic>? map) {
if (map == null) return null;
return CollectionPubMagicMetadata(
asc: map["asc"] as bool?,
coverID: map["coverID"],
);
}
}

View file

@ -610,6 +610,65 @@ class CollectionsService {
}
}
Future<void> updatePublicMagicMetadata(
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.
final Map<String, dynamic> jsonToUpdate =
jsonDecode(collection.mMdPubEncodedJson ?? '{}');
newMetadataUpdate.forEach((key, value) {
jsonToUpdate[key] = value;
});
final key = getCollectionKey(collection.id);
final encryptedMMd = await CryptoUtil.encryptChaCha(
utf8.encode(jsonEncode(jsonToUpdate)) as Uint8List,
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.
final int currentVersion = max(collection.mMbPubVersion, 1);
final params = UpdateMagicMetadataRequest(
id: collection.id,
magicMetadata: MetadataRequest(
version: currentVersion,
count: jsonToUpdate.length,
data: CryptoUtil.bin2base64(encryptedMMd.encryptedData!),
header: CryptoUtil.bin2base64(encryptedMMd.header!),
),
);
await _enteDio.put(
"/collections/public-magic-metadata",
data: params,
);
// update the local information so that it's reflected on UI
collection.mMdPubEncodedJson = jsonEncode(jsonToUpdate);
collection.pubMagicMetadata =
CollectionPubMagicMetadata.fromJson(jsonToUpdate);
collection.mMbPubVersion = currentVersion + 1;
_cacheCollectionAttributes(collection);
// trigger sync to fetch the latest collection state from server
sync().ignore();
} on DioError catch (e) {
if (e.response != null && e.response?.statusCode == 409) {
_logger.severe('collection magic data out of sync');
sync().ignore();
}
rethrow;
} catch (e, s) {
_logger.severe("failed to sync magic metadata", e, s);
rethrow;
}
}
Future<void> createShareUrl(
Collection collection, {
bool enableCollect = false,
@ -701,9 +760,9 @@ class CollectionsService {
final c = response.data["collections"];
for (final collectionData in c) {
final collection = Collection.fromMap(collectionData);
final collectionKey =
_getAndCacheDecryptedKey(collection, source: "fetchCollection");
if (collectionData['magicMetadata'] != null) {
final collectionKey =
_getAndCacheDecryptedKey(collection, source: "fetchCollection");
final utfEncodedMmd = await CryptoUtil.decryptChaCha(
CryptoUtil.base642bin(collectionData['magicMetadata']['data']),
collectionKey,
@ -715,6 +774,21 @@ class CollectionsService {
collection.mMdEncodedJson,
);
}
if (collectionData['pubMagicMetadata'] != null) {
final utfEncodedMmd = await CryptoUtil.decryptChaCha(
CryptoUtil.base642bin(collectionData['pubMagicMetadata']['data']),
collectionKey,
CryptoUtil.base642bin(collectionData['pubMagicMetadata']['header']),
);
collection.mMdPubEncodedJson = utf8.decode(utfEncodedMmd);
collection.mMbPubVersion =
collectionData['pubMagicMetadata']['version'];
collection.pubMagicMetadata =
CollectionPubMagicMetadata.fromEncodedJson(
collection.mMdEncodedJson,
);
}
collections.add(collection);
}
return collections;

View file

@ -2,6 +2,7 @@ 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/collection_meta_event.dart";
import 'package:photos/events/collection_updated_event.dart';
import 'package:photos/events/files_updated_event.dart';
import 'package:photos/models/collection.dart';
@ -65,6 +66,13 @@ class CollectionPage extends StatelessWidget {
reloadEvent: Bus.instance
.on<CollectionUpdatedEvent>()
.where((event) => event.collectionID == c.collection.id),
forceReloadEvents: [
Bus.instance.on<CollectionMetaEvent>().where(
(event) =>
event.id == c.collection.id &&
event.type == CollectionMetaEventType.sortChanged,
)
],
removalEventTypes: const {
EventType.deletedFromRemote,
EventType.deletedFromEverywhere,
@ -74,7 +82,7 @@ class CollectionPage extends StatelessWidget {
selectedFiles: _selectedFiles,
initialFiles: initialFiles,
albumName: c.collection.name,
sortOrderAsc: false,
sortAsyncFn: () => c.collection.pubMagicMetadata.asc ?? false,
);
return Scaffold(
appBar: PreferredSize(

View file

@ -25,6 +25,8 @@ typedef GalleryLoader = Future<FileLoadResult> Function(
bool? asc,
});
typedef SortAscFn = bool Function();
class Gallery extends StatefulWidget {
final GalleryLoader asyncLoader;
final List<File>? initialFiles;
@ -42,7 +44,9 @@ class Gallery extends StatefulWidget {
final Widget loadingWidget;
final bool disableScroll;
final bool limitSelectionToOne;
final bool sortOrderAsc;
// add a Function variable to get sort value in bool
final SortAscFn? sortAsyncFn;
const Gallery({
required this.asyncLoader,
@ -61,7 +65,7 @@ class Gallery extends StatefulWidget {
this.loadingWidget = const EnteLoadingWidget(),
this.disableScroll = false,
this.limitSelectionToOne = false,
this.sortOrderAsc = false,
this.sortAsyncFn,
Key? key,
}) : super(key: key);
@ -84,6 +88,7 @@ class _GalleryState extends State<Gallery> {
StreamSubscription<TabDoubleTapEvent>? _tabDoubleTapEvent;
final _forceReloadEventSubscriptions = <StreamSubscription<Event>>[];
late String _logTag;
bool _sortOrderAsc = false;
@override
void initState() {
@ -91,6 +96,7 @@ class _GalleryState extends State<Gallery> {
"Gallery_${widget.tagPrefix}${kDebugMode ? "_" + widget.albumName! : ""}";
_logger = Logger(_logTag);
_logger.finest("init Gallery");
_sortOrderAsc = widget.sortAsyncFn != null ? widget.sortAsyncFn!() : false;
_itemScroller = ItemScrollController();
if (widget.reloadEvent != null) {
_reloadEventSubscription = widget.reloadEvent!.listen((event) async {
@ -122,13 +128,15 @@ class _GalleryState extends State<Gallery> {
_forceReloadEventSubscriptions.add(
event.listen((event) async {
_logger.finest("Force refresh all files on ${event.reason}");
_sortOrderAsc =
widget.sortAsyncFn != null ? widget.sortAsyncFn!() : false;
final result = await _loadFiles();
_setFilesAndReload(result.files);
}),
);
}
}
if (widget.initialFiles != null && !widget.sortOrderAsc) {
if (widget.initialFiles != null && !_sortOrderAsc) {
_onFilesLoaded(widget.initialFiles!);
}
_loadFiles(limit: kInitialLoadLimit).then((result) async {
@ -156,7 +164,7 @@ class _GalleryState extends State<Gallery> {
galleryLoadStartTime,
galleryLoadEndTime,
limit: limit,
asc: widget.sortOrderAsc,
asc: _sortOrderAsc,
);
final endTime = DateTime.now().microsecondsSinceEpoch;
final duration = Duration(microseconds: endTime - startTime);
@ -216,7 +224,7 @@ class _GalleryState extends State<Gallery> {
disableScroll: widget.disableScroll,
emptyState: widget.emptyState,
asyncLoader: widget.asyncLoader,
sortOrderAsc: widget.sortOrderAsc,
sortOrderAsc: _sortOrderAsc,
removalEventTypes: widget.removalEventTypes,
tagPrefix: widget.tagPrefix,
scrollBottomSafeArea: widget.scrollBottomSafeArea,
@ -248,7 +256,7 @@ class _GalleryState extends State<Gallery> {
if (dailyFiles.isNotEmpty) {
collatedFiles.add(dailyFiles);
}
if (widget.sortOrderAsc) {
if (_sortOrderAsc) {
collatedFiles
.sort((a, b) => a[0].creationTime!.compareTo(b[0].creationTime!));
} else {

View file

@ -283,6 +283,22 @@ class _GalleryAppBarWidgetState extends State<GalleryAppBarWidget> {
// Do not show archive option for favorite collection. If collection is
// already archived, allow user to unarchive that collection.
if (isArchived || widget.collection!.type != CollectionType.favorites) {
items.add(
PopupMenuItem(
value: 6,
child: Row(
children: [
const Icon(Icons.sort_outlined),
const Padding(
padding: EdgeInsets.all(8),
),
Text(
S.of(context).sortAlbumsBy,
),
],
),
),
);
items.add(
PopupMenuItem(
value: 2,
@ -375,6 +391,8 @@ class _GalleryAppBarWidgetState extends State<GalleryAppBarWidget> {
await _leaveAlbum(context);
} else if (value == 5) {
await _deleteBackedUpFiles(context);
} else if (value == 6) {
await _showSortOption(context);
} else {
showToast(context, S.of(context).somethingWentWrong);
}
@ -386,6 +404,31 @@ class _GalleryAppBarWidgetState extends State<GalleryAppBarWidget> {
return actions;
}
Future<void> _showSortOption(BuildContext bContext) async {
final bool? sortByAsc = await showMenu<bool>(
context: bContext,
position: RelativeRect.fromLTRB(
MediaQuery.of(context).size.width,
kToolbarHeight + 12,
12,
0,
),
items: [
PopupMenuItem(
value: false,
child: Text(S.of(context).sortNewestFirst),
),
PopupMenuItem(
value: true,
child: Text(S.of(context).sortOldestFirst),
),
],
);
if (sortByAsc != null) {
changeSortOrder(bContext, widget.collection!, sortByAsc);
}
}
Future<void> _trashCollection() async {
final collectionWithThumbnail =
await CollectionsService.instance.getCollectionsWithThumbnails();

View file

@ -2,6 +2,7 @@ import 'package:flutter/material.dart';
import 'package:logging/logging.dart';
import 'package:path/path.dart';
import 'package:photos/core/event_bus.dart';
import "package:photos/events/collection_meta_event.dart";
import 'package:photos/events/force_reload_home_gallery_event.dart';
import "package:photos/generated/l10n.dart";
import 'package:photos/models/collection.dart';
@ -77,6 +78,25 @@ Future<void> changeCollectionVisibility(
}
}
Future<void> changeSortOrder(
BuildContext context,
Collection collection,
bool sortedInAscOrder,
) async {
try {
final Map<String, dynamic> update = {"asc": sortedInAscOrder};
await CollectionsService.instance
.updatePublicMagicMetadata(collection, update);
Bus.instance.fire(
CollectionMetaEvent(collection.id, CollectionMetaEventType.sortChanged),
);
} catch (e, s) {
_logger.severe("failed to update collection visibility", e, s);
showShortToast(context, S.of(context).somethingWentWrong);
rethrow;
}
}
Future<bool> editTime(
BuildContext context,
List<File> files,