diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 00000000..e6eedc57 --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,13 @@ +{ + "version": "0.2.0", + "configurations": [ + { + "name": "Flutter", + "type": "dart", + "request": "launch", + "program": "lib/main.dart" + } + + ], + "compounds": [] +} \ No newline at end of file diff --git a/.vscode/settings.json b/.vscode/settings.json index 3662b370..9e26dfee 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,3 +1 @@ -{ - "typescript.tsdk": "node_modules/typescript/lib" -} \ No newline at end of file +{} \ No newline at end of file diff --git a/.vscode/tasks.json b/.vscode/tasks.json index 74807a2d..f67eb4c6 100644 --- a/.vscode/tasks.json +++ b/.vscode/tasks.json @@ -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": [] } \ No newline at end of file diff --git a/lib/components/Home.dart b/lib/components/Home.dart index 6e87b34a..32484a1b 100644 --- a/lib/components/Home.dart +++ b/lib/components/Home.dart @@ -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 { Theme.of(context).navigationRailTheme.backgroundColor, child: MoveWindow(), ), - Expanded(child: MoveWindow()) + Expanded(child: MoveWindow()), + const TitleBarActionButtons(), ], )), - MinimizeWindowButton(animate: true), - MaximizeWindowButton(animate: true), - CloseWindowButton(animate: true), ], ), ), diff --git a/lib/components/PageWindowTitleBar.dart b/lib/components/PageWindowTitleBar.dart index 861e4aa6..85caf6a7 100644 --- a/lib/components/PageWindowTitleBar.dart +++ b/lib/components/PageWindowTitleBar.dart @@ -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().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() ], ), ); diff --git a/lib/components/Player.dart b/lib/components/Player.dart index de0dd5bd..668bad65 100644 --- a/lib/components/Player.dart +++ b/lib/components/Player.dart @@ -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 { - late MPVPlayer player; - bool _isPlaying = false; bool _shuffled = false; double _duration = 0; @@ -29,80 +28,69 @@ class _PlayerState extends State { 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().player; await player.start(); double volume = await player.getProperty("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(); + 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(); - 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().player; player.removeAllByEvent(MPVEvents.paused); player.removeAllByEvent(MPVEvents.resumed); player.removeAllByEvent(MPVEvents.status); @@ -115,7 +103,7 @@ class _PlayerState extends State { }).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 { @override Widget build(BuildContext context) { + MPVPlayer player = context.watch().player; + return Container( color: Theme.of(context).backgroundColor, child: Consumer( 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 { 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( diff --git a/lib/components/PlaylistCard.dart b/lib/components/PlaylistCard.dart index fa5f18b2..7c24daf4 100644 --- a/lib/components/PlaylistCard.dart +++ b/lib/components/PlaylistCard.dart @@ -47,7 +47,13 @@ class _PlaylistCardState extends State { 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, diff --git a/lib/components/PlaylistView.dart b/lib/components/PlaylistView.dart index a4845064..59e318a6 100644 --- a/lib/components/PlaylistView.dart +++ b/lib/components/PlaylistView.dart @@ -23,18 +23,34 @@ class _PlaylistViewState extends State { 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 { ), ), 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 { 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 { 0: FixedColumnWidth(40), 1: FlexColumnWidth(), 2: FlexColumnWidth(), - 3: FixedColumnWidth(40), + 3: FixedColumnWidth(45), }, children: [ TableRow( diff --git a/lib/main.dart b/lib/main.dart index d30050ae..09e26b52 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -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(create: (context) => Playback()), + ChangeNotifierProvider( + 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, ), diff --git a/lib/provider/PlayerDI.dart b/lib/provider/PlayerDI.dart new file mode 100644 index 00000000..29532b99 --- /dev/null +++ b/lib/provider/PlayerDI.dart @@ -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(); + } +}