theme_controller.dart 4.6 KB

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