From c5279dca72a45bffff8920ff7debffdc288cc06f Mon Sep 17 00:00:00 2001 From: MdSaifAliMolla <145194907+MdSaifAliMolla@users.noreply.github.com> Date: Sun, 2 Nov 2025 02:38:31 +0530 Subject: [PATCH 1/4] settings: Improved header-textstyle --- lib/widgets/settings.dart | 24 +++++++++++++++++++++--- 1 file changed, 21 insertions(+), 3 deletions(-) diff --git a/lib/widgets/settings.dart b/lib/widgets/settings.dart index bad16c336b..74e5acf169 100644 --- a/lib/widgets/settings.dart +++ b/lib/widgets/settings.dart @@ -5,6 +5,7 @@ import '../model/settings.dart'; import 'app_bar.dart'; import 'page.dart'; import 'store.dart'; +import 'text.dart'; class SettingsPage extends StatelessWidget { const SettingsPage({super.key}); @@ -18,9 +19,9 @@ class SettingsPage extends StatelessWidget { Widget build(BuildContext context) { final zulipLocalizations = ZulipLocalizations.of(context); return Scaffold( - appBar: ZulipAppBar( - title: Text(zulipLocalizations.settingsPageTitle)), + appBar: ZulipAppBar(title: Text(zulipLocalizations.settingsPageTitle),centerTitle: true), body: ListView(children: [ + _SettingsHeader(title: ZulipLocalizations.of(context).themeSettingTitle), const _ThemeSetting(), const _BrowserPreferenceSetting(), const _VisitFirstUnreadSetting(), @@ -34,6 +35,23 @@ class SettingsPage extends StatelessWidget { } } +class _SettingsHeader extends StatelessWidget { + const _SettingsHeader({required this.title}); + + final String title; + + @override + Widget build(BuildContext context) { + return Material( + color: Colors.transparent, + child: Padding( + padding: const EdgeInsets.fromLTRB(16, 16, 16,8), + child: Text(title, + style: TextStyle( + fontSize: 17).merge(weightVariableTextStyle(context, wght: 600)), + ))); + }} + class _ThemeSetting extends StatelessWidget { const _ThemeSetting(); @@ -51,7 +69,7 @@ class _ThemeSetting extends StatelessWidget { onChanged: (newValue) => _handleChange(context, newValue), child: Column( children: [ - ListTile(title: Text(zulipLocalizations.themeSettingTitle)), + //ListTile(title: Text(zulipLocalizations.themeSettingTitle)), for (final themeSettingOption in [null, ...ThemeSetting.values]) RadioListTile.adaptive( title: Text(ThemeSetting.displayName( From 0d329504e7aa74783481aaa1807f7b494efda220 Mon Sep 17 00:00:00 2001 From: MdSaifAliMolla <145194907+MdSaifAliMolla@users.noreply.github.com> Date: Sun, 2 Nov 2025 03:16:58 +0530 Subject: [PATCH 2/4] settings: modular settings navigation item Added to maintain consistent style and reuse the components enhancing modularity. --- lib/widgets/settings.dart | 95 ++++++++++++++++++++++++--------------- 1 file changed, 58 insertions(+), 37 deletions(-) diff --git a/lib/widgets/settings.dart b/lib/widgets/settings.dart index 74e5acf169..8ca792d3da 100644 --- a/lib/widgets/settings.dart +++ b/lib/widgets/settings.dart @@ -3,9 +3,11 @@ import 'package:flutter/material.dart'; import '../generated/l10n/zulip_localizations.dart'; import '../model/settings.dart'; import 'app_bar.dart'; +import 'icons.dart'; import 'page.dart'; import 'store.dart'; import 'text.dart'; +import 'theme.dart'; class SettingsPage extends StatelessWidget { const SettingsPage({super.key}); @@ -23,12 +25,31 @@ class SettingsPage extends StatelessWidget { body: ListView(children: [ _SettingsHeader(title: ZulipLocalizations.of(context).themeSettingTitle), const _ThemeSetting(), - const _BrowserPreferenceSetting(), - const _VisitFirstUnreadSetting(), - const _MarkReadOnScrollSetting(), + _BrowserPreferenceSetting(), + _SettingsNavitem( + title: 'Notifications', + onTap: () { + // TODO: Implement notifications settings page + }), + _SettingsNavitem( + title: 'Open message feeds at', + subtitle: VisitFirstUnreadSettingPage._valueDisplayName( + GlobalStoreWidget.settingsOf(context).visitFirstUnread, + zulipLocalizations: zulipLocalizations), + onTap: () => Navigator.push(context, + VisitFirstUnreadSettingPage.buildRoute()), + ), + _SettingsNavitem( + title: 'Mark messages as read on scroll', + subtitle: MarkReadOnScrollSettingPage._valueDisplayName( + GlobalStoreWidget.settingsOf(context).markReadOnScroll, + zulipLocalizations: zulipLocalizations), + onTap: () => Navigator.push(context, + MarkReadOnScrollSettingPage.buildRoute()), + ), if (GlobalSettingsStore.experimentalFeatureFlags.isNotEmpty) - ListTile( - title: Text(zulipLocalizations.experimentalFeatureSettingsPageTitle), + _SettingsNavitem( + title: zulipLocalizations.experimentalFeatureSettingsPageTitle, onTap: () => Navigator.push(context, ExperimentalFeaturesPage.buildRoute())) ])); @@ -52,6 +73,38 @@ class _SettingsHeader extends StatelessWidget { ))); }} +class _SettingsNavitem extends StatelessWidget { + const _SettingsNavitem({ + required this.title, + this.subtitle, + required this.onTap, + }); + + final String title; + final String? subtitle; + final VoidCallback onTap; + + @override + Widget build(BuildContext context) { + final designVariables = DesignVariables.of(context); + return Material( + color: Colors.transparent, + child: ListTile( + title: Text(title, + style: TextStyle( + color: designVariables.contextMenuItemText, + fontSize: 20).merge(weightVariableTextStyle(context, wght: 600))), + subtitle: subtitle != null ? Text( + subtitle!, + style: TextStyle( + fontSize: 17).merge(weightVariableTextStyle(context, wght: 400))) : null, + onTap: onTap, + trailing: Icon( + ZulipIcons.chevron_right, + color: designVariables.contextMenuItemIcon,), + ));} +} + class _ThemeSetting extends StatelessWidget { const _ThemeSetting(); @@ -103,22 +156,6 @@ class _BrowserPreferenceSetting extends StatelessWidget { } } -class _VisitFirstUnreadSetting extends StatelessWidget { - const _VisitFirstUnreadSetting(); - - @override - Widget build(BuildContext context) { - final zulipLocalizations = ZulipLocalizations.of(context); - final globalSettings = GlobalStoreWidget.settingsOf(context); - return ListTile( - title: Text(zulipLocalizations.initialAnchorSettingTitle), - subtitle: Text(VisitFirstUnreadSettingPage._valueDisplayName( - globalSettings.visitFirstUnread, zulipLocalizations: zulipLocalizations)), - onTap: () => Navigator.push(context, - VisitFirstUnreadSettingPage.buildRoute())); - } -} - class VisitFirstUnreadSettingPage extends StatelessWidget { const VisitFirstUnreadSettingPage({super.key}); @@ -165,22 +202,6 @@ class VisitFirstUnreadSettingPage extends StatelessWidget { } } -class _MarkReadOnScrollSetting extends StatelessWidget { - const _MarkReadOnScrollSetting(); - - @override - Widget build(BuildContext context) { - final zulipLocalizations = ZulipLocalizations.of(context); - final globalSettings = GlobalStoreWidget.settingsOf(context); - return ListTile( - title: Text(zulipLocalizations.markReadOnScrollSettingTitle), - subtitle: Text(MarkReadOnScrollSettingPage._valueDisplayName( - globalSettings.markReadOnScroll, zulipLocalizations: zulipLocalizations)), - onTap: () => Navigator.push(context, - MarkReadOnScrollSettingPage.buildRoute())); - } -} - class MarkReadOnScrollSettingPage extends StatelessWidget { const MarkReadOnScrollSettingPage({super.key}); From ad524f38fea07c4ffbddec40efc8a185afc52c42 Mon Sep 17 00:00:00 2001 From: MdSaifAliMolla <145194907+MdSaifAliMolla@users.noreply.github.com> Date: Wed, 5 Nov 2025 15:07:46 +0530 Subject: [PATCH 3/4] settings: Replace RadioListTile with CustomRadioTile --- lib/widgets/settings.dart | 130 ++++++++++++++++++++++---------- test/widgets/settings_test.dart | 15 ++-- 2 files changed, 99 insertions(+), 46 deletions(-) diff --git a/lib/widgets/settings.dart b/lib/widgets/settings.dart index 8ca792d3da..770dd65347 100644 --- a/lib/widgets/settings.dart +++ b/lib/widgets/settings.dart @@ -68,8 +68,7 @@ class _SettingsHeader extends StatelessWidget { child: Padding( padding: const EdgeInsets.fromLTRB(16, 16, 16,8), child: Text(title, - style: TextStyle( - fontSize: 17).merge(weightVariableTextStyle(context, wght: 600)), + style: TextStyle(fontSize: 17).merge(weightVariableTextStyle(context, wght: 600)), ))); }} @@ -117,18 +116,19 @@ class _ThemeSetting extends StatelessWidget { Widget build(BuildContext context) { final zulipLocalizations = ZulipLocalizations.of(context); final globalSettings = GlobalStoreWidget.settingsOf(context); - return RadioGroup( - groupValue: globalSettings.themeSetting, - onChanged: (newValue) => _handleChange(context, newValue), + final themeSetting = globalSettings.themeSetting; + return Material( + color: Colors.transparent, child: Column( children: [ - //ListTile(title: Text(zulipLocalizations.themeSettingTitle)), - for (final themeSettingOption in [null, ...ThemeSetting.values]) - RadioListTile.adaptive( - title: Text(ThemeSetting.displayName( + for (final themeSettingOption in [ThemeSetting.dark, ThemeSetting.light, null]) + CustomRadioTile( + value: themeSettingOption, + groupValue: themeSetting, + label: ThemeSetting.displayName( themeSetting: themeSettingOption, - zulipLocalizations: zulipLocalizations)), - value: themeSettingOption), + zulipLocalizations: zulipLocalizations), + onChanged: (v) => _handleChange(context, v)), ])); } } @@ -188,17 +188,18 @@ class VisitFirstUnreadSettingPage extends StatelessWidget { final globalSettings = GlobalStoreWidget.settingsOf(context); return Scaffold( appBar: AppBar(title: Text(zulipLocalizations.initialAnchorSettingTitle)), - body: RadioGroup( - groupValue: globalSettings.visitFirstUnread, - onChanged: (newValue) => _handleChange(context, newValue), - child: Column(children: [ - ListTile(title: Text(zulipLocalizations.initialAnchorSettingDescription)), - for (final value in VisitFirstUnreadSetting.values) - RadioListTile.adaptive( - title: Text(_valueDisplayName(value, - zulipLocalizations: zulipLocalizations)), - value: value), - ]))); + body: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [ + Padding( + padding: const EdgeInsets.all(16.0), + child: Text(zulipLocalizations.initialAnchorSettingDescription, + style: TextStyle(fontSize: 17).merge(weightVariableTextStyle(context, wght: 400)))), + for (final value in VisitFirstUnreadSetting.values) + CustomRadioTile( + value: value, + groupValue: globalSettings.visitFirstUnread, + label: _valueDisplayName(value, zulipLocalizations: zulipLocalizations), + onChanged:(newValue) => _handleChange(context, newValue)) + ])); } } @@ -245,22 +246,18 @@ class MarkReadOnScrollSettingPage extends StatelessWidget { final globalSettings = GlobalStoreWidget.settingsOf(context); return Scaffold( appBar: AppBar(title: Text(zulipLocalizations.markReadOnScrollSettingTitle)), - body: RadioGroup( - groupValue: globalSettings.markReadOnScroll, - onChanged: (newValue) => _handleChange(context, newValue), - child: Column(children: [ - ListTile(title: Text(zulipLocalizations.markReadOnScrollSettingDescription)), - for (final value in MarkReadOnScrollSetting.values) - RadioListTile.adaptive( - title: Text(_valueDisplayName(value, - zulipLocalizations: zulipLocalizations)), - subtitle: () { - final result = _valueDescription(value, - zulipLocalizations: zulipLocalizations); - return result == null ? null : Text(result); - }(), - value: value), - ]))); + body: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [ + Padding(padding: const EdgeInsets.all(16.0), + child: Text(zulipLocalizations.markReadOnScrollSettingDescription, + style: TextStyle(fontSize: 17).merge(weightVariableTextStyle(context, wght: 400)))), + for (final value in MarkReadOnScrollSetting.values) + CustomRadioTile( + value: value, + groupValue: globalSettings.markReadOnScroll, + label: _valueDisplayName(value, zulipLocalizations: zulipLocalizations), + onChanged: (newValue) => _handleChange(context, newValue), + description: _valueDescription(value, zulipLocalizations: zulipLocalizations)) + ])); } } @@ -291,3 +288,60 @@ class ExperimentalFeaturesPage extends StatelessWidget { ])); } } + +class CustomRadioTile extends StatelessWidget { + final T value; + final T groupValue; + final String label; + final ValueChanged onChanged; + final String? description; + + const CustomRadioTile({ + super.key, + required this.value, + required this.groupValue, + required this.label, + required this.onChanged, + this.description, + }); + + @override + Widget build(BuildContext context) { + final selected = value == groupValue; + final size = 20.0; + final colr = const Color(0xff4370f0); + + return InkWell( + onTap: () => onChanged(value), + borderRadius: BorderRadius.circular(6), + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 6), + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + AnimatedContainer( + duration: const Duration(milliseconds: 200), + margin: const EdgeInsets.only(top: 4), + width: size, + height: size, + decoration: BoxDecoration( + color: selected ? colr : Colors.transparent, + border: Border.all(color: selected ? colr : Colors.grey.shade400, width: 2), + borderRadius: BorderRadius.circular(size / 2)), + child: selected? const Icon(ZulipIcons.check, size: 16, color: Colors.white): null), + const SizedBox(width: 12), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text(label,style: TextStyle(fontSize: 18).merge(weightVariableTextStyle(context, wght: 500))), + if (description != null) + Padding( + padding: const EdgeInsets.only(top: 2), + child: Text(description!, + style: TextStyle(fontSize: 17).merge(weightVariableTextStyle(context, wght: 400)),), + )])), + ]))); + } +} + diff --git a/test/widgets/settings_test.dart b/test/widgets/settings_test.dart index a56b93d7d9..66337e04fe 100644 --- a/test/widgets/settings_test.dart +++ b/test/widgets/settings_test.dart @@ -51,17 +51,15 @@ void main() { Finder findRadioListTileWithTitle(String title) => find.ancestor( of: find.text(title), - matching: find.byType(RadioListTile)); + matching: find.byType(CustomRadioTile)); void checkRadioButtonAppearsChecked(WidgetTester tester, String title, bool expectedIsChecked, {String? subtitle}) { - check(tester.semantics.find(findRadioListTileWithTitle(title))) - .containsSemantics( - label: subtitle == null - ? title - : '$title\n$subtitle', - isInMutuallyExclusiveGroup: true, - hasCheckedState: true, isChecked: expectedIsChecked); + final tile = tester.widget>(findRadioListTileWithTitle(title)); + if (expectedIsChecked) { + check(tile.value).equals(tile.groupValue); + } else { + check(tile.value).not((it) => it.equals(tile.groupValue));} } testWidgets('SettingsPage is scrollable when taller than a screenful', (tester) async { @@ -317,3 +315,4 @@ void main() { // [GlobalSettingsStore.experimentalFeatureFlags] so that tests can // control making it empty, or non-empty, at will.) } + From 6f5f562ee0ec3b31ff778509d89ca3391dc1ff58 Mon Sep 17 00:00:00 2001 From: MdSaifAliMolla <145194907+MdSaifAliMolla@users.noreply.github.com> Date: Wed, 5 Nov 2025 15:19:18 +0530 Subject: [PATCH 4/4] settings: Replace SwitchListTile with _CustomSwitch --- lib/widgets/settings.dart | 65 +++++++++++++++++++++++++-------- test/widgets/settings_test.dart | 7 ++-- 2 files changed, 52 insertions(+), 20 deletions(-) diff --git a/lib/widgets/settings.dart b/lib/widgets/settings.dart index 770dd65347..14cfde41ed 100644 --- a/lib/widgets/settings.dart +++ b/lib/widgets/settings.dart @@ -3,6 +3,7 @@ import 'package:flutter/material.dart'; import '../generated/l10n/zulip_localizations.dart'; import '../model/settings.dart'; import 'app_bar.dart'; +import 'button.dart'; import 'icons.dart'; import 'page.dart'; import 'store.dart'; @@ -95,12 +96,11 @@ class _SettingsNavitem extends StatelessWidget { fontSize: 20).merge(weightVariableTextStyle(context, wght: 600))), subtitle: subtitle != null ? Text( subtitle!, - style: TextStyle( - fontSize: 17).merge(weightVariableTextStyle(context, wght: 400))) : null, + style: TextStyle(fontSize: 17).merge(weightVariableTextStyle(context, wght: 400))) : null, onTap: onTap, trailing: Icon( ZulipIcons.chevron_right, - color: designVariables.contextMenuItemIcon,), + color: designVariables.contextMenuItemIcon), ));} } @@ -146,13 +146,19 @@ class _BrowserPreferenceSetting extends StatelessWidget { @override Widget build(BuildContext context) { final zulipLocalizations = ZulipLocalizations.of(context); + final designVariables = DesignVariables.of(context); final globalSettings = GlobalStoreWidget.settingsOf(context); final openLinksWithInAppBrowser = globalSettings.effectiveBrowserPreference == BrowserPreference.inApp; - return SwitchListTile.adaptive( - title: Text(zulipLocalizations.openLinksWithInAppBrowser), - value: openLinksWithInAppBrowser, - onChanged: (newValue) => _handleChange(context, newValue)); + return Material( + color: Colors.transparent, + child: ListTile( + title: Text(zulipLocalizations.openLinksWithInAppBrowser, + style: TextStyle( + color: designVariables.contextMenuItemText, fontSize: 20).merge(weightVariableTextStyle(context, wght: 600))), + trailing: _CustomSwitch( + value: openLinksWithInAppBrowser, + onChanged: (newValue) => _handleChange(context, newValue)))); } } @@ -273,19 +279,28 @@ class ExperimentalFeaturesPage extends StatelessWidget { final zulipLocalizations = ZulipLocalizations.of(context); final globalSettings = GlobalStoreWidget.settingsOf(context); final flags = GlobalSettingsStore.experimentalFeatureFlags; + final designVariables = DesignVariables.of(context); assert(flags.isNotEmpty); return Scaffold( appBar: AppBar( title: Text(zulipLocalizations.experimentalFeatureSettingsPageTitle)), - body: Column(children: [ - ListTile( - title: Text(zulipLocalizations.experimentalFeatureSettingsWarning)), - for (final flag in flags) - SwitchListTile.adaptive( - title: Text(flag.name), // no i18n; these are developer-facing settings - value: globalSettings.getBool(flag), - onChanged: (value) => globalSettings.setBool(flag, value)), - ])); + body: ListView( + padding: const EdgeInsets.symmetric(vertical: 8), + children: [Padding( + padding: const EdgeInsets.all(16.0), + child: Text(zulipLocalizations.experimentalFeatureSettingsWarning, + style: TextStyle(fontSize: 17).merge(weightVariableTextStyle(context, wght: 400)))), + for (final flag in flags) + Padding(padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), + child: Row( + children: [Expanded( + child: Text(flag.name, + style: TextStyle(fontSize: 20, color: designVariables.contextMenuItemText).merge(weightVariableTextStyle(context, wght: 600)))), + _CustomSwitch( + value: globalSettings.getBool(flag), + onChanged: (value) => globalSettings.setBool(flag, value)), + ])), + ])); } } @@ -344,4 +359,22 @@ class CustomRadioTile extends StatelessWidget { ]))); } } +class _CustomSwitch extends StatelessWidget { + const _CustomSwitch({ + required this.value, + required this.onChanged, + }); + + final bool value; + final ValueChanged onChanged; + + @override + Widget build(BuildContext context) { + return GestureDetector( + behavior: HitTestBehavior.translucent, + onTap: () => onChanged(!value), + child: Toggle(value: value, onChanged: onChanged )); + } +} + diff --git a/test/widgets/settings_test.dart b/test/widgets/settings_test.dart index 66337e04fe..1aef07a364 100644 --- a/test/widgets/settings_test.dart +++ b/test/widgets/settings_test.dart @@ -4,6 +4,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_checks/flutter_checks.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:zulip/model/settings.dart'; +import 'package:zulip/widgets/button.dart'; import 'package:zulip/widgets/page.dart'; import 'package:zulip/widgets/settings.dart'; import 'package:zulip/widgets/store.dart'; @@ -130,15 +131,13 @@ void main() { }); group('BrowserPreference', () { - Finder useInAppBrowserSwitchFinder = find.ancestor( - of: find.text('Open links with in-app browser'), - matching: find.byType(SwitchListTile)); + Finder useInAppBrowserSwitchFinder = find.byType(Toggle).first; void checkSwitchAndGlobalSettings(WidgetTester tester, { required bool checked, required BrowserPreference? expectedBrowserPreference, }) { - check(tester.widget(useInAppBrowserSwitchFinder)) + check(tester.widget(useInAppBrowserSwitchFinder)) .value.equals(checked); check(testBinding.globalStore) .settings.browserPreference.equals(expectedBrowserPreference);