import 'dart:math'; import 'dart:ui'; import 'package:chewie_audio/chewie_audio.dart'; import 'package:fluent_ui/fluent_ui.dart' as fluent; import 'package:fluentui_system_icons/fluentui_system_icons.dart'; import 'package:flutter/material.dart'; import 'package:music_web_player/api.dart'; import 'package:music_web_player/ui/cover_image.dart'; import 'package:music_web_player/ui/player_web_interface.dart'; import 'package:video_player/video_player.dart'; extension DurationExt on Duration { String get formatted { return "$inMinutes:${(inSeconds % 60).toString().padLeft(2, '0')}"; } } const double playlistWidth = 300; class MusicPlayer extends StatefulWidget { final MusicsList musicsList; const MusicPlayer({Key? key, required this.musicsList}) : super(key: key); @override State createState() => _MusicPlayerState(); } class _MusicPlayerState extends State { final rng = Random(); VideoPlayerController? _videoPlayerController; ChewieAudioController? _chewieAudioController; bool get _isPlaying => _videoPlayerController?.value.isPlaying ?? false; Duration? get _duration => _videoPlayerController?.value.duration; Duration? get _position => _videoPlayerController?.value.position; final List _stack = []; int currMusicPos = 0; var _showPlaylist = false; final _filterController = fluent.TextEditingController(); MusicsList? _filteredList; MusicEntry get currMusic { if (currMusicPos < 0) currMusicPos = 0; // Automatically choose next music if required if (currMusicPos >= _stack.length) { var nextId = rng.nextInt(widget.musicsList.length); _stack.add(widget.musicsList[nextId]); } return _stack[currMusicPos]; } Future _play() async { if (_chewieAudioController != null) { await _chewieAudioController!.play(); } else { _videoPlayerController = VideoPlayerController.network(currMusic.musicURL); await _videoPlayerController!.initialize(); _chewieAudioController = ChewieAudioController( videoPlayerController: _videoPlayerController!, autoPlay: true, showControls: false); _videoPlayerController!.addListener(() => setState(() { // Automatically play next music if required if (_videoPlayerController != null && _duration == _position) { if (_duration?.inSeconds == _position?.inSeconds && (_duration?.inSeconds ?? 0) > 0) { _playNext(); } } })); updateMusicSession( entry: currMusic, onPlay: _play, onPause: _pause, onNextTrack: _playNext, onPreviousTrack: _playPrevious, onStop: _stop, ); } } Future _stop() async { _chewieAudioController?.dispose(); _videoPlayerController?.dispose(); _chewieAudioController = null; _videoPlayerController = null; } void _pause() async { await _chewieAudioController?.pause(); } void _playPrevious() async { currMusicPos -= 1; await _stop(); await _play(); } void _playNext() async { currMusicPos += 1; await _stop(); await _play(); } void _playMusic(MusicEntry music) async { _stack.insert(currMusicPos + 1, music); _playNext(); } void _updatePosition(Duration d) => _videoPlayerController?.seekTo(d); void _refreshFilteredList() { final value = _filterController.value.text.toLowerCase(); if (value.isEmpty) { setState(() => _filteredList = null); return; } setState(() { _filteredList = widget.musicsList .where((m) => m.fullName.toLowerCase().contains(value)) .toList(); }); } void _clearFilter() { _filterController.text = ""; _refreshFilteredList(); } @override void dispose() { super.dispose(); _stop(); } @override Widget build(BuildContext context) { return LayoutBuilder( builder: (context, constraints) { final mainAreaWidth = constraints.maxWidth - (_showPlaylist ? playlistWidth : 0); return fluent.Row( children: [ SizedBox( width: mainAreaWidth, child: Stack( children: [ // Background image CoverImage( music: currMusic, width: mainAreaWidth, height: constraints.maxHeight, fit: BoxFit.cover, ), // Blur background image ClipRRect( // Clip it cleanly. child: BackdropFilter( filter: ImageFilter.blur(sigmaX: 10, sigmaY: 10), child: Container( color: Colors.black.withOpacity(0.8), alignment: Alignment.center, child: SizedBox( width: mainAreaWidth, height: constraints.maxHeight, ), ), ), ), fluent.SizedBox( width: mainAreaWidth, child: _buildPlayerWidget(), ), Positioned( top: 10, right: 10, child: _buildToggleListButton()), ], ), ), // Playlist _showPlaylist ? SizedBox( width: playlistWidth, height: constraints.maxHeight, child: _buildPlaylistPane(), ) : Container(), ], ); }, ); } Widget _buildPlayerWidget() => fluent.Center( child: SizedBox( width: 250, child: Column( mainAxisAlignment: fluent.MainAxisAlignment.center, crossAxisAlignment: fluent.CrossAxisAlignment.center, children: [ Material( borderRadius: const BorderRadius.all( Radius.circular(18.0), ), clipBehavior: Clip.hardEdge, child: CoverImage( width: 250, height: 250, music: currMusic, fit: BoxFit.cover, backgroundColor: Colors.black12, icon: const Icon(FluentIcons.music_note_2_24_regular, size: 90), ), ), const SizedBox(height: 40), Text( currMusic.title, style: const TextStyle(fontSize: 22), textAlign: TextAlign.center, ), const SizedBox(height: 20), Text(currMusic.artist, textAlign: TextAlign.center), const fluent.SizedBox(height: 40), fluent.Row( children: [ DurationText(_position), const SizedBox(width: 15), Flexible( child: fluent.Slider( max: _duration?.inSeconds as double? ?? 0, value: _position?.inSeconds as double? ?? 0, onChanged: (d) => _updatePosition(Duration(seconds: d.toInt())), label: _position?.formatted, ), ), const SizedBox(width: 15), DurationText(_duration), ], ), const fluent.SizedBox(height: 40), fluent.Row( children: [ IconButton( icon: const PlayerIcon(fluent.FluentIcons.previous), onPressed: currMusicPos == 0 ? null : _playPrevious, ), const Spacer(), IconButton( icon: PlayerIcon(_isPlaying ? fluent.FluentIcons.pause : fluent.FluentIcons.play), onPressed: _isPlaying ? _pause : _play, ), const Spacer(), IconButton( icon: const PlayerIcon(fluent.FluentIcons.next), onPressed: _playNext, ), ], ) ], ), ), ); Widget _buildToggleListButton() => fluent.ToggleButton( checked: _showPlaylist, onChanged: (s) => setState(() => _showPlaylist = s), child: const fluent.Icon(fluent.FluentIcons.playlist_music), ); Widget _buildPlaylistPane() { return Column( children: [ fluent.TextBox( controller: _filterController, placeholder: "Filter list...", onChanged: (s) => _refreshFilteredList(), suffix: _filterController.text.isEmpty ? null : fluent.IconButton( icon: const Icon(fluent.FluentIcons.clear), onPressed: _clearFilter, ), ), Flexible( child: ListView.builder( itemBuilder: (c, i) { final music = (_filteredList ?? widget.musicsList)[i]; return ListTile( title: Text(music.title), subtitle: Text(music.artist), selected: currMusic == music, onTap: () => _playMusic(music), ); }, itemCount: (_filteredList ?? widget.musicsList).length, ), ), ], ); } } class PlayerIcon extends StatelessWidget { final IconData icon; const PlayerIcon(this.icon, {Key? key}) : super(key: key); @override Widget build(BuildContext context) => Icon(icon, size: 35); } class DurationText extends StatelessWidget { final Duration? duration; const DurationText(this.duration, {Key? key}) : super(key: key); @override Widget build(BuildContext context) { return Text( duration?.formatted ?? "0:00", style: const TextStyle(fontSize: 10), ); } }