From 177f4d2b84ab3925b16c0ba170b21d7cdcb387f2 Mon Sep 17 00:00:00 2001 From: NC1107 Date: Thu, 21 May 2026 23:27:16 -0400 Subject: [PATCH] refactor(client): introduce SettingsListTile and migrate 5 callsites Centralises the standard settings-row shape (icon + title + optional subtitle + chevron-or-custom-trailing) so each section stops re-declaring contentPadding: EdgeInsets.zero, the muted-icon size, theme-aware text styles, and the chevron trailing. Migrates 4 sites in about_section and 1 in data_storage_section. Other callsites with bespoke trailing widgets or stack-based leading remain untouched for now. --- .../src/screens/settings/about_section.dart | 93 ++++--------------- .../settings/data_storage_section.dart | 16 +--- .../widgets/settings/settings_list_tile.dart | 74 +++++++++++++++ 3 files changed, 97 insertions(+), 86 deletions(-) create mode 100644 apps/client/lib/src/widgets/settings/settings_list_tile.dart diff --git a/apps/client/lib/src/screens/settings/about_section.dart b/apps/client/lib/src/screens/settings/about_section.dart index 694bbb45..0247d68a 100644 --- a/apps/client/lib/src/screens/settings/about_section.dart +++ b/apps/client/lib/src/screens/settings/about_section.dart @@ -23,6 +23,7 @@ import '../../theme/echo_theme.dart'; import '../../version.dart'; import '../../widgets/confirm_dialog.dart'; import '../../widgets/feedback_dialog.dart'; +import '../../widgets/settings/settings_list_tile.dart'; import '../../widgets/input_dialog.dart'; class AboutSection extends ConsumerStatefulWidget { @@ -554,52 +555,20 @@ class _AboutSectionState extends ConsumerState { const SizedBox(height: 12), _buildServersList(), const SizedBox(height: 8), - ListTile( - contentPadding: EdgeInsets.zero, - leading: Icon( - Icons.add_circle_outline, - color: context.textSecondary, - size: 22, - ), - title: Text( - 'Add server', - style: TextStyle(color: context.textPrimary, fontSize: 15), - ), - subtitle: Text( - 'Verifies the URL before adding it to your list.', - style: TextStyle(color: context.textMuted, fontSize: 12), - ), - trailing: Icon( - Icons.chevron_right, - color: context.textMuted, - size: 20, - ), + SettingsListTile( + icon: Icons.add_circle_outline, + title: 'Add server', + subtitle: 'Verifies the URL before adding it to your list.', onTap: _showAddServerDialog, ), const SizedBox(height: 16), Divider(color: context.border), const SizedBox(height: 16), // Debug logs entry (absorbed from former Debug section). - ListTile( - contentPadding: EdgeInsets.zero, - leading: Icon( - Icons.bug_report_outlined, - color: context.textSecondary, - size: 22, - ), - title: Text( - 'Debug Logs', - style: TextStyle(color: context.textPrimary, fontSize: 15), - ), - subtitle: Text( - 'View recent in-app log entries.', - style: TextStyle(color: context.textMuted, fontSize: 12), - ), - trailing: Icon( - Icons.chevron_right, - color: context.textMuted, - size: 20, - ), + SettingsListTile( + icon: Icons.bug_report_outlined, + title: 'Debug Logs', + subtitle: 'View recent in-app log entries.', onTap: () { Navigator.of(context).push( MaterialPageRoute( @@ -611,47 +580,21 @@ class _AboutSectionState extends ConsumerState { const SizedBox(height: 8), // Beta-prep #4c: surface the feedback dialog from About so testers // have a one-tap path to report issues without leaving the app. - ListTile( + SettingsListTile( key: const Key('about-send-feedback'), - contentPadding: EdgeInsets.zero, - leading: Icon( - Icons.feedback_outlined, - color: context.textSecondary, - size: 22, - ), - title: Text( - 'Send feedback', - style: TextStyle(color: context.textPrimary, fontSize: 15), - ), - subtitle: Text( - 'Report a bug or share a suggestion.', - style: TextStyle(color: context.textMuted, fontSize: 12), - ), - trailing: Icon( - Icons.chevron_right, - color: context.textMuted, - size: 20, - ), + icon: Icons.feedback_outlined, + title: 'Send feedback', + subtitle: 'Report a bug or share a suggestion.', onTap: () => showFeedbackDialog(context), ), // Privacy link — opens the GitHub-rendered docs/PRIVACY.md so the // canonical text isn't bundled in every app build. - ListTile( + SettingsListTile( key: const Key('about-privacy-link'), - contentPadding: EdgeInsets.zero, - leading: Icon( - Icons.privacy_tip_outlined, - color: context.textSecondary, - size: 22, - ), - title: Text( - 'Privacy', - style: TextStyle(color: context.textPrimary, fontSize: 15), - ), - subtitle: Text( - 'What Echo stores, what it does not, and where the data lives.', - style: TextStyle(color: context.textMuted, fontSize: 12), - ), + icon: Icons.privacy_tip_outlined, + title: 'Privacy', + subtitle: + 'What Echo stores, what it does not, and where the data lives.', trailing: Icon( Icons.open_in_new, color: context.textMuted, diff --git a/apps/client/lib/src/screens/settings/data_storage_section.dart b/apps/client/lib/src/screens/settings/data_storage_section.dart index 8bf0f91b..5796cb0d 100644 --- a/apps/client/lib/src/screens/settings/data_storage_section.dart +++ b/apps/client/lib/src/screens/settings/data_storage_section.dart @@ -10,6 +10,7 @@ import '../../services/message_cache.dart'; import '../../services/toast_service.dart'; import '../../theme/echo_theme.dart'; import '../../widgets/confirm_dialog.dart'; +import '../../widgets/settings/settings_list_tile.dart'; class DataStorageSection extends ConsumerStatefulWidget { const DataStorageSection({super.key}); @@ -155,17 +156,10 @@ class _DataStorageSectionState extends ConsumerState { ), const Divider(height: 24), // Cache size - ListTile( - contentPadding: EdgeInsets.zero, - leading: Icon(Icons.storage, color: context.textSecondary), - title: Text( - 'Message Cache', - style: TextStyle(color: context.textPrimary, fontSize: 14), - ), - subtitle: Text( - 'Estimated size: $_cacheSize', - style: TextStyle(color: context.textMuted, fontSize: 12), - ), + SettingsListTile( + icon: Icons.storage, + title: 'Message Cache', + subtitle: 'Estimated size: $_cacheSize', trailing: OutlinedButton( onPressed: _clearMessageCache, child: const Text('Clear'), diff --git a/apps/client/lib/src/widgets/settings/settings_list_tile.dart b/apps/client/lib/src/widgets/settings/settings_list_tile.dart new file mode 100644 index 00000000..8154e9a7 --- /dev/null +++ b/apps/client/lib/src/widgets/settings/settings_list_tile.dart @@ -0,0 +1,74 @@ +import 'package:flutter/material.dart'; + +import '../../theme/echo_theme.dart'; + +/// Standardised settings row: leading icon + title + optional subtitle + +/// optional trailing widget (defaults to a chevron when [onTap] is set). +/// +/// Use this instead of building a raw [ListTile] with `contentPadding: EdgeInsets.zero`, +/// muted icon and chevron, theme-aware text styles, etc. Every settings +/// section in the app uses the same recipe; this widget bakes it in so +/// callers stay focused on what the row does. +/// +/// For card-style rows with a colored icon badge (the redesigned settings +/// home list) use [CardRow] from `widgets/settings/card_row.dart` instead. +class SettingsListTile extends StatelessWidget { + /// Leading icon. + final IconData icon; + + /// Primary row label. + final String title; + + /// Optional supporting line shown under the title in muted text. + final String? subtitle; + + /// Optional trailing widget. When omitted AND [onTap] is non-null, + /// renders a chevron-right; when both are omitted, nothing trails the row. + final Widget? trailing; + + /// Tap handler. When null the row renders without a chevron and is inert. + final VoidCallback? onTap; + + /// When true, renders the row in destructive style: danger color on icon + /// and title, no default chevron. + final bool destructive; + + const SettingsListTile({ + super.key, + required this.icon, + required this.title, + this.subtitle, + this.trailing, + this.onTap, + this.destructive = false, + }); + + @override + Widget build(BuildContext context) { + final iconColor = destructive ? EchoTheme.danger : context.textSecondary; + final titleColor = destructive ? EchoTheme.danger : context.textPrimary; + + Widget? resolvedTrailing = trailing; + if (resolvedTrailing == null && onTap != null && !destructive) { + resolvedTrailing = Icon( + Icons.chevron_right, + color: context.textMuted, + size: 20, + ); + } + + return ListTile( + contentPadding: EdgeInsets.zero, + leading: Icon(icon, color: iconColor, size: 22), + title: Text(title, style: TextStyle(color: titleColor, fontSize: 15)), + subtitle: subtitle == null + ? null + : Text( + subtitle!, + style: TextStyle(color: context.textMuted, fontSize: 12), + ), + trailing: resolvedTrailing, + onTap: onTap, + ); + } +}