Files
2026-07-11 21:51:34 -05:00

310 lines
9.4 KiB
Dart

import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:mirror_ui/mirror_ui.dart';
import '../providers/settings_provider.dart';
import '../providers/connection_provider.dart';
import 'dashboard_editor_screen.dart';
class SettingsScreen extends ConsumerStatefulWidget {
const SettingsScreen({super.key});
@override
ConsumerState<SettingsScreen> createState() => _SettingsScreenState();
}
class _SettingsScreenState extends ConsumerState<SettingsScreen> {
late TextEditingController _mirrorHostCtrl;
late TextEditingController _mirrorPortCtrl;
late TextEditingController _workoutUrlCtrl;
late TextEditingController _plexHostCtrl;
late TextEditingController _plexPortCtrl;
late TextEditingController _plexTokenCtrl;
late TextEditingController _haUrlCtrl;
late TextEditingController _haTokenCtrl;
late TextEditingController _spotifyClientIdCtrl;
bool _initialized = false;
@override
void initState() {
super.initState();
_mirrorHostCtrl = TextEditingController();
_mirrorPortCtrl = TextEditingController();
_workoutUrlCtrl = TextEditingController();
_plexHostCtrl = TextEditingController();
_plexPortCtrl = TextEditingController();
_plexTokenCtrl = TextEditingController();
_haUrlCtrl = TextEditingController();
_haTokenCtrl = TextEditingController();
_spotifyClientIdCtrl = TextEditingController();
}
void _loadFromSettings(AppSettings settings) {
if (_initialized) return;
_initialized = true;
_mirrorHostCtrl.text = settings.mirrorHost;
_mirrorPortCtrl.text = settings.mirrorPort.toString();
_workoutUrlCtrl.text = settings.workoutPlayerUrl;
_plexHostCtrl.text = settings.plexHost ?? '';
_plexPortCtrl.text = settings.plexPort.toString();
_plexTokenCtrl.text = settings.plexToken ?? '';
_haUrlCtrl.text = settings.haBaseUrl ?? '';
_haTokenCtrl.text = settings.haToken ?? '';
_spotifyClientIdCtrl.text = settings.spotifyClientId ?? '';
}
@override
void dispose() {
_mirrorHostCtrl.dispose();
_mirrorPortCtrl.dispose();
_workoutUrlCtrl.dispose();
_plexHostCtrl.dispose();
_plexPortCtrl.dispose();
_plexTokenCtrl.dispose();
_haUrlCtrl.dispose();
_haTokenCtrl.dispose();
_spotifyClientIdCtrl.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
final settingsAsync = ref.watch(settingsProvider);
return Scaffold(
appBar: AppBar(
title: const Text('Settings'),
backgroundColor: MirrorTheme.background,
actions: [
TextButton(
onPressed: _save,
child: const Text('Save'),
),
],
),
backgroundColor: MirrorTheme.background,
body: settingsAsync.when(
loading: () => const Center(child: CircularProgressIndicator()),
error: (e, _) => Center(child: Text('Error: $e')),
data: (settings) {
_loadFromSettings(settings);
return _buildForm(context);
},
),
);
}
Widget _buildForm(BuildContext context) {
return ListView(
padding: const EdgeInsets.all(16),
children: [
_SectionHeader('Mirror Connection'),
_SettingsField(
controller: _mirrorHostCtrl,
label: 'Host',
hint: 'mirror.local or 192.168.x.x',
icon: Icons.cast_connected,
),
_SettingsField(
controller: _mirrorPortCtrl,
label: 'Port',
hint: '9090',
icon: Icons.numbers,
keyboardType: TextInputType.number,
),
const SizedBox(height: 24),
_SectionHeader('Workout Player'),
_SettingsField(
controller: _workoutUrlCtrl,
label: 'URL',
hint: 'http://192.168.86.172:8091',
icon: Icons.fitness_center,
),
const SizedBox(height: 24),
_SectionHeader('Home Assistant'),
_SettingsField(
controller: _haUrlCtrl,
label: 'URL',
hint: 'http://homeassistant.local:8123',
icon: Icons.home,
),
_SettingsField(
controller: _haTokenCtrl,
label: 'Long-Lived Access Token',
hint: 'eyJ...',
icon: Icons.key,
obscure: true,
),
const SizedBox(height: 24),
_SectionHeader('Dashboard Widgets'),
Card(
color: MirrorTheme.surface,
child: ListTile(
leading: const Icon(Icons.dashboard, color: MirrorTheme.accent),
title: const Text('Dashboard Layout'),
subtitle: const Text('Reorder and configure widgets'),
trailing: const Icon(Icons.chevron_right),
onTap: () {
Navigator.of(context).push(
MaterialPageRoute(builder: (_) => const DashboardEditorScreen()),
);
},
),
),
const SizedBox(height: 24),
_SectionHeader('Plex'),
_SettingsField(
controller: _plexHostCtrl,
label: 'Host',
hint: '192.168.86.x',
icon: Icons.movie,
),
_SettingsField(
controller: _plexPortCtrl,
label: 'Port',
hint: '32400',
icon: Icons.numbers,
keyboardType: TextInputType.number,
),
_SettingsField(
controller: _plexTokenCtrl,
label: 'Token',
hint: 'X-Plex-Token',
icon: Icons.key,
obscure: true,
),
const SizedBox(height: 24),
_SectionHeader('Spotify'),
_SettingsField(
controller: _spotifyClientIdCtrl,
label: 'Client ID',
hint: 'From developer.spotify.com',
icon: Icons.music_note,
),
const SizedBox(height: 12),
SizedBox(
width: double.infinity,
child: OutlinedButton.icon(
onPressed: () {
// Spotify OAuth flow would go here
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('Spotify OAuth not yet implemented')),
);
},
icon: const Icon(Icons.login),
label: const Text('Connect Spotify'),
),
),
const SizedBox(height: 32),
SizedBox(
width: double.infinity,
child: FilledButton.icon(
onPressed: _save,
icon: const Icon(Icons.save),
label: const Text('Save Settings'),
),
),
const SizedBox(height: 16),
SizedBox(
width: double.infinity,
child: OutlinedButton.icon(
onPressed: () {
ref.read(connectionProvider.notifier).disconnect();
Navigator.of(context).pop();
},
icon: const Icon(Icons.link_off, color: MirrorTheme.danger),
label: const Text('Disconnect from Mirror', style: TextStyle(color: MirrorTheme.danger)),
),
),
const SizedBox(height: 40),
],
);
}
void _save() {
final settings = AppSettings(
mirrorHost: _mirrorHostCtrl.text.trim(),
mirrorPort: int.tryParse(_mirrorPortCtrl.text.trim()) ?? 9090,
workoutPlayerUrl: _workoutUrlCtrl.text.trim(),
plexHost: _plexHostCtrl.text.trim().isEmpty ? null : _plexHostCtrl.text.trim(),
plexPort: int.tryParse(_plexPortCtrl.text.trim()) ?? 32400,
plexToken: _plexTokenCtrl.text.trim().isEmpty ? null : _plexTokenCtrl.text.trim(),
haBaseUrl: _haUrlCtrl.text.trim().isEmpty ? null : _haUrlCtrl.text.trim(),
haToken: _haTokenCtrl.text.trim().isEmpty ? null : _haTokenCtrl.text.trim(),
spotifyClientId: _spotifyClientIdCtrl.text.trim().isEmpty ? null : _spotifyClientIdCtrl.text.trim(),
);
ref.read(settingsProvider.notifier).save(settings);
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('Settings saved')),
);
Navigator.of(context).pop();
}
}
class _SectionHeader extends StatelessWidget {
final String title;
const _SectionHeader(this.title);
@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.only(bottom: 12),
child: Text(
title,
style: Theme.of(context).textTheme.labelLarge?.copyWith(
color: MirrorTheme.accent,
letterSpacing: 1,
),
),
);
}
}
class _SettingsField extends StatelessWidget {
final TextEditingController controller;
final String label;
final String hint;
final IconData icon;
final bool obscure;
final TextInputType? keyboardType;
const _SettingsField({
required this.controller,
required this.label,
required this.hint,
required this.icon,
this.obscure = false,
this.keyboardType,
});
@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.only(bottom: 12),
child: TextField(
controller: controller,
obscureText: obscure,
keyboardType: keyboardType,
decoration: InputDecoration(
labelText: label,
hintText: hint,
filled: true,
fillColor: MirrorTheme.surface,
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
borderSide: BorderSide.none,
),
prefixIcon: Icon(icon, size: 20),
contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 14),
),
),
);
}
}