Memory Leak Fixes: - Add max pending requests limit (100) to prevent unbounded growth - Cancel pending requests in _reconnect() before reconnecting - Track ImageStream listeners in expandable_player for proper cleanup - Add cleanup in dispose() for active image stream listeners Database Performance: - Add batchCacheItems() method using drift batch API - Chunk batches at 500 items to avoid SQLite limits - Refactor sync_service to use batch inserts (~10x faster) - Add BatchCacheItem data class for type-safe batch operations Race Condition Fix: - Make connection guard check-and-set atomic - Create completer before check to close race window WCAG Accessibility: - Mark ButtonSizes.sm/md as @Deprecated (below 44x44 minimum) - Add ButtonSizes.minAccessible constant (48x48) - Add clear documentation on WCAG Level AA/AAA requirements Provider Decomposition (partial): - Extract SleepTimerProvider with callback pattern - Extract ConnectionProvider skeleton for future integration - Prepare facade pattern for backward compatibility Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
130 lines
3.8 KiB
Dart
130 lines
3.8 KiB
Dart
import 'dart:async';
|
|
import 'package:flutter/foundation.dart';
|
|
import '../services/debug_logger.dart';
|
|
|
|
/// Callback type for when sleep timer expires and should pause playback
|
|
typedef OnSleepTimerExpired = void Function();
|
|
|
|
/// Provider for sleep timer functionality.
|
|
///
|
|
/// Extracted from MusicAssistantProvider to reduce complexity and improve testability.
|
|
/// This provider manages:
|
|
/// - Timed sleep (pause after X minutes)
|
|
/// - End of track sleep (pause when current track ends)
|
|
class SleepTimerProvider with ChangeNotifier {
|
|
final DebugLogger _logger = DebugLogger();
|
|
|
|
Timer? _sleepTimer;
|
|
DateTime? _sleepTimerEndTime;
|
|
int? _sleepTimerMinutes; // null = off, -1 = end of track, positive = minutes
|
|
Timer? _sleepTimerDisplayTimer;
|
|
|
|
/// Callback to pause playback when timer expires
|
|
OnSleepTimerExpired? onExpired;
|
|
|
|
// ============================================================================
|
|
// GETTERS
|
|
// ============================================================================
|
|
|
|
/// Whether a sleep timer is currently active
|
|
bool get isActive => _sleepTimerMinutes != null;
|
|
|
|
/// Current timer setting: null = off, -1 = end of track, positive = minutes
|
|
int? get minutes => _sleepTimerMinutes;
|
|
|
|
/// Time remaining until sleep timer expires (null if end-of-track mode or inactive)
|
|
Duration? get remaining {
|
|
if (_sleepTimerEndTime == null) return null;
|
|
final remaining = _sleepTimerEndTime!.difference(DateTime.now());
|
|
return remaining.isNegative ? Duration.zero : remaining;
|
|
}
|
|
|
|
// ============================================================================
|
|
// TIMER CONTROL
|
|
// ============================================================================
|
|
|
|
/// Set sleep timer. Pass null to turn off, -1 for end of track, or minutes.
|
|
void setTimer(int? minutes) {
|
|
// Cancel any existing timer
|
|
_cancelInternal();
|
|
|
|
_sleepTimerMinutes = minutes;
|
|
|
|
if (minutes == null) {
|
|
// Timer off
|
|
_logger.log('😴 Sleep timer: OFF');
|
|
notifyListeners();
|
|
return;
|
|
}
|
|
|
|
if (minutes == -1) {
|
|
// End of track - handled in player state updates
|
|
_sleepTimerEndTime = null; // No fixed end time
|
|
_logger.log('😴 Sleep timer: End of track');
|
|
notifyListeners();
|
|
return;
|
|
}
|
|
|
|
// Set timer for specified minutes
|
|
_sleepTimerEndTime = DateTime.now().add(Duration(minutes: minutes));
|
|
_logger.log('😴 Sleep timer: $minutes minutes (until $_sleepTimerEndTime)');
|
|
|
|
// Create the actual timer
|
|
_sleepTimer = Timer(Duration(minutes: minutes), _onTimerExpired);
|
|
|
|
// Start display update timer (every second for countdown)
|
|
_sleepTimerDisplayTimer = Timer.periodic(const Duration(seconds: 1), (_) {
|
|
notifyListeners(); // Update UI with remaining time
|
|
});
|
|
|
|
notifyListeners();
|
|
}
|
|
|
|
/// Cancel sleep timer
|
|
void cancel() {
|
|
_cancelInternal();
|
|
_sleepTimerMinutes = null;
|
|
_sleepTimerEndTime = null;
|
|
_logger.log('😴 Sleep timer: Cancelled');
|
|
notifyListeners();
|
|
}
|
|
|
|
void _cancelInternal() {
|
|
_sleepTimer?.cancel();
|
|
_sleepTimer = null;
|
|
_sleepTimerDisplayTimer?.cancel();
|
|
_sleepTimerDisplayTimer = null;
|
|
}
|
|
|
|
void _onTimerExpired() {
|
|
_logger.log('😴 Sleep timer expired - pausing playback');
|
|
_cancelInternal();
|
|
_sleepTimerMinutes = null;
|
|
_sleepTimerEndTime = null;
|
|
|
|
// Trigger the callback to pause playback
|
|
onExpired?.call();
|
|
|
|
notifyListeners();
|
|
}
|
|
|
|
/// Called when track ends - checks if "end of track" sleep timer is active
|
|
void checkEndOfTrack() {
|
|
if (_sleepTimerMinutes == -1) {
|
|
_logger.log('😴 End of track sleep timer triggered');
|
|
_sleepTimerMinutes = null;
|
|
|
|
// Trigger the callback to pause playback
|
|
onExpired?.call();
|
|
|
|
notifyListeners();
|
|
}
|
|
}
|
|
|
|
@override
|
|
void dispose() {
|
|
_cancelInternal();
|
|
super.dispose();
|
|
}
|
|
}
|