media_test_page.dart 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585
  1. import 'dart:io';
  2. import 'package:flutter/material.dart';
  3. import 'package:sino_med_cloud/core/utils/logger.dart';
  4. import 'package:sino_med_cloud/core/utils/toast_utils.dart';
  5. import 'package:sino_med_cloud/l10n/app_localizations.dart';
  6. import 'package:sino_med_cloud/media/media.dart';
  7. /// 媒体功能测试页面
  8. class MediaTestPage extends StatefulWidget {
  9. const MediaTestPage({super.key});
  10. @override
  11. State<MediaTestPage> createState() => _MediaTestPageState();
  12. }
  13. class _MediaTestPageState extends State<MediaTestPage> {
  14. File? _selectedImage;
  15. File? _cameraImage;
  16. bool _isLoading = false;
  17. /// 获取当前选中的图片(优先使用相册/系统相机,其次使用专业相机)
  18. File? get _currentImage => _selectedImage ?? _cameraImage;
  19. @override
  20. Widget build(BuildContext context) {
  21. final l10n = AppLocalizations.of(context)!;
  22. return Scaffold(
  23. appBar: AppBar(
  24. title: Text(l10n.mediaTest),
  25. elevation: 0,
  26. ),
  27. body: SingleChildScrollView(
  28. padding: const EdgeInsets.all(16),
  29. child: Column(
  30. crossAxisAlignment: CrossAxisAlignment.stretch,
  31. children: [
  32. // 测试按钮区域
  33. _buildTestButtons(),
  34. const SizedBox(height: 24),
  35. // 图片显示区域
  36. _buildImageDisplay(),
  37. const SizedBox(height: 24),
  38. ],
  39. ),
  40. ),
  41. );
  42. }
  43. /// 构建测试按钮区域
  44. Widget _buildTestButtons() {
  45. final l10n = AppLocalizations.of(context)!;
  46. return Column(
  47. crossAxisAlignment: CrossAxisAlignment.stretch,
  48. children: [
  49. Text(
  50. l10n.quickTest,
  51. style: const TextStyle(
  52. fontSize: 18,
  53. fontWeight: FontWeight.bold,
  54. ),
  55. ),
  56. const SizedBox(height: 12),
  57. // 从相册选择
  58. ElevatedButton.icon(
  59. onPressed: _isLoading ? null : _testPickFromGallery,
  60. icon: const Icon(Icons.photo_library),
  61. label: Text(l10n.pickFromGallery),
  62. style: ElevatedButton.styleFrom(
  63. padding: const EdgeInsets.symmetric(vertical: 16),
  64. ),
  65. ),
  66. const SizedBox(height: 12),
  67. // 使用系统相机拍照
  68. ElevatedButton.icon(
  69. onPressed: _isLoading ? null : _testPickFromCamera,
  70. icon: const Icon(Icons.camera_alt),
  71. label: Text(l10n.pickFromCamera),
  72. style: ElevatedButton.styleFrom(
  73. padding: const EdgeInsets.symmetric(vertical: 16),
  74. ),
  75. ),
  76. const SizedBox(height: 24),
  77. Text(
  78. l10n.professionalCameraTest,
  79. style: const TextStyle(
  80. fontSize: 18,
  81. fontWeight: FontWeight.bold,
  82. ),
  83. ),
  84. const SizedBox(height: 12),
  85. // 专业相机拍照
  86. ElevatedButton.icon(
  87. onPressed: _isLoading ? null : _testProfessionalCamera,
  88. icon: const Icon(Icons.camera),
  89. label: Text(l10n.useProfessionalCamera),
  90. style: ElevatedButton.styleFrom(
  91. padding: const EdgeInsets.symmetric(vertical: 16),
  92. ),
  93. ),
  94. const SizedBox(height: 24),
  95. Text(
  96. l10n.imageProcessingTest,
  97. style: const TextStyle(
  98. fontSize: 18,
  99. fontWeight: FontWeight.bold,
  100. ),
  101. ),
  102. const SizedBox(height: 12),
  103. // 获取图片信息
  104. ElevatedButton.icon(
  105. onPressed: _isLoading || _currentImage == null
  106. ? null
  107. : _testGetImageInfo,
  108. icon: const Icon(Icons.info_outline),
  109. label: Text(l10n.getImageInfo),
  110. style: ElevatedButton.styleFrom(
  111. padding: const EdgeInsets.symmetric(vertical: 16),
  112. ),
  113. ),
  114. const SizedBox(height: 12),
  115. // 去除 EXIF
  116. ElevatedButton.icon(
  117. onPressed: _isLoading || _currentImage == null
  118. ? null
  119. : _testRemoveExif,
  120. icon: const Icon(Icons.security),
  121. label: Text(l10n.removeExif),
  122. style: ElevatedButton.styleFrom(
  123. padding: const EdgeInsets.symmetric(vertical: 16),
  124. ),
  125. ),
  126. const SizedBox(height: 12),
  127. // 压缩图片
  128. ElevatedButton.icon(
  129. onPressed: _isLoading || _currentImage == null
  130. ? null
  131. : _testCompressImage,
  132. icon: const Icon(Icons.compress),
  133. label: Text(l10n.compressImage),
  134. style: ElevatedButton.styleFrom(
  135. padding: const EdgeInsets.symmetric(vertical: 16),
  136. ),
  137. ),
  138. const SizedBox(height: 12),
  139. // 清除图片
  140. OutlinedButton.icon(
  141. onPressed: _isLoading ? null : _clearImages,
  142. icon: const Icon(Icons.clear),
  143. label: Text(l10n.clearAllImages),
  144. ),
  145. ],
  146. );
  147. }
  148. /// 构建图片显示区域
  149. Widget _buildImageDisplay() {
  150. final l10n = AppLocalizations.of(context)!;
  151. return Column(
  152. crossAxisAlignment: CrossAxisAlignment.start,
  153. children: [
  154. Text(
  155. l10n.selectedImage,
  156. style: const TextStyle(
  157. fontSize: 18,
  158. fontWeight: FontWeight.bold,
  159. ),
  160. ),
  161. const SizedBox(height: 12),
  162. if (_selectedImage == null && _cameraImage == null)
  163. Container(
  164. height: 200,
  165. decoration: BoxDecoration(
  166. color: Colors.grey[200],
  167. borderRadius: BorderRadius.circular(8),
  168. ),
  169. child: Center(
  170. child: Text(
  171. l10n.noImage,
  172. style: const TextStyle(
  173. color: Colors.grey,
  174. fontSize: 16,
  175. ),
  176. ),
  177. ),
  178. )
  179. else
  180. Column(
  181. children: [
  182. if (_selectedImage != null) ...[
  183. Text(
  184. l10n.galleryOrSystemCamera,
  185. style: const TextStyle(
  186. fontSize: 14,
  187. color: Colors.grey,
  188. ),
  189. ),
  190. const SizedBox(height: 8),
  191. _buildImagePreview(_selectedImage!),
  192. const SizedBox(height: 16),
  193. ],
  194. if (_cameraImage != null) ...[
  195. Text(
  196. l10n.professionalCamera,
  197. style: const TextStyle(
  198. fontSize: 14,
  199. color: Colors.grey,
  200. ),
  201. ),
  202. const SizedBox(height: 8),
  203. _buildImagePreview(_cameraImage!),
  204. ],
  205. ],
  206. ),
  207. ],
  208. );
  209. }
  210. /// 构建图片预览
  211. Widget _buildImagePreview(File imageFile) {
  212. return Container(
  213. constraints: const BoxConstraints(maxHeight: 400),
  214. decoration: BoxDecoration(
  215. borderRadius: BorderRadius.circular(8),
  216. border: Border.all(color: Colors.grey[300]!),
  217. ),
  218. child: ClipRRect(
  219. borderRadius: BorderRadius.circular(8),
  220. child: Image.file(
  221. imageFile,
  222. fit: BoxFit.contain,
  223. errorBuilder: (context, error, stackTrace) {
  224. final l10n = AppLocalizations.of(context)!;
  225. return Container(
  226. height: 200,
  227. color: Colors.grey[200],
  228. child: Center(
  229. child: Text(l10n.imageLoadFailed),
  230. ),
  231. );
  232. },
  233. ),
  234. ),
  235. );
  236. }
  237. /// 测试从相册选择
  238. Future<void> _testPickFromGallery() async {
  239. setState(() {
  240. _isLoading = true;
  241. });
  242. try {
  243. AppLogger.d('开始测试从相册选择图片...');
  244. final file = await MediaService.pickFromGallery();
  245. if (file != null) {
  246. if (!mounted) return;
  247. final l10n = AppLocalizations.of(context)!;
  248. setState(() {
  249. _selectedImage = file;
  250. _cameraImage = null;
  251. });
  252. ToastUtils.showSuccess(l10n.galleryPickSuccess);
  253. AppLogger.d('相册选择成功: ${file.path}');
  254. } else {
  255. if (!mounted) return;
  256. final l10n = AppLocalizations.of(context)!;
  257. ToastUtils.show(l10n.userCancelledSelection);
  258. }
  259. } on MediaException catch (e) {
  260. AppLogger.e('相册选择失败', e);
  261. ToastUtils.showError(e.message);
  262. } catch (e) {
  263. AppLogger.e('相册选择异常', e);
  264. ToastUtils.showError('相册选择失败: ${e.toString()}');
  265. } finally {
  266. setState(() {
  267. _isLoading = false;
  268. });
  269. }
  270. }
  271. /// 测试使用系统相机拍照
  272. Future<void> _testPickFromCamera() async {
  273. setState(() {
  274. _isLoading = true;
  275. });
  276. try {
  277. AppLogger.d('开始测试使用系统相机拍照...');
  278. final file = await MediaService.pickFromCamera();
  279. if (file != null) {
  280. if (!mounted) return;
  281. final l10n = AppLocalizations.of(context)!;
  282. setState(() {
  283. _selectedImage = file;
  284. _cameraImage = null;
  285. });
  286. ToastUtils.showSuccess(l10n.cameraPhotoSuccess);
  287. AppLogger.d('拍照成功: ${file.path}');
  288. } else {
  289. if (!mounted) return;
  290. final l10n = AppLocalizations.of(context)!;
  291. ToastUtils.show(l10n.userCancelledPhoto);
  292. }
  293. } on MediaException catch (e) {
  294. AppLogger.e('拍照失败', e);
  295. ToastUtils.showError(e.message);
  296. } catch (e) {
  297. AppLogger.e('拍照异常', e);
  298. ToastUtils.showError('拍照失败: ${e.toString()}');
  299. } finally {
  300. setState(() {
  301. _isLoading = false;
  302. });
  303. }
  304. }
  305. /// 测试专业相机
  306. Future<void> _testProfessionalCamera() async {
  307. setState(() {
  308. _isLoading = true;
  309. });
  310. CameraService? cameraService;
  311. try {
  312. AppLogger.d('开始测试专业相机...');
  313. cameraService = CameraService();
  314. // 初始化相机
  315. await cameraService.init();
  316. ToastUtils.show('相机初始化成功,请拍照');
  317. // 显示拍照对话框
  318. if (!mounted) return;
  319. final l10n = AppLocalizations.of(context)!;
  320. final shouldTakePhoto = await showDialog<bool>(
  321. context: context,
  322. builder: (context) => AlertDialog(
  323. title: Text(l10n.professionalCamera),
  324. content: Text(l10n.cameraReady),
  325. actions: [
  326. TextButton(
  327. onPressed: () => Navigator.of(context).pop(false),
  328. child: Text(l10n.cancel),
  329. ),
  330. ElevatedButton(
  331. onPressed: () => Navigator.of(context).pop(true),
  332. child: Text(l10n.takePhoto),
  333. ),
  334. ],
  335. ),
  336. );
  337. if (shouldTakePhoto == true && mounted) {
  338. final file = await cameraService.takePicture();
  339. if (file != null) {
  340. setState(() {
  341. _cameraImage = file;
  342. _selectedImage = null;
  343. });
  344. ToastUtils.showSuccess(l10n.professionalCameraSuccess);
  345. AppLogger.d('专业相机拍照成功: ${file.path}');
  346. }
  347. }
  348. } on MediaException catch (e) {
  349. AppLogger.e('专业相机失败', e);
  350. ToastUtils.showError(e.message);
  351. } catch (e) {
  352. AppLogger.e('专业相机异常', e);
  353. ToastUtils.showError('专业相机失败: ${e.toString()}');
  354. } finally {
  355. await cameraService?.dispose();
  356. setState(() {
  357. _isLoading = false;
  358. });
  359. }
  360. }
  361. /// 测试获取图片信息
  362. Future<void> _testGetImageInfo() async {
  363. final currentImage = _currentImage;
  364. if (currentImage == null) return;
  365. setState(() {
  366. _isLoading = true;
  367. });
  368. try {
  369. AppLogger.d('开始获取图片信息...');
  370. final info = await MediaService.getImageInfo(currentImage);
  371. final l10n = AppLocalizations.of(context)!;
  372. final sizeInBytes = info['size'] as int;
  373. final sizeInKB = (sizeInBytes / 1024).toStringAsFixed(2);
  374. final infoText = '${l10n.imageWidth}: ${info['width']}${l10n.pixels}\n'
  375. '${l10n.imageHeight}: ${info['height']}${l10n.pixels}\n'
  376. '${l10n.imageSize}: $sizeInKB ${l10n.kilobytes}\n'
  377. '${l10n.imageFormat}: ${info['format']}';
  378. if (!mounted) return;
  379. // 使用 Alert 弹窗显示图片信息
  380. showDialog(
  381. context: context,
  382. builder: (context) => AlertDialog(
  383. title: Text(l10n.imageInfo),
  384. content: Text(
  385. infoText,
  386. style: const TextStyle(
  387. fontSize: 14,
  388. fontFamily: 'monospace',
  389. ),
  390. ),
  391. actions: [
  392. TextButton(
  393. onPressed: () => Navigator.of(context).pop(),
  394. child: Text(l10n.confirm),
  395. ),
  396. ],
  397. ),
  398. );
  399. ToastUtils.showSuccess(l10n.getImageInfoSuccess);
  400. AppLogger.d('图片信息: $info');
  401. } on MediaException catch (e) {
  402. AppLogger.e('获取图片信息失败', e);
  403. ToastUtils.showError(e.message);
  404. } catch (e) {
  405. AppLogger.e('获取图片信息异常', e);
  406. ToastUtils.showError('获取图片信息失败: ${e.toString()}');
  407. } finally {
  408. setState(() {
  409. _isLoading = false;
  410. });
  411. }
  412. }
  413. /// 测试去除 EXIF
  414. Future<void> _testRemoveExif() async {
  415. final currentImage = _currentImage;
  416. if (currentImage == null) return;
  417. setState(() {
  418. _isLoading = true;
  419. });
  420. try {
  421. AppLogger.d('开始去除 EXIF...');
  422. final cleanedFile = await MediaService.removeExif(currentImage);
  423. if (!mounted) return;
  424. final l10n = AppLocalizations.of(context)!;
  425. setState(() {
  426. // 更新对应的图片变量
  427. if (_selectedImage != null) {
  428. _selectedImage = cleanedFile;
  429. } else if (_cameraImage != null) {
  430. _cameraImage = cleanedFile;
  431. }
  432. });
  433. ToastUtils.showSuccess(l10n.removeExifSuccess);
  434. AppLogger.d('EXIF 去除成功: ${cleanedFile.path}');
  435. } on MediaException catch (e) {
  436. AppLogger.e('去除 EXIF 失败', e);
  437. ToastUtils.showError(e.message);
  438. } catch (e) {
  439. AppLogger.e('去除 EXIF 异常', e);
  440. ToastUtils.showError('去除 EXIF 失败: ${e.toString()}');
  441. } finally {
  442. setState(() {
  443. _isLoading = false;
  444. });
  445. }
  446. }
  447. /// 测试压缩图片
  448. Future<void> _testCompressImage() async {
  449. final currentImage = _currentImage;
  450. if (currentImage == null) return;
  451. setState(() {
  452. _isLoading = true;
  453. });
  454. try {
  455. AppLogger.d('开始压缩图片...');
  456. if (!mounted) return;
  457. final l10n = AppLocalizations.of(context)!;
  458. final originalSize = await currentImage.length();
  459. final compressedFile = await MediaService.compressImage(currentImage);
  460. final compressedSize = await compressedFile.length();
  461. setState(() {
  462. // 更新对应的图片变量
  463. if (_selectedImage != null) {
  464. _selectedImage = compressedFile;
  465. } else if (_cameraImage != null) {
  466. _cameraImage = compressedFile;
  467. }
  468. });
  469. final originalSizeKB = (originalSize / 1024).toStringAsFixed(2);
  470. final compressedSizeKB = (compressedSize / 1024).toStringAsFixed(2);
  471. final compressionRatio = ((1 - compressedSize / originalSize) * 100).toStringAsFixed(1);
  472. final compressInfo = '${l10n.originalSize}: $originalSizeKB ${l10n.kilobytes}\n'
  473. '${l10n.compressedSize}: $compressedSizeKB ${l10n.kilobytes}\n'
  474. '${l10n.compressionRatio}: $compressionRatio%';
  475. if (!mounted) return;
  476. // 使用 Alert 弹窗显示压缩信息
  477. showDialog(
  478. context: context,
  479. builder: (context) => AlertDialog(
  480. title: Text(l10n.compressImageSuccess),
  481. content: Text(
  482. compressInfo,
  483. style: const TextStyle(
  484. fontSize: 14,
  485. fontFamily: 'monospace',
  486. ),
  487. ),
  488. actions: [
  489. TextButton(
  490. onPressed: () => Navigator.of(context).pop(),
  491. child: Text(l10n.confirm),
  492. ),
  493. ],
  494. ),
  495. );
  496. ToastUtils.showSuccess(l10n.compressImageSuccess);
  497. AppLogger.d('图片压缩成功: ${compressedFile.path}');
  498. } on MediaException catch (e) {
  499. AppLogger.e('压缩图片失败', e);
  500. ToastUtils.showError(e.message);
  501. } catch (e) {
  502. AppLogger.e('压缩图片异常', e);
  503. ToastUtils.showError('压缩图片失败: ${e.toString()}');
  504. } finally {
  505. setState(() {
  506. _isLoading = false;
  507. });
  508. }
  509. }
  510. /// 清除所有图片
  511. void _clearImages() {
  512. final l10n = AppLocalizations.of(context)!;
  513. setState(() {
  514. _selectedImage = null;
  515. _cameraImage = null;
  516. });
  517. ToastUtils.show(l10n.clearAllImagesSuccess);
  518. }
  519. }