backup_folder_selection_page.dart 15 KB

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