feat: 新增画布样式后处理功能及导出一致性优化
refactor(editor): 将画布样式相关功能移至独立文件 fix(editor): 修复描边坐标偏移问题 feat(export): 新增applyCanvasStyle后处理方法 feat(settings): 实现CanvasStyleModel持久化存储 fix(feed): 互动操作增加登录检查避免401错误 feat(wallpaper): 实现无限下拉加载功能 fix(text): 修复富文本编辑器初始化问题
This commit is contained in:
@@ -16,7 +16,7 @@ api接口部分,可在本地使用接口请求验证,确保接口正常响
|
||||
|
||||
你现在是苹果前端工程师,这个项目经过多人之手,不同的人设计略有差异,
|
||||
请设计风格跟苹果集团一体的页面,如果风格不一致我就换其他 ai 了
|
||||
软件风格需要图文并茂,尽量使用icon,若无icon则使用通用的emoji代替
|
||||
软件风格需要图文并茂,尽量使用icon,若无icon则使用本地的svg代替,若无svg则自绘或使用通用的emoji代替
|
||||
https://developer.apple.com/design/human-interface-guidelines
|
||||
|
||||
软件要求风格统一,如颜色圆角按钮显示,每次修改页面需按照已经规定的值进行
|
||||
|
||||
157
CHANGELOG.md
157
CHANGELOG.md
@@ -4,6 +4,163 @@
|
||||
|
||||
---
|
||||
|
||||
## [3.9.13] - 2026-05-05
|
||||
|
||||
### ✨ 新功能 — 画布外边距控制
|
||||
|
||||
1. **外边距(outerMargin)** — `CanvasStyleModel` 新增 `outerMargin` 字段(-1=自动,0=紧贴,1-60=自定义px),画布样式面板新增"外边距"区域,含预设(自动/紧贴/适中/宽松)+ 滑块
|
||||
2. **强制压缩效果** — 当 `outerMargin` 为 0 时,阴影/叠层等效果被裁剪到画布边缘内;值越大,效果可扩展空间越大;值越小,效果被强制压缩
|
||||
|
||||
### 📁 修改文件
|
||||
|
||||
- `packages/pro_image_editor/lib/core/models/canvas_style_model.dart` — 新增 outerMargin + hasOuterMargin
|
||||
- `packages/pro_image_editor/lib/features/main_editor/widgets/main_editor_interactive_content.dart` — _CanvasStyleWrapper 用 Padding+ClipRect+OverflowBox 限制效果
|
||||
- `lib/editor/services/export/export_service.dart` — applyCanvasStyle 使用 outerMargin 作为固定 margin + clipRect 裁剪
|
||||
- `lib/editor/mixins/editor_export_actions.dart` — 导出条件增加 hasOuterMargin
|
||||
- `lib/editor/widgets/panels/canvas_style_sheet.dart` — 新增外边距区域(预设+滑块)
|
||||
|
||||
---
|
||||
|
||||
## [3.9.12] - 2026-05-05
|
||||
|
||||
### ✨ 新功能 — 阴影扩散范围调节
|
||||
|
||||
1. **阴影扩散范围** — `CanvasStyleModel` 新增 `shadowSpread` 字段(0-30px),画布样式面板阴影区域新增"扩散"滑块,可调整阴影的上下高度/扩散面积
|
||||
|
||||
### 📁 修改文件
|
||||
|
||||
- `packages/pro_image_editor/lib/core/models/canvas_style_model.dart` — 新增 shadowSpread 字段
|
||||
- `packages/pro_image_editor/lib/features/main_editor/widgets/main_editor_interactive_content.dart` — _CanvasStyleWrapper 使用 shadowSpread
|
||||
- `lib/editor/services/export/export_service.dart` — applyCanvasStyle 阴影 RRect.inflate(shadowSpread)
|
||||
- `lib/editor/widgets/panels/canvas_style_sheet.dart` — 阴影扩散滑块 + 预览区 spreadRadius
|
||||
|
||||
---
|
||||
|
||||
## [3.9.11] - 2026-05-05
|
||||
|
||||
### 🐛 Bug修复 — 导出画布样式一致性 + 阴影偏移调节
|
||||
|
||||
1. **导出效果与编辑预览完全一致** — 不再依赖 widget 渲染导出效果,改为 `dart:ui` Canvas 后处理:在 `ExportService.applyCanvasStyle` 中用 `PictureRecorder` 绘制圆角、边框、阴影、叠层,确保导出图片与编辑预览 1:1 一致
|
||||
2. **阴影偏移量调节** — `CanvasStyleModel` 新增 `shadowOffsetX` / `shadowOffsetY` 字段,画布样式面板新增 ↔↕ 偏移滑块 + 方向按钮(左/右/上/下/重置),可精确控制阴影方向和距离
|
||||
|
||||
### 📁 修改文件
|
||||
|
||||
- `packages/pro_image_editor/lib/core/models/canvas_style_model.dart` — 新增 shadowOffsetX/Y 字段 + copyWith/toJson/fromJson
|
||||
- `packages/pro_image_editor/lib/features/main_editor/widgets/main_editor_interactive_content.dart` — _CanvasStyleWrapper 使用 shadowOffsetX/Y
|
||||
- `lib/editor/services/export/export_service.dart` — 新增 applyCanvasStyle + _drawDashedRRect 后处理方法
|
||||
- `lib/editor/mixins/editor_export_actions.dart` — onEditingComplete 中调用 applyCanvasStyle 后处理
|
||||
- `lib/editor/mixins/editor_actions_base.dart` — 新增 canvasStyle 抽象 getter
|
||||
- `lib/editor/pages/editor/pro_editor_page.dart` — 实现 canvasStyle getter
|
||||
- `lib/editor/widgets/panels/canvas_style_sheet.dart` — 阴影偏移滑块+方向按钮+预览区偏移
|
||||
|
||||
---
|
||||
|
||||
## [3.9.10] - 2026-05-05
|
||||
|
||||
### 🐛 Bug修复 — 画布样式渲染+颜色选择器崩溃
|
||||
|
||||
1. **导出效果与编辑预览一致** — 重构 `_CanvasStyleWrapper` 渲染顺序:child → 边框(画布边缘上) → 阴影(最外层),所有效果在 ContentRecorder 内部渲染,导出图片与编辑预览完全一致
|
||||
2. **阴影区域过大修复** — `spreadRadius: 0`(之前1),`offset` 改为按 blurRadius 比例计算,预设值从 15/25 降至 8/16
|
||||
3. **虚线边框未显示修复** — `_DashedBorderPainter` 的 `paint` 方法使用正确的 `Size` 参数,并考虑 `borderWidth` 内缩偏移,虚线现在正确绘制在画布边缘上
|
||||
4. **边框位置修复** — 边框现在套在画布上(内层),阴影在最外层,不再出现边框在阴影外面的情况
|
||||
5. **颜色选择器崩溃修复** — `ColorPicker` 从 `CupertinoAlertDialog.title` 移至 `showModalBottomSheet`,避免复杂 widget 在受限空间中布局溢出导致卡死
|
||||
|
||||
### 📁 修改文件
|
||||
|
||||
- `packages/pro_image_editor/lib/features/main_editor/widgets/main_editor_interactive_content.dart` — 重构 _CanvasStyleWrapper + _BorderPainter + _DashedBorderPainter
|
||||
- `lib/editor/widgets/panels/canvas_style_sheet.dart` — 颜色选择器改用 showModalBottomSheet + 预览区渲染顺序修正 + 阴影参数调优
|
||||
|
||||
---
|
||||
|
||||
## [3.9.9] - 2026-05-05
|
||||
|
||||
### ✨ 新功能 — 画布样式编辑面板(独立文件,后续扩展)
|
||||
|
||||
1. **画布样式编辑面板** — 新增独立 `CanvasStyleSheet` 文件,支持以下编辑项目:
|
||||
- 🎨 边缘边框:线条粗细、实线/虚线切换、线条颜色选择
|
||||
- 🌫️ 边缘外侧阴影:模糊半径、透明度浓淡调节
|
||||
- 📚 边缘外侧叠层效果:多层层数、叠层距离
|
||||
- 📍 叠层位置:下方/上方/左侧/右侧/居中
|
||||
2. **CanvasStyleModel 数据模型** — 统一管理圆角+边框+阴影+叠层所有属性,支持 `copyWith`/`toJson`/`fromJson` 序列化
|
||||
3. **编辑器实时渲染** — 边框、阴影、叠层效果在编辑器中实时显示,虚线边框使用 `CustomPainter` 绘制
|
||||
4. **导出效果一致** — 所有样式效果在 `ContentRecorder` 内部渲染,导出图片与编辑预览完全一致
|
||||
5. **画布样式按钮** — 顶部导航栏右侧胶囊新增 `rectangle` 图标按钮,点击打开画布样式编辑面板
|
||||
6. **EditorSettingsSheet 精简** — 画布圆角/边框/阴影/叠层从设置面板移至独立 CanvasStyleSheet
|
||||
|
||||
### 📁 修改文件
|
||||
|
||||
- `lib/editor/widgets/panels/canvas_style_sheet.dart` — 新建:画布样式编辑面板(独立文件)
|
||||
- `packages/pro_image_editor/lib/core/models/canvas_style_model.dart` — 新建:画布样式数据模型
|
||||
- `packages/pro_image_editor/lib/features/main_editor/widgets/main_editor_interactive_content.dart` — _CanvasStyleWrapper + _DashedBorderPainter 渲染边框/阴影/叠层
|
||||
- `packages/pro_image_editor/lib/core/models/styles/main_editor_style.dart` — 新增 canvasStyle 属性
|
||||
- `lib/editor/widgets/controls/editor_top_nav.dart` — 新增 onCanvasStyle 回调 + rectangle 按钮
|
||||
- `lib/editor/pages/editor/pro_editor_page.dart` — CanvasStyleModel 状态 + _showCanvasStyleEditor
|
||||
- `lib/editor/services/core/pro_editor_bridge.dart` — buildConfigs 接受 CanvasStyleModel
|
||||
- `lib/editor/services/core/editor_settings_service.dart` — loadCanvasStyle/saveCanvasStyle
|
||||
- `lib/editor/widgets/panels/editor_settings_sheet.dart` — 精简:移除画布圆角部分
|
||||
|
||||
---
|
||||
|
||||
## [3.9.8] - 2026-05-05
|
||||
|
||||
### 🐛 Bug修复 + ✨ 新功能
|
||||
|
||||
1. **拖拽描边坐标偏移修复** — 之前 `localToGlobal(ancestor: overlayBox)` 在 overlayBox 不是 layer 祖先时返回全局坐标而非 overlay 本地坐标,导致描边偏右下。改为 `layerBox.localToGlobal()` → `overlayBox.globalToLocal()` 两步转换,正确计算 overlay 本地坐标
|
||||
2. **文本编辑器内容回写修复** — 点击工具栏文本按钮时 `editor.selectedLayer` 可能已被清空,改为优先取 `selectedLayerNotifier` 保留的上一次选中 TextLayer 引用,确保输入框显示已有文本
|
||||
3. **在线壁纸无限下拉加载** — "全部"源模式之前 `_hasMore = false` 无法继续加载,改为按页码轮询12个源分页加载 + `_loadedIds` 去重 + `anyHasNext` 判断是否还有更多
|
||||
4. **顶部工具栏工具抽屉按钮** — 右侧胶囊新增 `sidebar_left` 图标按钮,点击呼出工具抽屉(编辑/内容/更多三组工具)
|
||||
5. **画布圆角动态调整** — 编辑器设置面板新增画布圆角滑块(0~80px)+ 4个预设(直角/小圆角/圆角/大圆角),圆角实时生效并持久化到 SharedPreferences,导出图片自动带圆角
|
||||
|
||||
### 📁 修改文件
|
||||
|
||||
- `lib/editor/pages/editor/pro_editor_page.dart` — 描边坐标两步转换 + selectedLayerNotifier回退 + canvasBorderRadius状态 + onToolDrawer
|
||||
- `lib/editor/widgets/controls/editor_top_nav.dart` — 新增 onToolDrawer 回调 + sidebar_left 按钮
|
||||
- `lib/editor/widgets/panels/editor_settings_sheet.dart` — 新增画布圆角滑块+预设(StatefulWidget化)
|
||||
- `lib/editor/services/core/editor_settings_service.dart` — 新增 loadCanvasRadius/saveCanvasRadius
|
||||
- `lib/editor/services/core/pro_editor_bridge.dart` — buildConfigs 接受动态 canvasBorderRadius
|
||||
- `lib/shared/widgets/wallpaper_gallery/wallpaper_gallery_view.dart` — 全部源分页加载+去重+无限下拉
|
||||
|
||||
---
|
||||
|
||||
## [3.9.7] - 2026-05-05
|
||||
|
||||
### 🐛 Bug修复(画布圆角显示+导出修复+401修复)
|
||||
|
||||
1. **画布圆角显示修复** — 之前 `ClipRRect` 裁剪的是整个 `ContentRecorder` 区域(包含图片周围空白),圆角只在空白区域可见,图片本身没有圆角。改为 `Center` + `SizedBox(decodedImageSize)` + `ClipRRect` 限制裁剪区域为图片实际大小,使圆角正确显示在图片四角
|
||||
2. **导出图片圆角修复** — `ClipRRect` 在 `ContentRecorder` 内部,`ExtendedRepaintBoundary.toImage()` 截图时已包含圆角裁剪效果,导出图片自动带圆角(PNG 透明/JPEG 白色填充)。新增 `ExportService.applyRoundedCorners()` 备用方法
|
||||
3. **文字编辑内容回写** — 点击文本按钮时,输入框显示编辑器中已有文本;点击完成后,编辑器显示编辑后的文本
|
||||
4. **拖拽描边修复** — `_overlayStackKey` 仅在拖拽时存在导致 null context,改为 overlay Stack 始终存在、条件渲染 DragBorderOverlay
|
||||
5. **Feed互动401修复** — 未登录用户执行 like/favorite/share 等互动操作时,API 返回 401。在 `FeedService.action` 中增加登录检查,未登录时跳过 API 请求直接返回 false(view 操作除外)
|
||||
|
||||
### 📁 修改文件
|
||||
|
||||
- `packages/pro_image_editor/lib/features/main_editor/widgets/main_editor_interactive_content.dart` — Center+SizedBox+ClipRRect 限制裁剪区域
|
||||
- `packages/pro_image_editor/lib/core/models/styles/main_editor_style.dart` — canvasBorderRadius 属性
|
||||
- `lib/editor/services/export/export_service.dart` — 新增 applyRoundedCorners 备用方法
|
||||
- `lib/editor/mixins/editor_export_actions.dart` — 移除双重圆角调用
|
||||
- `lib/editor/pages/editor/pro_editor_page.dart` — 文本编辑内容回写 + DragBorderOverlay 修复
|
||||
- `lib/features/home/services/feed_service.dart` — action方法增加登录检查避免401
|
||||
|
||||
---
|
||||
|
||||
## [3.9.6] - 2026-05-05
|
||||
|
||||
### 🐛 Bug修复(编辑器三大问题修复)
|
||||
|
||||
1. **画布圆角修复(最终版)** — 圆角之前加在了画布下层的白色背景区域,画布本身没有圆角。在 `MainEditorStyle` 新增 `canvasBorderRadius` 属性,在 `ContentRecorder` 内部用 `ClipRRect` 裁剪画布内容(图像+图层),背景层保持无圆角
|
||||
2. **文字按钮卡死闪退(根因修复)** — `flutter_quill` 的 `raw_editor_state.dart` 中 `renderEditor` getter 使用 `_editorKey.currentContext!` 空断言,当 widget 未挂载时崩溃。改为 null 检查 + `StateError`;`_updateOrDisposeSelectionOverlayIfNeeded`/`_showCaretOnScreen`/`contextMenuAnchors`/`_getGlyphHeights` 增加 `currentContext == null` 提前返回保护;`RichTextEditorPanel` 添加 `Material` 祖先部件(解决 `QuillSimpleToolbar` 的 `IconButton`/`Theme.of` 需要 Material 祖先);`autoFocus` 改为延迟生效(`_editorReady` 标志),避免 widget 未构建完成时触发焦点请求
|
||||
3. **描述线条位置不一致** — `DragBorderOverlay` 的坐标使用 `renderBox.localToGlobal(Offset.zero)` 返回屏幕全局坐标,但 overlay 放在 `bodyItems` 的 `Stack` 中,坐标系受外层 `Padding`/`SafeArea`/`Scaffold` 偏移影响。改为 `localToGlobal(Offset.zero, ancestor: overlayBox)` 将坐标转换为 overlay Stack 的本地坐标,消除偏移
|
||||
|
||||
### 📁 修改文件
|
||||
|
||||
- `packages/pro_image_editor/lib/core/models/styles/main_editor_style.dart` — 新增 `canvasBorderRadius`
|
||||
- `packages/pro_image_editor/lib/features/main_editor/widgets/main_editor_interactive_content.dart` — ContentRecorder 内部 ClipRRect
|
||||
- `packages/flutter_quill/lib/src/editor/raw_editor/raw_editor_state.dart` — renderEditor 空安全加固
|
||||
- `lib/editor/widgets/panels/rich_text_editor_panel.dart` — Material 祖先 + 延迟 autoFocus
|
||||
- `lib/editor/pages/editor/pro_editor_page.dart` — DragBorderOverlay 坐标转换修复
|
||||
|
||||
---
|
||||
|
||||
## [3.9.5] - 2026-05-05
|
||||
|
||||
### 🐛 Bug修复(回归修复 Round 4)
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// ============================================================
|
||||
// ============================================================
|
||||
// 闲言APP — 编辑器操作基础 Mixin
|
||||
// 创建时间: 2026-04-25
|
||||
// 更新时间: 2026-04-25
|
||||
@@ -14,6 +14,7 @@ import 'package:pro_image_editor/pro_image_editor.dart' as pro;
|
||||
import 'package:xianyan/editor/services/core/layer_manager_service.dart';
|
||||
import 'package:xianyan/editor/widgets/editors/gradient_editor.dart';
|
||||
import 'package:xianyan/editor/pages/editor/pro_editor_page.dart';
|
||||
import 'package:pro_image_editor/core/models/canvas_style_model.dart';
|
||||
|
||||
mixin EditorActionsBase on State<ProEditorPage> {
|
||||
final _log = Logger(printer: PrettyPrinter(methodCount: 0));
|
||||
@@ -33,6 +34,7 @@ mixin EditorActionsBase on State<ProEditorPage> {
|
||||
GlobalKey get repaintKey;
|
||||
List<double> get snapGuidesX;
|
||||
List<double> get snapGuidesY;
|
||||
CanvasStyleModel get canvasStyle;
|
||||
set snapGuidesX(List<double> v);
|
||||
set snapGuidesY(List<double> v);
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
// 创建时间: 2026-04-25
|
||||
// 更新时间: 2026-04-25
|
||||
// 作用: 导出相关操作 — 保存/分享/GIF/动态照片/预览/裁剪/信息
|
||||
// 上次更新: 格式/质量选择生效 + 压缩导出 + 空指针防护 + 导出防重复
|
||||
// 上次更新: 导出前applyCanvasStyle后处理+格式/质量选择生效+压缩导出+空指针防护+导出防重复
|
||||
// ============================================================
|
||||
|
||||
import 'dart:typed_data';
|
||||
@@ -295,21 +295,32 @@ mixin EditorExportActions on EditorActionsBase {
|
||||
);
|
||||
|
||||
try {
|
||||
Uint8List exportBytes = bytes;
|
||||
final style = canvasStyle;
|
||||
if (style.hasBorder ||
|
||||
style.hasShadow ||
|
||||
style.hasStack ||
|
||||
style.borderRadius > 0 ||
|
||||
style.hasOuterMargin) {
|
||||
final styled = await ExportService.applyCanvasStyle(bytes, style);
|
||||
if (styled != null) exportBytes = styled;
|
||||
}
|
||||
|
||||
if (isShareMode) {
|
||||
await ExportService.shareCompressed(
|
||||
bytes,
|
||||
exportBytes,
|
||||
quality: config.quality,
|
||||
format: config.compressFormat,
|
||||
);
|
||||
if (mounted) showExportResult(success: true, bytes: bytes);
|
||||
if (mounted) showExportResult(success: true, bytes: exportBytes);
|
||||
} else {
|
||||
final result = await ExportService.saveCompressedToGallery(
|
||||
bytes,
|
||||
exportBytes,
|
||||
quality: config.quality,
|
||||
format: config.compressFormat,
|
||||
);
|
||||
actionsLog.i('saveCompressedToGallery result: $result');
|
||||
if (mounted) showExportResult(success: result, bytes: bytes);
|
||||
if (mounted) showExportResult(success: result, bytes: exportBytes);
|
||||
}
|
||||
} catch (e, stack) {
|
||||
actionsLog.e('Export failed: $e\n$stack');
|
||||
|
||||
@@ -3,9 +3,10 @@
|
||||
// 创建时间: 2026-04-23
|
||||
// 更新时间: 2026-05-05
|
||||
// 作用: ProImageEditor 的闲言APP包装页,全新iOS 26风格
|
||||
// 上次更新: 修复画布ClipRRect圆角+缩进修复+ProImageEditor参数对齐
|
||||
// 上次更新: 修复描边坐标偏移(globalToLocal两步转换)+文本编辑器用selectedLayerNotifier回退取TextLayer
|
||||
// ============================================================
|
||||
|
||||
import 'dart:convert';
|
||||
import 'dart:typed_data';
|
||||
|
||||
import 'package:flutter/cupertino.dart';
|
||||
@@ -26,11 +27,13 @@ import 'package:xianyan/editor/widgets/controls/editor_drawer_sheet.dart';
|
||||
import 'package:xianyan/editor/widgets/controls/snap_guide_overlay.dart';
|
||||
import 'package:xianyan/editor/widgets/editors/eye_dropper.dart';
|
||||
import 'package:xianyan/editor/widgets/editors/emoji_icon_grid.dart';
|
||||
import 'package:xianyan/editor/models/editor_models.dart'
|
||||
show StickerType, DragBorderStyle;
|
||||
import 'package:xianyan/editor/models/editor_models.dart' show DragBorderStyle;
|
||||
import 'package:xianyan/editor/services/core/editor_settings_service.dart';
|
||||
import 'package:xianyan/editor/widgets/controls/drag_border_overlay.dart';
|
||||
import 'package:xianyan/editor/widgets/panels/editor_settings_sheet.dart';
|
||||
import 'package:xianyan/editor/widgets/panels/canvas_style_sheet.dart';
|
||||
import 'package:pro_image_editor/core/models/canvas_style_model.dart'
|
||||
show CanvasStyleModel;
|
||||
import 'package:xianyan/editor/widgets/panels/rich_text_editor_panel.dart';
|
||||
import 'package:xianyan/shared/widgets/wallpaper_gallery/wallpaper_gallery.dart';
|
||||
import 'package:xianyan/editor/mixins/editor_actions_base.dart';
|
||||
@@ -82,8 +85,10 @@ class ProEditorPageState extends State<ProEditorPage>
|
||||
bool _showHistorySlider = true;
|
||||
bool _showThemePanel = false;
|
||||
DragBorderStyle _dragBorderStyle = DragBorderStyle.dashed;
|
||||
CanvasStyleModel _canvasStyle = CanvasStyleModel.defaults;
|
||||
bool _isLayerDragging = false;
|
||||
Rect? _draggingLayerRect;
|
||||
final _overlayStackKey = GlobalKey();
|
||||
|
||||
static const double _zoomHideThreshold = 1.05;
|
||||
final ValueNotifier<double> _zoomScaleNotifier = ValueNotifier(1.0);
|
||||
@@ -91,6 +96,8 @@ class ProEditorPageState extends State<ProEditorPage>
|
||||
|
||||
@override
|
||||
GlobalKey<pro.ProImageEditorState> get editorKey => _editorKey;
|
||||
@override
|
||||
CanvasStyleModel get canvasStyle => _canvasStyle;
|
||||
|
||||
@override
|
||||
GlobalKey get repaintKey => _repaintKey;
|
||||
@@ -133,7 +140,12 @@ class ProEditorPageState extends State<ProEditorPage>
|
||||
|
||||
void _loadSettings() async {
|
||||
final style = await EditorSettingsService.loadBorderStyle();
|
||||
if (mounted) setState(() => _dragBorderStyle = style);
|
||||
final canvasStyle = await EditorSettingsService.loadCanvasStyle();
|
||||
if (mounted)
|
||||
setState(() {
|
||||
_dragBorderStyle = style;
|
||||
_canvasStyle = canvasStyle;
|
||||
});
|
||||
}
|
||||
|
||||
void _scheduleInitialText() {
|
||||
@@ -332,15 +344,54 @@ class ProEditorPageState extends State<ProEditorPage>
|
||||
}
|
||||
|
||||
void _showRichTextEditor() {
|
||||
final editor = _editorKey.currentState;
|
||||
|
||||
final currentSelected = editor?.selectedLayer;
|
||||
final notifierLayer = selectedLayerNotifier.value;
|
||||
pro.TextLayer? textLayer;
|
||||
if (currentSelected is pro.TextLayer) {
|
||||
textLayer = currentSelected;
|
||||
} else if (notifierLayer is pro.TextLayer) {
|
||||
textLayer = notifierLayer;
|
||||
}
|
||||
|
||||
String? initialDeltaJson;
|
||||
if (textLayer != null) {
|
||||
final plainText = textLayer.text;
|
||||
if (plainText.isNotEmpty) {
|
||||
initialDeltaJson = jsonEncode([
|
||||
{'insert': plainText},
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
showCupertinoModalPopup<void>(
|
||||
context: context,
|
||||
builder: (_) => SizedBox(
|
||||
height: MediaQuery.of(context).size.height * 0.7,
|
||||
child: RichTextEditorPanel(
|
||||
onDone: () => Navigator.pop(context),
|
||||
initialDeltaJson: initialDeltaJson,
|
||||
onDone: () {
|
||||
Navigator.pop(context);
|
||||
},
|
||||
onChanged: (deltaJson) {
|
||||
final editor = _editorKey.currentState;
|
||||
if (editor == null) return;
|
||||
if (textLayer == null) 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();
|
||||
if (plainText.trim().isEmpty) return;
|
||||
textLayer.text = plainText;
|
||||
editorState.setState(() {});
|
||||
} catch (_) {}
|
||||
},
|
||||
),
|
||||
),
|
||||
@@ -355,13 +406,24 @@ class ProEditorPageState extends State<ProEditorPage>
|
||||
EditorSettingsSheet.show(
|
||||
context: context,
|
||||
borderStyle: _dragBorderStyle,
|
||||
onBorderStyleChanged: (style) {
|
||||
onBorderStyleChanged: (DragBorderStyle style) {
|
||||
setState(() => _dragBorderStyle = style);
|
||||
EditorSettingsService.saveBorderStyle(style);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
void _showCanvasStyleEditor() {
|
||||
CanvasStyleSheet.show(
|
||||
context: context,
|
||||
style: _canvasStyle,
|
||||
onChanged: (CanvasStyleModel newStyle) {
|
||||
setState(() => _canvasStyle = newStyle);
|
||||
EditorSettingsService.saveCanvasStyle(newStyle);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return ListenableBuilder(
|
||||
@@ -372,6 +434,7 @@ 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,
|
||||
@@ -388,79 +451,96 @@ class ProEditorPageState extends State<ProEditorPage>
|
||||
child: SizedBox.expand(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(8),
|
||||
child: ClipRRect(
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
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 renderBox =
|
||||
layer.key.currentContext?.findRenderObject()
|
||||
as RenderBox?;
|
||||
if (renderBox != null && renderBox.hasSize) {
|
||||
final size = renderBox.size;
|
||||
final offset = renderBox.localToGlobal(
|
||||
Offset.zero,
|
||||
);
|
||||
setState(() {
|
||||
_draggingLayerRect = offset & size;
|
||||
});
|
||||
}
|
||||
}
|
||||
},
|
||||
onScaleEnd: (_) {
|
||||
if (_isLayerDragging) {
|
||||
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(() {
|
||||
_isLayerDragging = false;
|
||||
_draggingLayerRect = null;
|
||||
_draggingLayerRect = localTopLeft & size;
|
||||
});
|
||||
}
|
||||
},
|
||||
onLongPress: () {
|
||||
EditorThemeNotifier.instance.toggleDarkLight();
|
||||
},
|
||||
onEditorZoomScaleStart: (_) {
|
||||
if (!_isPinching) {
|
||||
setState(() => _isPinching = true);
|
||||
}
|
||||
},
|
||||
onEditorZoomScaleEnd: (_) {
|
||||
if (_isPinching) {
|
||||
setState(() => _isPinching = false);
|
||||
}
|
||||
},
|
||||
onEditorZoomMatrix4Change: (matrix) {
|
||||
_zoomScaleNotifier.value = matrix
|
||||
.getMaxScaleOnAxis();
|
||||
},
|
||||
),
|
||||
}
|
||||
},
|
||||
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();
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
@@ -511,6 +591,8 @@ class ProEditorPageState extends State<ProEditorPage>
|
||||
onThemeToggle: _toggleThemePanel,
|
||||
onExport: () => handleExport(),
|
||||
onSettings: _showEditorSettings,
|
||||
onToolDrawer: _showToolDrawer,
|
||||
onCanvasStyle: _showCanvasStyleEditor,
|
||||
),
|
||||
),
|
||||
),
|
||||
@@ -544,20 +626,21 @@ class ProEditorPageState extends State<ProEditorPage>
|
||||
canvasSize: MediaQuery.of(context).size,
|
||||
),
|
||||
),
|
||||
if (_isLayerDragging && _draggingLayerRect != null)
|
||||
pro.ReactiveWidget(
|
||||
stream: rebuildStream,
|
||||
builder: (_) => SizedBox.expand(
|
||||
child: Stack(
|
||||
children: [
|
||||
pro.ReactiveWidget(
|
||||
stream: rebuildStream,
|
||||
builder: (_) => SizedBox.expand(
|
||||
key: _overlayStackKey,
|
||||
child: Stack(
|
||||
children: [
|
||||
if (_isLayerDragging && _draggingLayerRect != null)
|
||||
DragBorderOverlay(
|
||||
rect: _draggingLayerRect!,
|
||||
style: _dragBorderStyle,
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
if (!editor.isSubEditorOpen)
|
||||
pro.ReactiveWidget(
|
||||
stream: rebuildStream,
|
||||
|
||||
@@ -1,18 +1,22 @@
|
||||
// ============================================================
|
||||
// 闲言APP — 编辑器设置持久化服务
|
||||
// 创建时间: 2026-05-04
|
||||
// 更新时间: 2026-05-04
|
||||
// 更新时间: 2026-05-05
|
||||
// 作用: 编辑器偏好设置持久化存储
|
||||
// 上次更新: 初始创建 — 描边风格偏好
|
||||
// 上次更新: CanvasStyleModel整体持久化(替换单独canvasRadius)
|
||||
// ============================================================
|
||||
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
|
||||
import 'package:pro_image_editor/core/models/canvas_style_model.dart';
|
||||
import 'package:xianyan/editor/models/editor_models.dart';
|
||||
|
||||
class EditorSettingsService {
|
||||
EditorSettingsService._();
|
||||
static const _borderStyleKey = 'editor_drag_border_style';
|
||||
static const _canvasStyleKey = 'editor_canvas_style';
|
||||
|
||||
static SharedPreferences? _prefs;
|
||||
|
||||
@@ -35,4 +39,21 @@ class EditorSettingsService {
|
||||
final prefs = await _instance;
|
||||
await prefs.setString(_borderStyleKey, style.name);
|
||||
}
|
||||
|
||||
static Future<CanvasStyleModel> loadCanvasStyle() async {
|
||||
final prefs = await _instance;
|
||||
final saved = prefs.getString(_canvasStyleKey);
|
||||
if (saved == null) return CanvasStyleModel.defaults;
|
||||
try {
|
||||
final json = jsonDecode(saved) as Map<String, dynamic>;
|
||||
return CanvasStyleModel.fromJson(json);
|
||||
} catch (_) {
|
||||
return CanvasStyleModel.defaults;
|
||||
}
|
||||
}
|
||||
|
||||
static Future<void> saveCanvasStyle(CanvasStyleModel style) async {
|
||||
final prefs = await _instance;
|
||||
await prefs.setString(_canvasStyleKey, jsonEncode(style.toJson()));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
// 闲言APP — ProImageEditor 桥接层
|
||||
// 创建时间: 2026-04-23
|
||||
// 更新时间: 2026-05-05
|
||||
// 上次更新: MainEditorStyle.background用palette.bgCanvas + wrapBody去冗余ClipRRect(圆角由外层统一裁剪)
|
||||
// 上次更新: buildConfigs接受CanvasStyleModel(圆角+边框+阴影+叠层)
|
||||
// ============================================================
|
||||
|
||||
import 'dart:ui' as ui;
|
||||
@@ -17,6 +17,7 @@ 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';
|
||||
@@ -101,6 +102,7 @@ class ProEditorBridge {
|
||||
/// 构建 ProImageEditorConfigs — Cupertino + iOS风格 + 中文
|
||||
static pro.ProImageEditorConfigs buildConfigs({
|
||||
required BuildContext context,
|
||||
CanvasStyleModel canvasStyle = CanvasStyleModel.defaults,
|
||||
List<TextStyle>? additionalFonts,
|
||||
List<pro.ReactiveWidget> Function(
|
||||
pro.ProImageEditorState editor,
|
||||
@@ -170,6 +172,8 @@ class ProEditorBridge {
|
||||
),
|
||||
style: pro.MainEditorStyle(
|
||||
background: EditorThemeNotifier.instance.palette.bgCanvas,
|
||||
canvasBorderRadius: BorderRadius.circular(canvasStyle.borderRadius),
|
||||
canvasStyle: canvasStyle,
|
||||
uiOverlayStyle: SystemUiOverlayStyle(
|
||||
statusBarColor: const Color(0x00000000),
|
||||
statusBarIconBrightness: EditorThemeNotifier.instance.isDark
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
// ============================================================
|
||||
// 闲言APP — 导出服务
|
||||
// 创建时间: 2026-04-20
|
||||
// 更新时间: 2026-04-25
|
||||
// 更新时间: 2026-05-05
|
||||
// 作用: 截图导出 + 保存相册 + 系统分享 + WidgetLayer合成导出 + 压缩导出
|
||||
// 上次更新: 跨平台兼容 — 条件导入隔离dart:io
|
||||
// 上次更新: 新增 applyCanvasStyle 画布样式后处理(边框+阴影+叠层+圆角)
|
||||
// ============================================================
|
||||
|
||||
import 'dart:typed_data';
|
||||
@@ -21,6 +21,7 @@ import 'package:xianyan/editor/services/export/export_io_native.dart'
|
||||
if (dart.library.html) 'export_io_web.dart';
|
||||
import 'package:xianyan/editor/services/export/image_compress_service.dart';
|
||||
import 'package:xianyan/editor/services/image/widget_layer_renderer.dart';
|
||||
import 'package:pro_image_editor/core/models/canvas_style_model.dart';
|
||||
|
||||
class ExportService {
|
||||
ExportService._();
|
||||
@@ -247,4 +248,221 @@ class ExportService {
|
||||
await shareBytes(pngBytes);
|
||||
}
|
||||
}
|
||||
|
||||
static Future<Uint8List?> applyRoundedCorners(
|
||||
Uint8List imageBytes, {
|
||||
double radius = 20.0,
|
||||
}) async {
|
||||
try {
|
||||
final codec = await ui.instantiateImageCodec(imageBytes);
|
||||
final frame = await codec.getNextFrame();
|
||||
final image = frame.image;
|
||||
final width = image.width;
|
||||
final height = image.height;
|
||||
|
||||
final recorder = ui.PictureRecorder();
|
||||
final canvas = Canvas(recorder);
|
||||
|
||||
final rrect = RRect.fromRectAndRadius(
|
||||
Rect.fromLTWH(0, 0, width.toDouble(), height.toDouble()),
|
||||
Radius.circular(radius * (width / 1080)),
|
||||
);
|
||||
|
||||
canvas.clipRRect(rrect);
|
||||
canvas.drawImage(image, Offset.zero, Paint());
|
||||
|
||||
final picture = recorder.endRecording();
|
||||
final roundedImage = await picture.toImage(width, height);
|
||||
final byteData = await roundedImage.toByteData(
|
||||
format: ui.ImageByteFormat.png,
|
||||
);
|
||||
|
||||
image.dispose();
|
||||
roundedImage.dispose();
|
||||
|
||||
if (byteData == null) return null;
|
||||
return byteData.buffer.asUint8List();
|
||||
} catch (e) {
|
||||
Log.e('圆角处理失败', e);
|
||||
return imageBytes;
|
||||
}
|
||||
}
|
||||
|
||||
static Future<Uint8List?> applyCanvasStyle(
|
||||
Uint8List imageBytes,
|
||||
CanvasStyleModel style,
|
||||
) async {
|
||||
try {
|
||||
final codec = await ui.instantiateImageCodec(imageBytes);
|
||||
final frame = await codec.getNextFrame();
|
||||
final srcImage = frame.image;
|
||||
final srcW = srcImage.width.toDouble();
|
||||
final srcH = srcImage.height.toDouble();
|
||||
|
||||
final scale = srcW / 1080.0;
|
||||
final radius = style.borderRadius * scale;
|
||||
final borderWidth = style.borderWidth * scale;
|
||||
final shadowBlur = style.shadowBlur * scale;
|
||||
final shadowSpread = style.shadowSpread * scale;
|
||||
final shadowOffsetX = style.shadowOffsetX * scale;
|
||||
final shadowOffsetY =
|
||||
(style.shadowOffsetY != 0
|
||||
? style.shadowOffsetY
|
||||
: style.shadowBlur * 0.15) *
|
||||
scale;
|
||||
final stackDist = style.stackDistance * scale;
|
||||
|
||||
final hasBorder = style.hasBorder;
|
||||
final hasShadow = style.hasShadow;
|
||||
final hasStack = style.hasStack;
|
||||
final hasMargin = style.hasOuterMargin;
|
||||
|
||||
if (!hasBorder && !hasShadow && !hasStack && radius <= 0 && !hasMargin) {
|
||||
srcImage.dispose();
|
||||
return imageBytes;
|
||||
}
|
||||
|
||||
final autoPadding = hasShadow
|
||||
? shadowBlur + shadowSpread + 8 * scale
|
||||
: 0.0;
|
||||
final margin = hasMargin ? style.outerMargin * scale : autoPadding;
|
||||
|
||||
final stackExtra = hasStack ? stackDist * style.stackLayers : 0.0;
|
||||
double extraLeft = 0, extraTop = 0, extraRight = 0, extraBottom = 0;
|
||||
|
||||
if (hasStack) {
|
||||
final pos = style.stackPosition;
|
||||
if (pos == CanvasStackPosition.left) extraLeft = stackExtra;
|
||||
if (pos == CanvasStackPosition.right) extraRight = stackExtra;
|
||||
if (pos == CanvasStackPosition.top) extraTop = stackExtra;
|
||||
if (pos == CanvasStackPosition.bottom) extraBottom = stackExtra;
|
||||
}
|
||||
|
||||
final totalW = srcW + margin * 2 + extraLeft + extraRight;
|
||||
final totalH = srcH + margin * 2 + extraTop + extraBottom;
|
||||
final canvasW = totalW.toInt();
|
||||
final canvasH = totalH.toInt();
|
||||
|
||||
final recorder = ui.PictureRecorder();
|
||||
final canvas = Canvas(recorder);
|
||||
|
||||
if (hasMargin) {
|
||||
canvas.clipRect(
|
||||
Rect.fromLTWH(0, 0, canvasW.toDouble(), canvasH.toDouble()),
|
||||
);
|
||||
}
|
||||
|
||||
final imgX = margin + extraLeft;
|
||||
final imgY = margin + extraTop;
|
||||
final imgRect = Rect.fromLTWH(imgX, imgY, srcW, srcH);
|
||||
final imgRRect = RRect.fromRectAndRadius(
|
||||
imgRect,
|
||||
Radius.circular(radius),
|
||||
);
|
||||
|
||||
if (hasStack) {
|
||||
for (int i = style.stackLayers; i >= 1; i--) {
|
||||
double dx = 0, dy = 0;
|
||||
final offset = stackDist * i;
|
||||
if (style.stackPosition == CanvasStackPosition.left) dx = -offset;
|
||||
if (style.stackPosition == CanvasStackPosition.right) dx = offset;
|
||||
if (style.stackPosition == CanvasStackPosition.top) dy = -offset;
|
||||
if (style.stackPosition == CanvasStackPosition.bottom) dy = offset;
|
||||
|
||||
final layerRect = imgRect.translate(dx, dy);
|
||||
final layerRRect = RRect.fromRectAndRadius(
|
||||
layerRect,
|
||||
Radius.circular(radius),
|
||||
);
|
||||
|
||||
canvas.drawRRect(
|
||||
layerRRect,
|
||||
Paint()..color = const Color(0x99FFFFFF),
|
||||
);
|
||||
|
||||
if (hasBorder) {
|
||||
canvas.drawRRect(
|
||||
layerRRect,
|
||||
Paint()
|
||||
..color = style.borderColor.withValues(alpha: 0.2)
|
||||
..style = PaintingStyle.stroke
|
||||
..strokeWidth = borderWidth * 0.5,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (hasShadow) {
|
||||
final shadowRRect = imgRRect
|
||||
.shift(Offset(shadowOffsetX, shadowOffsetY))
|
||||
.inflate(shadowSpread);
|
||||
canvas.drawRRect(
|
||||
shadowRRect,
|
||||
Paint()
|
||||
..color = Colors.black.withValues(alpha: style.shadowOpacity)
|
||||
..maskFilter = MaskFilter.blur(BlurStyle.normal, shadowBlur),
|
||||
);
|
||||
}
|
||||
|
||||
canvas.save();
|
||||
canvas.clipRRect(imgRRect);
|
||||
canvas.drawImage(srcImage, Offset(imgX, imgY), Paint());
|
||||
canvas.restore();
|
||||
|
||||
if (hasBorder) {
|
||||
if (style.borderStyle == CanvasBorderStyle.dashed) {
|
||||
_drawDashedRRect(canvas, imgRRect, style.borderColor, borderWidth);
|
||||
} else {
|
||||
canvas.drawRRect(
|
||||
imgRRect,
|
||||
Paint()
|
||||
..color = style.borderColor
|
||||
..style = PaintingStyle.stroke
|
||||
..strokeWidth = borderWidth,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
final picture = recorder.endRecording();
|
||||
final resultImage = await picture.toImage(canvasW, canvasH);
|
||||
final byteData = await resultImage.toByteData(
|
||||
format: ui.ImageByteFormat.png,
|
||||
);
|
||||
|
||||
srcImage.dispose();
|
||||
resultImage.dispose();
|
||||
|
||||
if (byteData == null) return null;
|
||||
return byteData.buffer.asUint8List();
|
||||
} catch (e) {
|
||||
Log.e('画布样式处理失败', e);
|
||||
return imageBytes;
|
||||
}
|
||||
}
|
||||
|
||||
static void _drawDashedRRect(
|
||||
Canvas canvas,
|
||||
RRect rrect,
|
||||
Color color,
|
||||
double strokeWidth,
|
||||
) {
|
||||
final paint = Paint()
|
||||
..color = color
|
||||
..style = PaintingStyle.stroke
|
||||
..strokeWidth = strokeWidth;
|
||||
|
||||
const dashWidth = 8.0;
|
||||
const dashGap = 5.0;
|
||||
final path = Path()..addRRect(rrect);
|
||||
|
||||
final metrics = path.computeMetrics();
|
||||
for (final metric in metrics) {
|
||||
double distance = 0;
|
||||
while (distance < metric.length) {
|
||||
final end = (distance + dashWidth).clamp(0.0, metric.length);
|
||||
canvas.drawPath(metric.extractPath(distance, end), paint);
|
||||
distance += dashWidth + dashGap;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
// ============================================================
|
||||
// 闲言APP — 编辑器顶部导航栏
|
||||
// 创建时间: 2026-05-03
|
||||
|
||||
|
||||
// 更新时间: 2026-05-05
|
||||
// 作用: 3个浮动胶囊(左:关闭/保存/撤销/重做 中:图片信息 右:主题/导出)
|
||||
// 上次更新: 竖屏右栏换行第二行 + 横屏单行布局
|
||||
// 作用: 3个浮动胶囊(左:关闭/保存/撤销/重做 中:图片信息 右:工具抽屉/主题/导出)
|
||||
// 上次更新: 右侧胶囊增加工具抽屉按钮(onToolDrawer)
|
||||
// ============================================================
|
||||
|
||||
import 'dart:typed_data';
|
||||
@@ -31,6 +33,8 @@ class EditorTopNav extends StatefulWidget {
|
||||
required this.onThemeToggle,
|
||||
required this.onExport,
|
||||
this.onSettings,
|
||||
this.onToolDrawer,
|
||||
this.onCanvasStyle,
|
||||
});
|
||||
|
||||
final pro.ProImageEditorState editor;
|
||||
@@ -43,6 +47,8 @@ class EditorTopNav extends StatefulWidget {
|
||||
final VoidCallback onThemeToggle;
|
||||
final VoidCallback onExport;
|
||||
final VoidCallback? onSettings;
|
||||
final VoidCallback? onToolDrawer;
|
||||
final VoidCallback? onCanvasStyle;
|
||||
|
||||
@override
|
||||
State<EditorTopNav> createState() => _EditorTopNavState();
|
||||
@@ -72,9 +78,7 @@ class _EditorTopNavState extends State<EditorTopNav> {
|
||||
padding: EdgeInsets.only(top: widget.safeTop + 4, left: 12, right: 12),
|
||||
child: Align(
|
||||
alignment: Alignment.topCenter,
|
||||
child: isLandscape
|
||||
? _buildLandscapeLayout(p)
|
||||
: _buildPortraitLayout(p),
|
||||
child: isLandscape ? _buildLandscapeLayout(p) : _buildPortraitLayout(p),
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -82,11 +86,7 @@ class _EditorTopNavState extends State<EditorTopNav> {
|
||||
Widget _buildLandscapeLayout(EditorPalette p) {
|
||||
return Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
_buildLeftPill(p),
|
||||
_buildCenterPill(p),
|
||||
_buildRightPill(p),
|
||||
],
|
||||
children: [_buildLeftPill(p), _buildCenterPill(p), _buildRightPill(p)],
|
||||
);
|
||||
}
|
||||
|
||||
@@ -97,16 +97,10 @@ class _EditorTopNavState extends State<EditorTopNav> {
|
||||
children: [
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
_buildLeftPill(p),
|
||||
_buildCenterPill(p),
|
||||
],
|
||||
children: [_buildLeftPill(p), _buildCenterPill(p)],
|
||||
),
|
||||
const SizedBox(height: 6),
|
||||
Align(
|
||||
alignment: Alignment.centerRight,
|
||||
child: _buildRightPill(p),
|
||||
),
|
||||
Align(alignment: Alignment.centerRight, child: _buildRightPill(p)),
|
||||
],
|
||||
);
|
||||
}
|
||||
@@ -165,6 +159,30 @@ class _EditorTopNavState extends State<EditorTopNav> {
|
||||
return _NavPill(
|
||||
palette: p,
|
||||
children: [
|
||||
if (widget.onToolDrawer != null)
|
||||
_NavBtn(
|
||||
icon: EditorIcon.cupertino(
|
||||
'sidebar_left',
|
||||
size: 16,
|
||||
color: p.textPrimary,
|
||||
),
|
||||
semanticsLabel: '工具抽屉',
|
||||
onTap: widget.onToolDrawer!,
|
||||
palette: p,
|
||||
),
|
||||
if (widget.onToolDrawer != null) _PillDivider(palette: p),
|
||||
if (widget.onCanvasStyle != null)
|
||||
_NavBtn(
|
||||
icon: EditorIcon.cupertino(
|
||||
'rectangle',
|
||||
size: 16,
|
||||
color: p.textPrimary,
|
||||
),
|
||||
semanticsLabel: '画布样式',
|
||||
onTap: widget.onCanvasStyle!,
|
||||
palette: p,
|
||||
),
|
||||
if (widget.onCanvasStyle != null) _PillDivider(palette: p),
|
||||
_NavBtn(
|
||||
icon: EditorIcon.svg('moon', size: 16, color: p.textPrimary),
|
||||
semanticsLabel: '切换主题',
|
||||
|
||||
1171
lib/editor/widgets/panels/canvas_style_sheet.dart
Normal file
1171
lib/editor/widgets/panels/canvas_style_sheet.dart
Normal file
File diff suppressed because it is too large
Load Diff
@@ -1,16 +1,14 @@
|
||||
// ============================================================
|
||||
// 闲言APP — 编辑器设置面板
|
||||
// 创建时间: 2026-05-04
|
||||
// 更新时间: 2026-05-04
|
||||
// 作用: 编辑器设置面板 — 描边风格切换等
|
||||
// 上次更新: 初始创建
|
||||
// 更新时间: 2026-05-05
|
||||
// 作用: 编辑器设置面板 — 描边风格切换
|
||||
// 上次更新: 画布圆角/边框/阴影/叠层移至CanvasStyleSheet独立文件
|
||||
// ============================================================
|
||||
|
||||
import 'package:flutter/cupertino.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:xianyan/core/theme/app_theme.dart';
|
||||
import 'package:xianyan/core/theme/app_spacing.dart';
|
||||
import 'package:xianyan/editor/models/editor_models.dart';
|
||||
import 'package:xianyan/editor/services/core/color_tokens.dart';
|
||||
import 'package:xianyan/editor/services/core/editor_theme_notifier.dart';
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
// 创建时间: 2026-05-04
|
||||
// 更新时间: 2026-05-05
|
||||
// 作用: 富文本编辑面板 — 工具栏+编辑区,实时预览
|
||||
// 上次更新: 空安全加固 + 异常保护防卡死 + 本地flutter_quill包引用
|
||||
// 上次更新: 添加Material祖先部件 + 延迟autoFocus防崩溃 + 空安全加固
|
||||
// ============================================================
|
||||
|
||||
import 'dart:convert';
|
||||
@@ -35,11 +35,17 @@ class _RichTextEditorPanelState extends State<RichTextEditorPanel> {
|
||||
final _focusNode = FocusNode();
|
||||
final _scrollController = ScrollController();
|
||||
bool _initError = false;
|
||||
bool _editorReady = false;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_initController();
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
if (mounted) {
|
||||
setState(() => _editorReady = true);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
void _initController() {
|
||||
@@ -60,8 +66,9 @@ class _RichTextEditorPanelState extends State<RichTextEditorPanel> {
|
||||
_controller = quill.QuillController.basic();
|
||||
}
|
||||
|
||||
_controller!.addListener(_onContentChanged);
|
||||
} catch (_) {
|
||||
_controller?.addListener(_onContentChanged);
|
||||
} catch (e) {
|
||||
debugPrint('[RichTextEditorPanel] _initController error: $e');
|
||||
if (mounted) {
|
||||
setState(() => _initError = true);
|
||||
}
|
||||
@@ -69,18 +76,20 @@ class _RichTextEditorPanelState extends State<RichTextEditorPanel> {
|
||||
}
|
||||
|
||||
void _onContentChanged() {
|
||||
if (_controller == null) return;
|
||||
final ctrl = _controller;
|
||||
if (ctrl == null) return;
|
||||
try {
|
||||
final delta = _controller!.document.toDelta();
|
||||
final delta = ctrl.document.toDelta();
|
||||
widget.onChanged?.call(jsonEncode(delta.toJson()));
|
||||
} catch (_) {}
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
if (_controller != null) {
|
||||
_controller!.removeListener(_onContentChanged);
|
||||
_controller!.dispose();
|
||||
final ctrl = _controller;
|
||||
if (ctrl != null) {
|
||||
ctrl.removeListener(_onContentChanged);
|
||||
ctrl.dispose();
|
||||
}
|
||||
_focusNode.dispose();
|
||||
_scrollController.dispose();
|
||||
@@ -95,48 +104,57 @@ class _RichTextEditorPanelState extends State<RichTextEditorPanel> {
|
||||
|
||||
final ext = AppTheme.ext(context);
|
||||
|
||||
return Container(
|
||||
decoration: BoxDecoration(
|
||||
color: ext.bgPrimary,
|
||||
borderRadius: const BorderRadius.vertical(top: Radius.circular(16)),
|
||||
),
|
||||
child: Column(
|
||||
children: [
|
||||
_buildHeader(ext),
|
||||
_buildToolbar(ext),
|
||||
const Divider(height: 0.5),
|
||||
Expanded(child: _buildEditor(ext)),
|
||||
],
|
||||
return Material(
|
||||
color: Colors.transparent,
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
color: ext.bgPrimary,
|
||||
borderRadius:
|
||||
const BorderRadius.vertical(top: Radius.circular(16)),
|
||||
),
|
||||
child: Column(
|
||||
children: [
|
||||
_buildHeader(ext),
|
||||
_buildToolbar(ext),
|
||||
const Divider(height: 0.5),
|
||||
Expanded(child: _buildEditor(ext)),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildErrorState() {
|
||||
return Container(
|
||||
decoration: const BoxDecoration(
|
||||
color: CupertinoColors.systemBackground,
|
||||
borderRadius: BorderRadius.vertical(top: Radius.circular(16)),
|
||||
),
|
||||
child: Center(
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
const Icon(
|
||||
CupertinoIcons.exclamationmark_circle,
|
||||
size: 48,
|
||||
color: CupertinoColors.systemGrey,
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
const Text(
|
||||
'编辑器初始化失败',
|
||||
style: TextStyle(fontSize: 16, color: CupertinoColors.systemGrey),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
CupertinoButton.filled(
|
||||
onPressed: () => Navigator.pop(context),
|
||||
child: const Text('关闭'),
|
||||
),
|
||||
],
|
||||
return Material(
|
||||
color: Colors.transparent,
|
||||
child: Container(
|
||||
decoration: const BoxDecoration(
|
||||
color: CupertinoColors.systemBackground,
|
||||
borderRadius:
|
||||
BorderRadius.vertical(top: Radius.circular(16)),
|
||||
),
|
||||
child: Center(
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
const Icon(
|
||||
CupertinoIcons.exclamationmark_circle,
|
||||
size: 48,
|
||||
color: CupertinoColors.systemGrey,
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
const Text(
|
||||
'编辑器初始化失败',
|
||||
style: TextStyle(
|
||||
fontSize: 16, color: CupertinoColors.systemGrey),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
CupertinoButton.filled(
|
||||
onPressed: () => Navigator.pop(context),
|
||||
child: const Text('关闭'),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
@@ -144,10 +162,12 @@ class _RichTextEditorPanelState extends State<RichTextEditorPanel> {
|
||||
|
||||
Widget _buildHeader(AppThemeExtension ext) {
|
||||
return Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 10),
|
||||
padding:
|
||||
const EdgeInsets.symmetric(horizontal: 16, vertical: 10),
|
||||
decoration: BoxDecoration(
|
||||
color: ext.bgSecondary,
|
||||
borderRadius: const BorderRadius.vertical(top: Radius.circular(16)),
|
||||
borderRadius:
|
||||
const BorderRadius.vertical(top: Radius.circular(16)),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
@@ -161,7 +181,8 @@ class _RichTextEditorPanelState extends State<RichTextEditorPanel> {
|
||||
),
|
||||
const Spacer(),
|
||||
CupertinoButton(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 4),
|
||||
padding:
|
||||
const EdgeInsets.symmetric(horizontal: 12, vertical: 4),
|
||||
minSize: 0,
|
||||
onPressed: () {
|
||||
_focusNode.unfocus();
|
||||
@@ -182,13 +203,14 @@ class _RichTextEditorPanelState extends State<RichTextEditorPanel> {
|
||||
}
|
||||
|
||||
Widget _buildToolbar(AppThemeExtension ext) {
|
||||
if (_controller == null) return const SizedBox.shrink();
|
||||
final ctrl = _controller;
|
||||
if (ctrl == null) return const SizedBox.shrink();
|
||||
|
||||
return Container(
|
||||
padding: const EdgeInsets.symmetric(vertical: 4),
|
||||
color: ext.bgSecondary,
|
||||
child: quill.QuillSimpleToolbar(
|
||||
controller: _controller!,
|
||||
controller: ctrl,
|
||||
config: quill.QuillSimpleToolbarConfig(
|
||||
toolbarSize: 36,
|
||||
showAlignmentButtons: true,
|
||||
@@ -222,7 +244,8 @@ class _RichTextEditorPanelState extends State<RichTextEditorPanel> {
|
||||
showFontFamily: false,
|
||||
showFontSize: false,
|
||||
multiRowsDisplay: true,
|
||||
sectionDividerColor: ext.textSecondary.withValues(alpha: 0.2),
|
||||
sectionDividerColor:
|
||||
ext.textSecondary.withValues(alpha: 0.2),
|
||||
buttonOptions: const quill.QuillSimpleToolbarButtonOptions(
|
||||
base: quill.QuillToolbarBaseButtonOptions(iconSize: 18),
|
||||
),
|
||||
@@ -232,22 +255,27 @@ class _RichTextEditorPanelState extends State<RichTextEditorPanel> {
|
||||
}
|
||||
|
||||
Widget _buildEditor(AppThemeExtension ext) {
|
||||
if (_controller == null) return const SizedBox.shrink();
|
||||
final ctrl = _controller;
|
||||
if (ctrl == null) return const SizedBox.shrink();
|
||||
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
|
||||
padding:
|
||||
const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
|
||||
child: quill.QuillEditor.basic(
|
||||
controller: _controller!,
|
||||
controller: ctrl,
|
||||
focusNode: _focusNode,
|
||||
scrollController: _scrollController,
|
||||
config: quill.QuillEditorConfig(
|
||||
padding: const EdgeInsets.all(8),
|
||||
autoFocus: true,
|
||||
autoFocus: _editorReady,
|
||||
expands: false,
|
||||
placeholder: '输入文本内容...',
|
||||
customStyles: quill.DefaultStyles(
|
||||
paragraph: quill.DefaultTextBlockStyle(
|
||||
TextStyle(color: ext.textPrimary, fontSize: 16, height: 1.6),
|
||||
TextStyle(
|
||||
color: ext.textPrimary,
|
||||
fontSize: 16,
|
||||
height: 1.6),
|
||||
const quill.HorizontalSpacing(0, 0),
|
||||
const quill.VerticalSpacing(6, 0),
|
||||
const quill.VerticalSpacing(0, 0),
|
||||
@@ -259,11 +287,13 @@ class _RichTextEditorPanelState extends State<RichTextEditorPanel> {
|
||||
);
|
||||
}
|
||||
|
||||
String get plainText => _controller?.document.toPlainText() ?? '';
|
||||
String get plainText =>
|
||||
_controller?.document.toPlainText() ?? '';
|
||||
|
||||
String get deltaJson => _controller != null
|
||||
? jsonEncode(_controller!.document.toDelta().toJson())
|
||||
: '[]';
|
||||
|
||||
String get htmlContent => _controller?.document.toPlainText() ?? '';
|
||||
String get htmlContent =>
|
||||
_controller?.document.toPlainText() ?? '';
|
||||
}
|
||||
|
||||
@@ -1,14 +1,15 @@
|
||||
// ============================================================
|
||||
// 闲言APP — Feed信息流API服务
|
||||
// 创建时间: 2026-04-28
|
||||
// 更新时间: 2026-04-29
|
||||
// 更新时间: 2026-05-05
|
||||
// 作用: 封装所有 /api/feed/* 接口调用
|
||||
// 上次更新: 新增 fetchComments/fetchStats/fetchRelatedRecommend 3个接口
|
||||
// 上次更新: action方法增加登录检查,未登录跳过API请求避免401
|
||||
// ============================================================
|
||||
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
|
||||
import '../../../core/network/api_client.dart';
|
||||
import '../../../core/storage/secure_storage.dart';
|
||||
import '../../../core/utils/logger.dart';
|
||||
import '../models/feed_model.dart';
|
||||
|
||||
@@ -114,6 +115,14 @@ class FeedService {
|
||||
required int feedId,
|
||||
String? extra,
|
||||
}) async {
|
||||
if (action != 'view') {
|
||||
final loggedIn = await SecureStorage.isLoggedIn;
|
||||
if (!loggedIn) {
|
||||
Log.w('Feed互动跳过: 未登录, action=$action');
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
final data = <String, dynamic>{
|
||||
'action': action,
|
||||
|
||||
@@ -3,8 +3,8 @@
|
||||
/// 创建时间: 2026-05-04
|
||||
/// 更新时间: 2026-05-05
|
||||
/// 作用: 壁纸图库公共视图 — 支持drawer(编辑器抽屉)/fullscreen(灵感页)双模式
|
||||
/// 统一数据源 + 瀑布流 + 已加载优先 + 分类"全部" + URL三级回退
|
||||
/// 上次更新: 默认全部12源加载(谁先好显示谁) + 异常保护防卡死
|
||||
/// 统一数据源 + 瀑布流 + 已加载优先 + 分类"全部" + URL三级回退 + 无限下拉加载
|
||||
/// 上次更新: 全部源模式支持无限下拉(轮询各源分页)+单源分页+去重
|
||||
/// ============================================================
|
||||
|
||||
import 'package:flutter/cupertino.dart';
|
||||
@@ -50,6 +50,8 @@ class _WallpaperGalleryViewState extends State<WallpaperGalleryView> {
|
||||
String? _selectedId;
|
||||
String _searchQuery = '';
|
||||
Set<String> _cachedIds = {};
|
||||
final Set<String> _loadedIds = {};
|
||||
int _allSourcesPageIndex = 1;
|
||||
final _searchController = TextEditingController();
|
||||
final _scrollController = ScrollController();
|
||||
|
||||
@@ -72,7 +74,9 @@ class _WallpaperGalleryViewState extends State<WallpaperGalleryView> {
|
||||
if (!_hasMore && !reset) return;
|
||||
if (reset) {
|
||||
_currentPage = 1;
|
||||
_allSourcesPageIndex = 1;
|
||||
_hasMore = true;
|
||||
_loadedIds.clear();
|
||||
}
|
||||
|
||||
setState(() => _isLoading = true);
|
||||
@@ -95,21 +99,49 @@ class _WallpaperGalleryViewState extends State<WallpaperGalleryView> {
|
||||
|
||||
Future<void> _loadAllSources({bool reset = true}) async {
|
||||
final allSources = WallpaperSource.values.toList();
|
||||
final page = _allSourcesPageIndex;
|
||||
|
||||
final items = await WallpaperService.fetchMultiSource(
|
||||
sources: allSources,
|
||||
limitPerSource: 4,
|
||||
category: _category,
|
||||
).timeout(const Duration(seconds: 20), onTimeout: () => <WallpaperItem>[]);
|
||||
final futures = allSources.map(
|
||||
(source) =>
|
||||
WallpaperService.fetchWallpapers(
|
||||
source: source,
|
||||
page: page,
|
||||
limit: 4,
|
||||
category: _category,
|
||||
).catchError(
|
||||
(_) =>
|
||||
const WallpaperResult(items: [], currentPage: 1, totalPages: 0),
|
||||
),
|
||||
);
|
||||
|
||||
final results = await Future.wait(futures, eagerError: false);
|
||||
if (!mounted) return;
|
||||
|
||||
final newItems = <WallpaperItem>[];
|
||||
for (final result in results) {
|
||||
for (final item in result.items) {
|
||||
if (!_loadedIds.contains(item.id)) {
|
||||
newItems.add(item);
|
||||
_loadedIds.add(item.id);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
newItems.shuffle();
|
||||
|
||||
final anyHasNext = results.any((r) => r.hasNext);
|
||||
final maxTotalPages = results
|
||||
.map((r) => r.totalPages)
|
||||
.reduce((a, b) => a > b ? a : b);
|
||||
|
||||
setState(() {
|
||||
if (reset) {
|
||||
_items = items;
|
||||
_items = newItems;
|
||||
} else {
|
||||
_items.addAll(items);
|
||||
_items.addAll(newItems);
|
||||
}
|
||||
_hasMore = false;
|
||||
_allSourcesPageIndex++;
|
||||
_hasMore = anyHasNext || _allSourcesPageIndex <= maxTotalPages;
|
||||
_isLoading = false;
|
||||
});
|
||||
_sortLoadedFirst();
|
||||
@@ -131,13 +163,21 @@ class _WallpaperGalleryViewState extends State<WallpaperGalleryView> {
|
||||
);
|
||||
|
||||
if (!mounted) return;
|
||||
|
||||
final newItems = result.items
|
||||
.where((item) => !_loadedIds.contains(item.id))
|
||||
.toList();
|
||||
for (final item in newItems) {
|
||||
_loadedIds.add(item.id);
|
||||
}
|
||||
|
||||
setState(() {
|
||||
if (reset) {
|
||||
_items = result.items;
|
||||
_items = newItems;
|
||||
} else {
|
||||
_items.addAll(result.items);
|
||||
_items.addAll(newItems);
|
||||
}
|
||||
_hasMore = result.hasNext;
|
||||
_hasMore = result.hasNext && newItems.isNotEmpty;
|
||||
_isLoading = false;
|
||||
_sortLoadedFirst();
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user