backup_folder_selection_page.dart 12 KB

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