Merge pull request #173 from ente-io/club_by_time

This commit is contained in:
Vishnu Mohandas 2022-01-21 16:30:00 +05:30 committed by GitHub
commit acdae0e186
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
3 changed files with 110 additions and 50 deletions

View file

@ -24,40 +24,67 @@ class DeduplicationService {
ids.addAll(dupe.fileIDs);
}
final fileMap = await FilesDB.instance.getFilesFromIDs(ids);
return _filterDuplicatesByCreationTime(dupes, fileMap);
final result = <DuplicateFiles>[];
final missingFileIDs = <int>[];
for (final dupe in dupes.duplicates) {
final files = <File>[];
for (final id in dupe.fileIDs) {
final file = fileMap[id];
if (file != null) {
files.add(file);
} else {
missingFileIDs.add(id);
}
}
// Place files that are available locally at first to minimize the chances
// of a deletion followed by a re-upload
files.sort((first, second) {
if (first.localID != null && second.localID == null) {
return -1;
} else if (first.localID == null && second.localID != null) {
return 1;
}
return 0;
});
if (files.length > 1) {
result.add(DuplicateFiles(files, dupe.size));
}
}
if (missingFileIDs.isNotEmpty) {
_logger.severe(
"Missing files",
InvalidStateError("Could not find " +
missingFileIDs.length.toString() +
" files in local DB: " +
missingFileIDs.toString()));
}
return result;
} catch (e) {
_logger.severe(e);
rethrow;
}
}
List<DuplicateFiles> _filterDuplicatesByCreationTime(
DuplicateFilesResponse dupes, Map<int, File> fileMap) {
List<DuplicateFiles> clubDuplicatesByTime(List<DuplicateFiles> dupes) {
final result = <DuplicateFiles>[];
final missingFileIDs = <int>[];
for (final dupe in dupes.duplicates) {
for (final dupe in dupes) {
final files = <File>[];
final Map<int, int> creationTimeCounter = {};
int mostFrequentCreationTime = 0, mostFrequentCreationTimeCount = 0;
// Counts the frequency of creationTimes within the supposed duplicates
for (final id in dupe.fileIDs) {
final file = fileMap[id];
if (file != null) {
if (creationTimeCounter.containsKey(file.creationTime)) {
creationTimeCounter[file.creationTime]++;
} else {
creationTimeCounter[file.creationTime] = 0;
}
if (creationTimeCounter[file.creationTime] >
mostFrequentCreationTimeCount) {
mostFrequentCreationTimeCount =
creationTimeCounter[file.creationTime];
mostFrequentCreationTime = file.creationTime;
}
files.add(file);
for (final file in dupe.files) {
if (creationTimeCounter.containsKey(file.creationTime)) {
creationTimeCounter[file.creationTime]++;
} else {
missingFileIDs.add(id);
creationTimeCounter[file.creationTime] = 0;
}
if (creationTimeCounter[file.creationTime] >
mostFrequentCreationTimeCount) {
mostFrequentCreationTimeCount =
creationTimeCounter[file.creationTime];
mostFrequentCreationTime = file.creationTime;
}
files.add(file);
}
// Ignores those files that were not created within the most common creationTime
final incorrectDuplicates = <File>{};
@ -67,28 +94,10 @@ class DeduplicationService {
}
}
files.removeWhere((file) => incorrectDuplicates.contains(file));
// Place files that are available locally at first to minimize the chances
// of a deletion followed by a re-upload
files.sort((first, second) {
if (first.localID != null && second.localID == null) {
return -1;
} else if (first.localID == null && second.localID != null) {
return 1;
}
return 0;
});
if (files.length > 1) {
result.add(DuplicateFiles(files, dupe.size));
}
}
if (missingFileIDs.isNotEmpty) {
_logger.severe(
"Missing files",
InvalidStateError("Could not find " +
missingFileIDs.length.toString() +
" files in local DB: " +
missingFileIDs.toString()));
}
return result;
}

View file

@ -1,11 +1,12 @@
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter/widgets.dart';
import 'package:photos/core/constants.dart';
import 'package:photos/core/event_bus.dart';
import 'package:photos/events/user_details_changed_event.dart';
import 'package:photos/models/duplicate_files.dart';
import 'package:photos/models/file.dart';
import 'package:photos/services/deduplication_service.dart';
import 'package:photos/ui/common_elements.dart';
import 'package:photos/ui/detail_page.dart';
import 'package:photos/ui/thumbnail_widget.dart';
import 'package:photos/utils/data_util.dart';
@ -23,7 +24,7 @@ class DeduplicatePage extends StatefulWidget {
}
class _DeduplicatePageState extends State<DeduplicatePage> {
static final kHeaderRowCount = 2;
static final kHeaderRowCount = 3;
static final kDeleteIconOverlay = Container(
decoration: BoxDecoration(
gradient: LinearGradient(
@ -51,13 +52,23 @@ class _DeduplicatePageState extends State<DeduplicatePage> {
final Set<File> _selectedFiles = <File>{};
final Map<int, int> _fileSizeMap = {};
List<DuplicateFiles> _duplicates;
bool _shouldClubByCaptureTime = true;
SortKey sortKey = SortKey.size;
@override
void initState() {
super.initState();
for (final duplicate in widget.duplicates) {
_duplicates =
DeduplicationService.instance.clubDuplicatesByTime(widget.duplicates);
_selectAllFilesButFirst();
showToast("long-press on an item to view in full-screen");
}
void _selectAllFilesButFirst() {
_selectedFiles.clear();
for (final duplicate in _duplicates) {
for (int index = 0; index < duplicate.files.length; index++) {
// Select all items but the first
if (index != 0) {
@ -67,7 +78,6 @@ class _DeduplicatePageState extends State<DeduplicatePage> {
_fileSizeMap[duplicate.files[index].uploadedFileID] = duplicate.size;
}
}
showToast("long-press on an item to view in full-screen");
}
@override
@ -93,7 +103,7 @@ class _DeduplicatePageState extends State<DeduplicatePage> {
}
void _sortDuplicates() {
widget.duplicates.sort((first, second) {
_duplicates.sort((first, second) {
if (sortKey == SortKey.size) {
final aSize = first.files.length * first.size;
final bSize = second.files.length * second.size;
@ -117,15 +127,24 @@ class _DeduplicatePageState extends State<DeduplicatePage> {
if (index == 0) {
return _getHeader();
} else if (index == 1) {
return _getSortMenu();
return _getClubbingConfig();
} else if (index == 2) {
if (_duplicates.isNotEmpty) {
return _getSortMenu();
} else {
return Padding(
padding: EdgeInsets.only(top: 32),
child: nothingToSeeHere,
);
}
}
return Padding(
padding: const EdgeInsets.only(top: 10, bottom: 10),
child: _getGridView(widget.duplicates[index - kHeaderRowCount],
child: _getGridView(_duplicates[index - kHeaderRowCount],
index - kHeaderRowCount),
);
},
itemCount: widget.duplicates.length + kHeaderRowCount,
itemCount: _duplicates.length + kHeaderRowCount,
shrinkWrap: true,
),
),
@ -140,7 +159,8 @@ class _DeduplicatePageState extends State<DeduplicatePage> {
child: Column(
children: [
Text(
"the following files were clubbed based on their sizes and capture times",
"the following files were clubbed based on their sizes" +
(_shouldClubByCaptureTime ? " and capture times" : ""),
style: TextStyle(
color: Colors.white.withOpacity(0.6),
height: 1.2,
@ -156,11 +176,42 @@ class _DeduplicatePageState extends State<DeduplicatePage> {
height: 1.2,
),
),
Padding(
padding: EdgeInsets.all(12),
),
Divider(
height: 0,
),
],
),
);
}
Widget _getClubbingConfig() {
return Padding(
padding: EdgeInsets.fromLTRB(20, 0, 20, 4),
child: CheckboxListTile(
value: _shouldClubByCaptureTime,
onChanged: (value) {
_shouldClubByCaptureTime = value;
_resetEntriesAndSelection();
setState(() {});
},
title: Text("club by capture time"),
),
);
}
void _resetEntriesAndSelection() {
if (_shouldClubByCaptureTime) {
_duplicates =
DeduplicationService.instance.clubDuplicatesByTime(_duplicates);
} else {
_duplicates = widget.duplicates;
}
_selectAllFilesButFirst();
}
Widget _getSortMenu() {
Text sortOptionText(SortKey key) {
String text = key.toString();
@ -324,7 +375,7 @@ class _DeduplicatePageState extends State<DeduplicatePage> {
},
onLongPress: () {
HapticFeedback.lightImpact();
final files = widget.duplicates[index].files;
final files = _duplicates[index].files;
routeToPage(
context,
DetailPage(

View file

@ -11,7 +11,7 @@ description: ente photos application
# In iOS, build-name is used as CFBundleShortVersionString while build-number used as CFBundleVersion.
# Read more about iOS versioning at
# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
version: 0.4.11+271
version: 0.4.12+272
environment:
sdk: ">=2.10.0 <3.0.0"