Compare commits
	
		
			3 Commits
		
	
	
		
			1ff98b7bbf
			...
			d7b63b754f
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| d7b63b754f | |||
| a29e9dcfff | |||
| 702ce78a70 | 
							
								
								
									
										18
									
								
								lib/api.dart
									
									
									
									
									
								
							
							
						
						
									
										18
									
								
								lib/api.dart
									
									
									
									
									
								
							@@ -1,7 +1,7 @@
 | 
			
		||||
import 'package:dio/dio.dart';
 | 
			
		||||
import 'package:music_web_player/config.dart';
 | 
			
		||||
 | 
			
		||||
class MusicEntry {
 | 
			
		||||
class MusicEntry implements Comparable<MusicEntry> {
 | 
			
		||||
  final int id;
 | 
			
		||||
  final String artist;
 | 
			
		||||
  final String title;
 | 
			
		||||
@@ -13,6 +13,16 @@ class MusicEntry {
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  String get coverCacheKey => "cover-$id";
 | 
			
		||||
 | 
			
		||||
  String get fullName => "$artist - $title";
 | 
			
		||||
 | 
			
		||||
  @override
 | 
			
		||||
  String toString() => "MusicEntry($id, $artist, $title)";
 | 
			
		||||
 | 
			
		||||
  @override
 | 
			
		||||
  int compareTo(MusicEntry other) {
 | 
			
		||||
    return fullName.compareTo(other.fullName);
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
typedef MusicsList = List<MusicEntry>;
 | 
			
		||||
@@ -27,11 +37,15 @@ class API {
 | 
			
		||||
      throw Exception("Request failed with status ${response.statusCode} !");
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    return response.data
 | 
			
		||||
    final MusicsList list = response.data
 | 
			
		||||
        .map((r) =>
 | 
			
		||||
            MusicEntry(id: r["id"], artist: r["artist"], title: r["title"]))
 | 
			
		||||
        .toList()
 | 
			
		||||
        .cast<MusicEntry>();
 | 
			
		||||
 | 
			
		||||
    list.sort();
 | 
			
		||||
 | 
			
		||||
    return list;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -17,6 +17,8 @@ extension DurationExt on Duration {
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const double playlistWidth = 300;
 | 
			
		||||
 | 
			
		||||
class MusicPlayer extends StatefulWidget {
 | 
			
		||||
  final MusicsList musicsList;
 | 
			
		||||
 | 
			
		||||
@@ -38,19 +40,24 @@ class _MusicPlayerState extends State<MusicPlayer> {
 | 
			
		||||
 | 
			
		||||
  Duration? get _position => _videoPlayerController?.value.position;
 | 
			
		||||
 | 
			
		||||
  final List<MusicEntry> stack = [];
 | 
			
		||||
  final List<MusicEntry> _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) {
 | 
			
		||||
    if (currMusicPos >= _stack.length) {
 | 
			
		||||
      var nextId = rng.nextInt(widget.musicsList.length);
 | 
			
		||||
      stack.add(widget.musicsList[nextId]);
 | 
			
		||||
      _stack.add(widget.musicsList[nextId]);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    return stack[currMusicPos];
 | 
			
		||||
    return _stack[currMusicPos];
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  Future<void> _play() async {
 | 
			
		||||
@@ -101,8 +108,33 @@ class _MusicPlayerState extends State<MusicPlayer> {
 | 
			
		||||
    await _play();
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  void _playMusic(MusicEntry music) async {
 | 
			
		||||
    _stack.insert(currMusicPos + 1, music);
 | 
			
		||||
    _playNext();
 | 
			
		||||
    print(_stack);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  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();
 | 
			
		||||
@@ -112,108 +144,174 @@ class _MusicPlayerState extends State<MusicPlayer> {
 | 
			
		||||
  @override
 | 
			
		||||
  Widget build(BuildContext context) {
 | 
			
		||||
    return LayoutBuilder(
 | 
			
		||||
      builder: (context, constraints) => Stack(
 | 
			
		||||
        children: [
 | 
			
		||||
          // Background image
 | 
			
		||||
          CoverImage(
 | 
			
		||||
            music: currMusic,
 | 
			
		||||
            width: constraints.maxWidth,
 | 
			
		||||
            height: constraints.maxHeight,
 | 
			
		||||
            fit: BoxFit.cover,
 | 
			
		||||
          ),
 | 
			
		||||
      builder: (context, constraints) {
 | 
			
		||||
        final mainAreaWidth =
 | 
			
		||||
            constraints.maxWidth - (_showPlaylist ? playlistWidth : 0);
 | 
			
		||||
 | 
			
		||||
          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: constraints.maxWidth,
 | 
			
		||||
                height: constraints.maxHeight,
 | 
			
		||||
        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()),
 | 
			
		||||
                ],
 | 
			
		||||
              ),
 | 
			
		||||
            ),
 | 
			
		||||
          )),
 | 
			
		||||
 | 
			
		||||
          _buildCenter(),
 | 
			
		||||
        ],
 | 
			
		||||
      ),
 | 
			
		||||
            // Playlist
 | 
			
		||||
            _showPlaylist
 | 
			
		||||
                ? SizedBox(
 | 
			
		||||
                    width: playlistWidth,
 | 
			
		||||
                    height: constraints.maxHeight,
 | 
			
		||||
                    child: _buildPlaylistPane(),
 | 
			
		||||
                  )
 | 
			
		||||
                : Container(),
 | 
			
		||||
          ],
 | 
			
		||||
        );
 | 
			
		||||
      },
 | 
			
		||||
    );
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  Widget _buildCenter() {
 | 
			
		||||
    return 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),
 | 
			
		||||
  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),
 | 
			
		||||
                ),
 | 
			
		||||
              ),
 | 
			
		||||
              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: 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(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,
 | 
			
		||||
                ),
 | 
			
		||||
              ],
 | 
			
		||||
            )
 | 
			
		||||
          ],
 | 
			
		||||
                  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,
 | 
			
		||||
          ),
 | 
			
		||||
        ),
 | 
			
		||||
      ],
 | 
			
		||||
    );
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -22,7 +22,9 @@ class PlayerApp extends StatelessWidget {
 | 
			
		||||
      home: fluent.FluentTheme(
 | 
			
		||||
        child: const AppHome(),
 | 
			
		||||
        data: fluent.ThemeData(
 | 
			
		||||
            iconTheme: const IconThemeData(color: Colors.white)),
 | 
			
		||||
          iconTheme: const IconThemeData(color: Colors.white),
 | 
			
		||||
          brightness: fluent.Brightness.dark,
 | 
			
		||||
        ),
 | 
			
		||||
      ),
 | 
			
		||||
    );
 | 
			
		||||
  }
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user