Files
bvandeusen d115c267f4 feat(flutter): GoRouter shell + version gate + auth-aware redirects
Cold launch flow: no server-url -> /server-url. URL set, no token ->
/login. Token present -> /home with shell. VersionGate runs once when
the URL changes, hits /healthz, blocks if min_client_version > package
version. /home is a placeholder until Task 14.

Replaces Navigator.pushReplacementNamed in server_url_screen +
login_screen with context.go now that go_router owns the routes.
2026-05-02 17:24:08 -04:00

83 lines
2.7 KiB
Dart

import 'package:dio/dio.dart';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:go_router/go_router.dart';
import '../api/endpoints/health.dart';
import '../theme/theme_extension.dart';
import 'auth_provider.dart';
class ServerUrlScreen extends ConsumerStatefulWidget {
const ServerUrlScreen({super.key});
@override
ConsumerState<ServerUrlScreen> createState() => _ServerUrlScreenState();
}
class _ServerUrlScreenState extends ConsumerState<ServerUrlScreen> {
final _ctrl = TextEditingController();
bool _busy = false;
String? _error;
Future<void> _connect() async {
final url = _ctrl.text.trim();
if (url.isEmpty) {
setState(() => _error = 'Enter a server URL.');
return;
}
setState(() {
_busy = true;
_error = null;
});
try {
final dio = Dio(BaseOptions(baseUrl: url, connectTimeout: const Duration(seconds: 5)));
final body = await HealthApi(dio).check();
if (body['status'] != 'ok') {
throw StateError('unhealthy');
}
await ref.read(authControllerProvider.notifier).setServerUrl(url);
if (!mounted) return;
context.go('/login');
} on DioException catch (_) {
setState(() => _error = "Couldn't reach that server.");
} catch (_) {
setState(() => _error = "Couldn't reach that server.");
} finally {
if (mounted) setState(() => _busy = false);
}
}
@override
Widget build(BuildContext context) {
final fs = Theme.of(context).extension<FabledSwordTheme>()!;
return Scaffold(
body: SafeArea(
child: Padding(
padding: const EdgeInsets.all(24),
child: Column(crossAxisAlignment: CrossAxisAlignment.stretch, children: [
const SizedBox(height: 48),
Text('Connect to your Minstrel', style: TextStyle(fontFamily: fs.display.fontFamily, fontSize: 28)),
const SizedBox(height: 24),
TextField(
controller: _ctrl,
keyboardType: TextInputType.url,
decoration: const InputDecoration(labelText: 'Server URL', hintText: 'https://music.example.com'),
),
if (_error != null) Padding(
padding: const EdgeInsets.only(top: 8),
child: Text(_error!, style: TextStyle(color: fs.error)),
),
const SizedBox(height: 16),
FilledButton(
onPressed: _busy ? null : _connect,
child: _busy
? const SizedBox(width: 18, height: 18, child: CircularProgressIndicator(strokeWidth: 2))
: const Text('Connect'),
),
]),
),
),
);
}
}