// ============================================================ // 闲言APP — 编辑器对话框组件 // 创建时间: 2026-04-25 // 更新时间: 2026-04-25 // 作用: 导出选项面板等独立对话框 Widget // 上次更新: 动态照片按钮禁用(次要色+锁定图标+即将支持提示)+_buildActionRow支持enabled参数 // ============================================================ import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:flutter_image_compress/flutter_image_compress.dart'; class ExportConfig { final String format; final int quality; final CompressFormat compressFormat; const ExportConfig({ required this.format, required this.quality, required this.compressFormat, }); static const defaults = ExportConfig( format: 'JPEG', quality: 92, compressFormat: CompressFormat.jpeg, ); } class ExportOptionsSheet extends StatefulWidget { final void Function(ExportConfig config) onSaveToGallery; final void Function(ExportConfig config) onShare; final VoidCallback onExportGif; final VoidCallback onExportMotionPhoto; final VoidCallback onExportXycard; final VoidCallback onPreview; final VoidCallback onCrop; final VoidCallback onShowInfo; static const _formats = ['JPEG', 'PNG', 'WebP']; static const _qualityLabels = ['低', '中', '高', '原始']; static const _qualityValues = [60, 80, 92, 100]; static const _compressFormats = [ CompressFormat.jpeg, CompressFormat.png, CompressFormat.webp, ]; const ExportOptionsSheet({ super.key, required this.onSaveToGallery, required this.onShare, required this.onExportGif, required this.onExportMotionPhoto, required this.onExportXycard, required this.onPreview, required this.onCrop, required this.onShowInfo, }); @override State createState() => _ExportOptionsSheetState(); } class _ExportOptionsSheetState extends State { int _formatIndex = 0; int _qualityIndex = 2; ExportConfig get _currentConfig => ExportConfig( format: ExportOptionsSheet._formats[_formatIndex], quality: ExportOptionsSheet._qualityValues[_qualityIndex], compressFormat: ExportOptionsSheet._compressFormats[_formatIndex], ); @override Widget build(BuildContext context) { return Container( constraints: BoxConstraints( maxHeight: MediaQuery.of(context).size.height * 0.75, ), decoration: BoxDecoration( color: CupertinoColors.systemBackground.resolveFrom(context), borderRadius: const BorderRadius.vertical(top: Radius.circular(16)), ), child: SingleChildScrollView( padding: const EdgeInsets.only( top: 12, left: 16, right: 16, bottom: 32, ), child: Column( mainAxisSize: MainAxisSize.min, children: [ Center( child: Container( width: 36, height: 4, margin: const EdgeInsets.only(bottom: 16), decoration: BoxDecoration( color: CupertinoColors.separator.resolveFrom(context), borderRadius: BorderRadius.circular(2), ), ), ), Text( '📤 导出卡片', style: CupertinoTheme.of(context).textTheme.navTitleTextStyle, ), const SizedBox(height: 16), _buildFormatSelector(), const SizedBox(height: 12), _buildQualitySelector(), const SizedBox(height: 16), const Divider(height: 1), const SizedBox(height: 8), _buildActionRow( icon: '💾', label: '保存到相册', subtitle: _formatSubtitle, onTap: () => widget.onSaveToGallery(_currentConfig), ), _buildActionRow( icon: '📤', label: '分享图片', subtitle: _formatSubtitle, onTap: () => widget.onShare(_currentConfig), ), _buildActionRow( icon: '🎬', label: '导出GIF', subtitle: '动画格式', onTap: widget.onExportGif, ), _buildActionRow( icon: '📸', label: '动态照片', subtitle: 'Live Photo / Motion Photo', onTap: widget.onExportMotionPhoto, enabled: false, ), _buildActionRow( icon: '📦', label: '导出 .xycard', subtitle: '源文件格式', onTap: widget.onExportXycard, ), const Divider(height: 1), const SizedBox(height: 8), _buildActionRow( icon: '🔍', label: '全屏预览', subtitle: '缩放查看', onTap: widget.onPreview, ), _buildActionRow( icon: '✂️', label: '裁剪编辑', subtitle: '旋转/翻转/裁剪', onTap: widget.onCrop, ), _buildActionRow( icon: 'ℹ️', label: '图片信息', subtitle: '尺寸/格式/大小', onTap: widget.onShowInfo, ), ], ), ), ); } String get _formatSubtitle { final q = ExportOptionsSheet._qualityValues[_qualityIndex]; return '${ExportOptionsSheet._formats[_formatIndex]} · ${ExportOptionsSheet._qualityLabels[_qualityIndex]}($q%)'; } Widget _buildFormatSelector() { return Row( children: [ Text( '格式', style: TextStyle( fontSize: 14, color: CupertinoColors.secondaryLabel.resolveFrom(context), ), ), const SizedBox(width: 12), Expanded( child: CupertinoSegmentedControl( groupValue: _formatIndex, children: { for (int i = 0; i < ExportOptionsSheet._formats.length; i++) i: Padding( padding: const EdgeInsets.symmetric( horizontal: 8, vertical: 4, ), child: Text( ExportOptionsSheet._formats[i], style: const TextStyle(fontSize: 13), ), ), }, onValueChanged: (v) => setState(() => _formatIndex = v), ), ), ], ); } Widget _buildQualitySelector() { return Row( children: [ Text( '质量', style: TextStyle( fontSize: 14, color: CupertinoColors.secondaryLabel.resolveFrom(context), ), ), const SizedBox(width: 12), Expanded( child: CupertinoSegmentedControl( groupValue: _qualityIndex, children: { for (int i = 0; i < ExportOptionsSheet._qualityLabels.length; i++) i: Padding( padding: const EdgeInsets.symmetric( horizontal: 6, vertical: 4, ), child: Text( ExportOptionsSheet._qualityLabels[i], style: const TextStyle(fontSize: 13), ), ), }, onValueChanged: (v) => setState(() => _qualityIndex = v), ), ), ], ); } Widget _buildActionRow({ required String icon, required String label, required String subtitle, required VoidCallback onTap, bool enabled = true, }) { // 禁用状态:次要色 + 锁定图标 + "即将支持"提示 final labelColor = enabled ? CupertinoColors.label.resolveFrom(context) : CupertinoColors.tertiaryLabel.resolveFrom(context); final subtitleColor = enabled ? CupertinoColors.secondaryLabel.resolveFrom(context) : CupertinoColors.tertiaryLabel.resolveFrom(context); final trailingIcon = enabled ? CupertinoIcons.chevron_right : CupertinoIcons.lock_fill; final trailingColor = enabled ? CupertinoColors.tertiaryLabel.resolveFrom(context) : CupertinoColors.tertiaryLabel.resolveFrom(context); final displaySubtitle = enabled ? subtitle : '$subtitle · 即将支持'; return CupertinoButton( padding: const EdgeInsets.symmetric(vertical: 6), onPressed: enabled ? onTap : null, child: Row( children: [ Text(icon, style: TextStyle(fontSize: 20, color: labelColor)), const SizedBox(width: 10), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( label, style: TextStyle( fontSize: 15, fontWeight: FontWeight.w500, color: labelColor, ), ), Text( displaySubtitle, style: TextStyle( fontSize: 12, color: subtitleColor, ), ), ], ), ), Icon( trailingIcon, size: enabled ? 16 : 14, color: trailingColor, ), ], ), ); } }