main.dart 8.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276
  1. import 'dart:async';
  2. import 'package:flutter/material.dart';
  3. import 'package:photo_classifier/photo_classifier.dart';
  4. import 'package:permission_handler/permission_handler.dart';
  5. import 'package:photo_classifier/models.dart';
  6. void main() {
  7. runApp(const MyApp());
  8. }
  9. class MyApp extends StatefulWidget {
  10. const MyApp({super.key});
  11. @override
  12. State<MyApp> createState() => _MyAppState();
  13. }
  14. class _MyAppState extends State<MyApp> {
  15. final _photoclassifier = PhotoClassifier();
  16. bool _hasPermission = false;
  17. StreamSubscription<ClassificationEvent?>? _subscription;
  18. ClassificationProgress? _progress;
  19. ClassificationResult? _result;
  20. bool _isClassifying = false;
  21. String _errorMessage = '';
  22. @override
  23. void initState() {
  24. super.initState();
  25. _checkPermissions();
  26. }
  27. Future<void> _checkPermissions() async {
  28. final status = await Permission.photos.status;
  29. setState(() {
  30. _hasPermission = status.isGranted;
  31. if (!status.isGranted) {
  32. _errorMessage = '需要相册权限才能进行照片分类,请点击上方按钮申请权限';
  33. }
  34. });
  35. }
  36. @override
  37. Widget build(BuildContext context) {
  38. return MaterialApp(
  39. home: Scaffold(
  40. appBar: AppBar(
  41. title: const Text('照片分类插件示例'),
  42. ),
  43. body: Center(
  44. child: Column(
  45. mainAxisAlignment: MainAxisAlignment.center,
  46. children: [
  47. if (!_hasPermission)
  48. ElevatedButton(
  49. onPressed: _requestPermission,
  50. child: const Text('请求照片库权限'),
  51. ),
  52. if (!_hasPermission)
  53. Padding(
  54. padding: const EdgeInsets.only(top: 16.0),
  55. child: ElevatedButton(
  56. onPressed: () => openAppSettings(),
  57. style: ElevatedButton.styleFrom(
  58. backgroundColor: Colors.orange,
  59. ),
  60. child: const Text('打开系统设置'),
  61. ),
  62. ),
  63. if (_hasPermission && !_isClassifying)
  64. ElevatedButton(
  65. onPressed: _startClassificationWithStream,
  66. child: const Text('开始分类'),
  67. ),
  68. if (_isClassifying)
  69. ElevatedButton(
  70. onPressed: _cancelClassification,
  71. child: const Text('取消分类'),
  72. ),
  73. if (_progress != null) ...[
  74. const SizedBox(height: 20),
  75. Text('进度: ${(_progress!.rate * 100).toStringAsFixed(1)}%',
  76. style: const TextStyle(fontSize: 18)),
  77. const SizedBox(height: 10),
  78. LinearProgressIndicator(value: _progress!.rate),
  79. Text('已处理: ${_progress!.currentBatch}/${_progress!.totalBatches}'),
  80. ],
  81. if (_errorMessage.isNotEmpty) ...[
  82. const SizedBox(height: 20),
  83. Container(
  84. padding: const EdgeInsets.all(12),
  85. decoration: BoxDecoration(
  86. color: Colors.red.withOpacity(0.1),
  87. borderRadius: BorderRadius.circular(8),
  88. border: Border.all(color: Colors.red.withOpacity(0.5)),
  89. ),
  90. child: Column(
  91. children: [
  92. const Icon(Icons.error_outline, color: Colors.red, size: 28),
  93. const SizedBox(height: 8),
  94. Text(
  95. _errorMessage,
  96. style: const TextStyle(color: Colors.red),
  97. textAlign: TextAlign.center,
  98. ),
  99. ],
  100. ),
  101. ),
  102. ],
  103. if (_result != null && _progress?.isCompleted == true) ...[
  104. const SizedBox(height: 20),
  105. const Text('分类结果:', style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold)),
  106. _buildResultSummary(),
  107. ],
  108. if (_progress?.isCompleted == true)
  109. ElevatedButton(
  110. onPressed: _resetAll,
  111. style: ElevatedButton.styleFrom(
  112. backgroundColor: Colors.blueGrey,
  113. ),
  114. child: const Text('重置'),
  115. ),
  116. ],
  117. ),
  118. ),
  119. ),
  120. );
  121. }
  122. Widget _buildResultSummary() {
  123. if (_result == null) return const Text('无结果');
  124. return Expanded(
  125. child: ListView(
  126. children: [
  127. _buildResultItem('相似图片组', _result?.similarGroups, isGroup: true),
  128. _buildResultItem('人物照片', _result?.peopleGroups),
  129. _buildResultItem('屏幕截图', _result?.screenshotGroups),
  130. _buildResultItem('模糊图片', _result?.blurryGroups),
  131. ],
  132. ),
  133. );
  134. }
  135. Widget _buildResultItem(String title, dynamic items, {bool isGroup = false}) {
  136. if (items == null) return const SizedBox.shrink();
  137. final count = isGroup ? (items as List).length : (items as List).length;
  138. final itemsText = isGroup ? '$count 组' : '$count 张';
  139. return Padding(
  140. padding: const EdgeInsets.symmetric(vertical: 8.0),
  141. child: Column(
  142. crossAxisAlignment: CrossAxisAlignment.start,
  143. children: [
  144. Text('$title: $itemsText',
  145. style: const TextStyle(fontWeight: FontWeight.bold)),
  146. // 这里可以添加图片预览
  147. ],
  148. ),
  149. );
  150. }
  151. Future<void> _requestPermission() async {
  152. try {
  153. // 使用permission_handler请求权限
  154. final status = await Permission.photos.request();
  155. final granted = status.isGranted;
  156. setState(() {
  157. _hasPermission = granted;
  158. if (!granted) {
  159. _errorMessage = '需要相册权限才能进行照片分类';
  160. } else {
  161. _errorMessage = '';
  162. }
  163. });
  164. // 如果被拒绝且不能再次请求,提示用户前往设置
  165. if (status.isPermanentlyDenied || status.isDenied) {
  166. setState(() {
  167. _errorMessage = '权限被拒绝,请点击下方按钮前往设置手动开启相册权限';
  168. });
  169. }
  170. } catch (e) {
  171. setState(() {
  172. _errorMessage = '请求权限失败: $e';
  173. });
  174. }
  175. }
  176. Future<void> _startClassificationWithStream() async {
  177. setState(() {
  178. _isClassifying = true;
  179. _errorMessage = '';
  180. _progress = null;
  181. _result = null;
  182. });
  183. try {
  184. // 先配置分类器
  185. await _photoclassifier.configureClassifier(
  186. batchSize: 200,
  187. maxConcurrentProcessing: 4,
  188. similarityThreshold: 0.75,
  189. );
  190. // 设置流监听
  191. _subscription = _photoclassifier
  192. .startClassificationStream()
  193. .listen(
  194. (event) {
  195. if (event == null) return;
  196. setState(() {
  197. _progress = event.progress;
  198. print("批次: ${event.progress?.currentBatch}/${event.progress?.totalBatches}, 进度: ${event.progress?.batchDuration}");
  199. print("${event.result?.screenshotGroups?.length}");
  200. if (event.progress?.isCompleted == true) {
  201. _result = event.result;
  202. _isClassifying = false;
  203. _subscription?.cancel();
  204. // 取消订阅
  205. _subscription = null;
  206. }
  207. });
  208. },
  209. onError: (error) {
  210. setState(() {
  211. _errorMessage = '分类过程中出错: $error';
  212. _isClassifying = false;
  213. });
  214. },
  215. onDone: () {
  216. if (_progress?.isCompleted != true) {
  217. setState(() {
  218. _errorMessage = '分类过程意外结束';
  219. _isClassifying = false;
  220. });
  221. }
  222. },
  223. );
  224. } catch (e) {
  225. setState(() {
  226. _errorMessage = '启动分类失败: $e';
  227. _isClassifying = false;
  228. });
  229. }
  230. }
  231. void _cancelClassification() {
  232. _subscription?.cancel();
  233. _subscription = null;
  234. setState(() {
  235. _isClassifying = false;
  236. });
  237. _photoclassifier.resetClassifier();
  238. }
  239. void _resetAll() {
  240. setState(() {
  241. _progress = null;
  242. _result = null;
  243. _errorMessage = '';
  244. _isClassifying = false;
  245. });
  246. // _photoclassifier.resetClassifier();
  247. }
  248. @override
  249. void dispose() {
  250. _subscription?.cancel();
  251. super.dispose();
  252. }
  253. }