photo_scan_handler.dart 11 KB

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