Files
Client/lib/view/pages/settings/widgets/endpoint_picker.dart
T

207 lines
7.0 KiB
Dart
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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<SettingsCubit, model.Settings>(
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<void> 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<MarianumConnectEndpoint>(
groupValue: _selected,
onChanged: _selectEndpoint,
child: Column(
children: [
const RadioListTile<MarianumConnectEndpoint>(
title: Text('Normal'),
subtitle: Text(DevToolsSettings.liveUrl),
value: MarianumConnectEndpoint.live,
),
const RadioListTile<MarianumConnectEndpoint>(
title: Text('Beta'),
subtitle: Text(DevToolsSettings.betaUrl),
value: MarianumConnectEndpoint.beta,
),
RadioListTile<MarianumConnectEndpoint>(
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<void> _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';
}
}