From 952f0b0c988d59061aeb2b4184bc3771b749acef Mon Sep 17 00:00:00 2001 From: Elias Projahn Date: Fri, 24 Apr 2020 19:53:29 +0200 Subject: [PATCH] Program screen: Show work parts This is a rather complicated version of the program screen. In the future, the way how work parts are represented will be simplified. This commit exists for future reference, in case we decide to go back to the original work part representation. --- musicus/lib/screens/program.dart | 244 ++++++++++++++++++++++++++----- 1 file changed, 210 insertions(+), 34 deletions(-) diff --git a/musicus/lib/screens/program.dart b/musicus/lib/screens/program.dart index e0fc2b5..13d929c 100644 --- a/musicus/lib/screens/program.dart +++ b/musicus/lib/screens/program.dart @@ -3,10 +3,95 @@ import 'dart:async'; import 'package:flutter/material.dart'; import '../backend.dart'; +import '../database.dart'; import '../music_library.dart'; import '../widgets/play_pause_button.dart'; import '../widgets/recording_tile.dart'; +/// Data class to bundle information from the database on one track. +class ProgramItem { + /// ID of the recording. + /// + /// We don't need the real recording, as the [RecordingTile] widget handles + /// that for us. If the recording is the same one, as the one from the + /// previous track, this will be null. + final int recordingId; + + /// List of work parts contained in this track. + /// + /// This will include the parts linked in the track as well as all parents of + /// them, if there are gaps between them (i.e. some parts are missing). + final List workParts; + + ProgramItem({ + this.recordingId, + this.workParts, + }); +} + +/// Widget displaying a [ProgramItem]. +class ProgramTile extends StatelessWidget { + final ProgramItem item; + final bool isPlaying; + + ProgramTile({ + this.item, + this.isPlaying, + }); + + @override + Widget build(BuildContext context) { + return Row( + children: [ + Padding( + padding: const EdgeInsets.all(4.0), + child: isPlaying + ? const Icon(Icons.play_arrow) + : SizedBox( + width: 24.0, + height: 24.0, + ), + ), + Expanded( + child: Padding( + padding: const EdgeInsets.all(8.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + if (item.recordingId != null) ...[ + RecordingTile( + recordingId: item.recordingId, + ), + SizedBox( + height: 8.0, + ), + ], + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + for (final part in item.workParts) + Padding( + padding: EdgeInsets.only( + left: 8.0 + part.partLevel * 8.0, + ), + child: Text( + part.title, + style: TextStyle( + fontStyle: FontStyle.italic, + ), + ), + ), + ], + ), + ], + ), + ), + ), + ], + ); + } +} + class ProgramScreen extends StatefulWidget { @override _ProgramScreenState createState() => _ProgramScreenState(); @@ -14,6 +99,10 @@ class ProgramScreen extends StatefulWidget { class _ProgramScreenState extends State { BackendState backend; + + StreamSubscription> playlistSubscription; + List items = []; + StreamSubscription positionSubscription; double position = 0.0; bool seeking = false; @@ -24,6 +113,14 @@ class _ProgramScreenState extends State { backend = Backend.of(context); + if (playlistSubscription != null) { + playlistSubscription.cancel(); + } + + playlistSubscription = backend.player.playlist.listen((playlist) { + updateProgram(playlist); + }); + if (positionSubscription != null) { positionSubscription.cancel(); } @@ -37,6 +134,103 @@ class _ProgramScreenState extends State { }); } + /// Go through the tracks of [playlist] and preprocess them for displaying. + Future updateProgram(List playlist) async { + List newItems = []; + + // The following variables exist to adapt the resulting ProgramItem to its + // predecessor. + + // If the previous recording was the same, we won't need to include the + // recording data again. + int lastRecordingId; + + // If the previous work was the same, we won't need to retrieve its parts + // from the database again. + int lastWorkId; + + // We need to keep track of the last work part to be able to check for the + // parent work parts that were left out between the two last tracks. + int lastPartId; + + // This will always contain the parts of the current work. + List workParts = []; + + for (var i = 0; i < playlist.length; i++) { + // The data that will be stored in the resulting ProgramItem. + int newRecordingId; + List newWorkParts = []; + + final track = playlist[i]; + final recordingId = track.track.recordingId; + final partIds = track.track.partIds; + + // newRecordingId will be null, if the recording ID is the same. This + // also means, that the work is the same, so workParts doesn't have to + // be updated either. + if (recordingId != lastRecordingId) { + lastRecordingId = recordingId; + newRecordingId = recordingId; + + final recording = + await backend.db.recordingById(recordingId).getSingle(); + + if (recording.work != lastWorkId) { + workParts = await backend.db.workParts(recording.work).get(); + } + + lastWorkId = recording.work; + lastPartId = null; + } + + /// Search for all parent work parts of [partId] starting from the part + /// with the ID [startId] and add them to the part list. + void addParentParts(int startId, int partId) { + final level = workParts[partId].partLevel; + final List parents = List.filled(level - 1, null); + + for (var i = startId; i < partId; i++) { + final part = workParts[i]; + if (part.partLevel < parents.length) { + parents[part.partLevel] = part; + } + } + + newWorkParts.addAll(parents); + } + + for (final partId in partIds) { + // We will need to include all parent work parts first, if there were + // work parts left out between the last two tracks or if the current + // work part comes before the previous one. + if (lastPartId != null) { + if (partIds.first > lastPartId + 1) { + addParentParts(lastPartId + 1, partId); + } + } else if (partIds.first > 0) { + addParentParts(0, partId); + } + + newWorkParts.add(workParts[partId]); + + lastPartId = partId; + } + + newItems.add(ProgramItem( + recordingId: newRecordingId, + workParts: newWorkParts, + )); + } + + // Check, whether we are still a part of the widget tree, because this + // function might take some time. + if (mounted) { + setState(() { + items = newItems; + }); + } + } + @override Widget build(BuildContext context) { return Scaffold( @@ -47,41 +241,22 @@ class _ProgramScreenState extends State { ), title: Text('Program'), ), - body: StreamBuilder>( - stream: backend.player.playlist, + body: StreamBuilder( + stream: backend.player.currentIndex, builder: (context, snapshot) { - final playlist = snapshot.data; - - if (playlist != null && playlist.isNotEmpty) { - return StreamBuilder( - stream: backend.player.currentIndex, - 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: RecordingTile( - recordingId: track.track.recordingId, - ), - onTap: () { - backend.player.skipTo(index); - }, - ); - }, - ); - } else { - return Container(); - } + if (snapshot.hasData) { + return ListView.builder( + itemCount: items.length, + itemBuilder: (context, index) { + return InkWell( + child: ProgramTile( + item: items[index], + isPlaying: index == snapshot?.data, + ), + onTap: () { + backend.player.skipTo(index); + }, + ); }, ); } else { @@ -162,6 +337,7 @@ class _ProgramScreenState extends State { @override void dispose() { super.dispose(); + playlistSubscription.cancel(); positionSubscription.cancel(); } }