feat: Refactor client hubs to centralize hub actions and update UI styling.

This commit is contained in:
Achintha Isuru
2026-02-24 13:59:55 -05:00
parent e78d5938dd
commit e084dad4a7
3 changed files with 178 additions and 124 deletions

View File

@@ -8,6 +8,7 @@ class UiAppBar extends StatelessWidget implements PreferredSizeWidget {
const UiAppBar({ const UiAppBar({
super.key, super.key,
this.title, this.title,
this.subtitle,
this.titleWidget, this.titleWidget,
this.leading, this.leading,
this.actions, this.actions,
@@ -21,6 +22,9 @@ class UiAppBar extends StatelessWidget implements PreferredSizeWidget {
/// The title text to display in the app bar. /// The title text to display in the app bar.
final String? title; final String? title;
/// The subtitle text to display in the app bar.
final String? subtitle;
/// A widget to display instead of the title text. /// A widget to display instead of the title text.
final Widget? titleWidget; final Widget? titleWidget;
@@ -53,7 +57,19 @@ class UiAppBar extends StatelessWidget implements PreferredSizeWidget {
return AppBar( return AppBar(
title: title:
titleWidget ?? titleWidget ??
(title != null ? Text(title!, style: UiTypography.headline4b) : null), (title != null
? Column(
crossAxisAlignment: centerTitle
? CrossAxisAlignment.center
: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: <Widget>[
Text(title!, style: UiTypography.headline4b),
if (subtitle != null)
Text(subtitle!, style: UiTypography.body3r.textSecondary),
],
)
: null),
leading: leading:
leading ?? leading ??
(showBackButton (showBackButton

View File

@@ -35,6 +35,10 @@ class _EditHubPageState extends State<EditHubPage> {
_nameController = TextEditingController(text: widget.hub?.name); _nameController = TextEditingController(text: widget.hub?.name);
_addressController = TextEditingController(text: widget.hub?.address); _addressController = TextEditingController(text: widget.hub?.address);
_addressFocusNode = FocusNode(); _addressFocusNode = FocusNode();
// Update header on change
_nameController.addListener(() => setState(() {}));
_addressController.addListener(() => setState(() {}));
} }
@override @override
@@ -115,84 +119,79 @@ class _EditHubPageState extends State<EditHubPage> {
return Scaffold( return Scaffold(
backgroundColor: UiColors.bgMenu, backgroundColor: UiColors.bgMenu,
appBar: AppBar( appBar: UiAppBar(
backgroundColor: UiColors.foreground, title: widget.hub == null
leading: IconButton( ? t.client_hubs.add_hub_dialog.title
icon: const Icon(UiIcons.arrowLeft, color: UiColors.white), : t.client_hubs.edit_hub.title,
onPressed: () => Modular.to.pop(), subtitle: widget.hub == null
), ? t.client_hubs.add_hub_dialog.create_button
title: Column( : t.client_hubs.edit_hub.subtitle,
crossAxisAlignment: CrossAxisAlignment.start, onLeadingPressed: () => Modular.to.pop(),
children: <Widget>[
Text(
widget.hub == null
? t.client_hubs.add_hub_dialog.title
: t.client_hubs.edit_hub.title,
style: UiTypography.headline3m.white,
),
Text(
widget.hub == null
? t.client_hubs.add_hub_dialog.create_button
: t.client_hubs.edit_hub.subtitle,
style: UiTypography.footnote1r.copyWith(
color: UiColors.white.withValues(alpha: 0.7),
),
),
],
),
), ),
body: Stack( body: Stack(
children: <Widget>[ children: <Widget>[
SingleChildScrollView( SingleChildScrollView(
padding: const EdgeInsets.all(UiConstants.space5), child: Column(
child: Form( crossAxisAlignment: CrossAxisAlignment.stretch,
key: _formKey, children: <Widget>[
child: Column( Padding(
crossAxisAlignment: CrossAxisAlignment.stretch, padding: const EdgeInsets.all(UiConstants.space5),
children: <Widget>[ child: Form(
// ── Name field ────────────────────────────────── key: _formKey,
_FieldLabel(t.client_hubs.edit_hub.name_label), child: Column(
TextFormField( crossAxisAlignment: CrossAxisAlignment.stretch,
controller: _nameController, children: <Widget>[
style: UiTypography.body1r.textPrimary, // ── Name field ──────────────────────────────────
textInputAction: TextInputAction.next, _FieldLabel(t.client_hubs.edit_hub.name_label),
validator: (String? value) { TextFormField(
if (value == null || value.trim().isEmpty) { controller: _nameController,
return 'Name is required'; style: UiTypography.body1r.textPrimary,
} textInputAction: TextInputAction.next,
return null; validator: (String? value) {
}, if (value == null || value.trim().isEmpty) {
decoration: _inputDecoration( return 'Name is required';
t.client_hubs.edit_hub.name_hint, }
return null;
},
decoration: _inputDecoration(
t.client_hubs.edit_hub.name_hint,
),
),
const SizedBox(height: UiConstants.space4),
// ── Address field ────────────────────────────────
_FieldLabel(
t.client_hubs.edit_hub.address_label,
),
HubAddressAutocomplete(
controller: _addressController,
hintText: t.client_hubs.edit_hub.address_hint,
focusNode: _addressFocusNode,
onSelected: (Prediction prediction) {
_selectedPrediction = prediction;
},
),
const SizedBox(height: UiConstants.space8),
// ── Save button ──────────────────────────────────
UiButton.primary(
onPressed: isSaving ? null : _onSave,
text: widget.hub == null
? t
.client_hubs
.add_hub_dialog
.create_button
: t.client_hubs.edit_hub.save_button,
),
const SizedBox(height: 40),
],
), ),
), ),
),
const SizedBox(height: UiConstants.space4), ],
// ── Address field ────────────────────────────────
_FieldLabel(t.client_hubs.edit_hub.address_label),
HubAddressAutocomplete(
controller: _addressController,
hintText: t.client_hubs.edit_hub.address_hint,
focusNode: _addressFocusNode,
onSelected: (Prediction prediction) {
_selectedPrediction = prediction;
},
),
const SizedBox(height: UiConstants.space8),
// ── Save button ──────────────────────────────────
UiButton.primary(
onPressed: isSaving ? null : _onSave,
text: widget.hub == null
? t.client_hubs.add_hub_dialog.create_button
: t.client_hubs.edit_hub.save_button,
),
const SizedBox(height: 40),
],
),
), ),
), ),

View File

@@ -43,57 +43,105 @@ class HubDetailsPage extends StatelessWidget {
} }
}, },
child: Scaffold( child: Scaffold(
appBar: AppBar( appBar: UiAppBar(
title: Text(hub.name), title: hub.name,
backgroundColor: UiColors.foreground, subtitle: t.client_hubs.hub_details.title,
leading: IconButton( onLeadingPressed: () => Modular.to.pop(),
icon: const Icon(UiIcons.arrowLeft, color: UiColors.white),
onPressed: () => Modular.to.pop(),
),
actions: <Widget>[ actions: <Widget>[
IconButton( IconButton(
onPressed: () => _confirmDeleteHub(context), onPressed: () => _confirmDeleteHub(context),
icon: const Icon( icon: const Icon(UiIcons.delete, color: UiColors.iconSecondary),
UiIcons.delete,
color: UiColors.white,
size: 20,
),
), ),
TextButton.icon( UiIconButton(
onPressed: () => _navigateToEditPage(context), icon: UiIcons.edit,
icon: const Icon(UiIcons.edit, color: UiColors.white, size: 16), onTap: () => _navigateToEditPage(context),
label: Text( backgroundColor: UiColors.transparent,
t.client_hubs.hub_details.edit_button, iconColor: UiColors.iconSecondary,
style: const TextStyle(color: UiColors.white),
),
), ),
], ],
), ),
backgroundColor: UiColors.bgMenu, backgroundColor: UiColors.bgMenu,
body: Padding( body: SingleChildScrollView(
padding: const EdgeInsets.all(UiConstants.space5),
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.stretch,
children: <Widget>[ children: <Widget>[
_buildDetailItem( // ── Header ──────────────────────────────────────────
label: t.client_hubs.hub_details.name_label, Padding(
value: hub.name, padding: const EdgeInsets.all(UiConstants.space5),
icon: UiIcons.home, child: IntrinsicHeight(
child: Row(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: <Widget>[
Container(
width: 114,
decoration: BoxDecoration(
color: UiColors.primary.withValues(alpha: 0.08),
borderRadius: BorderRadius.circular(
UiConstants.radiusBase,
),
border: Border.all(color: UiColors.primary),
),
child: const Center(
child: Icon(
UiIcons.nfc,
color: UiColors.primary,
size: 32,
),
),
),
const SizedBox(width: UiConstants.space4),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Text(
hub.name,
style: UiTypography.headline1b.textPrimary,
),
const SizedBox(height: UiConstants.space1),
Row(
children: <Widget>[
const Icon(
UiIcons.mapPin,
size: 16,
color: UiColors.textSecondary,
),
const SizedBox(width: UiConstants.space1),
Expanded(
child: Text(
hub.address,
style: UiTypography.body2r.textSecondary,
maxLines: 2,
overflow: TextOverflow.ellipsis,
),
),
],
),
],
),
),
],
),
),
), ),
const SizedBox(height: UiConstants.space4), const Divider(height: 1, thickness: 0.5),
_buildDetailItem(
label: t.client_hubs.hub_details.address_label, Padding(
value: hub.address, padding: const EdgeInsets.all(UiConstants.space5),
icon: UiIcons.mapPin, child: Column(
), crossAxisAlignment: CrossAxisAlignment.stretch,
const SizedBox(height: UiConstants.space4), children: <Widget>[
_buildDetailItem( _buildDetailItem(
label: t.client_hubs.hub_details.nfc_label, label: t.client_hubs.hub_details.nfc_label,
value: value:
hub.nfcTagId ?? hub.nfcTagId ??
t.client_hubs.hub_details.nfc_not_assigned, t.client_hubs.hub_details.nfc_not_assigned,
icon: UiIcons.nfc, icon: UiIcons.nfc,
isHighlight: hub.nfcTagId != null, isHighlight: hub.nfcTagId != null,
),
],
),
), ),
], ],
), ),
@@ -114,13 +162,7 @@ class HubDetailsPage extends StatelessWidget {
decoration: BoxDecoration( decoration: BoxDecoration(
color: UiColors.white, color: UiColors.white,
borderRadius: BorderRadius.circular(UiConstants.radiusBase), borderRadius: BorderRadius.circular(UiConstants.radiusBase),
boxShadow: const <BoxShadow>[ border: Border.all(color: UiColors.border),
BoxShadow(
color: UiColors.popupShadow,
blurRadius: 10,
offset: Offset(0, 4),
),
],
), ),
child: Row( child: Row(
children: <Widget>[ children: <Widget>[
@@ -134,7 +176,7 @@ class HubDetailsPage extends StatelessWidget {
), ),
child: Icon( child: Icon(
icon, icon,
color: isHighlight ? UiColors.iconSuccess : UiColors.iconPrimary, color: isHighlight ? UiColors.iconSuccess : UiColors.iconThird,
size: 20, size: 20,
), ),
), ),
@@ -155,9 +197,6 @@ class HubDetailsPage extends StatelessWidget {
} }
Future<void> _navigateToEditPage(BuildContext context) async { Future<void> _navigateToEditPage(BuildContext context) async {
// We still need to pass a Bloc for the edit page, but it's handled by Modular.
// However, the Navigator extension expect a Bloc.
// I'll update the Navigator extension to NOT require a Bloc since it's in Modular.
final bool? saved = await Modular.to.toEditHub(hub: hub); final bool? saved = await Modular.to.toEditHub(hub: hub);
if (saved == true && context.mounted) { if (saved == true && context.mounted) {
Modular.to.pop(true); // Return true to indicate change Modular.to.pop(true); // Return true to indicate change