ente/lib/ui/deduplicate_page.dart

432 lines
12 KiB
Dart
Raw Normal View History

import 'package:flutter/material.dart';
import 'package:flutter/services.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';
2021-09-15 19:09:55 +00:00
import 'package:photos/ui/detail_page.dart';
import 'package:photos/ui/thumbnail_widget.dart';
import 'package:photos/utils/data_util.dart';
2021-09-15 20:50:13 +00:00
import 'package:photos/utils/delete_file_util.dart';
2021-09-15 19:09:55 +00:00
import 'package:photos/utils/navigation_util.dart';
2021-09-23 07:09:31 +00:00
import 'package:photos/utils/toast_util.dart';
2021-09-15 18:28:10 +00:00
class DeduplicatePage extends StatefulWidget {
final List<DuplicateFiles> duplicates;
DeduplicatePage(this.duplicates, {Key key}) : super(key: key);
2021-09-15 18:28:10 +00:00
@override
_DeduplicatePageState createState() => _DeduplicatePageState();
}
class _DeduplicatePageState extends State<DeduplicatePage> {
static final kHeaderRowCount = 3;
2021-09-15 21:09:24 +00:00
static final kDeleteIconOverlay = Container(
2021-09-15 18:28:10 +00:00
decoration: BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
colors: [
Colors.transparent,
Colors.black.withOpacity(0.6),
],
stops: const [0.75, 1],
),
),
child: Align(
alignment: Alignment.bottomRight,
child: Padding(
padding: const EdgeInsets.only(right: 8, bottom: 4),
child: Icon(
Icons.delete_forever,
size: 18,
color: Colors.red[700],
),
),
),
);
final Set<File> _selectedFiles = <File>{};
2021-09-20 14:55:52 +00:00
final Map<int, int> _fileSizeMap = {};
List<DuplicateFiles> _duplicates;
bool _shouldClubByCaptureTime = true;
2021-09-20 14:36:01 +00:00
SortKey sortKey = SortKey.size;
@override
void initState() {
super.initState();
_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++) {
2021-09-22 08:38:00 +00:00
// Select all items but the first
if (index != 0) {
2021-09-22 08:35:51 +00:00
_selectedFiles.add(duplicate.files[index]);
}
2021-09-22 08:38:00 +00:00
// Maintain a map of fileID to fileSize for quick "space freed" computation
2021-09-20 14:55:52 +00:00
_fileSizeMap[duplicate.files[index].uploadedFileID] = duplicate.size;
}
}
}
2021-09-15 18:28:10 +00:00
@override
Widget build(BuildContext context) {
2021-09-20 14:36:01 +00:00
_sortDuplicates();
return Scaffold(
appBar: AppBar(
title: Hero(
tag: "deduplicate",
child: Material(
type: MaterialType.transparency,
child: Text(
"deduplicate files",
style: TextStyle(
fontSize: 18,
),
),
),
),
),
body: _getBody(),
);
}
2021-09-20 14:36:01 +00:00
void _sortDuplicates() {
_duplicates.sort((first, second) {
2021-09-20 14:36:01 +00:00
if (sortKey == SortKey.size) {
final aSize = first.files.length * first.size;
final bSize = second.files.length * second.size;
return bSize - aSize;
} else if (sortKey == SortKey.count) {
return second.files.length - first.files.length;
} else {
return second.files.first.creationTime - first.files.first.creationTime;
}
});
}
Widget _getBody() {
2021-09-22 08:15:22 +00:00
return Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
2021-09-22 08:15:22 +00:00
Expanded(
child: ListView.builder(
itemBuilder: (context, index) {
if (index == 0) {
return _getHeader();
} else if (index == 1) {
return _getClubbingConfig();
} else if (index == 2) {
if (_duplicates.isNotEmpty) {
return _getSortMenu();
} else {
return Padding(
padding: EdgeInsets.only(top: 32),
child: nothingToSeeHere,
);
}
2021-09-22 08:15:22 +00:00
}
return Padding(
padding: const EdgeInsets.only(top: 10, bottom: 10),
child: _getGridView(_duplicates[index - kHeaderRowCount],
2021-09-22 10:06:36 +00:00
index - kHeaderRowCount),
2021-09-22 08:15:22 +00:00
);
},
itemCount: _duplicates.length + kHeaderRowCount,
2021-09-22 08:15:22 +00:00
shrinkWrap: true,
),
),
2021-09-22 08:15:22 +00:00
_selectedFiles.isEmpty ? Container() : _getDeleteButton(),
],
);
}
Padding _getHeader() {
return Padding(
2021-09-30 09:33:57 +00:00
padding: EdgeInsets.fromLTRB(20, 12, 20, 12),
child: Column(
children: [
Text(
"the following files were clubbed based on their sizes" +
(_shouldClubByCaptureTime ? " and capture times" : ""),
style: TextStyle(
color: Colors.white.withOpacity(0.6),
height: 1.2,
),
),
Padding(
padding: EdgeInsets.all(4),
),
Text(
"please review and delete the items you believe are duplicates",
style: TextStyle(
color: Colors.white.withOpacity(0.6),
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();
}
2021-09-20 14:36:01 +00:00
Widget _getSortMenu() {
Text sortOptionText(SortKey key) {
String text = key.toString();
switch (key) {
case SortKey.count:
text = "count";
break;
case SortKey.size:
text = "total size";
break;
case SortKey.time:
text = "time";
break;
}
return Text(
text,
style: TextStyle(
fontWeight: FontWeight.bold,
fontSize: 13,
color: Colors.white.withOpacity(0.6),
),
);
}
return Row(
// h4ck to align PopupMenuItems to end
mainAxisAlignment: MainAxisAlignment.spaceBetween,
crossAxisAlignment: CrossAxisAlignment.end,
children: [
Container(),
PopupMenuButton(
initialValue: sortKey?.index ?? 0,
child: Padding(
padding: const EdgeInsets.fromLTRB(24, 6, 24, 6),
child: Row(
mainAxisAlignment: MainAxisAlignment.end,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
sortOptionText(sortKey),
Padding(padding: EdgeInsets.only(left: 5.0)),
Icon(
Icons.sort,
color: Theme.of(context).buttonColor,
size: 20,
),
],
),
),
onSelected: (int index) {
setState(() {
sortKey = SortKey.values[index];
});
},
itemBuilder: (context) {
return List.generate(SortKey.values.length, (index) {
return PopupMenuItem(
value: index,
child: Align(
alignment: Alignment.topRight,
child: sortOptionText(SortKey.values[index]),
),
);
});
},
),
],
);
}
2021-09-15 20:02:42 +00:00
Widget _getDeleteButton() {
String text;
2021-09-22 08:15:22 +00:00
if (_selectedFiles.length == 1) {
2021-09-15 20:02:42 +00:00
text = "delete 1 item";
} else {
text = "delete " + _selectedFiles.length.toString() + " items";
2021-09-15 20:02:42 +00:00
}
2021-09-20 14:55:52 +00:00
int size = 0;
for (final file in _selectedFiles) {
size += _fileSizeMap[file.uploadedFileID];
}
2021-09-22 08:15:22 +00:00
return SizedBox(
width: double.infinity,
child: TextButton(
style: OutlinedButton.styleFrom(
backgroundColor: Colors.red[700],
),
child: Column(
children: [
Padding(padding: EdgeInsets.all(2)),
Text(
text,
style: TextStyle(
fontWeight: FontWeight.bold,
fontSize: 14,
2021-09-22 08:26:44 +00:00
color: Colors.white,
2021-09-20 14:55:52 +00:00
),
2021-09-22 08:15:22 +00:00
textAlign: TextAlign.center,
2021-09-20 14:55:52 +00:00
),
2021-09-22 08:15:22 +00:00
Padding(padding: EdgeInsets.all(2)),
Text(
formatBytes(size),
style: TextStyle(
color: Colors.white.withOpacity(0.7),
fontSize: 12,
),
2021-09-20 14:55:52 +00:00
),
2021-09-22 08:15:22 +00:00
Padding(padding: EdgeInsets.all(2)),
],
2021-09-20 14:55:52 +00:00
),
2021-09-22 08:26:44 +00:00
onPressed: () async {
await deleteFilesFromRemoteOnly(context, _selectedFiles.toList());
Bus.instance.fire(UserDetailsChangedEvent());
Navigator.of(context)
.pop(DeduplicationResult(_selectedFiles.length, size));
},
2021-09-20 14:55:52 +00:00
),
2021-09-15 20:02:42 +00:00
);
}
2021-09-15 19:09:55 +00:00
Widget _getGridView(DuplicateFiles duplicates, int itemIndex) {
return Column(
children: [
Padding(
padding: const EdgeInsets.fromLTRB(16, 8, 4, 4),
child: Text(
duplicates.files.length.toString() +
" files, " +
2021-09-15 18:19:22 +00:00
formatBytes(duplicates.size) +
" each",
style: TextStyle(
color: Colors.white.withOpacity(0.9),
),
),
),
GridView.builder(
shrinkWrap: true,
2021-09-22 10:06:36 +00:00
physics: NeverScrollableScrollPhysics(),
// to disable GridView's scrolling
itemBuilder: (context, index) {
2021-09-15 19:09:55 +00:00
return _buildFile(context, duplicates.files[index], itemIndex);
},
itemCount: duplicates.files.length,
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 4,
),
padding: EdgeInsets.all(0),
),
],
);
}
2021-09-14 19:29:24 +00:00
Widget _buildFile(BuildContext context, File file, int index) {
return GestureDetector(
onTap: () {
if (_selectedFiles.contains(file)) {
_selectedFiles.remove(file);
2021-09-15 18:28:10 +00:00
} else {
_selectedFiles.add(file);
2021-09-15 18:28:10 +00:00
}
setState(() {});
},
onLongPress: () {
HapticFeedback.lightImpact();
final files = _duplicates[index].files;
2021-09-15 19:09:55 +00:00
routeToPage(
2021-09-15 20:40:08 +00:00
context,
DetailPage(
DetailPageConfiguration(
files,
null,
files.indexOf(file),
"deduplicate_",
mode: DetailPageMode.minimalistic,
),
),
);
},
child: Container(
margin: const EdgeInsets.all(2.0),
decoration: BoxDecoration(
border: _selectedFiles.contains(file)
2021-09-15 18:28:10 +00:00
? Border.all(
width: 3,
color: Colors.red[700],
)
: null,
),
2021-09-15 18:28:10 +00:00
child: Stack(children: [
Hero(
tag: "deduplicate_" + file.tag(),
child: ThumbnailWidget(
file,
diskLoadDeferDuration: kThumbnailDiskLoadDeferDuration,
serverLoadDeferDuration: kThumbnailServerLoadDeferDuration,
shouldShowLivePhotoOverlay: true,
key: Key("deduplicate_" + file.tag()),
),
),
_selectedFiles.contains(file) ? kDeleteIconOverlay : Container(),
2021-09-15 18:28:10 +00:00
]),
),
);
}
}
2021-09-20 14:36:01 +00:00
enum SortKey {
size,
count,
time,
}
2021-09-22 08:26:44 +00:00
class DeduplicationResult {
final int count;
final int size;
DeduplicationResult(this.count, this.size);
}