backup_folder_selection_page.dart 15 KB

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