deduplicate_page.dart 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374
  1. import 'package:flutter/material.dart';
  2. import 'package:flutter/services.dart';
  3. import 'package:flutter/widgets.dart';
  4. import 'package:photos/core/constants.dart';
  5. import 'package:photos/core/event_bus.dart';
  6. import 'package:photos/events/user_details_changed_event.dart';
  7. import 'package:photos/models/duplicate_files.dart';
  8. import 'package:photos/models/file.dart';
  9. import 'package:photos/ui/detail_page.dart';
  10. import 'package:photos/ui/thumbnail_widget.dart';
  11. import 'package:photos/utils/data_util.dart';
  12. import 'package:photos/utils/delete_file_util.dart';
  13. import 'package:photos/utils/navigation_util.dart';
  14. class DeduplicatePage extends StatefulWidget {
  15. final List<DuplicateFiles> duplicates;
  16. DeduplicatePage(this.duplicates, {Key key}) : super(key: key);
  17. @override
  18. _DeduplicatePageState createState() => _DeduplicatePageState();
  19. }
  20. class _DeduplicatePageState extends State<DeduplicatePage> {
  21. static final kDeleteIconOverlay = Container(
  22. decoration: BoxDecoration(
  23. gradient: LinearGradient(
  24. begin: Alignment.topCenter,
  25. end: Alignment.bottomCenter,
  26. colors: [
  27. Colors.transparent,
  28. Colors.black.withOpacity(0.6),
  29. ],
  30. stops: const [0.75, 1],
  31. ),
  32. ),
  33. child: Align(
  34. alignment: Alignment.bottomRight,
  35. child: Padding(
  36. padding: const EdgeInsets.only(right: 8, bottom: 4),
  37. child: Icon(
  38. Icons.delete_forever,
  39. size: 18,
  40. color: Colors.red[700],
  41. ),
  42. ),
  43. ),
  44. );
  45. final Set<File> _selectedFiles = <File>{};
  46. final Map<int, int> _fileSizeMap = {};
  47. SortKey sortKey = SortKey.size;
  48. @override
  49. void initState() {
  50. super.initState();
  51. for (final duplicate in widget.duplicates) {
  52. for (int index = 0; index < duplicate.files.length; index++) {
  53. if (index != 0) {
  54. // _selectedFiles.add(duplicate.files[index]);
  55. }
  56. _fileSizeMap[duplicate.files[index].uploadedFileID] = duplicate.size;
  57. }
  58. }
  59. }
  60. @override
  61. Widget build(BuildContext context) {
  62. _sortDuplicates();
  63. return Scaffold(
  64. appBar: AppBar(
  65. title: Hero(
  66. tag: "deduplicate",
  67. child: Material(
  68. type: MaterialType.transparency,
  69. child: Text(
  70. "deduplicate files",
  71. style: TextStyle(
  72. fontSize: 18,
  73. ),
  74. ),
  75. ),
  76. ),
  77. ),
  78. body: _getBody(),
  79. );
  80. }
  81. void _sortDuplicates() {
  82. widget.duplicates.sort((first, second) {
  83. if (sortKey == SortKey.size) {
  84. final aSize = first.files.length * first.size;
  85. final bSize = second.files.length * second.size;
  86. return bSize - aSize;
  87. } else if (sortKey == SortKey.count) {
  88. return second.files.length - first.files.length;
  89. } else {
  90. return second.files.first.creationTime - first.files.first.creationTime;
  91. }
  92. });
  93. }
  94. Widget _getBody() {
  95. return Column(
  96. mainAxisAlignment: MainAxisAlignment.center,
  97. crossAxisAlignment: CrossAxisAlignment.center,
  98. children: [
  99. Expanded(
  100. child: ListView.builder(
  101. itemBuilder: (context, index) {
  102. if (index == 0) {
  103. return _getHeader();
  104. } else if (index == 1) {
  105. return _getSortMenu();
  106. }
  107. return Padding(
  108. padding: const EdgeInsets.only(top: 10, bottom: 10),
  109. child: _getGridView(widget.duplicates[index - 2], index - 2),
  110. );
  111. },
  112. itemCount: widget.duplicates.length,
  113. shrinkWrap: true,
  114. ),
  115. ),
  116. _selectedFiles.isEmpty ? Container() : _getDeleteButton(),
  117. ],
  118. );
  119. }
  120. Padding _getHeader() {
  121. return Padding(
  122. padding: EdgeInsets.fromLTRB(16, 12, 16, 8),
  123. child: Column(
  124. children: [
  125. Text(
  126. "the following files were clubbed based on their sizes and timestamps",
  127. style: TextStyle(
  128. color: Colors.white.withOpacity(0.6),
  129. height: 1.2,
  130. ),
  131. ),
  132. Padding(
  133. padding: EdgeInsets.all(4),
  134. ),
  135. Text(
  136. "please review and delete the items you believe are duplicates",
  137. style: TextStyle(
  138. color: Colors.white.withOpacity(0.6),
  139. height: 1.2,
  140. ),
  141. ),
  142. ],
  143. ),
  144. );
  145. }
  146. Widget _getSortMenu() {
  147. Text sortOptionText(SortKey key) {
  148. String text = key.toString();
  149. switch (key) {
  150. case SortKey.count:
  151. text = "count";
  152. break;
  153. case SortKey.size:
  154. text = "total size";
  155. break;
  156. case SortKey.time:
  157. text = "time";
  158. break;
  159. }
  160. return Text(
  161. text,
  162. style: TextStyle(
  163. fontWeight: FontWeight.bold,
  164. fontSize: 13,
  165. color: Colors.white.withOpacity(0.6),
  166. ),
  167. );
  168. }
  169. return Row(
  170. // h4ck to align PopupMenuItems to end
  171. mainAxisAlignment: MainAxisAlignment.spaceBetween,
  172. crossAxisAlignment: CrossAxisAlignment.end,
  173. children: [
  174. Container(),
  175. PopupMenuButton(
  176. initialValue: sortKey?.index ?? 0,
  177. child: Padding(
  178. padding: const EdgeInsets.fromLTRB(24, 6, 24, 6),
  179. child: Row(
  180. mainAxisAlignment: MainAxisAlignment.end,
  181. crossAxisAlignment: CrossAxisAlignment.center,
  182. children: [
  183. sortOptionText(sortKey),
  184. Padding(padding: EdgeInsets.only(left: 5.0)),
  185. Icon(
  186. Icons.sort,
  187. color: Theme.of(context).buttonColor,
  188. size: 20,
  189. ),
  190. ],
  191. ),
  192. ),
  193. onSelected: (int index) {
  194. setState(() {
  195. sortKey = SortKey.values[index];
  196. });
  197. },
  198. itemBuilder: (context) {
  199. return List.generate(SortKey.values.length, (index) {
  200. return PopupMenuItem(
  201. value: index,
  202. child: Align(
  203. alignment: Alignment.topRight,
  204. child: sortOptionText(SortKey.values[index]),
  205. ),
  206. );
  207. });
  208. },
  209. ),
  210. ],
  211. );
  212. }
  213. Widget _getDeleteButton() {
  214. String text;
  215. if (_selectedFiles.length == 1) {
  216. text = "delete 1 item";
  217. } else {
  218. text = "delete " + _selectedFiles.length.toString() + " items";
  219. }
  220. int size = 0;
  221. for (final file in _selectedFiles) {
  222. size += _fileSizeMap[file.uploadedFileID];
  223. }
  224. return SizedBox(
  225. width: double.infinity,
  226. child: TextButton(
  227. style: OutlinedButton.styleFrom(
  228. backgroundColor: Colors.red[700],
  229. ),
  230. child: Column(
  231. children: [
  232. Padding(padding: EdgeInsets.all(2)),
  233. Text(
  234. text,
  235. style: TextStyle(
  236. fontWeight: FontWeight.bold,
  237. fontSize: 14,
  238. color: Colors.white,
  239. ),
  240. textAlign: TextAlign.center,
  241. ),
  242. Padding(padding: EdgeInsets.all(2)),
  243. Text(
  244. formatBytes(size),
  245. style: TextStyle(
  246. color: Colors.white.withOpacity(0.7),
  247. fontSize: 12,
  248. ),
  249. ),
  250. Padding(padding: EdgeInsets.all(2)),
  251. ],
  252. ),
  253. onPressed: () async {
  254. await deleteFilesFromRemoteOnly(context, _selectedFiles.toList());
  255. Bus.instance.fire(UserDetailsChangedEvent());
  256. Navigator.of(context)
  257. .pop(DeduplicationResult(_selectedFiles.length, size));
  258. },
  259. ),
  260. );
  261. }
  262. Widget _getGridView(DuplicateFiles duplicates, int itemIndex) {
  263. return Column(
  264. children: [
  265. Padding(
  266. padding: const EdgeInsets.fromLTRB(16, 8, 4, 4),
  267. child: Text(
  268. duplicates.files.length.toString() +
  269. " files, " +
  270. formatBytes(duplicates.size) +
  271. " each",
  272. style: TextStyle(
  273. color: Colors.white.withOpacity(0.9),
  274. ),
  275. ),
  276. ),
  277. GridView.builder(
  278. shrinkWrap: true,
  279. physics:
  280. NeverScrollableScrollPhysics(), // to disable GridView's scrolling
  281. itemBuilder: (context, index) {
  282. return _buildFile(context, duplicates.files[index], itemIndex);
  283. },
  284. itemCount: duplicates.files.length,
  285. gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
  286. crossAxisCount: 4,
  287. ),
  288. padding: EdgeInsets.all(0),
  289. ),
  290. ],
  291. );
  292. }
  293. Widget _buildFile(BuildContext context, File file, int index) {
  294. return GestureDetector(
  295. onTap: () {
  296. if (_selectedFiles.contains(file)) {
  297. _selectedFiles.remove(file);
  298. } else {
  299. _selectedFiles.add(file);
  300. }
  301. setState(() {});
  302. },
  303. onLongPress: () {
  304. HapticFeedback.lightImpact();
  305. final files = widget.duplicates[index].files;
  306. routeToPage(
  307. context,
  308. DetailPage(
  309. DetailPageConfiguration(
  310. files,
  311. null,
  312. files.indexOf(file),
  313. "deduplicate_",
  314. mode: DetailPageMode.minimalistic,
  315. ),
  316. ),
  317. );
  318. },
  319. child: Container(
  320. margin: const EdgeInsets.all(2.0),
  321. decoration: BoxDecoration(
  322. border: _selectedFiles.contains(file)
  323. ? Border.all(
  324. width: 3,
  325. color: Colors.red[700],
  326. )
  327. : null,
  328. ),
  329. child: Stack(children: [
  330. Hero(
  331. tag: "deduplicate_" + file.tag(),
  332. child: ThumbnailWidget(
  333. file,
  334. diskLoadDeferDuration: kThumbnailDiskLoadDeferDuration,
  335. serverLoadDeferDuration: kThumbnailServerLoadDeferDuration,
  336. shouldShowLivePhotoOverlay: true,
  337. key: Key("deduplicate_" + file.tag()),
  338. ),
  339. ),
  340. _selectedFiles.contains(file) ? kDeleteIconOverlay : Container(),
  341. ]),
  342. ),
  343. );
  344. }
  345. }
  346. enum SortKey {
  347. size,
  348. count,
  349. time,
  350. }
  351. class DeduplicationResult {
  352. final int count;
  353. final int size;
  354. DeduplicationResult(this.count, this.size);
  355. }