photo_scan_handler.dart 9.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309
  1. import 'dart:async';
  2. import 'dart:io';
  3. import 'package:classify_photo/classify_photo.dart';
  4. import 'package:clean/widget/multi_segment_circle_indicator.dart';
  5. import 'package:flutter/Material.dart';
  6. import 'package:get/get.dart';
  7. import 'package:permission_handler/permission_handler.dart';
  8. import 'package:photo_classifier/models.dart';
  9. import 'package:photo_classifier/photo_classifier.dart';
  10. import 'package:wechat_assets_picker/wechat_assets_picker.dart';
  11. import '../../module/image_picker/image_picker_util.dart';
  12. import '../utils/file_size_calculator_util.dart';
  13. class PhotoScanHandler {
  14. final classifier = PhotoClassifier();
  15. var hasPermission = false.obs;
  16. var isClassifying = false.obs;
  17. var errorMessage = ''.obs;
  18. static var progress = Rxn<ClassificationProgress>();
  19. StreamSubscription<ClassificationEvent?>? _subscription;
  20. static RxBool isSimilarScanned = false.obs;
  21. static RxBool isPeopleScanned = false.obs;
  22. static RxBool isScreenShotScanned = false.obs;
  23. static RxBool isBlurryScanned = false.obs;
  24. // 人物图片
  25. static RxList<AssetEntity> peoplePhotos = <AssetEntity>[].obs;
  26. // 地点图片
  27. static Rx<AssetEntity?> locationPhoto = Rx<AssetEntity?>(null);
  28. // 截图照片
  29. static Rx<AssetEntity?> screenshotPhoto = Rx<AssetEntity?>(null);
  30. // 模糊照片
  31. static Rx<AssetEntity?> blurryPhoto = Rx<AssetEntity?>(null);
  32. // 相似照片
  33. static RxList<AssetEntity> similarPhotos = <AssetEntity>[].obs;
  34. static Rx<double> totalSpace = 0.0.obs;
  35. static Rx<double> usedSpace = 0.0.obs;
  36. static Rx<double> photoSpace = 0.0.obs;
  37. static Rx<double> freeSpace = 0.0.obs;
  38. static Rx<String> totalSpaceStr = "0.0 GB".obs;
  39. static Rx<String> usedSpaceStr = "0.0 GB".obs;
  40. static Rx<String> photoSpaceStr = "0.0 GB".obs;
  41. static Rx<String> freeSpaceStr = "0.0 GB".obs;
  42. // 计算已用存储百分比
  43. static double get usedSpacePercentage =>
  44. (usedSpace.value / totalSpace.value) * 100;
  45. // 计算照片占用存储百分比
  46. static double get photoSpacePercentage =>
  47. (photoSpace.value / totalSpace.value) * 100;
  48. // 计算可用存储百分比
  49. static double get freeSpacePercentage =>
  50. (freeSpace.value / totalSpace.value) * 100;
  51. static List<PieData> get pieDataList => [
  52. PieData("PhotoSpace", photoSpacePercentage, Colors.blue),
  53. PieData("OtherUsed", usedSpacePercentage - photoSpacePercentage,
  54. Colors.red),
  55. PieData("totalSpace", totalSpace.value, Colors.grey.withOpacity(0.1)),
  56. ];
  57. // 存储是否扫描完成
  58. static RxBool isStorageScanned = false.obs;
  59. Future<void> getStorageInfo() async {
  60. final classifyPhoto = ClassifyPhoto();
  61. try {
  62. final storageInfo = await classifyPhoto.getStorageInfo();
  63. // 转换为 GB
  64. final totalSpaceGB = storageInfo['totalSpace']! / (1000 * 1000 * 1000);
  65. final freeSpaceGB = storageInfo['freeSpace']! / (1024 * 1024 * 1024);
  66. final usedSpaceGB = storageInfo['usedSpace']! / (1024 * 1024 * 1024);
  67. final photoSpaceGB = storageInfo['photoSpace']! / (1024 * 1024 * 1024);
  68. totalSpaceStr.value = ImagePickerUtil.formatFileSize(
  69. storageInfo['totalSpace']!,
  70. decimals: 1);
  71. freeSpaceStr.value = ImagePickerUtil.formatFileSize(
  72. storageInfo['freeSpace']!,
  73. decimals: 1);
  74. usedSpaceStr.value = ImagePickerUtil.formatFileSize(
  75. storageInfo['usedSpace']!,
  76. decimals: 1);
  77. photoSpaceStr.value = ImagePickerUtil.formatFileSize(
  78. storageInfo['photoSpace']!,
  79. decimals: 1);
  80. totalSpace.value = totalSpaceGB.round().toDouble();
  81. freeSpace.value = freeSpaceGB;
  82. usedSpace.value = usedSpaceGB;
  83. photoSpace.value = photoSpaceGB;
  84. print('总容量: $totalSpaceStr');
  85. print('可用空间: $freeSpaceStr');
  86. print('已用空间: $usedSpaceStr');
  87. print('照片占用: $photoSpaceStr');
  88. isStorageScanned.value = true;
  89. } catch (e) {
  90. print('获取存储信息失败: $e');
  91. }
  92. }
  93. /// 执行所有的照片处理操作
  94. Future<void> handleAllPhotos() async {
  95. var currentStatus = await Permission.photos.status;
  96. if (Platform.isAndroid) {
  97. currentStatus = await Permission.storage.status;
  98. }
  99. if (currentStatus.isGranted) {
  100. if (Platform.isAndroid) {
  101. await handleAndroidPhotos();
  102. }
  103. // 已有完全权限,直接扫描
  104. PhotoManager.clearFileCache();
  105. startClassification();
  106. } else if (currentStatus.isLimited) {
  107. // 已有有限权限,显示自定义弹窗
  108. PhotoManager.clearFileCache();
  109. startClassification();
  110. } else {
  111. // 未授权,请求权限
  112. var result = await Permission.photos.request();
  113. if (Platform.isAndroid) {
  114. result = await Permission.storage.request();
  115. }
  116. if (result.isGranted || result.isLimited) {
  117. PhotoManager.clearFileCache();
  118. getStorageInfo();
  119. startClassification();
  120. if (Platform.isAndroid) {
  121. await handleAndroidPhotos();
  122. }
  123. } else {
  124. isSimilarScanned.value = true;
  125. isPeopleScanned.value = true;
  126. isBlurryScanned.value = true;
  127. isScreenShotScanned.value = true;
  128. }
  129. }
  130. }
  131. /// Android平台处理方式
  132. Future<void> handleAndroidPhotos() async {
  133. final List<AssetEntity> result = await ImagePickerUtil.loadAssets();
  134. ImagePickerUtil.peoplePhotos.value = result ?? [];
  135. ImagePickerUtil.locationPhotos['location'] = result ?? [];
  136. ImagePickerUtil.screenshotPhotos.value = result ?? [];
  137. ImagePickerUtil.similarPhotos.add(result ?? []);
  138. ImagePickerUtil.blurryPhotos.value = result ?? [];
  139. FileSizeCalculatorUtil.calculateTotalSize(
  140. assetIds: ImagePickerUtil.blurryPhotos.map((e) => e.id).toSet(),
  141. updateValue: (double totalSize) {
  142. ImagePickerUtil.blurrySize.value = totalSize.toInt()*1024;
  143. });
  144. // result = await ImagePickerUtil.loadAssetsPaged(page: 1, pageSize: 9);
  145. // ImagePickerUtil.similarPhotos.add(result ?? []);
  146. // result = await ImagePickerUtil.loadAssetsPaged(page: 2, pageSize: 9);
  147. // ImagePickerUtil.similarPhotos.add(result ?? []);
  148. print("handleAndroidPhotos $result");
  149. print(
  150. "ImagePickerUtil.peoplePhotos.value ${ImagePickerUtil.peoplePhotos.length}");
  151. isSimilarScanned.value = true;
  152. isPeopleScanned.value = true;
  153. isBlurryScanned.value = true;
  154. isScreenShotScanned.value = true;
  155. similarPhotos.clear();
  156. if (ImagePickerUtil.similarPhotos.isNotEmpty) {
  157. for (var group in ImagePickerUtil.similarPhotos) {
  158. print(
  159. " ImagePickerUtil.similarPhotos ${ImagePickerUtil.similarPhotos.length}");
  160. for (var asset in group) {
  161. similarPhotos.add(asset);
  162. if (similarPhotos.length == 4) {
  163. break;
  164. }
  165. }
  166. }
  167. }
  168. peoplePhotos.clear();
  169. if (ImagePickerUtil.peoplePhotos.isNotEmpty) {
  170. for (var personPhoto in ImagePickerUtil.peoplePhotos) {
  171. peoplePhotos.add(personPhoto);
  172. if (peoplePhotos.length == 2) {
  173. break;
  174. }
  175. }
  176. }
  177. if (ImagePickerUtil.blurryPhotos.isNotEmpty) {
  178. var asset = ImagePickerUtil.blurryPhotos.first;
  179. blurryPhoto.value = asset;
  180. }
  181. if (ImagePickerUtil.screenshotPhotos.isNotEmpty) {
  182. var asset = ImagePickerUtil.screenshotPhotos.first;
  183. screenshotPhoto.value = asset;
  184. }
  185. }
  186. Future<void> startClassification() async {
  187. isClassifying.value = true;
  188. errorMessage.value = '';
  189. progress.value = null;
  190. ImagePickerUtil.similarResult.clear();
  191. ImagePickerUtil.peopleResult.clear();
  192. ImagePickerUtil.screenshotResult.clear();
  193. ImagePickerUtil.blurryResult.clear();
  194. try {
  195. await classifier.configureClassifier(
  196. batchSize: 200,
  197. maxConcurrentProcessing: 4,
  198. similarityThreshold: 0.75,
  199. );
  200. _subscription = classifier.startClassificationStream().listen(
  201. (event) async {
  202. if (event == null) return;
  203. progress.value = event.progress;
  204. final result = event.result;
  205. if (result != null) {
  206. await ImagePickerUtil.loadClassifiedPhotoData(result);
  207. }
  208. if (event.progress?.isCompleted == true) {
  209. isClassifying.value = false;
  210. _subscription?.cancel();
  211. _subscription = null;
  212. completeClassification();
  213. }
  214. },
  215. onError: (error) {
  216. errorMessage.value = '分类过程中出错: $error';
  217. isClassifying.value = false;
  218. },
  219. onDone: () {
  220. if (progress.value?.isCompleted != true) {
  221. errorMessage.value = '分类过程意外结束';
  222. isClassifying.value = false;
  223. }
  224. },
  225. );
  226. } catch (e) {
  227. errorMessage.value = '启动分类失败: $e';
  228. isClassifying.value = false;
  229. }
  230. }
  231. void cancelClassification() {
  232. _subscription?.cancel();
  233. _subscription = null;
  234. isClassifying.value = false;
  235. classifier.resetClassifier();
  236. }
  237. void resetAll() {
  238. progress.value = null;
  239. ImagePickerUtil.similarResult.clear();
  240. ImagePickerUtil.peopleResult.clear();
  241. ImagePickerUtil.screenshotResult.clear();
  242. ImagePickerUtil.blurryResult.clear();
  243. errorMessage.value = '';
  244. isClassifying.value = false;
  245. }
  246. void completeClassification() {
  247. isSimilarScanned.value = true;
  248. isPeopleScanned.value = true;
  249. isBlurryScanned.value = true;
  250. isScreenShotScanned.value = true;
  251. }
  252. }