Functional program screen

This implements playlist updates and skipping functionality and updates
the program screen accordingly.
This commit is contained in:
Elias Projahn 2020-04-22 10:01:50 +02:00
parent 5344f16f53
commit 4c0bcb9d26
2 changed files with 125 additions and 2 deletions

View file

@ -23,6 +23,16 @@ class Player {
/// service is ready to play. /// service is ready to play.
final active = BehaviorSubject.seeded(false); final active = BehaviorSubject.seeded(false);
/// The current playlist.
///
/// If the player is not active, this will be an empty list.
final playlist = BehaviorSubject.seeded(<InternalTrack>[]);
/// Index of the currently played (or paused) track within the playlist.
///
/// This will be zero, if the player is not active!
final currentTrack = BehaviorSubject.seeded(0);
/// Whether we are currently playing or not. /// Whether we are currently playing or not.
/// ///
/// This will be false, if the player is not active. /// This will be false, if the player is not active.
@ -52,6 +62,8 @@ class Player {
/// Set everything to its default because the playback service was stopped. /// Set everything to its default because the playback service was stopped.
void _stop() { void _stop() {
active.add(false); active.add(false);
playlist.add([]);
currentTrack.add(0);
playing.add(false); playing.add(false);
position.add(const Duration()); position.add(const Duration());
duration.add(const Duration(seconds: 1)); duration.add(const Duration(seconds: 1));
@ -87,6 +99,8 @@ class Player {
final state = msg as PlaybackServiceState; final state = msg as PlaybackServiceState;
// TODO: Consider checking, whether values have actually changed. // TODO: Consider checking, whether values have actually changed.
playlist.add(state.playlist);
currentTrack.add(state.currentTrack);
playing.add(state.playing); playing.add(state.playing);
position.add(Duration(milliseconds: state.positionMs)); position.add(Duration(milliseconds: state.positionMs));
duration.add(Duration(milliseconds: state.durationMs)); duration.add(Duration(milliseconds: state.durationMs));
@ -138,6 +152,34 @@ class Player {
} }
} }
/// Play the previous track in the playlist.
///
/// If the player is not active or there is no previous track, this will do
/// nothing.
Future<void> skipToNext() async {
if (AudioService.running) {
await AudioService.skipToNext();
}
}
/// Skip to the next track in the playlist.
///
/// If the player is not active or there is no next track, this will do
/// nothing. If more than five seconds of the current track have been played,
/// this will go back to its beginning instead.
Future<void> skipToPrevious() async {
if (AudioService.running) {
await AudioService.skipToPrevious();
}
}
/// Switch to the track with the index [index] in the playlist.
Future<void> skipTo(int index) async {
if (AudioService.running) {
await AudioService.customAction('skipTo', index);
}
}
/// Tidy up. /// Tidy up.
void dispose() { void dispose() {
_playbackServiceStateSubscription.cancel(); _playbackServiceStateSubscription.cancel();
@ -288,6 +330,7 @@ class _PlaybackService extends BackgroundAudioTask {
super.onCustomAction(name, arguments); super.onCustomAction(name, arguments);
// addTracks expects a List<Map<String, dynamic>> as its argument. // addTracks expects a List<Map<String, dynamic>> as its argument.
// skipTo expects an integer as its argument.
if (name == 'addTracks') { if (name == 'addTracks') {
final tracksJson = jsonDecode(arguments); final tracksJson = jsonDecode(arguments);
final List<InternalTrack> tracks = List.castFrom( final List<InternalTrack> tracks = List.castFrom(
@ -297,6 +340,15 @@ class _PlaybackService extends BackgroundAudioTask {
_durationMs = newDurationMs; _durationMs = newDurationMs;
_setState(); _setState();
}); });
}
if (name == 'skipTo') {
final index = arguments as int;
if (index >= 0 && index < _playlist.length) {
_currentTrack = index;
_player.setUri(_playlist[index].uri);
_setState();
}
} else if (name == 'sendState') { } else if (name == 'sendState') {
// Send the current state to the main isolate. // Send the current state to the main isolate.
_setState(); _setState();
@ -331,6 +383,33 @@ class _PlaybackService extends BackgroundAudioTask {
}); });
} }
@override
void onSkipToNext() {
super.onSkipToNext();
if (_playlist.length > 1 && _currentTrack < _playlist.length - 1) {
_currentTrack++;
_player.setUri(_playlist[_currentTrack].uri);
_setState();
}
}
@override
void onSkipToPrevious() async {
super.onSkipToPrevious();
// If more than five seconds of the current track have been played, go back
// to its beginning, else, switch to the previous track.
if (await _player.getPosition() > 5000) {
await _player.setUri(_playlist[_currentTrack].uri);
_setState();
} else if (_playlist.length > 1 && _currentTrack > 0) {
_currentTrack--;
_player.setUri(_playlist[_currentTrack].uri);
_setState();
}
}
@override @override
void onStop() { void onStop() {
_player.stop(); _player.stop();

View file

@ -3,6 +3,7 @@ import 'dart:async';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import '../backend.dart'; import '../backend.dart';
import '../music_library.dart';
import '../widgets/play_pause_button.dart'; import '../widgets/play_pause_button.dart';
class ProgramScreen extends StatefulWidget { class ProgramScreen extends StatefulWidget {
@ -45,6 +46,45 @@ class _ProgramScreenState extends State<ProgramScreen> {
), ),
title: Text('Program'), title: Text('Program'),
), ),
body: StreamBuilder<List<InternalTrack>>(
stream: backend.player.playlist,
builder: (context, snapshot) {
final playlist = snapshot.data;
if (playlist != null && playlist.isNotEmpty) {
return StreamBuilder<int>(
stream: backend.player.currentTrack,
builder: (context, snapshot) {
if (snapshot.hasData) {
return ListView.builder(
itemCount: playlist.length,
itemBuilder: (context, index) {
final track = playlist[index];
return ListTile(
leading: index == snapshot.data
? const Icon(Icons.play_arrow)
: SizedBox(
width: 24.0,
height: 24.0,
),
title: Text(track.track.fileName),
onTap: () {
backend.player.skipTo(index);
},
);
},
);
} else {
return Container();
}
},
);
} else {
return Container();
}
},
),
bottomNavigationBar: BottomAppBar( bottomNavigationBar: BottomAppBar(
child: Column( child: Column(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
@ -82,12 +122,16 @@ class _ProgramScreenState extends State<ProgramScreen> {
Spacer(), Spacer(),
IconButton( IconButton(
icon: const Icon(Icons.skip_previous), icon: const Icon(Icons.skip_previous),
onPressed: () {}, onPressed: () {
backend.player.skipToPrevious();
},
), ),
PlayPauseButton(), PlayPauseButton(),
IconButton( IconButton(
icon: const Icon(Icons.skip_next), icon: const Icon(Icons.skip_next),
onPressed: () {}, onPressed: () {
backend.player.skipToNext();
},
), ),
Spacer(), Spacer(),
Padding( Padding(