import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import '../state/providers.dart'; import '../subsonic/credentials.dart'; import '../theme/tokens.dart'; /// Open the connect-server sheet. Future showConnectSheet(BuildContext context) { return showModalBottomSheet( context: context, backgroundColor: RatuneColors.background, isScrollControlled: true, builder: (_) => const _ConnectSheet(), ); } class _ConnectSheet extends ConsumerStatefulWidget { const _ConnectSheet(); @override ConsumerState<_ConnectSheet> createState() => _ConnectSheetState(); } class _ConnectSheetState extends ConsumerState<_ConnectSheet> { final _url = TextEditingController(); final _user = TextEditingController(); final _pass = TextEditingController(); bool _busy = false; @override void initState() { super.initState(); final creds = ref.read(connectionProvider).credentials; if (creds != null) { _url.text = creds.url; _user.text = creds.username; } } @override void dispose() { _url.dispose(); _user.dispose(); _pass.dispose(); super.dispose(); } Future _connect() async { setState(() => _busy = true); final ok = await ref.read(connectionProvider.notifier).connect( SubsonicCredentials( url: _url.text.trim(), username: _user.text.trim(), password: _pass.text, ), ); if (!mounted) return; setState(() => _busy = false); if (ok) Navigator.of(context).pop(); } @override Widget build(BuildContext context) { final conn = ref.watch(connectionProvider); final accent = Theme.of(context).colorScheme.primary; return Padding( padding: EdgeInsets.only( left: RatuneSpacing.xl, right: RatuneSpacing.xl, top: RatuneSpacing.xl, bottom: MediaQuery.of(context).viewInsets.bottom + RatuneSpacing.xl, ), child: Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.stretch, children: [ Text('Connect to server', style: TextStyle(color: accent, fontWeight: FontWeight.w700)), const SizedBox(height: RatuneSpacing.lg), _field(_url, 'Server URL', 'https://navidrome.example.com', keyboard: TextInputType.url), _field(_user, 'Username', 'you'), _field(_pass, 'Password', '••••••••', obscure: true), if (conn.status == ConnStatus.error && conn.error != null) ...[ const SizedBox(height: RatuneSpacing.md), Text(conn.error!, style: const TextStyle(color: Color(0xFFE06C75))), ], const SizedBox(height: RatuneSpacing.lg), FilledButton( onPressed: _busy ? null : _connect, style: FilledButton.styleFrom( backgroundColor: accent, foregroundColor: RatuneColors.background, shape: const RoundedRectangleBorder(), ), child: _busy ? const SizedBox( height: 16, width: 16, child: CircularProgressIndicator(strokeWidth: 2), ) : const Text('Connect'), ), ], ), ); } Widget _field( TextEditingController c, String label, String hint, { bool obscure = false, TextInputType? keyboard, }) { return Padding( padding: const EdgeInsets.only(bottom: RatuneSpacing.md), child: TextField( controller: c, obscureText: obscure, keyboardType: keyboard, autocorrect: false, enableSuggestions: false, style: const TextStyle(color: RatuneColors.foreground), decoration: InputDecoration( labelText: label, hintText: hint, labelStyle: const TextStyle(color: RatuneColors.dimmed), hintStyle: const TextStyle(color: RatuneColors.dimmed), enabledBorder: const OutlineInputBorder( borderRadius: BorderRadius.zero, borderSide: BorderSide(color: RatuneColors.border), ), focusedBorder: OutlineInputBorder( borderRadius: BorderRadius.zero, borderSide: BorderSide(color: Theme.of(context).colorScheme.primary), ), ), ), ); } }