Files
xianyan/lib/editor/services/export/draft_service.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

627 lines
21 KiB
Dart
Raw 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-05-16
// 作用: 基于 SharedPreferences 的本地草稿管理 + 缩略图生成
// 上次更新: 集成ImageCompressService缩略图生成
// ============================================================
import 'dart:convert';
import 'dart:typed_data';
import 'package:flutter/material.dart';
import 'package:pro_image_editor/pro_image_editor.dart' as pro;
import 'package:shared_preferences/shared_preferences.dart';
import 'package:xianyan/editor/models/editor_models.dart';
import 'package:xianyan/editor/services/export/image_compress_service.dart';
/// 草稿数据模型
class DraftItem {
final String id;
final String data;
final String hash;
final DateTime createdAt;
final Map<String, dynamic> preview;
final String? thumbnailBase64;
const DraftItem({
required this.id,
required this.data,
required this.hash,
required this.createdAt,
required this.preview,
this.thumbnailBase64,
});
factory DraftItem.fromJson(Map<String, dynamic> json) {
return DraftItem(
id: json['id'] as String,
data: json['data'] as String,
hash: json['hash'] as String,
createdAt: DateTime.parse(json['createdAt'] as String),
preview: Map<String, dynamic>.from(json['preview'] as Map),
thumbnailBase64: json['thumbnailBase64'] as String?,
);
}
Map<String, dynamic> toJson() => {
'id': id,
'data': data,
'hash': hash,
'createdAt': createdAt.toIso8601String(),
'preview': preview,
if (thumbnailBase64 != null) 'thumbnailBase64': thumbnailBase64,
};
Uint8List? get thumbnailBytes {
if (thumbnailBase64 == null) return null;
try {
return base64Decode(thumbnailBase64!);
} catch (_) {
return null;
}
}
}
/// 草稿服务 — 基于 SharedPreferences 的本地草稿管理
class DraftService {
static const _draftKey = 'editor_drafts';
static const _maxDrafts = 20;
/// 保存草稿(自动去重:相同内容不重复存储)
static Future<void> save(QuoteCanvasModel canvas) async {
try {
final prefs = await SharedPreferences.getInstance();
final drafts = await getAll();
final canvasJson = _canvasToJson(canvas);
final hash = _hashCanvas(canvas);
drafts.removeWhere((d) => d.hash == hash);
drafts.insert(
0,
DraftItem(
id: DateTime.now().millisecondsSinceEpoch.toString(),
data: canvasJson,
hash: hash,
createdAt: DateTime.now(),
preview: _buildPreview(canvas),
),
);
if (drafts.length > _maxDrafts) drafts.removeLast();
await prefs.setString(
_draftKey,
jsonEncode(drafts.map((d) => d.toJson()).toList()),
);
} catch (_) {}
}
/// 保存草稿并生成缩略图
static Future<void> saveWithThumbnail(
QuoteCanvasModel canvas,
Uint8List imageBytes,
) async {
try {
final prefs = await SharedPreferences.getInstance();
final drafts = await getAll();
final canvasJson = _canvasToJson(canvas);
final hash = _hashCanvas(canvas);
String? thumbBase64;
final thumb = await ImageCompressService.compressThumbnail(imageBytes);
if (thumb != null) {
thumbBase64 = base64Encode(thumb);
}
drafts.removeWhere((d) => d.hash == hash);
drafts.insert(
0,
DraftItem(
id: DateTime.now().millisecondsSinceEpoch.toString(),
data: canvasJson,
hash: hash,
createdAt: DateTime.now(),
preview: _buildPreview(canvas),
thumbnailBase64: thumbBase64,
),
);
if (drafts.length > _maxDrafts) drafts.removeLast();
await prefs.setString(
_draftKey,
jsonEncode(drafts.map((d) => d.toJson()).toList()),
);
} catch (_) {}
}
/// 获取所有草稿
static Future<List<DraftItem>> getAll() async {
try {
final prefs = await SharedPreferences.getInstance();
final raw = prefs.getString(_draftKey);
if (raw == null || raw.isEmpty) return [];
final list = jsonDecode(raw) as List;
return list
.map((e) => DraftItem.fromJson(Map<String, dynamic>.from(e as Map)))
.toList();
} catch (_) {
return [];
}
}
/// 加载指定草稿为画布模型
static QuoteCanvasModel? loadCanvas(DraftItem draft) {
try {
final json = jsonDecode(draft.data) as Map<String, dynamic>;
return _jsonToCanvas(json);
} catch (_) {
return null;
}
}
/// 删除草稿
static Future<void> delete(String id) async {
try {
final drafts = await getAll();
drafts.removeWhere((d) => d.id == id);
final prefs = await SharedPreferences.getInstance();
if (drafts.isEmpty) {
await prefs.remove(_draftKey);
} else {
await prefs.setString(
_draftKey,
jsonEncode(drafts.map((d) => d.toJson()).toList()),
);
}
} catch (_) {}
}
/// 清空所有草稿
static Future<void> clearAll() async {
try {
final prefs = await SharedPreferences.getInstance();
await prefs.remove(_draftKey);
} catch (_) {}
}
/// 获取草稿数量
static Future<int> count() async {
final drafts = await getAll();
return drafts.length;
}
static String _canvasToJson(QuoteCanvasModel c) {
return jsonEncode({
'aspectRatio': c.aspectRatio.name,
'showGrid': c.showGrid,
'gridSize': c.gridSize,
'background': {
'type': c.background.type.name,
'solidColor': c.background.solidColor.toARGB32(),
'gradientColors': c.background.gradientColors
.map((e) => e.toARGB32())
.toList(),
'gradientStops': c.background.gradientStops,
'gradientAngle': c.background.gradientAngle,
'imagePath': c.background.imagePath,
'blurSigma': c.background.blurSigma,
'imageOpacity': c.background.imageOpacity,
},
'textLayers': c.textLayers
.map(
(l) => {
'id': l.id,
'text': l.text,
'fontSize': l.fontSize,
'fontWeight': l.fontWeight.value,
'fontFamily': l.fontFamily,
'color': l.color.toARGB32(),
'textAlign': l.textAlign.name,
'offsetX': l.offsetX,
'offsetY': l.offsetY,
'rotation': l.rotation,
'scale': l.scale,
'letterSpacing': l.letterSpacing,
'lineHeight': l.lineHeight,
'italic': l.italic,
'underline': l.underline,
'strikethrough': l.strikethrough,
'strokeColor': l.strokeColor?.toARGB32(),
'strokeWidth': l.strokeWidth,
'shadowColor': l.shadowColor?.toARGB32(),
'shadowBlur': l.shadowBlur,
'shadowOffsetX': l.shadowOffsetX,
'shadowOffsetY': l.shadowOffsetY,
'visible': l.visible,
'locked': l.locked,
'zIndex': l.zIndex,
},
)
.toList(),
'watermarkStyle': c.watermarkStyle.name,
'watermarkText': c.watermarkText,
'glassCardLayers': c.glassCardLayers
.map(
(g) => {
'id': g.id,
'style': g.style.name,
'offsetX': g.offsetX,
'offsetY': g.offsetY,
'width': g.width,
'height': g.height,
'rotation': g.rotation,
'scale': g.scale,
'borderRadius': g.borderRadius,
'blurIntensity': g.blurIntensity,
'opacity': g.opacity,
'borderOpacity': g.borderOpacity,
'highlightStrength': g.highlightStrength,
'showHighlight': g.showHighlight,
'showInnerShadow': g.showInnerShadow,
'tintColor': g.tintColor.toARGB32(),
'visible': g.visible,
'locked': g.locked,
'zIndex': g.zIndex,
'containedTextIds': g.containedTextIds,
'containedStickerIds': g.containedStickerIds,
},
)
.toList(),
'stickerLayers': c.stickerLayers
.map(
(s) => {
'id': s.id,
'type': s.type.name,
'assetPath': s.assetPath,
'size': s.size,
'offsetX': s.offsetX,
'offsetY': s.offsetY,
'rotation': s.rotation,
'scale': s.scale,
'opacity': s.opacity,
'visible': s.visible,
'locked': s.locked,
'zIndex': s.zIndex,
},
)
.toList(),
'annotationLayers': c.annotationLayers
.map(
(a) => {
'id': a.id,
'type': a.type.name,
'points': a.points.map((p) => {'x': p.x, 'y': p.y}).toList(),
'strokeWidth': a.strokeWidth,
'color': a.color.toARGB32(),
'opacity': a.opacity,
'visible': a.visible,
},
)
.toList(),
'isLiquidGlassMode': c.isLiquidGlassMode,
});
}
static QuoteCanvasModel _jsonToCanvas(Map<String, dynamic> json) {
final ratioName = json['aspectRatio'] as String? ?? 'ratio9_16';
final ratio = CanvasAspectRatio.values.firstWhere(
(r) => r.name == ratioName,
orElse: () => CanvasAspectRatio.ratio9_16,
);
final bg = json['background'] as Map<String, dynamic>? ?? {};
final bgType = BackgroundType.values.firstWhere(
(t) => t.name == (bg['type'] as String? ?? 'solid'),
orElse: () => BackgroundType.solid,
);
final colorsRaw = bg['gradientColors'] as List? ?? [];
final gradientColors = colorsRaw.map((e) => Color(e as int)).toList();
if (gradientColors.length < 2) {
gradientColors.insert(0, const Color(0xFF6C63FF));
gradientColors.add(const Color(0xFF4ECDC4));
}
final stopsRaw = bg['gradientStops'] as List? ?? [0.0, 1.0];
final gradientStops = stopsRaw.map((e) => (e as num).toDouble()).toList();
final layersRaw = json['textLayers'] as List? ?? [];
final textLayers = layersRaw.map((l) {
final map = l as Map<String, dynamic>;
return TextLayer(
id: map['id'] as String? ?? '',
text: map['text'] as String? ?? '',
fontSize: (map['fontSize'] as num?)?.toDouble() ?? 24.0,
fontWeight: _parseFontWeight(map['fontWeight']),
fontFamily: map['fontFamily'] as String? ?? 'Inter',
color: Color(map['color'] as int? ?? 0xFF1A1A2E),
textAlign: _parseTextAlign(map['textAlign']),
offsetX: (map['offsetX'] as num?)?.toDouble() ?? 0.0,
offsetY: (map['offsetY'] as num?)?.toDouble() ?? 0.0,
rotation: (map['rotation'] as num?)?.toDouble() ?? 0.0,
scale: (map['scale'] as num?)?.toDouble() ?? 1.0,
letterSpacing: (map['letterSpacing'] as num?)?.toDouble() ?? 0.0,
lineHeight: (map['lineHeight'] as num?)?.toDouble() ?? 1.5,
italic: map['italic'] as bool? ?? false,
underline: map['underline'] as bool? ?? false,
strikethrough: map['strikethrough'] as bool? ?? false,
strokeColor: map['strokeColor'] != null
? Color(map['strokeColor'] as int)
: null,
strokeWidth: (map['strokeWidth'] as num?)?.toDouble() ?? 0.0,
shadowColor: map['shadowColor'] != null
? Color(map['shadowColor'] as int)
: null,
shadowBlur: (map['shadowBlur'] as num?)?.toDouble() ?? 0.0,
shadowOffsetX: (map['shadowOffsetX'] as num?)?.toDouble() ?? 0.0,
shadowOffsetY: (map['shadowOffsetY'] as num?)?.toDouble() ?? 0.0,
visible: map['visible'] as bool? ?? true,
locked: map['locked'] as bool? ?? false,
zIndex: map['zIndex'] as int? ?? 0,
);
}).toList();
return QuoteCanvasModel(
aspectRatio: ratio,
showGrid: json['showGrid'] as bool? ?? false,
gridSize: (json['gridSize'] as num?)?.toDouble() ?? 40.0,
background: BackgroundLayer(
type: bgType,
solidColor: Color(bg['solidColor'] as int? ?? 0xFF6C63FF),
gradientColors: gradientColors,
gradientStops: gradientStops,
gradientAngle: (bg['gradientAngle'] as num?)?.toDouble() ?? 135.0,
imagePath: bg['imagePath'] as String?,
blurSigma: (bg['blurSigma'] as num?)?.toDouble() ?? 0.0,
imageOpacity: (bg['imageOpacity'] as num?)?.toDouble() ?? 1.0,
),
textLayers: textLayers,
glassCardLayers: _jsonToGlassCards(json['glassCardLayers'] as List?),
stickerLayers: _jsonToStickers(json['stickerLayers'] as List?),
annotationLayers: _jsonToAnnotations(json['annotationLayers'] as List?),
watermarkStyle: WatermarkStyle.values.firstWhere(
(w) => w.name == (json['watermarkStyle'] as String? ?? 'none'),
orElse: () => WatermarkStyle.none,
),
watermarkText: json['watermarkText'] as String?,
isLiquidGlassMode: json['isLiquidGlassMode'] as bool? ?? false,
);
}
static String _hashCanvas(QuoteCanvasModel c) =>
'${c.background.type.name}_${c.background.solidColor.toARGB32()}_${c.textLayers.map((l) => "${l.id}_${l.text}").join("_")}';
static Map<String, dynamic> _buildPreview(QuoteCanvasModel c) => {
'text': c.textLayers.isNotEmpty ? c.textLayers.first.text : '',
'bgType': c.background.type.name,
'layerCount': c.textLayers.length,
};
static List<GlassCardLayer> _jsonToGlassCards(List<dynamic>? raw) {
if (raw == null) return [];
return raw.map((g) {
final map = g as Map<String, dynamic>;
return GlassCardLayer(
id: map['id'] as String? ?? '',
style: GlassStyle.values.firstWhere(
(s) => s.name == (map['style'] as String? ?? 'crystal'),
orElse: () => GlassStyle.crystal,
),
offsetX: (map['offsetX'] as num?)?.toDouble() ?? 0.0,
offsetY: (map['offsetY'] as num?)?.toDouble() ?? 0.0,
width: (map['width'] as num?)?.toDouble() ?? 200.0,
height: (map['height'] as num?)?.toDouble() ?? 150.0,
rotation: (map['rotation'] as num?)?.toDouble() ?? 0.0,
scale: (map['scale'] as num?)?.toDouble() ?? 1.0,
borderRadius: (map['borderRadius'] as num?)?.toDouble() ?? 20.0,
blurIntensity: (map['blurIntensity'] as num?)?.toDouble() ?? 25.0,
opacity: (map['opacity'] as num?)?.toDouble() ?? 0.65,
borderOpacity: (map['borderOpacity'] as num?)?.toDouble() ?? 0.15,
highlightStrength:
(map['highlightStrength'] as num?)?.toDouble() ?? 0.4,
showHighlight: map['showHighlight'] as bool? ?? true,
showInnerShadow: map['showInnerShadow'] as bool? ?? true,
tintColor: Color(map['tintColor'] as int? ?? 0xFFFFFFFF),
visible: map['visible'] as bool? ?? true,
locked: map['locked'] as bool? ?? false,
zIndex: map['zIndex'] as int? ?? 10,
containedTextIds: List<String>.from(
map['containedTextIds'] as List? ?? [],
),
containedStickerIds: List<String>.from(
map['containedStickerIds'] as List? ?? [],
),
);
}).toList();
}
static List<StickerLayer> _jsonToStickers(List<dynamic>? raw) {
if (raw == null) return [];
return raw.map((s) {
final map = s as Map<String, dynamic>;
return StickerLayer(
id: map['id'] as String? ?? '',
type: StickerType.values.firstWhere(
(t) => t.name == (map['type'] as String? ?? 'emoji'),
orElse: () => StickerType.emoji,
),
assetPath: map['assetPath'] as String? ?? '',
size: (map['size'] as num?)?.toDouble() ?? 80.0,
offsetX: (map['offsetX'] as num?)?.toDouble() ?? 0.0,
offsetY: (map['offsetY'] as num?)?.toDouble() ?? 0.0,
rotation: (map['rotation'] as num?)?.toDouble() ?? 0.0,
scale: (map['scale'] as num?)?.toDouble() ?? 1.0,
opacity: (map['opacity'] as num?)?.toDouble() ?? 1.0,
visible: map['visible'] as bool? ?? true,
locked: map['locked'] as bool? ?? false,
zIndex: map['zIndex'] as int? ?? 0,
);
}).toList();
}
static List<AnnotationLayer> _jsonToAnnotations(List<dynamic>? raw) {
if (raw == null) return [];
return raw.map((a) {
final map = a as Map<String, dynamic>;
final pointsRaw = map['points'] as List? ?? [];
final points = pointsRaw.map((p) {
final pm = p as Map<String, dynamic>;
return AnnotationPoint(
x: (pm['x'] as num?)?.toDouble() ?? 0.0,
y: (pm['y'] as num?)?.toDouble() ?? 0.0,
);
}).toList();
return AnnotationLayer(
id: map['id'] as String? ?? '',
type: AnnotationType.values.firstWhere(
(t) => t.name == (map['type'] as String? ?? 'line'),
orElse: () => AnnotationType.line,
),
points: points,
strokeWidth: (map['strokeWidth'] as num?)?.toDouble() ?? 3.0,
color: Color(map['color'] as int? ?? 0xFFFF0000),
opacity: (map['opacity'] as num?)?.toDouble() ?? 1.0,
visible: map['visible'] as bool? ?? true,
);
}).toList();
}
// ============================================================
// pro_image_editor 草稿支持
// ============================================================
static const _proDraftKey = 'editor_pro_drafts';
/// 保存 pro 编辑器草稿 (传入 JSON 字符串)
static Future<void> saveProDraft(
String stateHistoryJson, {
String? draftId,
}) async {
try {
final prefs = await SharedPreferences.getInstance();
final id = draftId ?? DateTime.now().millisecondsSinceEpoch.toString();
final drafts = await getAllProDrafts();
drafts.removeWhere((d) => d.id == id);
drafts.insert(
0,
ProDraftItem(id: id, data: stateHistoryJson, createdAt: DateTime.now()),
);
if (drafts.length > _maxDrafts) drafts.removeLast();
await prefs.setString(
_proDraftKey,
jsonEncode(drafts.map((d) => d.toJson()).toList()),
);
} catch (e) {
debugPrint('saveProDraft error: $e');
}
}
/// 获取所有 pro 草稿
static Future<List<ProDraftItem>> getAllProDrafts() async {
try {
final prefs = await SharedPreferences.getInstance();
final raw = prefs.getString(_proDraftKey);
if (raw == null || raw.isEmpty) return [];
final list = jsonDecode(raw) as List;
return list
.map(
(e) => ProDraftItem.fromJson(Map<String, dynamic>.from(e as Map)),
)
.toList();
} catch (e) {
return [];
}
}
/// 加载 pro 草稿为 ImportStateHistory
static pro.ImportStateHistory? loadProDraft(ProDraftItem draft) {
try {
return pro.ImportStateHistory.fromJson(draft.data);
} catch (e) {
debugPrint('loadProDraft error: $e');
return null;
}
}
/// 删除 pro 草稿
static Future<void> deleteProDraft(String id) async {
try {
final drafts = await getAllProDrafts();
drafts.removeWhere((d) => d.id == id);
final prefs = await SharedPreferences.getInstance();
if (drafts.isEmpty) {
await prefs.remove(_proDraftKey);
} else {
await prefs.setString(
_proDraftKey,
jsonEncode(drafts.map((d) => d.toJson()).toList()),
);
}
} catch (e) {
debugPrint('deleteProDraft error: $e');
}
}
/// 解析 FontWeight兼容旧格式(index 0-8)和新格式(value 100-900)
static FontWeight _parseFontWeight(dynamic raw) {
if (raw == null) return FontWeight.normal;
if (raw is int) {
if (raw <= 8) return FontWeight.values[raw.clamp(0, 8)];
final idx = (raw ~/ 100) - 1;
return FontWeight.values[idx.clamp(0, 8)];
}
return FontWeight.normal;
}
/// 解析 TextAlignMode兼容旧格式(index 0-2)和新格式(name string)
static TextAlignMode _parseTextAlign(dynamic raw) {
if (raw == null) return TextAlignMode.center;
if (raw is int)
return TextAlignMode.values[raw.clamp(
0,
TextAlignMode.values.length - 1,
)];
if (raw is String) {
return TextAlignMode.values.firstWhere(
(t) => t.name == raw,
orElse: () => TextAlignMode.center,
);
}
return TextAlignMode.center;
}
}
/// pro 编辑器草稿数据模型
class ProDraftItem {
final String id;
final String data;
final DateTime createdAt;
const ProDraftItem({
required this.id,
required this.data,
required this.createdAt,
});
factory ProDraftItem.fromJson(Map<String, dynamic> json) {
return ProDraftItem(
id: json['id'] as String,
data: json['data'] as String,
createdAt: DateTime.parse(json['createdAt'] as String),
);
}
Map<String, dynamic> toJson() => {
'id': id,
'data': data,
'createdAt': createdAt.toIso8601String(),
};
}