310 lines
9.4 KiB
Dart
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),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
}
|