Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
302 changes: 214 additions & 88 deletions lib/widgets/settings.dart
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,12 @@ 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';
import 'text.dart';
import 'theme.dart';

class SettingsPage extends StatelessWidget {
const SettingsPage({super.key});
Expand All @@ -18,22 +22,88 @@ 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(),
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()))
]));
}
}

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 _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();

Expand All @@ -46,18 +116,19 @@ class _ThemeSetting extends StatelessWidget {
Widget build(BuildContext context) {
final zulipLocalizations = ZulipLocalizations.of(context);
final globalSettings = GlobalStoreWidget.settingsOf(context);
return RadioGroup<ThemeSetting?>(
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<ThemeSetting?>.adaptive(
title: Text(ThemeSetting.displayName(
for (final themeSettingOption in [ThemeSetting.dark, ThemeSetting.light, null])
CustomRadioTile<ThemeSetting?>(
value: themeSettingOption,
groupValue: themeSetting,
label: ThemeSetting.displayName(
themeSetting: themeSettingOption,
zulipLocalizations: zulipLocalizations)),
value: themeSettingOption),
zulipLocalizations: zulipLocalizations),
onChanged: (v) => _handleChange(context, v)),
]));
}
}
Expand All @@ -75,29 +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));
}
}

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()));
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))));
}
}

Expand Down Expand Up @@ -133,33 +194,18 @@ class VisitFirstUnreadSettingPage extends StatelessWidget {
final globalSettings = GlobalStoreWidget.settingsOf(context);
return Scaffold(
appBar: AppBar(title: Text(zulipLocalizations.initialAnchorSettingTitle)),
body: RadioGroup<VisitFirstUnreadSetting>(
groupValue: globalSettings.visitFirstUnread,
onChanged: (newValue) => _handleChange(context, newValue),
child: Column(children: [
ListTile(title: Text(zulipLocalizations.initialAnchorSettingDescription)),
for (final value in VisitFirstUnreadSetting.values)
RadioListTile<VisitFirstUnreadSetting>.adaptive(
title: Text(_valueDisplayName(value,
zulipLocalizations: zulipLocalizations)),
value: value),
])));
}
}

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()));
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))
]));
}
}

Expand Down Expand Up @@ -206,22 +252,18 @@ class MarkReadOnScrollSettingPage extends StatelessWidget {
final globalSettings = GlobalStoreWidget.settingsOf(context);
return Scaffold(
appBar: AppBar(title: Text(zulipLocalizations.markReadOnScrollSettingTitle)),
body: RadioGroup<MarkReadOnScrollSetting>(
groupValue: globalSettings.markReadOnScroll,
onChanged: (newValue) => _handleChange(context, newValue),
child: Column(children: [
ListTile(title: Text(zulipLocalizations.markReadOnScrollSettingDescription)),
for (final value in MarkReadOnScrollSetting.values)
RadioListTile<MarkReadOnScrollSetting>.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))
]));
}
}

Expand All @@ -237,18 +279,102 @@ 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)),
])),
]));
}
}

class CustomRadioTile<T> extends StatelessWidget {
final T value;
final T groupValue;
final String label;
final ValueChanged<T?> 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)),),
)])),
])));
}
}
class _CustomSwitch extends StatelessWidget {
const _CustomSwitch({
required this.value,
required this.onChanged,
});

final bool value;
final ValueChanged<bool> onChanged;

@override
Widget build(BuildContext context) {
return GestureDetector(
behavior: HitTestBehavior.translucent,
onTap: () => onChanged(!value),
child: Toggle(value: value, onChanged: onChanged ));
}
}


Loading