此版本包含多项功能更新与问题修复: 1. 新增iOS ShareExtension分享扩展,支持多类型内容分享 2. 修复认证流程日志提示,更新用户名检测逻辑 3. 优化会话列表UI,替换emoji为CupertinoIcons原生图标 4. 修正搜索类型与频道名称映射,新增音频类型支持 5. 调整启动页布局与多语言配置 6. 重构布局约束,修复无界布局崩溃问题 7. 迁移开发者设置到更多设置页,新增日志级别配置 8. 优化TTS健康检查与自动回退逻辑 9. 新增笔记置顶会话跳转功能 10. 更新后端配置与本地化字符串 11. 重构稍后读模块,支持音频内容处理 12. 优化编辑器功能与字体管理页面 13. 新增本地数据库置顶笔记表 14. 修复Android MANAGE_STORAGE权限配置
313 lines
9.4 KiB
Dart
313 lines
9.4 KiB
Dart
// ============================================================
|
||
// 闲言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<ExportOptionsSheet> createState() => _ExportOptionsSheetState();
|
||
}
|
||
|
||
class _ExportOptionsSheetState extends State<ExportOptionsSheet> {
|
||
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<int>(
|
||
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<int>(
|
||
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,
|
||
),
|
||
],
|
||
),
|
||
);
|
||
}
|
||
}
|