Preserve Flutter mirror-os codebase
This commit is contained in:
@@ -0,0 +1,309 @@
|
||||
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),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user