Stop player on close support

PlaylistCard image placeholder
Player album placeholder
PlayerDI provider for global avilability of MPVPlayer
This commit is contained in:
Kingkor Roy Tirtho 2022-01-06 10:57:58 +06:00
parent f367d8f5c3
commit 6ea222c5b0
10 changed files with 201 additions and 99 deletions

13
.vscode/launch.json vendored Normal file
View File

@ -0,0 +1,13 @@
{
"version": "0.2.0",
"configurations": [
{
"name": "Flutter",
"type": "dart",
"request": "launch",
"program": "lib/main.dart"
}
],
"compounds": []
}

View File

@ -1,3 +1 @@
{
"typescript.tsdk": "node_modules/typescript/lib"
}
{}

12
.vscode/tasks.json vendored
View File

@ -1,12 +1,4 @@
{
"version": "2.0.0",
"tasks": [
{
"type": "npm",
"script": "start",
"problemMatcher": [],
"label": "npm: start",
"detail": "qode ./dist/index.js"
}
]
"version": "2.0.0",
"tasks": []
}

View File

@ -7,6 +7,7 @@ import 'package:shared_preferences/shared_preferences.dart';
import 'package:spotify/spotify.dart' hide Image;
import 'package:spotube/components/CategoryCard.dart';
import 'package:spotube/components/Login.dart';
import 'package:spotube/components/PageWindowTitleBar.dart';
import 'package:spotube/components/Player.dart' as player;
import 'package:spotube/components/Settings.dart';
import 'package:spotube/components/UserLibrary.dart';
@ -135,12 +136,10 @@ class _HomeState extends State<Home> {
Theme.of(context).navigationRailTheme.backgroundColor,
child: MoveWindow(),
),
Expanded(child: MoveWindow())
Expanded(child: MoveWindow()),
const TitleBarActionButtons(),
],
)),
MinimizeWindowButton(animate: true),
MaximizeWindowButton(animate: true),
CloseWindowButton(animate: true),
],
),
),

View File

@ -1,5 +1,52 @@
import 'package:bitsdojo_window/bitsdojo_window.dart';
import 'package:flutter/material.dart';
import 'package:mpv_dart/mpv_dart.dart';
import 'package:provider/provider.dart';
import 'package:spotube/provider/PlayerDI.dart';
class TitleBarActionButtons extends StatelessWidget {
const TitleBarActionButtons({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
MPVPlayer player = context.watch<PlayerDI>().player;
return Row(
children: [
TextButton(
onPressed: () {
appWindow.minimize();
},
style: ButtonStyle(
foregroundColor:
MaterialStateProperty.all(Theme.of(context).iconTheme.color),
),
child: const Icon(Icons.minimize_rounded)),
TextButton(
onPressed: () {
appWindow.maximizeOrRestore();
},
style: ButtonStyle(
foregroundColor:
MaterialStateProperty.all(Theme.of(context).iconTheme.color),
),
child: const Icon(Icons.crop_square_rounded)),
TextButton(
onPressed: () {
player.stop();
appWindow.close();
},
style: ButtonStyle(
foregroundColor:
MaterialStateProperty.all(Theme.of(context).iconTheme.color),
overlayColor: MaterialStateProperty.all(Colors.redAccent),
),
child: const Icon(
Icons.close_rounded,
)),
],
);
}
}
class PageWindowTitleBar extends StatelessWidget {
final Widget? leading;
@ -13,9 +60,7 @@ class PageWindowTitleBar extends StatelessWidget {
children: [
if (leading != null) leading!,
Expanded(child: MoveWindow(child: Center(child: center))),
MinimizeWindowButton(animate: true),
MaximizeWindowButton(animate: true),
CloseWindowButton(animate: true),
const TitleBarActionButtons()
],
),
);

View File

@ -8,6 +8,7 @@ import 'package:flutter/material.dart';
import 'package:mpv_dart/mpv_dart.dart';
import 'package:provider/provider.dart';
import 'package:spotify/spotify.dart';
import 'package:spotube/provider/PlayerDI.dart';
import 'package:spotube/provider/SpotifyDI.dart';
class Player extends StatefulWidget {
@ -18,8 +19,6 @@ class Player extends StatefulWidget {
}
class _PlayerState extends State<Player> {
late MPVPlayer player;
bool _isPlaying = false;
bool _shuffled = false;
double _duration = 0;
@ -29,80 +28,69 @@ class _PlayerState extends State<Player> {
double _volume = 0;
@override
void initState() {
player = MPVPlayer(
// verbose: true,
// debug: true,
audioOnly: true,
mpvArgs: [
"--ytdl-raw-options-set=format=140,http-chunk-size=300000",
"--script-opts=ytdl_hook-ytdl_path=yt-dlp",
],
);
(() async {
WidgetsBinding.instance?.addPostFrameCallback((timeStamp) async {
try {
MPVPlayer player = context.read<PlayerDI>().player;
await player.start();
double volume = await player.getProperty<double>("volume");
setState(() {
_volume = volume / 100;
});
player.on(MPVEvents.paused, null, (ev, context) {
setState(() {
_isPlaying = false;
});
});
player.on(MPVEvents.resumed, null, (ev, context) {
setState(() {
_isPlaying = true;
});
});
player.on(MPVEvents.status, null, (ev, _) async {
Map data = ev.eventData as Map;
Playback playback = context.read<Playback>();
if (data["property"] == "media-title" && data["value"] != null) {
var containsYtdl = (data["value"] as String).contains("ytsearch:");
if (containsYtdl) {
var props = (data["value"] as String).split("-");
var mediaTitle = props.last.trim();
var mediaArtists = props.first.split("ytsearch:").last.trim();
setState(() {
_isPlaying = true;
});
var matchedTracks = playback.currentPlaylist?.tracks.where(
(track) {
return track.name?.replaceAll("-", " ") == mediaTitle &&
artistsToString(track.artists ?? []) == mediaArtists;
},
) ??
[];
if (matchedTracks.isNotEmpty) {
playback.setCurrentTrack = matchedTracks.first;
}
}
}
if (data["property"] == "duration" && data["value"] != null) {
setState(() {
_duration = data["value"];
});
}
});
} catch (e) {
if (kDebugMode) {
print("[PLAYER]: $e");
}
}
})();
player.on(MPVEvents.paused, null, (ev, context) {
setState(() {
_isPlaying = false;
});
});
player.on(MPVEvents.resumed, null, (ev, context) {
setState(() {
_isPlaying = true;
});
});
WidgetsBinding.instance?.addPostFrameCallback((timeStamp) {
player.on(MPVEvents.status, null, (ev, _) async {
Map data = ev.eventData as Map;
Playback playback = context.read<Playback>();
if (data["property"] == "media-title" && data["value"] != null) {
var containsYtdl = (data["value"] as String).contains("ytsearch:");
if (containsYtdl) {
var props = (data["value"] as String).split("-");
var mediaTitle = props.last.trim();
var mediaArtists = props.first.split("ytsearch:").last.trim();
setState(() {
_isPlaying = true;
});
var matchedTracks = playback.currentPlaylist?.tracks.where(
(track) {
return track.name?.replaceAll("-", " ") == mediaTitle &&
artistsToString(track.artists ?? []) == mediaArtists;
},
) ??
[];
if (matchedTracks.isNotEmpty) {
playback.setCurrentTrack = matchedTracks.first;
}
}
}
if (data["property"] == "duration" && data["value"] != null) {
setState(() {
_duration = data["value"];
});
}
});
});
super.initState();
}
@override
void dispose() {
MPVPlayer player = context.read<PlayerDI>().player;
player.removeAllByEvent(MPVEvents.paused);
player.removeAllByEvent(MPVEvents.resumed);
player.removeAllByEvent(MPVEvents.status);
@ -115,7 +103,7 @@ class _PlayerState extends State<Player> {
}).join("\n");
}
Future playPlaylist(CurrentPlaylist playlist) async {
Future playPlaylist(MPVPlayer player, CurrentPlaylist playlist) async {
try {
if (player.isRunning() && playlist.id != _currentPlaylistId) {
var playlistPath = "/tmp/playlist-${playlist.id}.txt";
@ -146,12 +134,14 @@ class _PlayerState extends State<Player> {
@override
Widget build(BuildContext context) {
MPVPlayer player = context.watch<PlayerDI>().player;
return Container(
color: Theme.of(context).backgroundColor,
child: Consumer<Playback>(
builder: (context, playback, widget) {
if (playback.currentPlaylist != null) {
playPlaylist(playback.currentPlaylist!);
playPlaylist(player, playback.currentPlaylist!);
}
String? albumArt = playback.currentTrack?.album?.images?.last.url;
@ -166,6 +156,13 @@ class _PlayerState extends State<Player> {
imageUrl: albumArt,
maxHeightDiskCache: 50,
maxWidthDiskCache: 50,
placeholder: (context, url) {
return Container(
height: 50,
width: 50,
color: Colors.green[400],
);
},
),
// title of the currently playing track
Flexible(

View File

@ -47,7 +47,13 @@ class _PlaylistCardState extends State<PlaylistCard> {
ClipRRect(
borderRadius: BorderRadius.circular(8),
child: CachedNetworkImage(
imageUrl: widget.playlist.images![0].url!),
imageUrl: widget.playlist.images![0].url!,
progressIndicatorBuilder: (context, url, progress) {
return CircularProgressIndicator.adaptive(
value: progress.progress,
);
},
),
),
Positioned.directional(
textDirection: TextDirection.ltr,

View File

@ -23,18 +23,34 @@ class _PlaylistViewState extends State<PlaylistView> {
return (TableRow(
children: [
TableCell(
child: Text(
(track.key + 1).toString(),
textAlign: TextAlign.center,
child: Padding(
padding: const EdgeInsets.all(8.0),
child: Text(
(track.key + 1).toString(),
textAlign: TextAlign.center,
),
)),
TableCell(
child: Row(
children: [
if (thumbnailUrl != null)
CachedNetworkImage(
imageUrl: thumbnailUrl,
maxHeightDiskCache: 40,
maxWidthDiskCache: 40,
Padding(
padding: const EdgeInsets.all(8.0),
child: ClipRRect(
borderRadius: const BorderRadius.all(Radius.circular(5)),
child: CachedNetworkImage(
placeholder: (context, url) {
return Container(
height: 40,
width: 40,
color: Colors.green[300],
);
},
imageUrl: thumbnailUrl,
maxHeightDiskCache: 40,
maxWidthDiskCache: 40,
),
),
),
const SizedBox(width: 10),
Flexible(
@ -62,16 +78,22 @@ class _PlaylistViewState extends State<PlaylistView> {
),
),
TableCell(
child: Text(
track.value.album?.name ?? "",
overflow: TextOverflow.ellipsis,
child: Padding(
padding: const EdgeInsets.all(8.0),
child: Text(
track.value.album?.name ?? "",
overflow: TextOverflow.ellipsis,
),
),
),
TableCell(
child: Text(
duration,
textAlign: TextAlign.center,
overflow: TextOverflow.ellipsis,
child: Padding(
padding: const EdgeInsets.all(8.0),
child: Text(
duration,
textAlign: TextAlign.center,
overflow: TextOverflow.ellipsis,
),
),
)
],
@ -148,7 +170,10 @@ class _PlaylistViewState extends State<PlaylistView> {
snapshot.hasError
? const Center(child: Text("Error occurred"))
: !snapshot.hasData
? const CircularProgressIndicator.adaptive()
? const Expanded(
child: Center(
child: CircularProgressIndicator.adaptive()),
)
: Expanded(
child: Scrollbar(
child: ListView(
@ -159,7 +184,7 @@ class _PlaylistViewState extends State<PlaylistView> {
0: FixedColumnWidth(40),
1: FlexColumnWidth(),
2: FlexColumnWidth(),
3: FixedColumnWidth(40),
3: FixedColumnWidth(45),
},
children: [
TableRow(

View File

@ -1,5 +1,6 @@
import 'package:bitsdojo_window/bitsdojo_window.dart';
import 'package:flutter/material.dart';
import 'package:mpv_dart/mpv_dart.dart';
import 'package:provider/provider.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'package:spotify/spotify.dart';
@ -7,6 +8,7 @@ import 'package:spotube/components/Home.dart';
import 'package:spotube/models/LocalStorageKeys.dart';
import 'package:spotube/provider/Auth.dart';
import 'package:spotube/provider/Playback.dart';
import 'package:spotube/provider/PlayerDI.dart';
import 'package:spotube/provider/SpotifyDI.dart';
void main() {
@ -59,6 +61,15 @@ class MyApp extends StatelessWidget {
);
}),
ChangeNotifierProvider<Playback>(create: (context) => Playback()),
ChangeNotifierProvider<PlayerDI>(
create: (context) => PlayerDI(MPVPlayer(
audioOnly: true,
mpvArgs: [
"--ytdl-raw-options-set=format=140,http-chunk-size=300000",
"--script-opts=ytdl_hook-ytdl_path=yt-dlp",
],
)),
)
],
child: MaterialApp(
debugShowCheckedModeBanner: false,
@ -70,6 +81,7 @@ class MyApp extends StatelessWidget {
buttonColor: Colors.green,
),
shadowColor: Colors.grey[300],
backgroundColor: Colors.white,
textTheme: TextTheme(
bodyText1: TextStyle(color: Colors.grey[850]),
headline1: TextStyle(color: Colors.grey[850]),
@ -112,7 +124,7 @@ class MyApp extends StatelessWidget {
backgroundColor: Colors.blueGrey[900],
scaffoldBackgroundColor: Colors.blueGrey[900],
dialogBackgroundColor: Colors.blueGrey[800],
shadowColor: Colors.black12,
shadowColor: Colors.black26,
buttonTheme: const ButtonThemeData(
buttonColor: Colors.green,
),

View File

@ -0,0 +1,15 @@
import 'package:flutter/cupertino.dart';
import 'package:mpv_dart/mpv_dart.dart';
class PlayerDI extends ChangeNotifier {
MPVPlayer _player;
PlayerDI(this._player);
get player => _player;
setPlayer(MPVPlayer player) {
_player = player;
notifyListeners();
}
}