diff --git a/CHANGELOG.md b/CHANGELOG.md index e7dc5e2..5dc7204 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,11 +4,12 @@ ### Added -- 添加了局域网联机功能 +- 添加了联机功能 ### Improvement - 优化了单词卡片的排版 +- 添加了联机功能对Web的支持 ### Fix diff --git a/lib/funcs/local_pk_server.dart b/lib/funcs/local_pk_server.dart index 57dabe4..13d321f 100644 --- a/lib/funcs/local_pk_server.dart +++ b/lib/funcs/local_pk_server.dart @@ -1,416 +1,400 @@ -import 'dart:io' show HttpServer; -import 'dart:math'; +import 'dart:async'; import 'dart:convert'; -import 'dart:typed_data' show Uint8List; -import 'package:arabic_learning/funcs/ui.dart' show alart; +import 'dart:math'; +import 'package:arabic_learning/funcs/ui.dart'; +import 'package:archive/archive.dart'; import 'package:arabic_learning/funcs/utili.dart'; import 'package:arabic_learning/vars/config_structure.dart'; import 'package:arabic_learning/vars/global.dart'; -import 'package:flutter/material.dart' show BuildContext, Navigator; +import 'package:flutter/material.dart' show BuildContext, PageController; import 'package:logging/logging.dart'; -import 'package:network_info_plus/network_info_plus.dart'; import 'package:arabic_learning/vars/statics_var.dart'; import 'package:flutter/foundation.dart' show ChangeNotifier; -import 'package:shelf/shelf.dart'; -import 'package:shelf/shelf_io.dart' as io; -import 'package:shelf_router/shelf_router.dart'; -import 'package:dio/dio.dart' as dio; +import 'package:flutter_webrtc/flutter_webrtc.dart'; +import 'package:provider/provider.dart'; class PKServer with ChangeNotifier{ - - // both - bool connected = false; final Logger logger = Logger("PKServer"); - late final String? _localIP; - late int rndSeed; + bool isServer = false; + bool inited = false; + RTCPeerConnection? _connection; + RTCDataChannel? _channel; + static const Map _rtcConstraints = { + 'mandatory': { + 'OfferToReceiveAudio': false, + 'OfferToReceiveVideo': false, + }, + 'optional': [], + }; + static const Map _rtcConfig = { + 'iceServers': [ + {'urls': 'stun:stun.l.google.com:19302'}, + {'urls': 'stun:stun1.l.google.com:19302'}, + ] + }; + String? exitMessage; + int packageAmount = 0; + PageController? pageController; List selectableSource = []; ClassSelection? classSelection; + late int rndSeed; late Global global; - final NetworkInfo _networkInfo = NetworkInfo(); - DateTime? startTime; + Duration? delay; bool preparedP1 = false; bool preparedP2 = false; late PKState pkState; + bool over = false; + DateTime? startTime; + bool get connected => _connection?.connectionState == RTCPeerConnectionState.RTCPeerConnectionStateConnected; + String? connectpwd; - // server - bool started = false; - Duration? delay; - late HttpServer _server; - late int _port; - - // client - String? serverAddress; - final dio.Dio client = dio.Dio(); - - String? get connectpwd { - if(!started) return null; - List tmp = List.generate(3, (int index) => int.parse(_localIP!.split(".")[index + 1]), growable: false); - int iphex = (((tmp[0] << 16) | (tmp[1] << 8) | tmp[2]) << 16) | (_port & 0xFFFF); - Uint8List bytes = Uint8List(5); - bytes[0] = (iphex >> 32) & 0xFF; - bytes[1] = (iphex >> 24) & 0xFF; - bytes[2] = (iphex >> 16) & 0xFF; - bytes[3] = (iphex >> 8) & 0xFF; - bytes[4] = iphex & 0xFF; - return base64Encode(bytes).replaceAll("=", ""); - } - - Future init(Global outerglobal) async { - _localIP = await _networkInfo.getWifiIP(); - logger.finer("获取到局域网IP: $_localIP"); - global = outerglobal; - } + Future initHost(bool isHoster, BuildContext context, {String? offer}) async { + isServer = isHoster; + global = context.read(); - void renew() { - connected = false; - selectableSource = []; - classSelection = null; - startTime = null; - preparedP1 = false; - preparedP2 = false; - started = false; - delay = null; - serverAddress = null; + try { + logger.info("正在初始化WebRTC"); + _connection = await createPeerConnection(_rtcConfig, _rtcConstraints); + _connection!.onConnectionState = (state) { + logger.info("连接状态变更: $state"); + if(state == RTCPeerConnectionState.RTCPeerConnectionStateFailed && _channel != null && !over) { + disconnect(); + pageController!.jumpToPage(4); + } + notifyListeners(); + }; + if(isServer) { + _channel = await _connection!.createDataChannel("Connection", RTCDataChannelInit()); + logger.info("已创建信道"); + _channel!.onMessage = ((msg) { + logger.fine('收到消息:${msg.text}'); + }); + _setupDataChannel(); + RTCSessionDescription offer = await _connection!.createOffer(_rtcConstraints); + await _connection!.setLocalDescription(offer); + // 等待 ICE 收集完毕后再生成最终字符串,防止 Candidate 丢失 + await _waitForIceGathering(); + } else { + await _connection!.setRemoteDescription(RTCSessionDescription(utf8.decode(ZLibDecoderWeb().decodeBytes(base64Decode(offer!))), "offer")); + await _connection!.setLocalDescription(await _connection!.createAnswer(_rtcConstraints)); + _connection!.onDataChannel = (channel) { + logger.info("Client 接收到了来自 Server 的 DataChannel: ${channel.label}"); + _channel = channel; + _setupDataChannel(); + }; + await _waitForIceGathering(); + } + } catch (e) { + if(!context.mounted) return; + alart(context, "构建连接错误: $e"); + } + + notifyListeners(); } - Future startHost() async { - if(started) return true; - _port = Random().nextInt(55535)+10000; - logger.fine("正在启动服务,随机端口: $_port"); - final router = Router(); - - router.get('/api/check', (Request req) { - logger.fine("收到check请求"); - return Response.ok( - '{"version":${StaticsVar.appVersion}}', - headers: {'Content-Type': 'application/json'}, - ); - }); - - router.post('/api/testDictSum', (Request req) async { - if(connected == true) return Response.forbidden(""); - Map body = json.decode(await req.readAsString()); - logger.fine("收到testDictSum请求,负载: $body"); - // { - // "dictSum": ["SHA256", ...] - // } - List sumList = body["dictSum"]; - selectableSource.clear(); - for(SourceItem source in global.wordData.classes) { - if(sumList.contains(source.getHash(global.wordData.words))) selectableSource.add(source); - logger.fine("计算得到${source.sourceJsonFileName}在哈希中有匹配"); + Future _waitForIceGathering() async { + if (_connection!.iceGatheringState == RTCIceGatheringState.RTCIceGatheringStateComplete) { + return; + } + + final completer = Completer(); + _connection!.onIceGatheringState = (state) async { + if (state == RTCIceGatheringState.RTCIceGatheringStateComplete) { + if (!completer.isCompleted) completer.complete(); + String sdp = (await _connection!.getLocalDescription())!.sdp!; + sdp = optimizeSdp(sdp); + connectpwd = base64Encode(ZLibEncoderWeb().encodeBytes(utf8.encode(sdp), level: 9)); + logger.info("最终的SDP: $sdp"); + inited = true; + notifyListeners(); } - if(selectableSource.isNotEmpty) connected = true; + }; + + // 增加超时保护(某些网络下可能收不到完成信号) + return completer.future.timeout(const Duration(minutes: 1), onTimeout: () async { + logger.info("ICE 收集超时,尝试使用当前收集到的 Candidate"); + String sdp = (await _connection!.getLocalDescription())!.sdp!; + sdp = optimizeSdp(sdp); + connectpwd = base64Encode(ZLibEncoderWeb().encodeBytes(utf8.encode(sdp), level: 9)); + inited = true; notifyListeners(); - return Response.ok(json.encode({ - "accept": selectableSource.isNotEmpty, - "allowed": List.generate(selectableSource.length, (int index) => selectableSource[index].getHash(global.wordData.words), growable: false) - }), - headers: {'Content-Type': 'application/json'} - ); - }); - - router.get('/api/selection', (Request req) { - logger.fine("收到selection请求"); - if(classSelection == null) { - logger.fine("房主暂未完成选择,statue: false"); - return Response.ok(json.encode({"statue": false}), headers: {'Content-Type': 'application/json'}); - } - rndSeed = Random().nextInt(1024); - logger.finer("随机种子: $rndSeed"); - return Response.ok( - json.encode( - { - "statue": true, - "selected": List.generate(classSelection!.selectedClass.length, (int index) => classSelection!.selectedClass[index].getHash(), growable: false), - "seed": rndSeed - } - ), - headers: {'Content-Type': 'application/json'}, - ); }); + } - router.post('/api/prepare', (Request req) async { - Map body = json.decode(await req.readAsString()); - logger.fine("收到prepare请求,负载 $body"); - - if(body["time"] != null && delay == null) { - delay = DateTime.tryParse(body["time"])!.difference(DateTime.now()); - logger.info("已加载双端延迟补偿: ${delay!.inSeconds}秒"); + static String optimizeSdp(String sdp) { + List lines = sdp.split('\r\n'); + List newLines = []; + bool inDataBlock = false; + for (var line in lines) { + if( + line.startsWith('v=') || + line.startsWith('o=') || + line.startsWith('s=') || + line.startsWith('t=') || + line.startsWith('a=ice-ufrag:') || + line.startsWith('a=ice-pwd:') || + line.startsWith('a=fingerprint:') || + line.startsWith('a=setup:') || + line.startsWith('a=sctp-port:') || + line.startsWith('a=mid:') + ){ + newLines.add(line); + continue; } - if(body["prepared"]) { - preparedP2 = true; - logger.fine("对方准备完毕"); - if(preparedP1 && startTime == null) { - startTime = DateTime.now().add(Duration(seconds: 5)); - pkState = PKState( - testWords: getSelectedWords(global.wordData, classSelection!.selectedClass, doShuffle: true, shuffleSeed: rndSeed), - selfProgress: [], - sideProgress: [] - ); - logger.fine("已生成开始时间: $startTime(添加delay后为: ${startTime?.add(delay!).toIso8601String()}); PKState已初始化"); + if (line.startsWith('m=')) { + if(line.contains("application")) { + inDataBlock = true; + } else { + inDataBlock = false; } - notifyListeners(); - } - - - return Response.ok(json.encode({ - "prepared": preparedP1, - "start": startTime?.add(delay!).toIso8601String() - }), - headers: {'Content-Type': 'application/json'} - ); - }); - - router.post('/api/sync', (Request req) async { - Map body = json.decode(await req.readAsString()); - logger.finer("收到sync请求,负载: $body"); - bool changed = false; - if(body["progress"] != null && body["progress"].length != pkState.sideProgress.length) { - pkState.sideProgress = List.generate(body["progress"].length, (int index) => body["progress"][index] as bool); - logger.fine("已更新本地PKState.sideProgress"); - changed = true; } - if(pkState.sideProgress.length == pkState.testWords.length && body["tooken"] != null) { - pkState.sideTookenTime = body["tooken"]; - logger.fine("已更新本地PKState.sideTookenTime"); - changed = true; + if (inDataBlock) { + newLines.add(line); } - if(changed) notifyListeners(); - if(pkState.selfTookenTime != null) logger.fine("回报本地tokenTime: ${pkState.selfTookenTime}"); - return Response.ok(json.encode({ - "progress": pkState.selfProgress, - "tooken": pkState.selfTookenTime - }), - headers: {'Content-Type': 'application/json'} - ); - }); + } + return newLines.join('\r\n'); + } - router.get('/api/done', (Request req) async { - logger.info("收到done请求"); - Future.delayed(Duration(seconds: 1), () { - stopHost(); - }); + void _setupDataChannel() { + _channel?.onMessage = (RTCDataChannelMessage message) { + handleReceivedMessage(message.text); + }; + _channel?.onDataChannelState = (state) { + logger.fine("Channel 状态: $state"); notifyListeners(); - return Response.ok(null); - }); + }; + } - _server = await io.serve( - router.call, - '0.0.0.0', // 局域网可访问 - _port, - ); + void setPageControler(PageController pageController) => this.pageController = pageController; - logger.fine("服务端已启动"); - started = true; - return true; + Future loadAnswer(String answer, BuildContext context) async { + await _connection!.setRemoteDescription(RTCSessionDescription(utf8.decode(ZLibDecoderWeb().decodeBytes(base64Decode(answer))), "answer")); + while(!connected || _channel?.state != RTCDataChannelState.RTCDataChannelOpen){ + await Future.delayed(Duration(seconds: 1)); + logger.fine("等待连接远程"); + } + _channel!.send(RTCDataChannelMessage("ping")); + _questVersionCheck(); } - Future stopHost() async { - await _server.close(); - logger.info("服务端已关闭"); + void disconnect({bool normal = false}) async { + _channel?.close(); + _connection?.close(); + _channel = null; + _connection = null; } - List? decodeConnectPwd(String connectpwd){ - logger.info("尝试解码$connectpwd"); - while (connectpwd.length == 7) { - connectpwd = "$connectpwd="; - } - late Uint8List tmp; - try{ - tmp = base64Decode(connectpwd); - } catch (e) { - logger.warning("解码错误"); - return null; - } - - if(tmp.length != 5){ - logger.warning("解码错误"); - return null; - } - int comb = (tmp[0] << 32) | (tmp[1] << 24) | (tmp[2] << 16) | (tmp[3] << 8) | tmp[4]; - int port = comb & 0xFFFF; - int iphex= comb >> 16; - return [(iphex >> 16) & 0xFF, (iphex >> 8 & 0xFF), iphex & 0xFF, port]; - } + void _questVersionCheck() async => _channel!.send(RTCDataChannelMessage(json.encode({"step": 0, "version": StaticsVar.appVersion}))); - Future testConnect(String connectpwd) async { - List? addressinfo = decodeConnectPwd(connectpwd); - if(addressinfo == null) { - logger.severe("联机口令解析失败,终止连接"); - return 1; - } - serverAddress = "http://${_localIP!.split(".")[0]}.${addressinfo[0]}.${addressinfo[1]}.${addressinfo[2]}:${addressinfo[3]}"; - logger.info("服务端口解析结果: $serverAddress"); - final checkRes = await client.get("$serverAddress/api/check"); - if(checkRes.statusCode != 200) { - logger.severe("连接服务端失败"); - return 2; - } - if(checkRes.data["version"] != StaticsVar.appVersion) { - logger.severe("版本校验不通过,对方版本为: ${checkRes.data["version"]},我方为${StaticsVar.appVersion}"); - return 3; - } - logger.fine("双端版本校验通过"); - final dictRes = await client.post( - "$serverAddress/api/testDictSum", - data: { + void _questDictExchange() async { + _channel!.send(RTCDataChannelMessage( + json.encode({ + "step": 1, "dictSum": List.generate(global.wordData.classes.length, (int index) => global.wordData.classes[index].getHash(global.wordData.words), growable: false) - } + }) + )); + } + + void setSelectedClass(ClassSelection selection) { + classSelection = selection; + rndSeed = Random().nextInt(1024); + _channel!.send(RTCDataChannelMessage(json.encode({ + "step": 2, + "selected": List.generate(classSelection!.selectedClass.length, (int index) => classSelection!.selectedClass[index].toString(), growable: false), + "rndSeed": rndSeed + }))); + } + + void setPrepare() { + preparedP1 = true; + pkState = PKState( + testWords: getSelectedWords(global.wordData, classSelection!.selectedClass, doShuffle: true, shuffleSeed: rndSeed), + selfProgress: [], + sideProgress: [] ); - if(dictRes.statusCode != 200) { - logger.severe("连接服务端失败"); - return 2; + logger.fine("已完成PKState初始化"); + _channel!.send(RTCDataChannelMessage(json.encode({ + "step": 3 + }))); + if(isServer && preparedP1 && preparedP2) _questStartTime(); + notifyListeners(); + } + + void _questStartTime(){ + startTime = DateTime.now().add(Duration(seconds: 5)); + Future.delayed(Duration(seconds: 5), () => pageController!.nextPage(duration: Duration(milliseconds: 5), curve: StaticsVar.curve)); + _channel!.send(RTCDataChannelMessage(json.encode({ + "step": 4, + "startTime": startTime?.add(delay!).toIso8601String() + }))); + notifyListeners(); + } + + void handleReceivedMessage(String? message) { + if(message == null) return; + + // 方便在日志里跟踪一个包 + final int packageid = packageAmount++; + + // 处理心跳包 + if(message == "ping") { + logger.finer("[$packageid] 回复心跳包"); + _channel!.send(RTCDataChannelMessage("pong")); + Future.delayed(Duration(seconds: 1), (){ + if(_channel?.state == RTCDataChannelState.RTCDataChannelOpen) { + logger.finer("发送心跳包"); + _channel!.send(RTCDataChannelMessage("ping")); + } + }); + return; } - if(!dictRes.data["accept"]) { - logger.severe("本地与服务端无可使用的相同词库"); - return 4; + if(message == "pong") { + return; } - selectableSource.clear(); - List remoteDict = dictRes.data["allowed"]; - for(SourceItem source in global.wordData.classes) { - if(remoteDict.contains(source.getHash(global.wordData.words))) selectableSource.add(source); + + // 处理数据包 + Map? data; + int step = -1; + try { + data = json.decode(message); + if(data == null) throw Exception("null decoded"); + step = data["step"]??-1; + if(step == -1) throw Exception("null step block"); + } catch (e) { + logger.warning("[$packageid] 解析数据包错误: $e;原信息: $message"); + return; } - connected = true; - notifyListeners(); - return 0; - } - Future watingSelection(BuildContext context, Function onEnd) async { - logger.fine("开始等待服务端选择课程"); - bool isException = false; - while(classSelection == null) { - await Future.delayed(Duration(seconds: 1)); - try{ - logger.fine("正在检查选择情况"); - final selectionRes = await client.get("$serverAddress/api/selection", options: dio.Options(connectTimeout: Duration(seconds: 1))); - if(selectionRes.statusCode != 200) throw Exception("Unexcepted statusCode: ${selectionRes.statusCode}"); - Map payload = selectionRes.data; - logger.finer("此次检查结果: $payload"); - if(!payload["statue"]) continue; - rndSeed = payload["seed"]; + switch(step){ + /// 版本号检查结果 from Client + case 0 when data.containsKey("accepted"): { + logger.fine("[$packageid] 获取到版本检查结果"); + if(data["accepted"]??false) { + logger.fine("[$packageid] 版本检查通过"); + _questDictExchange(); + } else { + logger.warning("[$packageid] 版本检查未能通过,我方版本为${StaticsVar.appVersion},对方版本为${data["version"]}"); + exitMessage = "版本检查未能通过,我方版本为${StaticsVar.appVersion},对方版本为${data["version"]}"; + disconnect(); + } + break; + } + /// 版本号检查 from Server + case 0 when data.containsKey("version"): { + logger.fine("[$packageid] 进行版本匹配检查"); + if(data["version"] == StaticsVar.appVersion) { + logger.fine("[$packageid] 版本检查通过"); + _channel!.send(RTCDataChannelMessage(json.encode({"step": 0, "accepted": true}))); + } else { + logger.warning("[$packageid] 版本检查未能通过,我方版本为${StaticsVar.appVersion},对方版本为${data["version"]}"); + _channel!.send(RTCDataChannelMessage(json.encode({"step": 0, "accepted": false, "version": StaticsVar.appVersion}))); + } + break; + } + /// 词库共通检查结果 from Client + case 1 when data.containsKey("accepted"): { + logger.fine("[$packageid] 获取到词库检查结果"); + if(data["accepted"]) { + delay = DateTime.parse(data["time"]).difference(DateTime.now()); + List sumList = data["dictSum"]; + selectableSource.clear(); + for(SourceItem source in global.wordData.classes) { + if(sumList.contains(source.getHash(global.wordData.words))) selectableSource.add(source); + logger.fine("[$packageid] 计算得到${source.sourceJsonFileName}在哈希中有匹配"); + } + pageController!.nextPage(duration: Duration(milliseconds: 500), curve: StaticsVar.curve); + } else { + logger.warning("[$packageid] 双端没有任意词库匹配"); + exitMessage = "双端没有任意词库匹配"; + disconnect(); + } + break; + } + /// 词库共通检查 from Server + case 1 when data.containsKey("dictSum"): { + logger.fine("[$packageid] 进行词库检查"); + List sumList = data["dictSum"]; + selectableSource.clear(); + for(SourceItem source in global.wordData.classes) { + if(sumList.contains(source.getHash(global.wordData.words))) { + selectableSource.add(source); + logger.fine("[$packageid] 计算得到${source.sourceJsonFileName}在哈希中有匹配"); + } + } + if(selectableSource.isNotEmpty) { + _channel!.send(RTCDataChannelMessage(json.encode({ + "step": 1, + "accepted": true, + "dictSum": List.generate(selectableSource.length, (int index) => selectableSource[index].getHash(global.wordData.words)), + "time": DateTime.now().toIso8601String() + }))); + pageController!.nextPage(duration: Duration(milliseconds: 500), curve: StaticsVar.curve); + } else { + _channel!.send(RTCDataChannelMessage(json.encode({ + "step": 1, + "accepted": false + }))); + logger.warning("[$packageid] 双端没有任意词库匹配"); + exitMessage = "双端没有任意词库匹配"; + } + break; + } + /// 接受房主发送选择情况 from Server + case 2 when data.containsKey("selected"): { + logger.fine("[$packageid] 接收到房主的课程选择"); + rndSeed = data["rndSeed"]; List selectedClass = []; for(SourceItem sourceItem in selectableSource) { for(ClassItem classItem in sourceItem.subClasses){ - if(payload["selected"].contains(classItem.getHash())){ + if(data["selected"].contains(classItem.toString())){ selectedClass.add(classItem); } } } classSelection = ClassSelection(selectedClass: selectedClass, countInReview: false); - } catch (e) { - logger.shout("连接服务端发生错误: $e"); - if(context.mounted) alart(context, "连接丢失 ${e.toString()}", onConfirmed: () => Navigator.popUntil(context, (route) => route.isFirst)); - isException = true; + pageController!.nextPage(duration: Duration(milliseconds: 500), curve: StaticsVar.curve); break; - } - } - if(!isException) onEnd(); - } - - Future watingPrepare(BuildContext context) async { - logger.fine("开始等待双端准备"); - while (!preparedP2 || startTime == null) { - try{ - logger.fine("正在交换等待情况"); - await Future.delayed(Duration(seconds: 1)); - final prepareRes = await client.post( - "$serverAddress/api/prepare", - data: { - "prepared": preparedP1, - "time": DateTime.now().toIso8601String() - } - ); - if(prepareRes.statusCode != 200) throw Exception("Unexcepted statusCode: ${prepareRes.statusCode}"); - logger.finer("交换结果: ${prepareRes.data}"); - bool changed = false; - if(preparedP2 != prepareRes.data["prepared"]) { - preparedP2 = prepareRes.data["prepared"]; - changed = true; - } - if(preparedP1 && preparedP2 && prepareRes.data["start"] != null) { - startTime = DateTime.tryParse(prepareRes.data["start"]) as DateTime; - changed = true; - } - if(changed) notifyListeners(); - } catch (e) { - logger.shout("连接服务端失败: $e"); - if(context.mounted) alart(context, "连接丢失 ${e.toString()}", onConfirmed: () => Navigator.popUntil(context, (route) => route.isFirst)); + } + /// 接受对方完成准备 from both + case 3: { + preparedP2 = true; + if(isServer && preparedP1 && preparedP2) _questStartTime(); + notifyListeners(); break; } - } - } - - void initPK(BuildContext context) { - pkState = PKState( - testWords: getSelectedWords(global.wordData, classSelection!.selectedClass, doShuffle: true, shuffleSeed: rndSeed), - selfProgress: [], - sideProgress: [] - ); - logger.fine("已完成PKState初始化"); - syncPKState(context); - } - - Future syncPKState(BuildContext context) async { - logger.info("开始进行双端状态同步"); - int expCount = 0; - while(pkState.selfTookenTime == null || pkState.sideTookenTime == null) { - try{ - if(expCount == 5) { - break; - } - await Future.delayed(Duration(milliseconds: 500)); - logger.finer("进行状态数据交换"); - final syncRes = await client.post( - "$serverAddress/api/sync", - data: { - "progress": pkState.selfProgress, - "tooken": null - }, - options: dio.Options(connectTimeout: Duration(seconds: 1)) - ); - if(syncRes.statusCode != 200) throw Exception("Unexcepted statusCode: ${syncRes.statusCode}"); - logger.finer("对方交换结果为: ${syncRes.data}"); - bool changed = false; - if(syncRes.data["progress"] != null && syncRes.data["progress"].length != pkState.sideProgress.length) { - pkState.sideProgress = List.generate(syncRes.data["progress"].length, (int index) => syncRes.data["progress"][index] as bool); - logger.fine("已更新本地PKState.sideProgress"); - changed = true; + /// 接受服务端的开始时间 from Server + case 4: { + startTime = DateTime.parse(data["startTime"]); + Future.delayed(-DateTime.now().difference(startTime!), + ()=>pageController!.nextPage(duration: Duration(milliseconds: 500), curve: StaticsVar.curve)); + notifyListeners(); + break; + } + /// 同步双端状态 from both + case 5: { + pkState.sideProgress = List.generate(data["progress"].length, (int index)=>data!["progress"][index] as bool); + if(data["tookenTime"] != null) { + pkState.sideTookenTime = data["tookenTime"]; + over = true; } - if(pkState.sideProgress.length == pkState.testWords.length && syncRes.data["tooken"] != null) { - logger.fine("已更新本地PKState.sideTookenTime"); - pkState.sideTookenTime = syncRes.data["tooken"]; - changed = true; + if(pkState.selfTookenTime != null && pkState.sideTookenTime != null) { + disconnect(); } - expCount = 0; - if(changed) notifyListeners(); - } catch (e) { - logger.shout("同步状态失败 $e"); - expCount++; + notifyListeners(); } } - if(expCount == 5) { - if(context.mounted) alart(context, "连接丢失 无法连接到服务端", onConfirmed: () => Navigator.popUntil(context, (route) => route.isFirst)); - } - try{ - await client.post( - "$serverAddress/api/sync", - data: { - "progress": pkState.selfProgress, - "tooken": pkState.selfTookenTime - }, - options: dio.Options(connectTimeout: Duration(seconds: 1)) - ); - client.get("$serverAddress/api/done"); - logger.fine("已通知服务端联机进程完成"); - } catch (e) { - logger.shout("同步状态失败 $e"); - } - } void updateState(bool correct) { pkState.selfProgress.add(correct); + if(pkState.selfProgress.length == pkState.testWords.length) { + pkState.selfTookenTime = DateTime.now().difference(startTime!).inSeconds; + } + _channel!.send(RTCDataChannelMessage(json.encode({ + "step": 5, + "progress": pkState.selfProgress, + "tookenTime": pkState.selfTookenTime + }))); notifyListeners(); } diff --git a/lib/funcs/ui.dart b/lib/funcs/ui.dart index d845479..88c02b2 100644 --- a/lib/funcs/ui.dart +++ b/lib/funcs/ui.dart @@ -178,6 +178,7 @@ void alart(BuildContext context, String e, {Function? onConfirmed, Duration dela context.read().uiLogger.info("构建弹出窗口: 携带信息: $e ;确认参数: ${onConfirmed != null}; 延迟: ${delayConfirm.inMilliseconds}"); showDialog( context: context, + requestFocus: true, builder: (BuildContext context) { return AlertDialog( title: Text("提示"), diff --git a/lib/package_replacement/fake_local_pk_page.dart b/lib/package_replacement/fake_local_pk_page.dart deleted file mode 100644 index 03d2a42..0000000 --- a/lib/package_replacement/fake_local_pk_page.dart +++ /dev/null @@ -1,10 +0,0 @@ -import 'package:flutter/material.dart'; - -class LocalPKSelectPage extends StatelessWidget{ - const LocalPKSelectPage({super.key}); - - @override - Widget build(BuildContext context) { - throw UnimplementedError(); - } -} \ No newline at end of file diff --git a/lib/package_replacement/fake_local_pk_server.dart b/lib/package_replacement/fake_local_pk_server.dart deleted file mode 100644 index 9bdb386..0000000 --- a/lib/package_replacement/fake_local_pk_server.dart +++ /dev/null @@ -1,57 +0,0 @@ -import 'package:arabic_learning/vars/config_structure.dart'; -import 'package:arabic_learning/vars/global.dart'; -import 'package:flutter/material.dart'; - -class PKServer with ChangeNotifier{ - bool get connected => false; - - List selectableSource = []; - - String? connectpwd = ""; - - bool get started => false; - - DateTime? startTime; - - ClassSelection? classSelection; - - bool preparedP1 = false; - bool preparedP2 = false; - - int? get rndSeed => null; - - late PKState pkState; - - Future testConnect(String text) async {return 0;} - - void watingSelection(BuildContext context, Future Function() param1) {} - - void stopHost() {} - - void renew() {} - - Future? startHost() async {return false;} - - void watingPrepare(BuildContext context) {} - - void initPK(BuildContext context) {} - - double calculatePt(List selfProgress, int param1) {return 0;} - - void updateState(bool bool) {} - - Future init(Global read) async {} - -} - -class PKState { - final List testWords = []; - - int? selfTookenTime; - - int? sideTookenTime; - - List selfProgress = []; - - List sideProgress = []; -} \ No newline at end of file diff --git a/lib/pages/test_page.dart b/lib/pages/test_page.dart index bdcf55b..713ef97 100644 --- a/lib/pages/test_page.dart +++ b/lib/pages/test_page.dart @@ -1,13 +1,10 @@ -import 'package:arabic_learning/funcs/ui.dart'; -import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; import 'package:arabic_learning/vars/global.dart'; import 'package:arabic_learning/vars/statics_var.dart'; import 'package:arabic_learning/sub_pages_builder/test_pages/listening_test_page.dart' show ForeListeningSettingPage; -import 'package:arabic_learning/package_replacement/fake_local_pk_server.dart' if (dart.library.io) 'package:arabic_learning/funcs/local_pk_server.dart' show PKServer; -import 'package:arabic_learning/package_replacement/fake_local_pk_page.dart' if (dart.library.io) 'package:arabic_learning/sub_pages_builder/test_pages/local_pk_page.dart' show LocalPKSelectPage; +import 'package:arabic_learning/sub_pages_builder/test_pages/local_pk_page.dart' show LocalPKSelectPage; class TestPage extends StatelessWidget { const TestPage({super.key}); @@ -21,7 +18,7 @@ class TestPage extends StatelessWidget { SizedBox(height: mediaQuery.size.height * 0.05), ElevatedButton.icon( icon: Icon(Icons.connect_without_contact, size: 36.0), - label: FittedBox(child: Text('局域网联机', style: TextStyle(fontSize: 34.0))), + label: FittedBox(child: Text('联机', style: TextStyle(fontSize: 34.0))), style: ElevatedButton.styleFrom( backgroundColor: Theme.of(context).colorScheme.onPrimary.withAlpha(150), fixedSize: Size(mediaQuery.size.width * 0.8, mediaQuery.size.height * 0.15), @@ -30,18 +27,11 @@ class TestPage extends StatelessWidget { ), ), onPressed: () { - if(kIsWeb) { - alart(context, "局域网联机功能因为技术原因,不对网页端开放。"); - return; - } context.read().uiLogger.info("跳转: TestPage => LocalPKSelectPage"); Navigator.push( context, MaterialPageRoute( - builder: (context) => ChangeNotifierProvider( - create: (context) => PKServer()..init(context.read()), - child: LocalPKSelectPage() - ) + builder: (context) => LocalPKSelectPage() ) ); }, diff --git a/lib/sub_pages_builder/test_pages/local_pk_page.dart b/lib/sub_pages_builder/test_pages/local_pk_page.dart index add7407..cd9f05b 100644 --- a/lib/sub_pages_builder/test_pages/local_pk_page.dart +++ b/lib/sub_pages_builder/test_pages/local_pk_page.dart @@ -1,5 +1,3 @@ -import 'dart:async'; -import 'dart:io'; import 'dart:math'; import 'package:arabic_learning/funcs/ui.dart'; @@ -8,10 +6,11 @@ import 'package:arabic_learning/vars/config_structure.dart'; import 'package:arabic_learning/vars/global.dart'; import 'package:arabic_learning/vars/statics_var.dart'; import 'package:flutter/material.dart'; +import 'package:flutter/services.dart' show Clipboard, ClipboardData; import 'package:qr_flutter/qr_flutter.dart'; import 'package:mobile_scanner/mobile_scanner.dart'; import 'package:provider/provider.dart'; -import 'package:arabic_learning/package_replacement/fake_local_pk_server.dart' if (dart.library.io) 'package:arabic_learning/funcs/local_pk_server.dart'; +import 'package:arabic_learning/funcs/local_pk_server.dart'; class LocalPKSelectPage extends StatefulWidget { @@ -23,50 +22,26 @@ class LocalPKSelectPage extends StatefulWidget { class _LocalPKSelectPage extends State { final TextEditingController connectpwdController = TextEditingController(); final MobileScannerController scannerController = MobileScannerController(); - bool isconnecting = false; bool isScaning = false; - Future connecting(BuildContext context) async { - if(isconnecting || connectpwdController.text.isEmpty) return; - setState(() { - isconnecting = true; - }); - late final int statue; - try { - statue = await context.read().testConnect(connectpwdController.text); - } catch (e) { - if(context.mounted) alart(context, "连接发生错误: $e"); - } - if(!context.mounted) return; - if(statue == 0){ - PKServer notifier = context.read(); - Navigator.push( - context, - MaterialPageRoute(builder: (context) => ChangeNotifierProvider.value( - value: notifier, - child: LocalPKPage(isServer: false), - )) - ); - } else if (statue == 1) { - alart(context, "联机口令错误"); - } else if (statue == 2) { - alart(context, "连接服务端失败\n请检查是否在同一局域网内及对方防火墙是否放行"); - } else if (statue == 3) { - alart(context, "版本校验不通过: 双方版本不一致"); - } else if (statue == 4) { - alart(context, "本地与服务端无可使用的相同词库"); - } - if(isScaning) scannerController.dispose(); - setState(() { - isconnecting = false; - }); + void connecting() { + if(connectpwdController.text.isEmpty) return; + Navigator.push( + context, + MaterialPageRoute(builder: (context) => + ChangeNotifierProvider( + create: (context) => PKServer(), + child: LocalPKPage(isServer: false, offer: connectpwdController.text), + ) + ) + ); } @override Widget build(BuildContext context) { context.read().uiLogger.info("构建局域网联机主页面"); MediaQueryData mediaQuery = MediaQuery.of(context); - PKServer notifier = context.read(); + return Scaffold( appBar: AppBar(title: Text("局域网联机")), body: Column( @@ -81,8 +56,8 @@ class _LocalPKSelectPage extends State { Navigator.push( context, MaterialPageRoute( - builder: (context) => ChangeNotifierProvider.value( - value: notifier, + builder: (context) => ChangeNotifierProvider( + create: (context) => PKServer(), child: LocalPKPage(isServer: true), ) ) @@ -107,19 +82,31 @@ class _LocalPKSelectPage extends State { ), suffix: ElevatedButton( onPressed: () async { - connecting(context); + connecting(); }, child: Text("加入") ), ), onSubmitted: (text) async { - connecting(context); + connecting(); }, ), SizedBox(height: mediaQuery.size.height * 0.02), - if(Platform.isAndroid) ElevatedButton.icon( - onPressed: () { - if(isScaning) scannerController.stop(); + ElevatedButton.icon( + onPressed: () async { + if(isScaning){ + await scannerController.stop(); + if(context.mounted) context.read().uiLogger.fine("已关闭摄像头"); + } else { + try { + Set lens = await scannerController.getSupportedLenses(); + if(context.mounted) context.read().uiLogger.fine(lens); + } catch (e) { + if(context.mounted) alart(context, "尝试启用相机时出现以下问题: $e"); + return; + } + } + setState(() { isScaning = !isScaning; }); @@ -134,13 +121,15 @@ class _LocalPKSelectPage extends State { controller: scannerController, fit: BoxFit.scaleDown, onDetect: (barcodes) { - if(5 > (barcodes.barcodes.first.rawValue??"").length || 8 < (barcodes.barcodes.first.rawValue??"").length) return; - connectpwdController.text = barcodes.barcodes.first.rawValue??""; - connecting(context); + if(barcodes.barcodes.isEmpty) return; + setState(() { + connectpwdController.text = barcodes.barcodes.first.rawValue??""; + isScaning = !isScaning; + }); + connecting(); }, ), - ), - if(isconnecting) CircularProgressIndicator() + ) ], ), ); @@ -149,53 +138,209 @@ class _LocalPKSelectPage extends State { class LocalPKPage extends StatefulWidget { final bool isServer; - const LocalPKPage({super.key, required this.isServer}); + final String? offer; + const LocalPKPage({super.key, required this.isServer, this.offer}); @override - State createState() => _ServerPage(); + State createState() => _LocalPKPage(); } -class _ServerPage extends State { +class _LocalPKPage extends State { final PageController pageController = PageController(); - bool clientWaited = false; + + @override + void initState() { + context.read().initHost(widget.isServer, context, offer: widget.offer); + super.initState(); + } @override Widget build(BuildContext context) { - if(!widget.isServer && !clientWaited) { - context.read().watingSelection(context, () => pageController.nextPage(duration: Duration(milliseconds: 500), curve: StaticsVar.curve)); - clientWaited = true; + if(context.read().pageController == null) { + context.read().setPageControler(pageController); } + return PopScope( canPop: true, onPopInvokedWithResult: (didPop, result) { - if(widget.isServer) { - context.read().stopHost(); - } - context.read().renew(); + context.read().disconnect(); }, child: PageView( physics: NeverScrollableScrollPhysics(), controller: pageController, children: [ - widget.isServer ? ServerHostWatingPage(pageController: pageController) : ClientWatingPage(), - PKPreparePage(pageController: pageController), - PKOngoingPage() + widget.isServer ? ServerHostWatingPage() : ClientWatingPage(), + widget.isServer ? PKClassSelectionPage() : ClientWatingPage(), + PKPreparePage(), + PKOngoingPage(), + PKErrorPage() ], ), ); } } -class ServerHostWatingPage extends StatelessWidget { - final PageController pageController; - const ServerHostWatingPage({super.key, required this.pageController}); +class ServerHostWatingPage extends StatefulWidget { + const ServerHostWatingPage({super.key}); + + @override + State createState() => _ServerHostWatingPage(); +} + +class _ServerHostWatingPage extends State { + bool isScaning = false; + bool isConnecting = false; + MobileScannerController scannerController = MobileScannerController(); + TextEditingController connectpwdController = TextEditingController(); + + Future connectClient() async { + try { + await context.read().loadAnswer(connectpwdController.text, context); + } catch (e) { + if(context.mounted) { + // ignore: use_build_context_synchronously + alart(context, "连接错误: $e", onConfirmed: () { + setState(() { + isConnecting = false; + }); + }); + } + } + } + + @override + void dispose() { + scannerController.dispose(); + super.dispose(); + } @override Widget build(BuildContext context) { context.read().uiLogger.info("构建局域网联机课程选择页面"); MediaQueryData mediaQuery = MediaQuery.of(context); - return context.watch().connected - ? Scaffold( + + return Scaffold( + appBar: AppBar(title: Text(context.watch().inited ? "准备连接" : "正在启动服务")), + body: Center( + child: context.watch().inited + ? Column( + children: [ + TextField( + autocorrect: false, + controller: connectpwdController, + expands: false, + maxLines: 1, + keyboardType: TextInputType.visiblePassword, + decoration: InputDecoration( + labelText: "联机口令", + border: OutlineInputBorder( + borderRadius: StaticsVar.br, + borderSide: BorderSide(color: Theme.of(context).colorScheme.outline), + ), + suffix: ElevatedButton( + onPressed: () async { + if(isConnecting) return; + setState(() { + isConnecting = true; + }); + connectClient(); + }, + child: Text("加入") + ), + ), + onSubmitted: (text) async { + if(isConnecting) return; + setState(() { + isConnecting = true; + }); + connectClient(); + }, + ), + if(!isConnecting) isScaning + ? SizedBox( + width: mediaQuery.size.width * 0.8, + height: mediaQuery.size.height * 0.4, + child: MobileScanner( + controller: scannerController, + fit: BoxFit.scaleDown, + onDetect: (barcodes) async { + if(barcodes.barcodes.isEmpty || isConnecting) return; + setState(() { + isConnecting = true; + isScaning = false; + scannerController.stop(); + }); + connectpwdController.text = barcodes.barcodes.first.rawValue!; + connectClient(); + }, + ), + ) + : Container( + padding: EdgeInsets.all(8.0), + decoration: BoxDecoration( + borderRadius: StaticsVar.br, + color: Colors.white + ), + child: QrImageView( + data: context.read().connectpwd!, + backgroundColor: Colors.white, + version: QrVersions.auto, + size: min(mediaQuery.size.width, mediaQuery.size.height) * 0.8, + ), + ), + + isConnecting + ? Column( + children: [ + CircularProgressIndicator(), + Text("正在构建连接"), + ], + ) + : Row( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + children: [ + ElevatedButton.icon( + onPressed: () { + if(isScaning) scannerController.stop(); + setState(() { + isScaning = !isScaning; + }); + }, + icon: Icon(isScaning ? Icons.stop : Icons.qr_code_scanner), + label: Text(isScaning ? "停止扫描" : "扫描对方的二维码") + ), + ElevatedButton.icon( + onPressed: () { + Clipboard.setData(ClipboardData(text: context.read().connectpwd!)); + }, + icon: Icon(Icons.copy), + label: Text("复制口令到剪切板") + ), + ], + ), + ], + ) + + : Column( + mainAxisSize: MainAxisSize.min, + children: [ + CircularProgressIndicator(), + Text("服务加载中...\n该过程或将需要一分钟") + ], + ) + ), + ); + } +} + +class PKClassSelectionPage extends StatelessWidget { + const PKClassSelectionPage({super.key}); + + @override + Widget build(BuildContext context) { + MediaQueryData mediaQuery = MediaQuery.of(context); + + return Scaffold( appBar: AppBar(title: Text("连接成功")), body: Center( child: Column( @@ -208,47 +353,15 @@ class ServerHostWatingPage extends StatelessWidget { ElevatedButton( onPressed: () async { ClassSelection selection = await popSelectClasses(context, forceSelectRange: context.read().selectableSource, withCache: false, withReviewChoose: false); - if(!context.mounted) return; - context.read().classSelection = selection; - pageController.nextPage(duration: Duration(milliseconds: 500), curve: StaticsVar.curve); + if(!context.mounted || selection.selectedClass.isEmpty) return; + context.read().setSelectedClass(selection); + context.read().pageController!.nextPage(duration: Duration(milliseconds: 500), curve: StaticsVar.curve); }, child: Text("开始选课") ) ], ), ), - ) - : FutureBuilder( - future: context.read().startHost(), - initialData: false, - builder: (context, snapshot) { - return Scaffold( - appBar: AppBar(title: Text(snapshot.data??false ? "等待其他人进入..." : "正在启动服务")), - body: Center( - child: snapshot.data??false - ? Center(child: Column( - children: [ - Text("联机口令:\n${context.read().connectpwd}", style: Theme.of(context).textTheme.displayMedium), - SizedBox(height: mediaQuery.size.height * 0.1), - Container( - padding: EdgeInsets.all(8.0), - decoration: BoxDecoration( - borderRadius: StaticsVar.br, - color: Colors.white - ), - child: QrImageView( - data: context.read().connectpwd!, - backgroundColor: Colors.white, - version: QrVersions.auto, - size: min(mediaQuery.size.width, mediaQuery.size.height) * 0.4, - ), - ), - ], - )) - : CircularProgressIndicator(semanticsLabel: "服务加载中") - ), - ); - } ); } } @@ -259,46 +372,54 @@ class ClientWatingPage extends StatelessWidget { @override Widget build(BuildContext context) { context.read().uiLogger.info("构建局域网联机等待页面"); - + MediaQueryData mediaQuery = MediaQuery.of(context); + return Scaffold( - appBar: AppBar(title: Text("连接成功")), + appBar: AppBar(title: Text(context.watch().inited ? "收集信息中" : context.watch().connected ? "已连接" : "等待连接")), body: Center(child: Column( mainAxisSize: MainAxisSize.min, children: [ + if(context.watch().inited && !context.watch().connected) + ...[Container( + padding: EdgeInsets.all(8.0), + decoration: BoxDecoration( + borderRadius: StaticsVar.br, + color: Colors.white + ), + child: QrImageView( + data: context.read().connectpwd!, + backgroundColor: Colors.white, + version: QrVersions.auto, + size: min(mediaQuery.size.width, mediaQuery.size.height) * 0.8, + ), + ), + ElevatedButton.icon( + onPressed: () { + Clipboard.setData(ClipboardData(text: context.read().connectpwd!)); + }, + icon: Icon(Icons.copy), + label: Text("复制口令到剪切板") + )], + CircularProgressIndicator(), - Text("正在等待房主选择课程") + Text(context.read().inited ? + context.watch().selectableSource.isNotEmpty ? "正在等待房主选择课程" : "请将以上二维码给对方扫描或传递口令" + : "正在收集信息生成认证\n此过程或将需要一分钟") ] )) ); } } -class PKPreparePage extends StatefulWidget { - final PageController pageController; - const PKPreparePage({super.key, required this.pageController}); - - @override - State createState() => _PKPreparePage(); -} +class PKPreparePage extends StatelessWidget { + const PKPreparePage({super.key}); -class _PKPreparePage extends State { - bool watching = false; - bool downCount = false; @override Widget build(BuildContext context) { context.read().uiLogger.info("构建局域网联机准备页面"); MediaQueryData mediaQuery = MediaQuery.of(context); - if(!watching && !context.read().started) { - context.read().watingPrepare(context); - } - if(context.read().startTime != null&&!downCount) { - downCount = true; - Future.delayed(context.read().startTime!.difference(DateTime.now()), () { - if(!context.mounted) return; - widget.pageController.nextPage(duration: Duration(milliseconds: 500), curve: StaticsVar.curve); - }); - } + return Scaffold( appBar: AppBar(title: Text("请准备")), body: Center( @@ -332,9 +453,7 @@ class _PKPreparePage extends State { shape: RoundedRectangleBorder(borderRadius: StaticsVar.br) ), onPressed: (){ - setState(() { - context.read().preparedP1 = true; - }); + context.read().setPrepare(); }, child: Text("准备") ), @@ -361,7 +480,7 @@ class _PKPreparePage extends State { ) ], ), - if(downCount) TweenAnimationBuilder( + if(context.watch().startTime != null) TweenAnimationBuilder( tween: Tween( begin: 1, end: 0 @@ -394,22 +513,21 @@ class _PKOngoingPage extends State { PageController pageController = PageController(); List> choiceOptions = []; + @override + void initState() { + Random rnd = Random(context.read().rndSeed); + for(WordItem wordItem in context.read().pkState.testWords) { + List optionWords = getRandomWords(4, context.read().wordData, allowRepet: false, include: wordItem, shuffle: true, rnd: rnd); + choiceOptions.add(List.generate(4, (int index) => optionWords[index].chinese)); + } + super.initState(); + } + @override Widget build(BuildContext context) { context.read().uiLogger.info("构建局域网联机对局页面"); MediaQueryData mediaQuery = MediaQuery.of(context); - if(state == 0) { - if(!context.read().started) context.read().initPK(context); - - Random rnd = Random(context.read().rndSeed); - for(WordItem wordItem in context.read().pkState.testWords) { - List optionWords = getRandomWords(4, context.read().wordData, allowRepet: false, include: wordItem, shuffle: true, rnd: rnd); - choiceOptions.add(List.generate(4, (int index) => optionWords[index].chinese)); - } - state++; - } - return PopScope( canPop: false, child: Scaffold( @@ -426,7 +544,6 @@ class _PKOngoingPage extends State { physics: NeverScrollableScrollPhysics(), itemBuilder: (context, index) { if(index == context.read().pkState.testWords.length) { - context.read().pkState.selfTookenTime ??= DateTime.now().difference(context.read().startTime!).inSeconds; if(context.read().pkState.sideTookenTime == null) { return Center( child: Text("等待对方完成中...", style: Theme.of(context).textTheme.headlineSmall), @@ -682,4 +799,28 @@ class TopScoreBar extends StatelessWidget { ], ); } +} + +class PKErrorPage extends StatelessWidget { + const PKErrorPage({super.key}); + + @override + Widget build(BuildContext context) { + return Material( + child: Center( + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Icon(Icons.link_off, size: 64), + TextContainer(text: "连接丢失"), + TextContainer(text: "原因: ${context.read().exitMessage??"未知"}"), + ElevatedButton( + onPressed: () => Navigator.pop(context), + child: Text("返回") + ) + ], + ), + ), + ); + } } \ No newline at end of file diff --git a/lib/vars/config_structure.dart b/lib/vars/config_structure.dart index 1cec2b8..e4c56f6 100644 --- a/lib/vars/config_structure.dart +++ b/lib/vars/config_structure.dart @@ -696,19 +696,8 @@ class SourceItem { return classesMap; } - Map> toComparableMap(List words) { - Map>> classesMap = {}; - for(ClassItem classItem in subClasses) { - classesMap[classItem.className] = []; - for(int index in classItem.wordIndexs) { - classesMap[classItem.className]!.add(words[index].toMap()); - } - } - return classesMap; - } - String getHash(List words) { - return sha256.convert(utf8.encode(toComparableMap(words).toString())).toString(); + return sha256.convert(utf8.encode(sourceJsonFileName)).toString(); } } diff --git a/linux/flutter/generated_plugin_registrant.cc b/linux/flutter/generated_plugin_registrant.cc index faea87d..5c6bed2 100644 --- a/linux/flutter/generated_plugin_registrant.cc +++ b/linux/flutter/generated_plugin_registrant.cc @@ -6,11 +6,15 @@ #include "generated_plugin_registrant.h" +#include #include #include #include void fl_register_plugins(FlPluginRegistry* registry) { + g_autoptr(FlPluginRegistrar) flutter_webrtc_registrar = + fl_plugin_registry_get_registrar_for_plugin(registry, "FlutterWebRTCPlugin"); + flutter_web_r_t_c_plugin_register_with_registrar(flutter_webrtc_registrar); g_autoptr(FlPluginRegistrar) screen_retriever_linux_registrar = fl_plugin_registry_get_registrar_for_plugin(registry, "ScreenRetrieverLinuxPlugin"); screen_retriever_linux_plugin_register_with_registrar(screen_retriever_linux_registrar); diff --git a/linux/flutter/generated_plugins.cmake b/linux/flutter/generated_plugins.cmake index 420ab04..4c260d1 100644 --- a/linux/flutter/generated_plugins.cmake +++ b/linux/flutter/generated_plugins.cmake @@ -3,6 +3,7 @@ # list(APPEND FLUTTER_PLUGIN_LIST + flutter_webrtc screen_retriever_linux url_launcher_linux window_manager diff --git a/macos/Flutter/GeneratedPluginRegistrant.swift b/macos/Flutter/GeneratedPluginRegistrant.swift index 2bf2e54..42127b8 100644 --- a/macos/Flutter/GeneratedPluginRegistrant.swift +++ b/macos/Flutter/GeneratedPluginRegistrant.swift @@ -9,6 +9,7 @@ import audio_session import file_picker import flutter_local_notifications import flutter_tts +import flutter_webrtc import just_audio import mobile_scanner import network_info_plus @@ -22,6 +23,7 @@ func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { FilePickerPlugin.register(with: registry.registrar(forPlugin: "FilePickerPlugin")) FlutterLocalNotificationsPlugin.register(with: registry.registrar(forPlugin: "FlutterLocalNotificationsPlugin")) FlutterTtsPlugin.register(with: registry.registrar(forPlugin: "FlutterTtsPlugin")) + FlutterWebRTCPlugin.register(with: registry.registrar(forPlugin: "FlutterWebRTCPlugin")) JustAudioPlugin.register(with: registry.registrar(forPlugin: "JustAudioPlugin")) MobileScannerPlugin.register(with: registry.registrar(forPlugin: "MobileScannerPlugin")) NetworkInfoPlusPlugin.register(with: registry.registrar(forPlugin: "NetworkInfoPlusPlugin")) diff --git a/pubspec.lock b/pubspec.lock index 7c9501c..a8972b2 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -21,10 +21,10 @@ packages: dependency: "direct main" description: name: archive - sha256: "2fde1607386ab523f7a36bb3e7edb43bd58e6edaf2ffb29d8a6d578b297fdbbd" + sha256: a96e8b390886ee8abb49b7bd3ac8df6f451c621619f52a26e815fdcf568959ff url: "https://pub.dev" source: hosted - version: "4.0.7" + version: "4.0.9" args: dependency: transitive description: @@ -185,6 +185,14 @@ packages: url: "https://pub.dev" source: hosted version: "3.0.15" + dart_webrtc: + dependency: transitive + description: + name: dart_webrtc + sha256: "4ed7b9fa9924e5a81eb39271e2c2356739dd1039d60a13b86ba6c5f448625086" + url: "https://pub.dev" + source: hosted + version: "1.7.0" dbus: dependency: transitive description: @@ -336,6 +344,14 @@ packages: description: flutter source: sdk version: "0.0.0" + flutter_webrtc: + dependency: "direct main" + description: + name: flutter_webrtc + sha256: c549ea8ffb20167110ad0a28e5f17a2650b5bea8837d984898cd9b0ffd5fa78b + url: "https://pub.dev" + source: hosted + version: "1.3.1" frontend_server_client: dependency: transitive description: @@ -384,14 +400,6 @@ packages: url: "https://pub.dev" source: hosted version: "1.6.0" - http_methods: - dependency: transitive - description: - name: http_methods - sha256: "6bccce8f1ec7b5d701e7921dca35e202d425b57e317ba1a37f2638590e29e566" - url: "https://pub.dev" - source: hosted - version: "1.1.1" http_multi_server: dependency: transitive description: @@ -412,18 +420,18 @@ packages: dependency: "direct main" description: name: idb_shim - sha256: cc994e1095d69f2bcdf929246112ed0772d59d75be8a58995667acc2df091602 + sha256: "921301da0a735f336a28fc35c3abdbd4498895cc205fa1ea9f7e785e7d854ceb" url: "https://pub.dev" source: hosted - version: "2.8.2+3" + version: "2.8.2+4" image: dependency: transitive description: name: image - sha256: "492bd52f6c4fbb6ee41f781ff27765ce5f627910e1e0cbecfa3d9add5562604c" + sha256: f9881ff4998044947ec38d098bc7c8316ae1186fa786eddffdb867b9bc94dfce url: "https://pub.dev" source: hosted - version: "4.7.2" + version: "4.8.0" io: dependency: transitive description: @@ -432,14 +440,22 @@ packages: url: "https://pub.dev" source: hosted version: "1.0.5" + js: + dependency: transitive + description: + name: js + sha256: "53385261521cc4a0c4658fd0ad07a7d14591cf8fc33abbceae306ddb974888dc" + url: "https://pub.dev" + source: hosted + version: "0.7.2" json_annotation: dependency: transitive description: name: json_annotation - sha256: "805fa86df56383000f640384b282ce0cb8431f1a7a2396de92fb66186d8c57df" + sha256: cb09e7dac6210041fad964ed7fbee004f14258b4eca4040f72d1234062ace4c8 url: "https://pub.dev" source: hosted - version: "4.10.0" + version: "4.11.0" just_audio: dependency: "direct main" description: @@ -504,6 +520,14 @@ packages: url: "https://pub.dev" source: hosted version: "6.1.0" + logger: + dependency: transitive + description: + name: logger + sha256: a7967e31b703831a893bbc3c3dd11db08126fe5f369b5c648a36f821979f5be3 + url: "https://pub.dev" + source: hosted + version: "2.6.2" logging: dependency: "direct main" description: @@ -692,10 +716,10 @@ packages: dependency: transitive description: name: petitparser - sha256: "1a97266a94f7350d30ae522c0af07890c70b8e62c71e8e3920d1db4d23c057d1" + sha256: "91bd59303e9f769f108f8df05e371341b15d59e995e6806aefab827b58336675" url: "https://pub.dev" source: hosted - version: "7.0.1" + version: "7.0.2" platform: dependency: transitive description: @@ -724,10 +748,10 @@ packages: dependency: transitive description: name: posix - sha256: "6323a5b0fa688b6a010df4905a56b00181479e6d10534cecfecede2aa55add61" + sha256: "185ef7606574f789b40f289c233efa52e96dead518aed988e040a10737febb07" url: "https://pub.dev" source: hosted - version: "6.0.3" + version: "6.5.0" provider: dependency: "direct main" description: @@ -852,10 +876,10 @@ packages: dependency: transitive description: name: shared_preferences_android - sha256: cbc40be9be1c5af4dab4d6e0de4d5d3729e6f3d65b89d21e1815d57705644a6f + sha256: "8374d6200ab33ac99031a852eba4c8eb2170c4bf20778b3e2c9eccb45384fb41" url: "https://pub.dev" source: hosted - version: "2.4.20" + version: "2.4.21" shared_preferences_foundation: dependency: transitive description: @@ -897,7 +921,7 @@ packages: source: hosted version: "2.4.1" shelf: - dependency: "direct main" + dependency: transitive description: name: shelf sha256: e7dd780a7ffb623c57850b33f43309312fc863fb6aa3d276a754bb299839ef12 @@ -912,14 +936,6 @@ packages: url: "https://pub.dev" source: hosted version: "3.0.2" - shelf_router: - dependency: "direct main" - description: - name: shelf_router - sha256: f5e5d492440a7fb165fe1e2e1a623f31f734d3370900070b2b1e0d0428d59864 - url: "https://pub.dev" - source: hosted - version: "1.1.4" shelf_static: dependency: transitive description: @@ -940,50 +956,50 @@ packages: dependency: "direct main" description: name: sherpa_onnx - sha256: de41c9468ae24391c14de2cf9573fab6699a222e77c2f440db9c6136b31334e5 + sha256: "3e864a08018656f1c51d17a0aa43d021a2f78a7bb5cff15476d5a0dc4dff2956" url: "https://pub.dev" source: hosted - version: "1.12.24" + version: "1.12.28" sherpa_onnx_android: dependency: transitive description: name: sherpa_onnx_android - sha256: "9c82017bfc05c3362e29360fa0a10c28cfdf9a073d06c41414a067d02c0ca752" + sha256: bc690be549b33ff0bc5d518ca9b4df1013a175ef84c25646298d1bedd3a6e305 url: "https://pub.dev" source: hosted - version: "1.12.24" + version: "1.12.28" sherpa_onnx_ios: dependency: transitive description: name: sherpa_onnx_ios - sha256: "6509ac8a3f72bfd29bb0cc93b29aa721bdea974a5770453abba63c80d6d99dac" + sha256: "4f0bba765dafd9c9062a3be062aa11f7a012a0b34964244e3588c824d5fd6bbc" url: "https://pub.dev" source: hosted - version: "1.12.24" + version: "1.12.28" sherpa_onnx_linux: dependency: transitive description: name: sherpa_onnx_linux - sha256: "50a99ddb5f9c94bed4295aa5c462555850769dbfcf83ecdd8cbf30125a43dd51" + sha256: bcad6925062f7a79a4f3b4b672d2485f5b02074e31d5b7f0a4f1ddf718a8a821 url: "https://pub.dev" source: hosted - version: "1.12.24" + version: "1.12.28" sherpa_onnx_macos: dependency: transitive description: name: sherpa_onnx_macos - sha256: a2666a3b9f5a6b3cf020bbd8fe60cbfed6d61b29997f9e97eb2f398916da3474 + sha256: d6718808d4384e2babca6718b9bac668e98ed8d00a1492ac4d09ecf2b406f6e6 url: "https://pub.dev" source: hosted - version: "1.12.24" + version: "1.12.28" sherpa_onnx_windows: dependency: transitive description: name: sherpa_onnx_windows - sha256: d009a11263a3976af79e2b69f322c695aa80c344c40fb4b4417a294aae06a25d + sha256: ef6bc7a4c313d8ca710513a206fb650c4527e955068d8b4137b5a846f4809ead url: "https://pub.dev" source: hosted - version: "1.12.24" + version: "1.12.28" sky_engine: dependency: transitive description: flutter @@ -1169,10 +1185,10 @@ packages: dependency: transitive description: name: uuid - sha256: a11b666489b1954e01d992f3d601b1804a33937b5a8fe677bd26b8a9f96f96e8 + sha256: "1fef9e8e11e2991bb773070d4656b7bd5d850967a2456cfc83cf47925ba79489" url: "https://pub.dev" source: hosted - version: "4.5.2" + version: "4.5.3" vector_math: dependency: transitive description: @@ -1237,6 +1253,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.2.1" + webrtc_interface: + dependency: transitive + description: + name: webrtc_interface + sha256: ad0e5786b2acd3be72a3219ef1dde9e1cac071cf4604c685f11b61d63cdd6eb3 + url: "https://pub.dev" + source: hosted + version: "1.4.0" win32: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index b198354..26c909e 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -50,13 +50,11 @@ dependencies: logging: ^1.3.0 flutter_local_notifications: ^20.1.0 workmanager: ^0.9.0+3 - shelf: ^1.4.2 - shelf_router: ^1.1.4 + flutter_webrtc: ^1.3.1 network_info_plus: ^7.0.0 crypto: ^3.0.7 qr_flutter: ^4.1.0 mobile_scanner: ^7.2.0 - # file_saver: ^0.3.1 cupertino_icons: ^1.0.8 bk_tree: ^0.1.2 diff --git a/windows/flutter/generated_plugin_registrant.cc b/windows/flutter/generated_plugin_registrant.cc index 37e9265..b74446c 100644 --- a/windows/flutter/generated_plugin_registrant.cc +++ b/windows/flutter/generated_plugin_registrant.cc @@ -7,6 +7,7 @@ #include "generated_plugin_registrant.h" #include +#include #include #include #include @@ -15,6 +16,8 @@ void RegisterPlugins(flutter::PluginRegistry* registry) { FlutterTtsPluginRegisterWithRegistrar( registry->GetRegistrarForPlugin("FlutterTtsPlugin")); + FlutterWebRTCPluginRegisterWithRegistrar( + registry->GetRegistrarForPlugin("FlutterWebRTCPlugin")); JustAudioWindowsPluginRegisterWithRegistrar( registry->GetRegistrarForPlugin("JustAudioWindowsPlugin")); ScreenRetrieverWindowsPluginCApiRegisterWithRegistrar( diff --git a/windows/flutter/generated_plugins.cmake b/windows/flutter/generated_plugins.cmake index 4ba7ad7..f70a832 100644 --- a/windows/flutter/generated_plugins.cmake +++ b/windows/flutter/generated_plugins.cmake @@ -4,6 +4,7 @@ list(APPEND FLUTTER_PLUGIN_LIST flutter_tts + flutter_webrtc just_audio_windows screen_retriever_windows url_launcher_windows