backup_folder_selection_page.dart 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379
  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. ),
  131. Expanded(child: _getFolders()),
  132. Hero(
  133. tag: "select_folders",
  134. child: Container(
  135. width: double.infinity,
  136. decoration: BoxDecoration(
  137. boxShadow: [
  138. BoxShadow(
  139. color: Theme.of(context).backgroundColor,
  140. blurRadius: 24,
  141. offset: Offset(0, -8),
  142. spreadRadius: 4,
  143. )
  144. ],
  145. ),
  146. padding: EdgeInsets.only(
  147. left: 20,
  148. right: 20,
  149. bottom: Platform.isIOS ? 60 : 32,
  150. ),
  151. child: OutlinedButton(
  152. child: Text(widget.buttonText),
  153. onPressed: _selectedFolders.isEmpty
  154. ? null
  155. : () async {
  156. await Configuration.instance
  157. .setPathsToBackUp(_selectedFolders);
  158. Bus.instance.fire(BackupFoldersUpdatedEvent());
  159. Navigator.of(context).pop();
  160. },
  161. ),
  162. ),
  163. ),
  164. ],
  165. ),
  166. );
  167. }
  168. Widget _getFolders() {
  169. if (_latestFiles == null) {
  170. return loadWidget;
  171. }
  172. _sortFiles();
  173. final scrollController = ScrollController();
  174. return Container(
  175. padding: EdgeInsets.symmetric(horizontal: 20),
  176. child: Scrollbar(
  177. controller: scrollController,
  178. thumbVisibility: true,
  179. child: Padding(
  180. padding: const EdgeInsets.only(right: 4),
  181. child: ImplicitlyAnimatedReorderableList<File>(
  182. controller: scrollController,
  183. items: _latestFiles,
  184. areItemsTheSame: (oldItem, newItem) =>
  185. oldItem.deviceFolder == newItem.deviceFolder,
  186. onReorderFinished: (item, from, to, newItems) {
  187. setState(() {
  188. _latestFiles
  189. ..clear()
  190. ..addAll(newItems);
  191. });
  192. },
  193. itemBuilder: (context, itemAnimation, file, index) {
  194. return Reorderable(
  195. key: ValueKey(file),
  196. builder: (context, dragAnimation, inDrag) {
  197. final t = dragAnimation.value;
  198. final elevation = lerpDouble(0, 8, t);
  199. final themeColor = Theme.of(context).colorScheme.onSurface;
  200. final color =
  201. Color.lerp(themeColor, themeColor.withOpacity(0.8), t);
  202. return SizeFadeTransition(
  203. sizeFraction: 0.7,
  204. curve: Curves.easeInOut,
  205. animation: itemAnimation,
  206. child: Material(
  207. color: color,
  208. elevation: elevation,
  209. type: MaterialType.transparency,
  210. child: _getFileItem(file),
  211. ),
  212. );
  213. },
  214. );
  215. },
  216. ),
  217. ),
  218. ),
  219. );
  220. }
  221. Widget _getFileItem(File file) {
  222. final isSelected = _selectedFolders.contains(file.deviceFolder);
  223. return Padding(
  224. padding: const EdgeInsets.only(bottom: 1, right: 1),
  225. child: Container(
  226. decoration: BoxDecoration(
  227. border: Border.all(
  228. color: Theme.of(context).colorScheme.boxUnSelectColor,
  229. ),
  230. borderRadius: BorderRadius.all(
  231. Radius.circular(12),
  232. ),
  233. // color: isSelected
  234. // ? Theme.of(context).colorScheme.boxSelectColor
  235. // : Theme.of(context).colorScheme.boxUnSelectColor,
  236. gradient: isSelected
  237. ? LinearGradient(
  238. colors: const [Color(0xFF00DD4D), Color(0xFF43BA6C)],
  239. ) //same for both themes
  240. : LinearGradient(
  241. colors: [
  242. Theme.of(context).colorScheme.boxUnSelectColor,
  243. Theme.of(context).colorScheme.boxUnSelectColor
  244. ],
  245. ),
  246. ),
  247. padding: EdgeInsets.fromLTRB(8, 4, 4, 4),
  248. child: InkWell(
  249. child: Row(
  250. mainAxisAlignment: MainAxisAlignment.spaceBetween,
  251. children: [
  252. Row(
  253. children: [
  254. Checkbox(
  255. checkColor: Colors.green,
  256. activeColor: Colors.white,
  257. value: isSelected,
  258. onChanged: (value) {
  259. if (value) {
  260. _selectedFolders.add(file.deviceFolder);
  261. } else {
  262. _selectedFolders.remove(file.deviceFolder);
  263. }
  264. setState(() {});
  265. },
  266. ),
  267. Column(
  268. crossAxisAlignment: CrossAxisAlignment.start,
  269. children: [
  270. Container(
  271. constraints: BoxConstraints(maxWidth: 180),
  272. child: Text(
  273. file.deviceFolder,
  274. textAlign: TextAlign.left,
  275. style: TextStyle(
  276. fontFamily: 'Inter-Medium',
  277. fontSize: 18,
  278. fontWeight: FontWeight.w600,
  279. color: isSelected
  280. ? Colors.white
  281. : Theme.of(context)
  282. .colorScheme
  283. .onSurface
  284. .withOpacity(0.7),
  285. ),
  286. overflow: TextOverflow.ellipsis,
  287. maxLines: 2,
  288. ),
  289. ),
  290. Padding(padding: EdgeInsets.only(top: 2)),
  291. Text(
  292. _itemCount[file.deviceFolder].toString() +
  293. " item" +
  294. (_itemCount[file.deviceFolder] == 1 ? "" : "s"),
  295. textAlign: TextAlign.left,
  296. style: TextStyle(
  297. fontSize: 12,
  298. color: isSelected
  299. ? Colors.white
  300. : Theme.of(context).colorScheme.onSurface,
  301. ),
  302. ),
  303. ],
  304. ),
  305. ],
  306. ),
  307. _getThumbnail(file, isSelected),
  308. ],
  309. ),
  310. onTap: () {
  311. final value = !_selectedFolders.contains(file.deviceFolder);
  312. if (value) {
  313. _selectedFolders.add(file.deviceFolder);
  314. } else {
  315. _selectedFolders.remove(file.deviceFolder);
  316. }
  317. setState(() {});
  318. },
  319. ),
  320. ),
  321. );
  322. }
  323. void _sortFiles() {
  324. _latestFiles.sort((first, second) {
  325. if (_selectedFolders.contains(first.deviceFolder) &&
  326. _selectedFolders.contains(second.deviceFolder)) {
  327. return first.deviceFolder
  328. .toLowerCase()
  329. .compareTo(second.deviceFolder.toLowerCase());
  330. } else if (_selectedFolders.contains(first.deviceFolder)) {
  331. return -1;
  332. } else if (_selectedFolders.contains(second.deviceFolder)) {
  333. return 1;
  334. }
  335. return first.deviceFolder
  336. .toLowerCase()
  337. .compareTo(second.deviceFolder.toLowerCase());
  338. });
  339. }
  340. Widget _getThumbnail(File file, bool isSelected) {
  341. return ClipRRect(
  342. borderRadius: BorderRadius.circular(8),
  343. child: SizedBox(
  344. child: Stack(
  345. alignment: AlignmentDirectional.bottomEnd,
  346. children: [
  347. ThumbnailWidget(
  348. file,
  349. shouldShowSyncStatus: false,
  350. key: Key("backup_selection_widget" + file.tag()),
  351. ),
  352. Padding(
  353. padding: const EdgeInsets.all(9),
  354. child: isSelected
  355. ? Icon(
  356. Icons.local_police,
  357. color: Colors.white,
  358. )
  359. : null,
  360. ),
  361. ],
  362. ),
  363. height: 88,
  364. width: 88,
  365. ),
  366. );
  367. }
  368. }