fix: misselanious updates
This commit is contained in:
@@ -183,7 +183,7 @@ class _PinEntryViewState extends State<PinEntryView> {
|
||||
return Scaffold(
|
||||
body: SafeArea(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 24),
|
||||
padding: const EdgeInsets.fromLTRB(24, 16, 24, 0),
|
||||
child: Column(
|
||||
children: [
|
||||
Align(
|
||||
|
||||
+346
-223
@@ -1,4 +1,7 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
import '../l10n/app_localizations.dart';
|
||||
@@ -6,6 +9,8 @@ import '../utils/navigation.dart';
|
||||
import '../viewmodels/registration_view_model.dart';
|
||||
import '../viewmodels/remote_sync_view_model.dart';
|
||||
|
||||
const _codeLength = 6;
|
||||
|
||||
class RegistrationView extends StatefulWidget {
|
||||
const RegistrationView({super.key});
|
||||
|
||||
@@ -26,8 +31,6 @@ class _RegistrationViewState extends State<RegistrationView> {
|
||||
});
|
||||
}
|
||||
|
||||
AppLocalizations get _l10n => AppLocalizations.of(context);
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_deviceNameController.dispose();
|
||||
@@ -35,6 +38,23 @@ class _RegistrationViewState extends State<RegistrationView> {
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
Future<void> _register(RegistrationViewModel vm) async {
|
||||
if (!_formKey.currentState!.validate()) return;
|
||||
FocusScope.of(context).unfocus();
|
||||
|
||||
await vm.registerDevice(
|
||||
deviceName: _deviceNameController.text.trim(),
|
||||
registrationCode: _regCodeController.text.trim().toUpperCase(),
|
||||
);
|
||||
}
|
||||
|
||||
void _finish() {
|
||||
// The redirect away from /register only sees the base location of a
|
||||
// pushed route, so leave the screen ourselves.
|
||||
unawaited(context.read<RemoteSyncViewModel>().activate());
|
||||
context.popOrGo('/admin');
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final vm = context.watch<RegistrationViewModel>();
|
||||
@@ -44,11 +64,17 @@ class _RegistrationViewState extends State<RegistrationView> {
|
||||
backgroundColor: Theme.of(context).colorScheme.surface,
|
||||
appBar: AppBar(
|
||||
backgroundColor: Colors.transparent,
|
||||
leading: IconButton(
|
||||
tooltip: l10n.cancel,
|
||||
onPressed: () => context.popOrGo('/admin'),
|
||||
icon: const Icon(Icons.close_rounded),
|
||||
),
|
||||
title: Text(l10n.connectToRemoteServer),
|
||||
leading: vm.step == RegistrationStep.registered
|
||||
? null
|
||||
: IconButton(
|
||||
tooltip: l10n.cancel,
|
||||
onPressed: vm.step == RegistrationStep.registering
|
||||
? null
|
||||
: () => context.popOrGo('/admin'),
|
||||
icon: const Icon(Icons.close_rounded),
|
||||
),
|
||||
automaticallyImplyLeading: false,
|
||||
),
|
||||
body: SafeArea(
|
||||
child: Center(
|
||||
@@ -56,7 +82,15 @@ class _RegistrationViewState extends State<RegistrationView> {
|
||||
padding: const EdgeInsets.symmetric(horizontal: 32, vertical: 24),
|
||||
child: ConstrainedBox(
|
||||
constraints: const BoxConstraints(maxWidth: 480),
|
||||
child: _buildStep(vm, l10n),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
_StepIndicator(current: _stepIndex(vm)),
|
||||
const SizedBox(height: 32),
|
||||
_buildStep(vm, l10n),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
@@ -64,333 +98,422 @@ class _RegistrationViewState extends State<RegistrationView> {
|
||||
);
|
||||
}
|
||||
|
||||
int _stepIndex(RegistrationViewModel vm) {
|
||||
if (vm.step == RegistrationStep.registered) return 2;
|
||||
return vm.selectedVenue == null ? 0 : 1;
|
||||
}
|
||||
|
||||
Widget _buildStep(RegistrationViewModel vm, AppLocalizations l10n) {
|
||||
switch (vm.step) {
|
||||
case RegistrationStep.initial:
|
||||
case RegistrationStep.loadingVenues:
|
||||
return _buildLoading();
|
||||
return _buildLoading(l10n);
|
||||
case RegistrationStep.error:
|
||||
return _buildError(vm);
|
||||
return _buildError(vm, l10n);
|
||||
case RegistrationStep.venuesLoaded:
|
||||
case RegistrationStep.registering:
|
||||
if (vm.selectedVenue == null) {
|
||||
return _buildVenueSelection(vm, l10n);
|
||||
}
|
||||
return _buildDeviceDetails(vm, l10n);
|
||||
case RegistrationStep.registering:
|
||||
return _buildRegistering();
|
||||
case RegistrationStep.registered:
|
||||
return _buildSuccess(vm, l10n);
|
||||
case RegistrationStep.initial:
|
||||
return _buildLoading();
|
||||
}
|
||||
}
|
||||
|
||||
Widget _buildLoading() {
|
||||
Widget _buildLoading(AppLocalizations l10n) {
|
||||
return Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
const SizedBox(height: 60),
|
||||
Icon(
|
||||
Icons.cloud_outlined,
|
||||
size: 64,
|
||||
color: Theme.of(context).colorScheme.primary.withValues(alpha: 0.5),
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
const SizedBox(height: 40),
|
||||
const CircularProgressIndicator(strokeWidth: 3),
|
||||
const SizedBox(height: 16),
|
||||
Text(
|
||||
_l10n.connectingToServer,
|
||||
l10n.connectingToServer,
|
||||
style: Theme.of(context).textTheme.titleMedium,
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildError(RegistrationViewModel vm) {
|
||||
Widget _buildError(RegistrationViewModel vm, AppLocalizations l10n) {
|
||||
final scheme = Theme.of(context).colorScheme;
|
||||
|
||||
return Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
const SizedBox(height: 60),
|
||||
Icon(
|
||||
Icons.cloud_off_rounded,
|
||||
size: 64,
|
||||
color: Theme.of(context).colorScheme.error,
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
Icon(Icons.cloud_off_rounded, size: 56, color: scheme.error),
|
||||
const SizedBox(height: 16),
|
||||
Text(
|
||||
_l10n.failedToConnect,
|
||||
l10n.failedToConnect,
|
||||
style: Theme.of(context).textTheme.titleLarge,
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
vm.errorMessage ?? _l10n.unknownError,
|
||||
vm.errorMessage ?? l10n.unknownError,
|
||||
textAlign: TextAlign.center,
|
||||
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
|
||||
color: Theme.of(
|
||||
context,
|
||||
).colorScheme.onSurface.withValues(alpha: 0.6),
|
||||
color: scheme.onSurface.withValues(alpha: 0.6),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
FilledButton.icon(
|
||||
onPressed: () => vm.retry(),
|
||||
onPressed: vm.retry,
|
||||
icon: const Icon(Icons.refresh),
|
||||
label: Text(_l10n.retry),
|
||||
label: Text(l10n.retry),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildVenueSelection(RegistrationViewModel vm, AppLocalizations l10n) {
|
||||
final scheme = Theme.of(context).colorScheme;
|
||||
|
||||
return Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
const SizedBox(height: 40),
|
||||
Icon(
|
||||
Icons.cloud_outlined,
|
||||
size: 48,
|
||||
color: Theme.of(context).colorScheme.primary.withValues(alpha: 0.5),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Text(
|
||||
_l10n.connectToRemoteServer,
|
||||
textAlign: TextAlign.center,
|
||||
l10n.selectVenue,
|
||||
style: Theme.of(context).textTheme.headlineSmall,
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
_l10n.selectVenueToLink,
|
||||
textAlign: TextAlign.center,
|
||||
l10n.selectVenueToLink,
|
||||
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
|
||||
color: Theme.of(
|
||||
context,
|
||||
).colorScheme.onSurface.withValues(alpha: 0.6),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 32),
|
||||
Text(
|
||||
_l10n.selectVenue,
|
||||
style: Theme.of(context).textTheme.labelLarge?.copyWith(
|
||||
color: Theme.of(
|
||||
context,
|
||||
).colorScheme.onSurface.withValues(alpha: 0.7),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Container(
|
||||
decoration: BoxDecoration(
|
||||
border: Border.all(
|
||||
color: Theme.of(
|
||||
context,
|
||||
).colorScheme.onSurface.withValues(alpha: 0.15),
|
||||
),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
child: Column(
|
||||
children: [
|
||||
for (final venue in vm.venues)
|
||||
Material(
|
||||
color: Colors.transparent,
|
||||
child: InkWell(
|
||||
onTap: () => vm.selectVenue(venue),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 16,
|
||||
vertical: 14,
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(
|
||||
Icons.store_outlined,
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: Text(
|
||||
venue.name,
|
||||
style: Theme.of(context).textTheme.bodyLarge,
|
||||
),
|
||||
),
|
||||
Icon(
|
||||
Icons.chevron_right_rounded,
|
||||
color: Theme.of(
|
||||
context,
|
||||
).colorScheme.onSurface.withValues(alpha: 0.3),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
color: scheme.onSurface.withValues(alpha: 0.6),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
TextButton(
|
||||
onPressed: () => vm.retry(),
|
||||
child: Text(_l10n.refreshVenues),
|
||||
if (vm.venues.isEmpty)
|
||||
Container(
|
||||
padding: const EdgeInsets.all(24),
|
||||
decoration: BoxDecoration(
|
||||
color: scheme.surfaceContainerHighest.withValues(alpha: 0.5),
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
),
|
||||
child: Column(
|
||||
children: [
|
||||
Icon(
|
||||
Icons.storefront_outlined,
|
||||
size: 40,
|
||||
color: scheme.onSurface.withValues(alpha: 0.4),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
Text(
|
||||
l10n.noVenuesFound,
|
||||
textAlign: TextAlign.center,
|
||||
style: Theme.of(context).textTheme.bodyMedium,
|
||||
),
|
||||
],
|
||||
),
|
||||
)
|
||||
else
|
||||
for (final venue in vm.venues)
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(bottom: 8),
|
||||
child: Material(
|
||||
color: scheme.surfaceContainerHighest.withValues(alpha: 0.5),
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
clipBehavior: Clip.antiAlias,
|
||||
child: InkWell(
|
||||
onTap: () => vm.selectVenue(venue),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 16,
|
||||
vertical: 16,
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
CircleAvatar(
|
||||
backgroundColor: scheme.primaryContainer,
|
||||
foregroundColor: scheme.onPrimaryContainer,
|
||||
child: const Icon(Icons.storefront_outlined),
|
||||
),
|
||||
const SizedBox(width: 16),
|
||||
Expanded(
|
||||
child: Text(
|
||||
venue.name,
|
||||
style: Theme.of(context).textTheme.titleMedium,
|
||||
),
|
||||
),
|
||||
Icon(
|
||||
Icons.chevron_right_rounded,
|
||||
color: scheme.onSurface.withValues(alpha: 0.4),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Center(
|
||||
child: TextButton.icon(
|
||||
onPressed: vm.retry,
|
||||
icon: const Icon(Icons.refresh, size: 18),
|
||||
label: Text(l10n.refreshVenues),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildDeviceDetails(RegistrationViewModel vm, AppLocalizations l10n) {
|
||||
final scheme = Theme.of(context).colorScheme;
|
||||
final isBusy = vm.step == RegistrationStep.registering;
|
||||
|
||||
return Form(
|
||||
key: _formKey,
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
const SizedBox(height: 40),
|
||||
Icon(
|
||||
Icons.store_outlined,
|
||||
size: 48,
|
||||
color: Theme.of(context).colorScheme.primary.withValues(alpha: 0.5),
|
||||
Container(
|
||||
padding: const EdgeInsets.fromLTRB(16, 8, 8, 8),
|
||||
decoration: BoxDecoration(
|
||||
color: scheme.primaryContainer.withValues(alpha: 0.5),
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(Icons.storefront_outlined, color: scheme.primary),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: Text(
|
||||
vm.selectedVenue!.name,
|
||||
style: Theme.of(context).textTheme.titleMedium,
|
||||
),
|
||||
),
|
||||
TextButton(
|
||||
onPressed: isBusy ? null : vm.deselectVenue,
|
||||
child: Text(l10n.changeVenue),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
Text(
|
||||
l10n.enterDeviceDetails,
|
||||
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
|
||||
color: scheme.onSurface.withValues(alpha: 0.6),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Text(
|
||||
vm.selectedVenue!.name,
|
||||
textAlign: TextAlign.center,
|
||||
style: Theme.of(context).textTheme.headlineSmall,
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
_l10n.enterDeviceDetails,
|
||||
textAlign: TextAlign.center,
|
||||
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
|
||||
color: Theme.of(
|
||||
context,
|
||||
).colorScheme.onSurface.withValues(alpha: 0.6),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 32),
|
||||
TextFormField(
|
||||
controller: _deviceNameController,
|
||||
enabled: !isBusy,
|
||||
textInputAction: TextInputAction.next,
|
||||
decoration: InputDecoration(
|
||||
labelText: _l10n.deviceName,
|
||||
hintText: 'e.g. Bar terminal 1',
|
||||
border: OutlineInputBorder(),
|
||||
labelText: l10n.deviceName,
|
||||
hintText: l10n.deviceNameHint,
|
||||
prefixIcon: const Icon(Icons.tablet_android_outlined),
|
||||
border: const OutlineInputBorder(),
|
||||
),
|
||||
validator: (v) =>
|
||||
v == null || v.trim().isEmpty ? _l10n.deviceNameRequired : null,
|
||||
v == null || v.trim().isEmpty ? l10n.deviceNameRequired : null,
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
TextFormField(
|
||||
controller: _regCodeController,
|
||||
decoration: InputDecoration(
|
||||
labelText: _l10n.registrationCode,
|
||||
hintText: '6-character code from dashboard',
|
||||
border: OutlineInputBorder(),
|
||||
counterText: '',
|
||||
),
|
||||
maxLength: 6,
|
||||
enabled: !isBusy,
|
||||
autofocus: true,
|
||||
maxLength: _codeLength,
|
||||
textAlign: TextAlign.center,
|
||||
textCapitalization: TextCapitalization.characters,
|
||||
validator: (v) => v == null || v.trim().length < 6
|
||||
? _l10n.enterRegistrationCode
|
||||
textInputAction: TextInputAction.done,
|
||||
autocorrect: false,
|
||||
enableSuggestions: false,
|
||||
inputFormatters: [
|
||||
FilteringTextInputFormatter.allow(RegExp('[a-zA-Z0-9]')),
|
||||
_UpperCaseFormatter(),
|
||||
],
|
||||
style: Theme.of(context).textTheme.headlineSmall?.copyWith(
|
||||
letterSpacing: 8,
|
||||
fontFamily: 'monospace',
|
||||
),
|
||||
decoration: InputDecoration(
|
||||
labelText: l10n.registrationCode,
|
||||
hintText: '•' * _codeLength,
|
||||
helperText: l10n.registrationCodeHelper,
|
||||
border: const OutlineInputBorder(),
|
||||
),
|
||||
onFieldSubmitted: (_) => _register(vm),
|
||||
validator: (v) => v == null || v.trim().length < _codeLength
|
||||
? l10n.enterRegistrationCode
|
||||
: null,
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
if (vm.errorMessage != null)
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(bottom: 8),
|
||||
child: Text(
|
||||
vm.errorMessage!,
|
||||
style: TextStyle(
|
||||
color: Theme.of(context).colorScheme.error,
|
||||
fontSize: 13,
|
||||
),
|
||||
if (vm.errorMessage != null) ...[
|
||||
const SizedBox(height: 8),
|
||||
Container(
|
||||
padding: const EdgeInsets.all(12),
|
||||
decoration: BoxDecoration(
|
||||
color: scheme.errorContainer,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(Icons.error_outline, color: scheme.onErrorContainer),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: Text(
|
||||
vm.errorMessage!,
|
||||
style: TextStyle(color: scheme.onErrorContainer),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Row(
|
||||
children: [
|
||||
TextButton(
|
||||
onPressed: () {
|
||||
vm.reset();
|
||||
},
|
||||
child: Text(_l10n.back),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: FilledButton(
|
||||
onPressed: () async {
|
||||
if (!_formKey.currentState!.validate()) return;
|
||||
await vm.registerDevice(
|
||||
deviceName: _deviceNameController.text.trim(),
|
||||
registrationCode: _regCodeController.text
|
||||
.trim()
|
||||
.toUpperCase(),
|
||||
);
|
||||
},
|
||||
child: Text(_l10n.register),
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
const SizedBox(height: 24),
|
||||
FilledButton(
|
||||
style: FilledButton.styleFrom(
|
||||
minimumSize: const Size.fromHeight(52),
|
||||
),
|
||||
onPressed: isBusy ? null : () => _register(vm),
|
||||
child: isBusy
|
||||
? Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
const SizedBox(
|
||||
width: 18,
|
||||
height: 18,
|
||||
child: CircularProgressIndicator(strokeWidth: 2),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Text(l10n.registeringDevice),
|
||||
],
|
||||
)
|
||||
: Text(l10n.register),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildRegistering() {
|
||||
return Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
const SizedBox(height: 60),
|
||||
const CircularProgressIndicator(strokeWidth: 3),
|
||||
const SizedBox(height: 24),
|
||||
Text(
|
||||
_l10n.registeringDevice,
|
||||
style: Theme.of(context).textTheme.titleMedium,
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildSuccess(RegistrationViewModel vm, AppLocalizations l10n) {
|
||||
final scheme = Theme.of(context).colorScheme;
|
||||
|
||||
return Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
const SizedBox(height: 40),
|
||||
Icon(
|
||||
Icons.check_circle_rounded,
|
||||
size: 80,
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
Center(
|
||||
child: Container(
|
||||
padding: const EdgeInsets.all(20),
|
||||
decoration: BoxDecoration(
|
||||
color: scheme.primaryContainer,
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
child: Icon(
|
||||
Icons.check_rounded,
|
||||
size: 56,
|
||||
color: scheme.onPrimaryContainer,
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
Text(
|
||||
_l10n.deviceRegistered,
|
||||
l10n.deviceRegistered,
|
||||
textAlign: TextAlign.center,
|
||||
style: Theme.of(context).textTheme.headlineSmall,
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
Text(
|
||||
'${vm.selectedVenue?.name ?? "Venue"} is now linked.',
|
||||
textAlign: TextAlign.center,
|
||||
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
|
||||
color: Theme.of(
|
||||
context,
|
||||
).colorScheme.onSurface.withValues(alpha: 0.6),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
_l10n.openTabsWillSync,
|
||||
l10n.venueNowLinked(vm.selectedVenue?.name ?? ''),
|
||||
textAlign: TextAlign.center,
|
||||
style: Theme.of(context).textTheme.bodySmall?.copyWith(
|
||||
color: Theme.of(
|
||||
context,
|
||||
).colorScheme.onSurface.withValues(alpha: 0.5),
|
||||
style: Theme.of(context).textTheme.bodyLarge,
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
l10n.openTabsWillSync,
|
||||
textAlign: TextAlign.center,
|
||||
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
|
||||
color: scheme.onSurface.withValues(alpha: 0.6),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 32),
|
||||
FilledButton.icon(
|
||||
// Once configured, the router redirects to /bar by itself.
|
||||
onPressed: () => context.read<RemoteSyncViewModel>().activate(),
|
||||
style: FilledButton.styleFrom(
|
||||
minimumSize: const Size.fromHeight(52),
|
||||
),
|
||||
onPressed: _finish,
|
||||
icon: const Icon(Icons.arrow_forward),
|
||||
label: Text(_l10n.done),
|
||||
label: Text(l10n.done),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _StepIndicator extends StatelessWidget {
|
||||
final int current;
|
||||
|
||||
const _StepIndicator({required this.current});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final l10n = AppLocalizations.of(context);
|
||||
final scheme = Theme.of(context).colorScheme;
|
||||
final labels = [l10n.stepVenue, l10n.stepDevice, l10n.stepLinked];
|
||||
|
||||
return Row(
|
||||
children: [
|
||||
for (var i = 0; i < labels.length; i++) ...[
|
||||
if (i > 0)
|
||||
Expanded(
|
||||
child: Container(
|
||||
height: 2,
|
||||
margin: const EdgeInsets.only(bottom: 20),
|
||||
color: i <= current
|
||||
? scheme.primary
|
||||
: scheme.onSurface.withValues(alpha: 0.15),
|
||||
),
|
||||
),
|
||||
Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
CircleAvatar(
|
||||
radius: 14,
|
||||
backgroundColor: i <= current
|
||||
? scheme.primary
|
||||
: scheme.onSurface.withValues(alpha: 0.1),
|
||||
foregroundColor: i <= current
|
||||
? scheme.onPrimary
|
||||
: scheme.onSurface.withValues(alpha: 0.6),
|
||||
child: i < current
|
||||
? const Icon(Icons.check_rounded, size: 16)
|
||||
: Text(
|
||||
'${i + 1}',
|
||||
style: const TextStyle(
|
||||
fontSize: 13,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
labels[i],
|
||||
style: Theme.of(context).textTheme.labelSmall?.copyWith(
|
||||
color: i <= current
|
||||
? scheme.onSurface
|
||||
: scheme.onSurface.withValues(alpha: 0.5),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _UpperCaseFormatter extends TextInputFormatter {
|
||||
@override
|
||||
TextEditingValue formatEditUpdate(
|
||||
TextEditingValue oldValue,
|
||||
TextEditingValue newValue,
|
||||
) {
|
||||
return newValue.copyWith(text: newValue.text.toUpperCase());
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user