theme_controller.dart 4.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140
  1. import 'dart:async';
  2. import 'package:flutter/material.dart';
  3. import '../../data/repositories/settings_repository.dart';
  4. import 'theme_preference.dart';
  5. /// Resolves the light/dark choice, including the fixed time-of-day schedule.
  6. ///
  7. /// Automatic mode is dark from [nightStartMinutes] until [nightEndMinutes],
  8. /// wrapping around midnight (19:00 -> 07:00 by default). A timer wakes exactly
  9. /// at the next boundary so the app can switch without polling.
  10. class ThemeController extends ChangeNotifier {
  11. ThemeController(this._settings) {
  12. _preference = _settings.themePreference;
  13. _nightStartMinutes = _settings.nightStartMinutes;
  14. _nightEndMinutes = _settings.nightEndMinutes;
  15. _resolvedDark = _computeDark(DateTime.now());
  16. _scheduleBoundary();
  17. }
  18. final SettingsRepository _settings;
  19. ThemePreference _preference = ThemePreference.automatic;
  20. int _nightStartMinutes = 19 * 60;
  21. int _nightEndMinutes = 7 * 60;
  22. bool _resolvedDark = false;
  23. Timer? _boundaryTimer;
  24. bool _disposed = false;
  25. ThemePreference get preference => _preference;
  26. int get nightStartMinutes => _nightStartMinutes;
  27. int get nightEndMinutes => _nightEndMinutes;
  28. bool get isDark => _resolvedDark;
  29. ThemeMode get themeMode => _resolvedDark ? ThemeMode.dark : ThemeMode.light;
  30. Future<void> setPreference(ThemePreference preference) async {
  31. if (_preference == preference) return;
  32. _preference = preference;
  33. _resolvedDark = _computeDark(DateTime.now());
  34. await _settings.setThemePreference(preference);
  35. _scheduleBoundary();
  36. _notify();
  37. }
  38. Future<void> setNightWindow({
  39. required int startMinutes,
  40. required int endMinutes,
  41. }) async {
  42. _nightStartMinutes = startMinutes.clamp(0, 1439);
  43. _nightEndMinutes = endMinutes.clamp(0, 1439);
  44. await _settings.setNightWindow(
  45. startMinutes: _nightStartMinutes,
  46. endMinutes: _nightEndMinutes,
  47. );
  48. _resolvedDark = _computeDark(DateTime.now());
  49. _scheduleBoundary();
  50. _notify();
  51. }
  52. /// Re-evaluates the schedule after the app resumes or a settings page closes.
  53. void refreshNow() {
  54. if (_preference != ThemePreference.automatic) return;
  55. final next = _computeDark(DateTime.now());
  56. if (next != _resolvedDark) {
  57. _resolvedDark = next;
  58. _notify();
  59. }
  60. _scheduleBoundary();
  61. }
  62. bool _computeDark(DateTime now) {
  63. switch (_preference) {
  64. case ThemePreference.light:
  65. return false;
  66. case ThemePreference.dark:
  67. return true;
  68. case ThemePreference.automatic:
  69. final minutes = now.hour * 60 + now.minute;
  70. if (_nightStartMinutes == _nightEndMinutes) return false;
  71. if (_nightStartMinutes < _nightEndMinutes) {
  72. return minutes >= _nightStartMinutes && minutes < _nightEndMinutes;
  73. }
  74. return minutes >= _nightStartMinutes || minutes < _nightEndMinutes;
  75. }
  76. }
  77. void _scheduleBoundary() {
  78. _boundaryTimer?.cancel();
  79. _boundaryTimer = null;
  80. if (_preference != ThemePreference.automatic) return;
  81. final now = DateTime.now();
  82. final next = _nextBoundary(now);
  83. if (next == null) return;
  84. _boundaryTimer = Timer(
  85. next.difference(now) + const Duration(seconds: 1),
  86. () {
  87. _resolvedDark = _computeDark(DateTime.now());
  88. _notify();
  89. _scheduleBoundary();
  90. },
  91. );
  92. }
  93. DateTime? _nextBoundary(DateTime now) {
  94. DateTime at(int minutes) {
  95. return DateTime(
  96. now.year,
  97. now.month,
  98. now.day,
  99. minutes ~/ 60,
  100. minutes % 60,
  101. );
  102. }
  103. final candidates = <DateTime>[];
  104. for (final minutes in {_nightStartMinutes, _nightEndMinutes}) {
  105. var candidate = at(minutes);
  106. if (!candidate.isAfter(now)) {
  107. candidate = candidate.add(const Duration(days: 1));
  108. }
  109. candidates.add(candidate);
  110. }
  111. if (candidates.isEmpty) return null;
  112. candidates.sort();
  113. return candidates.first;
  114. }
  115. void _notify() {
  116. if (!_disposed) notifyListeners();
  117. }
  118. @override
  119. void dispose() {
  120. _disposed = true;
  121. _boundaryTimer?.cancel();
  122. super.dispose();
  123. }
  124. }