import 'dart:convert'; import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; import '../providers/device_provider.dart'; import '../theme/app_colors.dart'; class SettingsScreen extends StatefulWidget { const SettingsScreen({super.key}); @override State createState() => _SettingsScreenState(); } class _SettingsScreenState extends State { final TextEditingController _ipController = TextEditingController(); final TextEditingController _titleController = TextEditingController(); final TextEditingController _rotationController = TextEditingController(); final TextEditingController _widthController = TextEditingController(); final TextEditingController _heightController = TextEditingController(); final TextEditingController _hsvMinController = TextEditingController(); final TextEditingController _hsvMaxController = TextEditingController(); final TextEditingController _pointsController = TextEditingController(); Map? _fullConfig; List _availableConfigs = const []; String? _activeConfig; bool _isFullscreen = false; bool _loading = true; @override void initState() { super.initState(); WidgetsBinding.instance.addPostFrameCallback((_) => _loadData()); } @override void dispose() { _ipController.dispose(); _titleController.dispose(); _rotationController.dispose(); _widthController.dispose(); _heightController.dispose(); _hsvMinController.dispose(); _hsvMaxController.dispose(); _pointsController.dispose(); super.dispose(); } @override Widget build(BuildContext context) { final provider = context.watch(); final status = provider.status; _ipController.text = _ipController.text.isEmpty ? provider.deviceIp : _ipController.text; return Scaffold( appBar: AppBar(title: const Text('设置')), body: _loading ? const Center(child: CircularProgressIndicator()) : ListView( padding: const EdgeInsets.all(AppSpacing.md), children: [ Card( child: Padding( padding: const EdgeInsets.all(AppSpacing.md), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text('设备 IP 配置', style: Theme.of(context).textTheme.titleMedium), const SizedBox(height: AppSpacing.md), TextField( controller: _ipController, decoration: const InputDecoration(labelText: '设备 IP 地址'), ), const SizedBox(height: AppSpacing.md), SizedBox( width: double.infinity, child: FilledButton( onPressed: () async { await context.read().updateDeviceIp( _ipController.text.trim(), ); if (!mounted) { return; } await _loadData(); }, child: const Text('保存并重连'), ), ), ], ), ), ), const SizedBox(height: AppSpacing.lg), Card( child: Padding( padding: const EdgeInsets.all(AppSpacing.md), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text('可用配置文件', style: Theme.of(context).textTheme.titleMedium), const SizedBox(height: AppSpacing.md), DropdownButtonFormField( value: _activeConfig, items: _availableConfigs .map( (item) => DropdownMenuItem( value: item, child: Text(item), ), ) .toList(growable: false), onChanged: (value) => setState(() => _activeConfig = value), decoration: const InputDecoration(labelText: '当前配置'), ), const SizedBox(height: AppSpacing.md), SizedBox( width: double.infinity, child: FilledButton.tonal( onPressed: _activeConfig == null ? null : _handleSwitchConfig, child: const Text('切换配置'), ), ), ], ), ), ), const SizedBox(height: AppSpacing.lg), Card( child: Padding( padding: const EdgeInsets.all(AppSpacing.md), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text('显示设置', style: Theme.of(context).textTheme.titleMedium), const SizedBox(height: AppSpacing.md), SwitchListTile( contentPadding: EdgeInsets.zero, value: _isFullscreen, onChanged: (value) => setState(() => _isFullscreen = value), title: const Text('全屏模式'), ), TextField( controller: _titleController, decoration: const InputDecoration(labelText: '窗口标题'), ), const SizedBox(height: AppSpacing.md), Row( children: [ Expanded( child: TextField( controller: _rotationController, keyboardType: TextInputType.number, decoration: const InputDecoration(labelText: '旋转角度'), ), ), const SizedBox(width: AppSpacing.md), Expanded( child: TextField( controller: _widthController, keyboardType: TextInputType.number, decoration: const InputDecoration(labelText: '渲染宽度'), ), ), const SizedBox(width: AppSpacing.md), Expanded( child: TextField( controller: _heightController, keyboardType: TextInputType.number, decoration: const InputDecoration(labelText: '渲染高度'), ), ), ], ), const SizedBox(height: AppSpacing.md), TextField( controller: _hsvMinController, decoration: const InputDecoration(labelText: '色键下限 HSV (逗号分隔)'), ), const SizedBox(height: AppSpacing.md), TextField( controller: _hsvMaxController, decoration: const InputDecoration(labelText: '色键上限 HSV (逗号分隔)'), ), const SizedBox(height: AppSpacing.md), TextField( controller: _pointsController, minLines: 3, maxLines: 5, decoration: const InputDecoration(labelText: '透视点 JSON'), ), const SizedBox(height: AppSpacing.md), SizedBox( width: double.infinity, child: FilledButton( onPressed: _handleSaveDisplayConfig, child: const Text('保存显示设置'), ), ), ], ), ), ), const SizedBox(height: AppSpacing.lg), Card( child: Padding( padding: const EdgeInsets.all(AppSpacing.md), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text('关于信息', style: Theme.of(context).textTheme.titleMedium), const SizedBox(height: AppSpacing.md), _InfoRow(label: '设备名称', value: status.deviceName ?? 'ShowenV2'), _InfoRow(label: '连接方式', value: status.connectionType.toUpperCase()), _InfoRow(label: '设备地址', value: status.ipAddress ?? provider.deviceIp), _InfoRow( label: '实时通道', value: provider.webSocketConnected ? '已连接' : '未连接', ), ], ), ), ), ], ), ); } Future _loadData() async { final service = context.read().httpApiService; setState(() => _loading = true); try { final results = await Future.wait([ service.getConfig(), service.getAvailableConfigs(), ]); _fullConfig = Map.from(results[0] as Map); final available = Map.from(results[1] as Map); _availableConfigs = (available['configs'] as List? ?? const []) .map((item) => item.toString()) .toList(growable: false); _activeConfig = available['active']?.toString(); _applyDisplayConfig(Map.from(_fullConfig?['display'] as Map? ?? const {})); } finally { if (mounted) { setState(() => _loading = false); } } } Future _handleSwitchConfig() async { final activeConfig = _activeConfig; if (activeConfig == null) { return; } await context.read().httpApiService.switchConfig(activeConfig); if (!mounted) { return; } await _loadData(); } Future _handleSaveDisplayConfig() async { final config = _fullConfig; if (config == null) { return; } final nextConfig = Map.from(config); nextConfig['display'] = { ...Map.from(config['display'] as Map? ?? const {}), 'fullscreen': _isFullscreen, 'window_title': _titleController.text.trim(), 'rotation': int.tryParse(_rotationController.text.trim()) ?? 0, 'render_width': int.tryParse(_widthController.text.trim()) ?? 1024, 'render_height': int.tryParse(_heightController.text.trim()) ?? 1024, 'chroma_key': { 'hsv_min': _parseIntList(_hsvMinController.text), 'hsv_max': _parseIntList(_hsvMaxController.text), }, 'perspective_correction': { 'points': jsonDecode(_pointsController.text.trim().isEmpty ? '[]' : _pointsController.text.trim()), }, }; await context.read().httpApiService.updateConfig(nextConfig); _fullConfig = nextConfig; if (!mounted) { return; } ScaffoldMessenger.of(context).showSnackBar( const SnackBar(content: Text('显示设置已保存')), ); } void _applyDisplayConfig(Map display) { _isFullscreen = display['fullscreen'] as bool? ?? false; _titleController.text = display['window_title']?.toString() ?? ''; _rotationController.text = '${display['rotation'] ?? 0}'; _widthController.text = '${display['render_width'] ?? 1024}'; _heightController.text = '${display['render_height'] ?? 1024}'; final chromaKey = Map.from(display['chroma_key'] as Map? ?? const {}); _hsvMinController.text = (chromaKey['hsv_min'] as List? ?? const [0, 0, 200]).join(','); _hsvMaxController.text = (chromaKey['hsv_max'] as List? ?? const [180, 30, 255]).join(','); final perspective = Map.from(display['perspective_correction'] as Map? ?? const {}); _pointsController.text = jsonEncode(perspective['points'] ?? const []); } List _parseIntList(String raw) { return raw .split(',') .map((item) => int.tryParse(item.trim()) ?? 0) .toList(growable: false); } } class _InfoRow extends StatelessWidget { const _InfoRow({required this.label, required this.value}); final String label; final String value; @override Widget build(BuildContext context) { return Padding( padding: const EdgeInsets.only(bottom: AppSpacing.sm), child: Row( children: [ Expanded(child: Text(label, style: Theme.of(context).textTheme.bodyMedium)), Text(value, style: Theme.of(context).textTheme.bodyLarge), ], ), ); } }