import 'dart:async'; import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; import '../../core/theme/app_colors.dart'; import '../../core/widgets/app_icon.dart'; import 'jellyfin_controller.dart'; import 'settings_widgets.dart'; /// Settings card for the optional Jellyfin music server. Quick Connect is the /// only sign-in path; the token itself never leaves secure storage. class JellyfinSettingsCard extends StatefulWidget { const JellyfinSettingsCard({super.key}); @override State createState() => _JellyfinSettingsCardState(); } class _JellyfinSettingsCardState extends State with WidgetsBindingObserver { late final TextEditingController _url; final FocusNode _urlFocus = FocusNode(); @override void initState() { super.initState(); _url = TextEditingController( text: context.read().serverUrl, ); WidgetsBinding.instance.addObserver(this); } @override void dispose() { WidgetsBinding.instance.removeObserver(this); _url.dispose(); _urlFocus.dispose(); super.dispose(); } @override void didChangeAppLifecycleState(AppLifecycleState state) { if (state != AppLifecycleState.resumed || !mounted) return; context.read().onAppResumed(); } @override Widget build(BuildContext context) { final colors = context.colors; final theme = Theme.of(context); final jellyfin = context.watch(); if (!_urlFocus.hasFocus && _url.text != jellyfin.serverUrl) { _url.value = TextEditingValue( text: jellyfin.serverUrl, selection: TextSelection.collapsed(offset: jellyfin.serverUrl.length), ); } final statusColor = _statusColor(jellyfin, colors); final connected = jellyfin.connected; final showUrl = jellyfin.enabled && !connected && !jellyfin.pairing; return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( children: [ Container( width: 42, height: 42, decoration: BoxDecoration( color: colors.accentSoft, borderRadius: BorderRadius.circular(14), ), alignment: Alignment.center, child: AppIcon( 'assets/icons/music.svg', size: 20, color: colors.accent, ), ), const SizedBox(width: 14), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text('Jellyfin', style: theme.textTheme.titleMedium), const SizedBox(height: 2), Row( children: [ Container( width: 7, height: 7, decoration: BoxDecoration( color: statusColor, shape: BoxShape.circle, ), ), const SizedBox(width: 7), Expanded( child: Text( jellyfin.statusLabel, maxLines: 1, overflow: TextOverflow.ellipsis, style: theme.textTheme.bodySmall?.copyWith( color: colors.inkMuted, ), ), ), ], ), ], ), ), const SizedBox(width: 8), SettingsSwitch( value: jellyfin.enabled, semanticsLabel: jellyfin.enabled ? 'Turn off Jellyfin' : 'Turn on Jellyfin', onChanged: (value) { unawaited(jellyfin.setEnabled(value)); }, ), ], ), if (jellyfin.enabled) ...[ const SizedBox(height: 12), Divider(color: colors.border, height: 24), if (jellyfin.pairing) _QuickConnectCode(controller: jellyfin) else ...[ if (showUrl) ...[ Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ Expanded( child: TextField( controller: _url, focusNode: _urlFocus, keyboardType: TextInputType.url, textInputAction: TextInputAction.go, autocorrect: false, enabled: !jellyfin.busy, style: theme.textTheme.bodyMedium, onSubmitted: (value) { _urlFocus.unfocus(); unawaited(jellyfin.connect(value)); }, decoration: const InputDecoration( labelText: 'Server URL', hintText: 'https://jellyfin.example.com', isDense: true, ), ), ), const SizedBox(width: 10), Padding( padding: const EdgeInsets.only(top: 4), child: TextButton( onPressed: jellyfin.busy ? null : () { _urlFocus.unfocus(); unawaited(jellyfin.connect(_url.text)); }, style: TextButton.styleFrom( foregroundColor: colors.accent, ), child: Text(jellyfin.busy ? 'Connecting…' : 'Connect'), ), ), ], ), const SizedBox(height: 8), ], if (connected) ...[ Row( children: [ Expanded( child: Text( jellyfin.userName == null ? 'Signed in.' : 'Signed in as ${jellyfin.userName}.', style: theme.textTheme.bodySmall?.copyWith( color: colors.inkMuted, ), ), ), TextButton( onPressed: () { unawaited(jellyfin.removePairing()); }, style: TextButton.styleFrom(foregroundColor: colors.danger), child: const Text('Remove pairing'), ), ], ), ] else if (jellyfin.status == JellyfinStatus.offline) ...[ Row( children: [ Expanded( child: Text( 'Jellyfin is paused until the connection returns. ' 'It will retry automatically.', style: theme.textTheme.bodySmall?.copyWith( color: colors.inkMuted, ), ), ), TextButton( onPressed: () { unawaited(jellyfin.checkNow()); }, style: TextButton.styleFrom(foregroundColor: colors.accent), child: const Text('Retry now'), ), ], ), ] else if (jellyfin.busy) ...[ Text( 'Checking the server…', style: theme.textTheme.bodySmall?.copyWith( color: colors.inkMuted, ), ), ] else if (!jellyfin.needsRepair) ...[ Text( jellyfin.hasSession ? 'CarDash reuses the saved pairing. Connect to check it, ' 'or enter a different server URL.' : jellyfin.hasServerUrl ? 'Press Connect to pair this server with a Quick Connect ' 'code.' : 'Enter the address of your Jellyfin server to pair it ' 'with CarDash.', style: theme.textTheme.bodySmall?.copyWith( color: colors.inkMuted, ), ), ], ], if (jellyfin.error != null) ...[ const SizedBox(height: 8), Text( jellyfin.error!, style: theme.textTheme.bodySmall?.copyWith(color: colors.danger), ), ], ] else if (jellyfin.needsRepair) ...[ const SizedBox(height: 12), Text( '${jellyfin.error ?? 'The saved Jellyfin pairing is no longer valid.'} ' 'Pair again to keep using Jellyfin.', style: theme.textTheme.bodySmall?.copyWith(color: colors.danger), ), const SizedBox(height: 4), TextButton( onPressed: () { unawaited(jellyfin.pairAgain()); }, style: TextButton.styleFrom( foregroundColor: colors.accent, padding: EdgeInsets.zero, alignment: Alignment.centerLeft, ), child: const Text('Pair again'), ), ], const SizedBox(height: 8), Text( 'Only the music library is read. The access token is stored on this ' 'device and can be removed at any time.', style: theme.textTheme.bodySmall?.copyWith(color: colors.inkMuted), ), ], ); } Color _statusColor(JellyfinController jellyfin, AppColors colors) { if (jellyfin.connected) return colors.accent; if (jellyfin.error != null || jellyfin.status == JellyfinStatus.error || jellyfin.needsRepair) { return colors.danger; } if (jellyfin.status == JellyfinStatus.offline) return colors.warning; return colors.inkFaint; } } class _QuickConnectCode extends StatelessWidget { const _QuickConnectCode({required this.controller}); final JellyfinController controller; @override Widget build(BuildContext context) { final colors = context.colors; final theme = Theme.of(context); final code = controller.quickConnectCode ?? ''; return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Container( width: double.infinity, padding: const EdgeInsets.symmetric(horizontal: 22, vertical: 18), decoration: BoxDecoration( color: colors.accentSoft, borderRadius: BorderRadius.circular(18), ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( 'Quick Connect code', style: theme.textTheme.labelMedium?.copyWith( color: colors.inkMuted, ), ), const SizedBox(height: 4), SelectableText( code, style: theme.textTheme.headlineMedium?.copyWith( color: colors.ink, fontWeight: FontWeight.w700, letterSpacing: 6, ), ), const SizedBox(height: 6), Text( 'Open Jellyfin, choose Quick Connect and enter this code. ' 'CarDash connects as soon as you approve it.', style: theme.textTheme.bodySmall?.copyWith( color: colors.inkMuted, ), ), ], ), ), const SizedBox(height: 2), TextButton( onPressed: controller.cancelPairing, style: TextButton.styleFrom( foregroundColor: colors.inkMuted, padding: EdgeInsets.zero, alignment: Alignment.centerLeft, ), child: const Text('Cancel'), ), ], ); } }