207 lines
7.0 KiB
Dart
207 lines
7.0 KiB
Dart
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';
|
||
}
|
||
}
|