diff --git a/lib/customWidget/toast_util.dart b/lib/customWidget/toast_util.dart new file mode 100644 index 0000000..3488d00 --- /dev/null +++ b/lib/customWidget/toast_util.dart @@ -0,0 +1,107 @@ +import 'package:flutter/material.dart'; +import 'package:fluttertoast/fluttertoast.dart'; + +class ToastUtil { + /// 普通灰色背景提示(仅文字) + static void showNormal(BuildContext context, String message, { + ToastGravity gravity = ToastGravity.BOTTOM, + int duration = 2, + }) { + _showToast( + context: context, + message: message, + gravity: gravity, + duration: duration, + ); + } + + /// 成功提示(带图标) + static void showSuccess(BuildContext context, String message, { + ToastGravity gravity = ToastGravity.CENTER, + int duration = 3, + }) { + _showToast( + context: context, + message: message, + gravity: gravity, + duration: duration, + isSuccess: true, + ); + } + + /// 失败提示(带图标) + static void showError(BuildContext context, String message, { + ToastGravity gravity = ToastGravity.CENTER, + int duration = 4, + }) { + _showToast( + context: context, + message: message, + gravity: gravity, + duration: duration, + isError: true, + ); + } + + /// 内部通用方法 + static void _showToast({ + required BuildContext context, + required String message, + required ToastGravity gravity, + required int duration, + bool isSuccess = false, + bool isError = false, + }) { + // 如果有图标(成功或失败),则使用自定义 Widget + if (isSuccess || isError) { + final fToast = FToast(); + fToast.init(context); + fToast.showToast( + child: _buildIconToast(message, isSuccess: isSuccess), + gravity: gravity, + toastDuration: Duration(seconds: duration), + ); + } + // 普通文字提示 + else { + Fluttertoast.showToast( + msg: message, + toastLength: duration > 2 ? Toast.LENGTH_LONG : Toast.LENGTH_SHORT, + gravity: gravity, + backgroundColor: Colors.grey[500], + textColor: Colors.white, + fontSize: 16.0, + ); + } + } + + /// 构建带图标的 Toast Widget + static Widget _buildIconToast(String message, {bool isSuccess = true}) { + return Container( + padding: const EdgeInsets.symmetric(horizontal: 24.0, vertical: 16.0), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(25.0), + color: Colors.grey[850]?.withOpacity(0.9), + ), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Icon( + isSuccess ? Icons.check : Icons.error_outline, + color: isSuccess ? Colors.greenAccent[400] : Colors.redAccent[400], + size: 36.0, + ), + const SizedBox(height: 8.0), + Text( + message, + style: const TextStyle( + color: Colors.white, + fontSize: 16.0, + ), + textAlign: TextAlign.center, + ), + ], + ), + ); + } +} \ No newline at end of file diff --git a/lib/customWidget/video_player_widget.dart b/lib/customWidget/video_player_widget.dart new file mode 100644 index 0000000..a667e71 --- /dev/null +++ b/lib/customWidget/video_player_widget.dart @@ -0,0 +1,326 @@ +import 'dart:async'; +import 'dart:math'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:video_player/video_player.dart'; + +class VideoPlayerWidget extends StatefulWidget { + final VideoPlayerController? controller; + final String coverUrl; + final double aspectRatio; + final bool allowSeek; + final bool isFullScreen; + + const VideoPlayerWidget({ + Key? key, + required this.controller, + required this.coverUrl, + required this.aspectRatio, + this.allowSeek = true, + this.isFullScreen = false, + }) : super(key: key); + + @override + _VideoPlayerWidgetState createState() => _VideoPlayerWidgetState(); +} + +class _VideoPlayerWidgetState extends State { + bool _visibleControls = true; + Timer? _hideTimer; + late Timer _positionTimer; + Duration _currentPosition = Duration.zero; + Duration _totalDuration = Duration.zero; + bool _isPlaying = false; + final ValueNotifier _sliderValue = ValueNotifier(0.0); + + @override + void initState() { + super.initState(); + _startHideTimer(); + _startPositionTimer(); + + if (widget.controller != null) { + widget.controller!.addListener(_controllerListener); + if (widget.controller!.value.isInitialized) { + _updateControllerValues(); + } + } + } + + @override + void didUpdateWidget(covariant VideoPlayerWidget oldWidget) { + super.didUpdateWidget(oldWidget); + if (widget.controller != oldWidget.controller) { + oldWidget.controller?.removeListener(_controllerListener); + widget.controller?.addListener(_controllerListener); + _updateControllerValues(); + } + } + + void _controllerListener() { + if (mounted) setState(() {}); + + if (mounted && widget.controller != null) { + _updateControllerValues(); + } + } + + void _updateControllerValues() { + final controller = widget.controller!; + setState(() { + _isPlaying = controller.value.isPlaying; + _totalDuration = controller.value.duration; + _currentPosition = controller.value.position; + _sliderValue.value = _currentPosition.inMilliseconds.toDouble(); + }); + } + + @override + void dispose() { + _hideTimer?.cancel(); + _positionTimer.cancel(); + _sliderValue.dispose(); + widget.controller?.removeListener(_controllerListener); + super.dispose(); + } + + void _startHideTimer() { + _hideTimer?.cancel(); + _hideTimer = Timer(const Duration(seconds: 3), () { + if (mounted) setState(() => _visibleControls = false); + }); + } + + void _startPositionTimer() { + _positionTimer = Timer.periodic(const Duration(milliseconds: 200), (_) { + if (mounted && widget.controller != null && widget.controller!.value.isInitialized) { + setState(() { + _currentPosition = widget.controller!.value.position; + _totalDuration = widget.controller!.value.duration; + _isPlaying = widget.controller!.value.isPlaying; + _sliderValue.value = _currentPosition.inMilliseconds.toDouble(); + }); + } + }); + } + + void _toggleControls() { + setState(() => _visibleControls = !_visibleControls); + if (_visibleControls) _startHideTimer(); + else _hideTimer?.cancel(); + } + + void _togglePlayPause() { + if (widget.controller == null) return; + + setState(() { + _isPlaying = !_isPlaying; + }); + + if (_isPlaying) { + widget.controller!.play(); + } else { + widget.controller!.pause(); + } + _startHideTimer(); + } + + void _enterFullScreen() { + // 锁定横屏 + SystemChrome.setPreferredOrientations([ + DeviceOrientation.landscapeLeft, + DeviceOrientation.landscapeRight, + ]); + // 设置全屏模式 + SystemChrome.setEnabledSystemUIMode(SystemUiMode.immersiveSticky); + + Navigator.of(context).push(MaterialPageRoute( + builder: (ctx) => Scaffold( + backgroundColor: Colors.black, + // 使用SafeArea包裹整个全屏播放器 + body: SafeArea( + top: false, // 顶部不使用安全区域(状态栏区域) + bottom: false, // 底部不使用安全区域(导航栏区域) + child: Stack( + children: [ + // 全屏视频播放器 + VideoPlayerWidget( + controller: widget.controller, + coverUrl: widget.coverUrl, + aspectRatio: max( + widget.aspectRatio, + MediaQuery.of(ctx).size.width / MediaQuery.of(ctx).size.height + ), + allowSeek: widget.allowSeek, + isFullScreen: true, + ), + // 添加退出按钮,并包裹在SafeArea中 + SafeArea( + child: Align( + alignment: Alignment.topLeft, + child: Padding( + padding: const EdgeInsets.all(16.0), + child: GestureDetector( + onTap: () => Navigator.of(context).pop(), + child: Container( + padding: const EdgeInsets.all(6), + decoration: BoxDecoration( + color: Colors.black38, + shape: BoxShape.circle, + ), + child: const Icon( + Icons.arrow_back, + color: Colors.white, + size: 20, + ), + ), + ), + ), + ), + ), + ], + ), + ), + ), + )).then((_) { + // 恢复竖屏 + SystemChrome.setPreferredOrientations([DeviceOrientation.portraitUp]); + // 恢复系统UI + SystemChrome.setEnabledSystemUIMode(SystemUiMode.edgeToEdge); + }); + } + + @override + Widget build(BuildContext context) { + final screenSize = MediaQuery.of(context).size; + final fullScreenAspectRatio = max( + widget.aspectRatio, + screenSize.width / screenSize.height + ); + + return GestureDetector( + onTap: _toggleControls, + child: Stack( + fit: widget.isFullScreen ? StackFit.expand : StackFit.loose, + children: [ + // 视频播放区域 + if (widget.controller != null && widget.controller!.value.isInitialized) + AspectRatio( + aspectRatio: widget.isFullScreen ? fullScreenAspectRatio : widget.aspectRatio, + child: VideoPlayer(widget.controller!), + ) + else + Image.network( + widget.coverUrl, + fit: BoxFit.cover, + width: widget.isFullScreen ? double.infinity : null, + height: widget.isFullScreen ? double.infinity : null, + ), + + // 控制面板 + if (_visibleControls) + Positioned( + bottom: 0, + left: 0, + right: 0, + child: Container( + height: 50, + decoration: BoxDecoration( + gradient: LinearGradient( + begin: Alignment.bottomCenter, + end: Alignment.topCenter, + colors: [Colors.black.withOpacity(0.7), Colors.transparent], + ), + ), + padding: const EdgeInsets.symmetric(horizontal: 12), + child: Row( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + IconButton( + padding: EdgeInsets.zero, + icon: Icon( + _isPlaying ? Icons.pause : Icons.play_arrow, + size: 28, + color: Colors.white, + ), + onPressed: _togglePlayPause, + ), + const SizedBox(width: 0), + Expanded( + child: ValueListenableBuilder( + valueListenable: _sliderValue, + builder: (context, value, child) { + return SliderTheme( + data: SliderTheme.of(context).copyWith( + activeTrackColor: Colors.white, + inactiveTrackColor: Colors.white54, + thumbColor: Colors.white, + overlayColor: Colors.white24, + trackHeight: 2, + thumbShape: const RoundSliderThumbShape(enabledThumbRadius: 8), + ), + child: Slider( + value: value, + min: 0, + max: _totalDuration.inMilliseconds.toDouble(), + onChanged: widget.allowSeek && widget.controller != null + ? (v) { + widget.controller!.seekTo(Duration(milliseconds: v.toInt())); + setState(() { + _currentPosition = Duration(milliseconds: v.toInt()); + }); + _sliderValue.value = v; + _startHideTimer(); + } + : null, + ), + ); + } + ), + ), + const SizedBox(width: 0), + // 使用固定宽度的文本容器,避免进度条跳动 + SizedBox( + width: 110, // 固定宽度,防止进度条长度变化 + child: Text( + '${_formatDuration(_currentPosition)} / ${_formatDuration(_totalDuration)}', + style: const TextStyle( + color: Colors.white, + fontSize: 12, + fontFeatures: [FontFeature.tabularFigures()], // 等宽字体 + ), + ), + ), + const SizedBox(width: 0), + IconButton( + padding: EdgeInsets.zero, + icon: Icon( + widget.isFullScreen ? Icons.fullscreen_exit : Icons.fullscreen, + size: 28, + color: Colors.white, + ), + onPressed: () { + widget.isFullScreen ? Navigator.of(context).pop() : _enterFullScreen(); + }, + ), + ], + ), + ), + ), + ], + ), + ); + } + + String _formatDuration(Duration d) { + String twoDigits(int n) => n.toString().padLeft(2, '0'); + final hours = d.inHours; + final minutes = d.inMinutes.remainder(60); + final seconds = d.inSeconds.remainder(60); + + if (hours > 0) { + return '${twoDigits(hours)}:${twoDigits(minutes)}:${twoDigits(seconds)}'; + } + return '${twoDigits(minutes)}:${twoDigits(seconds)}'; + } +} \ No newline at end of file diff --git a/lib/http/ApiService.dart b/lib/http/ApiService.dart index 02a009f..20e6669 100644 --- a/lib/http/ApiService.dart +++ b/lib/http/ApiService.dart @@ -6,23 +6,30 @@ import 'package:qhd_prevention/tools/tools.dart'; import 'HttpManager.dart'; class ApiService { - // static const String basePath = "http://192.168.0.25:28199/"; - // static const String basePath = "http://192.168.20.240:8500/integrated_whb"; - // static const String baseFacePath = "http://192.168.0.25:38199/"; - // 人脸识别服务 - // static const String baseFacePath = "https://qaaqwh.qhdsafety.com/whb_stu_face/"; - - // static const String basePath = "https://qaaqwh.qhdsafety.com/integrated_whb/"; + // /// 人脸识别服务 + // static const String baseFacePath = + // "https://qaaqwh.qhdsafety.com/whb_stu_face/"; + // + // /// 登录及其他管理后台接口 + // static const String basePath = "https://qaaqwh.qhdsafety.com/integrated_whb"; + // + // /// 图片文件服务 // static const String baseImgPath = "https://file.zcloudchina.com/YTHFile"; - // static const String adminPath = "https://qaaqwh.qhdsafety.com/integrated_whb/"; - // static const String projectManagerUrl = 'https://pm.qhdsafety.com/zy-projectManage/'; - // static const String publicKey = 'MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDUoHAavCikaZxjlDM6Km8cX+ye78F4oF39AcEfnE1p2Yn9pJ9WFxYZ4Vkh6F8SKMi7k4nYsKceqB1RwG996SvHQ5C3pM3nbXCP4K15ad6QhN4a7lzlbLhiJcyIKszvvK8ncUDw8mVQ0j/2mwxv05yH6LN9OKU6Hzm1ninpWeE+awIDAQAB' + // + // /// 管理后台统一路径 + // static const String adminPath = + // "https://qaaqwh.qhdsafety.com/integrated_whb/"; + // + // /// 项目管理系统 + // static const String projectManagerUrl = + // 'https://pm.qhdsafety.com/zy-projectManage'; + /// 人脸识别服务 static const String baseFacePath = "https://qaaqwh.qhdsafety.com/whb_stu_face/"; /// 登录及其他管理后台接口 - static const String basePath = "https://qaaqwh.qhdsafety.com/integrated_whb"; + static const String basePath = "http://192.168.20.240:8500/integrated_whb/"; /// 图片文件服务 static const String baseImgPath = "https://file.zcloudchina.com/YTHFile"; @@ -270,6 +277,7 @@ U6Hzm1ninpWeE+awIDAQAB }, ); } + /// 视频详情 static Future> fnGetVideoPlayInfo(String VIDEOCOURSEWARE_ID) { return HttpManager().request( basePath, @@ -283,7 +291,7 @@ U6Hzm1ninpWeE+awIDAQAB }, ); } - static Future> fnSetUserFaceTime(String FACE_TIME) { + static Future> fnSetUserFaceTime(int FACE_TIME) { return HttpManager().request( baseFacePath, '/app/user/setUserFaceTime', @@ -297,6 +305,35 @@ U6Hzm1ninpWeE+awIDAQAB }, ); } + static Future> fnGetUserFaceTime(int FACE_TIME) { + return HttpManager().request( + baseFacePath, + '/app/user/getUserFaceTime', + method: Method.post, + data: { + 'loading':false, + 'FACE_TIME': FACE_TIME, + 'CORPINFO_ID': SessionService.instance.corpinfoId, + 'USER_ID': SessionService.instance.loginUserId, + + }, + ); + } + /// 清除人脸时间 + static Future> fnClearUserFaceTime() { + return HttpManager().request( + baseFacePath, + '/app/user/clearUserFaceTime', + method: Method.post, + data: { + 'loading':false, + 'CORPINFO_ID': SessionService.instance.corpinfoId, + 'USER_ID': SessionService.instance.loginUserId, + }, + ); + } + + static Future> fnGetVideoPlayProgress(String VIDEOCOURSEWARE_ID, String CURRICULUM_ID, String CLASS_ID, String STUDENT_ID) { return HttpManager().request( basePath, @@ -320,6 +357,7 @@ U6Hzm1ninpWeE+awIDAQAB '/app/edu/coursestudyvideorecord/save', method: Method.post, data: { + 'USERNAME': SessionService.instance.username, 'VIDEOCOURSEWARE_ID': VIDEOCOURSEWARE_ID, 'CURRICULUM_ID': CURRICULUM_ID, 'CHAPTER_ID': CHAPTER_ID, @@ -329,7 +367,6 @@ U6Hzm1ninpWeE+awIDAQAB 'CLASSCURRICULUM_ID': CLASSCURRICULUM_ID, 'STUDENT_ID': STUDENT_ID, 'loading': false, - 'USER_NAME': SessionService.instance.username, 'CORPINFO_ID': SessionService.instance.corpinfoId, 'USER_ID': SessionService.instance.loginUserId, @@ -505,7 +542,7 @@ U6Hzm1ninpWeE+awIDAQAB ); } /// 更新人脸信息 - static Future> reloadMyFace(String imagePath) async { + static Future> reloadMyFace(String imagePath, String studentId) async { final file = File(imagePath); if (!await file.exists()) { throw ApiException('file_not_found', '图片不存在:$imagePath'); diff --git a/lib/http/HttpManager.dart b/lib/http/HttpManager.dart index a068696..a4d8237 100644 --- a/lib/http/HttpManager.dart +++ b/lib/http/HttpManager.dart @@ -1,4 +1,5 @@ import 'dart:io'; +import 'dart:ui'; import 'package:dio/dio.dart'; /// 全局接口异常 @@ -6,6 +7,7 @@ class ApiException implements Exception { final String result; final String message; ApiException(this.result, this.message); + @override String toString() => 'ApiException($result): $message'; } @@ -30,11 +32,32 @@ class HttpManager { factory HttpManager() => _instance; late final Dio _dio; + // 添加401处理回调 + static VoidCallback? onUnauthorized; + void _initInterceptors() { _dio.interceptors ..add(LogInterceptor(request: true, responseBody: true, error: true)) ..add(InterceptorsWrapper(onError: (err, handler) { - // 全局错误处理,可根据 err.response?.statusCode 或 err.type 做不同处理 + // 捕获401错误 + if (err.response?.statusCode == 401) { + // 触发全局登出回调 + onUnauthorized?.call(); + // 创建自定义异常 + final apiException = ApiException( + '提示', + '您的账号已在其他设备登录,已自动下线' + ); + // 直接抛出业务异常,跳过后续错误处理 + return handler.reject( + DioException( + requestOptions: err.requestOptions, + error: apiException, + response: err.response, + type: DioExceptionType.badResponse, + ), + ); + } handler.next(err); })); } @@ -83,7 +106,7 @@ class HttpManager { ); break; case Method.post: - resp = await _dio.post( + resp = await _dio.post( url, data: data, queryParameters: params, @@ -92,7 +115,11 @@ class HttpManager { ); } } on DioException catch (e) { - // 网络或服务器错误 + // 如果已经是ApiException类型(401转换的) + if (e.error is ApiException) { + throw e.error as ApiException; + } + // 其他网络错误 throw ApiException('network_error', e.message ?? e.toString()); } @@ -136,7 +163,11 @@ extension HttpManagerUpload on HttpManager { return json; } on DioException catch (e) { + // 如果已经是ApiException类型(401转换的) + if (e.error is ApiException) { + throw e.error as ApiException; + } throw ApiException('network_error', e.message ?? e.toString()); } } -} +} \ No newline at end of file diff --git a/lib/main.dart b/lib/main.dart index ff7788f..90f249f 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,8 +1,29 @@ import 'package:flutter/material.dart'; +import 'package:qhd_prevention/pages/home/study/study_detail_page.dart'; import 'package:shared_preferences/shared_preferences.dart'; -import './pages/login_page.dart'; // 导入登录页面 -import './pages/main_tab.dart'; // 导入主页(带TabBar的页面) +import './pages/login_page.dart'; +import './pages/main_tab.dart'; import 'package:intl/date_symbol_data_local.dart'; +import 'http/HttpManager.dart'; + +// 全局导航键 +final GlobalKey navigatorKey = GlobalKey(); + +// 全局消息控制器 +class GlobalMessage { + static void showError(String message) { + final context = navigatorKey.currentContext; + if (context != null) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text(message), + backgroundColor: Colors.red, + duration: const Duration(seconds: 3), + ), + ); + } + } +} void main() async { // 确保Flutter引擎初始化完成 @@ -15,6 +36,25 @@ void main() async { // 获取登录状态 final isLoggedIn = prefs.getBool('isLoggedIn') ?? false; + // 初始化HTTP管理器 + HttpManager.onUnauthorized = () async { + // 清除登录状态 + final prefs = await SharedPreferences.getInstance(); + await prefs.setBool('isLoggedIn', false); + await prefs.remove('token'); // 如果有token的话 + + // 导航到登录页 + navigatorKey.currentState?.pushNamedAndRemoveUntil( + '/login', + (route) => false, + ); + + // 等一帧让页面构建完成 + Future.delayed(const Duration(milliseconds: 100), () { + GlobalMessage.showError('会话已过期,请重新登录'); + }); + }; + runApp(MyApp(isLoggedIn: isLoggedIn)); } @@ -27,6 +67,7 @@ class MyApp extends StatelessWidget { Widget build(BuildContext context) { return MaterialApp( title: '', + navigatorKey: navigatorKey, // 设置全局导航键 builder: (context, child) { return GestureDetector( behavior: HitTestBehavior.translucent, // 让空白区域也能点击 @@ -50,10 +91,20 @@ class MyApp extends StatelessWidget { border: InputBorder.none, contentPadding: EdgeInsets.symmetric(horizontal: 8), ), + snackBarTheme: const SnackBarThemeData( + behavior: SnackBarBehavior.floating, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.all(Radius.circular(8)), + ), + ), ), // 根据登录状态决定初始页面 home: isLoggedIn ? const MainPage() : const LoginPage(), debugShowCheckedModeBanner: false, + routes: { + '/login': (_) => const LoginPage(), + // ... 其他路由 + }, ); } } \ No newline at end of file diff --git a/lib/pages/home/home_page.dart b/lib/pages/home/home_page.dart index 85467b5..763106f 100644 --- a/lib/pages/home/home_page.dart +++ b/lib/pages/home/home_page.dart @@ -494,9 +494,9 @@ class _HomePageState extends State { final checkJson = await ApiService.getSafetyEnvironmentalInspectionCount(); setState(() { - int confirmCount = int.parse(checkJson['confirmCount']['confirmCount']); - int repulseCount = int.parse(checkJson['repulseCount']['repulseCount']); - int repulseAndCheckCount = int.parse(checkJson['repulseAndCheckCount']['repulseAndCheckCount']); + int confirmCount = checkJson['confirmCount']['confirmCount']; + int repulseCount = checkJson['repulseCount']['repulseCount']; + int repulseAndCheckCount = checkJson['repulseAndCheckCount']['repulseAndCheckCount']; _safetyEnvironmentalInspection = confirmCount + repulseCount + repulseAndCheckCount; }); diff --git a/lib/pages/home/study/face_ecognition_page.dart b/lib/pages/home/study/face_ecognition_page.dart index 60226f0..606eddd 100644 --- a/lib/pages/home/study/face_ecognition_page.dart +++ b/lib/pages/home/study/face_ecognition_page.dart @@ -1,7 +1,9 @@ import 'dart:async'; import 'package:camera/camera.dart'; import 'package:flutter/material.dart'; +import 'package:fluttertoast/fluttertoast.dart'; import 'package:qhd_prevention/customWidget/custom_button.dart'; +import 'package:qhd_prevention/customWidget/toast_util.dart'; import 'package:qhd_prevention/pages/my_appbar.dart'; import 'package:qhd_prevention/tools/tools.dart'; import '../../../http/ApiService.dart'; @@ -15,8 +17,8 @@ class FaceRecognitionPage extends StatefulWidget { const FaceRecognitionPage({ Key? key, - this.studentId = '', - required this.mode, + required this.studentId, + this.mode = FaceMode.auto, }) : super(key: key); @override @@ -27,8 +29,6 @@ class _FaceRecognitionPageState extends State { CameraController? _cameraController; Timer? _timer; int _attempts = 0; - String _message = ''; - String _tip = '请将人脸置于圆圈内'; static const int _maxAttempts = 8; static const Duration _interval = Duration(seconds: 2); @@ -61,54 +61,76 @@ class _FaceRecognitionPageState extends State { } } - Future _captureAndUpload() async { - if (_isManualMode) { - setState(() => _message = '请将人脸置于圆圈内'); - } else { - if (_attempts >= _maxAttempts) return _onTimeout(); - _attempts++; - } - - try { - final pic = await _cameraController!.takePicture(); - final res = await ApiService.getUserFace(pic.path, widget.studentId); - if (res['result'] == 'success') { - _onSuccess(); - } else { - setState(() => _message = '识别失败,请重试'); - } - } catch (_) { - setState(() => _message = '发生错误,请重试'); - } + /// 显示加载弹窗 + void _showLoading() { + showDialog( + context: context, + barrierDismissible: false, + builder: (_) => const Center(child: CircularProgressIndicator()), + ); } + /// 隐藏弹窗 + void _hideLoading() { + if (Navigator.canPop(context)) Navigator.pop(context); + } +/// 定时上传 + Future _captureAndUpload() async { + if (!_isManualMode) { + if (_attempts >= _maxAttempts) return _onTimeout(); + _attempts++; + try { + final pic = await _cameraController!.takePicture(); + final res = await ApiService.getUserFace(pic.path, widget.studentId); + if (res['result'] == 'success') { + _onSuccess(); + } + } catch (_) { + + } + } + } +/// 手动上传验证 Future _captureAndReload() async { - setState(() => _message = '上传中...'); + _showLoading(); try { final pic = await _cameraController!.takePicture(); - final res = await ApiService.reloadMyFace(pic.path,); + final res = await ApiService.reloadMyFace(pic.path, widget.studentId); + _hideLoading(); if (res['result'] == 'success') { _onSuccess(); } else { - setState(() => _message = '验证失败,请重试'); + ToastUtil.showError(context, '验证失败,请重试'); + } } catch (_) { - setState(() => _message = '发生错误,请重试'); + _hideLoading(); + _showToast('发生错误,请重试'); + ToastUtil.showError(context, '发生错误,请重试'); } } void _onSuccess() { _timer?.cancel(); - ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text('验证成功'))); + if (widget.mode == FaceMode.manual) { + ToastUtil.showSuccess(context, '已更新人脸信息'); + Future.delayed(const Duration(milliseconds: 800), () => Navigator.of(context).pop(true)); + return; + } + Future.delayed(const Duration(milliseconds: 800), () => Navigator.of(context).pop(true)); } void _onTimeout() { _timer?.cancel(); - setState(() => _message = '人脸超时,请重新识别!'); + ToastUtil.showError(context, '人脸超时,请重新识别!'); Future.delayed(const Duration(seconds: 3), () => Navigator.of(context).pop(false)); } + void _showToast(String msg) { + ToastUtil.showNormal(context, msg); + } + @override Widget build(BuildContext context) { if (_cameraController == null || !_cameraController!.value.isInitialized) { @@ -157,7 +179,7 @@ class _FaceRecognitionPageState extends State { mainAxisSize: MainAxisSize.min, children: [ Text( - _tip, + '请将人脸置于圆圈内', style: const TextStyle(fontSize: 18, color: Colors.black87), textAlign: TextAlign.center, ), diff --git a/lib/pages/home/study/study_detail_page.dart b/lib/pages/home/study/study_detail_page.dart index 1825d43..c52ac4b 100644 --- a/lib/pages/home/study/study_detail_page.dart +++ b/lib/pages/home/study/study_detail_page.dart @@ -1,19 +1,21 @@ import 'dart:async'; import 'package:flutter/material.dart'; -import 'package:video_player/video_player.dart'; +import 'package:qhd_prevention/pages/home/study/take_exam_page.dart'; +import 'package:qhd_prevention/pages/home/study/study_practise_page.dart'; +import 'package:qhd_prevention/pages/my_appbar.dart'; import 'package:qhd_prevention/customWidget/custom_alert_dialog.dart'; import 'package:qhd_prevention/customWidget/custom_button.dart'; import 'package:qhd_prevention/http/ApiService.dart'; -import 'package:qhd_prevention/pages/home/study/study_practise_page.dart'; -import 'package:qhd_prevention/pages/my_appbar.dart'; import 'package:qhd_prevention/tools/tools.dart'; +import 'package:video_player/video_player.dart'; +import '../../../customWidget/video_player_widget.dart'; +import '../../../http/HttpManager.dart'; import 'face_ecognition_page.dart'; class StudyDetailPage extends StatefulWidget { final Map studyDetailDetail; final String studentId; - const StudyDetailPage(this.studyDetailDetail, this.studentId, {super.key}); @override @@ -23,12 +25,14 @@ class StudyDetailPage extends StatefulWidget { class _StudyDetailPageState extends State with SingleTickerProviderStateMixin { late TabController _tabController; - VideoPlayerController? _videoController; - Map? _info; List _videoList = []; bool _loading = true; + // player controller extracted + VideoPlayerController? _videoController; + String _videoCoverUrl = ''; + Timer? _faceTimer; bool _throttleFlag = false; late int _faceTime; @@ -45,8 +49,7 @@ class _StudyDetailPageState extends State void initState() { super.initState(); _classId = widget.studyDetailDetail['CLASS_ID'] ?? ''; - _classCurriculumId = - widget.studyDetailDetail['CLASSCURRICULUM_ID'] ?? ''; + _classCurriculumId = widget.studyDetailDetail['CLASSCURRICULUM_ID'] ?? ''; _tabController = TabController(length: 2, vsync: this); WidgetsBinding.instance.addPostFrameCallback((_) { _showFaceIntro(); @@ -88,7 +91,11 @@ class _StudyDetailPageState extends State final pd = res['pd'] ?? {}; _faceTime = int.tryParse(pd['FACE_TIME']?.toString() ?? '10') ?? 10; _videoList = List.from(pd['VIDEOLIST'] ?? []); - // 计算 percent + // set initial face time + if (pd['ISFACE'] == '1') { + await ApiService.fnSetUserFaceTime(_faceTime); + } + // compute percent for (var item in _videoList) { if (item['nodes'] is List && (item['nodes'] as List).isNotEmpty) { for (var node in item['nodes']) { @@ -130,11 +137,14 @@ class _StudyDetailPageState extends State } Future _onVideoTap( - Map data, bool hasNodes, int fi, int ni) async { - // 停掉上一轮人脸定时 + Map data, + bool hasNodes, + int fi, + int ni, + ) async { + // clear face timer on backend + await ApiService.fnClearUserFaceTime(); _faceTimer?.cancel(); - - // 上报上一个视频进度 if (_currentVideoData != null && _lastReported > Duration.zero) { await _submitPlayTime(end: false, seconds: _lastReported.inSeconds); } @@ -145,31 +155,29 @@ class _StudyDetailPageState extends State _currentFirstIndex = fi; _currentNodeIndex = ni; - // 先暂停任何正在播放的视频 + // pause existing _videoController?.pause(); - // 统一认证逻辑 await _navigateFaceIfNeeded(() async { if ((data['IS_VIDEO'] ?? 0) == 1) { - // 资料 + // document if (data['VIDEOFILES'] != null) { await pushPage( StudyPractisePage(data['VIDEOCOURSEWARE_ID']), context, ); - await _submitPlayTime(end: true, seconds: int.parse(data['VIDEOTIME'] ?? '0')); + await _submitPlayTime( + end: true, + seconds: int.parse(data['VIDEOTIME'] ?? '0'), + ); } else { ScaffoldMessenger.of(context).showSnackBar( const SnackBar(content: Text('课件文件资源已失效,请联系管理员')), ); } } else { - // 视频 + // video await _getVideoPlayInfo(data['VIDEOCOURSEWARE_ID']); - // 播放并启动定时 - _videoController! - ..play() - ..addListener(_onTimeUpdate); _startFaceTimer(); } }); @@ -178,10 +186,7 @@ class _StudyDetailPageState extends State Future _navigateFaceIfNeeded(FutureOr Function() onPass) async { if (_info?['ISFACE'] == '1') { final passed = await pushPage( - FaceRecognitionPage( - studentId: widget.studentId, - mode: FaceMode.auto, - ), + FaceRecognitionPage(studentId: widget.studentId, mode: FaceMode.auto), context, ); if (passed == true) { @@ -199,24 +204,26 @@ class _StudyDetailPageState extends State Future _getVideoPlayInfo(String vidId) async { final res = await ApiService.fnGetVideoPlayInfo(vidId); final url = res['videoList']?[0]?['playURL'] ?? ''; + _videoCoverUrl = res['videoBase']?['coverURL'] ?? ''; + final prog = await ApiService.fnGetVideoPlayProgress( vidId, _currentVideoData!['CURRICULUM_ID'], _classId, widget.studentId, ); - final seen = prog['pd']?['RESOURCETIME'] ?? 0; + final seen = (double.tryParse(prog['pd']?['RESOURCETIME']) ?? 0.0).toInt(); + _videoController?.removeListener(_onTimeUpdate); _videoController?.dispose(); - _videoController = VideoPlayerController.networkUrl(Uri.parse(url)) - ..initialize().then((_) { - setState(() { - _videoController! - ..seekTo(Duration(seconds: seen)) - ..play() - ..addListener(_onTimeUpdate); - }); - }); + _videoController = VideoPlayerController.networkUrl(Uri.parse(url)); + await _videoController!.initialize(); + setState(() {}); + + _videoController! + ..seekTo(Duration(seconds: seen)) + ..play() + ..addListener(_onTimeUpdate); } void _onTimeUpdate() { @@ -226,9 +233,14 @@ class _StudyDetailPageState extends State _throttleFlag = true; _lastReported = curr; _submitPlayTime(end: false, seconds: curr.inSeconds) - .whenComplete(() => Future.delayed(const Duration(seconds: 1), () { - _throttleFlag = false; - })); + .whenComplete(() => _throttleFlag = false); + } + final pos = _videoController!.value.position; + final dur = _videoController!.value.duration; + if (pos >= dur) { + _submitPlayTime(end: true, seconds: dur.inSeconds); + ApiService.fnClearUserFaceTime(); + _faceTimer?.cancel(); } } @@ -237,95 +249,101 @@ class _StudyDetailPageState extends State required int seconds, }) async { if (_currentVideoData == null) return; - final pd = (await ApiService.fnSubmitPlayTime( - _currentVideoData!['VIDEOCOURSEWARE_ID'], - _currentVideoData!['CURRICULUM_ID'], - end ? '1' : '0', - seconds, - _currentVideoData!['CHAPTER_ID'], - widget.studentId, - _classCurriculumId, - _classId, - ))['pd']!; - // 更新列表 percent - final comp = pd['PLAYCOUNT'] != null && pd['PLAYCOUNT'] > 0; - final resT = pd['RESOURCETIME'] ?? seconds; - final pct = comp - ? 100 - : (resT / (_currentVideoData!['VIDEOTIME'] ?? 1) * 100).clamp(0, 100); - final str = '${pct.floor()}%'; - setState(() { - if (_hasNodes) { - _videoList[_currentFirstIndex]['nodes'][_currentNodeIndex] - ['percent'] = str; - } else { - _videoList[_currentFirstIndex]['percent'] = str; - } - }); - // 结束且可考试 - if (end && pd['CANEXAM'] == '1') { - _videoController?.pause(); - final ok = await showDialog( - context: context, - builder: (_) => CustomAlertDialog( - title: '提示', - content: '当前任务内所有课程均已学完,是否直接参加考试?', - confirmText: '是', - cancelText: '否', - ), - ) ?? - false; - if (ok) { - Navigator.of(context).pushReplacementNamed( - '/course_exam', - arguments: { - 'STAGEEXAMPAPERINPUT_ID': pd['paper']['STAGEEXAMPAPERINPUT_ID'], + try { + final pd = (await ApiService.fnSubmitPlayTime( + _currentVideoData!['VIDEOCOURSEWARE_ID'], + _currentVideoData!['CURRICULUM_ID'], + end ? '1' : '0', + seconds, + _currentVideoData!['CHAPTER_ID'], + widget.studentId, + _classCurriculumId, + _classId, + ))['pd']!; + + // 更新进度显示 + final comp = pd['PLAYCOUNT'] != null && pd['PLAYCOUNT'] > 0; + final resT = pd['RESOURCETIME'] ?? seconds; + final pct = comp + ? 100 + : (resT / (_currentVideoData!['VIDEOTIME'] ?? 1) * 100) + .clamp(0, 100); + final str = '${pct.floor()}%'; + setState(() { + if (_hasNodes) { + _videoList[_currentFirstIndex]['nodes'][_currentNodeIndex] + ['percent'] = str; + } else { + _videoList[_currentFirstIndex]['percent'] = str; + } + }); + + // 如果结束且可考试,弹框 + if (end && pd['CANEXAM'] == '1') { + _videoController?.pause(); + final ok = await showDialog( + context: context, + builder: (_) => CustomAlertDialog( + title: '提示', + content: '当前任务内所有课程均已学完,是否直接参加考试?', + confirmText: '是', + cancelText: '否', + ), + ) ?? + false; + if (ok) { + final arguments = { + 'STAGEEXAMPAPERINPUT_ID': + pd['paper']['STAGEEXAMPAPERINPUT_ID'], 'CLASS_ID': _classId, 'STUDENT_ID': widget.studentId, 'NUMBEROFEXAMS': pd['NUMBEROFEXAMS'], - }, - ); - } else { - _videoController?.play(); + }; + pushPage(TakeExamPage(arguments), context); + } else { + _videoController?.play(); + } } + } on ApiException catch (e) { + // 如果是 401 ,就登出并跳转登录 + + // 其他错误继续抛出 + rethrow; } } + void _startFaceTimer() { - _faceTimer = Timer.periodic(Duration(seconds: _faceTime), (_) { - _videoController?.pause(); - _showFaceAuthOnce(); + _faceTimer = Timer.periodic(Duration(seconds: _faceTime), (_) async { + final res = await ApiService.fnGetUserFaceTime(_faceTime); + final isPlaying = _videoController?.value.isPlaying == true; + final isVideo = _currentVideoData?['IS_VIDEO'] == 0; + final needAuth = res['data'] == false; + + if (isPlaying && isVideo && needAuth) { + _videoController!.pause(); + _videoController!.removeListener(_onTimeUpdate); + await _showFaceAuthOnce(); + } }); } Future _showFaceAuthOnce() async { final passed = await pushPage( - FaceRecognitionPage( - studentId: widget.studentId, - mode: FaceMode.auto, - ), + FaceRecognitionPage(studentId: widget.studentId, mode: FaceMode.auto), context, ); if (passed == true) { - _videoController?.play(); + await _videoController?.play(); + setState(() {}); _faceTimer?.cancel(); - _startFaceTimer(); - } else { - ScaffoldMessenger.of(context).showSnackBar( - const SnackBar(content: Text('人脸验证未通过,视频已暂停')), - ); } } - String secondsCount(dynamic sec) { - final s = int.tryParse('$sec') ?? 0; - final h = s ~/ 3600; - final m = (s % 3600) ~/ 60; - final ss = s % 60; - return '${h.toString().padLeft(2, '0')}:' - '${m.toString().padLeft(2, '0')}:' - '${ss.toString().padLeft(2, '0')}'; + + void _controllerListener() { + if (mounted) setState(() {}); } @override @@ -344,16 +362,12 @@ class _StudyDetailPageState extends State children: [ SizedBox( height: 250, - child: _videoController != null && - _videoController!.value.isInitialized - ? AspectRatio( - aspectRatio: _videoController!.value.aspectRatio, - child: VideoPlayer(_videoController!), - ) - : Image.network( - ApiService.baseImgPath + (info['COVERPATH'] ?? ''), - fit: BoxFit.cover, - width: double.infinity, + child: VideoPlayerWidget( + controller: _videoController, + coverUrl: _videoCoverUrl.isNotEmpty + ? ApiService.baseImgPath + _videoCoverUrl + : ApiService.baseImgPath + (info['COVERPATH'] ?? ''), + aspectRatio: _videoController?.value.aspectRatio ?? 16/9, ), ), Container( @@ -363,7 +377,9 @@ class _StudyDetailPageState extends State child: Text( info['CURRICULUMNAME'] ?? '', style: const TextStyle( - fontSize: 20, fontWeight: FontWeight.bold), + fontSize: 20, + fontWeight: FontWeight.bold, + ), ), ), const SizedBox(height: 10), @@ -371,8 +387,10 @@ class _StudyDetailPageState extends State color: Colors.white, child: TabBar( indicatorColor: Colors.blue, - labelStyle: - const TextStyle(fontSize: 16, color: Colors.black87), + labelStyle: const TextStyle( + fontSize: 16, + color: Colors.black87, + ), controller: _tabController, tabs: const [Tab(text: '课件目录'), Tab(text: '详情')], ), @@ -381,10 +399,7 @@ class _StudyDetailPageState extends State Expanded( child: TabBarView( controller: _tabController, - children: [ - _buildVideoList(), - _buildDetailView(info), - ], + children: [_buildVideoList(), _buildDetailView(info)], ), ), ], @@ -402,16 +417,19 @@ class _StudyDetailPageState extends State if (nodes != null && nodes.isNotEmpty) { return ExpansionTile( title: Text(item['NAME'] ?? ''), - children: nodes - .asMap() - .entries - .map((e) => _buildVideoItem( - e.value as Map, - true, - idx, - e.key, - )) - .toList(), + children: + nodes + .asMap() + .entries + .map( + (e) => _buildVideoItem( + e.value as Map, + true, + idx, + e.key, + ), + ) + .toList(), ); } return _buildVideoItem(item, false, idx, 0); @@ -420,7 +438,11 @@ class _StudyDetailPageState extends State } Widget _buildVideoItem( - Map m, bool hasNodes, int fi, int ni) { + Map m, + bool hasNodes, + int fi, + int ni, + ) { return Container( color: Colors.white, padding: const EdgeInsets.all(20), @@ -429,11 +451,13 @@ class _StudyDetailPageState extends State Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ - const Icon(Icons.file_copy_rounded, - color: Colors.grey, size: 20), + const Icon(Icons.file_copy_rounded, color: Colors.grey, size: 20), Expanded( - child: Text(m['NAME'] ?? '', - style: const TextStyle(fontSize: 14))), + child: Text( + m['NAME'] ?? '', + style: const TextStyle(fontSize: 14), + ), + ), ], ), const SizedBox(height: 10), @@ -441,8 +465,7 @@ class _StudyDetailPageState extends State onTap: () => _onVideoTap(m, hasNodes, fi, ni), child: Container( height: 80, - padding: - const EdgeInsets.symmetric(vertical: 10, horizontal: 8), + padding: const EdgeInsets.symmetric(vertical: 10, horizontal: 8), decoration: BoxDecoration( color: const Color(0xFFFAFAFA), borderRadius: BorderRadius.circular(5), @@ -451,28 +474,39 @@ class _StudyDetailPageState extends State crossAxisAlignment: CrossAxisAlignment.start, children: [ Expanded( - child: Text(m['COURSEWARENAME'] ?? '', - style: const TextStyle(fontSize: 14)), + child: Text( + m['COURSEWARENAME'] ?? '', + style: const TextStyle(fontSize: 14), + ), ), Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ - Text("进度:${m['percent']}", - style: const TextStyle(color: Colors.blue)), + Text( + "进度:${m['percent']}", + style: const TextStyle(color: Colors.blue), + ), if (m['IS_VIDEO'] == 0) ...[ Text(secondsCount(m['VIDEOTIME'])), const Icon(Icons.play_circle, color: Colors.blue), ], CustomButton( - onPressed: () => - pushPage(StudyPractisePage(m['VIDEOCOURSEWARE_ID']), context), + onPressed: + () => pushPage( + StudyPractisePage(m['VIDEOCOURSEWARE_ID']), + context, + ), text: "课后练习", backgroundColor: Colors.blue, height: 30, - textStyle: - const TextStyle(fontSize: 12, color: Colors.white), + textStyle: const TextStyle( + fontSize: 12, + color: Colors.white, + ), padding: const EdgeInsets.symmetric( - vertical: 2, horizontal: 12), + vertical: 2, + horizontal: 12, + ), borderRadius: 15, ), ], @@ -480,7 +514,7 @@ class _StudyDetailPageState extends State ], ), ), - ) + ), ], ), ); @@ -492,8 +526,10 @@ class _StudyDetailPageState extends State child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - Text(info['CURRICULUMINTRODUCE'] ?? '', - style: const TextStyle(fontSize: 16)), + Text( + info['CURRICULUMINTRODUCE'] ?? '', + style: const TextStyle(fontSize: 16), + ), const SizedBox(height: 16), if (info['COVERPATH'] != null) Image.network(ApiService.baseImgPath + info['COVERPATH']), @@ -502,3 +538,4 @@ class _StudyDetailPageState extends State ); } } + diff --git a/lib/pages/home/study/take_exam_page.dart b/lib/pages/home/study/take_exam_page.dart new file mode 100644 index 0000000..b6b7034 --- /dev/null +++ b/lib/pages/home/study/take_exam_page.dart @@ -0,0 +1,18 @@ +import 'package:flutter/material.dart'; +import 'package:qhd_prevention/pages/my_appbar.dart'; + +class TakeExamPage extends StatefulWidget { + const TakeExamPage(this.arguments,{super.key}); + final Map arguments; + @override + State createState() => _TakeExamPageState(); +} + +class _TakeExamPageState extends State { + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: MyAppbar(title: '开始考试'), + ); + } +} diff --git a/lib/tools/tools.dart b/lib/tools/tools.dart index 1c3df1b..a627836 100644 --- a/lib/tools/tools.dart +++ b/lib/tools/tools.dart @@ -238,7 +238,6 @@ class ClickUtil { _canClick = true; }); } else { - // 可替换成 Toast debugPrint('请稍后点击'); } } diff --git a/pubspec.lock b/pubspec.lock index acada0f..5910063 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -6,7 +6,7 @@ packages: description: name: args sha256: d0481093c50b1da8910eb0bb301626d4d8eb7284aa739614d2b394ee09e3ea04 - url: "https://pub.flutter-io.cn" + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" source: hosted version: "2.7.0" asn1lib: @@ -21,16 +21,16 @@ packages: dependency: transitive description: name: async - sha256: "758e6d74e971c3e5aceb4110bfd6698efc7f501675bcfe0c775459a8140750eb" - url: "https://pub.flutter-io.cn" + sha256: d2872f9c19731c2e5f10444b14686eb7cc85c76274bd6c16e1816bff9a3bab63 + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" source: hosted - version: "2.13.0" + version: "2.12.0" boolean_selector: dependency: transitive description: name: boolean_selector sha256: "8aab1771e1243a5063b8b0ff68042d67334e3feab9e95b9490f9a6ebf73b42ea" - url: "https://pub.flutter-io.cn" + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" source: hosted version: "2.1.2" camera: @@ -78,7 +78,7 @@ packages: description: name: characters sha256: f71061c654a3380576a52b451dd5532377954cf9dbd272a78fc8479606670803 - url: "https://pub.flutter-io.cn" + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" source: hosted version: "1.4.0" clock: @@ -86,7 +86,7 @@ packages: description: name: clock sha256: fddb70d9b5277016c77a80201021d40a2247104d9f4aa7bab7157b7e3f05b84b - url: "https://pub.flutter-io.cn" + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" source: hosted version: "1.1.2" collection: @@ -94,7 +94,7 @@ packages: description: name: collection sha256: "2f5709ae4d3d59dd8f7cd309b4e023046b57d8a6c82130785d2b0e5868084e76" - url: "https://pub.flutter-io.cn" + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" source: hosted version: "1.19.1" connectivity_plus: @@ -102,7 +102,7 @@ packages: description: name: connectivity_plus sha256: "051849e2bd7c7b3bc5844ea0d096609ddc3a859890ec3a9ac4a65a2620cc1f99" - url: "https://pub.flutter-io.cn" + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" source: hosted version: "6.1.4" connectivity_plus_platform_interface: @@ -110,7 +110,7 @@ packages: description: name: connectivity_plus_platform_interface sha256: "42657c1715d48b167930d5f34d00222ac100475f73d10162ddf43e714932f204" - url: "https://pub.flutter-io.cn" + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" source: hosted version: "2.0.1" convert: @@ -126,7 +126,7 @@ packages: description: name: cross_file sha256: "7caf6a750a0c04effbb52a676dce9a4a592e10ad35c34d6d2d0e4811160d5670" - url: "https://pub.flutter-io.cn" + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" source: hosted version: "0.3.4+2" crypto: @@ -134,7 +134,7 @@ packages: description: name: crypto sha256: "1e445881f28f22d6140f181e07737b22f1e099a5e1ff94b0af2f9e4a463f4855" - url: "https://pub.flutter-io.cn" + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" source: hosted version: "3.0.6" csslib: @@ -142,7 +142,7 @@ packages: description: name: csslib sha256: "09bad715f418841f976c77db72d5398dc1253c21fb9c0c7f0b0b985860b2d58e" - url: "https://pub.flutter-io.cn" + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" source: hosted version: "1.0.2" cupertino_icons: @@ -150,7 +150,7 @@ packages: description: name: cupertino_icons sha256: ba631d1c7f7bef6b729a622b7b752645a2d076dba9976925b8f25725a30e1ee6 - url: "https://pub.flutter-io.cn" + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" source: hosted version: "1.0.8" dbus: @@ -158,7 +158,7 @@ packages: description: name: dbus sha256: "79e0c23480ff85dc68de79e2cd6334add97e48f7f4865d17686dd6ea81a47e8c" - url: "https://pub.flutter-io.cn" + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" source: hosted version: "0.7.11" dio: @@ -190,7 +190,7 @@ packages: description: name: extended_image sha256: f6cbb1d798f51262ed1a3d93b4f1f2aa0d76128df39af18ecb77fa740f88b2e0 - url: "https://pub.flutter-io.cn" + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" source: hosted version: "10.0.1" extended_image_library: @@ -198,23 +198,23 @@ packages: description: name: extended_image_library sha256: "1f9a24d3a00c2633891c6a7b5cab2807999eb2d5b597e5133b63f49d113811fe" - url: "https://pub.flutter-io.cn" + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" source: hosted version: "5.0.1" fake_async: dependency: transitive description: name: fake_async - sha256: "5368f224a74523e8d2e7399ea1638b37aecfca824a3cc4dfdf77bf1fa905ac44" - url: "https://pub.flutter-io.cn" + sha256: "6a95e56b2449df2273fd8c45a662d6947ce1ebb7aafe80e550a3f68297f3cacc" + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" source: hosted - version: "1.3.3" + version: "1.3.2" ffi: dependency: transitive description: name: ffi sha256: "289279317b4b16eb2bb7e271abccd4bf84ec9bdcbe999e278a94b804f5630418" - url: "https://pub.flutter-io.cn" + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" source: hosted version: "2.1.4" file: @@ -222,7 +222,7 @@ packages: description: name: file sha256: a3b4f84adafef897088c160faf7dfffb7696046cb13ae90b508c2cbc95d3b8d4 - url: "https://pub.flutter-io.cn" + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" source: hosted version: "7.0.1" file_selector_linux: @@ -230,7 +230,7 @@ packages: description: name: file_selector_linux sha256: "54cbbd957e1156d29548c7d9b9ec0c0ebb6de0a90452198683a7d23aed617a33" - url: "https://pub.flutter-io.cn" + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" source: hosted version: "0.9.3+2" file_selector_macos: @@ -238,7 +238,7 @@ packages: description: name: file_selector_macos sha256: "8c9250b2bd2d8d4268e39c82543bacbaca0fda7d29e0728c3c4bbb7c820fd711" - url: "https://pub.flutter-io.cn" + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" source: hosted version: "0.9.4+3" file_selector_platform_interface: @@ -246,7 +246,7 @@ packages: description: name: file_selector_platform_interface sha256: a3994c26f10378a039faa11de174d7b78eb8f79e4dd0af2a451410c1a5c3f66b - url: "https://pub.flutter-io.cn" + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" source: hosted version: "2.6.2" file_selector_windows: @@ -254,7 +254,7 @@ packages: description: name: file_selector_windows sha256: "320fcfb6f33caa90f0b58380489fc5ac05d99ee94b61aa96ec2bff0ba81d3c2b" - url: "https://pub.flutter-io.cn" + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" source: hosted version: "0.9.3+4" flutter: @@ -267,7 +267,7 @@ packages: description: name: flutter_html sha256: "38a2fd702ffdf3243fb7441ab58aa1bc7e6922d95a50db76534de8260638558d" - url: "https://pub.flutter-io.cn" + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" source: hosted version: "3.0.0" flutter_lints: @@ -275,7 +275,7 @@ packages: description: name: flutter_lints sha256: "5398f14efa795ffb7a33e9b6a08798b26a180edac4ad7db3f231e40f82ce11e1" - url: "https://pub.flutter-io.cn" + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" source: hosted version: "5.0.0" flutter_plugin_android_lifecycle: @@ -283,7 +283,7 @@ packages: description: name: flutter_plugin_android_lifecycle sha256: f948e346c12f8d5480d2825e03de228d0eb8c3a737e4cdaa122267b89c022b5e - url: "https://pub.flutter-io.cn" + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" source: hosted version: "2.0.28" flutter_test: @@ -309,7 +309,7 @@ packages: description: name: html sha256: "6d1264f2dffa1b1101c25a91dff0dc2daee4c18e87cd8538729773c073dbf602" - url: "https://pub.flutter-io.cn" + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" source: hosted version: "0.15.6" http: @@ -317,7 +317,7 @@ packages: description: name: http sha256: "2c11f3f94c687ee9bad77c171151672986360b2b001d109814ee7140b2cf261b" - url: "https://pub.flutter-io.cn" + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" source: hosted version: "1.4.0" http_client_helper: @@ -325,7 +325,7 @@ packages: description: name: http_client_helper sha256: "8a9127650734da86b5c73760de2b404494c968a3fd55602045ffec789dac3cb1" - url: "https://pub.flutter-io.cn" + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" source: hosted version: "3.0.0" http_parser: @@ -333,7 +333,7 @@ packages: description: name: http_parser sha256: "178d74305e7866013777bab2c3d8726205dc5a4dd935297175b19a23a2e66571" - url: "https://pub.flutter-io.cn" + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" source: hosted version: "4.1.2" image_picker: @@ -341,7 +341,7 @@ packages: description: name: image_picker sha256: "021834d9c0c3de46bf0fe40341fa07168407f694d9b2bb18d532dc1261867f7a" - url: "https://pub.flutter-io.cn" + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" source: hosted version: "1.1.2" image_picker_android: @@ -349,7 +349,7 @@ packages: description: name: image_picker_android sha256: "317a5d961cec5b34e777b9252393f2afbd23084aa6e60fcf601dcf6341b9ebeb" - url: "https://pub.flutter-io.cn" + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" source: hosted version: "0.8.12+23" image_picker_for_web: @@ -357,7 +357,7 @@ packages: description: name: image_picker_for_web sha256: "717eb042ab08c40767684327be06a5d8dbb341fe791d514e4b92c7bbe1b7bb83" - url: "https://pub.flutter-io.cn" + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" source: hosted version: "3.0.6" image_picker_ios: @@ -365,7 +365,7 @@ packages: description: name: image_picker_ios sha256: "05da758e67bc7839e886b3959848aa6b44ff123ab4b28f67891008afe8ef9100" - url: "https://pub.flutter-io.cn" + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" source: hosted version: "0.8.12+2" image_picker_linux: @@ -373,7 +373,7 @@ packages: description: name: image_picker_linux sha256: "34a65f6740df08bbbeb0a1abd8e6d32107941fd4868f67a507b25601651022c9" - url: "https://pub.flutter-io.cn" + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" source: hosted version: "0.2.1+2" image_picker_macos: @@ -381,7 +381,7 @@ packages: description: name: image_picker_macos sha256: "1b90ebbd9dcf98fb6c1d01427e49a55bd96b5d67b8c67cf955d60a5de74207c1" - url: "https://pub.flutter-io.cn" + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" source: hosted version: "0.2.1+2" image_picker_platform_interface: @@ -389,7 +389,7 @@ packages: description: name: image_picker_platform_interface sha256: "886d57f0be73c4b140004e78b9f28a8914a09e50c2d816bdd0520051a71236a0" - url: "https://pub.flutter-io.cn" + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" source: hosted version: "2.10.1" image_picker_windows: @@ -397,7 +397,7 @@ packages: description: name: image_picker_windows sha256: "6ad07afc4eb1bc25f3a01084d28520496c4a3bb0cb13685435838167c9dcedeb" - url: "https://pub.flutter-io.cn" + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" source: hosted version: "0.2.1+1" intl: @@ -405,7 +405,7 @@ packages: description: name: intl sha256: "3df61194eb431efc39c4ceba583b95633a403f46c9fd341e550ce0bfa50e9aa5" - url: "https://pub.flutter-io.cn" + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" source: hosted version: "0.20.2" js: @@ -413,23 +413,23 @@ packages: description: name: js sha256: "53385261521cc4a0c4658fd0ad07a7d14591cf8fc33abbceae306ddb974888dc" - url: "https://pub.flutter-io.cn" + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" source: hosted version: "0.7.2" leak_tracker: dependency: transitive description: name: leak_tracker - sha256: "6bb818ecbdffe216e81182c2f0714a2e62b593f4a4f13098713ff1685dfb6ab0" - url: "https://pub.flutter-io.cn" + sha256: c35baad643ba394b40aac41080300150a4f08fd0fd6a10378f8f7c6bc161acec + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" source: hosted - version: "10.0.9" + version: "10.0.8" leak_tracker_flutter_testing: dependency: transitive description: name: leak_tracker_flutter_testing sha256: f8b613e7e6a13ec79cfdc0e97638fddb3ab848452eff057653abd3edba760573 - url: "https://pub.flutter-io.cn" + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" source: hosted version: "3.0.9" leak_tracker_testing: @@ -437,7 +437,7 @@ packages: description: name: leak_tracker_testing sha256: "6ba465d5d76e67ddf503e1161d1f4a6bc42306f9d66ca1e8f079a47290fb06d3" - url: "https://pub.flutter-io.cn" + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" source: hosted version: "3.0.1" lints: @@ -445,7 +445,7 @@ packages: description: name: lints sha256: c35bb79562d980e9a453fc715854e1ed39e24e7d0297a880ef54e17f9874a9d7 - url: "https://pub.flutter-io.cn" + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" source: hosted version: "5.1.1" list_counter: @@ -453,7 +453,7 @@ packages: description: name: list_counter sha256: c447ae3dfcd1c55f0152867090e67e219d42fe6d4f2807db4bbe8b8d69912237 - url: "https://pub.flutter-io.cn" + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" source: hosted version: "1.0.2" matcher: @@ -461,7 +461,7 @@ packages: description: name: matcher sha256: dc58c723c3c24bf8d3e2d3ad3f2f9d7bd9cf43ec6feaa64181775e60190153f2 - url: "https://pub.flutter-io.cn" + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" source: hosted version: "0.12.17" material_color_utilities: @@ -469,7 +469,7 @@ packages: description: name: material_color_utilities sha256: f7142bb1154231d7ea5f96bc7bde4bda2a0945d2806bb11670e30b850d56bdec - url: "https://pub.flutter-io.cn" + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" source: hosted version: "0.11.1" meta: @@ -477,7 +477,7 @@ packages: description: name: meta sha256: e3641ec5d63ebf0d9b41bd43201a66e3fc79a65db5f61fc181f04cd27aab950c - url: "https://pub.flutter-io.cn" + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" source: hosted version: "1.16.0" mime: @@ -485,7 +485,7 @@ packages: description: name: mime sha256: "41a20518f0cb1256669420fdba0cd90d21561e560ac240f26ef8322e45bb7ed6" - url: "https://pub.flutter-io.cn" + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" source: hosted version: "2.0.0" mobile_scanner: @@ -493,7 +493,7 @@ packages: description: name: mobile_scanner sha256: "54005bdea7052d792d35b4fef0f84ec5ddc3a844b250ecd48dc192fb9b4ebc95" - url: "https://pub.flutter-io.cn" + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" source: hosted version: "7.0.1" nested: @@ -501,7 +501,7 @@ packages: description: name: nested sha256: "03bac4c528c64c95c722ec99280375a6f2fc708eec17c7b3f07253b626cd2a20" - url: "https://pub.flutter-io.cn" + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" source: hosted version: "1.0.0" nm: @@ -509,7 +509,7 @@ packages: description: name: nm sha256: "2c9aae4127bdc8993206464fcc063611e0e36e72018696cd9631023a31b24254" - url: "https://pub.flutter-io.cn" + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" source: hosted version: "0.5.0" package_info_plus: @@ -517,7 +517,7 @@ packages: description: name: package_info_plus sha256: "7976bfe4c583170d6cdc7077e3237560b364149fcd268b5f53d95a991963b191" - url: "https://pub.flutter-io.cn" + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" source: hosted version: "8.3.0" package_info_plus_platform_interface: @@ -525,7 +525,7 @@ packages: description: name: package_info_plus_platform_interface sha256: "6c935fb612dff8e3cc9632c2b301720c77450a126114126ffaafe28d2e87956c" - url: "https://pub.flutter-io.cn" + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" source: hosted version: "3.2.0" path: @@ -533,7 +533,7 @@ packages: description: name: path sha256: "75cca69d1490965be98c73ceaea117e8a04dd21217b37b292c9ddbec0d955bc5" - url: "https://pub.flutter-io.cn" + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" source: hosted version: "1.9.1" path_provider: @@ -541,7 +541,7 @@ packages: description: name: path_provider sha256: "50c5dd5b6e1aaf6fb3a78b33f6aa3afca52bf903a8a5298f53101fdaee55bbcd" - url: "https://pub.flutter-io.cn" + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" source: hosted version: "2.1.5" path_provider_android: @@ -549,7 +549,7 @@ packages: description: name: path_provider_android sha256: d0d310befe2c8ab9e7f393288ccbb11b60c019c6b5afc21973eeee4dda2b35e9 - url: "https://pub.flutter-io.cn" + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" source: hosted version: "2.2.17" path_provider_foundation: @@ -557,7 +557,7 @@ packages: description: name: path_provider_foundation sha256: "4843174df4d288f5e29185bd6e72a6fbdf5a4a4602717eed565497429f179942" - url: "https://pub.flutter-io.cn" + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" source: hosted version: "2.4.1" path_provider_linux: @@ -565,7 +565,7 @@ packages: description: name: path_provider_linux sha256: f7a1fe3a634fe7734c8d3f2766ad746ae2a2884abe22e241a8b301bf5cac3279 - url: "https://pub.flutter-io.cn" + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" source: hosted version: "2.2.1" path_provider_platform_interface: @@ -573,7 +573,7 @@ packages: description: name: path_provider_platform_interface sha256: "88f5779f72ba699763fa3a3b06aa4bf6de76c8e5de842cf6f29e2e06476c2334" - url: "https://pub.flutter-io.cn" + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" source: hosted version: "2.1.2" path_provider_windows: @@ -581,7 +581,7 @@ packages: description: name: path_provider_windows sha256: bd6f00dbd873bfb70d0761682da2b3a2c2fccc2b9e84c495821639601d81afe7 - url: "https://pub.flutter-io.cn" + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" source: hosted version: "2.3.0" petitparser: @@ -589,7 +589,7 @@ packages: description: name: petitparser sha256: "07c8f0b1913bcde1ff0d26e57ace2f3012ccbf2b204e070290dad3bb22797646" - url: "https://pub.flutter-io.cn" + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" source: hosted version: "6.1.0" photo_manager: @@ -597,7 +597,7 @@ packages: description: name: photo_manager sha256: a0d9a7a9bc35eda02d33766412bde6d883a8b0acb86bbe37dac5f691a0894e8a - url: "https://pub.flutter-io.cn" + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" source: hosted version: "3.7.1" photo_manager_image_provider: @@ -605,7 +605,7 @@ packages: description: name: photo_manager_image_provider sha256: b6015b67b32f345f57cf32c126f871bced2501236c405aafaefa885f7c821e4f - url: "https://pub.flutter-io.cn" + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" source: hosted version: "2.2.0" photo_view: @@ -613,7 +613,7 @@ packages: description: name: photo_view sha256: "1fc3d970a91295fbd1364296575f854c9863f225505c28c46e0a03e48960c75e" - url: "https://pub.flutter-io.cn" + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" source: hosted version: "0.15.0" platform: @@ -621,7 +621,7 @@ packages: description: name: platform sha256: "5d6b1b0036a5f331ebc77c850ebc8506cbc1e9416c27e59b439f917a902a4984" - url: "https://pub.flutter-io.cn" + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" source: hosted version: "3.1.6" plugin_platform_interface: @@ -629,7 +629,7 @@ packages: description: name: plugin_platform_interface sha256: "4820fbfdb9478b1ebae27888254d445073732dae3d6ea81f0b7e06d5dedc3f02" - url: "https://pub.flutter-io.cn" + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" source: hosted version: "2.1.8" pointycastle: @@ -645,7 +645,7 @@ packages: description: name: provider sha256: "4abbd070a04e9ddc287673bf5a030c7ca8b685ff70218720abab8b092f53dd84" - url: "https://pub.flutter-io.cn" + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" source: hosted version: "6.1.5" shared_preferences: @@ -653,7 +653,7 @@ packages: description: name: shared_preferences sha256: "6e8bf70b7fef813df4e9a36f658ac46d107db4b4cfe1048b477d4e453a8159f5" - url: "https://pub.flutter-io.cn" + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" source: hosted version: "2.5.3" shared_preferences_android: @@ -661,7 +661,7 @@ packages: description: name: shared_preferences_android sha256: "20cbd561f743a342c76c151d6ddb93a9ce6005751e7aa458baad3858bfbfb6ac" - url: "https://pub.flutter-io.cn" + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" source: hosted version: "2.4.10" shared_preferences_foundation: @@ -669,7 +669,7 @@ packages: description: name: shared_preferences_foundation sha256: "6a52cfcdaeac77cad8c97b539ff688ccfc458c007b4db12be584fbe5c0e49e03" - url: "https://pub.flutter-io.cn" + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" source: hosted version: "2.5.4" shared_preferences_linux: @@ -677,7 +677,7 @@ packages: description: name: shared_preferences_linux sha256: "580abfd40f415611503cae30adf626e6656dfb2f0cee8f465ece7b6defb40f2f" - url: "https://pub.flutter-io.cn" + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" source: hosted version: "2.4.1" shared_preferences_platform_interface: @@ -685,7 +685,7 @@ packages: description: name: shared_preferences_platform_interface sha256: "57cbf196c486bc2cf1f02b85784932c6094376284b3ad5779d1b1c6c6a816b80" - url: "https://pub.flutter-io.cn" + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" source: hosted version: "2.4.1" shared_preferences_web: @@ -693,7 +693,7 @@ packages: description: name: shared_preferences_web sha256: c49bd060261c9a3f0ff445892695d6212ff603ef3115edbb448509d407600019 - url: "https://pub.flutter-io.cn" + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" source: hosted version: "2.4.3" shared_preferences_windows: @@ -701,7 +701,7 @@ packages: description: name: shared_preferences_windows sha256: "94ef0f72b2d71bc3e700e025db3710911bd51a71cefb65cc609dd0d9a982e3c1" - url: "https://pub.flutter-io.cn" + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" source: hosted version: "2.4.1" simple_gesture_detector: @@ -709,7 +709,7 @@ packages: description: name: simple_gesture_detector sha256: ba2cd5af24ff20a0b8d609cec3f40e5b0744d2a71804a2616ae086b9c19d19a3 - url: "https://pub.flutter-io.cn" + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" source: hosted version: "0.2.1" sky_engine: @@ -722,7 +722,7 @@ packages: description: name: source_span sha256: "254ee5351d6cb365c859e20ee823c3bb479bf4a293c22d17a9f1bf144ce86f7c" - url: "https://pub.flutter-io.cn" + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" source: hosted version: "1.10.1" stack_trace: @@ -730,7 +730,7 @@ packages: description: name: stack_trace sha256: "8b27215b45d22309b5cddda1aa2b19bdfec9df0e765f2de506401c071d38d1b1" - url: "https://pub.flutter-io.cn" + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" source: hosted version: "1.12.1" stream_channel: @@ -738,7 +738,7 @@ packages: description: name: stream_channel sha256: "969e04c80b8bcdf826f8f16579c7b14d780458bd97f56d107d3950fdbeef059d" - url: "https://pub.flutter-io.cn" + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" source: hosted version: "2.1.4" stream_transform: @@ -754,7 +754,7 @@ packages: description: name: string_scanner sha256: "921cd31725b72fe181906c6a94d987c78e3b98c2e205b397ea399d4054872b43" - url: "https://pub.flutter-io.cn" + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" source: hosted version: "1.4.1" table_calendar: @@ -762,7 +762,7 @@ packages: description: name: table_calendar sha256: "0c0c6219878b363a2d5f40c7afb159d845f253d061dc3c822aa0d5fe0f721982" - url: "https://pub.flutter-io.cn" + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" source: hosted version: "3.2.0" term_glyph: @@ -770,7 +770,7 @@ packages: description: name: term_glyph sha256: "7f554798625ea768a7518313e58f83891c7f5024f88e46e7182a4558850a4b8e" - url: "https://pub.flutter-io.cn" + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" source: hosted version: "1.2.2" test_api: @@ -778,7 +778,7 @@ packages: description: name: test_api sha256: fb31f383e2ee25fbbfe06b40fe21e1e458d14080e3c67e7ba0acfde4df4e0bbd - url: "https://pub.flutter-io.cn" + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" source: hosted version: "0.7.4" typed_data: @@ -786,7 +786,7 @@ packages: description: name: typed_data sha256: f9049c039ebfeb4cf7a7104a675823cd72dba8297f264b6637062516699fa006 - url: "https://pub.flutter-io.cn" + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" source: hosted version: "1.4.0" vector_math: @@ -794,7 +794,7 @@ packages: description: name: vector_math sha256: "80b3257d1492ce4d091729e3a67a60407d227c27241d6927be0130c98e741803" - url: "https://pub.flutter-io.cn" + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" source: hosted version: "2.1.4" video_player: @@ -802,7 +802,7 @@ packages: description: name: video_player sha256: "0d55b1f1a31e5ad4c4967bfaa8ade0240b07d20ee4af1dfef5f531056512961a" - url: "https://pub.flutter-io.cn" + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" source: hosted version: "2.10.0" video_player_android: @@ -810,7 +810,7 @@ packages: description: name: video_player_android sha256: "4a5135754a62dbc827a64a42ef1f8ed72c962e191c97e2d48744225c2b9ebb73" - url: "https://pub.flutter-io.cn" + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" source: hosted version: "2.8.7" video_player_avfoundation: @@ -818,7 +818,7 @@ packages: description: name: video_player_avfoundation sha256: "9fedd55023249f3a02738c195c906b4e530956191febf0838e37d0dac912f953" - url: "https://pub.flutter-io.cn" + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" source: hosted version: "2.8.0" video_player_platform_interface: @@ -826,7 +826,7 @@ packages: description: name: video_player_platform_interface sha256: cf2a1d29a284db648fd66cbd18aacc157f9862d77d2cc790f6f9678a46c1db5a - url: "https://pub.flutter-io.cn" + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" source: hosted version: "6.4.0" video_player_web: @@ -834,7 +834,7 @@ packages: description: name: video_player_web sha256: "9f3c00be2ef9b76a95d94ac5119fb843dca6f2c69e6c9968f6f2b6c9e7afbdeb" - url: "https://pub.flutter-io.cn" + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" source: hosted version: "2.4.0" visibility_detector: @@ -842,23 +842,23 @@ packages: description: name: visibility_detector sha256: dd5cc11e13494f432d15939c3aa8ae76844c42b723398643ce9addb88a5ed420 - url: "https://pub.flutter-io.cn" + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" source: hosted version: "0.4.0+2" vm_service: dependency: transitive description: name: vm_service - sha256: ddfa8d30d89985b96407efce8acbdd124701f96741f2d981ca860662f1c0dc02 - url: "https://pub.flutter-io.cn" + sha256: "0968250880a6c5fe7edc067ed0a13d4bae1577fe2771dcf3010d52c4a9d3ca14" + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" source: hosted - version: "15.0.0" + version: "14.3.1" web: dependency: transitive description: name: web sha256: "868d88a33d8a87b18ffc05f9f030ba328ffefba92d6c127917a2ba740f9cfe4a" - url: "https://pub.flutter-io.cn" + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" source: hosted version: "1.1.1" webview_flutter: @@ -866,7 +866,7 @@ packages: description: name: webview_flutter sha256: c3e4fe614b1c814950ad07186007eff2f2e5dd2935eba7b9a9a1af8e5885f1ba - url: "https://pub.flutter-io.cn" + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" source: hosted version: "4.13.0" webview_flutter_android: @@ -874,7 +874,7 @@ packages: description: name: webview_flutter_android sha256: "769f34fc9855f7d7789b786b79b7c37a60e92ff08f71e3a429208d7f5b81d944" - url: "https://pub.flutter-io.cn" + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" source: hosted version: "4.8.0" webview_flutter_platform_interface: @@ -882,7 +882,7 @@ packages: description: name: webview_flutter_platform_interface sha256: f0dc2dc3a2b1e3a6abdd6801b9355ebfeb3b8f6cde6b9dc7c9235909c4a1f147 - url: "https://pub.flutter-io.cn" + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" source: hosted version: "2.13.1" webview_flutter_wkwebview: @@ -890,7 +890,7 @@ packages: description: name: webview_flutter_wkwebview sha256: "71523b9048cf510cfa1fd4e0a3fa5e476a66e0884d5df51d59d5023dba237107" - url: "https://pub.flutter-io.cn" + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" source: hosted version: "3.22.1" wechat_assets_picker: @@ -898,7 +898,7 @@ packages: description: name: wechat_assets_picker sha256: cafe3d32564ed3cacf9822f251941f7b44fe9885c17c8de4fca7e939a459e1ef - url: "https://pub.flutter-io.cn" + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" source: hosted version: "9.5.1" wechat_picker_library: @@ -906,23 +906,23 @@ packages: description: name: wechat_picker_library sha256: a42e09cb85b15fc9410f6a69671371cc60aa99c4a1f7967f6593a7f665f6f47a - url: "https://pub.flutter-io.cn" + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" source: hosted version: "1.0.5" win32: dependency: transitive description: name: win32 - sha256: "66814138c3562338d05613a6e368ed8cfb237ad6d64a9e9334be3f309acfca03" - url: "https://pub.flutter-io.cn" + sha256: "329edf97fdd893e0f1e3b9e88d6a0e627128cc17cc316a8d67fda8f1451178ba" + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" source: hosted - version: "5.14.0" + version: "5.13.0" xdg_directories: dependency: transitive description: name: xdg_directories sha256: "7a3f37b05d989967cdddcbb571f1ea834867ae2faa29725fd085180e0883aa15" - url: "https://pub.flutter-io.cn" + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" source: hosted version: "1.1.0" xml: @@ -930,9 +930,9 @@ packages: description: name: xml sha256: b015a8ad1c488f66851d762d3090a21c600e479dc75e68328c52774040cf9226 - url: "https://pub.flutter-io.cn" + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" source: hosted version: "6.5.0" sdks: - dart: ">=3.8.0 <4.0.0" + dart: ">=3.7.0 <4.0.0" flutter: ">=3.29.0"