mirror of
				https://github.com/johrpan/musicus_mobile.git
				synced 2025-10-26 18:57:25 +01:00 
			
		
		
		
	
		
			
				
	
	
		
			318 lines
		
	
	
	
		
			9.2 KiB
		
	
	
	
		
			Dart
		
	
	
	
	
	
			
		
		
	
	
			318 lines
		
	
	
	
		
			9.2 KiB
		
	
	
	
		
			Dart
		
	
	
	
	
	
| import 'dart:async';
 | |
| 
 | |
| import 'package:flutter/material.dart';
 | |
| import 'package:musicus_common/musicus_common.dart';
 | |
| import 'package:musicus_database/musicus_database.dart';
 | |
| 
 | |
| import '../widgets/play_pause_button.dart';
 | |
| 
 | |
| class ProgramScreen extends StatefulWidget {
 | |
|   @override
 | |
|   _ProgramScreenState createState() => _ProgramScreenState();
 | |
| }
 | |
| 
 | |
| class _ProgramScreenState extends State<ProgramScreen> {
 | |
|   MusicusBackendState backend;
 | |
| 
 | |
|   StreamSubscription<bool> playerActiveSubscription;
 | |
| 
 | |
|   StreamSubscription<List<InternalTrack>> playlistSubscription;
 | |
|   List<Widget> widgets = [];
 | |
| 
 | |
|   StreamSubscription<double> positionSubscription;
 | |
|   double position = 0.0;
 | |
|   bool seeking = false;
 | |
| 
 | |
|   @override
 | |
|   void didChangeDependencies() {
 | |
|     super.didChangeDependencies();
 | |
| 
 | |
|     backend = MusicusBackend.of(context);
 | |
| 
 | |
|     if (playerActiveSubscription != null) {
 | |
|       playerActiveSubscription.cancel();
 | |
|     }
 | |
| 
 | |
|     // Close the program screen, if the player is no longer active.
 | |
|     playerActiveSubscription = backend.playback.active.listen((active) {
 | |
|       if (!active) {
 | |
|         Navigator.pop(context);
 | |
|       }
 | |
|     });
 | |
| 
 | |
|     if (playlistSubscription != null) {
 | |
|       playlistSubscription.cancel();
 | |
|     }
 | |
| 
 | |
|     playlistSubscription = backend.playback.playlist.listen((playlist) {
 | |
|       updateProgram(playlist);
 | |
|     });
 | |
| 
 | |
|     if (positionSubscription != null) {
 | |
|       positionSubscription.cancel();
 | |
|     }
 | |
| 
 | |
|     positionSubscription = backend.playback.normalizedPosition.listen((pos) {
 | |
|       if (!seeking) {
 | |
|         setState(() {
 | |
|           position = pos;
 | |
|         });
 | |
|       }
 | |
|     });
 | |
|   }
 | |
| 
 | |
|   /// Go through the tracks of [playlist] and preprocess them for displaying.
 | |
|   Future<void> updateProgram(List<InternalTrack> playlist) async {
 | |
|     List<Widget> newWidgets = [];
 | |
| 
 | |
|     // 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;
 | |
| 
 | |
|     // This will contain information on the last new work.
 | |
|     WorkInfo workInfo;
 | |
| 
 | |
|     // The index of the last displayed section.
 | |
|     int lastSectionIndex;
 | |
| 
 | |
|     for (var i = 0; i < playlist.length; i++) {
 | |
|       // The widgets displayed for this track.
 | |
|       List<Widget> children = [];
 | |
| 
 | |
|       final track = playlist[i];
 | |
|       final recordingId = track.track.recordingId;
 | |
|       final partIds = track.track.partIds;
 | |
| 
 | |
|       // If the recording is the same, the work will also be the same, so
 | |
|       // workInfo doesn't have to be updated either.
 | |
|       if (recordingId != lastRecordingId) {
 | |
|         lastRecordingId = recordingId;
 | |
| 
 | |
|         final recordingInfo = await backend.db.getRecording(recordingId);
 | |
| 
 | |
|         if (recordingInfo.recording.work != lastWorkId) {
 | |
|           lastWorkId = recordingInfo.recording.work;
 | |
|           workInfo = await backend.db.getWork(lastWorkId);
 | |
|           lastSectionIndex = null;
 | |
|         }
 | |
| 
 | |
|         children.addAll([
 | |
|           RecordingTile(
 | |
|             workInfo: workInfo,
 | |
|             recordingInfo: recordingInfo,
 | |
|           ),
 | |
|           SizedBox(
 | |
|             height: 8.0,
 | |
|           ),
 | |
|         ]);
 | |
|       }
 | |
| 
 | |
|       for (final partId in partIds) {
 | |
|         final partInfo = workInfo.parts[partId];
 | |
| 
 | |
|         final sectionIndex = workInfo.sections
 | |
|             .lastIndexWhere((s) => s.beforePartIndex <= partId);
 | |
|         if (sectionIndex != lastSectionIndex && sectionIndex >= 0) {
 | |
|           lastSectionIndex = sectionIndex;
 | |
|           children.add(Padding(
 | |
|             padding: const EdgeInsets.only(
 | |
|               bottom: 8.0,
 | |
|             ),
 | |
|             child: Text(workInfo.sections[sectionIndex].title),
 | |
|           ));
 | |
|         }
 | |
| 
 | |
|         children.add(Padding(
 | |
|           padding: const EdgeInsets.only(
 | |
|             left: 8.0,
 | |
|           ),
 | |
|           child: Text(
 | |
|             partInfo.part.title,
 | |
|             style: TextStyle(
 | |
|               fontStyle: FontStyle.italic,
 | |
|             ),
 | |
|           ),
 | |
|         ));
 | |
|       }
 | |
| 
 | |
|       newWidgets.add(Column(
 | |
|         crossAxisAlignment: CrossAxisAlignment.stretch,
 | |
|         children: children,
 | |
|       ));
 | |
|     }
 | |
| 
 | |
|     // Check, whether we are still a part of the widget tree, because this
 | |
|     // function might take some time.
 | |
|     if (mounted) {
 | |
|       setState(() {
 | |
|         widgets = newWidgets;
 | |
|       });
 | |
|     }
 | |
|   }
 | |
| 
 | |
|   @override
 | |
|   Widget build(BuildContext context) {
 | |
|     return Scaffold(
 | |
|       appBar: AppBar(
 | |
|         leading: IconButton(
 | |
|           icon: const Icon(Icons.keyboard_arrow_down),
 | |
|           onPressed: () => Navigator.pop(context),
 | |
|         ),
 | |
|         title: Text('Program'),
 | |
|       ),
 | |
|       body: StreamBuilder<int>(
 | |
|         stream: backend.playback.currentIndex,
 | |
|         builder: (context, snapshot) {
 | |
|           if (snapshot.hasData) {
 | |
|             return ListView.builder(
 | |
|               itemCount: widgets.length,
 | |
|               itemBuilder: (context, index) {
 | |
|                 return InkWell(
 | |
|                   child: Row(
 | |
|                     children: <Widget>[
 | |
|                       Padding(
 | |
|                         padding: const EdgeInsets.all(4.0),
 | |
|                         child: index == snapshot.data
 | |
|                             ? const Icon(Icons.play_arrow)
 | |
|                             : SizedBox(
 | |
|                                 width: 24.0,
 | |
|                                 height: 24.0,
 | |
|                               ),
 | |
|                       ),
 | |
|                       Expanded(
 | |
|                         child: Padding(
 | |
|                           padding: const EdgeInsets.all(8.0),
 | |
|                           child: widgets[index],
 | |
|                         ),
 | |
|                       ),
 | |
|                     ],
 | |
|                   ),
 | |
|                   onTap: () {
 | |
|                     backend.playback.skipTo(index);
 | |
|                   },
 | |
|                   onLongPress: () {
 | |
|                     showDialog(
 | |
|                         context: context,
 | |
|                         builder: (context) {
 | |
|                           return SimpleDialog(
 | |
|                             children: <Widget>[
 | |
|                               ListTile(
 | |
|                                 title: Text('Remove from playlist'),
 | |
|                                 onTap: () {
 | |
|                                   backend.playback.removeTrack(index);
 | |
|                                   Navigator.pop(context);
 | |
|                                 },
 | |
|                               ),
 | |
|                             ],
 | |
|                           );
 | |
|                         });
 | |
|                   },
 | |
|                 );
 | |
|               },
 | |
|             );
 | |
|           } else {
 | |
|             return Container();
 | |
|           }
 | |
|         },
 | |
|       ),
 | |
|       bottomNavigationBar: BottomAppBar(
 | |
|         child: Column(
 | |
|           mainAxisSize: MainAxisSize.min,
 | |
|           children: <Widget>[
 | |
|             Slider(
 | |
|               value: position,
 | |
|               onChangeStart: (_) {
 | |
|                 seeking = true;
 | |
|               },
 | |
|               onChangeEnd: (pos) {
 | |
|                 seeking = false;
 | |
|                 backend.playback.seekTo(pos);
 | |
|               },
 | |
|               onChanged: (pos) {
 | |
|                 setState(() {
 | |
|                   position = pos;
 | |
|                 });
 | |
|               },
 | |
|             ),
 | |
|             Row(
 | |
|               children: <Widget>[
 | |
|                 Padding(
 | |
|                   padding: const EdgeInsets.only(left: 24.0),
 | |
|                   child: StreamBuilder<Duration>(
 | |
|                     stream: backend.playback.position,
 | |
|                     builder: (context, snapshot) {
 | |
|                       if (snapshot.hasData) {
 | |
|                         return DurationText(snapshot.data);
 | |
|                       } else {
 | |
|                         return Container();
 | |
|                       }
 | |
|                     },
 | |
|                   ),
 | |
|                 ),
 | |
|                 Spacer(),
 | |
|                 IconButton(
 | |
|                   icon: const Icon(Icons.skip_previous),
 | |
|                   onPressed: () {
 | |
|                     backend.playback.skipToPrevious();
 | |
|                   },
 | |
|                 ),
 | |
|                 PlayPauseButton(),
 | |
|                 IconButton(
 | |
|                   icon: const Icon(Icons.skip_next),
 | |
|                   onPressed: () {
 | |
|                     backend.playback.skipToNext();
 | |
|                   },
 | |
|                 ),
 | |
|                 Spacer(),
 | |
|                 Padding(
 | |
|                   padding: const EdgeInsets.only(right: 20.0),
 | |
|                   child: StreamBuilder<Duration>(
 | |
|                     stream: backend.playback.duration,
 | |
|                     builder: (context, snapshot) {
 | |
|                       if (snapshot.hasData) {
 | |
|                         return DurationText(snapshot.data);
 | |
|                       } else {
 | |
|                         return Container();
 | |
|                       }
 | |
|                     },
 | |
|                   ),
 | |
|                 ),
 | |
|               ],
 | |
|             ),
 | |
|           ],
 | |
|         ),
 | |
|       ),
 | |
|     );
 | |
|   }
 | |
| 
 | |
|   @override
 | |
|   void dispose() {
 | |
|     super.dispose();
 | |
|     playerActiveSubscription.cancel();
 | |
|     playlistSubscription.cancel();
 | |
|     positionSubscription.cancel();
 | |
|   }
 | |
| }
 | |
| 
 | |
| class DurationText extends StatelessWidget {
 | |
|   final Duration duration;
 | |
| 
 | |
|   DurationText(this.duration);
 | |
| 
 | |
|   @override
 | |
|   Widget build(BuildContext context) {
 | |
|     final minutes = duration.inMinutes;
 | |
|     final seconds = (duration - Duration(minutes: minutes)).inSeconds;
 | |
| 
 | |
|     final secondsString = seconds >= 10 ? seconds.toString() : '0$seconds';
 | |
| 
 | |
|     return Text('$minutes:$secondsString');
 | |
|   }
 | |
| }
 | 
