import 'dart:async'; import 'package:flutter/material.dart'; import '../../data/repositories/settings_repository.dart'; import 'theme_preference.dart'; /// Resolves the light/dark choice, including the fixed time-of-day schedule. /// /// Automatic mode is dark from [nightStartMinutes] until [nightEndMinutes], /// wrapping around midnight (19:00 -> 07:00 by default). A timer wakes exactly /// at the next boundary so the app can switch without polling. class ThemeController extends ChangeNotifier { ThemeController(this._settings) { _preference = _settings.themePreference; _nightStartMinutes = _settings.nightStartMinutes; _nightEndMinutes = _settings.nightEndMinutes; _resolvedDark = _computeDark(DateTime.now()); _scheduleBoundary(); } final SettingsRepository _settings; ThemePreference _preference = ThemePreference.automatic; int _nightStartMinutes = 19 * 60; int _nightEndMinutes = 7 * 60; bool _resolvedDark = false; Timer? _boundaryTimer; bool _disposed = false; ThemePreference get preference => _preference; int get nightStartMinutes => _nightStartMinutes; int get nightEndMinutes => _nightEndMinutes; bool get isDark => _resolvedDark; ThemeMode get themeMode => _resolvedDark ? ThemeMode.dark : ThemeMode.light; Future setPreference(ThemePreference preference) async { if (_preference == preference) return; _preference = preference; _resolvedDark = _computeDark(DateTime.now()); await _settings.setThemePreference(preference); _scheduleBoundary(); _notify(); } Future setNightWindow({ required int startMinutes, required int endMinutes, }) async { _nightStartMinutes = startMinutes.clamp(0, 1439); _nightEndMinutes = endMinutes.clamp(0, 1439); await _settings.setNightWindow( startMinutes: _nightStartMinutes, endMinutes: _nightEndMinutes, ); _resolvedDark = _computeDark(DateTime.now()); _scheduleBoundary(); _notify(); } /// Re-evaluates the schedule after the app resumes or a settings page closes. void refreshNow() { if (_preference != ThemePreference.automatic) return; final next = _computeDark(DateTime.now()); if (next != _resolvedDark) { _resolvedDark = next; _notify(); } _scheduleBoundary(); } bool _computeDark(DateTime now) { switch (_preference) { case ThemePreference.light: return false; case ThemePreference.dark: return true; case ThemePreference.automatic: final minutes = now.hour * 60 + now.minute; if (_nightStartMinutes == _nightEndMinutes) return false; if (_nightStartMinutes < _nightEndMinutes) { return minutes >= _nightStartMinutes && minutes < _nightEndMinutes; } return minutes >= _nightStartMinutes || minutes < _nightEndMinutes; } } void _scheduleBoundary() { _boundaryTimer?.cancel(); _boundaryTimer = null; if (_preference != ThemePreference.automatic) return; final now = DateTime.now(); final next = _nextBoundary(now); if (next == null) return; _boundaryTimer = Timer( next.difference(now) + const Duration(seconds: 1), () { _resolvedDark = _computeDark(DateTime.now()); _notify(); _scheduleBoundary(); }, ); } DateTime? _nextBoundary(DateTime now) { DateTime at(int minutes) { return DateTime( now.year, now.month, now.day, minutes ~/ 60, minutes % 60, ); } final candidates = []; for (final minutes in {_nightStartMinutes, _nightEndMinutes}) { var candidate = at(minutes); if (!candidate.isAfter(now)) { candidate = candidate.add(const Duration(days: 1)); } candidates.add(candidate); } if (candidates.isEmpty) return null; candidates.sort(); return candidates.first; } void _notify() { if (!_disposed) notifyListeners(); } @override void dispose() { _disposed = true; _boundaryTimer?.cancel(); super.dispose(); } }