backup_folder_selection_page.dart 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337
  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. appBar: AppBar(
  59. title: Text(""),
  60. ),
  61. body: Column(
  62. mainAxisAlignment: MainAxisAlignment.center,
  63. children: [
  64. 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. Padding(
  77. padding: const EdgeInsets.only(left: 24, right: 48),
  78. child: Text(
  79. "Selected folders will be end-to-end encrypted and backed up",
  80. style: Theme.of(context).textTheme.caption.copyWith(height: 1.3),
  81. ),
  82. ),
  83. Padding(
  84. padding: EdgeInsets.all(10),
  85. ),
  86. _latestFiles == null
  87. ? Container()
  88. : GestureDetector(
  89. behavior: HitTestBehavior.translucent,
  90. child: Padding(
  91. padding: const EdgeInsets.fromLTRB(24, 6, 64, 12),
  92. child: Align(
  93. alignment: Alignment.centerLeft,
  94. child: Text(
  95. _selectedFolders.length == _allFolders.length
  96. ? "Unselect all"
  97. : "Select all",
  98. textAlign: TextAlign.right,
  99. style: Theme.of(context)
  100. .textTheme
  101. .overline
  102. .copyWith(decoration: TextDecoration.underline),
  103. ),
  104. ),
  105. ),
  106. onTap: () {
  107. final hasSelectedAll =
  108. _selectedFolders.length == _allFolders.length;
  109. // Flip selection
  110. if (hasSelectedAll) {
  111. _selectedFolders.clear();
  112. } else {
  113. _selectedFolders.addAll(_allFolders);
  114. }
  115. _latestFiles.sort((first, second) {
  116. return first.deviceFolder
  117. .toLowerCase()
  118. .compareTo(second.deviceFolder.toLowerCase());
  119. });
  120. setState(() {});
  121. }),
  122. Expanded(child: _getFolders()),
  123. Padding(
  124. padding: EdgeInsets.all(20),
  125. ),
  126. Hero(
  127. tag: "select_folders",
  128. child: Container(
  129. width: double.infinity,
  130. padding: EdgeInsets.only(
  131. left: 24, right: 24, bottom: Platform.isIOS ? 60 : 32),
  132. child: OutlinedButton(
  133. child: Text(widget.buttonText),
  134. onPressed: _selectedFolders.isEmpty
  135. ? null
  136. : () async {
  137. await Configuration.instance
  138. .setPathsToBackUp(_selectedFolders);
  139. Bus.instance.fire(BackupFoldersUpdatedEvent());
  140. Navigator.of(context).pop();
  141. },
  142. // padding: EdgeInsets.fromLTRB(12, 20, 12, 20),
  143. ),
  144. ),
  145. ),
  146. ],
  147. ),
  148. );
  149. }
  150. Widget _getFolders() {
  151. if (_latestFiles == null) {
  152. return loadWidget;
  153. }
  154. _sortFiles();
  155. final scrollController = ScrollController();
  156. return Container(
  157. padding: EdgeInsets.only(left: 20, right: 24),
  158. child: Scrollbar(
  159. controller: scrollController,
  160. isAlwaysShown: true,
  161. child: Padding(
  162. padding: const EdgeInsets.only(right: 4),
  163. child: ImplicitlyAnimatedReorderableList<File>(
  164. controller: scrollController,
  165. items: _latestFiles,
  166. areItemsTheSame: (oldItem, newItem) =>
  167. oldItem.deviceFolder == newItem.deviceFolder,
  168. onReorderFinished: (item, from, to, newItems) {
  169. setState(() {
  170. _latestFiles
  171. ..clear()
  172. ..addAll(newItems);
  173. });
  174. },
  175. itemBuilder: (context, itemAnimation, file, index) {
  176. return Reorderable(
  177. key: ValueKey(file),
  178. builder: (context, dragAnimation, inDrag) {
  179. final t = dragAnimation.value;
  180. final elevation = lerpDouble(0, 8, t);
  181. final themeColor = Theme.of(context).colorScheme.onSurface;
  182. final color =
  183. Color.lerp(themeColor, themeColor.withOpacity(0.8), t);
  184. return SizeFadeTransition(
  185. sizeFraction: 0.7,
  186. curve: Curves.easeInOut,
  187. animation: itemAnimation,
  188. child: Material(
  189. color: color,
  190. elevation: elevation,
  191. type: MaterialType.transparency,
  192. child: _getFileItem(file),
  193. ),
  194. );
  195. },
  196. );
  197. },
  198. ),
  199. ),
  200. ),
  201. );
  202. }
  203. Widget _getFileItem(File file) {
  204. final isSelected = _selectedFolders.contains(file.deviceFolder);
  205. return Padding(
  206. padding: const EdgeInsets.only(bottom: 1, right: 1),
  207. child: Container(
  208. decoration: BoxDecoration(
  209. border: Border.all(
  210. color: Theme.of(context).colorScheme.boxUnSelectColor,
  211. ),
  212. borderRadius: BorderRadius.all(
  213. Radius.circular(12),
  214. ),
  215. color: isSelected
  216. ? Theme.of(context).colorScheme.boxSelectColor
  217. : Theme.of(context).colorScheme.boxUnSelectColor,
  218. ),
  219. padding: EdgeInsets.fromLTRB(8, 4, 4, 4),
  220. child: InkWell(
  221. child: Row(
  222. mainAxisAlignment: MainAxisAlignment.spaceBetween,
  223. children: [
  224. Row(
  225. children: [
  226. Checkbox(
  227. checkColor: Colors.green,
  228. activeColor: Colors.white,
  229. value: isSelected,
  230. onChanged: (value) {
  231. if (value) {
  232. _selectedFolders.add(file.deviceFolder);
  233. } else {
  234. _selectedFolders.remove(file.deviceFolder);
  235. }
  236. setState(() {});
  237. },
  238. ),
  239. Column(
  240. crossAxisAlignment: CrossAxisAlignment.start,
  241. children: [
  242. Container(
  243. constraints: BoxConstraints(maxWidth: 180),
  244. child: Text(
  245. file.deviceFolder,
  246. textAlign: TextAlign.left,
  247. style: TextStyle(
  248. fontFamily: 'Inter-Medium',
  249. fontSize: 18,
  250. fontWeight: FontWeight.w600,
  251. color: isSelected
  252. ? Colors.white
  253. : Theme.of(context)
  254. .colorScheme
  255. .onSurface
  256. .withOpacity(0.7),
  257. ),
  258. overflow: TextOverflow.ellipsis,
  259. maxLines: 2,
  260. ),
  261. ),
  262. Padding(padding: EdgeInsets.only(top: 2)),
  263. Text(
  264. _itemCount[file.deviceFolder].toString() +
  265. " item" +
  266. (_itemCount[file.deviceFolder] == 1 ? "" : "s"),
  267. textAlign: TextAlign.left,
  268. style: TextStyle(
  269. fontSize: 12,
  270. color: isSelected
  271. ? Colors.white
  272. : Theme.of(context).colorScheme.onSurface,
  273. ),
  274. ),
  275. ],
  276. ),
  277. ],
  278. ),
  279. _getThumbnail(file),
  280. ],
  281. ),
  282. onTap: () {
  283. final value = !_selectedFolders.contains(file.deviceFolder);
  284. if (value) {
  285. _selectedFolders.add(file.deviceFolder);
  286. } else {
  287. _selectedFolders.remove(file.deviceFolder);
  288. }
  289. setState(() {});
  290. },
  291. ),
  292. ),
  293. );
  294. }
  295. void _sortFiles() {
  296. _latestFiles.sort((first, second) {
  297. if (_selectedFolders.contains(first.deviceFolder) &&
  298. _selectedFolders.contains(second.deviceFolder)) {
  299. return first.deviceFolder
  300. .toLowerCase()
  301. .compareTo(second.deviceFolder.toLowerCase());
  302. } else if (_selectedFolders.contains(first.deviceFolder)) {
  303. return -1;
  304. } else if (_selectedFolders.contains(second.deviceFolder)) {
  305. return 1;
  306. }
  307. return first.deviceFolder
  308. .toLowerCase()
  309. .compareTo(second.deviceFolder.toLowerCase());
  310. });
  311. }
  312. Widget _getThumbnail(File file) {
  313. return ClipRRect(
  314. borderRadius: BorderRadius.circular(4.0),
  315. child: SizedBox(
  316. child: ThumbnailWidget(
  317. file,
  318. shouldShowSyncStatus: false,
  319. key: Key("backup_selection_widget" + file.tag()),
  320. ),
  321. height: 88,
  322. width: 88,
  323. ),
  324. );
  325. }
  326. }