backup_folder_selection_page.dart 15 KB

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