diff --git a/.gitignore b/.gitignore index c6ec466..2088348 100644 --- a/.gitignore +++ b/.gitignore @@ -46,3 +46,4 @@ app.*.map.json # Widget Preview related .widget_preview/ android/.kotlin/ +.codebuddy/ diff --git a/lib/core/utils/display_mode_util.dart b/lib/core/utils/display_mode_util.dart new file mode 100644 index 0000000..d9bcbb1 --- /dev/null +++ b/lib/core/utils/display_mode_util.dart @@ -0,0 +1,35 @@ +import 'dart:io' show Platform; + +import 'package:flutter_displaymode/flutter_displaymode.dart'; + +/// 高刷新率(高刷)适配工具。 +/// +/// 部分 Android 设备出于省电考虑,默认不会启用屏幕的最高刷新率 +/// (如 120Hz / 144Hz),导致 Flutter 渲染帧率被系统限制在 60Hz 以下。 +/// 通过 [FlutterDisplayMode] 主动将显示模式切换到设备支持的最高刷新率, +/// 从而提升动画与滚动流畅度。 +/// +/// - Android:通过 `flutter_displaymode` 请求最高刷新率模式。 +/// - iOS:ProMotion 设备由系统 + Flutter 引擎自动匹配(无需额外配置, +/// `Info.plist` 中已设置 `CADisableMinimumFrameDurationOnPhone`)。 +class DisplayModeUtil { + const DisplayModeUtil._(); + + /// 在应用启动时调用,将 Android 屏幕切换到最高刷新率。 + /// + /// 保持当前分辨率不变,仅将刷新率提升到最高可用档位(如 60 → 120Hz)。 + /// 非关键流程,失败仅打印日志,不阻塞启动。仅在 Android 平台生效。 + static Future enableBestDisplayMode() async { + if (!Platform.isAndroid) { + return; + } + try { + // 官方便捷方法:在当前分辨率下选择最高刷新率的显示模式。 + await FlutterDisplayMode.setHighRefreshRate(); + } catch (e) { + // 高刷适配为可选优化,失败不影响主流程。 + // ignore: avoid_print + print('enableBestDisplayMode failed: $e'); + } + } +} diff --git a/lib/features/auth/presentation/auth_controller.dart b/lib/features/auth/presentation/auth_controller.dart index 9320b61..c0f3891 100644 --- a/lib/features/auth/presentation/auth_controller.dart +++ b/lib/features/auth/presentation/auth_controller.dart @@ -5,6 +5,7 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; import '../../../core/network/api_exception.dart'; import '../../../core/network/error_message.dart'; import '../../../core/storage/token_storage.dart'; +import '../../device/data/device_providers.dart'; import '../data/auth_providers.dart'; import '../domain/auth_models.dart'; import '../domain/auth_state.dart'; @@ -132,10 +133,16 @@ class AuthController extends Notifier { } /// 登出。 + /// + /// 除清除令牌与登录态外,还需失效当前账号的已绑定设备列表及由其推导的 + /// 选中设备 SN 等 Provider,否则切换账号登录后仍会读到上一账号缓存的设备/ + /// SN 数据(`myDevicesProvider` / `selectedDeviceSnProvider` 等持有旧值)。 void logout() { _countdownTimer?.cancel(); TokenStorage.clear(); state = const AuthState(); + // 失效当前账号相关的设备与选中 SN 缓存,避免下一账号登录后沿用旧 SN。 + ref.invalidate(myDevicesProvider); } Future _run(Future Function() action) async { diff --git a/lib/features/connection/data/protobuf_codec.dart b/lib/features/connection/data/protobuf_codec.dart index 7a241bd..fd10902 100644 --- a/lib/features/connection/data/protobuf_codec.dart +++ b/lib/features/connection/data/protobuf_codec.dart @@ -137,3 +137,102 @@ class ControlMessageEncoder { } } } + +/// 被控端上报的远程视频信息(REPORT_RESOLUTION / REPORT_STREAM_MODE)。 +class ReportInfo { + final int action; + final int width; + final int height; + final int fps; + final int streamMode; + final List supportedFps; + + const ReportInfo({ + this.action = 0, + this.width = 0, + this.height = 0, + this.fps = 0, + this.streamMode = 0, + this.supportedFps = const [], + }); +} + +/// protobuf wire format 解码器。 +/// +/// 与控制端 `com.ttstd.control.ControlMessage`(protobuf 3)字段号完全对齐, +/// 仅解析被控端会回传的字段(action/width/height/fps/stream_mode/supported_fps)。 +class ControlMessageDecoder { + /// 解析被控端上报的 REPORT_* 消息;无法解析返回 null。 + static ReportInfo? decode(Uint8List data) { + int action = 0; + int width = 0; + int height = 0; + int fps = 0; + int streamMode = 0; + final supportedFps = []; + + final reader = ByteData.sublistView(data); + var pos = 0; + try { + while (pos < data.length) { + final tag = _readVarint(reader, pos); + pos = tag.$2; + final field = tag.$1 >> 3; + final wireType = tag.$1 & 0x07; + switch (wireType) { + case 0: // varint + final v = _readVarint(reader, pos); + pos = v.$2; + final val = v.$1; + switch (field) { + case 1: + action = val; + break; + case 12: + width = val; + break; + case 13: + height = val; + break; + case 14: + fps = val; + break; + case 15: + streamMode = val; + break; + case 16: + supportedFps.add(val); + break; + } + break; + default: + // 未知 wire type:跳过(解析到 known fields 前不应出现) + return null; + } + } + } catch (_) { + return null; + } + return ReportInfo( + action: action, + width: width, + height: height, + fps: fps, + streamMode: streamMode, + supportedFps: supportedFps, + ); + } + + /// 读取一个 varint,返回 (值, 新的位置)。越界抛异常。 + static (int, int) _readVarint(ByteData data, int pos) { + var result = 0; + var shift = 0; + while (true) { + final byte = data.getUint8(pos++); + result |= (byte & 0x7F) << shift; + if ((byte & 0x80) == 0) break; + shift += 7; + } + return (result, pos); + } +} diff --git a/lib/features/connection/data/video_recorder.dart b/lib/features/connection/data/video_recorder.dart new file mode 100644 index 0000000..46df64a --- /dev/null +++ b/lib/features/connection/data/video_recorder.dart @@ -0,0 +1,99 @@ +import 'dart:io'; + +import 'package:flutter_webrtc/flutter_webrtc.dart'; +import 'package:path_provider/path_provider.dart'; + +/// 远程视频录制器:基于 flutter_webrtc 的 [MediaRecorder], +/// 把远端视频轨道(MediaStreamTrack)直接封装为 MP4 保存到本地。 +/// +/// 跨平台(Android / iOS)由 flutter_webrtc 内部完成 H.264 编码与封装, +/// 无需自行触碰原生 VideoSink。 +/// +/// 录制文件保存位置: +/// - Android:/Android/data/<包名>/files/WebRTCRecordings/(app 专属外部存储,无需存储权限) +/// - iOS:<App>/Documents/WebRTCRecordings/ +class VideoRecorder { + MediaRecorder? _recorder; + bool _recording = false; + String? _currentPath; + + /// 是否正在录制。 + bool get isRecording => _recording; + + /// 是否为空(从未真正开始)。 + String? get currentPath => _currentPath; + + /// 开始录制指定媒体流中的视频轨道。 + /// + /// 返回 true 表示成功开始;空流 / 无视频轨道会抛异常。 + Future start(MediaStream stream) async { + if (_recording) return false; + final videoTracks = stream.getVideoTracks(); + if (videoTracks.isEmpty) { + throw Exception('当前没有可用的远端视频轨道,无法录制'); + } + final path = await _buildOutputPath(); + // ignore: avoid_print + print('[VideoRecorder] 录制开始,保存地址:$path'); + _recorder = MediaRecorder(albumName: 'WebRTCRecordings'); + await _recorder!.start(path, videoTrack: videoTracks.first); + _currentPath = path; + _recording = true; + return true; + } + + /// 停止录制,返回最终保存的文件路径;未开始录制返回 null。 + Future stop() async { + if (!_recording || _recorder == null) return null; + try { + await _recorder!.stop(); + } catch (e) { + // 停止失败时仍清理状态,避免界面卡在"录制中" + // ignore: avoid_print + print('[VideoRecorder] stop error: $e'); + } finally { + _recorder = null; + _recording = false; + } + final p = _currentPath; + _currentPath = null; + // ignore: avoid_print + print('[VideoRecorder] 录制结束,已保存至:$p'); + return p; + } + + /// 释放资源(断开连接 / 页面销毁时调用)。 + Future dispose() async { + if (_recording && _recorder != null) { + try { + await _recorder!.stop(); + } catch (_) { + // 忽略 + } + } + _recorder = null; + _recording = false; + _currentPath = null; + } + + static String _pad(int n) => n.toString().padLeft(2, '0'); + + static Future _getRecordingDir() async { + final base = Platform.isAndroid + ? (await getExternalStorageDirectory())! + : await getApplicationDocumentsDirectory(); + final dir = Directory('${base.path}/WebRTCRecordings'); + if (!await dir.exists()) { + await dir.create(recursive: true); + } + return dir; + } + + static Future _buildOutputPath() async { + final dir = await _getRecordingDir(); + final now = DateTime.now(); + final name = 'rec_${now.year}${_pad(now.month)}${_pad(now.day)}_' + '${_pad(now.hour)}${_pad(now.minute)}${_pad(now.second)}.mp4'; + return '${dir.path}/$name'; + } +} diff --git a/lib/features/connection/data/webrtc_orchestrator.dart b/lib/features/connection/data/webrtc_orchestrator.dart index 213afd4..46996ff 100644 --- a/lib/features/connection/data/webrtc_orchestrator.dart +++ b/lib/features/connection/data/webrtc_orchestrator.dart @@ -54,8 +54,8 @@ class WebRtcOrchestrator { /// 远程屏幕渲染器(供页面 RTCVideoView 使用)。 final RTCVideoRenderer remoteRenderer = RTCVideoRenderer(); - /// 接收到的被控端报告消息(如 REPORT_RESOLUTION)。 - void Function(Uint8List data)? onControlMessage; + /// 被控端上报的 REPORT_* 消息(REPORT_RESOLUTION / REPORT_STREAM_MODE)。 + void Function(ReportInfo report)? onReport; /// 连接状态变化(用于 UI 显示 "已连接")。 void Function(bool connected)? onConnectionState; @@ -74,6 +74,7 @@ class WebRtcOrchestrator { /// 发起连接:创建 PeerConnection + DataChannel + 发送 OFFER。 Future start() async { + await remoteRenderer.initialize(); await _initPeerConnection(); // 创建 control_channel(有序、可靠,保证指令不丢) @@ -83,11 +84,18 @@ class WebRtcOrchestrator { _controlChannel = await _pc!.createDataChannel(_dataChannelLabel, dataChannelInit); _controlChannel!.onMessage = (RTCDataChannelMessage message) { if (message.isBinary) { - onControlMessage?.call(message.binary); + final report = ControlMessageDecoder.decode(message.binary); + if (report != null) { + onReport?.call(report); + } } }; _controlChannel!.onDataChannelState = (RTCDataChannelState state) { debugPrint('[webrtc] control_channel state: $state'); + if (state == RTCDataChannelState.RTCDataChannelOpen) { + // DataChannel 开启后发送一次默认分辨率指令,确保被控端开始推流(解决黑屏需操作才显示的 bug) + sendSetResolution(); + } }; // 创建 Offer @@ -127,22 +135,16 @@ class WebRtcOrchestrator { debugPrint('[webrtc] 收到远程屏幕轨道'); final stream = event.streams.isNotEmpty ? event.streams.first : null; if (stream != null) { - // RTCVideoRenderer 必须先 initialize() 再设置 srcObject,否则抛 - // "Call initialize before setting the stream" 异常,导致画面不渲染。 - remoteRenderer.initialize().then((_) { - remoteRenderer.srcObject = stream; - }).catchError((e) { - debugPrint('[webrtc] renderer initialize error: $e'); - }); + remoteRenderer.srcObject = stream; onConnectionState?.call(true); + // 媒体就绪后唤醒被控端持续推流,解决静止画面黑屏/需操作才显示。 + _wakeUpStreaming(); } }; _pc!.onConnectionState = (RTCPeerConnectionState state) { debugPrint('[webrtc] connection state: ${state.name}'); - if (state == RTCPeerConnectionState.RTCPeerConnectionStateConnected) { - onConnectionState?.call(true); - } else if (state == RTCPeerConnectionState.RTCPeerConnectionStateDisconnected || + if (state == RTCPeerConnectionState.RTCPeerConnectionStateDisconnected || state == RTCPeerConnectionState.RTCPeerConnectionStateFailed || state == RTCPeerConnectionState.RTCPeerConnectionStateClosed) { onConnectionState?.call(false); @@ -264,6 +266,31 @@ class WebRtcOrchestrator { sendCommand(bytes); } + /// 发送串流模式指令(mode:0=WebRTC 内置媒体流,1=自编码 H.264)。 + /// + /// 注意:被控端 `stream_mode` 语义是「编码方式」而非「按需/持续推流」。 + /// 本控制端未移植自编码解码链路(方案 §7.1 裁剪),因此**不要**发送 + /// `mode=1`,否则被控端会切到无人消费的 `video_channel` 自编码裸流导致黑屏。 + void sendSetStreamMode(int mode) { + final bytes = ControlMessageEncoder( + action: ActionType.setStreamMode, + streamMode: mode, + ).encode(); + sendCommand(bytes); + } + + /// 媒体连接建立后唤醒被控端推帧:延迟发送一次轻量指令,确保被控端已就绪。 + /// + /// 静止画面持续推流由被控端自身保障(`ScreenCaptureService` 的 heartbeat + /// 每秒刷新通知扰动 MediaProjection 强制产帧),控制端无需也不应发送 + /// `SET_STREAM_MODE`。这里仅请求一次默认分辨率(width=0=原生)触发关键帧刷新。 + void _wakeUpStreaming() { + Future.delayed(const Duration(milliseconds: 300), () { + // 请求一次默认分辨率(原生长边),触发被控端关键帧刷新远端画面。 + sendSetResolution(); + }); + } + /// 关闭连接。 Future dispose() async { try { diff --git a/lib/features/connection/presentation/connection_controller.dart b/lib/features/connection/presentation/connection_controller.dart index 3ebfec6..e111e3a 100644 --- a/lib/features/connection/presentation/connection_controller.dart +++ b/lib/features/connection/presentation/connection_controller.dart @@ -3,30 +3,84 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_webrtc/flutter_webrtc.dart'; import '../data/connection_providers.dart'; +import '../data/protobuf_codec.dart'; +import '../data/video_recorder.dart'; import '../data/webrtc_orchestrator.dart'; import '../domain/connection_models.dart'; +/// 分辨率预设(与被控端 SET_RESOLUTION 语义一致:width<=0 表示原始)。 +const List> kResolutionOptions = [ + {'label': '原始', 'width': 0, 'height': 0, 'fps': 0}, + {'label': '1080P', 'width': 1920, 'height': 0, 'fps': 0}, + {'label': '720P', 'width': 1280, 'height': 0, 'fps': 0}, + {'label': '480P', 'width': 854, 'height': 0, 'fps': 0}, +]; + +/// 默认帧率档位(收到被控端上报的 supported_fps 后以上报列表为准)。 +const List kDefaultFpsOptions = [15, 24, 30, 60]; + /// 远程控制会话状态(供 UI 渲染)。 class ConnectionSessionState { final ConnectionState state; final String? errorMessage; final bool hasVideo; + /// 是否正在录制远程视频。 + final bool recording; + /// 录制状态提示(已保存路径等)。 + final String recordStatus; + + /// 当前选中的分辨率预设下标。 + final int selectedResolution; + /// 分辨率预设列表。 + final List> resolutionOptions; + /// 帧率档位(收到被控端上报后以上报列表为准)。 + final List fpsOptions; + /// 被控端当前采集帧率(0 表示尚未收到上报)。 + final int currentFps; + /// 被控端最近上报的实际采集尺寸(切帧率时保持分辨率不变)。 + final int lastReportedWidth; + final int lastReportedHeight; + const ConnectionSessionState({ this.state = ConnectionState.idle, this.errorMessage, this.hasVideo = false, + this.recording = false, + this.recordStatus = '', + this.selectedResolution = 1, + this.resolutionOptions = kResolutionOptions, + this.fpsOptions = kDefaultFpsOptions, + this.currentFps = 0, + this.lastReportedWidth = 0, + this.lastReportedHeight = 0, }); ConnectionSessionState copyWith({ ConnectionState? state, String? errorMessage, bool? hasVideo, + bool? recording, + String? recordStatus, + int? selectedResolution, + List>? resolutionOptions, + List? fpsOptions, + int? currentFps, + int? lastReportedWidth, + int? lastReportedHeight, }) { return ConnectionSessionState( state: state ?? this.state, errorMessage: errorMessage ?? this.errorMessage, hasVideo: hasVideo ?? this.hasVideo, + recording: recording ?? this.recording, + recordStatus: recordStatus ?? this.recordStatus, + selectedResolution: selectedResolution ?? this.selectedResolution, + resolutionOptions: resolutionOptions ?? this.resolutionOptions, + fpsOptions: fpsOptions ?? this.fpsOptions, + currentFps: currentFps ?? this.currentFps, + lastReportedWidth: lastReportedWidth ?? this.lastReportedWidth, + lastReportedHeight: lastReportedHeight ?? this.lastReportedHeight, ); } } @@ -34,6 +88,7 @@ class ConnectionSessionState { /// 远程控制控制器:管理连接状态机(idle → connecting → connected → ended)。 class ConnectionController extends Notifier { WebRtcOrchestrator? _orchestrator; + final VideoRecorder _videoRecorder = VideoRecorder(); @override ConnectionSessionState build() { @@ -71,6 +126,9 @@ class ConnectionController extends Notifier { _orchestrator!.onConnectionState = (connected) { _onRtcState(connected); }; + _orchestrator!.onReport = (report) { + _onReport(report); + }; signaling.onMessage = (message) { // 先透传给 WebRTC 编排器处理(ANSWER/ICE 等),确保握手能继续 @@ -104,12 +162,33 @@ class ConnectionController extends Notifier { void _onRtcState(bool connected) { if (connected) { - state = const ConnectionSessionState( + state = state.copyWith( state: ConnectionState.connected, hasVideo: true, ); - } else if (state.state == ConnectionState.connected) { - state = const ConnectionSessionState(state: ConnectionState.ended); + // 连接成功后按默认分辨率(1080P)下发一次,确保默认生效。 + selectResolution(state.selectedResolution); + } else { + state = state.copyWith( + state: ConnectionState.ended, + hasVideo: false, + ); + } + } + + /// 处理被控端经 DataChannel 上报的 REPORT_RESOLUTION / REPORT_STREAM_MODE。 + void _onReport(ReportInfo report) { + if (report.width > 0 && report.height > 0) { + state = state.copyWith( + lastReportedWidth: report.width, + lastReportedHeight: report.height, + ); + } + if (report.fps > 0) { + state = state.copyWith(currentFps: report.fps); + } + if (report.supportedFps.isNotEmpty) { + state = state.copyWith(fpsOptions: report.supportedFps); } } @@ -143,8 +222,68 @@ class ConnectionController extends Notifier { _orchestrator?.sendSetResolution(width: width, height: height, fps: fps); } + /// 切换分辨率:写入状态并下发指令。 + void selectResolution(int index) { + final options = state.resolutionOptions; + if (index < 0 || index >= options.length) return; + state = state.copyWith(selectedResolution: index); + final o = options[index]; + _orchestrator?.sendSetResolution( + width: o['width'] as int, + height: o['height'] as int, + fps: o['fps'] as int, + ); + } + + /// 切换帧率:仅切帧率,分辨率保持不变。 + void selectFps(int fps) { + if (fps <= 0 || fps == state.currentFps) return; + _orchestrator?.sendSetResolution( + width: state.lastReportedWidth, + height: state.lastReportedHeight, + fps: fps, + ); + } + + /// 切换远程视频录制:开始 / 停止。 + Future toggleRecord() async { + if (state.recording) { + await _stopRecording(); + return; + } + await _startRecording(); + } + + Future _startRecording() async { + if (state.recording) return; + final renderer = _orchestrator?.remoteRenderer; + final stream = renderer?.srcObject; + if (stream == null) { + state = state.copyWith(recordStatus: '尚未接收到视频画面,无法录制'); + return; + } + try { + final ok = await _videoRecorder.start(stream); + if (ok) { + state = state.copyWith(recording: true, recordStatus: '录制中...'); + } + } catch (e) { + state = state.copyWith(recordStatus: ''); + debugPrint('[record] 开始录制失败: $e'); + } + } + + Future _stopRecording() async { + final path = await _videoRecorder.stop(); + state = state.copyWith( + recording: false, + recordStatus: path != null ? '已保存:$path' : '录制已停止', + ); + } + /// 关闭并清理。 Future disconnect() async { + await _videoRecorder.dispose(); await _orchestrator?.dispose(); _orchestrator = null; ref.read(signalingClientProvider).dispose(); diff --git a/lib/features/connection/presentation/remote_control_page.dart b/lib/features/connection/presentation/remote_control_page.dart index 3df1364..f71ef82 100644 --- a/lib/features/connection/presentation/remote_control_page.dart +++ b/lib/features/connection/presentation/remote_control_page.dart @@ -1,3 +1,5 @@ +import 'dart:async'; + import 'package:cupertino_ui/cupertino_ui.dart'; import 'package:flutter_webrtc/flutter_webrtc.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; @@ -24,6 +26,10 @@ class RemoteControlPage extends ConsumerStatefulWidget { } class _RemoteControlPageState extends ConsumerState { + /// 录制状态条是否可见。 + bool _showRecordStatus = false; + Timer? _recordStatusTimer; + @override void initState() { super.initState(); @@ -35,32 +41,85 @@ class _RemoteControlPageState extends ConsumerState { }); } + @override + void dispose() { + _recordStatusTimer?.cancel(); + super.dispose(); + } + @override Widget build(BuildContext context) { final session = ref.watch(connectionControllerProvider); final renderer = ref.read(connectionControllerProvider.notifier).remoteRenderer; final connected = session.state == connection.ConnectionState.connected; - return CupertinoPageScaffold( - navigationBar: CupertinoNavigationBar( - middle: const Text('远程控制'), - trailing: CupertinoButton( - padding: EdgeInsets.zero, - onPressed: () { - ref.read(connectionControllerProvider.notifier).disconnect(); - Navigator.of(context).maybePop(); - }, - child: const Text('退出'), + // 监听录制状态文字:录制中一直显示;非录制时更新后显示,3 秒后自动隐藏。 + ref.listen( + connectionControllerProvider.select((s) => s.recordStatus), + (previous, next) { + if (next.isEmpty) { + _recordStatusTimer?.cancel(); + if (_showRecordStatus) { + setState(() => _showRecordStatus = false); + } + } else { + _recordStatusTimer?.cancel(); + setState(() => _showRecordStatus = true); + // 录制中保持常显;非录制(如保存提示)3 秒后自动隐藏。 + final recording = ref.read(connectionControllerProvider).recording; + if (!recording) { + _recordStatusTimer = Timer(const Duration(seconds: 3), () { + if (mounted) setState(() => _showRecordStatus = false); + }); + } + } + }, + ); + + // 用 PopScope 统一处理所有退出入口(自带返回箭头 / 系统返回键): + // 无论从哪个入口离开页面,都在 pop 后断开远程连接。 + return PopScope( + canPop: true, + onPopInvokedWithResult: (didPop, _) { + if (didPop) { + ref.read(connectionControllerProvider.notifier).disconnect(); + } + }, + child: CupertinoPageScaffold( + navigationBar: CupertinoNavigationBar( + middle: const Text('远程控制'), + trailing: CupertinoButton( + padding: EdgeInsets.zero, + onPressed: () => _showMoreMenu(session), + child: const Icon(CupertinoIcons.ellipsis, size: 22), + ), ), - ), - child: SafeArea( - child: Column( - children: [ - Expanded( - child: _buildVideo(renderer, connected, session), - ), - _buildControlsBar(session), - ], + child: SafeArea( + child: Stack( + children: [ + Column( + children: [ + Expanded( + child: _buildVideo(renderer, connected, session), + ), + _buildControlsBar(session), + ], + ), + // 悬浮录制状态提示:叠加在视频区顶部,自适应宽度居中,不占用布局空间。 + if (_showRecordStatus && session.recordStatus.isNotEmpty) + Positioned( + top: 8, + left: 0, + right: 0, + child: IgnorePointer( + child: Align( + alignment: Alignment.topCenter, + child: _buildRecordStatusBar(session), + ), + ), + ), + ], + ), ), ), ); @@ -189,6 +248,24 @@ class _RemoteControlPageState extends ConsumerState { _lastMoveSample = null; } + /// 悬浮录制状态提示条:自适应宽度居中,录制中红色、非录制灰色。 + Widget _buildRecordStatusBar(ConnectionSessionState session) { + return Container( + padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 5), + decoration: BoxDecoration( + color: session.recording + ? CupertinoColors.systemRed.withValues(alpha: 0.9) + : CupertinoColors.systemGrey.withValues(alpha: 0.85), + borderRadius: BorderRadius.circular(14), + ), + child: Text( + session.recordStatus, + style: const TextStyle(fontSize: 12, color: CupertinoColors.white), + overflow: TextOverflow.ellipsis, + ), + ); + } + Widget _buildControlsBar(ConnectionSessionState session) { final connected = session.state == connection.ConnectionState.connected; return Container( @@ -197,37 +274,199 @@ class _RemoteControlPageState extends ConsumerState { color: CupertinoColors.systemGroupedBackground, border: Border(top: BorderSide(color: CupertinoColors.separator, width: 0.5)), ), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceEvenly, + child: Column( + mainAxisSize: MainAxisSize.min, children: [ - _CtrlButton( - icon: CupertinoIcons.speaker_slash_fill, - label: '静音', - enabled: connected, - onTap: () {}, - ), - _CtrlButton( - icon: CupertinoIcons.home, - label: '主页', - enabled: connected, - onTap: () => ref.read(connectionControllerProvider.notifier).sendKey(3), // KEYCODE_HOME - ), - _CtrlButton( - icon: CupertinoIcons.arrow_left, - label: '返回', - enabled: connected, - onTap: () => ref.read(connectionControllerProvider.notifier).sendKey(4), // KEYCODE_BACK - ), - _CtrlButton( - icon: CupertinoIcons.rectangle_grid_2x2, - label: '最近', - enabled: connected, - onTap: () => ref.read(connectionControllerProvider.notifier).sendKey(187), // KEYCODE_APP_SWITCH + // 单行:返回 / 主页 / 最近 + Row( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + children: [ + _CtrlButton( + icon: CupertinoIcons.arrow_left, + label: '返回', + enabled: connected, + onTap: () => ref.read(connectionControllerProvider.notifier).sendKey(4), // KEYCODE_BACK + ), + _CtrlButton( + icon: CupertinoIcons.home, + label: '主页', + enabled: connected, + onTap: () => ref.read(connectionControllerProvider.notifier).sendKey(3), // KEYCODE_HOME + ), + _CtrlButton( + icon: CupertinoIcons.rectangle_grid_2x2, + label: '最近', + enabled: connected, + onTap: () => ref.read(connectionControllerProvider.notifier).sendKey(187), // KEYCODE_APP_SWITCH + ), + ], ), ], ), ); } + + /// 弹出「更多」菜单(iOS 风格 ActionSheet):静音 / 分辨率 / 帧率 / 录制。 + void _showMoreMenu(ConnectionSessionState session) { + final controller = ref.read(connectionControllerProvider.notifier); + final recording = session.recording; + showCupertinoModalPopup( + context: context, + builder: (ctx) => CupertinoActionSheet( + title: const Text('更多操作'), + actions: [ + CupertinoActionSheetAction( + onPressed: () { + Navigator.of(ctx).pop(); + // TODO: 静音切换 + }, + child: const Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon(CupertinoIcons.speaker_slash_fill, + size: 18, color: CupertinoColors.systemGrey), + SizedBox(width: 8), + Text('静音'), + ], + ), + ), + CupertinoActionSheetAction( + onPressed: () { + Navigator.of(ctx).pop(); + _showResolutionMenu(session); + }, + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + const Icon(CupertinoIcons.slider_horizontal_3, + size: 18, color: CupertinoColors.systemGrey), + const SizedBox(width: 8), + Text( + session.resolutionOptions[session.selectedResolution]['label'] + as String, + ), + ], + ), + ), + CupertinoActionSheetAction( + onPressed: () { + Navigator.of(ctx).pop(); + _showFpsMenu(session); + }, + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + const Icon(CupertinoIcons.speedometer, + size: 18, color: CupertinoColors.systemGrey), + const SizedBox(width: 8), + Text( + session.currentFps > 0 ? '${session.currentFps}fps' : '帧率', + ), + ], + ), + ), + CupertinoActionSheetAction( + isDestructiveAction: recording, + onPressed: () { + Navigator.of(ctx).pop(); + controller.toggleRecord(); + }, + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon( + recording ? CupertinoIcons.stop_circle : CupertinoIcons.video_camera, + size: 18, + color: recording + ? CupertinoColors.destructiveRed + : CupertinoColors.systemGrey, + ), + const SizedBox(width: 8), + Text(recording ? '停止录制' : '录制'), + ], + ), + ), + ], + cancelButton: CupertinoActionSheetAction( + isDefaultAction: true, + onPressed: () => Navigator.of(ctx).pop(), + child: const Text('取消'), + ), + ), + ); + } + + /// 弹出分辨率选择菜单(iOS 风格 ActionSheet)。 + void _showResolutionMenu(ConnectionSessionState session) { + final controller = ref.read(connectionControllerProvider.notifier); + showCupertinoModalPopup( + context: context, + builder: (ctx) => CupertinoActionSheet( + title: const Text('切换分辨率'), + actions: [ + for (int i = 0; i < session.resolutionOptions.length; i++) + CupertinoActionSheetAction( + onPressed: () { + Navigator.of(ctx).pop(); + controller.selectResolution(i); + }, + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + if (i == session.selectedResolution) ...[ + const Icon(CupertinoIcons.check_mark, + size: 18, color: CupertinoColors.activeBlue), + const SizedBox(width: 8), + ], + Text(session.resolutionOptions[i]['label'] as String), + ], + ), + ), + ], + cancelButton: CupertinoActionSheetAction( + isDefaultAction: true, + onPressed: () => Navigator.of(ctx).pop(), + child: const Text('取消'), + ), + ), + ); + } + + /// 弹出帧率选择菜单(iOS 风格 ActionSheet);仅切帧率,分辨率保持不变。 + void _showFpsMenu(ConnectionSessionState session) { + final controller = ref.read(connectionControllerProvider.notifier); + showCupertinoModalPopup( + context: context, + builder: (ctx) => CupertinoActionSheet( + title: const Text('切换帧率'), + actions: [ + for (final fps in session.fpsOptions) + CupertinoActionSheetAction( + onPressed: () { + Navigator.of(ctx).pop(); + controller.selectFps(fps); + }, + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + if (fps == session.currentFps) ...[ + const Icon(CupertinoIcons.check_mark, + size: 18, color: CupertinoColors.activeBlue), + const SizedBox(width: 8), + ], + Text('${fps}fps'), + ], + ), + ), + ], + cancelButton: CupertinoActionSheetAction( + isDefaultAction: true, + onPressed: () => Navigator.of(ctx).pop(), + child: const Text('取消'), + ), + ), + ); + } } class _CtrlButton extends StatelessWidget { @@ -250,7 +489,11 @@ class _CtrlButton extends StatelessWidget { child: Column( mainAxisSize: MainAxisSize.min, children: [ - Icon(icon, size: 26, color: enabled ? CupertinoColors.activeBlue : CupertinoColors.systemGrey3), + Icon( + icon, + size: 20, + color: enabled ? CupertinoColors.activeBlue : CupertinoColors.systemGrey3, + ), const SizedBox(height: 4), Text(label, style: const TextStyle(fontSize: 12)), ], diff --git a/lib/features/home/presentation/home_tab.dart b/lib/features/home/presentation/home_tab.dart index f6b6b4a..aef253d 100644 --- a/lib/features/home/presentation/home_tab.dart +++ b/lib/features/home/presentation/home_tab.dart @@ -14,21 +14,21 @@ import 'location_map_view.dart'; /// /// 对应移动端设计稿的「平板信息页」:实时位置地图、最近截图、 /// 今日使用时长、设备操作(重启/关机/截屏/刷新/定位)与常用功能网格。 +/// +/// 性能优化说明: +/// - 首页包含百度地图(原生平台视图)与网络图片等高重绘成本内容。 +/// - 为提升下拉/滚动流畅度,将数据驱动的卡片(定位卡片、截图卡片)拆分为 +/// 独立的 `ConsumerWidget`,各自 `watch` 自己的 Provider,实现**局部刷新**, +/// 避免任意一个异步数据变化时重建整页(尤其避免重建地图原生视图)。 +/// - 地图使用 `RepaintBoundary` 隔离重绘;静态卡片尽量 `const` 化。 class HomeTab extends ConsumerWidget { const HomeTab({super.key}); @override Widget build(BuildContext context, WidgetRef ref) { final l10n = AppLocalizations.of(context); + // 仅在此 watch 设备 SN;设备切换时才重建整页结构,其余异步数据由各卡片自行 watch。 final sn = ref.watch(selectedDeviceSnProvider); - final locationAsync = sn == null - ? const AsyncValue.data(null) - : ref.watch(deviceLocationProvider(sn)); - final location = locationAsync.valueOrNull; - final locating = locationAsync.isLoading; - final screenshotsAsync = sn == null - ? const AsyncValue>.data([]) - : ref.watch(recentScreenshotsProvider(sn)); return CupertinoPageScaffold( navigationBar: CupertinoNavigationBar( @@ -84,17 +84,34 @@ class HomeTab extends ConsumerWidget { SliverPadding( padding: const EdgeInsets.only(bottom: 96), sliver: SliverList( - delegate: SliverChildListDelegate([ - _locationCard(l10n, - location: location, - locating: locating, - onTap: () => context.push('/map')), - _screenshotsCard(context, l10n, screenshotsAsync, ref), - _usageCard(l10n), - _deviceOpsCard(context, l10n, ref, sn), - SectionTitle(l10n.homeCommon), - _commonCard(context, ref, l10n, sn), - ]), + // 用 builder 按需懒构建,避免一次性持有所有卡片 Element; + // 配合下方各卡片自身的局部刷新,减少无效重建。 + delegate: SliverChildBuilderDelegate( + (context, index) { + // 定位卡片、截图卡片为独立 ConsumerWidget,各自 watch 自己的 + // Provider,实现局部刷新(数据变化时不重建其余静态卡片)。 + final child = switch (index) { + 0 => const _LocationCard(), + 1 => const _ScreenshotsCard(), + 2 => _usageCard(l10n), + 3 => _deviceOpsCard(context, l10n, ref, sn), + 4 => SectionTitle(l10n.homeCommon), + 5 => _commonCard(context, ref, l10n, sn), + _ => const SizedBox.shrink(), + }; + // 给每个 child 加上稳定 key,配合 findChildIndexCallback 实现 + // 滚动时 Element 复用,避免滚动离屏/回屏时重建高成本子树。 + return KeyedSubtree( + key: ValueKey(index), + child: child, + ); + }, + childCount: 6, + findChildIndexCallback: (key) { + final v = key is ValueKey ? key.value : null; + return (v != null && v < 6) ? v : null; + }, + ), ), ), ], @@ -144,243 +161,6 @@ class HomeTab extends ConsumerWidget { ), ); - Widget _locationCard( - AppLocalizations l10n, { - required DeviceLocation? location, - required bool locating, - required VoidCallback onTap, - }) => - GestureDetector( - onTap: onTap, - child: CardBox( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - // 标题栏:实时位置 + 定位状态,右上角查看详情入口 - Padding( - padding: const EdgeInsets.only(bottom: 12), - child: Row( - children: [ - Container( - width: 22, - height: 22, - decoration: BoxDecoration( - color: AppColors.green, - borderRadius: BorderRadius.circular(7), - ), - child: Center( - child: Text('📍', style: const TextStyle(fontSize: 13))), - ), - const SizedBox(width: 7), - Text(l10n.homeLocation, - style: const TextStyle( - fontSize: 15, - fontWeight: FontWeight.w700, - color: AppColors.ink)), - const SizedBox(width: 8), - // 定位状态:放在"实时位置"标题后面 - Container( - padding: const EdgeInsets.symmetric( - horizontal: 8, vertical: 2), - decoration: BoxDecoration( - color: locating - ? AppColors.surface - : AppColors.green.withValues(alpha: 0.12), - borderRadius: BorderRadius.circular(20), - ), - child: Text( - locating - ? l10n.homeLocating - : (location?.displayAddress.isNotEmpty == true - ? l10n.homeLocated - : l10n.homeNoLocation), - style: TextStyle( - fontSize: 11, - fontWeight: FontWeight.w600, - color: locating - ? AppColors.sub - : (location?.displayAddress.isNotEmpty == true - ? AppColors.green - : AppColors.sub), - ), - ), - ), - const Spacer(), - // 右上角查看详情 - GestureDetector( - onTap: onTap, - behavior: HitTestBehavior.opaque, - child: Row( - mainAxisSize: MainAxisSize.min, - children: [ - Text(l10n.homeViewDetail, - style: const TextStyle( - fontSize: 12, - fontWeight: FontWeight.w600, - color: AppColors.blue)), - const SizedBox(width: 2), - const Icon(CupertinoIcons.chevron_right, - size: 14, color: AppColors.blue), - ], - ), - ), - ], - ), - ), - Stack( - children: [ - // 百度地图实时位置(须明确宽高,否则地图空白) - ClipRRect( - borderRadius: BorderRadius.circular(14), - child: SizedBox( - width: double.infinity, - height: 150, - child: LocationMapView(location: location), - ), - ), - // 地址信息浮层 - // 右侧留出间隔,避免遮挡地图右侧的放大缩小按钮。 - Positioned( - left: 12, - right: 52, - bottom: 10, - child: Container( - padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 6), - decoration: BoxDecoration( - color: CupertinoColors.white.withValues(alpha: 0.92), - borderRadius: BorderRadius.circular(10), - ), - child: Text( - locating - ? '📍 ${l10n.homeLocating}…' - : (location?.displayAddress.isNotEmpty == true - ? '📍 ${location!.displayAddress}' - : '📍 ${l10n.homeAddress}'), - style: const TextStyle(fontSize: 12, color: AppColors.ink), - maxLines: 2, - overflow: TextOverflow.ellipsis, - ), - ), - ), - ], - ), - ], - ), - ), - ); - - Widget _screenshotsCard( - BuildContext context, - AppLocalizations l10n, - AsyncValue> screenshots, - WidgetRef ref, - ) => - CardBox( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - _cardHeader( - '🖼️', - AppColors.blue, - l10n.homeScreenshots, - trailing: l10n.homeViewAll, - onTrailingTap: (screenshots.valueOrNull?.isNotEmpty ?? false) - ? () => context.push('/screenshots', extra: 0) - : null, - ), - SizedBox( - height: 120, - child: screenshots.when( - loading: () => const Center( - child: CupertinoActivityIndicator(radius: 12)), - error: (e, _) => Center( - child: Text('加载失败', - style: const TextStyle(fontSize: 12, color: AppColors.sub)), - ), - data: (list) { - if (list.isEmpty) { - return Center( - child: Text(l10n.homeNoScreenshot, - style: const TextStyle(fontSize: 12, color: AppColors.sub)), - ); - } - // 卡片内容宽 = 屏幕宽 - 外间距(16×2) - 内边距(16×2)。 - // 一排恰好 3 张:减去 2 个间距(10×2) 后均分。 - const gap = 10.0; - const cardInsets = 16.0 * 2 + 16.0 * 2; - final itemWidth = - (MediaQuery.sizeOf(context).width - cardInsets - gap * 2) / 3; - return ListView.separated( - scrollDirection: Axis.horizontal, - itemCount: list.length, - separatorBuilder: (_, _) => const SizedBox(width: gap), - itemBuilder: (_, i) { - final item = list[i]; - // 统一把原始时间戳格式化为友好显示。 - final time = formatTimeString(item.uploadTime); - return GestureDetector( - onTap: () => context.push('/screenshots', extra: i), - child: ClipRRect( - borderRadius: BorderRadius.circular(12), - child: SizedBox( - width: itemWidth, - child: Stack( - fit: StackFit.expand, - children: [ - Image.network( - item.url ?? '', - fit: BoxFit.cover, - loadingBuilder: (ctx, child, progress) => - progress == null - ? child - : const Center( - child: CupertinoActivityIndicator( - radius: 10)), - errorBuilder: (ctx, err, _) => Container( - color: const Color(0xFFEEF2F7), - child: const Center( - child: Icon(CupertinoIcons.photo, - size: 22, color: AppColors.sub), - ), - ), - ), - Positioned( - left: 0, - right: 0, - bottom: 0, - child: Container( - padding: const EdgeInsets.symmetric( - horizontal: 6, vertical: 2), - decoration: BoxDecoration( - color: CupertinoColors.black - .withValues(alpha: 0.45), - borderRadius: const BorderRadius.only( - bottomLeft: Radius.circular(12), - bottomRight: Radius.circular(12), - ), - ), - child: Text(time, - maxLines: 1, - overflow: TextOverflow.ellipsis, - style: const TextStyle( - fontSize: 10, - color: CupertinoColors.white)), - ), - ), - ], - ), - ), - ), - ); - }, - ); - }, - ), - ), - ], - ), - ); - Widget _usageCard(AppLocalizations l10n) => CardBox( child: Column( crossAxisAlignment: CrossAxisAlignment.start, @@ -925,3 +705,296 @@ class _MoreOpItem { final String label; final String op; } + +/// 实时位置卡片(独立 ConsumerWidget)。 +/// +/// 自己 `watch` 定位 Provider,定位数据变化时**仅重建本卡片**,避免连带 +/// 重建整页其它卡片(尤其百度地图原生视图与静态卡片),是首页流畅度的关键。 +class _LocationCard extends ConsumerWidget { + const _LocationCard(); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final l10n = AppLocalizations.of(context); + final sn = ref.watch(selectedDeviceSnProvider); + final locationAsync = sn == null + ? const AsyncValue.data(null) + : ref.watch(deviceLocationProvider(sn)); + final location = locationAsync.valueOrNull; + final locating = locationAsync.isLoading; + final hasAddress = location?.displayAddress.isNotEmpty == true; + + return GestureDetector( + onTap: () => context.push('/map'), + child: CardBox( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // 标题栏:实时位置 + 定位状态,右上角查看详情入口 + Padding( + padding: const EdgeInsets.only(bottom: 12), + child: Row( + children: [ + Container( + width: 22, + height: 22, + decoration: BoxDecoration( + color: AppColors.green, + borderRadius: BorderRadius.circular(7), + ), + child: Center( + child: Text('📍', style: const TextStyle(fontSize: 13))), + ), + const SizedBox(width: 7), + Text(l10n.homeLocation, + style: const TextStyle( + fontSize: 15, + fontWeight: FontWeight.w700, + color: AppColors.ink)), + const SizedBox(width: 8), + // 定位状态:放在"实时位置"标题后面 + Container( + padding: + const EdgeInsets.symmetric(horizontal: 8, vertical: 2), + decoration: BoxDecoration( + color: locating + ? AppColors.surface + : AppColors.green.withValues(alpha: 0.12), + borderRadius: BorderRadius.circular(20), + ), + child: Text( + locating + ? l10n.homeLocating + : (hasAddress ? l10n.homeLocated : l10n.homeNoLocation), + style: TextStyle( + fontSize: 11, + fontWeight: FontWeight.w600, + color: locating + ? AppColors.sub + : (hasAddress ? AppColors.green : AppColors.sub), + ), + ), + ), + const Spacer(), + // 右上角查看详情 + GestureDetector( + onTap: () => context.push('/map'), + behavior: HitTestBehavior.opaque, + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Text(l10n.homeViewDetail, + style: const TextStyle( + fontSize: 12, + fontWeight: FontWeight.w600, + color: AppColors.blue)), + const SizedBox(width: 2), + const Icon(CupertinoIcons.chevron_right, + size: 14, color: AppColors.blue), + ], + ), + ), + ], + ), + ), + Stack( + children: [ + // 百度地图实时位置(须明确宽高,否则地图空白)。 + // RepaintBoundary 隔离原生视图重绘,避免地图刷新污染父层布局。 + RepaintBoundary( + child: ClipRRect( + borderRadius: BorderRadius.circular(14), + child: SizedBox( + width: double.infinity, + height: 150, + child: LocationMapView(location: location), + ), + ), + ), + // 地址信息浮层 + // 右侧留出间隔,避免遮挡地图右侧的放大缩小按钮。 + Positioned( + left: 12, + right: 52, + bottom: 10, + child: Container( + padding: + const EdgeInsets.symmetric(horizontal: 10, vertical: 6), + decoration: BoxDecoration( + color: CupertinoColors.white.withValues(alpha: 0.92), + borderRadius: BorderRadius.circular(10), + ), + child: Text( + locating + ? '📍 ${l10n.homeLocating}…' + : (hasAddress + ? '📍 ${location!.displayAddress}' + : '📍 ${l10n.homeAddress}'), + style: const TextStyle(fontSize: 12, color: AppColors.ink), + maxLines: 2, + overflow: TextOverflow.ellipsis, + ), + ), + ), + ], + ), + ], + ), + ), + ); + } +} + +/// 最近截图卡片(独立 ConsumerWidget)。 +/// +/// 自己 `watch` 截图 Provider,截图数据变化时仅重建本卡片,避免连带重建 +/// 整页其它卡片。水平缩略图列表用 `RepaintBoundary` 隔离重绘。 +class _ScreenshotsCard extends ConsumerWidget { + const _ScreenshotsCard(); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final l10n = AppLocalizations.of(context); + final sn = ref.watch(selectedDeviceSnProvider); + final screenshots = sn == null + ? const AsyncValue>.data([]) + : ref.watch(recentScreenshotsProvider(sn)); + + return CardBox( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // 标题栏:最近截图 + 查看全部入口 + Padding( + padding: const EdgeInsets.only(bottom: 12), + child: Row( + children: [ + Container( + width: 22, + height: 22, + decoration: BoxDecoration( + color: AppColors.blue, + borderRadius: BorderRadius.circular(7), + ), + child: Center( + child: Text('🖼️', style: const TextStyle(fontSize: 13))), + ), + const SizedBox(width: 7), + Text(l10n.homeScreenshots, + style: const TextStyle( + fontSize: 15, + fontWeight: FontWeight.w700, + color: AppColors.ink)), + const Spacer(), + if (screenshots.valueOrNull?.isNotEmpty ?? false) + GestureDetector( + onTap: () => context.push('/screenshots', extra: 0), + child: Padding( + padding: const EdgeInsets.symmetric(vertical: 4), + child: Text(l10n.homeViewAll, + style: const TextStyle( + fontSize: 12, + fontWeight: FontWeight.w600, + color: AppColors.green)), + ), + ), + ], + ), + ), + SizedBox( + height: 120, + child: screenshots.when( + loading: () => + const Center(child: CupertinoActivityIndicator(radius: 12)), + error: (e, _) => Center( + child: Text('加载失败', + style: const TextStyle(fontSize: 12, color: AppColors.sub)), + ), + data: (list) { + if (list.isEmpty) { + return Center( + child: Text(l10n.homeNoScreenshot, + style: + const TextStyle(fontSize: 12, color: AppColors.sub)), + ); + } + // 卡片内容宽 = 屏幕宽 - 外间距(16×2) - 内边距(16×2)。 + // 一排恰好 3 张:减去 2 个间距(10×2) 后均分。 + const gap = 10.0; + const cardInsets = 16.0 * 2 + 16.0 * 2; + final itemWidth = + (MediaQuery.sizeOf(context).width - cardInsets - gap * 2) / 3; + return RepaintBoundary( + child: ListView.separated( + scrollDirection: Axis.horizontal, + itemCount: list.length, + separatorBuilder: (_, _) => const SizedBox(width: gap), + itemBuilder: (_, i) { + final item = list[i]; + // 统一把原始时间戳格式化为友好显示。 + final time = formatTimeString(item.uploadTime); + return GestureDetector( + onTap: () => context.push('/screenshots', extra: i), + child: ClipRRect( + borderRadius: BorderRadius.circular(12), + child: SizedBox( + width: itemWidth, + child: Stack( + fit: StackFit.expand, + children: [ + Image.network( + item.url ?? '', + fit: BoxFit.cover, + loadingBuilder: (ctx, child, progress) => + progress == null + ? child + : const Center( + child: CupertinoActivityIndicator( + radius: 10)), + errorBuilder: (ctx, err, _) => Container( + color: const Color(0xFFEEF2F7), + child: const Center( + child: Icon(CupertinoIcons.photo, + size: 22, color: AppColors.sub), + ), + ), + ), + Positioned( + left: 0, + right: 0, + bottom: 0, + child: Container( + padding: const EdgeInsets.symmetric( + horizontal: 6, vertical: 2), + decoration: BoxDecoration( + color: CupertinoColors.black + .withValues(alpha: 0.45), + borderRadius: const BorderRadius.only( + bottomLeft: Radius.circular(12), + bottomRight: Radius.circular(12), + ), + ), + child: Text(time, + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: const TextStyle( + fontSize: 10, + color: CupertinoColors.white)), + ), + ), + ], + ), + ), + ), + ); + }, + ), + ); + }, + ), + ), + ], + ), + ); + } +} diff --git a/lib/features/home/presentation/location_map_view.dart b/lib/features/home/presentation/location_map_view.dart index 2e36942..b667640 100644 --- a/lib/features/home/presentation/location_map_view.dart +++ b/lib/features/home/presentation/location_map_view.dart @@ -26,10 +26,21 @@ class LocationMapView extends ConsumerStatefulWidget { class _LocationMapViewState extends ConsumerState { BMFMapController? _mapController; bool _markerAdded = false; + bool _mapReady = false; + + /// 首次创建时计算并缓存的初始地图中心,之后固定复用。 + /// + /// ⚠️ 关键性能点:`BMFMapWidget` 的 `mapOptions` 一旦变化会触发原生地图重建。 + /// 若每次 build 都新建 `BMFMapOptions`(对象 `==` 未重写,恒为不相等), + /// 父级卡片下拉刷新导致的 rebuild 就会反复重建原生地图,造成卡顿。 + /// 因此这里只在首次 build 创建一次,中心坐标的更新统一走 + /// [_syncLocation](`setCenterCoordinate`),不再改 mapOptions。 + late final BMFCoordinate _initialCenter; @override void initState() { super.initState(); + _initialCenter = _computeCenter(widget.location); } @override @@ -43,7 +54,7 @@ class _LocationMapViewState extends ConsumerState { @override void didUpdateWidget(covariant LocationMapView oldWidget) { super.didUpdateWidget(oldWidget); - // 定位数据变化时刷新标记位置。 + // 定位数据变化时刷新标记位置(仅当地图已就绪且数据确实变化)。 if (oldWidget.location != widget.location) { _syncLocation(widget.location); } @@ -55,6 +66,8 @@ class _LocationMapViewState extends ConsumerState { // 按官方「显示定位」文档:地图加载完成后再执行图层/覆盖物操作, // 否则 addMarker / showUserLocation 等在地图未就绪时调用会不生效。 controller.setMapDidLoadCallback(callback: () { + if (_mapReady) return; // 幂等保护,避免重复注册导致重复原生调用。 + _mapReady = true; debugPrint('[LocationMapView] mapDidLoad, 执行图层与标记初始化'); // 1. 初始化定位图层 + 补偿蓝色定位点。 _enableUserLocationLayer(); @@ -113,7 +126,8 @@ class _LocationMapViewState extends ConsumerState { if (coord == null) return; final bd = wgs84ToBd09(coord.lat, coord.lon); - controller.setCenterCoordinate(bd, true); + // 关闭动画:避免滚动/下拉刷新期间触发原生地图的动画平移重绘,降低卡顿。 + controller.setCenterCoordinate(bd, false); if (!_markerAdded) { // Marker 标题展示设备 SN(缺失时兜底为"设备位置")。 final sn = location!.sn; @@ -160,9 +174,11 @@ class _LocationMapViewState extends ConsumerState { @override Widget build(BuildContext context) { - // watch 本机定位 Provider:地图可见时自动启动手机定位。 - ref.watch(locationControllerProvider); // 监听本机定位结果:成功后把蓝色定位点更新到地图。 + // ⚠️ Riverpod 要求 ref.listen 只能在 build 期间调用,故放在此处。 + // 使用 ref.listen 而非 ref.watch:仅建立监听、**不使 build 依赖该状态**, + // 因此本机定位状态变化不会触发地图原生视图(AndroidView / UiKitView)重建, + // 这是首页下拉/滚动卡顿的关键优化。ref.listen 同样会触发 Provider 初始化以启动定位。 ref.listen( locationControllerProvider, (prev, next) { @@ -181,7 +197,7 @@ class _LocationMapViewState extends ConsumerState { child: BMFMapWidget( onBMFMapCreated: _onMapCreated, mapOptions: BMFMapOptions( - center: _computeCenter(widget.location), + center: _initialCenter, // 复用首次缓存中心,避免 rebuild 触发地图重建。 zoomLevel: 16, showMapScaleBar: false, ), diff --git a/lib/main.dart b/lib/main.dart index 42f6846..59c5e0c 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -11,6 +11,7 @@ import 'app/constants/app_constants.dart'; import 'app/router/app_router.dart'; import 'core/storage/token_storage.dart'; import 'core/utils/device_info_util.dart'; +import 'core/utils/display_mode_util.dart'; import 'core/utils/system_ui_util.dart'; /// 应用入口。 @@ -19,6 +20,10 @@ import 'core/utils/system_ui_util.dart'; void main() async { WidgetsFlutterBinding.ensureInitialized(); + // Android 高刷适配:将屏幕切换到设备支持的最高刷新率(120Hz / 144Hz), + // 提升动画与滚动流畅度。iOS ProMotion 由系统自动匹配,无需处理。 + await DisplayModeUtil.enableBestDisplayMode(); + // 初始化百度地图 SDK(需申请 AK;坐标类型使用 BD09LL,设备 GPS 坐标在展示前转换)。 BMFMapSDK.setAgreePrivacy(true); BMFMapSDK.setApiKeyAndCoordType( diff --git a/pubspec.lock b/pubspec.lock index 370abb1..bb2b732 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -6,7 +6,7 @@ packages: description: name: _fe_analyzer_shared sha256: da0d9209ca76bde579f2da330aeb9df62b6319c834fa7baae052021b0462401f - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "85.0.0" analyzer: @@ -14,7 +14,7 @@ packages: description: name: analyzer sha256: f4ad0fea5f102201015c9aae9d93bc02f75dd9491529a8c21f88d17a8523d44c - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "7.6.0" analyzer_plugin: @@ -22,7 +22,7 @@ packages: description: name: analyzer_plugin sha256: a5ab7590c27b779f3d4de67f31c4109dbe13dd7339f86461a6f2a8ab2594d8ce - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "0.13.4" android_id: @@ -30,7 +30,7 @@ packages: description: name: android_id sha256: "543bbfcf316de69d3ac36601d74eeaacd0248178a2671b00ad30d09f35bd3581" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "0.5.2+1" args: @@ -38,7 +38,7 @@ packages: description: name: args sha256: d0481093c50b1da8910eb0bb301626d4d8eb7284aa739614d2b394ee09e3ea04 - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "2.7.0" async: @@ -46,7 +46,7 @@ packages: description: name: async sha256: e2eb0491ba5ddb6177742d2da23904574082139b07c1e33b8503b9f46f3e1a37 - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "2.13.1" azlistview_plus: @@ -54,7 +54,7 @@ packages: description: name: azlistview_plus sha256: bbf08532db8ba2b9054100f68829e1841b23f3244e54712934f0f192651d8319 - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "3.0.0" boolean_selector: @@ -62,7 +62,7 @@ packages: description: name: boolean_selector sha256: "8aab1771e1243a5063b8b0ff68042d67334e3feab9e95b9490f9a6ebf73b42ea" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "2.1.2" build: @@ -70,7 +70,7 @@ packages: description: name: build sha256: "51dc711996cbf609b90cbe5b335bbce83143875a9d58e4b5c6d3c4f684d3dda7" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "2.5.4" build_config: @@ -78,7 +78,7 @@ packages: description: name: build_config sha256: "4ae2de3e1e67ea270081eaee972e1bd8f027d459f249e0f1186730784c2e7e33" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "1.1.2" build_daemon: @@ -86,7 +86,7 @@ packages: description: name: build_daemon sha256: "79e05eaf15a48d7230b053a4363b8eaac0cc234bbd0134c3229455481f55cbc6" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "4.1.5" build_resolvers: @@ -94,7 +94,7 @@ packages: description: name: build_resolvers sha256: ee4257b3f20c0c90e72ed2b57ad637f694ccba48839a821e87db762548c22a62 - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "2.5.4" build_runner: @@ -102,7 +102,7 @@ packages: description: name: build_runner sha256: "382a4d649addbfb7ba71a3631df0ec6a45d5ab9b098638144faf27f02778eb53" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "2.5.4" build_runner_core: @@ -110,7 +110,7 @@ packages: description: name: build_runner_core sha256: "85fbbb1036d576d966332a3f5ce83f2ce66a40bea1a94ad2d5fc29a19a0d3792" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "9.1.2" built_collection: @@ -118,7 +118,7 @@ packages: description: name: built_collection sha256: "376e3dd27b51ea877c28d525560790aee2e6fbb5f20e2f85d5081027d94e2100" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "5.1.1" built_value: @@ -126,7 +126,7 @@ packages: description: name: built_value sha256: "31b24be6615ec7fcf70b3aa5a7469fe35826485e639a16dd7eb83ba30e4cc6a8" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "8.12.7" characters: @@ -134,7 +134,7 @@ packages: description: name: characters sha256: faf38497bda5ead2a8c7615f4f7939df04333478bf32e4173fcb06d428b5716b - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "1.4.1" checked_yaml: @@ -142,7 +142,7 @@ packages: description: name: checked_yaml sha256: "959525d3162f249993882720d52b7e0c833978df229be20702b33d48d91de70f" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "2.0.4" clock: @@ -150,23 +150,23 @@ packages: description: name: clock sha256: fddb70d9b5277016c77a80201021d40a2247104d9f4aa7bab7157b7e3f05b84b - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "1.1.2" code_assets: dependency: transitive description: name: code_assets - sha256: bf394f466ba9205f1812a0433b392d6af280f155f56651eda7c18cc32ed493b8 - url: "https://pub.dev" + sha256: cfd4f5f575a49c5f10ca856e9846073f1e6c3ee94912377eea5f6cefc5272941 + url: "https://pub.flutter-io.cn" source: hosted - version: "1.2.1" + version: "2.0.0" code_builder: dependency: transitive description: name: code_builder sha256: "6a6cab2ba4680d6423f34a9b972a4c9a94ebe1b62ecec4e1a1f2cba91fd1319d" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "4.11.1" collection: @@ -174,7 +174,7 @@ packages: description: name: collection sha256: "2f5709ae4d3d59dd8f7cd309b4e023046b57d8a6c82130785d2b0e5868084e76" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "1.19.1" convert: @@ -182,23 +182,23 @@ packages: description: name: convert sha256: b30acd5944035672bc15c6b7a8b47d773e41e2f17de064350988c5d02adb1c68 - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "3.1.2" cross_file: dependency: transitive description: name: cross_file - sha256: "92c9c43c383bfa1c32079d3bc492d55d6d4318044b7b47edaff8971cbb555c51" - url: "https://pub.dev" + sha256: f141ea4f277af142a0356955707f6556f37b03947d39d55585981a06ca437bd6 + url: "https://pub.flutter-io.cn" source: hosted - version: "0.3.5+4" + version: "0.3.5+5" crypto: dependency: transitive description: name: crypto sha256: c8ea0233063ba03258fbcf2ca4d6dadfefe14f02fab57702265467a19f27fadf - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "3.0.7" cupertino_icons: @@ -206,23 +206,23 @@ packages: description: name: cupertino_icons sha256: "41e005c33bd814be4d3096aff55b1908d419fde52ca656c8c47719ec745873cd" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "1.0.9" cupertino_ui: dependency: "direct main" description: name: cupertino_ui - sha256: "7ed8ce4159d342eec4c65f4ea6eec57adaf9365404378541f38efc1da20a5b3d" - url: "https://pub.dev" + sha256: "2137c0d41f3b62cc4d3d2495e6c354bd6b6133cd21c6bb155736cc31dbd49a11" + url: "https://pub.flutter-io.cn" source: hosted - version: "1.0.0" + version: "1.0.1" custom_lint_core: dependency: transitive description: name: custom_lint_core sha256: "31110af3dde9d29fb10828ca33f1dce24d2798477b167675543ce3d208dee8be" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "0.7.5" custom_lint_visitor: @@ -230,7 +230,7 @@ packages: description: name: custom_lint_visitor sha256: "4a86a0d8415a91fbb8298d6ef03e9034dc8e323a599ddc4120a0e36c433983a2" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "1.0.0+7.7.0" dart_style: @@ -238,7 +238,7 @@ packages: description: name: dart_style sha256: "8a0e5fba27e8ee025d2ffb4ee820b4e6e2cf5e4246a6b1a477eb66866947e0bb" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "3.1.1" dart_webrtc: @@ -246,7 +246,7 @@ packages: description: name: dart_webrtc sha256: f6d615bddea5e458ce180a914f3055c234ffb52fb7397a51b3491e76d6d7edb2 - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "1.8.1" device_info_plus: @@ -254,7 +254,7 @@ packages: description: name: device_info_plus sha256: "0891702f96b2e465fe567b7ec448380e6b1c14f60af552a8536d9f583b6b8442" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "13.2.0" device_info_plus_platform_interface: @@ -262,7 +262,7 @@ packages: description: name: device_info_plus_platform_interface sha256: "04b173a92e2d9161dfead145667037c8d834db725ce2e7b942bfe18fd2f45a46" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "8.1.0" dio: @@ -270,7 +270,7 @@ packages: description: name: dio sha256: "0df44ebba85e503958eb75d07eedd3c86275a58c1d3eda2f2ce8f0a2c3abbb3c" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "5.11.0" dio_web_adapter: @@ -278,7 +278,7 @@ packages: description: name: dio_web_adapter sha256: "0786d0b7295a373de356fc0af4f6f1d0ab2844ed31b19dfc5e7556b70e24212c" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "2.2.1" fake_async: @@ -286,7 +286,7 @@ packages: description: name: fake_async sha256: "5368f224a74523e8d2e7399ea1638b37aecfca824a3cc4dfdf77bf1fa905ac44" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "1.3.3" ffi: @@ -294,7 +294,7 @@ packages: description: name: ffi sha256: "6d7fd89431262d8f3125e81b50d3847a091d846eafcd4fdb88dd06f36d705a45" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "2.2.0" ffi_leak_tracker: @@ -302,7 +302,7 @@ packages: description: name: ffi_leak_tracker sha256: "4093d4ef9ca06ffe2786e73bfb25e22aa92112b9bb4ec941f11e3e6b61489a97" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "0.1.2" file: @@ -310,7 +310,7 @@ packages: description: name: file sha256: a3b4f84adafef897088c160faf7dfffb7696046cb13ae90b508c2cbc95d3b8d4 - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "7.0.1" file_selector_linux: @@ -318,7 +318,7 @@ packages: description: name: file_selector_linux sha256: "2567f398e06ac72dcf2e98a0c95df2a9edd03c2c2e0cacd4780f20cdf56263a0" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "0.9.4" file_selector_macos: @@ -326,7 +326,7 @@ packages: description: name: file_selector_macos sha256: "5e0bbe9c312416f1787a68259ea1505b52f258c587f12920422671807c4d618a" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "0.9.5" file_selector_platform_interface: @@ -334,7 +334,7 @@ packages: description: name: file_selector_platform_interface sha256: "35e0bd61ebcdb91a3505813b055b09b79dfdc7d0aee9c09a7ba59ae4bb13dc85" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "2.7.0" file_selector_windows: @@ -342,7 +342,7 @@ packages: description: name: file_selector_windows sha256: "62197474ae75893a62df75939c777763d39c2bc5f73ce5b88497208bc269abfd" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "0.9.3+5" fixnum: @@ -350,7 +350,7 @@ packages: description: name: fixnum sha256: b6dc7065e46c974bc7c5f143080a6764ec7a4be6da1285ececdc37be96de53be - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "1.1.1" flutter: @@ -363,7 +363,7 @@ packages: description: name: flutter_baidu_mapapi_base sha256: bafde09cb9c623fede29a0abd5bea2367e2dd7227784670af64cec5d4d81366d - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "3.9.9" flutter_baidu_mapapi_map: @@ -371,7 +371,7 @@ packages: description: name: flutter_baidu_mapapi_map sha256: "670e48687e1ec6a09df48e55800dcf6c08193d977265a6a6da12b7a6471af956" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "3.9.9" flutter_bmflocation: @@ -379,15 +379,23 @@ packages: description: name: flutter_bmflocation sha256: c7af05b856f058603fd648bfe3786a6395f2ae5a6efaabaab8b3f9c96cdb8f3f - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "3.8.4+1" + flutter_displaymode: + dependency: "direct main" + description: + name: flutter_displaymode + sha256: ecd44b1e902b0073b42ff5b55bf283f38e088270724cdbb7f7065ccf54aa60a8 + url: "https://pub.flutter-io.cn" + source: hosted + version: "0.7.0" flutter_lints: dependency: "direct dev" description: name: flutter_lints sha256: "3105dc8492f6183fb076ccf1f351ac3d60564bff92e20bfc4af9cc1651f4e7e1" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "6.0.0" flutter_localizations: @@ -400,7 +408,7 @@ packages: description: name: flutter_plugin_android_lifecycle sha256: "3854fe5e3bff0b113c658f260b90c95dea17c92db0f2addeac2e343dd9969785" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "2.0.35" flutter_riverpod: @@ -408,7 +416,7 @@ packages: description: name: flutter_riverpod sha256: "9532ee6db4a943a1ed8383072a2e3eeda041db5657cdf6d2acecf3c21ecbe7e1" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "2.6.1" flutter_secure_storage: @@ -416,7 +424,7 @@ packages: description: name: flutter_secure_storage sha256: "15e8c8fe269fdf7d469b23008ab3df521c8b826ed345820532364c31bdebace6" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "11.0.0" flutter_secure_storage_darwin: @@ -424,7 +432,7 @@ packages: description: name: flutter_secure_storage_darwin sha256: ac6d76a752de0cd738334eb4b21743fc4943f449f5b6e308f18838b048c02ac0 - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "0.4.0" flutter_secure_storage_linux: @@ -432,7 +440,7 @@ packages: description: name: flutter_secure_storage_linux sha256: "76fa9c841b3b1619fc5b5bc36efc7d158fa2356f223b6caeb1d0c80a54168546" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "3.0.2" flutter_secure_storage_platform_interface: @@ -440,7 +448,7 @@ packages: description: name: flutter_secure_storage_platform_interface sha256: "788060052712555182aba55ecb5f8b6e5cb9cfe8f776c83249a61fe3ce877db4" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "2.0.3" flutter_secure_storage_web: @@ -448,7 +456,7 @@ packages: description: name: flutter_secure_storage_web sha256: "073a62b3aeb866ab4ce795f960413948e51e5a42a9b0c8333b6daf5bb3208a1c" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "2.1.1" flutter_secure_storage_windows: @@ -456,7 +464,7 @@ packages: description: name: flutter_secure_storage_windows sha256: "471951813a97006d899db4948acc654a4f28c440083ea08178935ce20b173ec1" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "4.2.2" flutter_test: @@ -474,7 +482,7 @@ packages: description: name: flutter_webrtc sha256: e997161d7da3adedd3d430691b20931b0b4d96fa48bb60938d9ba0bf6fca98be - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "1.6.0" freezed: @@ -482,7 +490,7 @@ packages: description: name: freezed sha256: "59a584c24b3acdc5250bb856d0d3e9c0b798ed14a4af1ddb7dc1c7b41df91c9c" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "2.5.8" freezed_annotation: @@ -490,7 +498,7 @@ packages: description: name: freezed_annotation sha256: c2e2d632dd9b8a2b7751117abcfc2b4888ecfe181bd9fca7170d9ef02e595fe2 - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "2.4.4" frontend_server_client: @@ -498,7 +506,7 @@ packages: description: name: frontend_server_client sha256: f64a0333a82f30b0cca061bc3d143813a486dc086b574bfb233b7c1372427694 - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "4.0.0" glob: @@ -506,7 +514,7 @@ packages: description: name: glob sha256: c3f1ee72c96f8f78935e18aa8cecced9ab132419e8625dc187e1c2408efc20de - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "2.1.3" go_router: @@ -514,7 +522,7 @@ packages: description: name: go_router sha256: f02fd7d2a4dc512fec615529824fdd217fecb3a3d3de68360293a551f21634b3 - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "14.8.1" graphs: @@ -522,23 +530,23 @@ packages: description: name: graphs sha256: "741bbf84165310a68ff28fe9e727332eef1407342fca52759cb21ad8177bb8d0" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "2.3.2" hooks: dependency: transitive description: name: hooks - sha256: "9a62a50b50b769a737bc0a8ff381f333529df3ab746b2f6b02e83760231455ba" - url: "https://pub.dev" + sha256: eaac480a35ec0814146c2c48d96aaa829e0e44a7662c88ae84c9edf4bc35651f + url: "https://pub.flutter-io.cn" source: hosted - version: "2.0.2" + version: "2.2.0" http: dependency: transitive description: name: http sha256: "87721a4a50b19c7f1d49001e51409bddc46303966ce89a65af4f4e6004896412" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "1.6.0" http_multi_server: @@ -546,7 +554,7 @@ packages: description: name: http_multi_server sha256: aa6199f908078bb1c5efb8d8638d4ae191aac11b311132c3ef48ce352fb52ef8 - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "3.2.2" http_parser: @@ -554,7 +562,7 @@ packages: description: name: http_parser sha256: "178d74305e7866013777bab2c3d8726205dc5a4dd935297175b19a23a2e66571" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "4.1.2" image_picker: @@ -562,7 +570,7 @@ packages: description: name: image_picker sha256: d8402284df184bc05f4a2210c6c23983b0720f4cd87cbd05c5390a78af602667 - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "1.2.3" image_picker_android: @@ -570,7 +578,7 @@ packages: description: name: image_picker_android sha256: "6f3a1995eafb000333174fae92202622033b0ee7fd917a6cd3730295264df84a" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "0.8.13+19" image_picker_for_web: @@ -578,7 +586,7 @@ packages: description: name: image_picker_for_web sha256: "66257a3191ab360d23a55c8241c91a6e329d31e94efa7be9cf7a212e65850214" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "3.1.1" image_picker_ios: @@ -586,7 +594,7 @@ packages: description: name: image_picker_ios sha256: b9c4a438a9ff4f60808c9cf0039b93a42bb6c2211ef6ebb647394b2b3fa84588 - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "0.8.13+6" image_picker_linux: @@ -594,7 +602,7 @@ packages: description: name: image_picker_linux sha256: "1f81c5f2046b9ab724f85523e4af65be1d47b038160a8c8deed909762c308ed4" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "0.2.2" image_picker_macos: @@ -602,7 +610,7 @@ packages: description: name: image_picker_macos sha256: "86f0f15a309de7e1a552c12df9ce5b59fe927e71385329355aec4776c6a8ec91" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "0.2.2+1" image_picker_platform_interface: @@ -610,7 +618,7 @@ packages: description: name: image_picker_platform_interface sha256: "567e056716333a1647c64bb6bd873cff7622233a5c3f694be28a583d4715690c" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "2.11.1" image_picker_windows: @@ -618,7 +626,7 @@ packages: description: name: image_picker_windows sha256: d248c86554a72b5495a31c56f060cf73a41c7ff541689327b1a7dbccc33adfae - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "0.2.2" intl: @@ -626,7 +634,7 @@ packages: description: name: intl sha256: "1ca20c894b1717686a2319b8548763d812bc0aabdac580420a44c5178c57a867" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "0.20.3" io: @@ -634,7 +642,7 @@ packages: description: name: io sha256: dfd5a80599cf0165756e3181807ed3e77daf6dd4137caaad72d0b7931597650b - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "1.0.5" jni: @@ -642,7 +650,7 @@ packages: description: name: jni sha256: f038e58b4dc2c9037f50e233175086337e0b305e356d28211bf55f21c504cbd3 - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "1.0.3" jni_flutter: @@ -650,7 +658,7 @@ packages: description: name: jni_flutter sha256: "7b717011ea40d04fd47c2731d3d1d36eb99eba3435c2753d62489e8c3c9991d5" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "1.0.2" jni_util: @@ -658,7 +666,7 @@ packages: description: name: jni_util sha256: "1ba86da04a5f2bf18fde2edb235587e70c5b0fc5bd4ba955f46b00942c3fc35f" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "1.0.0" js: @@ -666,7 +674,7 @@ packages: description: name: js sha256: "53385261521cc4a0c4658fd0ad07a7d14591cf8fc33abbceae306ddb974888dc" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "0.7.2" json_annotation: @@ -674,7 +682,7 @@ packages: description: name: json_annotation sha256: "1ce844379ca14835a50d2f019a3099f419082cfdd231cd86a142af94dd5c6bb1" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "4.9.0" json_serializable: @@ -682,7 +690,7 @@ packages: description: name: json_serializable sha256: c50ef5fc083d5b5e12eef489503ba3bf5ccc899e487d691584699b4bdefeea8c - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "6.9.5" leak_tracker: @@ -690,7 +698,7 @@ packages: description: name: leak_tracker sha256: "33e2e26bdd85a0112ec15400c8cbffea70d0f9c3407491f672a2fad47915e2de" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "11.0.2" leak_tracker_flutter_testing: @@ -698,7 +706,7 @@ packages: description: name: leak_tracker_flutter_testing sha256: "1dbc140bb5a23c75ea9c4811222756104fbcd1a27173f0c34ca01e16bea473c1" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "3.0.10" leak_tracker_testing: @@ -706,7 +714,7 @@ packages: description: name: leak_tracker_testing sha256: "8d5a2d49f4a66b49744b23b018848400d23e54caf9463f4eb20df3eb8acb2eb1" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "3.0.2" lints: @@ -714,7 +722,7 @@ packages: description: name: lints sha256: "12f842a479589fea194fe5c5a3095abc7be0c1f2ddfa9a0e76aed1dbd26a87df" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "6.1.0" logger: @@ -722,7 +730,7 @@ packages: description: name: logger sha256: "25aee487596a6257655a1e091ec2ae66bc30e7af663592cc3a27e6591e05035c" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "2.7.0" logging: @@ -730,7 +738,7 @@ packages: description: name: logging sha256: c8245ada5f1717ed44271ed1c26b8ce85ca3228fd2ffdb75468ab01979309d61 - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "1.3.0" lpinyin: @@ -738,7 +746,7 @@ packages: description: name: lpinyin sha256: "0bb843363f1f65170efd09fbdfc760c7ec34fc6354f9fcb2f89e74866a0d814a" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "2.0.3" matcher: @@ -746,7 +754,7 @@ packages: description: name: matcher sha256: "31bd099b47c10cd1aeb55146a2d46ce0277630ecef3f7dae54ad7873f36696cd" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "0.12.20" material_color_utilities: @@ -754,23 +762,23 @@ packages: description: name: material_color_utilities sha256: "9c337007e82b1889149c82ed242ed1cb24a66044e30979c44912381e9be4c48b" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "0.13.0" material_ui: dependency: "direct main" description: name: material_ui - sha256: d9b4f6c69b80bc83d0a14357c86e4c14c8076e807ae73cf2960c8560f623995f - url: "https://pub.dev" + sha256: "7ba1ca315d50a004791dbab290417c52a61466d56db2822fe3c5c2994903110c" + url: "https://pub.flutter-io.cn" source: hosted - version: "1.0.0" + version: "1.1.0" meta: dependency: transitive description: name: meta sha256: "307249ce4ff29d58a18e97f6345f539382eb9c9c29ecda628900f31de0443dd9" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "1.19.0" mime: @@ -778,87 +786,87 @@ packages: description: name: mime sha256: "41a20518f0cb1256669420fdba0cd90d21561e560ac240f26ef8322e45bb7ed6" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "2.0.0" mmkv: dependency: "direct main" description: name: mmkv - sha256: af1b7a0f1ebf6dd26c0bbcf8a214eb0ac4d456d6087363719d7bccfa7615b95b - url: "https://pub.dev" + sha256: "35ac6a8a82c478ad27da0a80f1a13b4294a43d44190e07cd90cfae5701da8cce" + url: "https://pub.flutter-io.cn" source: hosted - version: "2.4.1" + version: "2.4.2" mmkv_android: dependency: transitive description: name: mmkv_android - sha256: "0ba77fdfa74c42c06ada6dae7bef5634482e983cf41adecf79441a3d52b4ef12" - url: "https://pub.dev" + sha256: "57a834577adc2dcb9fb7f5b4336dc2320c0cdbe85c54f6a979f29a9b3ca61a27" + url: "https://pub.flutter-io.cn" source: hosted - version: "2.4.1" + version: "2.4.2" mmkv_ios: dependency: transitive description: name: mmkv_ios - sha256: "2db1adfcb54bdcbe53270ae558d6f328c98fc93431dc6951aafe37c2ec1b2cb1" - url: "https://pub.dev" + sha256: "0d5aa7c3fd181b31a13297ab90dc6daab97324c7fc1c0c2897490c05649da12b" + url: "https://pub.flutter-io.cn" source: hosted - version: "2.4.1" + version: "2.4.2" mmkv_linux: dependency: transitive description: name: mmkv_linux - sha256: ca193279250054089736ae4aba36b31ed615bdae1f85f2f7235f668299ccedca - url: "https://pub.dev" + sha256: b8dd1e6d36aa96d8bfb88bf68ebe16c420a37f3f6b2dc5223f81c151e4b27bd1 + url: "https://pub.flutter-io.cn" source: hosted - version: "2.4.1" + version: "2.4.2" mmkv_ohos: dependency: transitive description: name: mmkv_ohos - sha256: "6d53f04e556acd265fa8e0841b17696009c76c4ec3d6bef0d81e4cc16de2d683" - url: "https://pub.dev" + sha256: b9b561776ff38e029ead03dda0af716d02b3b6cd698f8de8c599cbe3c9b571cb + url: "https://pub.flutter-io.cn" source: hosted - version: "2.4.1" + version: "2.4.2" mmkv_platform_interface: dependency: transitive description: name: mmkv_platform_interface sha256: bef7422b14f84297fe637adc82b9fe018155a429fcbaef53efb06ecf005b0288 - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "2.4.0" mmkv_win32: dependency: transitive description: name: mmkv_win32 - sha256: a8f97c1d9c92073b0a92237093d54a6d4bf2203cd1420cb4bdacb8c70d62e340 - url: "https://pub.dev" + sha256: "544f708d6e8821eb4ba10c07a23a049d7d6155eb81f89edfedbda1b0f4647447" + url: "https://pub.flutter-io.cn" source: hosted - version: "2.4.1" + version: "2.4.2" mobile_scanner: dependency: "direct main" description: name: mobile_scanner sha256: d234581c090526676fd8fab4ada92f35c6746e3fb4f05a399665d75a399fb760 - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "5.2.3" objective_c: dependency: transitive description: name: objective_c - sha256: b7fb95a6d9a4f009edd63dc5ac69f07420b23a16161c6dd8660290b59c602e8e - url: "https://pub.dev" + sha256: ad56fd53a78ff6b1472fa59ff2a4e8b8ccabafc586fc263a1dfad0b99b5553e3 + url: "https://pub.flutter-io.cn" source: hosted - version: "9.5.0" + version: "9.6.0" package_config: dependency: transitive description: name: package_config sha256: f096c55ebb7deb7e384101542bfba8c52696c1b56fca2eb62827989ef2353bbc - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "2.2.0" path: @@ -866,7 +874,7 @@ packages: description: name: path sha256: "75cca69d1490965be98c73ceaea117e8a04dd21217b37b292c9ddbec0d955bc5" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "1.9.1" path_provider: @@ -874,7 +882,7 @@ packages: description: name: path_provider sha256: a7f4874f987173da295a61c181b8ee71dab59b332a486b391babf26a1b884825 - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "2.1.6" path_provider_android: @@ -882,7 +890,7 @@ packages: description: name: path_provider_android sha256: "69cbd515a62b94d32a7944f086b2f82b4ac40a1d45bebfc00813a430ab2dabcd" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "2.3.1" path_provider_foundation: @@ -890,7 +898,7 @@ packages: description: name: path_provider_foundation sha256: "2a376b7d6392d80cd3705782d2caa734ca4727776db0b6ec36ef3f1855197699" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "2.6.0" path_provider_linux: @@ -898,7 +906,7 @@ packages: description: name: path_provider_linux sha256: "58c2005f147315b11e9b4a7bc889cd5203e250cba8e3f012dae259b4972b5c16" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "2.2.2" path_provider_platform_interface: @@ -906,7 +914,7 @@ packages: description: name: path_provider_platform_interface sha256: "484838772624c3a4b94f1e44a3e19897fee738f2d5c4ce448443b0417f7c9dda" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "2.1.3" path_provider_windows: @@ -914,7 +922,7 @@ packages: description: name: path_provider_windows sha256: bd6f00dbd873bfb70d0761682da2b3a2c2fccc2b9e84c495821639601d81afe7 - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "2.3.0" permission_handler: @@ -922,7 +930,7 @@ packages: description: name: permission_handler sha256: fe54465bcc62a4564c6e4db337bbaded6c0c0fa6e10487414436d163114784f6 - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "12.0.3" permission_handler_android: @@ -930,7 +938,7 @@ packages: description: name: permission_handler_android sha256: "1e3bc410ca1bf84662104b100eb126e066cb55791b7451307f9708d4007350e6" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "13.0.1" permission_handler_apple: @@ -938,7 +946,7 @@ packages: description: name: permission_handler_apple sha256: f49cb15a064ea9d974fc7fbb302099353b7b170d07284e86e264561579e5bcf8 - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "9.6.1" permission_handler_html: @@ -946,7 +954,7 @@ packages: description: name: permission_handler_html sha256: "6ea98b3f17f60d3b527f2647ed2ab4dc0f6bfe25b22cb1c363f5d8f62252f6ac" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "0.1.4+1" permission_handler_platform_interface: @@ -954,7 +962,7 @@ packages: description: name: permission_handler_platform_interface sha256: a5c8a97ecf5616112a5b16d4b8e9ec0e5ae90ef63ac69c0d7b8ae240be760b23 - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "4.4.0" permission_handler_windows: @@ -962,7 +970,7 @@ packages: description: name: permission_handler_windows sha256: caeae01858a0a7d2df67a445ac98e1ad95e55a0e77c73044f4e9b1c8c2289cbd - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "0.2.2" platform: @@ -970,7 +978,7 @@ packages: description: name: platform sha256: "5d6b1b0036a5f331ebc77c850ebc8506cbc1e9416c27e59b439f917a902a4984" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "3.1.6" plugin_platform_interface: @@ -978,7 +986,7 @@ packages: description: name: plugin_platform_interface sha256: "4820fbfdb9478b1ebae27888254d445073732dae3d6ea81f0b7e06d5dedc3f02" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "2.1.8" pool: @@ -986,7 +994,7 @@ packages: description: name: pool sha256: "978783255c543aa3586a1b3c21f6e9d720eb315376a915872c61ef8b5c20177d" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "1.5.2" protobuf: @@ -994,7 +1002,7 @@ packages: description: name: protobuf sha256: "75ec242d22e950bdcc79ee38dd520ce4ee0bc491d7fadc4ea47694604d22bf06" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "6.0.0" pub_semver: @@ -1002,7 +1010,7 @@ packages: description: name: pub_semver sha256: "5bfcf68ca79ef689f8990d1160781b4bad40a3bd5e5218ad4076ddb7f4081585" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "2.2.0" pubspec_parse: @@ -1010,23 +1018,23 @@ packages: description: name: pubspec_parse sha256: "0560ba233314abbed0a48a2956f7f022cce7c3e1e73df540277da7544cad4082" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "1.5.0" record_use: dependency: transitive description: name: record_use - sha256: "2551bd8eecfe95d14ae75f6021ad0248be5c27f138c2ec12fcb52b500b3ba1ed" - url: "https://pub.dev" + sha256: "1cb8564af8d43b464294411db9217f5ec04891c6f22ee2c32d73ae05e88a6bd2" + url: "https://pub.flutter-io.cn" source: hosted - version: "0.6.0" + version: "1.1.1" riverpod: dependency: transitive description: name: riverpod sha256: "59062512288d3056b2321804332a13ffdd1bf16df70dcc8e506e411280a72959" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "2.6.1" riverpod_analyzer_utils: @@ -1034,7 +1042,7 @@ packages: description: name: riverpod_analyzer_utils sha256: "837a6dc33f490706c7f4632c516bcd10804ee4d9ccc8046124ca56388715fdf3" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "0.5.9" riverpod_annotation: @@ -1042,7 +1050,7 @@ packages: description: name: riverpod_annotation sha256: e14b0bf45b71326654e2705d462f21b958f987087be850afd60578fcd502d1b8 - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "2.6.1" riverpod_generator: @@ -1050,7 +1058,7 @@ packages: description: name: riverpod_generator sha256: "120d3310f687f43e7011bb213b90a436f1bbc300f0e4b251a72c39bccb017a4f" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "2.6.4" scrollable_positioned_list: @@ -1058,7 +1066,7 @@ packages: description: name: scrollable_positioned_list sha256: "1b54d5f1329a1e263269abc9e2543d90806131aa14fe7c6062a8054d57249287" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "0.3.8" shared_preferences: @@ -1066,7 +1074,7 @@ packages: description: name: shared_preferences sha256: c3025c5534b01739267eb7d76959bbc25a6d10f6988e1c2a3036940133dd10bf - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "2.5.5" shared_preferences_android: @@ -1074,7 +1082,7 @@ packages: description: name: shared_preferences_android sha256: "0634e64bd719f89c012f392938e173521f535d3ecaf66558fa94a056d22b5cc7" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "2.4.27" shared_preferences_foundation: @@ -1082,7 +1090,7 @@ packages: description: name: shared_preferences_foundation sha256: "4e7eaffc2b17ba398759f1151415869a34771ba11ebbccd1b0145472a619a64f" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "2.5.6" shared_preferences_linux: @@ -1090,7 +1098,7 @@ packages: description: name: shared_preferences_linux sha256: "580abfd40f415611503cae30adf626e6656dfb2f0cee8f465ece7b6defb40f2f" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "2.4.1" shared_preferences_platform_interface: @@ -1098,7 +1106,7 @@ packages: description: name: shared_preferences_platform_interface sha256: "649dc798a33931919ea356c4305c2d1f81619ea6e92244070b520187b5140ef9" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "2.4.2" shared_preferences_web: @@ -1106,7 +1114,7 @@ packages: description: name: shared_preferences_web sha256: c49bd060261c9a3f0ff445892695d6212ff603ef3115edbb448509d407600019 - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "2.4.3" shared_preferences_windows: @@ -1114,7 +1122,7 @@ packages: description: name: shared_preferences_windows sha256: "94ef0f72b2d71bc3e700e025db3710911bd51a71cefb65cc609dd0d9a982e3c1" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "2.4.1" shelf: @@ -1122,7 +1130,7 @@ packages: description: name: shelf sha256: e7dd780a7ffb623c57850b33f43309312fc863fb6aa3d276a754bb299839ef12 - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "1.4.2" shelf_web_socket: @@ -1130,7 +1138,7 @@ packages: description: name: shelf_web_socket sha256: "3632775c8e90d6c9712f883e633716432a27758216dfb61bd86a8321c0580925" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "3.0.0" sky_engine: @@ -1143,7 +1151,7 @@ packages: description: name: source_gen sha256: "35c8150ece9e8c8d263337a265153c3329667640850b9304861faea59fc98f6b" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "2.0.0" source_helper: @@ -1151,7 +1159,7 @@ packages: description: name: source_helper sha256: a447acb083d3a5ef17f983dd36201aeea33fedadb3228fa831f2f0c92f0f3aca - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "1.3.7" source_span: @@ -1159,7 +1167,7 @@ packages: description: name: source_span sha256: "56a02f1f4cd1a2d96303c0144c93bd6d909eea6bee6bf5a0e0b685edbd4c47ab" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "1.10.2" stack_trace: @@ -1167,7 +1175,7 @@ packages: description: name: stack_trace sha256: "8b27215b45d22309b5cddda1aa2b19bdfec9df0e765f2de506401c071d38d1b1" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "1.12.1" state_notifier: @@ -1175,7 +1183,7 @@ packages: description: name: state_notifier sha256: b8677376aa54f2d7c58280d5a007f9e8774f1968d1fb1c096adcb4792fba29bb - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "1.0.0" stream_channel: @@ -1183,7 +1191,7 @@ packages: description: name: stream_channel sha256: "969e04c80b8bcdf826f8f16579c7b14d780458bd97f56d107d3950fdbeef059d" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "2.1.4" stream_transform: @@ -1191,7 +1199,7 @@ packages: description: name: stream_transform sha256: ad47125e588cfd37a9a7f86c7d6356dde8dfe89d071d293f80ca9e9273a33871 - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "2.1.1" string_scanner: @@ -1199,7 +1207,7 @@ packages: description: name: string_scanner sha256: "921cd31725b72fe181906c6a94d987c78e3b98c2e205b397ea399d4054872b43" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "1.4.1" synchronized: @@ -1207,7 +1215,7 @@ packages: description: name: synchronized sha256: "3a7b5d17422dd0f8d5c6c14feaa5a1c65638b9455f871a96f08437562c046931" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "3.4.1+2" term_glyph: @@ -1215,7 +1223,7 @@ packages: description: name: term_glyph sha256: "7f554798625ea768a7518313e58f83891c7f5024f88e46e7182a4558850a4b8e" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "1.2.2" test_api: @@ -1223,7 +1231,7 @@ packages: description: name: test_api sha256: "2a122cbe059f8b610d3a5415f42e255b6c17b1f21eee1d960f31080237fb4f11" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "0.7.12" timing: @@ -1231,7 +1239,7 @@ packages: description: name: timing sha256: "62ee18aca144e4a9f29d212f5a4c6a053be252b895ab14b5821996cff4ed90fe" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "1.0.2" typed_data: @@ -1239,7 +1247,7 @@ packages: description: name: typed_data sha256: f9049c039ebfeb4cf7a7104a675823cd72dba8297f264b6637062516699fa006 - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "1.4.0" uuid: @@ -1247,7 +1255,7 @@ packages: description: name: uuid sha256: "9b129329f58692f6e6578329498a8fe9fbe98f090beb764ffbb8ee2eadd01dcd" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "4.6.0" vector_math: @@ -1255,23 +1263,23 @@ packages: description: name: vector_math sha256: f36f9f3be64c6198714492bb455c11056e33e2f85d9a0b676a48301e44fdcf47 - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "2.4.2" vm_service: dependency: transitive description: name: vm_service - sha256: "0016aef94fc66495ac78af5859181e3f3bf2026bd8eecc72b9565601e19ab360" - url: "https://pub.dev" + sha256: "5f37239c4851efcef929cea7824e76df7f2f0970aef85d66bbc430afa40e72f0" + url: "https://pub.flutter-io.cn" source: hosted - version: "15.2.0" + version: "15.3.0" watcher: dependency: transitive description: name: watcher sha256: "1398c9f081a753f9226febe8900fce8f7d0a67163334e1c94a2438339d79d635" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "1.2.1" web: @@ -1279,7 +1287,7 @@ packages: description: name: web sha256: "868d88a33d8a87b18ffc05f9f030ba328ffefba92d6c127917a2ba740f9cfe4a" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "1.1.1" web_socket: @@ -1287,7 +1295,7 @@ packages: description: name: web_socket sha256: "34d64019aa8e36bf9842ac014bb5d2f5586ca73df5e4d9bf5c936975cae6982c" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "1.0.1" web_socket_channel: @@ -1295,7 +1303,7 @@ packages: description: name: web_socket_channel sha256: d645757fb0f4773d602444000a8131ff5d48c9e47adfe9772652dd1a4f2d45c8 - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "3.0.3" webrtc_interface: @@ -1303,7 +1311,7 @@ packages: description: name: webrtc_interface sha256: c6f100eac5057d9a817a60473126f9828c796d42884d498af4f339c97b21014f - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "1.5.1" win32: @@ -1311,7 +1319,7 @@ packages: description: name: win32 sha256: a0b93865d5644f11cf6a8c3f6db909f1ec168958b5805f6cc684adea957cd63d - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "6.4.0" win32_registry: @@ -1319,7 +1327,7 @@ packages: description: name: win32_registry sha256: "73b1d78920a9d6e03f8b4e43e612b87bf3152a0e5c5e5150267762b7c4116904" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "3.0.3" xdg_directories: @@ -1327,7 +1335,7 @@ packages: description: name: xdg_directories sha256: "7a3f37b05d989967cdddcbb571f1ea834867ae2faa29725fd085180e0883aa15" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "1.1.0" yaml: @@ -1335,7 +1343,7 @@ packages: description: name: yaml sha256: b9da305ac7c39faa3f030eccd175340f968459dae4af175130b3fc47e40d76ce - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "3.1.3" sdks: diff --git a/pubspec.yaml b/pubspec.yaml index ed52a37..7c6c051 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -97,6 +97,7 @@ dependencies: material_ui: any intl: any lpinyin: ^2.0.3 + flutter_displaymode: ^0.7.0 dev_dependencies: flutter_test: sdk: flutter