Files
xianyan/lib/editor/providers/editor_provider.dart
Developer 7564e8893d chore: 完成多平台适配与代码优化
此提交包含多项变更:
1. 新增鸿蒙平台支持,完善设备检测与数据库适配
2. 替换旧版分享插件API为SharePlus
3. 批量迁移StateNotifier到Notifier以适配新版Riverpod
4. 修复zip编码判断、图表API参数等bug
5. 更新应用图标、启动页资源与多尺寸适配图标
6. 调整Android最小SDK版本与应用名称
7. 优化日志打印与正则表达式使用
8. 修正编辑器画布样式初始化与配置逻辑
9. 更新依赖与CI插件配置
2026-05-17 07:17:07 +08:00

739 lines
22 KiB
Dart
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// ============================================================
// 闲言APP — 编辑器状态管理
// 创建时间: 2026-04-20
// 更新时间: 2026-04-24
// 作用: 使用 Riverpod StateNotifier 管理编辑器画布状态
// 上次更新: 撤销/重做增强 — 操作历史记录+描述
// ============================================================
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'dart:async';
import 'package:xianyan/core/network/api_client.dart';
import 'package:xianyan/core/network/api_exception.dart';
import 'package:xianyan/core/theme/app_colors.dart';
import 'package:xianyan/core/utils/logger.dart';
import 'package:xianyan/editor/models/editor_models.dart';
import 'package:xianyan/editor/services/export/draft_service.dart';
// ============================================================
// 操作历史记录
// ============================================================
/// 操作记录
class UndoAction {
const UndoAction({
required this.description,
required this.emoji,
required this.timestamp,
required this.snapshot,
});
final String description;
final String emoji;
final DateTime timestamp;
final QuoteCanvasModel snapshot;
}
// ============================================================
// 编辑器状态
// ============================================================
/// 编辑器状态
class EditorState {
const EditorState({
required this.canvas,
this.undoStack = const [],
this.redoStack = const [],
this.actionHistory = const [],
this.isPanelVisible = false,
this.activeTool = EditorTool.text,
this.isExporting = false,
this.isFetchingQuote = false,
});
final QuoteCanvasModel canvas;
final List<QuoteCanvasModel> undoStack;
final List<QuoteCanvasModel> redoStack;
final List<UndoAction> actionHistory;
final bool isPanelVisible;
final EditorTool activeTool;
final bool isExporting;
final bool isFetchingQuote;
bool get canUndo => undoStack.isNotEmpty;
bool get canRedo => redoStack.isNotEmpty;
EditorState copyWith({
QuoteCanvasModel? canvas,
List<QuoteCanvasModel>? undoStack,
List<QuoteCanvasModel>? redoStack,
List<UndoAction>? actionHistory,
bool? isPanelVisible,
EditorTool? activeTool,
bool? isExporting,
bool? isFetchingQuote,
}) {
return EditorState(
canvas: canvas ?? this.canvas,
undoStack: undoStack ?? this.undoStack,
redoStack: redoStack ?? this.redoStack,
actionHistory: actionHistory ?? this.actionHistory,
isPanelVisible: isPanelVisible ?? this.isPanelVisible,
activeTool: activeTool ?? this.activeTool,
isExporting: isExporting ?? this.isExporting,
isFetchingQuote: isFetchingQuote ?? this.isFetchingQuote,
);
}
}
// ============================================================
// 编辑器工具枚举 (7Tab)
// ============================================================
/// 编辑器工具
enum EditorTool {
text('📝', '文字'),
background('🖼️', '背景'),
effects('', '效果'),
watermark('💧', '水印'),
layers('📋', '图层'),
glass('🧊', '氢设计'),
annotation('✏️', '标注');
const EditorTool(this.emoji, this.label);
final String emoji;
final String label;
}
// ============================================================
// 编辑器 StateNotifier
// ============================================================
/// 编辑器状态控制器
class EditorNotifier extends Notifier<EditorState> {
@override
EditorState build() => EditorState(canvas: _buildInitialCanvas(null));
static const int _maxUndoDepth = 50;
Timer? _autoSaveTimer;
/// 自动保存草稿(防抖 800ms
void autoSaveDraft() {
_autoSaveTimer?.cancel();
_autoSaveTimer = Timer(const Duration(milliseconds: 800), () {
DraftService.save(state.canvas);
});
}
static QuoteCanvasModel _buildInitialCanvas(String? initialText) {
return QuoteCanvasModel(
background: const BackgroundLayer(
type: BackgroundType.gradient,
gradientColors: [LightColors.primary, LightColors.primaryDark],
gradientBegin: Alignment.topLeft,
gradientEnd: Alignment.bottomRight,
),
textLayers: [
TextLayer(
id: 'title',
text: initialText ?? '在此输入文字',
fontSize: 32.0,
fontWeight: FontWeight.bold,
color: Colors.white,
offsetY: -40.0,
),
const TextLayer(
id: 'author',
text: '—— 作者',
fontSize: 16.0,
color: Color(0xCCFFFFFF),
offsetY: 40.0,
),
],
selectedLayerId: 'title',
);
}
// ---- 撤销/重做 ----
void _pushUndo({String description = '编辑', String emoji = '✏️'}) {
final newStack = [...state.undoStack, state.canvas];
if (newStack.length > _maxUndoDepth) {
newStack.removeRange(0, newStack.length - _maxUndoDepth);
}
final newHistory = [
...state.actionHistory,
UndoAction(
description: description,
emoji: emoji,
timestamp: DateTime.now(),
snapshot: state.canvas,
),
];
if (newHistory.length > _maxUndoDepth) {
newHistory.removeRange(0, newHistory.length - _maxUndoDepth);
}
state = state.copyWith(
undoStack: newStack,
redoStack: const [],
actionHistory: newHistory,
);
}
void undo() {
if (!state.canUndo) return;
final previous = state.undoStack.last;
state = state.copyWith(
canvas: previous,
undoStack: state.undoStack.sublist(0, state.undoStack.length - 1),
redoStack: [...state.redoStack, state.canvas],
);
}
void redo() {
if (!state.canRedo) return;
final next = state.redoStack.last;
state = state.copyWith(
canvas: next,
redoStack: state.redoStack.sublist(0, state.redoStack.length - 1),
undoStack: [...state.undoStack, state.canvas],
);
}
// ---- 画布操作 ----
void loadTemplate(EditorTemplate template) {
_pushUndo(description: '加载模板', emoji: '📋');
state = state.copyWith(canvas: template.toCanvas());
}
void loadCanvas(QuoteCanvasModel canvas) {
_pushUndo(description: '加载画布', emoji: '📂');
final targetCanvas = canvas.textLayers.isNotEmpty
? canvas.copyWith(selectedLayerId: canvas.textLayers.first.id)
: canvas;
state = state.copyWith(canvas: targetCanvas);
}
void updateCanvasRatio(CanvasAspectRatio ratio) {
if (ratio == state.canvas.aspectRatio) return;
_pushUndo(description: '调整画布比例', emoji: '📐');
final oldW = state.canvas.canvasWidth;
final oldH = state.canvas.canvasHeight;
final newW = ratio.width;
final newH = ratio.height;
final scaleX = newW / oldW;
final scaleY = newH / oldH;
final scaledLayers = state.canvas.textLayers.map((l) {
return l.copyWith(
offsetX: l.offsetX * scaleX,
offsetY: l.offsetY * scaleY,
fontSize: l.fontSize * ((scaleX + scaleY) / 2).clamp(0.5, 2.0),
);
}).toList();
state = state.copyWith(
canvas: state.canvas.copyWith(
aspectRatio: ratio,
textLayers: scaledLayers,
),
);
autoSaveDraft();
}
void toggleGrid() {
state = state.copyWith(
canvas: state.canvas.copyWith(showGrid: !state.canvas.showGrid),
);
}
// ---- 背景操作 ----
void updateBackground(BackgroundLayer background) {
_pushUndo(description: '更新背景', emoji: '🖼️');
state = state.copyWith(
canvas: state.canvas.copyWith(background: background),
);
}
void updateBackgroundLive(BackgroundLayer background) {
state = state.copyWith(
canvas: state.canvas.copyWith(background: background),
);
}
void commitBackground(BackgroundLayer background) {
updateBackground(background);
autoSaveDraft();
}
// ---- 文字层操作 ----
void updateSelectedLayer(TextLayer updated) {
_pushUndo(description: '编辑文字');
final newLayers = state.canvas.textLayers.map((l) {
return l.id == updated.id ? updated : l;
}).toList();
state = state.copyWith(
canvas: state.canvas.copyWith(textLayers: newLayers),
);
}
void updateSelectedLayerLive(TextLayer updated) {
final newLayers = state.canvas.textLayers.map((l) {
return l.id == updated.id ? updated : l;
}).toList();
state = state.copyWith(
canvas: state.canvas.copyWith(textLayers: newLayers),
);
}
void commitSelectedLayer(TextLayer updated) {
updateSelectedLayer(updated);
autoSaveDraft();
}
void addTextLayer() {
_pushUndo(description: '添加文字', emoji: '📝');
final id = 'text_${DateTime.now().millisecondsSinceEpoch}';
const newLayer = TextLayer(
id: 'placeholder',
text: '新文字',
fontSize: 20.0,
color: Colors.white,
);
state = state.copyWith(
canvas: state.canvas.copyWith(
textLayers: [
...state.canvas.textLayers,
newLayer.copyWith(id: id),
],
selectedLayerId: id,
),
);
}
void removeTextLayer(String layerId) {
_pushUndo(description: '删除文字', emoji: '🗑️');
final newLayers = state.canvas.textLayers
.where((l) => l.id != layerId)
.toList();
state = state.copyWith(
canvas: state.canvas.copyWith(
textLayers: newLayers,
selectedLayerId: state.canvas.selectedLayerId == layerId
? null
: state.canvas.selectedLayerId,
),
);
}
void selectLayer(String? layerId) {
state = state.copyWith(
canvas: state.canvas.copyWith(selectedLayerId: layerId),
);
}
void updateLayerPosition(String layerId, double offsetX, double offsetY) {
final newLayers = state.canvas.textLayers.map((l) {
return l.id == layerId
? l.copyWith(offsetX: offsetX, offsetY: offsetY)
: l;
}).toList();
state = state.copyWith(
canvas: state.canvas.copyWith(textLayers: newLayers),
);
}
void commitLayerPosition(String layerId) {
final layer = state.canvas.textLayers
.where((l) => l.id == layerId)
.firstOrNull;
if (layer == null) return;
_pushUndo(description: '选择图层', emoji: '👆');
autoSaveDraft();
}
/// 更新文字层缩放值(双指缩放后调用)
void updateLayerScale(String layerId, double newScale) {
final newLayers = state.canvas.textLayers.map((l) {
return l.id == layerId ? l.copyWith(scale: newScale) : l;
}).toList();
state = state.copyWith(
canvas: state.canvas.copyWith(textLayers: newLayers),
);
autoSaveDraft();
}
// ---- 图层管理 ----
void toggleLayerVisibility(String layerId) {
_pushUndo(description: '切换可见性', emoji: '👁️');
final textIndex = state.canvas.textLayers.indexWhere(
(l) => l.id == layerId,
);
if (textIndex != -1) {
final newLayers = state.canvas.textLayers.map((l) {
return l.id == layerId ? l.copyWith(visible: !l.visible) : l;
}).toList();
state = state.copyWith(
canvas: state.canvas.copyWith(textLayers: newLayers),
);
return;
}
final glassIndex = state.canvas.glassCardLayers.indexWhere(
(c) => c.id == layerId,
);
if (glassIndex != -1) {
final newCards = state.canvas.glassCardLayers.map((c) {
return c.id == layerId ? c.copyWith(visible: !c.visible) : c;
}).toList();
state = state.copyWith(
canvas: state.canvas.copyWith(glassCardLayers: newCards),
);
return;
}
final stickerIndex = state.canvas.stickerLayers.indexWhere(
(s) => s.id == layerId,
);
if (stickerIndex != -1) {
final newStickers = state.canvas.stickerLayers.map((s) {
return s.id == layerId ? s.copyWith(visible: !s.visible) : s;
}).toList();
state = state.copyWith(
canvas: state.canvas.copyWith(stickerLayers: newStickers),
);
return;
}
}
void toggleLayerLock(String layerId) {
_pushUndo(description: '切换锁定', emoji: '🔒');
final textIndex = state.canvas.textLayers.indexWhere(
(l) => l.id == layerId,
);
if (textIndex != -1) {
final newLayers = state.canvas.textLayers.map((l) {
return l.id == layerId ? l.copyWith(locked: !l.locked) : l;
}).toList();
state = state.copyWith(
canvas: state.canvas.copyWith(textLayers: newLayers),
);
return;
}
final glassIndex = state.canvas.glassCardLayers.indexWhere(
(c) => c.id == layerId,
);
if (glassIndex != -1) {
final newCards = state.canvas.glassCardLayers.map((c) {
return c.id == layerId ? c.copyWith(locked: !c.locked) : c;
}).toList();
state = state.copyWith(
canvas: state.canvas.copyWith(glassCardLayers: newCards),
);
return;
}
final stickerIndex = state.canvas.stickerLayers.indexWhere(
(s) => s.id == layerId,
);
if (stickerIndex != -1) {
final newStickers = state.canvas.stickerLayers.map((s) {
return s.id == layerId ? s.copyWith(locked: !s.locked) : s;
}).toList();
state = state.copyWith(
canvas: state.canvas.copyWith(stickerLayers: newStickers),
);
return;
}
}
void reorderLayers(int oldIndex, int newIndex) {
_pushUndo(description: '调整图层顺序', emoji: '↕️');
final layers = List<TextLayer>.from(state.canvas.textLayers);
if (newIndex > oldIndex) newIndex -= 1;
final item = layers.removeAt(oldIndex);
layers.insert(newIndex, item);
final reordered = layers
.asMap()
.entries
.map((e) => e.value.copyWith(zIndex: e.key))
.toList();
state = state.copyWith(
canvas: state.canvas.copyWith(textLayers: reordered),
);
}
void duplicateLayer(String layerId) {
_pushUndo(description: '复制图层', emoji: '📋');
final layerIndex = state.canvas.textLayers.indexWhere(
(l) => l.id == layerId,
);
if (layerIndex == -1) return;
final layer = state.canvas.textLayers[layerIndex];
final newId = 'text_${DateTime.now().millisecondsSinceEpoch}';
final duplicated = layer.copyWith(
id: newId,
offsetX: layer.offsetX + 20,
offsetY: layer.offsetY + 20,
);
state = state.copyWith(
canvas: state.canvas.copyWith(
textLayers: [...state.canvas.textLayers, duplicated],
selectedLayerId: newId,
),
);
}
// ---- 贴纸操作 ----
void addStickerLayer(StickerLayer sticker) {
_pushUndo(description: '添加贴纸', emoji: '🎭');
state = state.copyWith(
canvas: state.canvas.copyWith(
stickerLayers: [...state.canvas.stickerLayers, sticker],
),
);
}
void removeStickerLayer(String stickerId) {
_pushUndo(description: '删除贴纸', emoji: '🗑️');
final newStickers = state.canvas.stickerLayers
.where((s) => s.id != stickerId)
.toList();
state = state.copyWith(
canvas: state.canvas.copyWith(stickerLayers: newStickers),
);
}
void updateStickerPosition(String stickerId, double offsetX, double offsetY) {
final newStickers = state.canvas.stickerLayers.map((s) {
return s.id == stickerId
? s.copyWith(offsetX: offsetX, offsetY: offsetY)
: s;
}).toList();
state = state.copyWith(
canvas: state.canvas.copyWith(stickerLayers: newStickers),
);
}
void updateStickerScale(String stickerId, double scale) {
_pushUndo(description: '缩放贴纸', emoji: '🔍');
final newStickers = state.canvas.stickerLayers.map((s) {
return s.id == stickerId ? s.copyWith(scale: scale) : s;
}).toList();
state = state.copyWith(
canvas: state.canvas.copyWith(stickerLayers: newStickers),
);
}
void updateStickerRotation(String stickerId, double rotation) {
_pushUndo(description: '旋转贴纸', emoji: '🔄');
final newStickers = state.canvas.stickerLayers.map((s) {
return s.id == stickerId ? s.copyWith(rotation: rotation) : s;
}).toList();
state = state.copyWith(
canvas: state.canvas.copyWith(stickerLayers: newStickers),
);
}
// ---- 液态玻璃 (Liquid Glass) ----
void toggleLiquidGlassMode() {
state = state.copyWith(
canvas: state.canvas.copyWith(
isLiquidGlassMode: !state.canvas.isLiquidGlassMode,
),
);
}
void addGlassCardLayer(GlassStyle style) {
_pushUndo(description: '添加玻璃卡片', emoji: '💎');
final card = GlassCardLayer(
id: 'glass_${DateTime.now().millisecondsSinceEpoch}',
style: style,
offsetX: (state.canvas.canvasWidth - 200) / 2,
offsetY: (state.canvas.canvasHeight - 150) / 2,
);
state = state.copyWith(
canvas: state.canvas.copyWith(
glassCardLayers: [...state.canvas.glassCardLayers, card],
isLiquidGlassMode: true,
),
);
selectLayer(card.id);
}
void removeGlassCardLayer(String cardId) {
_pushUndo(description: '删除玻璃卡片', emoji: '🗑️');
final newCards = state.canvas.glassCardLayers
.where((c) => c.id != cardId)
.toList();
state = state.copyWith(
canvas: state.canvas.copyWith(glassCardLayers: newCards),
);
if (state.canvas.selectedLayerId == cardId) {
selectLayer(null);
}
}
void updateGlassCardPosition(String cardId, double offsetX, double offsetY) {
final newCards = state.canvas.glassCardLayers.map((c) {
return c.id == cardId
? c.copyWith(offsetX: offsetX, offsetY: offsetY)
: c;
}).toList();
state = state.copyWith(
canvas: state.canvas.copyWith(glassCardLayers: newCards),
);
}
void updateGlassCard(GlassCardLayer updated) {
_pushUndo(description: '编辑玻璃卡片', emoji: '💎');
final newCards = state.canvas.glassCardLayers.map((c) {
return c.id == updated.id ? updated : c;
}).toList();
state = state.copyWith(
canvas: state.canvas.copyWith(glassCardLayers: newCards),
);
}
/// 加载液态玻璃预设模板
void loadGlassPresetTemplate(EditorTemplate template) {
_pushUndo(description: '加载玻璃预设', emoji: '🪟');
state = state.copyWith(
canvas: template.toCanvas().copyWith(isLiquidGlassMode: true),
);
}
// ----
// ---- 标注层 ----
/// 添加标注层
void addAnnotationLayer(AnnotationLayer annotation) {
_pushUndo(description: '添加标注', emoji: '📌');
state = state.copyWith(
canvas: state.canvas.copyWith(
annotationLayers: [...state.canvas.annotationLayers, annotation],
),
);
}
/// 删除标注层
void removeAnnotationLayer(String annotationId) {
_pushUndo(description: '删除标注', emoji: '🗑️');
state = state.copyWith(
canvas: state.canvas.copyWith(
annotationLayers: state.canvas.annotationLayers
.where((a) => a.id != annotationId)
.toList(),
),
);
}
/// 更新标注层
void updateAnnotationLayer(AnnotationLayer updated) {
final newAnnotations = state.canvas.annotationLayers.map((a) {
return a.id == updated.id ? updated : a;
}).toList();
state = state.copyWith(
canvas: state.canvas.copyWith(annotationLayers: newAnnotations),
);
}
/// 清除所有标注层
void clearAnnotationLayers() {
_pushUndo(description: '清除标注', emoji: '🧹');
state = state.copyWith(canvas: state.canvas.copyWith(annotationLayers: []));
}
// ----
// ---- 水印 ----
void updateWatermark(WatermarkStyle style, {String? customText}) {
_pushUndo(description: '更新水印', emoji: '💧');
state = state.copyWith(
canvas: state.canvas.copyWith(
watermarkStyle: style,
watermarkText: customText ?? state.canvas.watermarkText,
),
);
}
// ---- 面板控制 ----
void togglePanel({EditorTool? tool}) {
final newTool = tool ?? state.activeTool;
final newVisible = tool != null ? true : !state.isPanelVisible;
final shouldClose =
tool != null && tool == state.activeTool && state.isPanelVisible;
state = state.copyWith(
isPanelVisible: shouldClose ? false : newVisible,
activeTool: newTool,
);
}
void hidePanel() {
state = state.copyWith(isPanelVisible: false);
}
// ---- 导出 ----
void setExporting(bool exporting) {
state = state.copyWith(isExporting: exporting);
}
// ---- 调试:一言句子 ----
Future<void> fetchHitokotoForEditorTest() async {
if (state.isFetchingQuote) return;
state = state.copyWith(isFetchingQuote: true);
try {
final resp = await ApiClient.instance.dio.get<Map<String, dynamic>>(
'https://v1.hitokoto.cn/',
queryParameters: const {'encode': 'json'},
);
final data = resp.data;
if (data == null) throw const ApiException(code: -8, message: '一言返回为空');
final hitokoto = (data['hitokoto'] as String?)?.trim();
final from = (data['from'] as String?)?.trim();
final fromWho = (data['from_who'] as String?)?.trim();
if (hitokoto == null || hitokoto.isEmpty)
throw const ApiException(code: -9, message: '一言返回缺少正文');
final authorText = [
if (fromWho != null && fromWho.isNotEmpty) fromWho,
if (from != null && from.isNotEmpty) from,
].join(' · ');
_pushUndo(description: '加载一言', emoji: '💬');
final newLayers = state.canvas.textLayers.map((l) {
if (l.id == 'title') return l.copyWith(text: hitokoto);
if (l.id == 'author')
return l.copyWith(text: authorText.isEmpty ? '——' : '—— $authorText');
return l;
}).toList();
state = state.copyWith(
canvas: state.canvas.copyWith(
textLayers: newLayers,
selectedLayerId: 'title',
),
);
} on ApiException catch (e) {
Log.e('一言接口业务异常: ${e.message}');
} catch (e) {
Log.e('一言接口网络异常', e);
} finally {
state = state.copyWith(isFetchingQuote: false);
}
}
}
// ============================================================
// Provider
// ============================================================
final editorProvider = NotifierProvider<EditorNotifier, EditorState>(
EditorNotifier.new,
);