mirror of
https://github.com/hanskokx/arcane_framework.git
synced 2026-05-14 10:29:06 +02:00
+44
-16
@@ -59,25 +59,10 @@ class MainApp extends StatelessWidget {
|
|||||||
debugShowCheckedModeBanner: false,
|
debugShowCheckedModeBanner: false,
|
||||||
theme: Arcane.theme.light,
|
theme: Arcane.theme.light,
|
||||||
darkTheme: Arcane.theme.dark,
|
darkTheme: Arcane.theme.dark,
|
||||||
themeMode:
|
themeMode: Arcane.theme.currentModeOf(context),
|
||||||
ArcaneTheme.of(context)?.themeMode ?? Arcane.theme.currentTheme,
|
|
||||||
home: Scaffold(
|
home: Scaffold(
|
||||||
appBar: AppBar(
|
appBar: AppBar(
|
||||||
title: const Text("Arcane Framework Example"),
|
title: const Text("Arcane Framework Example"),
|
||||||
actions: [
|
|
||||||
IconButton(
|
|
||||||
icon: const Icon(Icons.settings_system_daydream),
|
|
||||||
onPressed: () {
|
|
||||||
Arcane.theme.followSystemTheme(context);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
IconButton(
|
|
||||||
icon: const Icon(Icons.contrast),
|
|
||||||
onPressed: () {
|
|
||||||
Arcane.theme.switchTheme();
|
|
||||||
},
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
),
|
||||||
body: const HomeScreen(),
|
body: const HomeScreen(),
|
||||||
),
|
),
|
||||||
@@ -101,6 +86,49 @@ class _HomeScreenState extends State<HomeScreen> {
|
|||||||
child: Column(
|
child: Column(
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
children: [
|
children: [
|
||||||
|
Card(
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.all(8.0),
|
||||||
|
child: Column(
|
||||||
|
children: [
|
||||||
|
Column(
|
||||||
|
children: [
|
||||||
|
Checkbox(
|
||||||
|
value: Arcane.theme.isFollowingSystemTheme,
|
||||||
|
onChanged: (value) {
|
||||||
|
if (value == true) {
|
||||||
|
Arcane.theme.followSystemTheme(context);
|
||||||
|
} else {
|
||||||
|
Arcane.theme.switchTheme(
|
||||||
|
themeMode: Arcane.theme.systemTheme,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
),
|
||||||
|
const Text("Use system theme"),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
Switch(
|
||||||
|
value: Arcane.theme.currentTheme == ThemeMode.dark,
|
||||||
|
thumbIcon: WidgetStateProperty.resolveWith((states) {
|
||||||
|
if (states.contains(WidgetState.selected)) {
|
||||||
|
return const Icon(Icons.dark_mode);
|
||||||
|
}
|
||||||
|
return const Icon(Icons.light_mode);
|
||||||
|
}),
|
||||||
|
onChanged: (_) {
|
||||||
|
Arcane.theme.switchTheme();
|
||||||
|
},
|
||||||
|
),
|
||||||
|
Text(
|
||||||
|
"The current theme mode is ${context.themeMode.name} and\n"
|
||||||
|
"is ${Arcane.theme.isFollowingSystemTheme ? "" : "not "}"
|
||||||
|
"following the system theme.",
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
Text(
|
Text(
|
||||||
"Authentication status: ${Arcane.auth.status.name}",
|
"Authentication status: ${Arcane.auth.status.name}",
|
||||||
),
|
),
|
||||||
|
|||||||
+3
-16
@@ -1,4 +1,5 @@
|
|||||||
import "package:arcane_framework/arcane_framework.dart";
|
import "package:arcane_framework/arcane_framework.dart";
|
||||||
|
import "package:arcane_framework/src/services/reactive_theme/reactive_theme_switcher.dart";
|
||||||
import "package:flutter/material.dart";
|
import "package:flutter/material.dart";
|
||||||
|
|
||||||
/// A root widget for an Arcane-powered application.
|
/// A root widget for an Arcane-powered application.
|
||||||
@@ -54,22 +55,9 @@ class _ArcaneAppState extends State<ArcaneApp> with WidgetsBindingObserver {
|
|||||||
return ArcaneEnvironmentProvider(
|
return ArcaneEnvironmentProvider(
|
||||||
child: ArcaneServiceProvider(
|
child: ArcaneServiceProvider(
|
||||||
serviceInstances: widget.services,
|
serviceInstances: widget.services,
|
||||||
child: Builder(
|
child: ArcaneThemeSwitcher(
|
||||||
key: _appKey,
|
key: _appKey,
|
||||||
builder: (BuildContext currentContext) {
|
|
||||||
return StreamBuilder<ThemeMode>(
|
|
||||||
stream: ArcaneReactiveTheme.I.currentThemeStream,
|
|
||||||
initialData: ArcaneReactiveTheme.I.currentTheme,
|
|
||||||
builder: (context, AsyncSnapshot<ThemeMode> snapshot) {
|
|
||||||
final ThemeMode themeMode = snapshot.data ?? ThemeMode.light;
|
|
||||||
|
|
||||||
return ArcaneTheme(
|
|
||||||
themeMode: themeMode,
|
|
||||||
child: widget.child,
|
child: widget.child,
|
||||||
);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
@@ -95,10 +83,9 @@ class _ArcaneAppState extends State<ArcaneApp> with WidgetsBindingObserver {
|
|||||||
// and use it to check the system theme
|
// and use it to check the system theme
|
||||||
if (mounted && _appKey.currentContext != null) {
|
if (mounted && _appKey.currentContext != null) {
|
||||||
// Use the current context from the key to check system theme
|
// Use the current context from the key to check system theme
|
||||||
final BuildContext currentContext = _appKey.currentContext!;
|
|
||||||
if (ArcaneReactiveTheme.I.isFollowingSystemTheme) {
|
if (ArcaneReactiveTheme.I.isFollowingSystemTheme) {
|
||||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||||
ArcaneReactiveTheme.I.checkSystemTheme(currentContext);
|
ArcaneReactiveTheme.I.followSystemTheme(context);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,3 +19,11 @@ extension DarkMode on BuildContext {
|
|||||||
return brightness == Brightness.dark;
|
return brightness == Brightness.dark;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
extension ArcaneThemeContext on BuildContext {
|
||||||
|
/// Get the current theme mode from the nearest ArcaneThemeInherited widget
|
||||||
|
ThemeMode get themeMode {
|
||||||
|
return ArcaneTheme.of(this)?.themeMode ??
|
||||||
|
ArcaneReactiveTheme.I.currentTheme;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -27,12 +27,16 @@ class ArcaneReactiveTheme extends ArcaneService {
|
|||||||
|
|
||||||
/// Whether the theme service is currently following the system theme.
|
/// Whether the theme service is currently following the system theme.
|
||||||
///
|
///
|
||||||
/// When true, the theme will automatically switch between light and dark
|
/// When `true`, the theme will automatically switch between light and dark
|
||||||
/// based on the system's brightness setting.
|
/// based on the system's brightness setting.
|
||||||
bool get isFollowingSystemTheme => _followingSystemTheme;
|
bool get isFollowingSystemTheme => _followingSystemTheme;
|
||||||
|
|
||||||
final ValueNotifier<ThemeMode> _systemThemeNotifier =
|
final StreamController<ThemeMode> _systemStreamController =
|
||||||
ValueNotifier(ThemeMode.system);
|
StreamController<ThemeMode>.broadcast(
|
||||||
|
onCancel: () {
|
||||||
|
I._systemStreamController.close();
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
final StreamController<ThemeMode> _themeStreamController =
|
final StreamController<ThemeMode> _themeStreamController =
|
||||||
StreamController<ThemeMode>.broadcast(
|
StreamController<ThemeMode>.broadcast(
|
||||||
@@ -41,17 +45,20 @@ class ArcaneReactiveTheme extends ArcaneService {
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
/// Stream of theme mode 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 currentThemeStream => I._themeStreamController.stream;
|
Stream<ThemeMode> get themeChanges => I._themeStreamController.stream;
|
||||||
|
|
||||||
|
/// Returns the `ThemeData` corresponding to the current system theme
|
||||||
|
ThemeMode get systemTheme => _currentSystemTheme;
|
||||||
|
|
||||||
|
/// Tracks the current system theme mode
|
||||||
|
ThemeMode _currentSystemTheme = ThemeMode.system;
|
||||||
|
|
||||||
ThemeMode _currentTheme = ThemeMode.light;
|
ThemeMode _currentTheme = ThemeMode.light;
|
||||||
|
|
||||||
/// The currently active theme mode (light or dark).
|
/// The currently active theme mode (light or dark).
|
||||||
ThemeMode get currentTheme => _currentTheme;
|
ThemeMode get currentTheme => _currentTheme;
|
||||||
|
|
||||||
/// A listenable that notifies listeners when the system theme mode changes.
|
|
||||||
ThemeMode get systemTheme => I._systemThemeNotifier.value;
|
|
||||||
|
|
||||||
/// 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());
|
||||||
|
|
||||||
@@ -70,14 +77,22 @@ class ArcaneReactiveTheme extends ArcaneService {
|
|||||||
/// ValueNotifier for the light theme that can be observed for changes.
|
/// ValueNotifier for the light theme that can be observed for changes.
|
||||||
ValueNotifier<ThemeData> get lightTheme => I._lightTheme;
|
ValueNotifier<ThemeData> get lightTheme => I._lightTheme;
|
||||||
|
|
||||||
|
/// Returns the current `ThemeMode` being used by `ArcaneReactiveTheme`.
|
||||||
|
/// Will automatically update when the theme changes.
|
||||||
|
ThemeMode currentModeOf(BuildContext context) => context.themeMode;
|
||||||
|
|
||||||
/// Switches the current theme between light and dark modes.
|
/// Switches the current theme between light and dark modes.
|
||||||
///
|
///
|
||||||
/// If the theme is currently light, it switches to dark, and vice versa. It also
|
/// If the theme is currently light, it switches to dark, and vice versa. It
|
||||||
/// notifies listeners to update the UI accordingly.
|
/// also notifies listeners to update the UI accordingly.
|
||||||
///
|
///
|
||||||
/// Example:
|
/// Example:
|
||||||
/// ```dart
|
/// ```dart
|
||||||
/// ArcaneReactiveTheme.I.switchTheme();
|
/// ArcaneReactiveTheme.I.switchTheme();
|
||||||
|
/// // or
|
||||||
|
/// ArcaneReactiveTheme.I.switchTheme(themeMode: ThemeMode.dark);
|
||||||
|
/// // or
|
||||||
|
/// Arcane.theme.switchTheme(themeMode: ThemeMode.light);
|
||||||
/// ```
|
/// ```
|
||||||
ArcaneReactiveTheme switchTheme({ThemeMode? themeMode}) {
|
ArcaneReactiveTheme switchTheme({ThemeMode? themeMode}) {
|
||||||
_followingSystemTheme = false;
|
_followingSystemTheme = false;
|
||||||
@@ -103,40 +118,25 @@ class ArcaneReactiveTheme extends ArcaneService {
|
|||||||
/// Example:
|
/// Example:
|
||||||
/// ```dart
|
/// ```dart
|
||||||
/// ArcaneReactiveTheme.I.followSystemTheme(context);
|
/// ArcaneReactiveTheme.I.followSystemTheme(context);
|
||||||
|
/// // or
|
||||||
|
/// Arcane.theme.followSystemTheme(context);
|
||||||
/// ```
|
/// ```
|
||||||
ArcaneReactiveTheme followSystemTheme(BuildContext context) {
|
ArcaneReactiveTheme followSystemTheme(BuildContext context) {
|
||||||
_followingSystemTheme = true;
|
_followingSystemTheme = true;
|
||||||
|
|
||||||
// Always check the system theme when this method is called
|
_currentSystemTheme = context.isDarkMode ? ThemeMode.dark : ThemeMode.light;
|
||||||
checkSystemTheme(context);
|
_systemStreamController.add(_currentSystemTheme);
|
||||||
|
_updateTheme(_currentSystemTheme);
|
||||||
|
|
||||||
|
notifyListeners();
|
||||||
|
|
||||||
return I;
|
return I;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Check and apply the system theme if we're following it
|
|
||||||
///
|
|
||||||
/// This is called automatically when the system brightness changes if
|
|
||||||
/// [followSystemTheme] has been enabled.
|
|
||||||
void checkSystemTheme(BuildContext context) {
|
|
||||||
if (!_followingSystemTheme) return;
|
|
||||||
|
|
||||||
final Brightness systemBrightness =
|
|
||||||
MediaQuery.platformBrightnessOf(context);
|
|
||||||
|
|
||||||
final ThemeMode systemMode =
|
|
||||||
systemBrightness == Brightness.dark ? ThemeMode.dark : ThemeMode.light;
|
|
||||||
|
|
||||||
// Only update and notify if the theme actually changed
|
|
||||||
if (systemMode != _currentTheme) {
|
|
||||||
_updateTheme(systemMode);
|
|
||||||
notifyListeners();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Sets a custom `ThemeData` for the dark theme.
|
/// Sets a custom `ThemeData` for the dark theme.
|
||||||
///
|
///
|
||||||
/// This allows you to customize the dark theme and notify listeners to apply the
|
/// This allows you to customize the dark theme and notify listeners to apply
|
||||||
/// changes immediately.
|
/// the changes immediately.
|
||||||
///
|
///
|
||||||
/// Example:
|
/// Example:
|
||||||
/// ```dart
|
/// ```dart
|
||||||
@@ -150,8 +150,8 @@ class ArcaneReactiveTheme extends ArcaneService {
|
|||||||
|
|
||||||
/// Sets a custom `ThemeData` for the light theme.
|
/// Sets a custom `ThemeData` for the light theme.
|
||||||
///
|
///
|
||||||
/// This allows you to customize the light theme and notify listeners to apply the
|
/// This allows you to customize the light theme and notify listeners to apply
|
||||||
/// changes immediately.
|
/// the changes immediately.
|
||||||
///
|
///
|
||||||
/// Example:
|
/// Example:
|
||||||
/// ```dart
|
/// ```dart
|
||||||
|
|||||||
@@ -0,0 +1,47 @@
|
|||||||
|
import "dart:async";
|
||||||
|
|
||||||
|
import "package:arcane_framework/arcane_framework.dart";
|
||||||
|
import "package:flutter/material.dart";
|
||||||
|
|
||||||
|
class ArcaneThemeSwitcher extends StatefulWidget {
|
||||||
|
final Widget child;
|
||||||
|
|
||||||
|
const ArcaneThemeSwitcher({
|
||||||
|
required this.child,
|
||||||
|
Key? key,
|
||||||
|
}) : super(key: key);
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<ArcaneThemeSwitcher> createState() => _ArcaneThemeSwitcherState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _ArcaneThemeSwitcherState extends State<ArcaneThemeSwitcher> {
|
||||||
|
late final StreamSubscription<ThemeMode> _themeModeSubscription;
|
||||||
|
ThemeMode _currentThemeMode = ArcaneReactiveTheme.I.currentTheme;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
_themeModeSubscription =
|
||||||
|
ArcaneReactiveTheme.I.themeChanges.listen((ThemeMode newMode) {
|
||||||
|
if (mounted)
|
||||||
|
setState(() {
|
||||||
|
_currentThemeMode = newMode;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
_themeModeSubscription.cancel();
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return ArcaneTheme(
|
||||||
|
themeMode: _currentThemeMode,
|
||||||
|
child: widget.child,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,3 +1,5 @@
|
|||||||
|
import "dart:async";
|
||||||
|
|
||||||
import "package:arcane_framework/src/services/reactive_theme/reactive_theme_service.dart";
|
import "package:arcane_framework/src/services/reactive_theme/reactive_theme_service.dart";
|
||||||
import "package:flutter/material.dart";
|
import "package:flutter/material.dart";
|
||||||
|
|
||||||
@@ -16,12 +18,12 @@ class ArcaneTheme extends InheritedWidget {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
bool updateShouldNotify(ArcaneTheme oldWidget) {
|
bool updateShouldNotify(ArcaneTheme oldWidget) {
|
||||||
return oldWidget.themeMode != themeMode;
|
return themeMode != oldWidget.themeMode;
|
||||||
}
|
}
|
||||||
|
|
||||||
static ArcaneReactiveTheme get service => ArcaneReactiveTheme.I;
|
static ArcaneReactiveTheme get service => ArcaneReactiveTheme.I;
|
||||||
static bool get isFollowingSystemTheme => service.isFollowingSystemTheme;
|
static bool get isFollowingSystemTheme => service.isFollowingSystemTheme;
|
||||||
static Stream<ThemeMode> get currentThemeStream => service.currentThemeStream;
|
static Stream<ThemeMode> get themeChanges => service.themeChanges;
|
||||||
static ThemeMode get currentTheme => service.currentTheme;
|
static ThemeMode get currentTheme => service.currentTheme;
|
||||||
static ThemeMode get systemTheme => service.systemTheme;
|
static ThemeMode get systemTheme => service.systemTheme;
|
||||||
static ThemeData get dark => service.dark;
|
static ThemeData get dark => service.dark;
|
||||||
@@ -33,7 +35,6 @@ class ArcaneTheme extends InheritedWidget {
|
|||||||
service.switchTheme;
|
service.switchTheme;
|
||||||
static ArcaneReactiveTheme Function(BuildContext context)
|
static ArcaneReactiveTheme Function(BuildContext context)
|
||||||
get followSystemTheme => service.followSystemTheme;
|
get followSystemTheme => service.followSystemTheme;
|
||||||
static void get checkSystemTheme => service.checkSystemTheme;
|
|
||||||
static ArcaneReactiveTheme Function(ThemeData theme) get setDarkTheme =>
|
static ArcaneReactiveTheme Function(ThemeData theme) get setDarkTheme =>
|
||||||
service.setDarkTheme;
|
service.setDarkTheme;
|
||||||
static ArcaneReactiveTheme Function(ThemeData theme) get setLightTheme =>
|
static ArcaneReactiveTheme Function(ThemeData theme) get setLightTheme =>
|
||||||
|
|||||||
@@ -96,44 +96,33 @@ void main() {
|
|||||||
await tester.pumpWidget(
|
await tester.pumpWidget(
|
||||||
const MediaQuery(
|
const MediaQuery(
|
||||||
data: MediaQueryData(platformBrightness: Brightness.light),
|
data: MediaQueryData(platformBrightness: Brightness.light),
|
||||||
|
child: ArcaneApp(
|
||||||
child: SizedBox(),
|
child: SizedBox(),
|
||||||
),
|
),
|
||||||
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
final BuildContext lightContext = tester.element(find.byType(SizedBox));
|
final BuildContext lightContext = tester.element(find.byType(SizedBox));
|
||||||
theme.followSystemTheme(lightContext);
|
Arcane.theme.followSystemTheme(lightContext);
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
|
||||||
expect(theme.currentTheme, equals(ThemeMode.light));
|
expect(theme.currentTheme, equals(ThemeMode.light));
|
||||||
|
|
||||||
await tester.pumpWidget(
|
await tester.pumpWidget(
|
||||||
const MediaQuery(
|
const MediaQuery(
|
||||||
data: MediaQueryData(platformBrightness: Brightness.dark),
|
data: MediaQueryData(platformBrightness: Brightness.dark),
|
||||||
|
child: ArcaneApp(
|
||||||
child: SizedBox(),
|
child: SizedBox(),
|
||||||
),
|
),
|
||||||
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
final BuildContext darkContext = tester.element(find.byType(SizedBox));
|
final BuildContext darkContext = tester.element(find.byType(SizedBox));
|
||||||
theme.followSystemTheme(darkContext);
|
Arcane.theme.followSystemTheme(darkContext);
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
|
||||||
expect(theme.currentTheme, equals(ThemeMode.dark));
|
expect(theme.currentTheme, equals(ThemeMode.dark));
|
||||||
});
|
});
|
||||||
|
|
||||||
testWidgets("followSystemTheme only switches when needed",
|
|
||||||
(WidgetTester tester) async {
|
|
||||||
await tester.pumpWidget(
|
|
||||||
const MediaQuery(
|
|
||||||
data: MediaQueryData(platformBrightness: Brightness.light),
|
|
||||||
child: SizedBox(),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
final BuildContext lightContext = tester.element(find.byType(SizedBox));
|
|
||||||
|
|
||||||
int switchCount = 0;
|
|
||||||
theme.addListener(() => switchCount++);
|
|
||||||
|
|
||||||
// Already light, shouldn't switch
|
|
||||||
theme.followSystemTheme(lightContext);
|
|
||||||
expect(switchCount, equals(0));
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user