import 'dart:async'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:shared_preferences/shared_preferences.dart'; void main() { runApp(const WirelessTextSyncerApp()); } class WirelessTextSyncerApp extends StatelessWidget { const WirelessTextSyncerApp({super.key}); @override Widget build(BuildContext context) { return MaterialApp( debugShowCheckedModeBanner: false, title: 'WirelessTextSyncer', theme: ThemeData( colorScheme: ColorScheme.fromSeed(seedColor: const Color(0xFF2563EB)), scaffoldBackgroundColor: const Color(0xFFF8FAFC), useMaterial3: true, ), home: const InputSyncPage(), ); } } enum SendButtonState { idle, sending, success } const connectionInfoFontFamily = 'WDXL Lubrifont SC'; class ConnectionSnapshot { const ConnectionSnapshot({ this.connected = false, this.connecting = false, this.host = '', this.port = 8181, this.name = '', this.lastError, }); factory ConnectionSnapshot.fromMap(Map? map) { if (map == null) { return const ConnectionSnapshot(); } return ConnectionSnapshot( connected: map['connected'] == true, connecting: map['connecting'] == true, host: (map['host'] as String?) ?? '', port: (map['port'] as num?)?.toInt() ?? 8181, name: (map['name'] as String?) ?? '', lastError: map['lastError'] as String?, ); } final bool connected; final bool connecting; final String host; final int port; final String name; final String? lastError; } class DiscoveredDevice { const DiscoveredDevice({ required this.name, required this.host, required this.port, }); factory DiscoveredDevice.fromMap(Map map) { return DiscoveredDevice( name: (map['name'] as String?) ?? 'Windows 桌面端', host: (map['host'] as String?) ?? '', port: (map['port'] as num?)?.toInt() ?? 8181, ); } final String name; final String host; final int port; String get endpoint => '$host:$port'; } class NativeConnectionApi { static const MethodChannel _methodChannel = MethodChannel( 'wireless_text_syncer/connection', ); static const EventChannel _eventChannel = EventChannel( 'wireless_text_syncer/connection_state', ); Stream watchState() { return _eventChannel.receiveBroadcastStream().map((event) { return ConnectionSnapshot.fromMap(event as Map?); }); } Future getState() async { try { final state = await _methodChannel.invokeMapMethod( 'getState', ); return ConnectionSnapshot.fromMap(state); } on MissingPluginException { return const ConnectionSnapshot(); } } Future connect(String host, int port, {String? name}) async { await _methodChannel.invokeMethod('connect', { 'host': host, 'port': port, if (name != null) 'name': name, }); } Future disconnect() async { await _methodChannel.invokeMethod('disconnect'); } Future sendText(String text) async { final result = await _methodChannel.invokeMethod('sendText', { 'text': text, }); return result ?? false; } Future sendEnter() async { final result = await _methodChannel.invokeMethod('sendEnter'); return result ?? false; } Future> startDiscovery() async { try { final result = await _methodChannel.invokeListMethod( 'startDiscovery', ); return (result ?? const []) .whereType>() .map(DiscoveredDevice.fromMap) .where((device) => device.host.isNotEmpty) .toList(); } on MissingPluginException { return const []; } } } class InputSyncPage extends StatefulWidget { const InputSyncPage({super.key}); @override State createState() => _InputSyncPageState(); } class _InputSyncPageState extends State with TickerProviderStateMixin, WidgetsBindingObserver { static const _hostKey = 'server_host'; static const _portKey = 'server_port'; static const _historyKey = 'send_history'; static const _clearAfterSendKey = 'clear_after_send'; static const _appendEnterKey = 'append_enter'; static const _maxHistoryItems = 10; final api = NativeConnectionApi(); final hostController = TextEditingController(); final portController = TextEditingController(text: '8181'); final inputController = TextEditingController(); final inputFocusNode = FocusNode(); late final AnimationController statusPulseController; late final AnimationController radarPulseController; StreamSubscription? stateSubscription; List sendHistory = []; List discoveredDevices = []; SendButtonState sendButtonState = SendButtonState.idle; ConnectionSnapshot connection = const ConnectionSnapshot(); bool appendEnter = false; bool clearAfterSend = false; bool headerExpanded = true; bool inputFocused = false; bool scanning = false; @override void initState() { super.initState(); WidgetsBinding.instance.addObserver(this); statusPulseController = AnimationController( vsync: this, duration: const Duration(milliseconds: 1300), lowerBound: 0.55, upperBound: 1, )..repeat(reverse: true); radarPulseController = AnimationController( vsync: this, duration: const Duration(milliseconds: 1500), )..repeat(); inputController.addListener(_handleDraftChanged); inputFocusNode.addListener(_handleFocusChanged); _loadSavedState(); _subscribeConnectionState(); } @override void dispose() { WidgetsBinding.instance.removeObserver(this); stateSubscription?.cancel(); statusPulseController.dispose(); radarPulseController.dispose(); hostController.dispose(); portController.dispose(); inputController.dispose(); inputFocusNode.dispose(); super.dispose(); } @override void didChangeAppLifecycleState(AppLifecycleState state) { if (state == AppLifecycleState.resumed) { unawaited(_syncAppendEnterFromPrefs()); } } Future _loadSavedState() async { final prefs = await SharedPreferences.getInstance(); await prefs.reload(); final nativeState = await api.getState(); if (!mounted) { return; } setState(() { hostController.text = prefs.getString(_hostKey) ?? nativeState.host; portController.text = prefs.getString(_portKey) ?? nativeState.port.toString(); clearAfterSend = prefs.getBool(_clearAfterSendKey) ?? false; appendEnter = prefs.getBool(_appendEnterKey) ?? false; sendHistory = prefs.getStringList(_historyKey) ?? []; connection = nativeState; headerExpanded = !nativeState.connected; }); if (!nativeState.connected) { unawaited(_scanForDevices()); } } void _subscribeConnectionState() { stateSubscription = api.watchState().listen((nextState) { if (!mounted) { return; } setState(() { connection = nextState; if (nextState.connected) { headerExpanded = false; hostController.text = nextState.host; portController.text = nextState.port.toString(); } else if (!nextState.connecting) { headerExpanded = true; } if (!nextState.connected) { sendButtonState = SendButtonState.idle; } }); if (nextState.connected) { inputFocusNode.requestFocus(); } else if (nextState.lastError != null) { _showToast(nextState.lastError!); } }, onError: (_) {}); } void _handleDraftChanged() { setState(() {}); } void _handleFocusChanged() { setState(() { inputFocused = inputFocusNode.hasFocus; }); } Future _scanForDevices() async { if (scanning) { return; } setState(() { scanning = true; }); final devices = await api.startDiscovery(); if (!mounted) { return; } setState(() { scanning = false; discoveredDevices = devices; }); if (devices.length == 1 && !connection.connected && !connection.connecting) { await _connectDevice(devices.single); } } Future _connect({String? name}) async { final host = hostController.text.trim(); final portText = portController.text.trim(); final port = int.tryParse(portText); if (host.isEmpty || port == null) { _showToast('请先填写 Windows IP 和端口'); setState(() { headerExpanded = true; }); return; } final prefs = await SharedPreferences.getInstance(); await prefs.setString(_hostKey, host); await prefs.setString(_portKey, port.toString()); setState(() { connection = ConnectionSnapshot( connecting: true, host: host, port: port, name: name ?? '', ); }); try { await api.connect(host, port, name: name); } on PlatformException catch (exception) { _showToast(exception.message ?? '连接失败,请检查地址'); setState(() { connection = ConnectionSnapshot( host: host, port: port, name: name ?? '', ); headerExpanded = true; }); } } Future _connectDevice(DiscoveredDevice device) async { hostController.text = device.host; portController.text = device.port.toString(); await _connect(name: device.name); } Future _disconnect() async { await api.disconnect(); setState(() { connection = ConnectionSnapshot( host: hostController.text.trim(), port: int.tryParse(portController.text.trim()) ?? 8181, name: connection.name, ); headerExpanded = true; sendButtonState = SendButtonState.idle; }); _showToast('已断开连接'); } Future _sendCurrentText() async { if (!_canSend) { return; } final draft = inputController.text; setState(() { sendButtonState = SendButtonState.sending; }); try { final sent = await api.sendText(draft); final enterSent = !sent || !appendEnter ? true : await api.sendEnter(); if (!sent || !enterSent) { _showToast('发送失败,连接已断开'); setState(() { sendButtonState = SendButtonState.idle; headerExpanded = true; }); return; } await _saveHistory(draft); if (clearAfterSend) { inputController.clear(); } if (!mounted) { return; } setState(() { sendButtonState = SendButtonState.success; }); await Future.delayed(const Duration(milliseconds: 1500)); if (mounted) { setState(() { sendButtonState = SendButtonState.idle; }); inputFocusNode.requestFocus(); } } on PlatformException { _showToast('发送失败,连接已断开'); setState(() { sendButtonState = SendButtonState.idle; headerExpanded = true; }); } } Future _saveHistory(String text) async { final trimmed = text.trim(); if (trimmed.isEmpty) { return; } final nextHistory = [ text, ...sendHistory.where((item) => item != text), ].take(_maxHistoryItems).toList(); final prefs = await SharedPreferences.getInstance(); await prefs.setStringList(_historyKey, nextHistory); if (mounted) { setState(() { sendHistory = nextHistory; }); } } Future _setClearAfterSend(bool value) async { final prefs = await SharedPreferences.getInstance(); await prefs.setBool(_clearAfterSendKey, value); setState(() { clearAfterSend = value; }); } Future _setAppendEnter(bool value) async { final prefs = await SharedPreferences.getInstance(); await prefs.setBool(_appendEnterKey, value); setState(() { appendEnter = value; }); } Future _syncAppendEnterFromPrefs() async { final prefs = await SharedPreferences.getInstance(); await prefs.reload(); final nextAppendEnter = prefs.getBool(_appendEnterKey) ?? false; if (!mounted || nextAppendEnter == appendEnter) { return; } setState(() { appendEnter = nextAppendEnter; }); } void _restoreHistory(String text) { inputController.text = text; inputController.selection = TextSelection.collapsed(offset: text.length); Navigator.of(context).pop(); _showToast('已从历史记录恢复'); inputFocusNode.requestFocus(); } void _showHistorySheet() { showModalBottomSheet( context: context, showDragHandle: true, isScrollControlled: true, backgroundColor: Theme.of(context).colorScheme.surface, builder: (context) { return SafeArea( child: SizedBox( height: MediaQuery.of(context).size.height * 0.58, child: Column( children: [ Padding( padding: const EdgeInsets.fromLTRB(20, 0, 12, 10), child: Row( children: [ const Expanded( child: Text( '发送历史', style: TextStyle( fontSize: 18, fontWeight: FontWeight.w700, ), ), ), IconButton( tooltip: '关闭', onPressed: () => Navigator.of(context).pop(), icon: const Icon(Icons.close), ), ], ), ), Expanded( child: sendHistory.isEmpty ? const _HistoryEmptyState() : ListView.separated( padding: const EdgeInsets.fromLTRB(16, 0, 16, 20), itemBuilder: (context, index) { final item = sendHistory[index]; return ListTile( minVerticalPadding: 12, shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(8), ), tileColor: const Color(0xFFF8FAFC), title: Text( item, maxLines: 3, overflow: TextOverflow.ellipsis, ), subtitle: Text('${item.length} 字符'), onTap: () => _restoreHistory(item), ); }, separatorBuilder: (_, _) => const SizedBox(height: 8), itemCount: sendHistory.length, ), ), ], ), ), ); }, ); } void _showToast(String message) { if (!mounted) { return; } ScaffoldMessenger.of(context) ..hideCurrentSnackBar() ..showSnackBar( SnackBar( behavior: SnackBarBehavior.floating, content: Text(message), duration: const Duration(milliseconds: 1600), ), ); } bool get _canSend { return connection.connected && inputController.text.isNotEmpty && sendButtonState != SendButtonState.sending; } @override Widget build(BuildContext context) { return Scaffold( resizeToAvoidBottomInset: true, body: SafeArea( child: Column( children: [ _buildHeader(context), Expanded(child: _buildDraftBoard(context)), _buildFooter(context), ], ), ), ); } Widget _buildHeader(BuildContext context) { final colorScheme = Theme.of(context).colorScheme; return Material( color: colorScheme.surface, elevation: 1, child: AnimatedSize( duration: const Duration(milliseconds: 260), curve: Curves.easeOutCubic, child: Padding( padding: const EdgeInsets.fromLTRB(12, 8, 12, 10), child: Column( children: [ InkWell( borderRadius: BorderRadius.circular(8), onTap: () { setState(() { headerExpanded = !headerExpanded; }); }, child: SizedBox( height: 52, child: Row( children: [ Expanded( child: _ConnectionSummary( connected: connection.connected, connecting: connection.connecting, endpoint: connection.host.isEmpty ? '未设置地址' : '${connection.host}:${connection.port}', deviceName: connection.name.isEmpty ? 'Windows 桌面端' : connection.name, animation: statusPulseController, ), ), const SizedBox(width: 8), Icon( headerExpanded ? Icons.keyboard_arrow_up : Icons.keyboard_arrow_down, ), ], ), ), ), AnimatedCrossFade( firstChild: const SizedBox.shrink(), secondChild: Padding( padding: const EdgeInsets.only(top: 8), child: Column( children: [ Row( children: [ Expanded( flex: 7, child: TextField( controller: hostController, decoration: const InputDecoration( labelText: 'IP 地址', border: OutlineInputBorder(), isDense: true, ), keyboardType: TextInputType.url, ), ), const SizedBox(width: 8), Expanded( flex: 3, child: TextField( controller: portController, decoration: const InputDecoration( labelText: '端口', border: OutlineInputBorder(), isDense: true, ), keyboardType: TextInputType.number, ), ), const SizedBox(width: 8), IconButton.filledTonal( tooltip: connection.connected ? '断开连接' : '连接', style: IconButton.styleFrom( minimumSize: const Size(48, 48), foregroundColor: connection.connected ? colorScheme.error : colorScheme.primary, ), onPressed: connection.connecting ? null : connection.connected ? _disconnect : _connect, icon: Icon( connection.connected ? Icons.link_off : Icons.link, ), ), ], ), const SizedBox(height: 12), _buildRadar(context), ], ), ), crossFadeState: headerExpanded ? CrossFadeState.showSecond : CrossFadeState.showFirst, duration: const Duration(milliseconds: 260), sizeCurve: Curves.easeOutCubic, ), ], ), ), ), ); } Widget _buildRadar(BuildContext context) { final colorScheme = Theme.of(context).colorScheme; return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( children: [ AnimatedBuilder( animation: radarPulseController, builder: (context, child) { final scale = scanning ? 0.85 + radarPulseController.value * 0.3 : 1.0; return Transform.scale( scale: scale, child: Icon( Icons.radar, color: scanning ? colorScheme.primary : colorScheme.onSurfaceVariant, ), ); }, ), const SizedBox(width: 8), Expanded( child: Text( '局域网设备雷达', style: TextStyle( color: colorScheme.onSurfaceVariant, fontWeight: FontWeight.w700, ), ), ), TextButton.icon( onPressed: scanning ? null : _scanForDevices, icon: scanning ? const SizedBox( width: 16, height: 16, child: CircularProgressIndicator(strokeWidth: 2), ) : const Icon(Icons.refresh), label: Text(scanning ? '扫描中' : '刷新'), ), ], ), const SizedBox(height: 8), if (discoveredDevices.isEmpty) Container( width: double.infinity, padding: const EdgeInsets.all(12), decoration: BoxDecoration( color: const Color(0xFFF8FAFC), borderRadius: BorderRadius.circular(8), border: Border.all(color: const Color(0xFFE2E8F0)), ), child: Text( scanning ? '正在寻找附近的 Windows 桌面端...' : '暂无发现,可刷新或使用上方 IP 手动连接', style: TextStyle(color: colorScheme.onSurfaceVariant), ), ) else ...discoveredDevices.map((device) { return Padding( padding: const EdgeInsets.only(bottom: 8), child: InkWell( borderRadius: BorderRadius.circular(8), onTap: connection.connecting ? null : () => _connectDevice(device), child: Container( padding: const EdgeInsets.all(12), decoration: BoxDecoration( color: colorScheme.surface, borderRadius: BorderRadius.circular(8), border: Border.all(color: const Color(0xFFE2E8F0)), ), child: Row( children: [ const Icon(Icons.desktop_windows_outlined), const SizedBox(width: 10), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( device.name, maxLines: 1, overflow: TextOverflow.ellipsis, style: const TextStyle( fontWeight: FontWeight.w700, ), ), Text( device.endpoint, style: TextStyle( color: colorScheme.onSurfaceVariant, fontSize: 12, ), ), ], ), ), Container( padding: const EdgeInsets.symmetric( horizontal: 8, vertical: 4, ), decoration: BoxDecoration( color: const Color(0xFFDCFCE7), borderRadius: BorderRadius.circular(999), ), child: const Text( '可连接', style: TextStyle( color: Color(0xFF166534), fontSize: 12, fontWeight: FontWeight.w700, ), ), ), ], ), ), ), ); }), ], ); } Widget _buildDraftBoard(BuildContext context) { final colorScheme = Theme.of(context).colorScheme; return LayoutBuilder( builder: (context, constraints) { if (constraints.maxHeight < 72) { return const SizedBox.shrink(); } return Padding( padding: const EdgeInsets.fromLTRB(12, 12, 12, 8), child: Column( children: [ SizedBox( height: 44, child: Row( children: [ Text( '草稿板', style: TextStyle( color: colorScheme.onSurfaceVariant, fontWeight: FontWeight.w600, ), ), const Spacer(), IconButton( tooltip: '历史记录', onPressed: _showHistorySheet, icon: const Icon(Icons.history), ), AnimatedSwitcher( duration: const Duration(milliseconds: 180), child: inputController.text.isEmpty ? const SizedBox(width: 48, height: 48) : IconButton( key: const ValueKey('clear-draft'), tooltip: '清空', onPressed: inputController.clear, icon: const Icon(Icons.delete_outline), ), ), ], ), ), Expanded( child: AnimatedContainer( duration: const Duration(milliseconds: 180), curve: Curves.easeOut, decoration: BoxDecoration( color: colorScheme.surface, borderRadius: BorderRadius.circular(8), border: Border.all( color: inputFocused ? colorScheme.primary : const Color(0xFFE2E8F0), width: inputFocused ? 2 : 1, ), ), child: Stack( children: [ TextField( controller: inputController, focusNode: inputFocusNode, autofocus: true, expands: true, maxLines: null, minLines: null, textAlignVertical: TextAlignVertical.top, decoration: const InputDecoration( border: InputBorder.none, contentPadding: EdgeInsets.fromLTRB(16, 16, 16, 42), hintText: '点击输入或语音录入... 发送后草稿会保留,方便随时修改', ), keyboardType: TextInputType.multiline, ), Positioned( right: 14, bottom: 10, child: Text( '${inputController.text.length} 字符', style: TextStyle( color: colorScheme.onSurfaceVariant.withValues( alpha: 0.72, ), fontSize: 12, ), ), ), ], ), ), ), ], ), ); }, ); } Widget _buildFooter(BuildContext context) { final colorScheme = Theme.of(context).colorScheme; return Material( color: colorScheme.surface, elevation: 4, child: Padding( padding: const EdgeInsets.fromLTRB(12, 10, 12, 12), child: Column( mainAxisSize: MainAxisSize.min, children: [ Row( children: [ Expanded( child: _FooterToggle( label: '发送后清空', value: clearAfterSend, onChanged: _setClearAfterSend, ), ), const SizedBox(width: 8), Expanded( child: _FooterToggle( label: '追加回车', value: appendEnter, onChanged: _setAppendEnter, ), ), ], ), const SizedBox(height: 10), SizedBox( width: double.infinity, height: 52, child: FilledButton.icon( onPressed: _canSend ? _sendCurrentText : null, style: FilledButton.styleFrom( backgroundColor: sendButtonState == SendButtonState.success ? const Color(0xFF16A34A) : null, disabledBackgroundColor: const Color(0xFFE2E8F0), disabledForegroundColor: const Color(0xFF64748B), ), icon: _buildSendButtonIcon(), label: Text( _sendButtonText, style: const TextStyle(fontWeight: FontWeight.w700), ), ), ), ], ), ), ); } Widget _buildSendButtonIcon() { return switch (sendButtonState) { SendButtonState.sending => const SizedBox( width: 18, height: 18, child: CircularProgressIndicator(strokeWidth: 2), ), SendButtonState.success => const Icon(Icons.check), SendButtonState.idle => const Icon(Icons.send), }; } String get _sendButtonText { return switch (sendButtonState) { SendButtonState.sending => '发送中...', SendButtonState.success => '发送成功', SendButtonState.idle => '发送到电脑', }; } } class _ConnectionSummary extends StatelessWidget { const _ConnectionSummary({ required this.connected, required this.connecting, required this.endpoint, required this.deviceName, required this.animation, }); final bool connected; final bool connecting; final String endpoint; final String deviceName; final Animation animation; @override Widget build(BuildContext context) { final colorScheme = Theme.of(context).colorScheme; final iconColor = connected ? const Color(0xFF16A34A) : connecting ? colorScheme.primary : colorScheme.error; final primaryTextColor = connected ? const Color(0xFF166534) : const Color(0xFF0F172A); final secondaryTextColor = connected ? const Color(0xFF15803D) : colorScheme.onSurfaceVariant; return Row( children: [ AnimatedBuilder( animation: animation, builder: (context, child) { return Transform.scale( scale: connecting ? animation.value : 1, child: Container( width: 42, height: 42, alignment: Alignment.center, decoration: BoxDecoration( color: iconColor.withValues(alpha: 0.12), borderRadius: BorderRadius.circular(8), ), child: Icon( connected ? Icons.sync : connecting ? Icons.sync : Icons.sync_disabled, color: iconColor, size: 24, ), ), ); }, ), const SizedBox(width: 10), Expanded( child: SizedBox( height: 64, child: Column( mainAxisAlignment: MainAxisAlignment.center, crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( endpoint, maxLines: 1, overflow: TextOverflow.ellipsis, style: TextStyle( color: primaryTextColor, fontFamily: connectionInfoFontFamily, fontSize: 21, fontWeight: FontWeight.w700, height: 1.15, ), ), const SizedBox(height: 3), Text( deviceName, maxLines: 1, overflow: TextOverflow.ellipsis, style: TextStyle( color: secondaryTextColor, fontFamily: connectionInfoFontFamily, fontSize: 18, height: 1.15, ), ), ], ), ), ), ], ); } } class _ConnectionIndicator extends StatelessWidget { const _ConnectionIndicator({ required this.connected, required this.connecting, required this.animation, }); final bool connected; final bool connecting; final Animation animation; @override Widget build(BuildContext context) { final color = connected ? const Color(0xFF16A34A) : connecting ? Theme.of(context).colorScheme.primary : const Color(0xFFDC2626); return AnimatedBuilder( animation: animation, builder: (context, child) { return _StatusDot( color: color, scale: connected || connecting ? animation.value : 1, ); }, ); } } class _StatusDot extends StatelessWidget { const _StatusDot({required this.color, required this.scale}); final Color color; final double scale; @override Widget build(BuildContext context) { return Transform.scale( scale: scale, child: Container( width: 12, height: 12, decoration: BoxDecoration( color: color, shape: BoxShape.circle, boxShadow: [ BoxShadow( color: color.withValues(alpha: 0.24), blurRadius: 8, spreadRadius: 2, ), ], ), ), ); } } class _FooterToggle extends StatelessWidget { const _FooterToggle({ required this.label, required this.value, required this.onChanged, }); final String label; final bool value; final ValueChanged onChanged; @override Widget build(BuildContext context) { return InkWell( borderRadius: BorderRadius.circular(8), onTap: () => onChanged(!value), child: Container( constraints: const BoxConstraints(minHeight: 44), padding: const EdgeInsets.only(left: 10), decoration: BoxDecoration( borderRadius: BorderRadius.circular(8), border: Border.all(color: const Color(0xFFE2E8F0)), ), child: Row( children: [ Expanded( child: Text( label, overflow: TextOverflow.ellipsis, style: const TextStyle(fontWeight: FontWeight.w600), ), ), Switch.adaptive(value: value, onChanged: onChanged), ], ), ), ); } } class _HistoryEmptyState extends StatelessWidget { const _HistoryEmptyState(); @override Widget build(BuildContext context) { final colorScheme = Theme.of(context).colorScheme; return Center( child: Column( mainAxisSize: MainAxisSize.min, children: [ Icon( Icons.history_toggle_off, size: 56, color: colorScheme.onSurfaceVariant.withValues(alpha: 0.45), ), const SizedBox(height: 12), Text('暂无发送记录', style: TextStyle(color: colorScheme.onSurfaceVariant)), ], ), ); } }