此提交包含多项变更: 1. 新增鸿蒙平台支持,完善设备检测与数据库适配 2. 替换旧版分享插件API为SharePlus 3. 批量迁移StateNotifier到Notifier以适配新版Riverpod 4. 修复zip编码判断、图表API参数等bug 5. 更新应用图标、启动页资源与多尺寸适配图标 6. 调整Android最小SDK版本与应用名称 7. 优化日志打印与正则表达式使用 8. 修正编辑器画布样式初始化与配置逻辑 9. 更新依赖与CI插件配置
739 lines
22 KiB
Dart
739 lines
22 KiB
Dart
// ============================================================
|
||
// 闲言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,
|
||
);
|