Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
55 changes: 52 additions & 3 deletions lib/core/theme/theme_controller.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand Down Expand Up @@ -79,6 +79,8 @@ class ThemeController extends ChangeNotifier {
ThemeFolderWatcher? _themeFolderWatcher;
bool _editorPreviewActive = false;
String? _editorPreviewRestoreThemeId;
Brightness? _lastPlatformBrightness;
bool _isObservingBrightness = false;

QueryaTheme? _cachedLightTheme;
QueryaTheme? _cachedDarkTheme;
Expand All @@ -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 /
Expand Down Expand Up @@ -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();
}
Expand All @@ -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();
}
Expand Down Expand Up @@ -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;
}
Expand Down
67 changes: 67 additions & 0 deletions test/core/theme/theme_controller_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -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);
});
});
}
Loading