import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import '../../../../api/marianumconnect/auth/token_storage.dart'; import '../../../../state/app/modules/settings/bloc/settings_cubit.dart'; import '../../../../storage/dev_tools_settings.dart'; import '../../../../storage/settings.dart' as model; import '../../../../widget/details_bottom_sheet.dart'; /// Bottom-sheet that lets the user switch the active Marianum-Connect /// endpoint (live / beta / custom). Shared between the dev-tools section and /// the login screen so both places offer the exact same picker. /// /// On commit the change lands in the SettingsCubit (which in turn updates /// the dio singleton via the BlocBuilder in `main.dart`) and the currently /// stored bearer token is cleared — that token belongs to the old host and /// would be rejected by the new one. class MarianumConnectEndpointPicker { const MarianumConnectEndpointPicker._(); static void show(BuildContext context, SettingsCubit settings) { showDetailsBottomSheet( context, header: const ListTile(title: Text('Marianum-Connect-Server')), children: (sheetCtx) => [ BlocBuilder( bloc: settings, builder: (_, _) { final dev = settings.val().devToolsSettings; return _PickerBody( current: dev.marianumConnectEndpoint, customUrl: dev.marianumConnectCustomUrl, onChanged: (next, custom) async { final mutable = settings.val(write: true).devToolsSettings; mutable.marianumConnectEndpoint = next; if (custom != null) mutable.marianumConnectCustomUrl = custom; await const MarianumConnectTokenStorage().clear(); }, ); }, ), ], ); } /// Short human-readable label of the currently selected endpoint — used by /// the settings list tile and the login screen hint. static String labelFor(DevToolsSettings dev) { switch (dev.marianumConnectEndpoint) { case MarianumConnectEndpoint.live: return 'Normal (${DevToolsSettings.liveUrl})'; case MarianumConnectEndpoint.beta: return 'Beta (${DevToolsSettings.betaUrl})'; case MarianumConnectEndpoint.custom: final url = DevToolsSettings.sanitizeCustomUrl( dev.marianumConnectCustomUrl, ); return url == null ? 'Eigener Server (ungültig – Normal wird verwendet)' : 'Eigener Server ($url)'; } } } class _PickerBody extends StatefulWidget { final MarianumConnectEndpoint current; final String customUrl; final Future Function(MarianumConnectEndpoint next, String? customUrl) onChanged; const _PickerBody({ required this.current, required this.customUrl, required this.onChanged, }); @override State<_PickerBody> createState() => _PickerBodyState(); } class _PickerBodyState extends State<_PickerBody> { late MarianumConnectEndpoint _selected; late TextEditingController _customController; String? _customError; @override void initState() { super.initState(); _selected = widget.current; _customController = TextEditingController(text: widget.customUrl); } @override void dispose() { _customController.dispose(); super.dispose(); } @override Widget build(BuildContext context) { final allowsHttp = DevToolsSettings.allowsHttpCustomEndpoint; return Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.stretch, children: [ RadioGroup( groupValue: _selected, onChanged: _selectEndpoint, child: Column( children: [ const RadioListTile( title: Text('Normal'), subtitle: Text(DevToolsSettings.liveUrl), value: MarianumConnectEndpoint.live, ), const RadioListTile( title: Text('Beta'), subtitle: Text(DevToolsSettings.betaUrl), value: MarianumConnectEndpoint.beta, ), RadioListTile( title: const Text('Eigener Server'), subtitle: Text( allowsHttp ? 'HTTP oder HTTPS, ohne abschließenden Slash. ' 'HTTP nur für lokale Entwicklung.' : 'Nur HTTPS-URLs, ohne abschließenden Slash.', ), value: MarianumConnectEndpoint.custom, ), ], ), ), if (_selected == MarianumConnectEndpoint.custom) Padding( padding: const EdgeInsets.fromLTRB(24, 0, 24, 16), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ TextField( controller: _customController, keyboardType: TextInputType.url, decoration: InputDecoration( labelText: allowsHttp ? 'http(s)://...' : 'https://...', errorText: _customError, ), onChanged: (_) => setState(() => _customError = null), ), if (allowsHttp && _isHttpUrl(_customController.text)) const Padding( padding: EdgeInsets.only(top: 8), child: Text( '⚠ HTTP überträgt den Bearer-Token unverschlüsselt. ' 'Nur für lokale Entwicklung benutzen.', style: TextStyle(fontSize: 12, color: Colors.orange), ), ), ], ), ), Padding( padding: const EdgeInsets.all(16), child: FilledButton( onPressed: _confirm, child: const Text('Übernehmen'), ), ), ], ); } void _selectEndpoint(MarianumConnectEndpoint? value) { if (value == null) return; setState(() { _selected = value; _customError = null; }); } Future _confirm() async { if (_selected == MarianumConnectEndpoint.custom) { final sanitized = DevToolsSettings.sanitizeCustomUrl( _customController.text, ); if (sanitized == null) { setState( () => _customError = DevToolsSettings.allowsHttpCustomEndpoint ? 'Ungültige URL (http(s)://host[:port])' : 'Ungültige URL — nur HTTPS erlaubt', ); return; } await widget.onChanged(_selected, sanitized); } else { await widget.onChanged(_selected, null); } if (!mounted) return; Navigator.of(context).pop(); } static bool _isHttpUrl(String value) { final trimmed = value.trim(); final uri = Uri.tryParse(trimmed); return uri != null && uri.hasScheme && uri.scheme == 'http'; } }