chore: 批量更新v6.5.21版本,整合多项功能修复与优化

主要变更:
1. 新增多风格音效资源与管理文档
2. 修复翻译服务空响应处理与Dio日志异常捕获
3. 完善Web端平台适配与路径获取Stub
4. 优化设备配对与文件传输功能
5. 新增角色命名常量与摇一摇检测器
6. 修复Riverpod dispose与鸿蒙导航路由
7. 新增每日通知服务与流体着色器
8. 优化备份服务与数据管理页面
9. 新增隐私设置附近设备发现选项
10. 重构诗词提供者支持历史记录
11. 完善桌面端构建配置与开发脚本
12. 清理旧版工具部署脚本
This commit is contained in:
Developer
2026-05-21 00:19:14 +08:00
parent 27672343b8
commit f9c19463f9
175 changed files with 33869 additions and 14702 deletions

View File

@@ -1,9 +1,9 @@
// ============================================================
// 闲言APP — ProImageEditor 包装页
// 创建时间: 2026-04-23
// 更新时间: 2026-05-05
// 更新时间: 2026-05-20
// 作用: ProImageEditor 的闲言APP包装页全新iOS 26风格
// 上次更新: 修复描边坐标偏移(globalToLocal两步转换)+文本编辑器用selectedLayerNotifier回退取TextLayer
// 上次更新: 画布背景改透明+ColoredBox提供背景色配合CanvasStyleMiddleware裁剪修复
// ============================================================
import 'dart:convert';
@@ -15,6 +15,7 @@ import 'package:flutter_svg/flutter_svg.dart';
import 'package:pro_image_editor/pro_image_editor.dart' as pro;
import 'package:xianyan/core/router/editor_router.dart';
import 'package:xianyan/editor/services/core/canvas_style_middleware.dart';
import 'package:xianyan/editor/services/core/pro_editor_bridge.dart';
import 'package:xianyan/editor/services/core/editor_theme_notifier.dart';
import 'package:xianyan/editor/widgets/controls/editor_system_ui.dart';
@@ -365,6 +366,8 @@ class ProEditorPageState extends State<ProEditorPage>
}
}
String? latestDeltaJson;
showCupertinoModalPopup<void>(
context: context,
builder: (_) => SizedBox(
@@ -373,8 +376,45 @@ class ProEditorPageState extends State<ProEditorPage>
initialDeltaJson: initialDeltaJson,
onDone: () {
Navigator.pop(context);
final deltaJson = latestDeltaJson;
if (deltaJson == null || deltaJson.isEmpty) return;
final editorState = _editorKey.currentState;
if (editorState == null) return;
try {
final deltaOps = jsonDecode(deltaJson) as List<dynamic>;
final plainText = deltaOps
.whereType<Map<String, dynamic>>()
.where((op) => op.containsKey('insert'))
.map((op) {
final v = op['insert'];
return v is String ? v : '';
})
.join()
.trim();
if (plainText.isEmpty) return;
if (textLayer != null) {
textLayer.text = plainText;
editorState.setState(() {});
} else {
editorState.addLayer(
pro.TextLayer(
text: plainText,
color: const Color(0xFFFFFFFF),
background: Colors.transparent,
align: TextAlign.center,
offset: const Offset(0.5, 0.3),
),
);
}
} catch (_) {}
},
onChanged: (deltaJson) {
latestDeltaJson = deltaJson;
if (textLayer == null) return;
final editorState = _editorKey.currentState;
if (editorState == null) return;
@@ -434,10 +474,10 @@ class ProEditorPageState extends State<ProEditorPage>
final safeBottom = MediaQuery.of(context).viewPadding.bottom;
final configs = ProEditorBridge.buildConfigs(
context: context,
canvasStyle: _canvasStyle,
bodyItemsBuilder: (editor, stream) =>
_buildMainBodyWidgets(editor, stream, safeTop, safeBottom),
onCloseWarning: handleCloseWarning,
canvasBackground: Colors.transparent,
);
return MediaQuery.removeViewPadding(
@@ -453,94 +493,103 @@ class ProEditorPageState extends State<ProEditorPage>
padding: const EdgeInsets.all(8),
child: RepaintBoundary(
key: _repaintKey,
child: pro.ProImageEditor.memory(
widget.imageBytes,
key: _editorKey,
configs: configs,
callbacks: pro.ProImageEditorCallbacks(
onImageEditingComplete: onEditingComplete,
onCloseEditor: (mode) => Navigator.of(context).pop(),
mainEditorCallbacks: pro.MainEditorCallbacks(
onCreateTextLayer:
widget.initialText != null &&
widget.initialText!.isNotEmpty
? () async => pro.TextLayer(
text: widget.initialText!,
color: const Color(0xFFFFFFFF),
background: Colors.transparent,
align: TextAlign.center,
offset: const Offset(0.5, 0.3),
)
: null,
onLayerTapUp: (layer) => _onLayerTapUp(layer),
onScaleStart: (_) {
if (!_isLayerDragging) {
setState(() => _isLayerDragging = true);
}
},
onScaleUpdate: (details) {
final editorState = _editorKey.currentState;
final layer = editorState?.selectedLayer;
if (layer != null) {
final layerBox =
layer.key.currentContext?.findRenderObject()
as RenderBox?;
final overlayBox =
_overlayStackKey.currentContext
?.findRenderObject()
as RenderBox?;
if (layerBox != null &&
layerBox.hasSize &&
overlayBox != null) {
final globalTopLeft = layerBox.localToGlobal(
Offset.zero,
);
final globalBottomRight = layerBox.localToGlobal(
Offset(
layerBox.size.width,
layerBox.size.height,
),
);
final localTopLeft = overlayBox.globalToLocal(
globalTopLeft,
);
final localBottomRight = overlayBox.globalToLocal(
globalBottomRight,
);
final size = Size(
(localBottomRight.dx - localTopLeft.dx).abs(),
(localBottomRight.dy - localTopLeft.dy).abs(),
);
setState(() {
_draggingLayerRect = localTopLeft & size;
});
}
}
},
onScaleEnd: (_) {
if (_isLayerDragging) {
setState(() {
_isLayerDragging = false;
_draggingLayerRect = null;
});
}
},
onLongPress: () {
EditorThemeNotifier.instance.toggleDarkLight();
},
onEditorZoomScaleStart: (_) {
if (!_isPinching) {
setState(() => _isPinching = true);
}
},
onEditorZoomScaleEnd: (_) {
if (_isPinching) {
setState(() => _isPinching = false);
}
},
onEditorZoomMatrix4Change: (matrix) {
_zoomScaleNotifier.value = matrix.getMaxScaleOnAxis();
},
child: CanvasStyleMiddleware(
style: _canvasStyle,
child: ColoredBox(
color: p.bgCanvas,
child: pro.ProImageEditor.memory(
widget.imageBytes,
key: _editorKey,
configs: configs,
callbacks: pro.ProImageEditorCallbacks(
onImageEditingComplete: onEditingComplete,
onCloseEditor: (mode) => Navigator.of(context).pop(),
mainEditorCallbacks: pro.MainEditorCallbacks(
onCreateTextLayer:
widget.initialText != null &&
widget.initialText!.isNotEmpty
? () async => pro.TextLayer(
text: widget.initialText!,
color: const Color(0xFFFFFFFF),
background: Colors.transparent,
align: TextAlign.center,
offset: const Offset(0.5, 0.3),
)
: null,
onLayerTapUp: (layer) => _onLayerTapUp(layer),
onScaleStart: (_) {
if (!_isLayerDragging) {
setState(() => _isLayerDragging = true);
}
},
onScaleUpdate: (details) {
final editorState = _editorKey.currentState;
final layer = editorState?.selectedLayer;
if (layer != null) {
final layerBox =
layer.key.currentContext?.findRenderObject()
as RenderBox?;
final overlayBox =
_overlayStackKey.currentContext
?.findRenderObject()
as RenderBox?;
if (layerBox != null &&
layerBox.hasSize &&
overlayBox != null) {
final globalTopLeft = layerBox.localToGlobal(
Offset.zero,
);
final globalBottomRight = layerBox
.localToGlobal(
Offset(
layerBox.size.width,
layerBox.size.height,
),
);
final localTopLeft = overlayBox.globalToLocal(
globalTopLeft,
);
final localBottomRight = overlayBox
.globalToLocal(globalBottomRight);
final size = Size(
(localBottomRight.dx - localTopLeft.dx)
.abs(),
(localBottomRight.dy - localTopLeft.dy)
.abs(),
);
setState(() {
_draggingLayerRect = localTopLeft & size;
});
}
}
},
onScaleEnd: (_) {
if (_isLayerDragging) {
setState(() {
_isLayerDragging = false;
_draggingLayerRect = null;
});
}
},
onLongPress: () {
EditorThemeNotifier.instance.toggleDarkLight();
},
onEditorZoomScaleStart: (_) {
if (!_isPinching) {
setState(() => _isPinching = true);
}
},
onEditorZoomScaleEnd: (_) {
if (_isPinching) {
setState(() => _isPinching = false);
}
},
onEditorZoomMatrix4Change: (matrix) {
_zoomScaleNotifier.value = matrix
.getMaxScaleOnAxis();
},
),
),
),
),
),

View File

@@ -0,0 +1,200 @@
// ============================================================
// 闲言APP — 画布样式中间件
// 创建时间: 2026-05-20
// 更新时间: 2026-05-20
// 作用: 通过包裹ProImageEditor实现画布样式实时预览
// 圆角/边框/阴影/叠层/外边距,无需魔改三方库
// 上次更新: 重构渲染层级 — ClipRRect直接裁剪child样式作用于画布本身
// ============================================================
//
// 设计原理:
// ProImageEditorState 没有 didUpdateWidget
// 导致 wrapBody 回调在 configs 变更后不会更新。
// 因此本中间件在父级直接包裹 ProImageEditor
// 通过 setState 驱动重建,确保样式始终跟随 _canvasStyle。
//
// 渲染层级 (从内到外):
// 1. child — ProImageEditor 编辑器本体 (背景已透明)
// 2. clipRadius — ClipRRect 圆角裁剪 (直接裁剪child)
// 3. border — 边框 (紧贴裁剪后的画布边缘)
// 4. shadow — BoxShadow 阴影 (基于裁剪后画布生成)
// 5. stackLayers — Stack 叠层卡片
// 6. outerMargin — Padding 外边距
//
// 关键: ProImageEditor 的 Scaffold 背景需设为透明,
// 画布背景色由外层 ColoredBox 提供,
// 这样 ClipRRect 才能正确裁剪整个画布内容。
//
// 导出路径:
// 编辑时预览 → 本中间件 (实时渲染)
// 导出时后处理 → ExportService.applyCanvasStyle() (dart:ui 精确渲染)
// ============================================================
import 'package:dotted_border/dotted_border.dart';
import 'package:flutter/material.dart';
import 'package:pro_image_editor/core/models/canvas_style_model.dart';
class CanvasStyleMiddleware extends StatelessWidget {
const CanvasStyleMiddleware({
super.key,
required this.style,
required this.child,
});
final CanvasStyleModel style;
final Widget child;
@override
Widget build(BuildContext context) {
final radius = style.borderRadius.clamp(0.0, 80.0);
final borderRad = BorderRadius.circular(radius);
final hasAnyStyle =
radius > 0 ||
style.hasBorder ||
style.hasShadow ||
style.hasStack ||
(style.hasOuterMargin && style.outerMargin > 0);
if (!hasAnyStyle) return child;
Widget result = child;
if (radius > 0) {
result = ClipRRect(borderRadius: borderRad, child: result);
}
if (style.hasBorder) {
result = _buildBorder(result, borderRad);
}
if (style.hasShadow) {
result = Container(
decoration: BoxDecoration(
borderRadius: borderRad,
boxShadow: [
BoxShadow(
color: Colors.black.withValues(alpha: style.shadowOpacity),
blurRadius: style.shadowBlur,
spreadRadius: style.shadowSpread,
offset: Offset(style.shadowOffsetX, style.shadowOffsetY),
),
],
),
child: result,
);
}
if (style.hasStack && style.stackLayers > 1) {
result = _buildStackLayers(result, borderRad);
}
if (style.hasOuterMargin && style.outerMargin > 0) {
result = Padding(
padding: EdgeInsets.all(style.outerMargin),
child: result,
);
}
return result;
}
Widget _buildStackLayers(Widget content, BorderRadius borderRad) {
final children = <Widget>[];
for (int i = style.stackLayers; i >= 1; i--) {
final offset = style.stackDistance * i;
final dx = _stackOffsetX(offset);
final dy = _stackOffsetY(offset);
children.add(
Transform.translate(
offset: Offset(dx, dy),
child: Container(
decoration: BoxDecoration(
color: Colors.white.withValues(alpha: 0.15),
borderRadius: borderRad,
border: style.hasBorder
? Border.all(
color: style.borderColor.withValues(alpha: 0.2),
width: style.borderWidth * 0.5,
)
: null,
),
),
),
);
}
children.add(content);
return Stack(
alignment: style.stackPosition.alignment,
clipBehavior: Clip.none,
children: children,
);
}
double _stackOffsetX(double offset) {
final pos = style.stackPosition;
if (pos == CanvasStackPosition.left ||
pos == CanvasStackPosition.topLeft ||
pos == CanvasStackPosition.centerLeft ||
pos == CanvasStackPosition.bottomLeft) {
return -offset;
}
if (pos == CanvasStackPosition.right ||
pos == CanvasStackPosition.topRight ||
pos == CanvasStackPosition.centerRight ||
pos == CanvasStackPosition.bottomRight) {
return offset;
}
return 0.0;
}
double _stackOffsetY(double offset) {
final pos = style.stackPosition;
if (pos == CanvasStackPosition.top ||
pos == CanvasStackPosition.topLeft ||
pos == CanvasStackPosition.topCenter ||
pos == CanvasStackPosition.topRight) {
return -offset;
}
if (pos == CanvasStackPosition.bottom ||
pos == CanvasStackPosition.bottomLeft ||
pos == CanvasStackPosition.bottomCenter ||
pos == CanvasStackPosition.bottomRight) {
return offset;
}
return 0.0;
}
Widget _buildBorder(Widget content, BorderRadius borderRad) {
if (style.borderStyle == CanvasBorderStyle.dashed ||
style.borderStyle == CanvasBorderStyle.dotted) {
final dashPattern = style.borderStyle == CanvasBorderStyle.dashed
? const [6.0, 4.0]
: const [2.0, 3.0];
return DottedBorder(
options: RoundedRectDottedBorderOptions(
dashPattern: dashPattern,
strokeWidth: style.borderWidth,
color: style.borderColor,
radius: Radius.circular(borderRad.topLeft.x),
padding: EdgeInsets.zero,
strokeCap: StrokeCap.round,
),
child: content,
);
}
return Container(
decoration: BoxDecoration(
borderRadius: borderRad,
border: Border.all(color: style.borderColor, width: style.borderWidth),
),
child: content,
);
}
}

View File

@@ -1,8 +1,9 @@
// ============================================================
// 闲言APP — ProImageEditor 桥接层
// 创建时间: 2026-04-23
// 更新时间: 2026-05-05
// 上次更新: buildConfigs接受CanvasStyleModel(圆角+边框+阴影+叠层)
// 更新时间: 2026-05-20
// 上次更新: 移除wrapBody方案画布样式改由CanvasStyleMiddleware中间件实现
// 新增canvasBackground参数支持透明画布背景
// ============================================================
import 'dart:ui' as ui;
@@ -17,7 +18,6 @@ import 'package:pro_image_editor/designs/frosted_glass/frosted_glass.dart';
import 'package:xianyan/core/theme/app_radius.dart';
import 'package:xianyan/core/theme/app_spacing.dart';
import 'package:xianyan/editor/models/editor_models.dart';
import 'package:pro_image_editor/core/models/canvas_style_model.dart';
import 'package:xianyan/editor/services/core/editor_theme_notifier.dart';
import 'package:xianyan/editor/services/core/sticker_config_builder.dart';
import 'package:xianyan/editor/services/core/widget_layer_factory.dart';
@@ -102,7 +102,6 @@ class ProEditorBridge {
/// 构建 ProImageEditorConfigs — Cupertino + iOS风格 + 中文
static pro.ProImageEditorConfigs buildConfigs({
required BuildContext context,
CanvasStyleModel? canvasStyle,
List<TextStyle>? additionalFonts,
List<pro.ReactiveWidget> Function(
pro.ProImageEditorState editor,
@@ -110,6 +109,7 @@ class ProEditorBridge {
)?
bodyItemsBuilder,
Future<bool> Function(pro.ProImageEditorState editor)? onCloseWarning,
Color? canvasBackground,
}) {
final isDark = CupertinoTheme.brightnessOf(context) == Brightness.dark;
@@ -140,9 +140,6 @@ class ProEditorBridge {
builder: (_) => SizedBox(key: key, height: 0),
),
bodyItems: bodyItemsBuilder,
wrapBody: (editor, rebuildStream, content) {
return content;
},
closeWarningDialog:
onCloseWarning ??
(editor) async {
@@ -169,7 +166,8 @@ class ProEditorBridge {
},
),
style: pro.MainEditorStyle(
background: EditorThemeNotifier.instance.palette.bgCanvas,
background:
canvasBackground ?? EditorThemeNotifier.instance.palette.bgCanvas,
uiOverlayStyle: SystemUiOverlayStyle(
statusBarColor: const Color(0x00000000),
statusBarIconBrightness: EditorThemeNotifier.instance.isDark

View File

@@ -1,9 +1,9 @@
// ============================================================
// 闲言APP — 导出服务
// 创建时间: 2026-04-20
// 更新时间: 2026-05-05
// 更新时间: 2026-05-20
// 作用: 截图导出 + 保存相册 + 系统分享 + WidgetLayer合成导出 + 压缩导出
// 上次更新: 新增 applyCanvasStyle 画布样式后处理(边框+阴影+叠层+圆角)
// 上次更新: 验证applyCanvasStyle渲染层级与CanvasStyleMiddleware一致
// ============================================================
import 'dart:typed_data';

View File

@@ -1,9 +1,9 @@
// ============================================================
// 闲言APP — 编辑器底部工具栏 v2
// 创建时间: 2026-05-03
// 更新时间: 2026-05-04
// 更新时间: 2026-05-20
// 作用: Tab分类+工具按钮+分割线+状态行+Home指示条
// 上次更新: emoji→Icon全量替换使用EditorIconData映射表
// 上次更新: 图片格式信息→提示词按钮+新增onShowPromptPanel回调
// ============================================================
import 'dart:typed_data';
@@ -15,7 +15,6 @@ import 'package:pro_image_editor/pro_image_editor.dart' as pro;
import 'package:xianyan/editor/services/core/color_tokens.dart';
import 'package:xianyan/editor/services/core/editor_theme_notifier.dart';
import 'package:xianyan/editor/widgets/controls/editor_icon.dart';
import 'package:xianyan/editor/services/image/image_info_service.dart';
class _ToolDef {
const _ToolDef({
@@ -68,6 +67,7 @@ class EditorBottomToolbarV2 extends StatefulWidget {
this.onShowLayerPanel,
this.onShowEmojiIconGrid,
this.onShowRichTextEditor,
this.onShowPromptPanel,
});
final pro.ProImageEditorState editor;
@@ -94,6 +94,7 @@ class EditorBottomToolbarV2 extends StatefulWidget {
final VoidCallback? onShowLayerPanel;
final VoidCallback? onShowEmojiIconGrid;
final VoidCallback? onShowRichTextEditor;
final VoidCallback? onShowPromptPanel;
@override
State<EditorBottomToolbarV2> createState() => _EditorBottomToolbarV2State();
@@ -104,7 +105,6 @@ class _EditorBottomToolbarV2State extends State<EditorBottomToolbarV2>
int _activeTab = 0;
int _activeToolIndex = -1;
bool _showDebugInfo = false;
ImageInfoResult? _imageInfo;
late PageController _pageController;
late List<_ToolTab> _tabs;
@@ -112,7 +112,6 @@ class _EditorBottomToolbarV2State extends State<EditorBottomToolbarV2>
@override
void initState() {
super.initState();
_loadImageInfo();
_initTabs();
_pageController = PageController(initialPage: _activeTab);
}
@@ -248,15 +247,9 @@ class _EditorBottomToolbarV2State extends State<EditorBottomToolbarV2>
];
}
void _loadImageInfo() {
final info = ImageInfoService.getImageInfoFromBytes(widget.imageBytes);
if (mounted) setState(() => _imageInfo = info);
}
@override
void didUpdateWidget(covariant EditorBottomToolbarV2 oldWidget) {
super.didUpdateWidget(oldWidget);
if (widget.imageBytes != oldWidget.imageBytes) _loadImageInfo();
_initTabs();
}
@@ -464,17 +457,12 @@ class _EditorBottomToolbarV2State extends State<EditorBottomToolbarV2>
}
Widget _buildImageInfo(EditorPalette p) {
if (_imageInfo == null) return const SizedBox.shrink();
return Row(
mainAxisSize: MainAxisSize.min,
children: [
Text(_imageInfo!.formatEmoji, style: const TextStyle(fontSize: 10)),
const SizedBox(width: 3),
Text(
'${_imageInfo!.format} · ${_imageInfo!.sizeText}',
style: TextStyle(color: p.textHint, fontSize: 10),
),
],
return _StatusChip(
iconName: 'textformat',
label: '提示词',
isActive: false,
palette: p,
onTap: () => widget.onShowPromptPanel?.call(),
);
}