fix: misselanious updates

This commit is contained in:
2026-10-26 22:18:56 +01:00
parent 71af6f9ffe
commit 65e5e286b0
9 changed files with 477 additions and 233 deletions
+9
View File
@@ -319,6 +319,15 @@
"deviceRegistered": "Device registered!", "deviceRegistered": "Device registered!",
"openTabsWillSync": "Open tabs will sync to the dashboard.", "openTabsWillSync": "Open tabs will sync to the dashboard.",
"done": "Done", "done": "Done",
"stepVenue": "Venue",
"stepDevice": "Device",
"stepLinked": "Linked",
"noVenuesFound": "No venues found. Create one on the dashboard first.",
"changeVenue": "Change",
"deviceNameHint": "e.g. Bar terminal 1",
"registrationCodeHelper": "Find this code on the dashboard under devices.",
"venueNowLinked": "This device is now linked to {venue}.",
"@venueNowLinked": {"placeholders": {"venue": {"type": "String"}}},
"scanToPay": "Let the customer scan this code to pay {amount}", "scanToPay": "Let the customer scan this code to pay {amount}",
"@scanToPay": { "@scanToPay": {
"placeholders": { "placeholders": {
+48
View File
@@ -1880,6 +1880,54 @@ abstract class AppLocalizations {
/// **'Done'** /// **'Done'**
String get done; String get done;
/// No description provided for @stepVenue.
///
/// In en, this message translates to:
/// **'Venue'**
String get stepVenue;
/// No description provided for @stepDevice.
///
/// In en, this message translates to:
/// **'Device'**
String get stepDevice;
/// No description provided for @stepLinked.
///
/// In en, this message translates to:
/// **'Linked'**
String get stepLinked;
/// No description provided for @noVenuesFound.
///
/// In en, this message translates to:
/// **'No venues found. Create one on the dashboard first.'**
String get noVenuesFound;
/// No description provided for @changeVenue.
///
/// In en, this message translates to:
/// **'Change'**
String get changeVenue;
/// No description provided for @deviceNameHint.
///
/// In en, this message translates to:
/// **'e.g. Bar terminal 1'**
String get deviceNameHint;
/// No description provided for @registrationCodeHelper.
///
/// In en, this message translates to:
/// **'Find this code on the dashboard under devices.'**
String get registrationCodeHelper;
/// No description provided for @venueNowLinked.
///
/// In en, this message translates to:
/// **'This device is now linked to {venue}.'**
String venueNowLinked(String venue);
/// No description provided for @scanToPay. /// No description provided for @scanToPay.
/// ///
/// In en, this message translates to: /// In en, this message translates to:
+28
View File
@@ -977,6 +977,34 @@ class AppLocalizationsEn extends AppLocalizations {
@override @override
String get done => 'Done'; String get done => 'Done';
@override
String get stepVenue => 'Venue';
@override
String get stepDevice => 'Device';
@override
String get stepLinked => 'Linked';
@override
String get noVenuesFound =>
'No venues found. Create one on the dashboard first.';
@override
String get changeVenue => 'Change';
@override
String get deviceNameHint => 'e.g. Bar terminal 1';
@override
String get registrationCodeHelper =>
'Find this code on the dashboard under devices.';
@override
String venueNowLinked(String venue) {
return 'This device is now linked to $venue.';
}
@override @override
String scanToPay(String amount) { String scanToPay(String amount) {
return 'Let the customer scan this code to pay $amount'; return 'Let the customer scan this code to pay $amount';
+28
View File
@@ -990,6 +990,34 @@ class AppLocalizationsNl extends AppLocalizations {
@override @override
String get done => 'Klaar'; String get done => 'Klaar';
@override
String get stepVenue => 'Locatie';
@override
String get stepDevice => 'Toestel';
@override
String get stepLinked => 'Gekoppeld';
@override
String get noVenuesFound =>
'Geen locaties gevonden. Maak er eerst een aan op het dashboard.';
@override
String get changeVenue => 'Wijzigen';
@override
String get deviceNameHint => 'bv. Bartoestel 1';
@override
String get registrationCodeHelper =>
'Je vindt deze code op het dashboard bij toestellen.';
@override
String venueNowLinked(String venue) {
return 'Dit toestel is nu gekoppeld aan $venue.';
}
@override @override
String scanToPay(String amount) { String scanToPay(String amount) {
return 'Laat de klant deze code scannen om $amount te betalen'; return 'Laat de klant deze code scannen om $amount te betalen';
+8
View File
@@ -318,5 +318,13 @@
"deviceRegistered": "Toestel geregistreerd!", "deviceRegistered": "Toestel geregistreerd!",
"openTabsWillSync": "Open poefs worden gesynchroniseerd met het dashboard.", "openTabsWillSync": "Open poefs worden gesynchroniseerd met het dashboard.",
"done": "Klaar", "done": "Klaar",
"stepVenue": "Locatie",
"stepDevice": "Toestel",
"stepLinked": "Gekoppeld",
"noVenuesFound": "Geen locaties gevonden. Maak er eerst een aan op het dashboard.",
"changeVenue": "Wijzigen",
"deviceNameHint": "bv. Bartoestel 1",
"registrationCodeHelper": "Je vindt deze code op het dashboard bij toestellen.",
"venueNowLinked": "Dit toestel is nu gekoppeld aan {venue}.",
"scanToPay": "Laat de klant deze code scannen om {amount} te betalen" "scanToPay": "Laat de klant deze code scannen om {amount} te betalen"
} }
+8 -8
View File
@@ -34,11 +34,19 @@ class RegistrationViewModel extends ChangeNotifier {
void selectVenue(Venue venue) { void selectVenue(Venue venue) {
_selectedVenue = venue; _selectedVenue = venue;
_errorMessage = null;
notifyListeners();
}
void deselectVenue() {
_selectedVenue = null;
_errorMessage = null;
notifyListeners(); notifyListeners();
} }
Future<void> fetchVenues() async { Future<void> fetchVenues() async {
_step = RegistrationStep.loadingVenues; _step = RegistrationStep.loadingVenues;
_selectedVenue = null;
_errorMessage = null; _errorMessage = null;
notifyListeners(); notifyListeners();
@@ -96,12 +104,4 @@ class RegistrationViewModel extends ChangeNotifier {
} }
Future<void> retry() => fetchVenues(); Future<void> retry() => fetchVenues();
void reset() {
_step = RegistrationStep.initial;
_venues = [];
_selectedVenue = null;
_errorMessage = null;
notifyListeners();
}
} }
+1 -1
View File
@@ -183,7 +183,7 @@ class _PinEntryViewState extends State<PinEntryView> {
return Scaffold( return Scaffold(
body: SafeArea( body: SafeArea(
child: Padding( child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 24), padding: const EdgeInsets.fromLTRB(24, 16, 24, 0),
child: Column( child: Column(
children: [ children: [
Align( Align(
+346 -223
View File
@@ -1,4 +1,7 @@
import 'dart:async';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
import '../l10n/app_localizations.dart'; import '../l10n/app_localizations.dart';
@@ -6,6 +9,8 @@ import '../utils/navigation.dart';
import '../viewmodels/registration_view_model.dart'; import '../viewmodels/registration_view_model.dart';
import '../viewmodels/remote_sync_view_model.dart'; import '../viewmodels/remote_sync_view_model.dart';
const _codeLength = 6;
class RegistrationView extends StatefulWidget { class RegistrationView extends StatefulWidget {
const RegistrationView({super.key}); const RegistrationView({super.key});
@@ -26,8 +31,6 @@ class _RegistrationViewState extends State<RegistrationView> {
}); });
} }
AppLocalizations get _l10n => AppLocalizations.of(context);
@override @override
void dispose() { void dispose() {
_deviceNameController.dispose(); _deviceNameController.dispose();
@@ -35,6 +38,23 @@ class _RegistrationViewState extends State<RegistrationView> {
super.dispose(); 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 @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final vm = context.watch<RegistrationViewModel>(); final vm = context.watch<RegistrationViewModel>();
@@ -44,11 +64,17 @@ class _RegistrationViewState extends State<RegistrationView> {
backgroundColor: Theme.of(context).colorScheme.surface, backgroundColor: Theme.of(context).colorScheme.surface,
appBar: AppBar( appBar: AppBar(
backgroundColor: Colors.transparent, backgroundColor: Colors.transparent,
leading: IconButton( title: Text(l10n.connectToRemoteServer),
tooltip: l10n.cancel, leading: vm.step == RegistrationStep.registered
onPressed: () => context.popOrGo('/admin'), ? null
icon: const Icon(Icons.close_rounded), : IconButton(
), tooltip: l10n.cancel,
onPressed: vm.step == RegistrationStep.registering
? null
: () => context.popOrGo('/admin'),
icon: const Icon(Icons.close_rounded),
),
automaticallyImplyLeading: false,
), ),
body: SafeArea( body: SafeArea(
child: Center( child: Center(
@@ -56,7 +82,15 @@ class _RegistrationViewState extends State<RegistrationView> {
padding: const EdgeInsets.symmetric(horizontal: 32, vertical: 24), padding: const EdgeInsets.symmetric(horizontal: 32, vertical: 24),
child: ConstrainedBox( child: ConstrainedBox(
constraints: const BoxConstraints(maxWidth: 480), 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) { Widget _buildStep(RegistrationViewModel vm, AppLocalizations l10n) {
switch (vm.step) { switch (vm.step) {
case RegistrationStep.initial:
case RegistrationStep.loadingVenues: case RegistrationStep.loadingVenues:
return _buildLoading(); return _buildLoading(l10n);
case RegistrationStep.error: case RegistrationStep.error:
return _buildError(vm); return _buildError(vm, l10n);
case RegistrationStep.venuesLoaded: case RegistrationStep.venuesLoaded:
case RegistrationStep.registering:
if (vm.selectedVenue == null) { if (vm.selectedVenue == null) {
return _buildVenueSelection(vm, l10n); return _buildVenueSelection(vm, l10n);
} }
return _buildDeviceDetails(vm, l10n); return _buildDeviceDetails(vm, l10n);
case RegistrationStep.registering:
return _buildRegistering();
case RegistrationStep.registered: case RegistrationStep.registered:
return _buildSuccess(vm, l10n); return _buildSuccess(vm, l10n);
case RegistrationStep.initial:
return _buildLoading();
} }
} }
Widget _buildLoading() { Widget _buildLoading(AppLocalizations l10n) {
return Column( return Column(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
children: [ children: [
const SizedBox(height: 60), const SizedBox(height: 40),
Icon(
Icons.cloud_outlined,
size: 64,
color: Theme.of(context).colorScheme.primary.withValues(alpha: 0.5),
),
const SizedBox(height: 24),
const CircularProgressIndicator(strokeWidth: 3), const CircularProgressIndicator(strokeWidth: 3),
const SizedBox(height: 16), const SizedBox(height: 16),
Text( Text(
_l10n.connectingToServer, l10n.connectingToServer,
style: Theme.of(context).textTheme.titleMedium, style: Theme.of(context).textTheme.titleMedium,
), ),
], ],
); );
} }
Widget _buildError(RegistrationViewModel vm) { Widget _buildError(RegistrationViewModel vm, AppLocalizations l10n) {
final scheme = Theme.of(context).colorScheme;
return Column( return Column(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
children: [ children: [
const SizedBox(height: 60), Icon(Icons.cloud_off_rounded, size: 56, color: scheme.error),
Icon( const SizedBox(height: 16),
Icons.cloud_off_rounded,
size: 64,
color: Theme.of(context).colorScheme.error,
),
const SizedBox(height: 24),
Text( Text(
_l10n.failedToConnect, l10n.failedToConnect,
style: Theme.of(context).textTheme.titleLarge, style: Theme.of(context).textTheme.titleLarge,
), ),
const SizedBox(height: 12), const SizedBox(height: 8),
Text( Text(
vm.errorMessage ?? _l10n.unknownError, vm.errorMessage ?? l10n.unknownError,
textAlign: TextAlign.center, textAlign: TextAlign.center,
style: Theme.of(context).textTheme.bodyMedium?.copyWith( style: Theme.of(context).textTheme.bodyMedium?.copyWith(
color: Theme.of( color: scheme.onSurface.withValues(alpha: 0.6),
context,
).colorScheme.onSurface.withValues(alpha: 0.6),
), ),
), ),
const SizedBox(height: 24), const SizedBox(height: 24),
FilledButton.icon( FilledButton.icon(
onPressed: () => vm.retry(), onPressed: vm.retry,
icon: const Icon(Icons.refresh), icon: const Icon(Icons.refresh),
label: Text(_l10n.retry), label: Text(l10n.retry),
), ),
], ],
); );
} }
Widget _buildVenueSelection(RegistrationViewModel vm, AppLocalizations l10n) { Widget _buildVenueSelection(RegistrationViewModel vm, AppLocalizations l10n) {
final scheme = Theme.of(context).colorScheme;
return Column( return Column(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.stretch, crossAxisAlignment: CrossAxisAlignment.stretch,
children: [ 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( Text(
_l10n.connectToRemoteServer, l10n.selectVenue,
textAlign: TextAlign.center,
style: Theme.of(context).textTheme.headlineSmall, style: Theme.of(context).textTheme.headlineSmall,
), ),
const SizedBox(height: 8), const SizedBox(height: 4),
Text( Text(
_l10n.selectVenueToLink, l10n.selectVenueToLink,
textAlign: TextAlign.center,
style: Theme.of(context).textTheme.bodyMedium?.copyWith( style: Theme.of(context).textTheme.bodyMedium?.copyWith(
color: Theme.of( color: scheme.onSurface.withValues(alpha: 0.6),
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),
),
],
),
),
),
),
],
), ),
), ),
const SizedBox(height: 24), const SizedBox(height: 24),
TextButton( if (vm.venues.isEmpty)
onPressed: () => vm.retry(), Container(
child: Text(_l10n.refreshVenues), 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) { Widget _buildDeviceDetails(RegistrationViewModel vm, AppLocalizations l10n) {
final scheme = Theme.of(context).colorScheme;
final isBusy = vm.step == RegistrationStep.registering;
return Form( return Form(
key: _formKey, key: _formKey,
child: Column( child: Column(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.stretch, crossAxisAlignment: CrossAxisAlignment.stretch,
children: [ children: [
const SizedBox(height: 40), Container(
Icon( padding: const EdgeInsets.fromLTRB(16, 8, 8, 8),
Icons.store_outlined, decoration: BoxDecoration(
size: 48, color: scheme.primaryContainer.withValues(alpha: 0.5),
color: Theme.of(context).colorScheme.primary.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), 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( TextFormField(
controller: _deviceNameController, controller: _deviceNameController,
enabled: !isBusy,
textInputAction: TextInputAction.next,
decoration: InputDecoration( decoration: InputDecoration(
labelText: _l10n.deviceName, labelText: l10n.deviceName,
hintText: 'e.g. Bar terminal 1', hintText: l10n.deviceNameHint,
border: OutlineInputBorder(), prefixIcon: const Icon(Icons.tablet_android_outlined),
border: const OutlineInputBorder(),
), ),
validator: (v) => validator: (v) =>
v == null || v.trim().isEmpty ? _l10n.deviceNameRequired : null, v == null || v.trim().isEmpty ? l10n.deviceNameRequired : null,
), ),
const SizedBox(height: 16), const SizedBox(height: 16),
TextFormField( TextFormField(
controller: _regCodeController, controller: _regCodeController,
decoration: InputDecoration( enabled: !isBusy,
labelText: _l10n.registrationCode, autofocus: true,
hintText: '6-character code from dashboard', maxLength: _codeLength,
border: OutlineInputBorder(), textAlign: TextAlign.center,
counterText: '',
),
maxLength: 6,
textCapitalization: TextCapitalization.characters, textCapitalization: TextCapitalization.characters,
validator: (v) => v == null || v.trim().length < 6 textInputAction: TextInputAction.done,
? _l10n.enterRegistrationCode 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, : null,
), ),
const SizedBox(height: 8), if (vm.errorMessage != null) ...[
if (vm.errorMessage != null) const SizedBox(height: 8),
Padding( Container(
padding: const EdgeInsets.only(bottom: 8), padding: const EdgeInsets.all(12),
child: Text( decoration: BoxDecoration(
vm.errorMessage!, color: scheme.errorContainer,
style: TextStyle( borderRadius: BorderRadius.circular(12),
color: Theme.of(context).colorScheme.error, ),
fontSize: 13, 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( const SizedBox(height: 24),
children: [ FilledButton(
TextButton( style: FilledButton.styleFrom(
onPressed: () { minimumSize: const Size.fromHeight(52),
vm.reset(); ),
}, onPressed: isBusy ? null : () => _register(vm),
child: Text(_l10n.back), child: isBusy
), ? Row(
const SizedBox(width: 12), mainAxisSize: MainAxisSize.min,
Expanded( children: [
child: FilledButton( const SizedBox(
onPressed: () async { width: 18,
if (!_formKey.currentState!.validate()) return; height: 18,
await vm.registerDevice( child: CircularProgressIndicator(strokeWidth: 2),
deviceName: _deviceNameController.text.trim(), ),
registrationCode: _regCodeController.text const SizedBox(width: 12),
.trim() Text(l10n.registeringDevice),
.toUpperCase(), ],
); )
}, : Text(l10n.register),
child: 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) { Widget _buildSuccess(RegistrationViewModel vm, AppLocalizations l10n) {
final scheme = Theme.of(context).colorScheme;
return Column( return Column(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [ children: [
const SizedBox(height: 40), Center(
Icon( child: Container(
Icons.check_circle_rounded, padding: const EdgeInsets.all(20),
size: 80, decoration: BoxDecoration(
color: Theme.of(context).colorScheme.primary, color: scheme.primaryContainer,
shape: BoxShape.circle,
),
child: Icon(
Icons.check_rounded,
size: 56,
color: scheme.onPrimaryContainer,
),
),
), ),
const SizedBox(height: 24), const SizedBox(height: 24),
Text( Text(
_l10n.deviceRegistered, l10n.deviceRegistered,
textAlign: TextAlign.center, textAlign: TextAlign.center,
style: Theme.of(context).textTheme.headlineSmall, 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), const SizedBox(height: 8),
Text( Text(
_l10n.openTabsWillSync, l10n.venueNowLinked(vm.selectedVenue?.name ?? ''),
textAlign: TextAlign.center, textAlign: TextAlign.center,
style: Theme.of(context).textTheme.bodySmall?.copyWith( style: Theme.of(context).textTheme.bodyLarge,
color: Theme.of( ),
context, const SizedBox(height: 4),
).colorScheme.onSurface.withValues(alpha: 0.5), Text(
l10n.openTabsWillSync,
textAlign: TextAlign.center,
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
color: scheme.onSurface.withValues(alpha: 0.6),
), ),
), ),
const SizedBox(height: 32), const SizedBox(height: 32),
FilledButton.icon( FilledButton.icon(
// Once configured, the router redirects to /bar by itself. style: FilledButton.styleFrom(
onPressed: () => context.read<RemoteSyncViewModel>().activate(), minimumSize: const Size.fromHeight(52),
),
onPressed: _finish,
icon: const Icon(Icons.arrow_forward), 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());
}
}
+1 -1
View File
@@ -2,7 +2,7 @@ name: kooltap
description: "KoolTap: a tablet point-of-sale app for running bar tabs." description: "KoolTap: a tablet point-of-sale app for running bar tabs."
publish_to: 'none' publish_to: 'none'
version: 1.3.8+0 version: 1.3.9+0
environment: environment:
sdk: ^3.12.2 sdk: ^3.12.2