backup_folder_selection_page.dart 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345
  1. import 'dart:io';
  2. import 'dart:ui';
  3. import 'package:flutter/material.dart';
  4. import 'package:implicitly_animated_reorderable_list/implicitly_animated_reorderable_list.dart';
  5. import 'package:implicitly_animated_reorderable_list/transitions.dart';
  6. import 'package:photos/core/configuration.dart';
  7. import 'package:photos/core/event_bus.dart';
  8. import 'package:photos/db/files_db.dart';
  9. import 'package:photos/ente_theme_data.dart';
  10. import 'package:photos/events/backup_folders_updated_event.dart';
  11. import 'package:photos/models/file.dart';
  12. import 'package:photos/ui/loading_widget.dart';
  13. import 'package:photos/ui/thumbnail_widget.dart';
  14. class BackupFolderSelectionPage extends StatefulWidget {
  15. final bool shouldSelectAll;
  16. final String buttonText;
  17. const BackupFolderSelectionPage({
  18. @required this.buttonText,
  19. this.shouldSelectAll = false,
  20. Key key,
  21. }) : super(key: key);
  22. @override
  23. _BackupFolderSelectionPageState createState() =>
  24. _BackupFolderSelectionPageState();
  25. }
  26. class _BackupFolderSelectionPageState extends State<BackupFolderSelectionPage> {
  27. final Set<String> _allFolders = <String>{};
  28. Set<String> _selectedFolders = <String>{};
  29. List<File> _latestFiles;
  30. Map<String, int> _itemCount;
  31. @override
  32. void initState() {
  33. _selectedFolders = Configuration.instance.getPathsToBackUp();
  34. FilesDB.instance.getLatestLocalFiles().then((files) async {
  35. _itemCount = await FilesDB.instance.getFileCountInDeviceFolders();
  36. setState(() {
  37. _latestFiles = files;
  38. _latestFiles.sort((first, second) {
  39. return first.deviceFolder
  40. .toLowerCase()
  41. .compareTo(second.deviceFolder.toLowerCase());
  42. });
  43. for (final file in _latestFiles) {
  44. _allFolders.add(file.deviceFolder);
  45. }
  46. if (widget.shouldSelectAll) {
  47. _selectedFolders.addAll(_allFolders);
  48. }
  49. _selectedFolders.removeWhere((folder) => !_allFolders.contains(folder));
  50. });
  51. });
  52. super.initState();
  53. }
  54. @override
  55. Widget build(BuildContext context) {
  56. return Scaffold(
  57. body: Column(
  58. mainAxisAlignment: MainAxisAlignment.center,
  59. children: [
  60. SizedBox(
  61. height: 0,
  62. ),
  63. SafeArea(
  64. child: Container(
  65. padding: EdgeInsets.all(24),
  66. child: Text(
  67. 'Select folders for backup',
  68. textAlign: TextAlign.left,
  69. style: TextStyle(
  70. color: Theme.of(context).colorScheme.onSurface,
  71. fontFamily: 'Inter-Bold',
  72. fontSize: 32,
  73. fontWeight: FontWeight.bold),
  74. ),
  75. ),
  76. ),
  77. Padding(
  78. padding: const EdgeInsets.only(left: 24, right: 48),
  79. child: Text(
  80. "Selected folders will be end-to-end encrypted and backed up",
  81. style: Theme.of(context).textTheme.caption.copyWith(height: 1.3),
  82. ),
  83. ),
  84. Padding(
  85. padding: EdgeInsets.all(10),
  86. ),
  87. _latestFiles == null
  88. ? Container()
  89. : GestureDetector(
  90. behavior: HitTestBehavior.translucent,
  91. child: Padding(
  92. padding: const EdgeInsets.fromLTRB(24, 6, 64, 12),
  93. child: Align(
  94. alignment: Alignment.centerLeft,
  95. child: Text(
  96. _selectedFolders.length == _allFolders.length
  97. ? "Unselect all"
  98. : "Select all",
  99. textAlign: TextAlign.right,
  100. style: Theme.of(context)
  101. .textTheme
  102. .overline
  103. .copyWith(decoration: TextDecoration.underline),
  104. ),
  105. ),
  106. ),
  107. onTap: () {
  108. final hasSelectedAll =
  109. _selectedFolders.length == _allFolders.length;
  110. // Flip selection
  111. if (hasSelectedAll) {
  112. _selectedFolders.clear();
  113. } else {
  114. _selectedFolders.addAll(_allFolders);
  115. }
  116. _latestFiles.sort((first, second) {
  117. return first.deviceFolder
  118. .toLowerCase()
  119. .compareTo(second.deviceFolder.toLowerCase());
  120. });
  121. setState(() {});
  122. }),
  123. Expanded(child: _getFolders()),
  124. Hero(
  125. tag: "select_folders",
  126. child: Container(
  127. width: double.infinity,
  128. padding: EdgeInsets.only(
  129. left: 24, right: 24, bottom: Platform.isIOS ? 60 : 32),
  130. child: OutlinedButton(
  131. child: Text(widget.buttonText),
  132. onPressed: _selectedFolders.isEmpty
  133. ? null
  134. : () async {
  135. await Configuration.instance
  136. .setPathsToBackUp(_selectedFolders);
  137. Bus.instance.fire(BackupFoldersUpdatedEvent());
  138. Navigator.of(context).pop();
  139. },
  140. // padding: EdgeInsets.fromLTRB(12, 20, 12, 20),
  141. ),
  142. ),
  143. ),
  144. ],
  145. ),
  146. );
  147. }
  148. Widget _getFolders() {
  149. if (_latestFiles == null) {
  150. return loadWidget;
  151. }
  152. _sortFiles();
  153. final scrollController = ScrollController();
  154. return Container(
  155. padding: EdgeInsets.symmetric(horizontal: 20),
  156. child: Scrollbar(
  157. controller: scrollController,
  158. isAlwaysShown: true,
  159. child: Padding(
  160. padding: const EdgeInsets.only(right: 4),
  161. child: ImplicitlyAnimatedReorderableList<File>(
  162. controller: scrollController,
  163. items: _latestFiles,
  164. areItemsTheSame: (oldItem, newItem) =>
  165. oldItem.deviceFolder == newItem.deviceFolder,
  166. onReorderFinished: (item, from, to, newItems) {
  167. setState(() {
  168. _latestFiles
  169. ..clear()
  170. ..addAll(newItems);
  171. });
  172. },
  173. itemBuilder: (context, itemAnimation, file, index) {
  174. return Reorderable(
  175. key: ValueKey(file),
  176. builder: (context, dragAnimation, inDrag) {
  177. final t = dragAnimation.value;
  178. final elevation = lerpDouble(0, 8, t);
  179. final themeColor = Theme.of(context).colorScheme.onSurface;
  180. final color =
  181. Color.lerp(themeColor, themeColor.withOpacity(0.8), t);
  182. return SizeFadeTransition(
  183. sizeFraction: 0.7,
  184. curve: Curves.easeInOut,
  185. animation: itemAnimation,
  186. child: Material(
  187. color: color,
  188. elevation: elevation,
  189. type: MaterialType.transparency,
  190. child: _getFileItem(file),
  191. ),
  192. );
  193. },
  194. );
  195. },
  196. ),
  197. ),
  198. ),
  199. );
  200. }
  201. Widget _getFileItem(File file) {
  202. final isSelected = _selectedFolders.contains(file.deviceFolder);
  203. return Padding(
  204. padding: const EdgeInsets.only(bottom: 1, right: 1),
  205. child: Container(
  206. decoration: BoxDecoration(
  207. border: Border.all(
  208. color: Theme.of(context).colorScheme.boxUnSelectColor,
  209. ),
  210. borderRadius: BorderRadius.all(
  211. Radius.circular(12),
  212. ),
  213. color: isSelected
  214. ? Theme.of(context).colorScheme.boxSelectColor
  215. : Theme.of(context).colorScheme.boxUnSelectColor,
  216. ),
  217. padding: EdgeInsets.fromLTRB(8, 4, 4, 4),
  218. child: InkWell(
  219. child: Row(
  220. mainAxisAlignment: MainAxisAlignment.spaceBetween,
  221. children: [
  222. Row(
  223. children: [
  224. Checkbox(
  225. checkColor: Colors.green,
  226. activeColor: Colors.white,
  227. value: isSelected,
  228. onChanged: (value) {
  229. if (value) {
  230. _selectedFolders.add(file.deviceFolder);
  231. } else {
  232. _selectedFolders.remove(file.deviceFolder);
  233. }
  234. setState(() {});
  235. },
  236. ),
  237. Column(
  238. crossAxisAlignment: CrossAxisAlignment.start,
  239. children: [
  240. Container(
  241. constraints: BoxConstraints(maxWidth: 180),
  242. child: Text(
  243. file.deviceFolder,
  244. textAlign: TextAlign.left,
  245. style: TextStyle(
  246. fontFamily: 'Inter-Medium',
  247. fontSize: 18,
  248. fontWeight: FontWeight.w600,
  249. color: isSelected
  250. ? Colors.white
  251. : Theme.of(context)
  252. .colorScheme
  253. .onSurface
  254. .withOpacity(0.7),
  255. ),
  256. overflow: TextOverflow.ellipsis,
  257. maxLines: 2,
  258. ),
  259. ),
  260. Padding(padding: EdgeInsets.only(top: 2)),
  261. Text(
  262. _itemCount[file.deviceFolder].toString() +
  263. " item" +
  264. (_itemCount[file.deviceFolder] == 1 ? "" : "s"),
  265. textAlign: TextAlign.left,
  266. style: TextStyle(
  267. fontSize: 12,
  268. color: isSelected
  269. ? Colors.white
  270. : Theme.of(context).colorScheme.onSurface,
  271. ),
  272. ),
  273. ],
  274. ),
  275. ],
  276. ),
  277. _getThumbnail(file, isSelected),
  278. ],
  279. ),
  280. onTap: () {
  281. final value = !_selectedFolders.contains(file.deviceFolder);
  282. if (value) {
  283. _selectedFolders.add(file.deviceFolder);
  284. } else {
  285. _selectedFolders.remove(file.deviceFolder);
  286. }
  287. setState(() {});
  288. },
  289. ),
  290. ),
  291. );
  292. }
  293. void _sortFiles() {
  294. _latestFiles.sort((first, second) {
  295. if (_selectedFolders.contains(first.deviceFolder) &&
  296. _selectedFolders.contains(second.deviceFolder)) {
  297. return first.deviceFolder
  298. .toLowerCase()
  299. .compareTo(second.deviceFolder.toLowerCase());
  300. } else if (_selectedFolders.contains(first.deviceFolder)) {
  301. return -1;
  302. } else if (_selectedFolders.contains(second.deviceFolder)) {
  303. return 1;
  304. }
  305. return first.deviceFolder
  306. .toLowerCase()
  307. .compareTo(second.deviceFolder.toLowerCase());
  308. });
  309. }
  310. Widget _getThumbnail(File file, bool isSelected) {
  311. return ClipRRect(
  312. borderRadius: BorderRadius.circular(8),
  313. child: SizedBox(
  314. child: Stack(alignment: AlignmentDirectional.bottomEnd, children: [
  315. ThumbnailWidget(
  316. file,
  317. shouldShowSyncStatus: false,
  318. key: Key("backup_selection_widget" + file.tag()),
  319. ),
  320. Padding(
  321. padding: const EdgeInsets.all(9),
  322. child: isSelected
  323. ? Icon(
  324. Icons.local_police,
  325. color: Colors.white,
  326. )
  327. : null),
  328. ]),
  329. height: 88,
  330. width: 88,
  331. ),
  332. );
  333. }
  334. }