Files
Developer f91be94e9c refactor: 完成项目架构重构,统一模块导入路径
- 清理大量废弃的 barrel 导出文件,移除冗余的中间导出层
- 修复所有相对路径导入错误,统一调整为扁平化模块引用
- 更新多平台 pubspec 版本号与依赖库版本
- 补充后端功能问题管理后台与脚本工具
- 调整部分页面的快捷方式文案适配新功能
- 更新部分翻译覆盖率与API文档
2026-06-12 08:53:57 +08:00

271 lines
7.8 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 — 协作画布Riverpod状态管理
// 创建时间: 2026-05-14
// 更新时间: 2026-06-01
// 作用: CanvasState + CanvasNotifier封装Engine和SyncService
// 上次更新: 修复leaveCanvas未清除canvasId/remoteCursors/participants的bug、copyWith添加clearCanvasId支持
// ============================================================
import 'dart:async';
import 'dart:ui';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:xianyan/core/utils/logger.dart';
import 'package:xianyan/features/file_transfer/providers/shared_signaling_provider.dart';
import 'package:xianyan/features/file_transfer/services/signaling_service.dart';
import 'models/stroke.dart';
import 'services/canvas_engine.dart';
import 'services/canvas_sync_service.dart';
class CanvasState {
const CanvasState({
this.strokes = const [],
this.activeStroke,
this.currentTool = StrokeType.pen,
this.currentColor = '#000000',
this.currentWidth = 3.0,
this.remoteCursors = const {},
this.cursorOpacities = const {},
this.participants = const [],
this.isConnected = false,
this.isSyncing = false,
this.canvasId,
});
final List<Stroke> strokes;
final Stroke? activeStroke;
final StrokeType currentTool;
final String currentColor;
final double currentWidth;
final Map<String, Offset> remoteCursors;
final Map<String, double> cursorOpacities;
final List<String> participants;
final bool isConnected;
final bool isSyncing;
final String? canvasId;
CanvasState copyWith({
List<Stroke>? strokes,
Stroke? activeStroke,
StrokeType? currentTool,
String? currentColor,
double? currentWidth,
Map<String, Offset>? remoteCursors,
Map<String, double>? cursorOpacities,
List<String>? participants,
bool? isConnected,
bool? isSyncing,
String? canvasId,
bool clearActiveStroke = false,
bool clearCanvasId = false,
}) {
return CanvasState(
strokes: strokes ?? this.strokes,
activeStroke: clearActiveStroke
? null
: (activeStroke ?? this.activeStroke),
currentTool: currentTool ?? this.currentTool,
currentColor: currentColor ?? this.currentColor,
currentWidth: currentWidth ?? this.currentWidth,
remoteCursors: remoteCursors ?? this.remoteCursors,
cursorOpacities: cursorOpacities ?? this.cursorOpacities,
participants: participants ?? this.participants,
isConnected: isConnected ?? this.isConnected,
isSyncing: isSyncing ?? this.isSyncing,
canvasId: clearCanvasId ? null : (canvasId ?? this.canvasId),
);
}
}
class CanvasNotifier extends Notifier<CanvasState> {
bool _disposed = false;
final Map<String, Timer> _cursorFadeTimers = {};
@override
CanvasState build() {
_disposed = false;
ref.onDispose(_onDispose);
_signaling = ref.read(sharedSignalingProvider);
_engine = CanvasEngine();
_syncService = CanvasSyncService(signaling: _signaling);
_engine.addListener(_onEngineChanged);
_syncService.onRemoteStroke = _handleRemoteStroke;
_syncService.onRemoteSnapshot = _handleRemoteSnapshot;
_syncService.onRemoteCursor = _handleRemoteCursor;
_syncService.onParticipantsChanged = _handleParticipantsChanged;
_syncService.onSnapshotRequest = _handleSnapshotRequest;
return const CanvasState();
}
late SignalingService _signaling;
late CanvasEngine _engine;
late CanvasSyncService _syncService;
CanvasEngine get engine => _engine;
void setUserId(String userId) {
_engine.userId = userId;
}
void setTool(StrokeType tool) {
_engine.setTool(tool);
}
void setColor(String color) {
_engine.setColor(color);
}
void setWidth(double width) {
_engine.setWidth(width);
}
void startStroke(Offset point) {
_engine.startStroke(point);
}
void addPoint(Offset point) {
_engine.addPoint(point);
broadcastCursor(point);
}
void endStroke() {
final stroke = _engine.endStroke();
if (stroke != null) {
_syncService.broadcastStroke(stroke);
}
}
void undo() {
_engine.undo();
}
void redo() {
_engine.redo();
}
void clearCanvas() {
_engine.clearCanvas();
}
void joinCanvas(String canvasId, String deviceId, {String? peerDeviceId}) {
_syncService.joinCanvas(canvasId, deviceId, peerId: peerDeviceId);
state = state.copyWith(canvasId: canvasId, isConnected: true);
Future.delayed(const Duration(milliseconds: 500), () {
_syncService.requestSnapshot();
});
}
void leaveCanvas() {
_syncService.leaveCanvas();
for (final timer in _cursorFadeTimers.values) {
timer.cancel();
}
_cursorFadeTimers.clear();
Future.microtask(() {
if (_disposed) return;
state = state.copyWith(
isConnected: false,
clearCanvasId: true,
remoteCursors: {},
cursorOpacities: {},
participants: [],
);
});
}
void broadcastCursor(Offset position) {
_syncService.broadcastCursor(position);
}
void requestSnapshotSync() {
if (state.isSyncing) return;
state = state.copyWith(isSyncing: true);
_syncService.requestSnapshot();
Future.delayed(const Duration(seconds: 10), () {
if (!_disposed && state.isSyncing) {
state = state.copyWith(isSyncing: false);
Log.w('CanvasProvider: snapshot sync timed out');
}
});
}
void _onEngineChanged() {
Future.microtask(() {
if (_disposed) return;
state = state.copyWith(
strokes: _engine.strokes,
activeStroke: _engine.activeStroke,
currentTool: _engine.currentTool,
currentColor: _engine.currentColor,
currentWidth: _engine.currentWidth,
clearActiveStroke: _engine.activeStroke == null,
);
}).catchError((_) {});
}
void _handleRemoteStroke(Stroke stroke) {
Log.d('CanvasProvider: received remote stroke from ${stroke.userId}');
_engine.mergeRemoteStroke(stroke);
}
void _handleRemoteSnapshot(List<Stroke> strokes) {
Log.i('CanvasProvider: received snapshot with ${strokes.length} strokes');
_engine.mergeSnapshot(strokes);
Future.microtask(() {
if (_disposed) return;
state = state.copyWith(isSyncing: false);
});
}
void _handleRemoteCursor(String userId, Offset position) {
_cursorFadeTimers[userId]?.cancel();
_cursorFadeTimers[userId] = Timer(const Duration(seconds: 3), () {
if (_disposed) return;
final opacities = Map<String, double>.from(state.cursorOpacities);
opacities[userId] = 0.0;
state = state.copyWith(cursorOpacities: opacities);
});
Future.microtask(() {
if (_disposed) return;
final updated = Map<String, Offset>.from(state.remoteCursors);
updated[userId] = position;
final opacities = Map<String, double>.from(state.cursorOpacities);
opacities[userId] = 1.0;
state = state.copyWith(remoteCursors: updated, cursorOpacities: opacities);
});
}
void _handleParticipantsChanged(List<String> participants) {
Future.microtask(() {
if (_disposed) return;
state = state.copyWith(participants: participants);
}).catchError((_) {});
}
void _handleSnapshotRequest(String requestingDeviceId, String canvasId) {
if (state.canvasId != canvasId) return;
final strokes = _engine.strokes;
Log.i(
'CanvasProvider: sending snapshot response to $requestingDeviceId with ${strokes.length} strokes',
);
_syncService.sendSnapshotResponse(requestingDeviceId, strokes);
}
void _onDispose() {
_disposed = true;
for (final timer in _cursorFadeTimers.values) {
timer.cancel();
}
_cursorFadeTimers.clear();
_engine.removeListener(_onEngineChanged);
_engine.dispose();
_syncService.dispose();
}
}
final canvasProvider = NotifierProvider<CanvasNotifier, CanvasState>(
CanvasNotifier.new,
);