similar_photo_view.dart 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509
  1. import 'dart:io';
  2. import 'dart:math';
  3. import 'package:clean/base/base_page.dart';
  4. import 'package:clean/data/bean/photos_type.dart';
  5. import 'package:clean/module/similar_photo/similar_photo_controller.dart';
  6. import 'package:clean/resource/assets.gen.dart';
  7. import 'package:clean/router/app_pages.dart';
  8. import 'package:flutter/material.dart';
  9. import 'package:flutter_screenutil/flutter_screenutil.dart';
  10. import 'package:get/get.dart';
  11. import 'package:wechat_assets_picker/wechat_assets_picker.dart';
  12. import '../../utils/styles.dart';
  13. class SimilarPhotoPage extends BasePage<SimilarPhotoController> {
  14. const SimilarPhotoPage({super.key});
  15. static void start() {
  16. Get.toNamed(RoutePath.similarPhoto);
  17. }
  18. @override
  19. bool statusBarDarkFont() => false;
  20. @override
  21. bool immersive() => true;
  22. @override
  23. Widget buildBody(BuildContext context) {
  24. return Stack(children: [
  25. Container(
  26. child: SafeArea(child: Obx(() {
  27. if (controller.photoGroups.isEmpty) {
  28. return _noNoPicturesCard();
  29. }
  30. return Column(
  31. children: [
  32. _titleCard(),
  33. Flexible(
  34. child: Obx(() {
  35. return ListView(
  36. padding: EdgeInsets.symmetric(horizontal: 16.w),
  37. children: [
  38. ...controller.photoGroups.map((group) => Column(
  39. children: [
  40. _buildPhotoGroup(
  41. imagesList: group.images,
  42. imageCount: group.images.length,
  43. selectCount:group.selectedImages
  44. .asMap()
  45. .entries
  46. .where((entry) => entry.value && entry.key < group.images.length)
  47. .length,
  48. ),
  49. SizedBox(height: 15.h),
  50. ],
  51. ))
  52. ],
  53. );
  54. }),
  55. ),
  56. _bottomBarCard(),
  57. ],
  58. );
  59. })),
  60. ),
  61. IgnorePointer(
  62. child: Assets.images.bgHome.image(
  63. width: 360.w,
  64. ),
  65. ),
  66. ]);
  67. }
  68. Widget _titleCard() {
  69. return Container(
  70. alignment: Alignment.centerLeft,
  71. padding: EdgeInsets.only(left: 16.w, top: 14.h),
  72. child: Column(
  73. crossAxisAlignment: CrossAxisAlignment.start,
  74. children: [
  75. GestureDetector(
  76. onTap: () => Get.back(),
  77. child: Assets.images.iconBackArrow.image(
  78. width: 28.w,
  79. height: 28.h,
  80. ),
  81. ),
  82. (controller.photoGroups.isEmpty)
  83. ? SizedBox()
  84. : Container(
  85. child: Column(
  86. children: [
  87. SizedBox(height: 12.h),
  88. Text(
  89. 'Similar Photos',
  90. style: TextStyle(
  91. color: Colors.white,
  92. fontSize: 24.sp,
  93. fontWeight: FontWeight.w700,
  94. ),
  95. ),
  96. SizedBox(height: 20.h),
  97. ],
  98. ),
  99. )
  100. ],
  101. ),
  102. );
  103. }
  104. Widget _bottomBarCard() {
  105. return GestureDetector(
  106. onTap: controller.clickJumpSelect,
  107. child: Container(
  108. width: 360.w,
  109. height: 81.h,
  110. padding: EdgeInsets.symmetric(horizontal: 16.w),
  111. decoration: ShapeDecoration(
  112. color: Color(0xFF23232A),
  113. shape: RoundedRectangleBorder(
  114. side:
  115. BorderSide(width: 1.w, color: Colors.white.withOpacity(0.1)),
  116. borderRadius: BorderRadius.only(
  117. topLeft: Radius.circular(14.r),
  118. topRight: Radius.circular(14.r),
  119. ),
  120. ),
  121. ),
  122. child: Row(
  123. mainAxisAlignment: MainAxisAlignment.spaceBetween,
  124. children: [
  125. Obx(() {
  126. return Text(
  127. '${controller.selectedFileCount.value} files selected (${controller.selectedFilesSizeString})',
  128. textAlign: TextAlign.center,
  129. style: TextStyle(
  130. color: Colors.white.withValues(alpha: 0.9),
  131. fontSize: 13.sp,
  132. fontWeight: FontWeight.w500,
  133. ),
  134. );
  135. }),
  136. GestureDetector(
  137. onTap: () => controller.clickDelete(),
  138. child: Container(
  139. width: 108.w,
  140. height: 38.h,
  141. decoration: ShapeDecoration(
  142. color: Color(0xFF0279FB),
  143. shape: RoundedRectangleBorder(
  144. borderRadius: BorderRadius.circular(10.r),
  145. ),
  146. ),
  147. child: Row(
  148. mainAxisAlignment: MainAxisAlignment.spaceEvenly,
  149. children: [
  150. Text(
  151. 'Delete',
  152. textAlign: TextAlign.center,
  153. style: TextStyle(
  154. color: Colors.white,
  155. fontSize: 16.sp,
  156. fontWeight: FontWeight.w500,
  157. ),
  158. ),
  159. Assets.images.iconDelete.image(
  160. width: 18.w,
  161. height: 18.h,
  162. ),
  163. ],
  164. ),
  165. ),
  166. )
  167. ],
  168. ),
  169. ));
  170. }
  171. Widget _buildPhotoGroup({
  172. required List<AssetEntity> imagesList,
  173. required int imageCount,
  174. required int selectCount,
  175. }) {
  176. return Container(
  177. padding: EdgeInsets.symmetric(horizontal: 12.w),
  178. margin: EdgeInsets.only(top: 14.h),
  179. width: 328.w,
  180. height: 258.h,
  181. decoration: ShapeDecoration(
  182. color: Colors.white.withValues(alpha: 0.12),
  183. shape: RoundedRectangleBorder(
  184. borderRadius: BorderRadius.circular(14.sp),
  185. ),
  186. ),
  187. child: Column(
  188. mainAxisAlignment: MainAxisAlignment.spaceEvenly,
  189. children: [
  190. Row(
  191. mainAxisAlignment: MainAxisAlignment.spaceBetween,
  192. children: [
  193. Row(
  194. children: [
  195. Text(
  196. "photos $imageCount",
  197. textAlign: TextAlign.center,
  198. style: TextStyle(
  199. color: Colors.white,
  200. fontSize: 14.sp,
  201. fontWeight: FontWeight.w500,
  202. ),
  203. ),
  204. ],
  205. ),
  206. GestureDetector(
  207. onTap: () => controller.toggleGroupSelection(imagesList),
  208. child: Obx(() => Text(
  209. controller.photoGroups
  210. .firstWhere((g) =>
  211. g.images.toSet().containsAll(imagesList))
  212. .isSelected
  213. .value
  214. ? 'Deselect All'
  215. : 'Select All',
  216. style: TextStyle(
  217. color: Colors.white.withValues(alpha: 0.7),
  218. fontSize: 14.sp,
  219. fontWeight: FontWeight.w400,
  220. ),
  221. )),
  222. ),
  223. ],
  224. ),
  225. SizedBox(
  226. height: 148.h,
  227. child: ListView(
  228. scrollDirection: Axis.horizontal,
  229. physics: BouncingScrollPhysics(),
  230. children: [
  231. // 第一张大图
  232. if (imageCount > 0)
  233. GestureDetector(
  234. onTap: () => controller.clickImage(
  235. imagesList, 0, PhotosType.similarPhotos),
  236. child: SizedBox(
  237. width: 148.w,
  238. height: 148.h,
  239. child: Obx(() {
  240. final group = controller.photoGroups.firstWhere(
  241. (g) => g.images.toSet().containsAll(imagesList));
  242. return Stack(
  243. children: [
  244. Container(
  245. decoration: ShapeDecoration(
  246. color: Colors.white.withValues(alpha: 0.12),
  247. shape: RoundedRectangleBorder(
  248. borderRadius: BorderRadius.circular(8.r),
  249. ),
  250. ),
  251. child: ClipRRect(
  252. borderRadius: BorderRadius.circular(8.r),
  253. child: AssetEntityImage(
  254. width: 148.w,
  255. height: 148.h,
  256. group.images[0],
  257. fit: BoxFit.cover,
  258. thumbnailSize:
  259. const ThumbnailSize.square(300),
  260. isOriginal: false,
  261. frameBuilder: Styles.customFrameBuilder(
  262. width: 120.w,
  263. height: 120.w,
  264. ),
  265. )),
  266. ),
  267. Positioned(
  268. left: 8.w,
  269. top: 8.h,
  270. child: Container(
  271. width: 108.w,
  272. height: 26.h,
  273. padding: EdgeInsets.symmetric(
  274. horizontal: 8.w,
  275. vertical: 4.h,
  276. ),
  277. decoration: ShapeDecoration(
  278. color: Colors.black.withValues(alpha: 0.74),
  279. shape: RoundedRectangleBorder(
  280. borderRadius:
  281. BorderRadius.circular(14.21.r),
  282. ),
  283. ),
  284. child: Row(
  285. mainAxisAlignment:
  286. MainAxisAlignment.spaceAround,
  287. children: [
  288. Assets.images.iconSimilarBest.image(
  289. width: 11.37.w,
  290. height: 11.37.h,
  291. ),
  292. Text(
  293. 'Best result',
  294. textAlign: TextAlign.center,
  295. style: TextStyle(
  296. color: Colors.white,
  297. fontSize: 13.sp,
  298. fontWeight: FontWeight.w400,
  299. ),
  300. ),
  301. ],
  302. ),
  303. ),
  304. ),
  305. Positioned(
  306. right: 4.w,
  307. bottom: 4.h,
  308. child: GestureDetector(
  309. onTap: () => controller.toggleImageSelection(
  310. imagesList, 0),
  311. child: Container(
  312. child: group.selectedImages[0]
  313. ? Center(
  314. child:
  315. Assets.images.iconSelected.image(
  316. width: 20.w,
  317. height: 20.h,
  318. ),
  319. )
  320. : Center(
  321. child: Assets.images.iconUnselected
  322. .image(
  323. width: 20.w,
  324. height: 20.h,
  325. ),
  326. ),
  327. ),
  328. ),
  329. ),
  330. ],
  331. );
  332. }),
  333. ),
  334. ),
  335. // 其他图片2x2网格
  336. if (imageCount > 1)
  337. ...List.generate(((imageCount - 1) / 4).ceil(), (gridIndex) {
  338. return Container(
  339. margin: EdgeInsets.only(left: 8.w),
  340. width: 142.w,
  341. child: GridView.count(
  342. physics: NeverScrollableScrollPhysics(),
  343. crossAxisCount: 2,
  344. mainAxisSpacing: 8.h,
  345. crossAxisSpacing: 8.w,
  346. children: List.generate(
  347. min(4, imageCount - 1 - gridIndex * 4),
  348. (index) {
  349. final realIndex = gridIndex * 4 + index + 1;
  350. return GestureDetector(
  351. onTap: () => controller.clickImage(imagesList,
  352. realIndex, PhotosType.similarPhotos),
  353. child: Obx(() {
  354. final group = controller.photoGroups.firstWhere(
  355. (g) => g.images
  356. .toSet()
  357. .containsAll(imagesList));
  358. return Container(
  359. decoration: ShapeDecoration(
  360. color: Colors.white.withValues(alpha: 0.12),
  361. shape: RoundedRectangleBorder(
  362. borderRadius: BorderRadius.circular(8.r),
  363. ),
  364. ),
  365. child: Stack(
  366. children: [
  367. ClipRRect(
  368. borderRadius:
  369. BorderRadius.circular(8.r),
  370. child: AssetEntityImage(
  371. group.images[realIndex],
  372. width: 142.w,
  373. frameBuilder: Styles.customFrameBuilder(
  374. width: 120.w,
  375. height: 120.w,
  376. ),
  377. thumbnailSize:
  378. const ThumbnailSize.square(300),
  379. isOriginal: false,
  380. fit: BoxFit.cover,
  381. ),
  382. ),
  383. Positioned(
  384. right: 4.w,
  385. bottom: 4.h,
  386. child: Obx(() {
  387. final isSelected =
  388. group.selectedImages[realIndex];
  389. return GestureDetector(
  390. onTap: () =>
  391. controller.toggleImageSelection(
  392. imagesList, realIndex),
  393. child: Container(
  394. child: isSelected
  395. ? Center(
  396. child: Assets
  397. .images.iconSelected
  398. .image(
  399. width: 20.w,
  400. height: 20.h,
  401. ),
  402. )
  403. : Center(
  404. child: Assets
  405. .images.iconUnselected
  406. .image(
  407. width: 20.w,
  408. height: 20.h,
  409. ),
  410. ),
  411. ),
  412. );
  413. }),
  414. ),
  415. ],
  416. ),
  417. );
  418. }),
  419. );
  420. },
  421. ),
  422. ),
  423. );
  424. }),
  425. ],
  426. ),
  427. ),
  428. GestureDetector(
  429. onTap: () => controller.clickSingleGroupDelete(imagesList),
  430. child: Container(
  431. width: 162.w,
  432. height: 38.h,
  433. decoration: ShapeDecoration(
  434. color: Color(0xFF0279FB),
  435. shape: RoundedRectangleBorder(
  436. borderRadius: BorderRadius.circular(10.r),
  437. ),
  438. ),
  439. child: Center(
  440. child: Text(
  441. 'Move $selectCount to trash',
  442. style: TextStyle(
  443. color: Colors.white,
  444. fontSize: 16.sp,
  445. fontWeight: FontWeight.w500,
  446. ),
  447. ),
  448. ),
  449. ),
  450. )
  451. ],
  452. ),
  453. );
  454. }
  455. Widget _noNoPicturesCard() {
  456. return Column(
  457. // mainAxisAlignment: MainAxisAlignment.start,
  458. children: [
  459. _titleCard(),
  460. Spacer(flex: 1),
  461. Column(
  462. crossAxisAlignment: CrossAxisAlignment.center,
  463. children: [
  464. Container(
  465. width: 70.w,
  466. height: 70.h,
  467. clipBehavior: Clip.antiAlias,
  468. decoration: BoxDecoration(),
  469. child: Assets.images.iconNoPictures.image(),
  470. ),
  471. SizedBox(height: 22.h),
  472. Text(
  473. 'No pictures found',
  474. textAlign: TextAlign.center,
  475. style: TextStyle(
  476. color: Colors.white,
  477. fontSize: 20.sp,
  478. fontWeight: FontWeight.w700,
  479. ),
  480. ),
  481. SizedBox(height: 12.h),
  482. Text(
  483. 'No pictures available at the moment',
  484. textAlign: TextAlign.center,
  485. style: TextStyle(
  486. color: Colors.white.withValues(alpha: 0.6),
  487. fontSize: 14.sp,
  488. fontWeight: FontWeight.w400,
  489. ),
  490. ),
  491. ],
  492. ),
  493. Spacer(
  494. flex: 3,
  495. ),
  496. ],
  497. );
  498. }
  499. }