backup_album_selection_page.dart 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384
  1. import 'package:auto_route/auto_route.dart';
  2. import 'package:easy_localization/easy_localization.dart';
  3. import 'package:flutter/material.dart';
  4. import 'package:flutter_hooks/flutter_hooks.dart';
  5. import 'package:fluttertoast/fluttertoast.dart';
  6. import 'package:hooks_riverpod/hooks_riverpod.dart';
  7. import 'package:immich_mobile/constants/immich_colors.dart';
  8. import 'package:immich_mobile/modules/backup/providers/backup.provider.dart';
  9. import 'package:immich_mobile/modules/backup/ui/album_info_card.dart';
  10. import 'package:immich_mobile/modules/backup/ui/album_info_list_tile.dart';
  11. import 'package:immich_mobile/shared/ui/immich_loading_indicator.dart';
  12. import 'package:immich_mobile/shared/ui/immich_toast.dart';
  13. class BackupAlbumSelectionPage extends HookConsumerWidget {
  14. const BackupAlbumSelectionPage({Key? key}) : super(key: key);
  15. @override
  16. Widget build(BuildContext context, WidgetRef ref) {
  17. // final availableAlbums = ref.watch(backupProvider).availableAlbums;
  18. final selectedBackupAlbums = ref.watch(backupProvider).selectedBackupAlbums;
  19. final excludedBackupAlbums = ref.watch(backupProvider).excludedBackupAlbums;
  20. final isDarkTheme = Theme.of(context).brightness == Brightness.dark;
  21. final allAlbums = ref.watch(backupProvider).availableAlbums;
  22. // Albums which are displayed to the user
  23. // by filtering out based on search
  24. final filteredAlbums = useState(allAlbums);
  25. final albums = filteredAlbums.value;
  26. useEffect(
  27. () {
  28. ref.watch(backupProvider.notifier).getBackupInfo();
  29. return null;
  30. },
  31. [],
  32. );
  33. buildAlbumSelectionList() {
  34. if (albums.isEmpty) {
  35. return const SliverToBoxAdapter(
  36. child: Center(
  37. child: ImmichLoadingIndicator(),
  38. ),
  39. );
  40. }
  41. return SliverPadding(
  42. padding: const EdgeInsets.symmetric(vertical: 12.0),
  43. sliver: SliverList(
  44. delegate: SliverChildBuilderDelegate(
  45. ((context, index) {
  46. var thumbnailData = albums[index].thumbnailData;
  47. return AlbumInfoListTile(
  48. imageData: thumbnailData,
  49. albumInfo: albums[index],
  50. );
  51. }),
  52. childCount: albums.length,
  53. ),
  54. ),
  55. );
  56. }
  57. buildAlbumSelectionGrid() {
  58. if (albums.isEmpty) {
  59. return const SliverToBoxAdapter(
  60. child: Center(
  61. child: ImmichLoadingIndicator(),
  62. ),
  63. );
  64. }
  65. return SliverPadding(
  66. padding: const EdgeInsets.all(12.0),
  67. sliver: SliverGrid.builder(
  68. gridDelegate: const SliverGridDelegateWithMaxCrossAxisExtent(
  69. maxCrossAxisExtent: 300,
  70. mainAxisSpacing: 12,
  71. crossAxisSpacing: 12,
  72. ),
  73. itemCount: albums.length,
  74. itemBuilder: ((context, index) {
  75. var thumbnailData = albums[index].thumbnailData;
  76. return AlbumInfoCard(
  77. imageData: thumbnailData,
  78. albumInfo: albums[index],
  79. );
  80. }),
  81. ),
  82. );
  83. }
  84. buildSelectedAlbumNameChip() {
  85. return selectedBackupAlbums.map((album) {
  86. void removeSelection() {
  87. if (ref.watch(backupProvider).selectedBackupAlbums.length == 1) {
  88. ImmichToast.show(
  89. context: context,
  90. msg: "backup_err_only_album".tr(),
  91. toastType: ToastType.error,
  92. gravity: ToastGravity.BOTTOM,
  93. );
  94. return;
  95. }
  96. ref.watch(backupProvider.notifier).removeAlbumForBackup(album);
  97. }
  98. return Padding(
  99. padding: const EdgeInsets.only(right: 8.0),
  100. child: GestureDetector(
  101. onTap: removeSelection,
  102. child: Chip(
  103. label: Text(
  104. album.name,
  105. style: TextStyle(
  106. fontSize: 10,
  107. color: isDarkTheme ? Colors.black : Colors.white,
  108. fontWeight: FontWeight.bold,
  109. ),
  110. ),
  111. backgroundColor: Theme.of(context).primaryColor,
  112. deleteIconColor: isDarkTheme ? Colors.black : Colors.white,
  113. deleteIcon: const Icon(
  114. Icons.cancel_rounded,
  115. size: 15,
  116. ),
  117. onDeleted: removeSelection,
  118. ),
  119. ),
  120. );
  121. }).toSet();
  122. }
  123. buildExcludedAlbumNameChip() {
  124. return excludedBackupAlbums.map((album) {
  125. void removeSelection() {
  126. ref
  127. .watch(backupProvider.notifier)
  128. .removeExcludedAlbumForBackup(album);
  129. }
  130. return GestureDetector(
  131. onTap: removeSelection,
  132. child: Padding(
  133. padding: const EdgeInsets.only(right: 8.0),
  134. child: Chip(
  135. label: Text(
  136. album.name,
  137. style: TextStyle(
  138. fontSize: 10,
  139. color: isDarkTheme ? Colors.black : immichBackgroundColor,
  140. fontWeight: FontWeight.bold,
  141. ),
  142. ),
  143. backgroundColor: Colors.red[300],
  144. deleteIconColor:
  145. isDarkTheme ? Colors.black : immichBackgroundColor,
  146. deleteIcon: const Icon(
  147. Icons.cancel_rounded,
  148. size: 15,
  149. ),
  150. onDeleted: removeSelection,
  151. ),
  152. ),
  153. );
  154. }).toSet();
  155. }
  156. buildSearchBar() {
  157. return Padding(
  158. padding: const EdgeInsets.only(left: 16.0, right: 16, bottom: 8.0),
  159. child: TextFormField(
  160. onChanged: (searchValue) {
  161. if (searchValue.isEmpty) {
  162. filteredAlbums.value = allAlbums;
  163. } else {
  164. filteredAlbums.value = allAlbums
  165. .where(
  166. (album) => album.name
  167. .toLowerCase()
  168. .contains(searchValue.toLowerCase()),
  169. )
  170. .toList();
  171. }
  172. },
  173. decoration: InputDecoration(
  174. contentPadding: const EdgeInsets.symmetric(
  175. horizontal: 8.0,
  176. vertical: 8.0,
  177. ),
  178. hintText: "Search",
  179. hintStyle: TextStyle(
  180. color: isDarkTheme ? Colors.white : Colors.grey,
  181. fontSize: 14.0,
  182. ),
  183. prefixIcon: const Icon(
  184. Icons.search,
  185. color: Colors.grey,
  186. ),
  187. border: OutlineInputBorder(
  188. borderRadius: BorderRadius.circular(10),
  189. borderSide: BorderSide.none,
  190. ),
  191. filled: true,
  192. fillColor: isDarkTheme ? Colors.white30 : Colors.grey[200],
  193. ),
  194. ),
  195. );
  196. }
  197. return Scaffold(
  198. appBar: AppBar(
  199. leading: IconButton(
  200. onPressed: () => AutoRouter.of(context).pop(),
  201. icon: const Icon(Icons.arrow_back_ios_rounded),
  202. ),
  203. title: const Text(
  204. "backup_album_selection_page_select_albums",
  205. style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold),
  206. ).tr(),
  207. elevation: 0,
  208. ),
  209. body: CustomScrollView(
  210. physics: const ClampingScrollPhysics(),
  211. slivers: [
  212. SliverToBoxAdapter(
  213. child: Column(
  214. crossAxisAlignment: CrossAxisAlignment.start,
  215. children: [
  216. Padding(
  217. padding: const EdgeInsets.symmetric(
  218. vertical: 8.0,
  219. horizontal: 16.0,
  220. ),
  221. child: const Text(
  222. "backup_album_selection_page_selection_info",
  223. style: TextStyle(
  224. fontWeight: FontWeight.bold,
  225. fontSize: 14,
  226. ),
  227. ).tr(),
  228. ),
  229. // Selected Album Chips
  230. Padding(
  231. padding: const EdgeInsets.symmetric(horizontal: 16.0),
  232. child: Wrap(
  233. children: [
  234. ...buildSelectedAlbumNameChip(),
  235. ...buildExcludedAlbumNameChip(),
  236. ],
  237. ),
  238. ),
  239. Padding(
  240. padding:
  241. const EdgeInsets.symmetric(horizontal: 16.0, vertical: 8),
  242. child: Card(
  243. margin: const EdgeInsets.all(0),
  244. shape: RoundedRectangleBorder(
  245. borderRadius: BorderRadius.circular(10),
  246. side: BorderSide(
  247. color: isDarkTheme
  248. ? const Color.fromARGB(255, 0, 0, 0)
  249. : const Color.fromARGB(255, 235, 235, 235),
  250. width: 1,
  251. ),
  252. ),
  253. elevation: 0,
  254. borderOnForeground: false,
  255. child: Column(
  256. children: [
  257. ListTile(
  258. visualDensity: VisualDensity.compact,
  259. title: const Text(
  260. "backup_album_selection_page_total_assets",
  261. style: TextStyle(
  262. fontWeight: FontWeight.bold,
  263. fontSize: 14,
  264. ),
  265. ).tr(),
  266. trailing: Text(
  267. ref
  268. .watch(backupProvider)
  269. .allUniqueAssets
  270. .length
  271. .toString(),
  272. style: const TextStyle(fontWeight: FontWeight.bold),
  273. ),
  274. ),
  275. ],
  276. ),
  277. ),
  278. ),
  279. ListTile(
  280. title: Text(
  281. "backup_album_selection_page_albums_device".tr(
  282. args: [
  283. ref
  284. .watch(backupProvider)
  285. .availableAlbums
  286. .length
  287. .toString(),
  288. ],
  289. ),
  290. style: const TextStyle(
  291. fontWeight: FontWeight.bold,
  292. fontSize: 14,
  293. ),
  294. ),
  295. subtitle: Padding(
  296. padding: const EdgeInsets.symmetric(vertical: 8.0),
  297. child: Text(
  298. "backup_album_selection_page_albums_tap",
  299. style: TextStyle(
  300. fontSize: 12,
  301. color: Theme.of(context).primaryColor,
  302. fontWeight: FontWeight.bold,
  303. ),
  304. ).tr(),
  305. ),
  306. trailing: IconButton(
  307. splashRadius: 16,
  308. icon: Icon(
  309. Icons.info,
  310. size: 20,
  311. color: Theme.of(context).primaryColor,
  312. ),
  313. onPressed: () {
  314. // show the dialog
  315. showDialog(
  316. context: context,
  317. builder: (BuildContext context) {
  318. return AlertDialog(
  319. shape: RoundedRectangleBorder(
  320. borderRadius: BorderRadius.circular(10),
  321. ),
  322. elevation: 5,
  323. title: Text(
  324. 'backup_album_selection_page_selection_info',
  325. style: TextStyle(
  326. fontSize: 16,
  327. fontWeight: FontWeight.bold,
  328. color: Theme.of(context).primaryColor,
  329. ),
  330. ).tr(),
  331. content: SingleChildScrollView(
  332. child: ListBody(
  333. children: [
  334. const Text(
  335. 'backup_album_selection_page_assets_scatter',
  336. style: TextStyle(
  337. fontSize: 14,
  338. ),
  339. ).tr(),
  340. ],
  341. ),
  342. ),
  343. );
  344. },
  345. );
  346. },
  347. ),
  348. ),
  349. buildSearchBar(),
  350. ],
  351. ),
  352. ),
  353. SliverLayoutBuilder(
  354. builder: (context, constraints) {
  355. if (constraints.crossAxisExtent > 600) {
  356. return buildAlbumSelectionGrid();
  357. } else {
  358. return buildAlbumSelectionList();
  359. }
  360. },
  361. ),
  362. ],
  363. ),
  364. );
  365. }
  366. }