import 'dart:convert'; import 'package:flutter/material.dart'; import 'package:go_router/go_router.dart'; import 'package:http/http.dart' as http; import 'package:mileograph_flutter/services/endpoint_service.dart'; import 'package:mileograph_flutter/services/data_service.dart'; import 'package:provider/provider.dart'; class SettingsPage extends StatefulWidget { const SettingsPage({super.key}); @override State createState() => _SettingsPageState(); } class _SettingsPageState extends State { late final TextEditingController _endpointController; bool _saving = false; @override void initState() { super.initState(); final endpoint = context.read().baseUrl; _endpointController = TextEditingController(text: endpoint); } @override void dispose() { _endpointController.dispose(); super.dispose(); } Future _probeVersion(String url) async { try { var uri = Uri.parse(url.trim()); if (uri.scheme.isEmpty) { uri = Uri.parse('https://$url'); } // Probe the provided API endpoint as-is. final target = uri; final res = await http.get(target).timeout(const Duration(seconds: 10)); debugPrint( 'Endpoint probe ${target.toString()} -> ${res.statusCode} ${res.body}', ); if (res.statusCode < 200 || res.statusCode >= 300) return null; final body = res.body.trim(); debugPrint('Endpoint probe body: $body'); // Try JSON first String? version; try { final parsed = jsonDecode(body); debugPrint('Endpoint probe parsed: $parsed'); if (parsed is Map && parsed['version'] is String) { version = parsed['version'] as String; } else if (parsed is String) { final candidate = parsed.trim().replaceAll('"', ''); if (RegExp(r'^\d+\.\d+\.\d+$').hasMatch(candidate)) { version = candidate; } } } catch (_) { // fall back to raw body parsing } version ??= body.split(RegExp(r'\s+')).firstWhere( (part) => RegExp(r'^\d+\.\d+\.\d+$').hasMatch(part), orElse: () => '', ); if (version.isEmpty) return null; final isValid = RegExp(r'^\d+\.\d+\.\d+$').hasMatch(version); return isValid ? version : null; } catch (_) { return null; } } Future _save() async { final endpointService = context.read(); final dataService = context.read(); final messenger = ScaffoldMessenger.of(context); final value = _endpointController.text.trim(); if (value.isEmpty) { messenger.showSnackBar( const SnackBar(content: Text('Please enter an endpoint URL.')), ); return; } setState(() => _saving = true); try { final version = await _probeVersion(value); if (version == null) { if (mounted) { messenger.showSnackBar( const SnackBar( content: Text('Endpoint test failed: no valid version returned.'), ), ); } return; } await endpointService.setBaseUrl(value); if (mounted) { messenger.showSnackBar( SnackBar(content: Text('Endpoint set to "$value" ($version)')), ); await Future.wait([ dataService.fetchHomepageStats(), dataService.fetchOnThisDay(), dataService.fetchTrips(), dataService.fetchHadTraction(), dataService.fetchLatestLocoChanges(), dataService.fetchLegs(), ]); } } catch (e) { if (mounted) { messenger.showSnackBar( SnackBar(content: Text('Failed to save endpoint: $e')), ); } } finally { if (mounted) { setState(() => _saving = false); } } } @override Widget build(BuildContext context) { final endpointService = context.watch(); if (!endpointService.isLoaded) { return const Scaffold( body: Center(child: CircularProgressIndicator()), ); } return Scaffold( appBar: AppBar( title: const Text('Settings'), leading: IconButton( icon: const Icon(Icons.arrow_back), onPressed: () { final navigator = Navigator.of(context); if (navigator.canPop()) { navigator.pop(); } else { context.go('/'); } }, ), ), body: Padding( padding: const EdgeInsets.all(16.0), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( 'API endpoint', style: Theme.of(context).textTheme.titleMedium?.copyWith( fontWeight: FontWeight.w700, ), ), const SizedBox(height: 8), Text( 'Set the base URL for the Mileograph API. Leave blank to use the default.', style: Theme.of(context).textTheme.bodySmall, ), const SizedBox(height: 16), TextField( controller: _endpointController, decoration: const InputDecoration( labelText: 'Endpoint URL', hintText: 'https://mileograph.co.uk/api/v1', border: OutlineInputBorder(), ), ), const SizedBox(height: 16), Row( children: [ FilledButton.icon( onPressed: _saving ? null : _save, icon: _saving ? const SizedBox( width: 18, height: 18, child: CircularProgressIndicator(strokeWidth: 2), ) : const Icon(Icons.save), label: const Text('Save endpoint'), ), const SizedBox(width: 12), TextButton( onPressed: _saving ? null : () { _endpointController.text = EndpointService.defaultBaseUrl; }, child: const Text('Reset to default'), ), ], ), const SizedBox(height: 12), Text( 'Current: ${endpointService.baseUrl}', style: Theme.of(context).textTheme.labelSmall, ), ], ), ), ); } }