import 'dart:io'; import 'package:desktop_drop/desktop_drop.dart'; import 'package:flutter/material.dart'; import '../../../../backend/api/commands.dart' as commands; import '../../../../backend/discovery/model.dart'; import '../../modals/send_files_modal.dart'; import '../../modals/send_text_modal.dart'; class PeerCard extends StatefulWidget { const PeerCard({super.key, required this.peer}); final Peer peer; @override State createState() => _PeerCardState(); } class _PeerCardState extends State { bool _dragging = false; String? get _targetIp { if (widget.peer.routes.isEmpty) { return null; } return widget.peer.routes.values.first.ip; } Future _openTextModal() async { final targetIp = _targetIp; if (targetIp == null || !mounted) { return; } await showDialog( context: context, builder: (_) => SendTextModal(peer: widget.peer, targetIp: targetIp), ); } Future _openFilesModal(List files) async { final targetIp = _targetIp; if (targetIp == null || !mounted) { return; } await showDialog( context: context, builder: (_) => SendFilesModal( peer: widget.peer, targetIp: targetIp, initialPaths: files, ), ); } bool get _enableDragDrop => Platform.isLinux || Platform.isWindows || Platform.isMacOS; @override Widget build(BuildContext context) { final theme = Theme.of(context); final targetIp = _targetIp; final card = Card( clipBehavior: Clip.antiAlias, shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(18), side: BorderSide( color: _dragging ? theme.colorScheme.primary : theme.colorScheme.outlineVariant, width: _dragging ? 2 : 1, ), ), child: Padding( padding: const EdgeInsets.all(14), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( children: [ CircleAvatar( backgroundColor: theme.colorScheme.primaryContainer, child: Icon( _iconForOs(widget.peer.os), color: theme.colorScheme.onPrimaryContainer, ), ), const SizedBox(width: 10), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( widget.peer.name, maxLines: 1, overflow: TextOverflow.ellipsis, style: theme.textTheme.titleMedium, ), Text( targetIp ?? '无路由', style: theme.textTheme.bodySmall?.copyWith( color: theme.colorScheme.onSurfaceVariant, ), ), ], ), ), ], ), const SizedBox(height: 10), Wrap( spacing: 8, runSpacing: 8, children: [ Chip( avatar: const Icon(Icons.shield_rounded, size: 16), label: Text(widget.peer.enableTls ? 'TLS on' : 'TLS off'), ), if (widget.peer.trustMismatch) const Chip( avatar: Icon(Icons.warning_amber_rounded, size: 16), label: Text('Trust Mismatch'), ), FutureBuilder( future: commands.isTrusted(peerId: widget.peer.id), builder: (context, snapshot) { final trusted = snapshot.data ?? false; return Chip( avatar: Icon( trusted ? Icons.verified_user_rounded : Icons.gpp_maybe_rounded, size: 16, ), label: Text(trusted ? 'Trusted' : 'Untrusted'), ); }, ), Chip( avatar: const Icon(Icons.hub_rounded, size: 16), label: Text('${widget.peer.routes.length} routes'), ), ], ), const Spacer(), if (_enableDragDrop && _dragging) Container( width: double.infinity, padding: const EdgeInsets.symmetric(vertical: 12), decoration: BoxDecoration( borderRadius: BorderRadius.circular(12), color: theme.colorScheme.primaryContainer.withValues( alpha: 0.55, ), ), child: const Center(child: Text('释放后发送文件')), ), Row( children: [ Expanded( child: FilledButton.tonalIcon( onPressed: targetIp == null ? null : _openTextModal, icon: const Icon(Icons.chat_bubble_rounded), label: const Text('文本'), ), ), const SizedBox(width: 8), Expanded( child: FilledButton.icon( onPressed: targetIp == null ? null : () => _openFilesModal(const []), icon: const Icon(Icons.upload_file_rounded), label: const Text('文件/文件夹'), ), ), ], ), const SizedBox(height: 8), FutureBuilder( future: commands.isTrusted(peerId: widget.peer.id), builder: (context, snapshot) { final trusted = snapshot.data ?? false; return SizedBox( width: double.infinity, child: OutlinedButton.icon( onPressed: () async { if (trusted) { await commands.untrustPeer(peerId: widget.peer.id); } else { await commands.trustPeer(peerId: widget.peer.id); } if (mounted) { setState(() {}); } }, icon: Icon( trusted ? Icons.verified_user_rounded : Icons.gpp_maybe_rounded, ), label: Text(trusted ? '取消信任' : '设为信任设备'), ), ); }, ), if (targetIp == null) Padding( padding: const EdgeInsets.only(top: 8), child: Text( '该设备暂无可用传输路由', style: theme.textTheme.bodySmall?.copyWith( color: theme.colorScheme.error, ), ), ), ], ), ), ); if (!_enableDragDrop) { return card; } return DropTarget( onDragEntered: (_) => setState(() => _dragging = true), onDragExited: (_) => setState(() => _dragging = false), onDragDone: (details) { final files = details.files .map((f) => f.path) .where((e) => e.isNotEmpty) .toList(); setState(() => _dragging = false); if (files.isNotEmpty) { _openFilesModal(files); } }, child: card, ); } IconData _iconForOs(String os) { final normalized = os.toLowerCase(); if (normalized.contains('windows')) { return Icons.laptop_windows_rounded; } if (normalized.contains('mac') || normalized.contains('darwin')) { return Icons.laptop_mac_rounded; } if (normalized.contains('linux')) { return Icons.computer_rounded; } if (normalized.contains('android')) { return Icons.phone_android_rounded; } if (normalized.contains('ios') || normalized.contains('iphone')) { return Icons.phone_iphone_rounded; } return Platform.isAndroid ? Icons.devices_rounded : Icons.device_hub_rounded; } }