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,
|
||||
theme: Arcane.theme.light,
|
||||
darkTheme: Arcane.theme.dark,
|
||||
themeMode:
|
||||
ArcaneTheme.of(context)?.themeMode ?? Arcane.theme.currentTheme,
|
||||
themeMode: Arcane.theme.currentModeOf(context),
|
||||
home: Scaffold(
|
||||
appBar: AppBar(
|
||||
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(),
|
||||
),
|
||||
@@ -101,6 +86,49 @@ class _HomeScreenState extends State<HomeScreen> {
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
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(
|
||||
"Authentication status: ${Arcane.auth.status.name}",
|
||||
),
|
||||
|
||||
+4
-17
@@ -1,4 +1,5 @@
|
||||
import "package:arcane_framework/arcane_framework.dart";
|
||||
import "package:arcane_framework/src/services/reactive_theme/reactive_theme_switcher.dart";
|
||||
import "package:flutter/material.dart";
|
||||
|
||||
/// A root widget for an Arcane-powered application.
|
||||
@@ -54,22 +55,9 @@ class _ArcaneAppState extends State<ArcaneApp> with WidgetsBindingObserver {
|
||||
return ArcaneEnvironmentProvider(
|
||||
child: ArcaneServiceProvider(
|
||||
serviceInstances: widget.services,
|
||||
child: Builder(
|
||||
child: ArcaneThemeSwitcher(
|
||||
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
|
||||
if (mounted && _appKey.currentContext != null) {
|
||||
// Use the current context from the key to check system theme
|
||||
final BuildContext currentContext = _appKey.currentContext!;
|
||||
if (ArcaneReactiveTheme.I.isFollowingSystemTheme) {
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
ArcaneReactiveTheme.I.checkSystemTheme(currentContext);
|
||||
ArcaneReactiveTheme.I.followSystemTheme(context);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,3 +19,11 @@ extension DarkMode on BuildContext {
|
||||
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.
|
||||
///
|
||||
/// 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.
|
||||
bool get isFollowingSystemTheme => _followingSystemTheme;
|
||||
|
||||
final ValueNotifier<ThemeMode> _systemThemeNotifier =
|
||||
ValueNotifier(ThemeMode.system);
|
||||
final StreamController<ThemeMode> _systemStreamController =
|
||||
StreamController<ThemeMode>.broadcast(
|
||||
onCancel: () {
|
||||
I._systemStreamController.close();
|
||||
},
|
||||
);
|
||||
|
||||
final StreamController<ThemeMode> _themeStreamController =
|
||||
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<ThemeMode> get currentThemeStream => I._themeStreamController.stream;
|
||||
/// Stream of `ThemeMode` changes that can be listened to for reactive UI updates.
|
||||
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;
|
||||
|
||||
/// The currently active theme mode (light or dark).
|
||||
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.
|
||||
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<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.
|
||||
///
|
||||
/// If the theme is currently light, it switches to dark, and vice versa. It also
|
||||
/// notifies listeners to update the UI accordingly.
|
||||
/// If the theme is currently light, it switches to dark, and vice versa. It
|
||||
/// also notifies listeners to update the UI accordingly.
|
||||
///
|
||||
/// Example:
|
||||
/// ```dart
|
||||
/// ArcaneReactiveTheme.I.switchTheme();
|
||||
/// // or
|
||||
/// ArcaneReactiveTheme.I.switchTheme(themeMode: ThemeMode.dark);
|
||||
/// // or
|
||||
/// Arcane.theme.switchTheme(themeMode: ThemeMode.light);
|
||||
/// ```
|
||||
ArcaneReactiveTheme switchTheme({ThemeMode? themeMode}) {
|
||||
_followingSystemTheme = false;
|
||||
@@ -103,40 +118,25 @@ class ArcaneReactiveTheme extends ArcaneService {
|
||||
/// Example:
|
||||
/// ```dart
|
||||
/// ArcaneReactiveTheme.I.followSystemTheme(context);
|
||||
/// // or
|
||||
/// Arcane.theme.followSystemTheme(context);
|
||||
/// ```
|
||||
ArcaneReactiveTheme followSystemTheme(BuildContext context) {
|
||||
_followingSystemTheme = true;
|
||||
|
||||
// Always check the system theme when this method is called
|
||||
checkSystemTheme(context);
|
||||
_currentSystemTheme = context.isDarkMode ? ThemeMode.dark : ThemeMode.light;
|
||||
_systemStreamController.add(_currentSystemTheme);
|
||||
_updateTheme(_currentSystemTheme);
|
||||
|
||||
notifyListeners();
|
||||
|
||||
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.
|
||||
///
|
||||
/// This allows you to customize the dark theme and notify listeners to apply the
|
||||
/// changes immediately.
|
||||
/// This allows you to customize the dark theme and notify listeners to apply
|
||||
/// the changes immediately.
|
||||
///
|
||||
/// Example:
|
||||
/// ```dart
|
||||
@@ -150,8 +150,8 @@ class ArcaneReactiveTheme extends ArcaneService {
|
||||
|
||||
/// Sets a custom `ThemeData` for the light theme.
|
||||
///
|
||||
/// This allows you to customize the light theme and notify listeners to apply the
|
||||
/// changes immediately.
|
||||
/// This allows you to customize the light theme and notify listeners to apply
|
||||
/// the changes immediately.
|
||||
///
|
||||
/// Example:
|
||||
/// ```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:flutter/material.dart";
|
||||
|
||||
@@ -16,12 +18,12 @@ class ArcaneTheme extends InheritedWidget {
|
||||
|
||||
@override
|
||||
bool updateShouldNotify(ArcaneTheme oldWidget) {
|
||||
return oldWidget.themeMode != themeMode;
|
||||
return themeMode != oldWidget.themeMode;
|
||||
}
|
||||
|
||||
static ArcaneReactiveTheme get service => ArcaneReactiveTheme.I;
|
||||
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 systemTheme => service.systemTheme;
|
||||
static ThemeData get dark => service.dark;
|
||||
@@ -33,7 +35,6 @@ class ArcaneTheme extends InheritedWidget {
|
||||
service.switchTheme;
|
||||
static ArcaneReactiveTheme Function(BuildContext context)
|
||||
get followSystemTheme => service.followSystemTheme;
|
||||
static void get checkSystemTheme => service.checkSystemTheme;
|
||||
static ArcaneReactiveTheme Function(ThemeData theme) get setDarkTheme =>
|
||||
service.setDarkTheme;
|
||||
static ArcaneReactiveTheme Function(ThemeData theme) get setLightTheme =>
|
||||
|
||||
@@ -96,44 +96,33 @@ void main() {
|
||||
await tester.pumpWidget(
|
||||
const MediaQuery(
|
||||
data: MediaQueryData(platformBrightness: Brightness.light),
|
||||
child: SizedBox(),
|
||||
child: ArcaneApp(
|
||||
child: 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));
|
||||
|
||||
await tester.pumpWidget(
|
||||
const MediaQuery(
|
||||
data: MediaQueryData(platformBrightness: Brightness.dark),
|
||||
child: SizedBox(),
|
||||
child: ArcaneApp(
|
||||
child: 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));
|
||||
});
|
||||
|
||||
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