Skip to content

Commit c54b92a

Browse files
committed
theme: Track theme through global settings
Signed-off-by: Zixuan James Li <zixuan@zulip.com>
1 parent 9897cff commit c54b92a

File tree

5 files changed

+96
-35
lines changed

5 files changed

+96
-35
lines changed

lib/widgets/app.dart

Lines changed: 38 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -212,41 +212,45 @@ class _ZulipAppState extends State<ZulipApp> with WidgetsBindingObserver {
212212

213213
@override
214214
Widget build(BuildContext context) {
215-
final themeData = zulipThemeData(context);
216215
return GlobalStoreWidget(
217-
child: MaterialApp(
218-
onGenerateTitle: (BuildContext context) {
219-
return ZulipLocalizations.of(context).zulipAppTitle;
220-
},
221-
localizationsDelegates: ZulipLocalizations.localizationsDelegates,
222-
supportedLocales: ZulipLocalizations.supportedLocales,
223-
theme: themeData,
224-
225-
navigatorKey: ZulipApp.navigatorKey,
226-
navigatorObservers: [
227-
if (widget.navigatorObservers != null)
228-
...widget.navigatorObservers!,
229-
_PreventEmptyStack(),
230-
],
231-
builder: (BuildContext context, Widget? child) {
232-
if (!ZulipApp.ready.value) {
233-
SchedulerBinding.instance.addPostFrameCallback(
234-
(_) => widget._declareReady());
235-
}
236-
GlobalLocalizations.zulipLocalizations = ZulipLocalizations.of(context);
237-
return child!;
238-
},
239-
240-
// We use onGenerateInitialRoutes for the real work of specifying the
241-
// initial nav state. To do that we need [MaterialApp] to decide to
242-
// build a [Navigator]... which means specifying either `home`, `routes`,
243-
// `onGenerateRoute`, or `onUnknownRoute`. Make it `onGenerateRoute`.
244-
// It never actually gets called, though: `onGenerateInitialRoutes`
245-
// handles startup, and then we always push whole routes with methods
246-
// like [Navigator.push], never mere names as with [Navigator.pushNamed].
247-
onGenerateRoute: (_) => null,
248-
249-
onGenerateInitialRoutes: _handleGenerateInitialRoutes));
216+
child: Builder(
217+
builder: (context) {
218+
return MaterialApp(
219+
onGenerateTitle: (BuildContext context) {
220+
return ZulipLocalizations.of(context).zulipAppTitle;
221+
},
222+
localizationsDelegates: ZulipLocalizations.localizationsDelegates,
223+
supportedLocales: ZulipLocalizations.supportedLocales,
224+
// The context has to be taken from the [Builder] because
225+
// [zulipThemeData] requires access to [GlobalStoreWidget] in the tree.
226+
theme: zulipThemeData(context),
227+
228+
navigatorKey: ZulipApp.navigatorKey,
229+
navigatorObservers: [
230+
if (widget.navigatorObservers != null)
231+
...widget.navigatorObservers!,
232+
_PreventEmptyStack(),
233+
],
234+
builder: (BuildContext context, Widget? child) {
235+
if (!ZulipApp.ready.value) {
236+
SchedulerBinding.instance.addPostFrameCallback(
237+
(_) => widget._declareReady());
238+
}
239+
GlobalLocalizations.zulipLocalizations = ZulipLocalizations.of(context);
240+
return child!;
241+
},
242+
243+
// We use onGenerateInitialRoutes for the real work of specifying the
244+
// initial nav state. To do that we need [MaterialApp] to decide to
245+
// build a [Navigator]... which means specifying either `home`, `routes`,
246+
// `onGenerateRoute`, or `onUnknownRoute`. Make it `onGenerateRoute`.
247+
// It never actually gets called, though: `onGenerateInitialRoutes`
248+
// handles startup, and then we always push whole routes with methods
249+
// like [Navigator.push], never mere names as with [Navigator.pushNamed].
250+
onGenerateRoute: (_) => null,
251+
252+
onGenerateInitialRoutes: _handleGenerateInitialRoutes);
253+
}));
250254
}
251255
}
252256

lib/widgets/theme.dart

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,24 @@
11
import 'package:flutter/material.dart';
22

33
import '../api/model/model.dart';
4+
import '../model/settings.dart';
45
import 'compose_box.dart';
56
import 'content.dart';
67
import 'emoji_reaction.dart';
78
import 'message_list.dart';
89
import 'channel_colors.dart';
10+
import 'store.dart';
911
import 'text.dart';
1012

1113
ThemeData zulipThemeData(BuildContext context) {
1214
final DesignVariables designVariables;
1315
final List<ThemeExtension> themeExtensions;
14-
Brightness brightness = MediaQuery.platformBrightnessOf(context);
16+
final globalSettings = GlobalStoreWidget.of(context).globalSettings;
17+
Brightness brightness = switch (globalSettings.themeSetting) {
18+
null => MediaQuery.platformBrightnessOf(context),
19+
ThemeSetting.light => Brightness.light,
20+
ThemeSetting.dark => Brightness.dark,
21+
};
1522

1623
// This applies Material 3's color system to produce a palette of
1724
// appropriately matching and contrasting colors for use in a UI.

test/flutter_checks.dart

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,10 @@ extension ValueListenableChecks<T> on Subject<ValueListenable<T>> {
7474
Subject<T> get value => has((c) => c.value, 'value');
7575
}
7676

77+
extension ThemeDataChecks on Subject<ThemeData> {
78+
Subject<Brightness> get brightness => has((x) => x.brightness, 'brightness');
79+
}
80+
7781
extension TextChecks on Subject<Text> {
7882
Subject<String?> get data => has((t) => t.data, 'data');
7983
Subject<TextStyle?> get style => has((t) => t.style, 'style');

test/widgets/test_app.dart

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,8 @@ class TestZulipApp extends StatelessWidget {
7373
title: 'Zulip',
7474
localizationsDelegates: ZulipLocalizations.localizationsDelegates,
7575
supportedLocales: ZulipLocalizations.supportedLocales,
76+
// The context has to be taken from the [Builder] because
77+
// [zulipThemeData] requires access to [GlobalStoreWidget] in the tree.
7678
theme: zulipThemeData(context),
7779

7880
navigatorObservers: navigatorObservers ?? const [],

test/widgets/theme_test.dart

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,18 @@
11
import 'package:checks/checks.dart';
2+
import 'package:drift/drift.dart';
23
import 'package:flutter/material.dart';
34
import 'package:flutter/rendering.dart';
45
import 'package:flutter_test/flutter_test.dart';
6+
import 'package:zulip/model/database.dart';
7+
import 'package:zulip/model/settings.dart';
58
import 'package:zulip/widgets/channel_colors.dart';
69
import 'package:zulip/widgets/text.dart';
710
import 'package:zulip/widgets/theme.dart';
811

912
import '../example_data.dart' as eg;
1013
import '../flutter_checks.dart';
1114
import '../model/binding.dart';
15+
import '../model/store_checks.dart';
1216
import 'colors_checks.dart';
1317
import 'test_app.dart';
1418

@@ -99,6 +103,46 @@ void main() {
99103
});
100104
});
101105

106+
testWidgets('when globalSettings.themeSetting is null, follow system setting', (tester) async {
107+
addTearDown(testBinding.reset);
108+
109+
tester.platformDispatcher.platformBrightnessTestValue = Brightness.light;
110+
addTearDown(tester.platformDispatcher.clearPlatformBrightnessTestValue);
111+
112+
await tester.pumpWidget(const TestZulipApp(child: Placeholder()));
113+
await tester.pump();
114+
check(testBinding.globalStore).globalSettings.themeSetting.isNull();
115+
116+
final element = tester.element(find.byType(Placeholder));
117+
check(zulipThemeData(element)).brightness.equals(Brightness.light);
118+
119+
tester.platformDispatcher.platformBrightnessTestValue = Brightness.dark;
120+
await tester.pump();
121+
check(zulipThemeData(element)).brightness.equals(Brightness.dark);
122+
});
123+
124+
testWidgets('when globalSettings.themeSetting is non-null, override system setting', (tester) async {
125+
addTearDown(testBinding.reset);
126+
127+
tester.platformDispatcher.platformBrightnessTestValue = Brightness.light;
128+
addTearDown(tester.platformDispatcher.clearPlatformBrightnessTestValue);
129+
130+
await tester.pumpWidget(const TestZulipApp(child: Placeholder()));
131+
await tester.pump();
132+
check(testBinding.globalStore).globalSettings.themeSetting.isNull();
133+
134+
final element = tester.element(find.byType(Placeholder));
135+
check(zulipThemeData(element)).brightness.equals(Brightness.light);
136+
137+
await testBinding.globalStore.updateGlobalSettings(
138+
const GlobalSettingsCompanion(themeSetting: Value(ThemeSetting.dark)));
139+
check(zulipThemeData(element)).brightness.equals(Brightness.dark);
140+
141+
await testBinding.globalStore.updateGlobalSettings(
142+
const GlobalSettingsCompanion(themeSetting: Value(null)));
143+
check(zulipThemeData(element)).brightness.equals(Brightness.light);
144+
});
145+
102146
group('colorSwatchFor', () {
103147
const baseColor = 0xff76ce90;
104148

0 commit comments

Comments
 (0)