ente/lib/ui/tools/deduplicate_page.dart

464 lines
14 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';
2022-04-28 13:08:27 +00:00
import 'package:photos/ente_theme_data.dart';
import 'package:photos/events/user_details_changed_event.dart';
2023-04-07 05:57:56 +00:00
import "package:photos/generated/l10n.dart";
import 'package:photos/models/duplicate_files.dart';
2023-08-25 04:39:30 +00:00
import 'package:photos/models/file/file.dart';
import 'package:photos/services/collections_service.dart';
2023-12-14 03:19:11 +00:00
import "package:photos/theme/ente_theme.dart";
import 'package:photos/ui/viewer/file/detail_page.dart';
import 'package:photos/ui/viewer/file/thumbnail_widget.dart';
2022-09-12 12:16:34 +00:00
import 'package:photos/ui/viewer/gallery/empty_state.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-15 18:28:10 +00:00
class DeduplicatePage extends StatefulWidget {
final List<DuplicateFiles> duplicates;
const DeduplicatePage(this.duplicates, {Key? key}) : super(key: key);
2021-09-15 18:28:10 +00:00
@override
2022-07-03 09:45:00 +00:00
State<DeduplicatePage> createState() => _DeduplicatePageState();
2021-09-15 18:28:10 +00:00
}
class _DeduplicatePageState extends State<DeduplicatePage> {
2022-09-05 00:50:06 +00:00
static const crossAxisCount = 4;
static const crossAxisSpacing = 4.0;
static const headerRowCount = 3;
static final selectedOverlay = Container(
2022-09-03 13:21:00 +00:00
color: Colors.black.withOpacity(0.4),
child: const Align(
2021-09-15 18:28:10 +00:00
alignment: Alignment.bottomRight,
child: Padding(
2022-09-03 13:21:00 +00:00
padding: EdgeInsets.only(right: 4, bottom: 4),
2021-09-15 18:28:10 +00:00
child: Icon(
2022-09-03 13:21:00 +00:00
Icons.check_circle,
size: 24,
color: Colors.white,
2021-09-15 18:28:10 +00:00
),
),
),
);
2023-08-24 16:56:24 +00:00
final Set<EnteFile> _selectedFiles = <EnteFile>{};
2023-12-14 03:19:11 +00:00
final Set<int> unselectedGrids = <int>{};
final Map<int?, int> _fileSizeMap = {};
2023-12-14 03:19:11 +00:00
late List<DuplicateFiles> _duplicates;
2021-09-20 14:36:01 +00:00
SortKey sortKey = SortKey.size;
@override
void initState() {
2023-12-14 04:10:50 +00:00
_duplicates = widget.duplicates;
2023-12-14 03:19:11 +00:00
unselectedGrids.clear();
_selectAllFilesButFirst();
2023-04-21 04:55:03 +00:00
super.initState();
}
void _selectAllFilesButFirst() {
_selectedFiles.clear();
2023-12-14 03:19:11 +00:00
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(
2022-06-07 11:50:36 +00:00
elevation: 0,
2023-04-07 05:57:56 +00:00
title: Text(S.of(context).deduplicateFiles),
2022-09-03 07:49:10 +00:00
actions: <Widget>[
PopupMenuButton(
constraints: const BoxConstraints(minWidth: 180),
shape: const RoundedRectangleBorder(
borderRadius: BorderRadius.all(
Radius.circular(8),
),
),
onSelected: (dynamic value) {
2022-09-03 07:49:10 +00:00
setState(() {
_selectedFiles.clear();
});
},
offset: const Offset(0, 50),
itemBuilder: (BuildContext context) => [
PopupMenuItem(
value: true,
height: 32,
child: Row(
children: [
const Icon(
Icons.remove_circle_outline,
size: 20,
),
const SizedBox(width: 12),
Padding(
padding: const EdgeInsets.only(bottom: 1),
child: Text(
2023-04-07 05:57:56 +00:00
S.of(context).deselectAll,
2022-09-03 07:49:10 +00:00
style: Theme.of(context)
.textTheme
2023-06-13 06:41:31 +00:00
.titleMedium!
2022-09-03 07:49:10 +00:00
.copyWith(fontWeight: FontWeight.w600),
),
),
],
),
2023-08-19 11:39:56 +00:00
),
2022-09-03 07:49:10 +00:00
],
2023-08-19 11:39:56 +00:00
),
2022-09-03 07:49:10 +00:00
],
),
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 {
2022-12-30 15:42:03 +00:00
return second.files.first.creationTime! -
first.files.first.creationTime!;
2021-09-20 14:36:01 +00:00
}
});
}
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 const SizedBox.shrink();
2021-09-22 08:15:22 +00:00
} else if (index == 1) {
return const SizedBox.shrink();
} else if (index == 2) {
if (_duplicates.isNotEmpty) {
2023-04-07 05:57:56 +00:00
return _getSortMenu(context);
} else {
2022-07-04 06:02:17 +00:00
return const Padding(
padding: EdgeInsets.only(top: 32),
2022-07-04 06:02:17 +00:00
child: EmptyState(),
);
}
2021-09-22 08:15:22 +00:00
}
return Padding(
padding: const EdgeInsets.symmetric(vertical: 8),
2022-06-11 08:23:52 +00:00
child: _getGridView(
2022-09-05 00:50:06 +00:00
_duplicates[index - headerRowCount],
index - headerRowCount,
2022-06-11 08:23:52 +00:00
),
2021-09-22 08:15:22 +00:00
);
},
2022-09-05 00:50:06 +00:00
itemCount: _duplicates.length + headerRowCount,
2021-09-22 08:15:22 +00:00
shrinkWrap: true,
),
),
2022-09-05 13:54:53 +00:00
_selectedFiles.isEmpty
? const SizedBox.shrink()
: Column(
children: [
_getDeleteButton(),
const SizedBox(height: crossAxisSpacing / 2),
],
),
],
);
}
2023-04-07 05:57:56 +00:00
Widget _getSortMenu(BuildContext context) {
2021-09-20 14:36:01 +00:00
Text sortOptionText(SortKey key) {
String text = key.toString();
switch (key) {
case SortKey.count:
2023-04-07 05:57:56 +00:00
text = S.of(context).count;
2021-09-20 14:36:01 +00:00
break;
case SortKey.size:
2023-04-07 05:57:56 +00:00
text = S.of(context).totalSize;
2021-09-20 14:36:01 +00:00
break;
case SortKey.time:
2023-04-07 05:57:56 +00:00
text = S.of(context).time;
2021-09-20 14:36:01 +00:00
break;
}
return Text(
text,
2023-06-13 06:41:31 +00:00
style: Theme.of(context).textTheme.titleMedium!.copyWith(
2022-06-11 08:23:52 +00:00
fontSize: 14,
color: Theme.of(context).iconTheme.color!.withOpacity(0.7),
2022-06-11 08:23:52 +00:00
),
2021-09-20 14:36:01 +00:00
);
}
return Row(
// h4ck to align PopupMenuItems to end
mainAxisAlignment: MainAxisAlignment.spaceBetween,
crossAxisAlignment: CrossAxisAlignment.end,
children: [
const SizedBox.shrink(),
2021-09-20 14:36:01 +00:00
PopupMenuButton(
2022-12-30 15:42:03 +00:00
initialValue: sortKey.index,
2021-09-20 14:36:01 +00:00
child: Padding(
padding: const EdgeInsets.fromLTRB(24, 6, 24, 6),
child: Row(
2022-06-13 13:59:17 +00:00
mainAxisAlignment: MainAxisAlignment.start,
2021-09-20 14:36:01 +00:00
crossAxisAlignment: CrossAxisAlignment.center,
children: [
sortOptionText(sortKey),
2022-07-04 06:02:17 +00:00
const Padding(padding: EdgeInsets.only(left: 4)),
2021-09-20 14:36:01 +00:00
Icon(
Icons.sort,
2022-06-13 13:59:17 +00:00
color: Theme.of(context).colorScheme.iconColor,
2021-09-20 14:36:01 +00:00
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(
2022-06-13 13:59:17 +00:00
alignment: Alignment.centerLeft,
2021-09-20 14:36:01 +00:00
child: sortOptionText(SortKey.values[index]),
),
);
});
},
),
],
);
}
2021-09-15 20:02:42 +00:00
Widget _getDeleteButton() {
2023-06-27 08:24:22 +00:00
final String text = S.of(context).deleteItemCount(_selectedFiles.length);
2021-09-20 14:55:52 +00:00
int size = 0;
for (final file in _selectedFiles) {
size += _fileSizeMap[file.uploadedFileID]!;
2021-09-20 14:55:52 +00:00
}
2021-09-22 08:15:22 +00:00
return SizedBox(
width: double.infinity,
child: SafeArea(
child: Padding(
2022-09-05 13:54:53 +00:00
padding: const EdgeInsets.symmetric(horizontal: crossAxisSpacing / 2),
child: TextButton(
style: OutlinedButton.styleFrom(
backgroundColor:
Theme.of(context).colorScheme.inverseBackgroundColor,
),
child: Column(
mainAxisAlignment: MainAxisAlignment.end,
children: [
2022-09-05 13:54:53 +00:00
const Padding(padding: EdgeInsets.all(4)),
Text(
text,
style: TextStyle(
fontWeight: FontWeight.bold,
fontSize: 14,
color: Theme.of(context).colorScheme.inverseTextColor,
),
textAlign: TextAlign.center,
),
const Padding(padding: EdgeInsets.all(2)),
Text(
formatBytes(size),
style: TextStyle(
color: Theme.of(context)
.colorScheme
.inverseTextColor
.withOpacity(0.7),
fontSize: 12,
),
),
const Padding(padding: EdgeInsets.all(2)),
],
),
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(
2022-05-03 06:29:09 +00:00
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Padding(
2023-12-14 03:19:11 +00:00
padding: const EdgeInsets.fromLTRB(2, 4, 2, 12),
child: GestureDetector(
onTap: () {
if (unselectedGrids.contains(itemIndex)) {
unselectedGrids.remove(itemIndex);
} else {
unselectedGrids.add(itemIndex);
}
setState(() {});
},
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
S.of(context).duplicateItemsGroup(
duplicates.files.length,
formatBytes(duplicates.size),
),
style: Theme.of(context).textTheme.titleSmall,
2023-06-27 08:24:22 +00:00
),
2023-12-14 03:19:11 +00:00
unselectedGrids.contains(itemIndex)
? Icon(
Icons.check_circle_outlined,
color: getEnteColorScheme(context).strokeMuted,
size: 24,
)
: const Icon(
Icons.check_circle,
size: 24,
),
],
),
),
),
2022-09-03 13:21:00 +00:00
Padding(
2022-09-05 00:50:06 +00:00
padding: const EdgeInsets.symmetric(horizontal: crossAxisSpacing / 2),
2022-09-03 13:21:00 +00:00
child: GridView.builder(
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
// to disable GridView's scrolling
itemBuilder: (context, index) {
return _buildFile(context, duplicates.files[index], itemIndex);
},
itemCount: duplicates.files.length,
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
2022-09-05 00:50:06 +00:00
crossAxisCount: crossAxisCount,
crossAxisSpacing: crossAxisSpacing,
childAspectRatio: 0.75,
2022-09-03 13:21:00 +00:00
),
padding: const EdgeInsets.all(0),
),
),
],
);
}
2023-08-24 16:56:24 +00:00
Widget _buildFile(BuildContext context, EnteFile file, int index) {
return GestureDetector(
onTap: () {
2023-12-14 03:19:11 +00:00
final files = _duplicates[index].files;
routeToPage(
context,
DetailPage(
DetailPageConfiguration(
files,
null,
files.indexOf(file),
"deduplicate_",
mode: DetailPageMode.minimalistic,
),
),
forceCustomPageRoute: true,
);
},
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,
),
),
forceCustomPageRoute: true,
2021-09-15 20:40:08 +00:00
);
},
2022-09-03 13:21:00 +00:00
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
2022-09-03 13:21:00 +00:00
children: [
SizedBox(
2022-09-05 00:50:06 +00:00
//the numerator will give the width of the screen excuding the whitespaces in the the grid row
height: (MediaQuery.of(context).size.width -
(crossAxisSpacing * crossAxisCount)) /
crossAxisCount,
2023-12-14 03:19:11 +00:00
child: Hero(
tag: "deduplicate_" + file.tag,
child: ClipRRect(
borderRadius: BorderRadius.circular(4),
child: ThumbnailWidget(
file,
diskLoadDeferDuration: thumbnailDiskLoadDeferDuration,
serverLoadDeferDuration: thumbnailServerLoadDeferDuration,
shouldShowLivePhotoOverlay: true,
key: Key("deduplicate_" + file.tag),
2022-09-03 13:21:00 +00:00
),
2023-12-14 03:19:11 +00:00
),
2021-09-15 18:28:10 +00:00
),
2022-09-03 13:21:00 +00:00
),
const SizedBox(height: 6),
Padding(
padding: const EdgeInsets.only(right: 2),
child: Text(
CollectionsService.instance
.getCollectionByID(file.collectionID!)!
.displayName,
2022-12-30 15:42:03 +00:00
style:
2023-06-13 06:41:31 +00:00
Theme.of(context).textTheme.bodySmall!.copyWith(fontSize: 12),
overflow: TextOverflow.ellipsis,
),
),
2022-09-03 13:21:00 +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);
}