From 32b0ad48225f74e61f7b679ca6275da64875b287 Mon Sep 17 00:00:00 2001 From: ZhuchkaTriplesix Date: Fri, 25 Sep 2026 12:15:58 +0300 Subject: [PATCH] fix(theme): listen to platformDispatcher.onPlatformBrightnessChanged for ThemeMode.system (Closes #899) --- lib/core/theme/theme_controller.dart | 55 +++++++++++++++++- test/core/theme/theme_controller_test.dart | 67 ++++++++++++++++++++++ 2 files changed, 119 insertions(+), 3 deletions(-) diff --git a/lib/core/theme/theme_controller.dart b/lib/core/theme/theme_controller.dart index 148c40a..497e7b7 100644 --- a/lib/core/theme/theme_controller.dart +++ b/lib/core/theme/theme_controller.dart @@ -24,7 +24,7 @@ import 'theme_remote_install_service.dart'; import '../extensions/extension_paths.dart'; /// Active theme state: preset, optional imported colors, user overrides. -class ThemeController extends ChangeNotifier { +class ThemeController extends ChangeNotifier with WidgetsBindingObserver { static const String builtinQueryaDarkId = 'querya-dark'; static const String builtinQueryaLightId = 'querya-light'; @@ -79,6 +79,8 @@ class ThemeController extends ChangeNotifier { ThemeFolderWatcher? _themeFolderWatcher; bool _editorPreviewActive = false; String? _editorPreviewRestoreThemeId; + Brightness? _lastPlatformBrightness; + bool _isObservingBrightness = false; QueryaTheme? _cachedLightTheme; QueryaTheme? _cachedDarkTheme; @@ -90,6 +92,9 @@ class ThemeController extends ChangeNotifier { ThemeMode get themeMode => _themeMode; + @visibleForTesting + bool get isObservingPlatformBrightness => _isObservingBrightness; + /// Preference toggle for theme cross-fades. /// /// [QueryaApp] still gates animation with [QueryaThemeMotion] (motion level / @@ -238,8 +243,21 @@ class ThemeController extends ChangeNotifier { await _themeFolderWatcher?.stop(); } + WidgetsBinding? get _binding { + try { + return WidgetsBinding.instance; + } catch (_) { + return null; + } + } + @override void dispose() { + if (_isObservingBrightness) { + _binding?.removeObserver(this); + _isObservingBrightness = false; + _lastPlatformBrightness = null; + } unawaited(stopThemeFolderWatcher()); super.dispose(); } @@ -254,7 +272,35 @@ class ThemeController extends ChangeNotifier { _cachedMaterialThemeScheme = null; } + void _updateBrightnessObserver() { + final binding = _binding; + if (binding == null) return; + final shouldObserve = _themeMode == ThemeMode.system; + if (shouldObserve && !_isObservingBrightness) { + _lastPlatformBrightness = binding.platformDispatcher.platformBrightness; + binding.addObserver(this); + _isObservingBrightness = true; + } else if (!shouldObserve && _isObservingBrightness) { + binding.removeObserver(this); + _isObservingBrightness = false; + _lastPlatformBrightness = null; + } + } + + @override + void didChangePlatformBrightness() { + if (_themeMode != ThemeMode.system) return; + final binding = _binding; + if (binding == null) return; + final current = binding.platformDispatcher.platformBrightness; + if (_lastPlatformBrightness != current) { + _lastPlatformBrightness = current; + _notifyThemeChanged(); + } + } + void _notifyThemeChanged() { + _updateBrightnessObserver(); _invalidateThemeCache(); notifyListeners(); } @@ -660,8 +706,11 @@ class ThemeController extends ChangeNotifier { Brightness _effectiveBrightness() { if (_themeMode == ThemeMode.system) { - final b = WidgetsBinding.instance.platformDispatcher.platformBrightness; - return b; + final binding = _binding; + if (binding != null) { + return binding.platformDispatcher.platformBrightness; + } + return Brightness.dark; } return _themeMode == ThemeMode.light ? Brightness.light : Brightness.dark; } diff --git a/test/core/theme/theme_controller_test.dart b/test/core/theme/theme_controller_test.dart index 0be27a8..de342f3 100644 --- a/test/core/theme/theme_controller_test.dart +++ b/test/core/theme/theme_controller_test.dart @@ -68,6 +68,7 @@ void main() { late ThemeRegistryService registry; setUpAll(() async { + TestWidgetsFlutterBinding.ensureInitialized(); tempDir = await Directory.systemTemp.createTemp('querya_theme_controller_test_'); PathProviderPlatform.instance = _FakePathProvider(tempDir.path); @@ -498,4 +499,70 @@ void main() { c.activeTheme.colorScheme.primary, parseQueryaThemeColor('#38BDF8')); }); }); + + group('ThemeMode.system platform brightness listener (#899)', () { + test( + 'listens to platform brightness when ThemeMode.system and updates activeTheme', + () async { + final c = ThemeController.instance; + await c.load(); + + // Initially not system, so should not observe + await c.setThemeMode(ThemeMode.dark); + expect(c.isObservingPlatformBrightness, isFalse); + + // Set to system mode with dark platform brightness + TestWidgetsFlutterBinding.instance.platformDispatcher + .platformBrightnessTestValue = Brightness.dark; + await c.setThemeMode(ThemeMode.system); + expect(c.themeMode, ThemeMode.system); + expect(c.isObservingPlatformBrightness, isTrue); + expect(c.activeTheme.brightness, Brightness.dark); + + // Track listener notifications + var notifyCount = 0; + void listener() => notifyCount++; + c.addListener(listener); + + try { + // Change platform brightness to light + TestWidgetsFlutterBinding.instance.platformDispatcher + .platformBrightnessTestValue = Brightness.light; + c.didChangePlatformBrightness(); + + expect(notifyCount, 1); + expect(c.activeTheme.brightness, Brightness.light); + + // Repeating same brightness does not fire redundant notification + c.didChangePlatformBrightness(); + expect(notifyCount, 1); + + // Change back to dark + TestWidgetsFlutterBinding.instance.platformDispatcher + .platformBrightnessTestValue = Brightness.dark; + c.didChangePlatformBrightness(); + expect(notifyCount, 2); + expect(c.activeTheme.brightness, Brightness.dark); + + // Switching away from system unregisters observer + await c.setThemeMode(ThemeMode.light); + expect(c.isObservingPlatformBrightness, isFalse); + } finally { + c.removeListener(listener); + TestWidgetsFlutterBinding.instance.platformDispatcher + .clearPlatformBrightnessTestValue(); + } + }); + + test('cleans up brightness observer on resetToDefaults', () async { + final c = ThemeController.instance; + await c.load(); + await c.setThemeMode(ThemeMode.system); + expect(c.isObservingPlatformBrightness, isTrue); + + await c.resetToDefaults(); + expect(c.themeMode, ThemeMode.dark); + expect(c.isObservingPlatformBrightness, isFalse); + }); + }); }