Merge pull request #173 from ente-io/club_by_time
This commit is contained in:
commit
acdae0e186
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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"
|
||||
|
|
Loading…
Reference in a new issue