backup_folder_selection_page.dart 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366
  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. appBar: widget.shouldSelectAll
  58. ? null
  59. : AppBar(
  60. elevation: 0,
  61. title: Text(""),
  62. ),
  63. body: Column(
  64. mainAxisAlignment: MainAxisAlignment.center,
  65. children: [
  66. SizedBox(
  67. height: 0,
  68. ),
  69. SafeArea(
  70. child: Container(
  71. padding: EdgeInsets.fromLTRB(24, 32, 24, 8),
  72. child: Text(
  73. 'Select folders for backup',
  74. textAlign: TextAlign.left,
  75. style: TextStyle(
  76. color: Theme.of(context).colorScheme.onSurface,
  77. fontFamily: 'Inter-Bold',
  78. fontSize: 32,
  79. fontWeight: FontWeight.bold,
  80. ),
  81. ),
  82. ),
  83. ),
  84. Padding(
  85. padding: const EdgeInsets.only(left: 24, right: 48),
  86. child: Text(
  87. "Selected folders will be encrypted and backed up",
  88. style: Theme.of(context).textTheme.caption.copyWith(height: 1.3),
  89. ),
  90. ),
  91. Padding(
  92. padding: EdgeInsets.all(10),
  93. ),
  94. _latestFiles == null
  95. ? Container()
  96. : GestureDetector(
  97. behavior: HitTestBehavior.translucent,
  98. child: Padding(
  99. padding: const EdgeInsets.fromLTRB(24, 6, 64, 12),
  100. child: Align(
  101. alignment: Alignment.centerLeft,
  102. child: Text(
  103. _selectedFolders.length == _allFolders.length
  104. ? "Unselect all"
  105. : "Select all",
  106. textAlign: TextAlign.right,
  107. style: TextStyle(
  108. decoration: TextDecoration.underline,
  109. fontSize: 16,
  110. ),
  111. ),
  112. ),
  113. ),
  114. onTap: () {
  115. final hasSelectedAll =
  116. _selectedFolders.length == _allFolders.length;
  117. // Flip selection
  118. if (hasSelectedAll) {
  119. _selectedFolders.clear();
  120. } else {
  121. _selectedFolders.addAll(_allFolders);
  122. }
  123. _latestFiles.sort((first, second) {
  124. return first.deviceFolder
  125. .toLowerCase()
  126. .compareTo(second.deviceFolder.toLowerCase());
  127. });
  128. setState(() {});
  129. }),
  130. Expanded(child: _getFolders()),
  131. Hero(
  132. tag: "select_folders",
  133. child: Container(
  134. width: double.infinity,
  135. decoration: BoxDecoration(boxShadow: [
  136. BoxShadow(
  137. color: Theme.of(context).backgroundColor,
  138. blurRadius: 24,
  139. offset: Offset(0, -8),
  140. spreadRadius: 4)
  141. ]),
  142. padding: EdgeInsets.only(
  143. left: 20, right: 20, bottom: Platform.isIOS ? 60 : 32),
  144. child: OutlinedButton(
  145. child: Text(widget.buttonText),
  146. onPressed: _selectedFolders.isEmpty
  147. ? null
  148. : () async {
  149. await Configuration.instance
  150. .setPathsToBackUp(_selectedFolders);
  151. Bus.instance.fire(BackupFoldersUpdatedEvent());
  152. Navigator.of(context).pop();
  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. thumbVisibility: 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. gradient: isSelected
  230. ? LinearGradient(colors: const [
  231. Color(0xFF00DD4D),
  232. Color(0xFF43BA6C)
  233. ]) //same for both themes
  234. : LinearGradient(colors: [
  235. Theme.of(context).colorScheme.boxUnSelectColor,
  236. Theme.of(context).colorScheme.boxUnSelectColor
  237. ])),
  238. padding: EdgeInsets.fromLTRB(8, 4, 4, 4),
  239. child: InkWell(
  240. child: Row(
  241. mainAxisAlignment: MainAxisAlignment.spaceBetween,
  242. children: [
  243. Row(
  244. children: [
  245. Checkbox(
  246. checkColor: Colors.green,
  247. activeColor: Colors.white,
  248. value: isSelected,
  249. onChanged: (value) {
  250. if (value) {
  251. _selectedFolders.add(file.deviceFolder);
  252. } else {
  253. _selectedFolders.remove(file.deviceFolder);
  254. }
  255. setState(() {});
  256. },
  257. ),
  258. Column(
  259. crossAxisAlignment: CrossAxisAlignment.start,
  260. children: [
  261. Container(
  262. constraints: BoxConstraints(maxWidth: 180),
  263. child: Text(
  264. file.deviceFolder,
  265. textAlign: TextAlign.left,
  266. style: TextStyle(
  267. fontFamily: 'Inter-Medium',
  268. fontSize: 18,
  269. fontWeight: FontWeight.w600,
  270. color: isSelected
  271. ? Colors.white
  272. : Theme.of(context)
  273. .colorScheme
  274. .onSurface
  275. .withOpacity(0.7),
  276. ),
  277. overflow: TextOverflow.ellipsis,
  278. maxLines: 2,
  279. ),
  280. ),
  281. Padding(padding: EdgeInsets.only(top: 2)),
  282. Text(
  283. _itemCount[file.deviceFolder].toString() +
  284. " item" +
  285. (_itemCount[file.deviceFolder] == 1 ? "" : "s"),
  286. textAlign: TextAlign.left,
  287. style: TextStyle(
  288. fontSize: 12,
  289. color: isSelected
  290. ? Colors.white
  291. : Theme.of(context).colorScheme.onSurface,
  292. ),
  293. ),
  294. ],
  295. ),
  296. ],
  297. ),
  298. _getThumbnail(file, isSelected),
  299. ],
  300. ),
  301. onTap: () {
  302. final value = !_selectedFolders.contains(file.deviceFolder);
  303. if (value) {
  304. _selectedFolders.add(file.deviceFolder);
  305. } else {
  306. _selectedFolders.remove(file.deviceFolder);
  307. }
  308. setState(() {});
  309. },
  310. ),
  311. ),
  312. );
  313. }
  314. void _sortFiles() {
  315. _latestFiles.sort((first, second) {
  316. if (_selectedFolders.contains(first.deviceFolder) &&
  317. _selectedFolders.contains(second.deviceFolder)) {
  318. return first.deviceFolder
  319. .toLowerCase()
  320. .compareTo(second.deviceFolder.toLowerCase());
  321. } else if (_selectedFolders.contains(first.deviceFolder)) {
  322. return -1;
  323. } else if (_selectedFolders.contains(second.deviceFolder)) {
  324. return 1;
  325. }
  326. return first.deviceFolder
  327. .toLowerCase()
  328. .compareTo(second.deviceFolder.toLowerCase());
  329. });
  330. }
  331. Widget _getThumbnail(File file, bool isSelected) {
  332. return ClipRRect(
  333. borderRadius: BorderRadius.circular(8),
  334. child: SizedBox(
  335. child: Stack(alignment: AlignmentDirectional.bottomEnd, children: [
  336. ThumbnailWidget(
  337. file,
  338. shouldShowSyncStatus: false,
  339. key: Key("backup_selection_widget" + file.tag()),
  340. ),
  341. Padding(
  342. padding: const EdgeInsets.all(9),
  343. child: isSelected
  344. ? Icon(
  345. Icons.local_police,
  346. color: Colors.white,
  347. )
  348. : null),
  349. ]),
  350. height: 88,
  351. width: 88,
  352. ),
  353. );
  354. }
  355. }