feat: Refactor client hubs to centralize hub actions and update UI styling.
This commit is contained in:
@@ -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
|
||||||
|
|||||||
@@ -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),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user