- 清理大量废弃的 barrel 导出文件,移除冗余的中间导出层 - 修复所有相对路径导入错误,统一调整为扁平化模块引用 - 更新多平台 pubspec 版本号与依赖库版本 - 补充后端功能问题管理后台与脚本工具 - 调整部分页面的快捷方式文案适配新功能 - 更新部分翻译覆盖率与API文档
271 lines
7.8 KiB
Dart
271 lines
7.8 KiB
Dart
// ============================================================
|
||
// 闲言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,
|
||
);
|