mobile-music/lib/screens/connect_sheet.dart
2026-07-29 14:14:18 -04:00

144 lines
4.4 KiB
Dart

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<void> showConnectSheet(BuildContext context) {
return showModalBottomSheet<void>(
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<void> _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),
),
),
),
);
}
}