feat(useragent): added connection info setup screen
This commit is contained in:
@@ -14,13 +14,13 @@ class Bootstrap extends HookConsumerWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final container = ProviderScope.containerOf( context);
|
||||
final container = ProviderScope.containerOf(context);
|
||||
final completer = useMemoized(() {
|
||||
final completer = Completer<void>();
|
||||
completer.future.then((_) async {
|
||||
if (context.mounted) {
|
||||
final router = AutoRouter.of(context);
|
||||
router.replace(const DashboardRouter());
|
||||
router.replace(const ServerInfoSetupRoute());
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -2,9 +2,10 @@ import 'package:auto_route/auto_route.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:mtcore/markettakers.dart' as mt;
|
||||
|
||||
|
||||
@RoutePage()
|
||||
class AboutScreen extends StatelessWidget {
|
||||
const AboutScreen({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return mt.AboutScreen(decription: "Arbiter is bla bla bla");
|
||||
|
||||
284
useragent/lib/screens/server_info_setup.dart
Normal file
284
useragent/lib/screens/server_info_setup.dart
Normal file
@@ -0,0 +1,284 @@
|
||||
import 'package:arbiter/features/arbiter_url.dart';
|
||||
import 'package:arbiter/features/server_info_storage.dart';
|
||||
import 'package:arbiter/providers/server_info.dart';
|
||||
import 'package:arbiter/router.gr.dart';
|
||||
import 'package:auto_route/auto_route.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:sizer/sizer.dart';
|
||||
|
||||
@RoutePage()
|
||||
class ServerInfoSetupScreen extends HookConsumerWidget {
|
||||
const ServerInfoSetupScreen({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final storedServerInfo = ref.watch(serverInfoProvider);
|
||||
final resolvedServerInfo = storedServerInfo.asData?.value;
|
||||
final controller = useTextEditingController();
|
||||
final errorText = useState<String?>(null);
|
||||
final isSaving = useState(false);
|
||||
|
||||
useEffect(() {
|
||||
final serverInfo = resolvedServerInfo;
|
||||
if (serverInfo != null) {
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
if (context.mounted) {
|
||||
context.router.replace(const DashboardRouter());
|
||||
}
|
||||
});
|
||||
}
|
||||
return null;
|
||||
}, [context, resolvedServerInfo]);
|
||||
|
||||
Future<void> saveRemoteServerInfo() async {
|
||||
errorText.value = null;
|
||||
isSaving.value = true;
|
||||
|
||||
try {
|
||||
final arbiterUrl = ArbiterUrl.parse(controller.text.trim());
|
||||
await ref
|
||||
.read(serverInfoProvider.notifier)
|
||||
.save(
|
||||
address: arbiterUrl.host,
|
||||
port: arbiterUrl.port,
|
||||
caCert: arbiterUrl.caCert,
|
||||
);
|
||||
|
||||
if (context.mounted) {
|
||||
context.router.replace(const DashboardRouter());
|
||||
}
|
||||
} on FormatException catch (error) {
|
||||
errorText.value = error.message;
|
||||
} catch (_) {
|
||||
errorText.value = 'Failed to store connection settings.';
|
||||
} finally {
|
||||
isSaving.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
if (storedServerInfo.isLoading) {
|
||||
return const Scaffold(body: Center(child: CircularProgressIndicator()));
|
||||
}
|
||||
|
||||
if (storedServerInfo.hasError) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(title: const Text('Server Info Setup')),
|
||||
body: Center(
|
||||
child: Padding(
|
||||
padding: EdgeInsets.symmetric(horizontal: 6.w),
|
||||
child: Text(
|
||||
'Failed to load stored server info.',
|
||||
style: Theme.of(context).textTheme.bodyLarge,
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
final serverInfo = resolvedServerInfo;
|
||||
if (serverInfo != null) {
|
||||
return _RedirectingView(serverInfo: serverInfo);
|
||||
}
|
||||
|
||||
return Scaffold(
|
||||
appBar: AppBar(title: const Text('Server Info Setup')),
|
||||
body: LayoutBuilder(
|
||||
builder: (context, constraints) {
|
||||
final useRowLayout = constraints.maxWidth > constraints.maxHeight;
|
||||
final gap = 2.h;
|
||||
final horizontalPadding = 6.w;
|
||||
final verticalPadding = 3.h;
|
||||
final options = [
|
||||
const _OptionCard(
|
||||
title: 'Local',
|
||||
subtitle: 'Will start and connect to a local service in a future update.',
|
||||
enabled: false,
|
||||
child: SizedBox.shrink(),
|
||||
),
|
||||
_OptionCard(
|
||||
title: 'Remote',
|
||||
subtitle: 'Paste an Arbiter URL to store the server address, port, and CA fingerprint.',
|
||||
child: _RemoteConnectionForm(
|
||||
controller: controller,
|
||||
errorText: errorText.value,
|
||||
isSaving: isSaving.value,
|
||||
onSave: saveRemoteServerInfo,
|
||||
),
|
||||
),
|
||||
];
|
||||
|
||||
return ListView(
|
||||
padding: EdgeInsets.symmetric(
|
||||
horizontal: horizontalPadding,
|
||||
vertical: verticalPadding,
|
||||
),
|
||||
children: [
|
||||
_SetupHeader(gap: gap),
|
||||
SizedBox(height: gap),
|
||||
useRowLayout
|
||||
? Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Expanded(child: options[0]),
|
||||
SizedBox(width: 3.w),
|
||||
Expanded(child: options[1]),
|
||||
],
|
||||
)
|
||||
: Column(
|
||||
children: [
|
||||
options[0],
|
||||
SizedBox(height: gap),
|
||||
options[1],
|
||||
],
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _SetupHeader extends StatelessWidget {
|
||||
const _SetupHeader({required this.gap});
|
||||
|
||||
final double gap;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
'Choose how this user agent should reach Arbiter.',
|
||||
style: Theme.of(context).textTheme.headlineSmall,
|
||||
),
|
||||
SizedBox(height: gap * 0.5),
|
||||
Text(
|
||||
'Remote accepts the shareable Arbiter URL emitted by the server.',
|
||||
style: Theme.of(context).textTheme.bodyMedium,
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _RedirectingView extends StatelessWidget {
|
||||
const _RedirectingView({required this.serverInfo});
|
||||
|
||||
final StoredServerInfo serverInfo;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
body: Center(
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
const CircularProgressIndicator(),
|
||||
SizedBox(height: 2.h),
|
||||
Text('Using saved server ${serverInfo.address}:${serverInfo.port}'),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _RemoteConnectionForm extends StatelessWidget {
|
||||
const _RemoteConnectionForm({
|
||||
required this.controller,
|
||||
required this.errorText,
|
||||
required this.isSaving,
|
||||
required this.onSave,
|
||||
});
|
||||
|
||||
final TextEditingController controller;
|
||||
final String? errorText;
|
||||
final bool isSaving;
|
||||
final VoidCallback onSave;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
TextField(
|
||||
controller: controller,
|
||||
decoration: const InputDecoration(
|
||||
border: OutlineInputBorder(),
|
||||
labelText: 'Arbiter URL',
|
||||
hintText: 'arbiter://host:port?cert=...',
|
||||
),
|
||||
minLines: 2,
|
||||
maxLines: 4,
|
||||
),
|
||||
if (errorText != null) ...[
|
||||
SizedBox(height: 1.5.h),
|
||||
Text(
|
||||
errorText!,
|
||||
style: TextStyle(color: Theme.of(context).colorScheme.error),
|
||||
),
|
||||
],
|
||||
SizedBox(height: 2.h),
|
||||
Align(
|
||||
alignment: Alignment.centerLeft,
|
||||
child: FilledButton(
|
||||
onPressed: isSaving ? null : onSave,
|
||||
child: Text(isSaving ? 'Saving...' : 'Save connection'),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _OptionCard extends StatelessWidget {
|
||||
const _OptionCard({
|
||||
required this.title,
|
||||
required this.subtitle,
|
||||
required this.child,
|
||||
this.enabled = true,
|
||||
});
|
||||
|
||||
final String title;
|
||||
final String subtitle;
|
||||
final Widget child;
|
||||
final bool enabled;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = Theme.of(context);
|
||||
return Card(
|
||||
child: Padding(
|
||||
padding: EdgeInsets.all(2.h),
|
||||
child: Opacity(
|
||||
opacity: enabled ? 1 : 0.55,
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Text(title, style: theme.textTheme.titleLarge),
|
||||
if (!enabled) ...[
|
||||
SizedBox(width: 2.w),
|
||||
const Chip(label: Text('Coming soon')),
|
||||
],
|
||||
],
|
||||
),
|
||||
SizedBox(height: 1.h),
|
||||
Text(subtitle, style: theme.textTheme.bodyMedium),
|
||||
if (enabled) ...[
|
||||
SizedBox(height: 2.h),
|
||||
child,
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user