mirror of
https://github.com/hanskokx/arcane_framework.git
synced 2026-05-14 02:19:08 +02:00
Setting a theme style now automatically switches to that theme
Signed-off-by: Hans Kokx <hans.d.kokx@gmail.com>
This commit is contained in:
@@ -1,16 +1,15 @@
|
|||||||
import "dart:async";
|
|
||||||
|
|
||||||
import "package:arcane_framework/src/services/reactive_theme/reactive_theme_service.dart";
|
|
||||||
import "package:flutter/material.dart";
|
import "package:flutter/material.dart";
|
||||||
|
|
||||||
class ArcaneTheme extends InheritedWidget {
|
class ArcaneTheme extends InheritedWidget {
|
||||||
final ThemeMode themeMode;
|
final ThemeMode themeMode;
|
||||||
final bool followSystem;
|
final bool followSystem;
|
||||||
|
final ThemeData? theme;
|
||||||
|
|
||||||
const ArcaneTheme({
|
const ArcaneTheme({
|
||||||
required this.themeMode,
|
|
||||||
required this.followSystem,
|
|
||||||
required super.child,
|
required super.child,
|
||||||
|
this.themeMode = ThemeMode.light,
|
||||||
|
this.followSystem = false,
|
||||||
|
this.theme,
|
||||||
super.key,
|
super.key,
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -21,60 +20,7 @@ class ArcaneTheme extends InheritedWidget {
|
|||||||
@override
|
@override
|
||||||
bool updateShouldNotify(ArcaneTheme oldWidget) {
|
bool updateShouldNotify(ArcaneTheme oldWidget) {
|
||||||
return themeMode != oldWidget.themeMode ||
|
return themeMode != oldWidget.themeMode ||
|
||||||
followSystem != oldWidget.followSystem;
|
followSystem != oldWidget.followSystem ||
|
||||||
|
theme != oldWidget.theme;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Returns the singleton instance of the [ArcaneReactiveTheme] service.
|
|
||||||
ArcaneReactiveTheme get service => ArcaneReactiveTheme.I;
|
|
||||||
|
|
||||||
/// Indicates whether the theme is currently set to follow the system theme.
|
|
||||||
bool get isFollowingSystemTheme => service.isFollowingSystemTheme;
|
|
||||||
|
|
||||||
/// Provides a stream of [ThemeMode] changes that can be listened to for reactive updates.
|
|
||||||
Stream<ThemeMode> get themeChanges => service.themeChanges;
|
|
||||||
|
|
||||||
/// Returns the currently active [ThemeMode].
|
|
||||||
ThemeMode get currentTheme => service.currentTheme;
|
|
||||||
|
|
||||||
/// Returns the [ThemeMode] currently set at the OS/system level.
|
|
||||||
ThemeMode get systemTheme => service.systemTheme;
|
|
||||||
|
|
||||||
/// Returns the dark [ThemeData] configuration.
|
|
||||||
ThemeData get dark => service.dark;
|
|
||||||
|
|
||||||
/// Returns a [ValueNotifier] containing the dark [ThemeData], allowing for reactive updates.
|
|
||||||
ValueNotifier<ThemeData> get darkTheme => service.darkTheme;
|
|
||||||
|
|
||||||
/// Returns the light [ThemeData] configuration.
|
|
||||||
ThemeData get light => service.light;
|
|
||||||
|
|
||||||
/// Returns a [ValueNotifier] containing the light [ThemeData], allowing for reactive updates.
|
|
||||||
ValueNotifier<ThemeData> get lightTheme => service.lightTheme;
|
|
||||||
|
|
||||||
/// A shortcut to the [ArcaneReactiveTheme] function that switches the active theme mode.
|
|
||||||
///
|
|
||||||
/// - [themeMode] (Optional): Specify which theme mode to switch to.
|
|
||||||
/// Otherwise, tries to determine whether to switch to light or dark mode, automatically.
|
|
||||||
ArcaneReactiveTheme Function({ThemeMode? themeMode}) get switchTheme =>
|
|
||||||
service.switchTheme;
|
|
||||||
|
|
||||||
/// A shortcut to the [ArcaneReactiveTheme] function that follows the system theme.
|
|
||||||
///
|
|
||||||
/// - [context]: The [BuildContext] required to access system theme information.
|
|
||||||
void Function(BuildContext context) followSystemTheme(
|
|
||||||
BuildContext context,
|
|
||||||
) =>
|
|
||||||
service.followSystemTheme;
|
|
||||||
|
|
||||||
/// A shortcut to the [ArcaneReactiveTheme] function that updates the dark theme configuration.
|
|
||||||
///
|
|
||||||
/// The function accepts a [ThemeData] parameter to set as the new dark theme.
|
|
||||||
ArcaneReactiveTheme Function(ThemeData theme) get setDarkTheme =>
|
|
||||||
service.setDarkTheme;
|
|
||||||
|
|
||||||
/// A shortcut to the [ArcaneReactiveTheme] function that updates the light theme configuration.
|
|
||||||
///
|
|
||||||
/// The function accepts a [ThemeData] parameter to set as the new light theme.
|
|
||||||
ArcaneReactiveTheme Function(ThemeData theme) get setLightTheme =>
|
|
||||||
service.setLightTheme;
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -24,6 +24,6 @@ extension ArcaneThemeContext on BuildContext {
|
|||||||
/// Get the current theme mode from the nearest ArcaneThemeInherited widget
|
/// Get the current theme mode from the nearest ArcaneThemeInherited widget
|
||||||
ThemeMode get themeMode {
|
ThemeMode get themeMode {
|
||||||
return ArcaneTheme.of(this)?.themeMode ??
|
return ArcaneTheme.of(this)?.themeMode ??
|
||||||
ArcaneReactiveTheme.I.currentTheme;
|
ArcaneReactiveTheme.I.currentThemeMode;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -38,26 +38,41 @@ class ArcaneReactiveTheme extends ArcaneService {
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
final StreamController<ThemeMode> _themeStreamController =
|
final StreamController<ThemeMode> _themeModeStreamController =
|
||||||
StreamController<ThemeMode>.broadcast(
|
StreamController<ThemeMode>.broadcast(
|
||||||
|
onCancel: () {
|
||||||
|
I._themeModeStreamController.close();
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
final StreamController<ThemeData> _themeStreamController =
|
||||||
|
StreamController<ThemeData>.broadcast(
|
||||||
onCancel: () {
|
onCancel: () {
|
||||||
I._themeStreamController.close();
|
I._themeStreamController.close();
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
/// Stream of `ThemeMode` changes that can be listened to for reactive UI updates.
|
/// Stream of `ThemeMode` changes that can be listened to for reactive UI updates.
|
||||||
Stream<ThemeMode> get themeChanges => I._themeStreamController.stream;
|
Stream<ThemeMode> get themeModeChanges => I._themeModeStreamController.stream;
|
||||||
|
|
||||||
|
/// Stream of `ThemeData` changes that can be listened to for reactive UI updates.
|
||||||
|
Stream<ThemeData> get themeDataChanges => I._themeStreamController.stream;
|
||||||
|
|
||||||
/// Returns the `ThemeData` corresponding to the current system theme
|
/// Returns the `ThemeData` corresponding to the current system theme
|
||||||
ThemeMode get systemTheme => _currentSystemTheme;
|
ThemeMode get systemThemeMode => _currentSystemThemeMode;
|
||||||
|
|
||||||
/// Tracks the current system theme mode
|
/// Tracks the current system theme mode
|
||||||
ThemeMode _currentSystemTheme = ThemeMode.system;
|
ThemeMode _currentSystemThemeMode = ThemeMode.system;
|
||||||
|
|
||||||
ThemeMode _currentTheme = ThemeMode.light;
|
ThemeMode _currentThemeMode = ThemeMode.light;
|
||||||
|
|
||||||
/// The currently active theme mode (light or dark).
|
/// The currently active theme mode (light or dark).
|
||||||
ThemeMode get currentTheme => _currentTheme;
|
ThemeMode get currentThemeMode => _currentThemeMode;
|
||||||
|
|
||||||
|
ThemeData _currentTheme = ThemeData();
|
||||||
|
|
||||||
|
/// The currently active theme style.
|
||||||
|
ThemeData get currentTheme => _currentTheme;
|
||||||
|
|
||||||
/// The `ThemeData` for the dark theme.
|
/// The `ThemeData` for the dark theme.
|
||||||
final ValueNotifier<ThemeData> _darkTheme = ValueNotifier(ThemeData.dark());
|
final ValueNotifier<ThemeData> _darkTheme = ValueNotifier(ThemeData.dark());
|
||||||
@@ -101,7 +116,7 @@ class ArcaneReactiveTheme extends ArcaneService {
|
|||||||
_updateTheme(themeMode);
|
_updateTheme(themeMode);
|
||||||
} else {
|
} else {
|
||||||
_updateTheme(
|
_updateTheme(
|
||||||
currentTheme == ThemeMode.dark ? ThemeMode.light : ThemeMode.dark,
|
currentThemeMode == ThemeMode.dark ? ThemeMode.light : ThemeMode.dark,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -124,10 +139,14 @@ class ArcaneReactiveTheme extends ArcaneService {
|
|||||||
ArcaneReactiveTheme followSystemTheme(BuildContext context) {
|
ArcaneReactiveTheme followSystemTheme(BuildContext context) {
|
||||||
_followingSystemTheme = true;
|
_followingSystemTheme = true;
|
||||||
|
|
||||||
_currentSystemTheme = context.isDarkMode ? ThemeMode.dark : ThemeMode.light;
|
_currentSystemThemeMode =
|
||||||
_systemStreamController.add(_currentSystemTheme);
|
context.isDarkMode ? ThemeMode.dark : ThemeMode.light;
|
||||||
_updateTheme(_currentSystemTheme);
|
_systemStreamController.add(_currentSystemThemeMode);
|
||||||
|
_updateTheme(_currentSystemThemeMode);
|
||||||
|
|
||||||
|
final ThemeData theme = systemThemeMode == ThemeMode.dark ? dark : light;
|
||||||
|
_themeStreamController.add(theme);
|
||||||
|
_currentTheme = theme;
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
|
|
||||||
return I;
|
return I;
|
||||||
@@ -144,6 +163,8 @@ class ArcaneReactiveTheme extends ArcaneService {
|
|||||||
/// ```
|
/// ```
|
||||||
ArcaneReactiveTheme setDarkTheme(ThemeData theme) {
|
ArcaneReactiveTheme setDarkTheme(ThemeData theme) {
|
||||||
_darkTheme.value = theme;
|
_darkTheme.value = theme;
|
||||||
|
_themeStreamController.add(theme);
|
||||||
|
_currentTheme = theme;
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
return I;
|
return I;
|
||||||
}
|
}
|
||||||
@@ -159,6 +180,8 @@ class ArcaneReactiveTheme extends ArcaneService {
|
|||||||
/// ```
|
/// ```
|
||||||
ArcaneReactiveTheme setLightTheme(ThemeData theme) {
|
ArcaneReactiveTheme setLightTheme(ThemeData theme) {
|
||||||
_lightTheme.value = theme;
|
_lightTheme.value = theme;
|
||||||
|
_themeStreamController.add(theme);
|
||||||
|
_currentTheme = theme;
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
return I;
|
return I;
|
||||||
}
|
}
|
||||||
@@ -173,12 +196,14 @@ class ArcaneReactiveTheme extends ArcaneService {
|
|||||||
_lightTheme.value = ThemeData.light();
|
_lightTheme.value = ThemeData.light();
|
||||||
_followingSystemTheme = false;
|
_followingSystemTheme = false;
|
||||||
_updateTheme(ThemeMode.light);
|
_updateTheme(ThemeMode.light);
|
||||||
|
_themeStreamController.add(_lightTheme.value);
|
||||||
|
_currentTheme = _lightTheme.value;
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Updates the current theme mode and broadcasts the change.
|
/// Updates the current theme mode and broadcasts the change.
|
||||||
void _updateTheme(ThemeMode themeMode) {
|
void _updateTheme(ThemeMode themeMode) {
|
||||||
_currentTheme = themeMode;
|
_currentThemeMode = themeMode;
|
||||||
_themeStreamController.add(themeMode);
|
_themeModeStreamController.add(themeMode);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,27 +16,33 @@ class ArcaneThemeSwitcher extends StatefulWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class _ArcaneThemeSwitcherState extends State<ArcaneThemeSwitcher> {
|
class _ArcaneThemeSwitcherState extends State<ArcaneThemeSwitcher> {
|
||||||
late final StreamSubscription<ThemeMode> _subscription;
|
late final StreamSubscription<ThemeMode> _themeModeSubscription;
|
||||||
|
late final StreamSubscription<ThemeData> _themeSubscription;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
_subscription = ArcaneReactiveTheme.I.themeChanges.listen((_) {
|
_themeModeSubscription = ArcaneReactiveTheme.I.themeModeChanges.listen((_) {
|
||||||
|
setState(() {});
|
||||||
|
});
|
||||||
|
_themeSubscription = ArcaneReactiveTheme.I.themeDataChanges.listen((_) {
|
||||||
setState(() {});
|
setState(() {});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void dispose() {
|
void dispose() {
|
||||||
_subscription.cancel();
|
_themeModeSubscription.cancel();
|
||||||
|
_themeSubscription.cancel();
|
||||||
super.dispose();
|
super.dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return ArcaneTheme(
|
return ArcaneTheme(
|
||||||
themeMode: ArcaneReactiveTheme.I.currentTheme,
|
themeMode: ArcaneReactiveTheme.I.currentThemeMode,
|
||||||
followSystem: ArcaneReactiveTheme.I.isFollowingSystemTheme,
|
followSystem: ArcaneReactiveTheme.I.isFollowingSystemTheme,
|
||||||
|
theme: ArcaneReactiveTheme.I.currentTheme,
|
||||||
child: widget.child,
|
child: widget.child,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,15 +16,15 @@ void main() {
|
|||||||
|
|
||||||
group("theme mode", () {
|
group("theme mode", () {
|
||||||
test("initial mode is light", () {
|
test("initial mode is light", () {
|
||||||
expect(theme.currentTheme, equals(ThemeMode.light));
|
expect(theme.currentThemeMode, equals(ThemeMode.light));
|
||||||
});
|
});
|
||||||
|
|
||||||
test("switchTheme toggles between light and dark", () {
|
test("switchTheme toggles between light and dark", () {
|
||||||
expect(theme.currentTheme, equals(ThemeMode.light));
|
expect(theme.currentThemeMode, equals(ThemeMode.light));
|
||||||
theme.switchTheme();
|
theme.switchTheme();
|
||||||
expect(theme.currentTheme, equals(ThemeMode.dark));
|
expect(theme.currentThemeMode, equals(ThemeMode.dark));
|
||||||
theme.switchTheme();
|
theme.switchTheme();
|
||||||
expect(theme.currentTheme, equals(ThemeMode.light));
|
expect(theme.currentThemeMode, equals(ThemeMode.light));
|
||||||
});
|
});
|
||||||
|
|
||||||
test("switching theme notifies listeners", () {
|
test("switching theme notifies listeners", () {
|
||||||
@@ -66,7 +66,7 @@ void main() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
theme.addListener(() {
|
theme.addListener(() {
|
||||||
currentTheme = theme.currentTheme;
|
currentTheme = theme.currentThemeMode;
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(currentTheme, ThemeMode.system);
|
expect(currentTheme, ThemeMode.system);
|
||||||
@@ -106,7 +106,7 @@ void main() {
|
|||||||
Arcane.theme.followSystemTheme(lightContext);
|
Arcane.theme.followSystemTheme(lightContext);
|
||||||
await tester.pumpAndSettle();
|
await tester.pumpAndSettle();
|
||||||
|
|
||||||
expect(theme.currentTheme, equals(ThemeMode.light));
|
expect(theme.currentThemeMode, equals(ThemeMode.light));
|
||||||
|
|
||||||
await tester.pumpWidget(
|
await tester.pumpWidget(
|
||||||
const MediaQuery(
|
const MediaQuery(
|
||||||
@@ -121,7 +121,7 @@ void main() {
|
|||||||
Arcane.theme.followSystemTheme(darkContext);
|
Arcane.theme.followSystemTheme(darkContext);
|
||||||
await tester.pumpAndSettle();
|
await tester.pumpAndSettle();
|
||||||
|
|
||||||
expect(theme.currentTheme, equals(ThemeMode.dark));
|
expect(theme.currentThemeMode, equals(ThemeMode.dark));
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user