media_test_page.dart 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584
  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. return Container(
  225. height: 200,
  226. color: Colors.grey[200],
  227. child: const Center(
  228. child: Text('图片加载失败'),
  229. ),
  230. );
  231. },
  232. ),
  233. ),
  234. );
  235. }
  236. /// 测试从相册选择
  237. Future<void> _testPickFromGallery() async {
  238. setState(() {
  239. _isLoading = true;
  240. });
  241. try {
  242. AppLogger.d('开始测试从相册选择图片...');
  243. final file = await MediaService.pickFromGallery();
  244. if (file != null) {
  245. if (!mounted) return;
  246. final l10n = AppLocalizations.of(context)!;
  247. setState(() {
  248. _selectedImage = file;
  249. _cameraImage = null;
  250. });
  251. ToastUtils.showSuccess(l10n.galleryPickSuccess);
  252. AppLogger.d('相册选择成功: ${file.path}');
  253. } else {
  254. if (!mounted) return;
  255. final l10n = AppLocalizations.of(context)!;
  256. ToastUtils.show(l10n.userCancelledSelection);
  257. }
  258. } on MediaException catch (e) {
  259. AppLogger.e('相册选择失败', e);
  260. ToastUtils.showError(e.message);
  261. } catch (e) {
  262. AppLogger.e('相册选择异常', e);
  263. ToastUtils.showError('相册选择失败: ${e.toString()}');
  264. } finally {
  265. setState(() {
  266. _isLoading = false;
  267. });
  268. }
  269. }
  270. /// 测试使用系统相机拍照
  271. Future<void> _testPickFromCamera() async {
  272. setState(() {
  273. _isLoading = true;
  274. });
  275. try {
  276. AppLogger.d('开始测试使用系统相机拍照...');
  277. final file = await MediaService.pickFromCamera();
  278. if (file != null) {
  279. if (!mounted) return;
  280. final l10n = AppLocalizations.of(context)!;
  281. setState(() {
  282. _selectedImage = file;
  283. _cameraImage = null;
  284. });
  285. ToastUtils.showSuccess(l10n.cameraPhotoSuccess);
  286. AppLogger.d('拍照成功: ${file.path}');
  287. } else {
  288. if (!mounted) return;
  289. final l10n = AppLocalizations.of(context)!;
  290. ToastUtils.show(l10n.userCancelledPhoto);
  291. }
  292. } on MediaException catch (e) {
  293. AppLogger.e('拍照失败', e);
  294. ToastUtils.showError(e.message);
  295. } catch (e) {
  296. AppLogger.e('拍照异常', e);
  297. ToastUtils.showError('拍照失败: ${e.toString()}');
  298. } finally {
  299. setState(() {
  300. _isLoading = false;
  301. });
  302. }
  303. }
  304. /// 测试专业相机
  305. Future<void> _testProfessionalCamera() async {
  306. setState(() {
  307. _isLoading = true;
  308. });
  309. CameraService? cameraService;
  310. try {
  311. AppLogger.d('开始测试专业相机...');
  312. cameraService = CameraService();
  313. // 初始化相机
  314. await cameraService.init();
  315. ToastUtils.show('相机初始化成功,请拍照');
  316. // 显示拍照对话框
  317. if (!mounted) return;
  318. final l10n = AppLocalizations.of(context)!;
  319. final shouldTakePhoto = await showDialog<bool>(
  320. context: context,
  321. builder: (context) => AlertDialog(
  322. title: Text(l10n.professionalCamera),
  323. content: Text(l10n.cameraReady),
  324. actions: [
  325. TextButton(
  326. onPressed: () => Navigator.of(context).pop(false),
  327. child: Text(l10n.cancel),
  328. ),
  329. ElevatedButton(
  330. onPressed: () => Navigator.of(context).pop(true),
  331. child: const Text('拍照'),
  332. ),
  333. ],
  334. ),
  335. );
  336. if (shouldTakePhoto == true && mounted) {
  337. final file = await cameraService.takePicture();
  338. if (file != null) {
  339. setState(() {
  340. _cameraImage = file;
  341. _selectedImage = null;
  342. });
  343. ToastUtils.showSuccess(l10n.professionalCameraSuccess);
  344. AppLogger.d('专业相机拍照成功: ${file.path}');
  345. }
  346. }
  347. } on MediaException catch (e) {
  348. AppLogger.e('专业相机失败', e);
  349. ToastUtils.showError(e.message);
  350. } catch (e) {
  351. AppLogger.e('专业相机异常', e);
  352. ToastUtils.showError('专业相机失败: ${e.toString()}');
  353. } finally {
  354. await cameraService?.dispose();
  355. setState(() {
  356. _isLoading = false;
  357. });
  358. }
  359. }
  360. /// 测试获取图片信息
  361. Future<void> _testGetImageInfo() async {
  362. final currentImage = _currentImage;
  363. if (currentImage == null) return;
  364. setState(() {
  365. _isLoading = true;
  366. });
  367. try {
  368. AppLogger.d('开始获取图片信息...');
  369. final info = await MediaService.getImageInfo(currentImage);
  370. final l10n = AppLocalizations.of(context)!;
  371. final sizeInBytes = info['size'] as int;
  372. final sizeInKB = (sizeInBytes / 1024).toStringAsFixed(2);
  373. final infoText = '${l10n.imageWidth}: ${info['width']}${l10n.pixels}\n'
  374. '${l10n.imageHeight}: ${info['height']}${l10n.pixels}\n'
  375. '${l10n.imageSize}: $sizeInKB ${l10n.kilobytes}\n'
  376. '${l10n.imageFormat}: ${info['format']}';
  377. if (!mounted) return;
  378. // 使用 Alert 弹窗显示图片信息
  379. showDialog(
  380. context: context,
  381. builder: (context) => AlertDialog(
  382. title: Text(l10n.imageInfo),
  383. content: Text(
  384. infoText,
  385. style: const TextStyle(
  386. fontSize: 14,
  387. fontFamily: 'monospace',
  388. ),
  389. ),
  390. actions: [
  391. TextButton(
  392. onPressed: () => Navigator.of(context).pop(),
  393. child: Text(l10n.confirm),
  394. ),
  395. ],
  396. ),
  397. );
  398. ToastUtils.showSuccess(l10n.getImageInfoSuccess);
  399. AppLogger.d('图片信息: $info');
  400. } on MediaException catch (e) {
  401. AppLogger.e('获取图片信息失败', e);
  402. ToastUtils.showError(e.message);
  403. } catch (e) {
  404. AppLogger.e('获取图片信息异常', e);
  405. ToastUtils.showError('获取图片信息失败: ${e.toString()}');
  406. } finally {
  407. setState(() {
  408. _isLoading = false;
  409. });
  410. }
  411. }
  412. /// 测试去除 EXIF
  413. Future<void> _testRemoveExif() async {
  414. final currentImage = _currentImage;
  415. if (currentImage == null) return;
  416. setState(() {
  417. _isLoading = true;
  418. });
  419. try {
  420. AppLogger.d('开始去除 EXIF...');
  421. final cleanedFile = await MediaService.removeExif(currentImage);
  422. if (!mounted) return;
  423. final l10n = AppLocalizations.of(context)!;
  424. setState(() {
  425. // 更新对应的图片变量
  426. if (_selectedImage != null) {
  427. _selectedImage = cleanedFile;
  428. } else if (_cameraImage != null) {
  429. _cameraImage = cleanedFile;
  430. }
  431. });
  432. ToastUtils.showSuccess(l10n.removeExifSuccess);
  433. AppLogger.d('EXIF 去除成功: ${cleanedFile.path}');
  434. } on MediaException catch (e) {
  435. AppLogger.e('去除 EXIF 失败', e);
  436. ToastUtils.showError(e.message);
  437. } catch (e) {
  438. AppLogger.e('去除 EXIF 异常', e);
  439. ToastUtils.showError('去除 EXIF 失败: ${e.toString()}');
  440. } finally {
  441. setState(() {
  442. _isLoading = false;
  443. });
  444. }
  445. }
  446. /// 测试压缩图片
  447. Future<void> _testCompressImage() async {
  448. final currentImage = _currentImage;
  449. if (currentImage == null) return;
  450. setState(() {
  451. _isLoading = true;
  452. });
  453. try {
  454. AppLogger.d('开始压缩图片...');
  455. if (!mounted) return;
  456. final l10n = AppLocalizations.of(context)!;
  457. final originalSize = await currentImage.length();
  458. final compressedFile = await MediaService.compressImage(currentImage);
  459. final compressedSize = await compressedFile.length();
  460. setState(() {
  461. // 更新对应的图片变量
  462. if (_selectedImage != null) {
  463. _selectedImage = compressedFile;
  464. } else if (_cameraImage != null) {
  465. _cameraImage = compressedFile;
  466. }
  467. });
  468. final originalSizeKB = (originalSize / 1024).toStringAsFixed(2);
  469. final compressedSizeKB = (compressedSize / 1024).toStringAsFixed(2);
  470. final compressionRatio = ((1 - compressedSize / originalSize) * 100).toStringAsFixed(1);
  471. final compressInfo = '${l10n.originalSize}: $originalSizeKB ${l10n.kilobytes}\n'
  472. '${l10n.compressedSize}: $compressedSizeKB ${l10n.kilobytes}\n'
  473. '${l10n.compressionRatio}: $compressionRatio%';
  474. if (!mounted) return;
  475. // 使用 Alert 弹窗显示压缩信息
  476. showDialog(
  477. context: context,
  478. builder: (context) => AlertDialog(
  479. title: Text(l10n.compressImageSuccess),
  480. content: Text(
  481. compressInfo,
  482. style: const TextStyle(
  483. fontSize: 14,
  484. fontFamily: 'monospace',
  485. ),
  486. ),
  487. actions: [
  488. TextButton(
  489. onPressed: () => Navigator.of(context).pop(),
  490. child: Text(l10n.confirm),
  491. ),
  492. ],
  493. ),
  494. );
  495. ToastUtils.showSuccess(l10n.compressImageSuccess);
  496. AppLogger.d('图片压缩成功: ${compressedFile.path}');
  497. } on MediaException catch (e) {
  498. AppLogger.e('压缩图片失败', e);
  499. ToastUtils.showError(e.message);
  500. } catch (e) {
  501. AppLogger.e('压缩图片异常', e);
  502. ToastUtils.showError('压缩图片失败: ${e.toString()}');
  503. } finally {
  504. setState(() {
  505. _isLoading = false;
  506. });
  507. }
  508. }
  509. /// 清除所有图片
  510. void _clearImages() {
  511. final l10n = AppLocalizations.of(context)!;
  512. setState(() {
  513. _selectedImage = null;
  514. _cameraImage = null;
  515. });
  516. ToastUtils.show(l10n.clearAllImagesSuccess);
  517. }
  518. }