diff --git a/lib/services/player_service.dart b/lib/services/player_service.dart index 41ad637..b9dc25a 100644 --- a/lib/services/player_service.dart +++ b/lib/services/player_service.dart @@ -33,6 +33,10 @@ class PlayerService extends ChangeNotifier { // ─── Wiederholmodus (Repeat) ─── Wiederholmodus _wiederholmodus = Wiederholmodus.aus; + // ─── Sleep-Timer ─── + Timer? _sleepTimer; + DateTime? _sleepEnde; + AudioPlayer get _p { if (_player == null) { _player = AudioPlayer(); @@ -301,9 +305,43 @@ class PlayerService extends ChangeNotifier { notifyListeners(); } + // ─── Sleep-Timer ─── + + bool get sleepTimerAktiv => _sleepTimer != null; + + /// Verbleibende Zeit bis zum automatischen Pausieren (null wenn inaktiv). + Duration? get sleepRestzeit { + final ende = _sleepEnde; + if (ende == null) return null; + final rest = ende.difference(DateTime.now()); + return rest.isNegative ? Duration.zero : rest; + } + + /// Startet den Sleep-Timer — nach [dauer] wird die Wiedergabe pausiert. + void setSleepTimer(Duration dauer) { + cancelSleepTimer(); + _sleepEnde = DateTime.now().add(dauer); + _sleepTimer = Timer(dauer, () { + _sleepTimer = null; + _sleepEnde = null; + pause(); + notifyListeners(); + }); + notifyListeners(); + } + + /// Bricht den Sleep-Timer ab. + void cancelSleepTimer() { + _sleepTimer?.cancel(); + _sleepTimer = null; + _sleepEnde = null; + notifyListeners(); + } + @override void dispose() { _autoNextSub?.cancel(); + _sleepTimer?.cancel(); _player?.dispose(); _player = null; _songWechsel.close(); diff --git a/lib/widgets/warteschlange_sheet.dart b/lib/widgets/warteschlange_sheet.dart index 854dc6c..cf42796 100644 --- a/lib/widgets/warteschlange_sheet.dart +++ b/lib/widgets/warteschlange_sheet.dart @@ -1,3 +1,4 @@ +import 'dart:async'; import 'package:flutter/material.dart'; import '../services/player_service.dart'; import '../models/song.dart'; @@ -14,6 +15,22 @@ class WarteschlangeSheet extends StatefulWidget { class _WarteschlangeSheetState extends State { final PlayerService _player = PlayerService(); + Timer? _tick; + + @override + void initState() { + super.initState(); + // Sekunden-Tick für die Sleep-Timer-Restzeitanzeige + _tick = Timer.periodic(const Duration(seconds: 1), (_) { + if (mounted && _player.sleepTimerAktiv) setState(() {}); + }); + } + + @override + void dispose() { + _tick?.cancel(); + super.dispose(); + } @override Widget build(BuildContext context) { @@ -77,6 +94,8 @@ class _WarteschlangeSheetState extends State { ), ), const Divider(height: 1, color: MeloTheme.dunkel2), + _sleepTimerZeile(), + const Divider(height: 1, color: MeloTheme.dunkel2), // Liste if (queue.isEmpty) const Expanded( @@ -108,6 +127,75 @@ class _WarteschlangeSheetState extends State { ); } + /// Sleep-Timer-Zeile: Optionen Aus/15/30/45/60 Min + Restzeit wenn aktiv + Widget _sleepTimerZeile() { + final aktiv = _player.sleepTimerAktiv; + final rest = _player.sleepRestzeit; + return Padding( + padding: const EdgeInsets.fromLTRB(20, 8, 20, 8), + child: Row( + children: [ + Icon( + Icons.bedtime, + size: 16, + color: aktiv ? MeloTheme.rot : MeloTheme.textSekundaer, + ), + const SizedBox(width: 8), + const Text( + 'Schlaf-Timer', + style: TextStyle(fontSize: 13, color: Colors.white), + ), + if (aktiv && rest != null) ...[ + const SizedBox(width: 8), + Text( + _formatRestzeit(rest), + style: const TextStyle(fontSize: 12, color: MeloTheme.rot), + ), + ], + const Spacer(), + _zeitChip('Aus', null, aktiv: !aktiv), + _zeitChip('15', const Duration(minutes: 15)), + _zeitChip('30', const Duration(minutes: 30)), + _zeitChip('45', const Duration(minutes: 45)), + _zeitChip('60', const Duration(minutes: 60)), + ], + ), + ); + } + + Widget _zeitChip(String label, Duration? dauer, {bool aktiv = false}) { + return GestureDetector( + onTap: () { + if (dauer == null) { + _player.cancelSleepTimer(); + } else { + _player.setSleepTimer(dauer); + } + }, + child: Container( + margin: const EdgeInsets.only(left: 6), + padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4), + decoration: BoxDecoration( + color: aktiv ? MeloTheme.rot : MeloTheme.dunkel2, + borderRadius: BorderRadius.circular(12), + ), + child: Text( + label, + style: TextStyle( + fontSize: 11, + color: aktiv ? Colors.white : MeloTheme.textSekundaer, + ), + ), + ), + ); + } + + String _formatRestzeit(Duration rest) { + final min = rest.inMinutes; + final sek = rest.inSeconds % 60; + return '⏱ ${min.toString().padLeft(2, '0')}:${sek.toString().padLeft(2, '0')}'; + } + Widget _eintrag(Song song, int index, int aktueller) { final istAktuell = index == aktueller; return ListTile(