import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:image_picker/image_picker.dart'; import '../../../api/photo_api.dart'; import '../../../core/network/api_env.dart'; import '../../../core/network/api_exception.dart'; import '../../../models/photo.dart'; import '../../../models/watermark.dart'; import '../../../providers/watermark_provider.dart'; import '../../../utils/toast.dart'; import 'watermark_edit_page.dart'; import 'widgets/watermark_settings_sheet.dart'; /// 水印相机模块入口:拍照 / 相册选图 → 进编辑页加水印; /// 下面列出**已经上传过的原图**,点一张就能重新打水印。 /// /// 刻意不做自绘相机预览(原项目也是调系统相机):省掉一整套相机权限、 /// 预览流、方向处理的复杂度,代价只是没法实时看到水印。 class WatermarkCameraPage extends ConsumerStatefulWidget { const WatermarkCameraPage({super.key}); @override ConsumerState createState() => _WatermarkCameraPageState(); } class _WatermarkCameraPageState extends ConsumerState { static const _pageSize = 21; // 3 列,正好 7 行 final List _photos = []; int _total = 0; bool _loading = true; bool _loadingMore = false; bool _hasMore = false; int _page = 0; String? _error; /// 正在下载哪一张的原图(下载完才进编辑页) int? _openingId; double? _openProgress; @override void initState() { super.initState(); _loadFirstPage(); } // ---------------- 列表 ---------------- Future _loadFirstPage() async { setState(() { _loading = true; _error = null; }); try { final page = await ref.read(photoApiProvider).list(page: 1, size: _pageSize); if (!mounted) return; setState(() { _photos ..clear() ..addAll(page.items); _total = page.total; _page = 1; _hasMore = page.hasMore; _loading = false; }); } on ApiException catch (e) { if (!mounted) return; setState(() { _loading = false; _error = e.message; }); } catch (_) { if (!mounted) return; setState(() { _loading = false; _error = '照片加载失败'; }); } } Future _loadMore() async { if (_loadingMore || !_hasMore) return; setState(() => _loadingMore = true); try { final next = _page + 1; final page = await ref.read(photoApiProvider).list(page: next, size: _pageSize); if (!mounted) return; setState(() { // 按 id 去重:翻页期间如果有新照片传上来,可能两页里都有 final existing = _photos.map((p) => p.id).toSet(); _photos.addAll(page.items.where((p) => !existing.contains(p.id))); _total = page.total; _page = next; _hasMore = page.hasMore; _loadingMore = false; }); } catch (_) { if (!mounted) return; setState(() => _loadingMore = false); } } // ---------------- 选照片 ---------------- /// 选图。**不传 imageQuality / maxWidth** —— 要的就是原图, /// 编辑页会在合成时按最长边 1920 缩放,原始文件则原样备份到服务器。 Future _pick(ImageSource source) async { try { final file = await ImagePicker().pickImage(source: source); if (file == null || !mounted) return; await Navigator.of(context).push( MaterialPageRoute(builder: (_) => WatermarkEditPage(imagePath: file.path)), ); // 回来刷新一下:刚才那张已经传上去了,列表该多一张 if (mounted) await _loadFirstPage(); } catch (e) { if (mounted) showToast(context, '打开相机/相册失败:$e', error: true); } } /// 点一张老照片 → 下载原图 → 进编辑页重新打水印 Future _openPhoto(PhotoItem photo) async { if (_openingId != null) return; setState(() { _openingId = photo.id; _openProgress = null; }); try { final path = await ref.read(photoApiProvider).downloadOriginal( photo.url, onProgress: (received, total) { if (!mounted || total <= 0) return; setState(() => _openProgress = received / total); }, ); if (!mounted) return; setState(() { _openingId = null; _openProgress = null; }); await Navigator.of(context).push( MaterialPageRoute( builder: (_) => WatermarkEditPage( imagePath: path, // 服务器上已经有这张原图了,别再传一遍 uploadOriginal: false, // 用拍摄当时的地点天气,而不是你现在站的地方 initialData: photo.toWatermarkData(), ), ), ); } on ApiException catch (e) { if (!mounted) return; setState(() { _openingId = null; _openProgress = null; }); showToast(context, e.message, error: true); } catch (_) { if (!mounted) return; setState(() { _openingId = null; _openProgress = null; }); showToast(context, '原图下载失败,请重试', error: true); } } // ---------------- 构建 ---------------- @override Widget build(BuildContext context) { final settings = ref.watch(watermarkSettingsProvider); return Scaffold( appBar: AppBar( title: const Text('水印相机'), actions: [ IconButton( tooltip: '水印设置', icon: const Icon(Icons.tune), onPressed: () => showWatermarkSettingsSheet(context), ), ], ), body: Stack( children: [ ListView( padding: const EdgeInsets.fromLTRB(16, 16, 16, 24), children: [ _introCard(), const SizedBox(height: 20), _captureButton( icon: Icons.camera_alt, label: '拍照', primary: true, onTap: () => _pick(ImageSource.camera), ), const SizedBox(height: 12), _captureButton( icon: Icons.photo_library_outlined, label: '从相册选', primary: false, onTap: () => _pick(ImageSource.gallery), ), const SizedBox(height: 24), _settingsCard(context, settings), const SizedBox(height: 24), _recentSection(), ], ), if (_openingId != null) _downloadOverlay(), ], ), ); } Widget _introCard() { return Container( padding: const EdgeInsets.all(18), decoration: BoxDecoration( gradient: const LinearGradient( begin: Alignment.topLeft, end: Alignment.bottomRight, colors: [Color(0xFFFFE5CC), Color(0xFFFFD4A8)], ), borderRadius: BorderRadius.circular(16), ), child: const Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( '拍一张,自动加水印', style: TextStyle( fontSize: 17, fontWeight: FontWeight.bold, color: Color(0xFF5D4037), ), ), SizedBox(height: 6), Text( '时间、地点、天气自动填好,可以拖动位置、换颜色。\n' '加好水印的照片直接存进手机相册;原图会同时备份到服务器。', style: TextStyle(fontSize: 12.5, height: 1.6, color: Color(0xFF8D6E63)), ), ], ), ); } Widget _captureButton({ required IconData icon, required String label, required bool primary, required VoidCallback onTap, }) { return FilledButton.icon( onPressed: onTap, style: FilledButton.styleFrom( backgroundColor: primary ? const Color(0xFFC8956E) : Colors.white, foregroundColor: primary ? Colors.white : const Color(0xFFC8956E), minimumSize: const Size.fromHeight(52), shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(26), side: primary ? BorderSide.none : const BorderSide(color: Color(0xFFC8956E)), ), elevation: 0, ), icon: Icon(icon, size: 20), label: Text( label, style: const TextStyle(fontSize: 15, fontWeight: FontWeight.w600), ), ); } Widget _settingsCard(BuildContext context, WatermarkSettings settings) { final items = [ if (settings.showTime) '时间', if (settings.showAddress) '地点', if (settings.showWeather) '天气', if (settings.showRemark) '备注', if (settings.showBrand) '品牌角标', ]; return InkWell( onTap: () => showWatermarkSettingsSheet(context), borderRadius: BorderRadius.circular(16), child: Container( padding: const EdgeInsets.fromLTRB(18, 16, 14, 16), decoration: BoxDecoration( color: Colors.white, borderRadius: BorderRadius.circular(16), ), child: Row( children: [ Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ const Text( '当前水印内容', style: TextStyle( fontSize: 14, fontWeight: FontWeight.w600, color: Color(0xFF5D4037), ), ), const SizedBox(height: 8), if (items.isEmpty) const Text( '全部关闭(照片不会加任何水印)', style: TextStyle(fontSize: 12, color: Color(0xFFBCAAA4)), ) else Wrap( spacing: 6, runSpacing: 6, children: items .map( (t) => Container( padding: const EdgeInsets.symmetric( horizontal: 10, vertical: 4, ), decoration: BoxDecoration( color: const Color(0xFFFFF3E6), borderRadius: BorderRadius.circular(10), ), child: Text( t, style: const TextStyle( fontSize: 12, color: Color(0xFF8D6E63), ), ), ), ) .toList(), ), ], ), ), const Icon(Icons.chevron_right, color: Color(0xFFBCAAA4)), ], ), ), ); } /// 已上传照片的网格 —— 点一张就能重新打水印(换颜色/位置/补备注) Widget _recentSection() { return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( children: [ const Text( '最近拍摄', style: TextStyle( fontSize: 14, fontWeight: FontWeight.w600, color: Color(0xFF5D4037), ), ), const Spacer(), if (_total > 0) Text( '共 $_total 张', style: const TextStyle(fontSize: 12, color: Color(0xFFBCAAA4)), ), ], ), const SizedBox(height: 4), const Text( '点一张可以重新加水印(原图没带水印,随便改)', style: TextStyle(fontSize: 11.5, color: Color(0xFFBCAAA4)), ), const SizedBox(height: 12), _recentBody(), ], ); } Widget _recentBody() { if (_loading) { return const Padding( padding: EdgeInsets.symmetric(vertical: 36), child: Center( child: SizedBox( width: 22, height: 22, child: CircularProgressIndicator(strokeWidth: 2), ), ), ); } if (_error != null) { return Container( width: double.infinity, padding: const EdgeInsets.symmetric(vertical: 22, horizontal: 16), decoration: BoxDecoration( color: Colors.white, borderRadius: BorderRadius.circular(14), ), child: Column( children: [ Text( _error!, textAlign: TextAlign.center, style: const TextStyle(fontSize: 12.5, color: Color(0xFFE53935)), ), const SizedBox(height: 10), TextButton( onPressed: _loadFirstPage, child: const Text( '重试', style: TextStyle(fontSize: 13, color: Color(0xFFC8956E)), ), ), ], ), ); } if (_photos.isEmpty) { return Container( width: double.infinity, padding: const EdgeInsets.symmetric(vertical: 28, horizontal: 16), decoration: BoxDecoration( color: Colors.white, borderRadius: BorderRadius.circular(14), ), child: const Text( '还没拍过。拍一张试试,原图会自动存到服务器', textAlign: TextAlign.center, style: TextStyle(fontSize: 12.5, color: Color(0xFFBCAAA4)), ), ); } return Column( children: [ GridView.builder( shrinkWrap: true, physics: const NeverScrollableScrollPhysics(), gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount( crossAxisCount: 3, crossAxisSpacing: 8, mainAxisSpacing: 8, ), itemCount: _photos.length, itemBuilder: (context, i) => _photoTile(_photos[i]), ), if (_hasMore) Padding( padding: const EdgeInsets.only(top: 14), child: TextButton( onPressed: _loadingMore ? null : _loadMore, child: _loadingMore ? const SizedBox( width: 16, height: 16, child: CircularProgressIndicator(strokeWidth: 2), ) : const Text( '加载更多', style: TextStyle(fontSize: 13, color: Color(0xFFC8956E)), ), ), ), ], ); } Widget _photoTile(PhotoItem photo) { // 缩略图没生成出来时退回原图(慢,但总比空着好) final url = resolveAssetUrl(photo.thumbUrl ?? photo.url); return GestureDetector( onTap: () => _openPhoto(photo), child: ClipRRect( borderRadius: BorderRadius.circular(10), child: Container( color: const Color(0xFFF0EAE4), child: Image.network( url, fit: BoxFit.cover, // 缩略图本来就只有 400 宽,这里再限一次解码尺寸,省内存 cacheWidth: 400, loadingBuilder: (context, child, progress) { if (progress == null) return child; return const Center( child: SizedBox( width: 16, height: 16, child: CircularProgressIndicator(strokeWidth: 2), ), ); }, errorBuilder: (context, _, _) => const Center( child: Icon(Icons.broken_image_outlined, size: 20, color: Color(0xFFBCAAA4)), ), ), ), ), ); } /// 下载原图时的遮罩。原图好几 MB,不提示的话用户会以为卡死了 Widget _downloadOverlay() { final percent = _openProgress == null ? null : (_openProgress! * 100).round(); return Positioned.fill( child: ColoredBox( color: const Color(0x99000000), child: Center( child: Container( padding: const EdgeInsets.symmetric(horizontal: 26, vertical: 22), decoration: BoxDecoration( color: Colors.white, borderRadius: BorderRadius.circular(14), ), child: Column( mainAxisSize: MainAxisSize.min, children: [ const SizedBox( width: 26, height: 26, child: CircularProgressIndicator(strokeWidth: 2.5), ), const SizedBox(height: 14), Text( percent == null ? '正在下载原图…' : '正在下载原图… $percent%', style: const TextStyle(fontSize: 13, color: Color(0xFF5D4037)), ), ], ), ), ), ), ); } }