backup_folder_selection_page.dart 15 KB

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