replaced mpv audio backed with just_audio

Player works as previous except shuffling
Youtube Search is handled through youtube_explode now
This commit is contained in:
Kingkor Roy Tirtho 2022-01-11 23:05:31 +06:00
parent f9d3d58398
commit 07b1891cb4
15 changed files with 402 additions and 274 deletions

View File

@ -1,15 +1,11 @@
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(
@ -32,7 +28,6 @@ class TitleBarActionButtons extends StatelessWidget {
child: const Icon(Icons.crop_square_rounded)),
TextButton(
onPressed: () {
player.stop();
appWindow.close();
},
style: ButtonStyle(

View File

@ -1,16 +1,16 @@
import 'dart:io';
import 'dart:async';
import 'package:cached_network_image/cached_network_image.dart';
import 'package:flutter/foundation.dart';
import 'package:hotkey_manager/hotkey_manager.dart';
import 'package:just_audio/just_audio.dart';
import 'package:spotify/spotify.dart';
import 'package:spotube/components/PlayerControls.dart';
import 'package:spotube/helpers/artist-to-string.dart';
import 'package:spotube/helpers/search-youtube.dart';
import 'package:spotube/models/GlobalKeyActions.dart';
import 'package:spotube/provider/Playback.dart';
import 'package:flutter/material.dart';
import 'package:mpv_dart/mpv_dart.dart';
import 'package:provider/provider.dart';
import 'package:spotube/provider/PlayerDI.dart';
import 'package:spotube/provider/SpotifyDI.dart';
class Player extends StatefulWidget {
@ -20,169 +20,200 @@ class Player extends StatefulWidget {
_PlayerState createState() => _PlayerState();
}
class _PlayerState extends State<Player> {
class _PlayerState extends State<Player> with WidgetsBindingObserver {
late AudioPlayer player;
bool _isPlaying = false;
bool _shuffled = false;
double _duration = 0;
Duration? _duration;
String? _currentPlaylistId;
String? _currentTrackId;
double _volume = 0;
List<HotKey> _hotKeys = [];
final List<HotKey> _hotKeys = [];
@override
void initState() {
super.initState();
player = AudioPlayer();
WidgetsBinding.instance?.addObserver(this);
WidgetsBinding.instance?.addPostFrameCallback((timeStamp) async {
try {
MPVPlayer player = context.read<PlayerDI>().player;
if (!player.isRunning()) {
await player.start();
}
double volume = await player.getProperty<double>("volume");
setState(() {
_volume = volume / 100;
_volume = player.volume;
});
player.on(MPVEvents.paused, null, (ev, context) {
player.playingStream.listen((playing) async {
setState(() {
_isPlaying = false;
_isPlaying = playing;
});
});
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 track = playback.currentPlaylist?.tracks.where((track) {
String title = data["value"];
return title.contains(track.name!) &&
track.artists!
.every((artist) => title.contains(artist.name!));
});
if (track != null && track.isNotEmpty) {
setState(() {
_isPlaying = true;
});
playback.setCurrentTrack = track.first;
player.durationStream.listen((duration) async {
if (duration != null) {
// Actually things doesn't work all the time as they were
// described. So instead of listening to a `playback.ready`
// stream, it has to listen to duration stream since duration
// is always added to the Stream sink after all icyMetadata has
// been loaded thus indicating buffering started
if (duration != Duration.zero && duration != _duration) {
// this line is for prev/next or already playing playlist
if (player.playing) await player.pause();
await player.play();
}
}
if (data["property"] == "duration" && data["value"] != null) {
setState(() {
_duration = data["value"];
_duration = duration;
});
}
playOrPause(key) async {
_isPlaying ? await player.pause() : await player.play();
}
List<GlobalKeyActions> keyWithActions = [
GlobalKeyActions(
HotKey(KeyCode.space, scope: HotKeyScope.inapp),
playOrPause,
),
GlobalKeyActions(
HotKey(KeyCode.mediaPlayPause),
playOrPause,
),
GlobalKeyActions(HotKey(KeyCode.mediaTrackNext), (key) async {
await player.next();
}),
GlobalKeyActions(HotKey(KeyCode.mediaTrackPrevious), (key) async {
await player.prev();
}),
GlobalKeyActions(HotKey(KeyCode.mediaStop), (key) async {
await player.stop();
setState(() {
_isPlaying = false;
_currentPlaylistId = null;
_duration = 0;
_shuffled = false;
});
playback.reset();
})
];
await Future.wait(
keyWithActions.map((e) {
return hotKeyManager.register(
e.hotKey,
keyDownHandler: e.onKeyDown,
);
}),
);
});
} catch (e) {
if (kDebugMode) {
print("[PLAYER]: $e");
player.processingStateStream.listen((event) async {
try {
if (event == ProcessingState.completed && _currentTrackId != null) {
_movePlaylistPositionBy(1);
}
} catch (e, stack) {
print("[PrecessingStateStreamListener] $e");
print(stack);
}
});
playOrPause(key) async {
try {
_isPlaying ? await player.pause() : await player.play();
} catch (e, stack) {
print("[PlayPauseShortcut] $e");
print(stack);
}
}
List<GlobalKeyActions> keyWithActions = [
GlobalKeyActions(
HotKey(KeyCode.space, scope: HotKeyScope.inapp),
playOrPause,
),
GlobalKeyActions(
HotKey(KeyCode.mediaPlayPause),
playOrPause,
),
GlobalKeyActions(HotKey(KeyCode.mediaTrackNext), (key) async {
_movePlaylistPositionBy(1);
}),
GlobalKeyActions(HotKey(KeyCode.mediaTrackPrevious), (key) async {
_movePlaylistPositionBy(-1);
}),
GlobalKeyActions(HotKey(KeyCode.mediaStop), (key) async {
Playback playback = context.read<Playback>();
setState(() {
_isPlaying = false;
_currentTrackId = null;
_duration = null;
_shuffled = false;
});
playback.reset();
})
];
await Future.wait(
keyWithActions.map((e) {
return hotKeyManager.register(
e.hotKey,
keyDownHandler: e.onKeyDown,
);
}),
);
} catch (e) {
print("[Player.initState()]: $e");
}
});
super.initState();
}
@override
void dispose() async {
MPVPlayer player = context.read<PlayerDI>().player;
player.removeAllByEvent(MPVEvents.paused);
player.removeAllByEvent(MPVEvents.resumed);
player.removeAllByEvent(MPVEvents.status);
await Future.wait(_hotKeys.map((e) => hotKeyManager.unregister(e)));
void dispose() {
WidgetsBinding.instance?.removeObserver(this);
player.dispose();
Future.wait(_hotKeys.map((e) => hotKeyManager.unregister(e)));
super.dispose();
}
String playlistToStr(CurrentPlaylist playlist) {
var tracks = playlist.tracks.map((track) {
var artists = artistsToString(track.artists ?? []);
var title = track.name?.replaceAll("-", " ");
return "ytdl://ytsearch:$artists - $title";
}).toList();
return tracks.join("\n");
@override
void didChangeAppLifecycleState(AppLifecycleState state) {
if (state == AppLifecycleState.paused) {
// Release the player's resources when not in use. We use "stop" so that
// if the app resumes later, it will still remember what position to
// resume from.
player.stop();
}
}
Future playPlaylist(MPVPlayer player, CurrentPlaylist playlist) async {
try {
if (player.isRunning() && playlist.id != _currentPlaylistId) {
var playlistPath = "/tmp/playlist-${playlist.id}.txt";
File file = File(playlistPath);
var newPlaylist = playlistToStr(playlist);
void _movePlaylistPositionBy(int pos) {
Playback playback = context.read<Playback>();
if (playback.currentTrack != null && playback.currentPlaylist != null) {
int index = playback.currentPlaylist!.trackIds
.indexOf(playback.currentTrack!.id!) +
pos;
if (!await file.exists()) {
await file.create();
}
await file.writeAsString(newPlaylist);
await player.loadPlaylist(playlistPath);
var safeIndex = index > playback.currentPlaylist!.trackIds.length - 1
? 0
: index < 0
? playback.currentPlaylist!.trackIds.length
: index;
Track? track =
playback.currentPlaylist!.tracks.asMap().containsKey(safeIndex)
? playback.currentPlaylist!.tracks.elementAt(safeIndex)
: null;
if (track != null) {
playback.setCurrentTrack = track;
setState(() {
_currentPlaylistId = playlist.id;
_shuffled = false;
_duration = null;
});
}
} catch (e, stackTrace) {
print("[Player]: $e");
print(stackTrace);
}
}
Future _playTrack(Track currentTrack, Playback playback) async {
try {
if (currentTrack.id != _currentTrackId) {
if (currentTrack.uri != null) {
await player
.setAudioSource(
AudioSource.uri(Uri.parse(currentTrack.uri!)),
preload: true,
)
.then((value) async {
setState(() {
_currentTrackId = currentTrack.id;
if (_duration != null) {
_duration = value;
}
});
});
}
var ytTrack = await toYoutubeTrack(currentTrack);
if (playback.setTrackUriById(currentTrack.id!, ytTrack.uri!)) {
await player
.setAudioSource(AudioSource.uri(Uri.parse(ytTrack.uri!)))
.then((value) {
setState(() {
_currentTrackId = currentTrack.id;
});
});
}
}
} catch (e, stack) {
print("[Player._playTrack()] $e");
print(stack);
}
}
@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(player, playback.currentPlaylist!);
if (playback.currentPlaylist != null &&
playback.currentTrack != null) {
_playTrack(playback.currentTrack!, playback);
}
String? albumArt = playback.currentTrack?.album?.images?.last.url;
@ -223,33 +254,89 @@ class _PlayerState extends State<Player> {
Flexible(
flex: 3,
child: PlayerControls(
player: player,
positionStream: player.positionStream,
isPlaying: _isPlaying,
duration: _duration,
duration: _duration ?? Duration.zero,
shuffled: _shuffled,
onShuffle: () {
if (!_shuffled) {
player.shuffle().then(
(value) => setState(() {
_shuffled = true;
}),
);
} else {
player.unshuffle().then(
(value) => setState(() {
_shuffled = false;
}),
);
onNext: () async {
try {
await player.pause();
await player.seek(Duration.zero);
_movePlaylistPositionBy(1);
} catch (e, stack) {
print("[PlayerControls.onNext()] $e");
print(stack);
}
},
onStop: () {
setState(() {
_isPlaying = false;
_currentPlaylistId = null;
_duration = 0;
_shuffled = false;
});
playback.reset();
onPrevious: () async {
try {
await player.pause();
await player.seek(Duration.zero);
_movePlaylistPositionBy(-1);
} catch (e, stack) {
print("[PlayerControls.onPrevious()] $e");
print(stack);
}
},
onPause: () async {
try {
await player.pause();
} catch (e, stack) {
print("[PlayerControls.onPause()] $e");
print(stack);
}
},
onPlay: () async {
try {
await player.play();
} catch (e, stack) {
print("[PlayerControls.onPlay()] $e");
print(stack);
}
},
onSeek: (value) async {
try {
await player.seek(Duration(seconds: value.toInt()));
} catch (e, stack) {
print("[PlayerControls.onSeek()] $e");
print(stack);
}
},
onShuffle: () async {
try {
if (!_shuffled) {
await player.setShuffleModeEnabled(true).then(
(value) => setState(() {
_shuffled = true;
}),
);
} else {
await player.setShuffleModeEnabled(false).then(
(value) => setState(() {
_shuffled = false;
}),
);
}
} catch (e, stack) {
print("[PlayerControls.onShuffle()] $e");
print(stack);
}
},
onStop: () async {
try {
await player.pause();
await player.seek(Duration.zero);
setState(() {
_isPlaying = false;
_currentTrackId = null;
_duration = null;
_shuffled = false;
});
playback.reset();
} catch (e, stack) {
print("[PlayerControls.onStop()] $e");
print(stack);
}
},
),
),
@ -289,12 +376,17 @@ class _PlayerState extends State<Player> {
constraints: const BoxConstraints(maxWidth: 200),
child: Slider.adaptive(
value: _volume,
onChanged: (value) {
player.volume(value * 100).then((_) {
setState(() {
_volume = value;
onChanged: (value) async {
try {
await player.setVolume(value).then((_) {
setState(() {
_volume = value;
});
});
});
} catch (e, stack) {
print("[VolumeSlider.onChange()] $e");
print(stack);
}
},
),
),

View File

@ -1,22 +1,32 @@
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:mpv_dart/mpv_dart.dart';
import 'package:spotube/helpers/zero-pad-num-str.dart';
class PlayerControls extends StatefulWidget {
final MPVPlayer player;
final Stream<Duration> positionStream;
final bool isPlaying;
final double duration;
final Duration duration;
final bool shuffled;
final Function? onStop;
final Function? onShuffle;
final Function(double value)? onSeek;
final Function? onNext;
final Function? onPrevious;
final Function? onPlay;
final Function? onPause;
const PlayerControls({
required this.player,
required this.positionStream,
required this.isPlaying,
required this.duration,
required this.shuffled,
this.onShuffle,
this.onStop,
this.onSeek,
this.onNext,
this.onPrevious,
this.onPlay,
this.onPause,
Key? key,
}) : super(key: key);
@ -25,64 +35,58 @@ class PlayerControls extends StatefulWidget {
}
class _PlayerControlsState extends State<PlayerControls> {
double currentPos = 0;
StreamSubscription? _timePositionListener;
@override
void initState() {
super.initState();
widget.player.on(MPVEvents.timeposition, null, (ev, context) {
widget.player.getPercentPosition().then((value) {
setState(() {
currentPos = value / 100;
});
});
});
}
@override
void dispose() {
widget.player.removeAllByEvent(MPVEvents.timeposition);
void dispose() async {
await _timePositionListener?.cancel();
super.dispose();
}
@override
Widget build(BuildContext context) {
var totalDuration = Duration(seconds: widget.duration.toInt());
var totalMinutes = zeroPadNumStr(totalDuration.inMinutes.remainder(60));
var totalSeconds = zeroPadNumStr(totalDuration.inSeconds.remainder(60));
var currentDuration =
Duration(seconds: (widget.duration * currentPos).toInt());
var currentMinutes = zeroPadNumStr(currentDuration.inMinutes.remainder(60));
var currentSeconds = zeroPadNumStr(currentDuration.inSeconds.remainder(60));
return Container(
constraints: const BoxConstraints(maxWidth: 700),
child: Column(
children: [
Row(
children: [
Expanded(
child: Slider.adaptive(
value: currentPos,
onChanged: (value) async {
try {
setState(() {
currentPos = value;
});
await widget.player.goToPosition(value * widget.duration);
} catch (e) {
print("[PlayerControls]: $e");
}
},
),
),
Text(
"$currentMinutes:$currentSeconds/$totalMinutes:$totalSeconds",
)
],
),
StreamBuilder<Duration>(
stream: widget.positionStream,
builder: (context, snapshot) {
var totalMinutes =
zeroPadNumStr(widget.duration.inMinutes.remainder(60));
var totalSeconds =
zeroPadNumStr(widget.duration.inSeconds.remainder(60));
var currentMinutes = snapshot.hasData
? zeroPadNumStr(snapshot.data!.inMinutes.remainder(60))
: "00";
var currentSeconds = snapshot.hasData
? zeroPadNumStr(snapshot.data!.inSeconds.remainder(60))
: "00";
var sliderMax = widget.duration.inSeconds;
var sliderValue = snapshot.data?.inSeconds ?? 0;
return Row(
children: [
Expanded(
child: Slider.adaptive(
// cannot divide by zero
// there's an edge case for value being bigger
// than total duration. Keeping it resolved
value: (sliderMax == 0 || sliderValue > sliderMax)
? 0
: sliderValue / sliderMax,
onChanged: (value) {},
onChangeEnd: (value) {
widget.onSeek?.call(value * sliderMax);
},
),
),
Text(
"$currentMinutes:$currentSeconds/$totalMinutes:$totalSeconds",
)
],
);
}),
Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
@ -95,13 +99,8 @@ class _PlayerControlsState extends State<PlayerControls> {
}),
IconButton(
icon: const Icon(Icons.skip_previous_rounded),
onPressed: () async {
bool moved = await widget.player.prev();
if (moved) {
setState(() {
currentPos = 0;
});
}
onPressed: () {
widget.onPrevious?.call();
}),
IconButton(
icon: Icon(
@ -109,30 +108,17 @@ class _PlayerControlsState extends State<PlayerControls> {
? Icons.pause_rounded
: Icons.play_arrow_rounded,
),
onPressed: () async {
onPressed: () {
widget.isPlaying
? await widget.player.pause()
: await widget.player.play();
? widget.onPause?.call()
: widget.onPlay?.call();
}),
IconButton(
icon: const Icon(Icons.skip_next_rounded),
onPressed: () async {
bool moved = await widget.player.next();
if (moved) {
setState(() {
currentPos = 0;
});
}
}),
onPressed: () => widget.onNext?.call()),
IconButton(
icon: const Icon(Icons.stop_rounded),
onPressed: () async {
await widget.player.stop();
widget.onStop?.call();
setState(() {
currentPos = 0;
});
},
onPressed: () => widget.onStop?.call(),
)
],
)

View File

@ -87,6 +87,7 @@ class _PlaylistCardState extends State<PlaylistCard> {
name: widget.playlist.name!,
thumbnail: widget.playlist.images!.first.url!,
);
playback.setCurrentTrack = tracks.first;
},
child: Icon(
isPlaylistPlaying

View File

@ -155,6 +155,7 @@ class _PlaylistViewState extends State<PlaylistView> {
.images![0]
.url!,
);
playback.setCurrentTrack = tracks.first;
}
}
: null,

View File

@ -0,0 +1,24 @@
import 'package:spotify/spotify.dart';
import 'package:youtube_explode_dart/youtube_explode_dart.dart';
YoutubeExplode youtube = YoutubeExplode();
Future<Track> toYoutubeTrack(Track track) async {
var artistsName = track.artists?.map((ar) => ar.name).toList() ?? [];
String queryString =
"${artistsName.first} - ${track.name}${artistsName.length > 1 ? " feat. ${artistsName.sublist(1).join(" ")}" : ""}";
SearchList videos = await youtube.search.getVideos(queryString);
List<Video> matchedVideos = videos.where((video) {
return video.title.contains(track.name!) &&
(track.artists?.every((artist) => video.title.contains(artist.name!)) ??
false);
}).toList();
Video ytVideo = matchedVideos.isNotEmpty ? matchedVideos.first : videos.first;
var trackManifest = await youtube.videos.streams.getManifest(ytVideo.id);
track.uri = trackManifest.audioOnly.first.url.toString();
return track;
}

View File

@ -1,7 +1,6 @@
import 'package:bitsdojo_window/bitsdojo_window.dart';
import 'package:flutter/material.dart';
import 'package:hotkey_manager/hotkey_manager.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';
@ -9,18 +8,16 @@ 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';
import 'package:spotube/provider/UserPreferences.dart';
void main() async {
// Must add this line.
WidgetsFlutterBinding.ensureInitialized();
// For hot reload, `unregisterAll()` needs to be called.
await hotKeyManager.unregisterAll();
runApp(MyApp());
doWhenWindowReady(() {
appWindow.minSize = const Size(900, 700);
appWindow.size = const Size(900, 700);
appWindow.alignment = Alignment.center;
appWindow.maximize();
appWindow.show();
@ -67,15 +64,6 @@ 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",
],
)),
),
ChangeNotifierProvider<UserPreferences>(
create: (context) {
return UserPreferences();

View File

@ -12,6 +12,8 @@ class CurrentPlaylist {
required String this.name,
required String this.thumbnail,
});
List<String> get trackIds => tracks.map((e) => e.id!).toList();
}
class Playback extends ChangeNotifier {
@ -40,6 +42,22 @@ class Playback extends ChangeNotifier {
_currentTrack = null;
notifyListeners();
}
/// sets the provided id matched track's uri\
/// Doesn't notify listeners\
/// @returns `bool` - `true` if succeed & `false` when failed
bool setTrackUriById(String id, String uri) {
if (_currentPlaylist == null) return false;
try {
int index =
_currentPlaylist!.tracks.indexWhere((element) => element.id == id);
if (index == -1) return false;
_currentPlaylist!.tracks[index].uri = uri;
return _currentPlaylist!.tracks[index].uri == uri;
} catch (e) {
return false;
}
}
}
var x = Playback();

View File

@ -1,15 +0,0 @@
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();
}
}

View File

@ -8,6 +8,7 @@
#include <bitsdojo_window_linux/bitsdojo_window_plugin.h>
#include <hotkey_manager/hotkey_manager_plugin.h>
#include <libwinmedia/libwinmedia_plugin.h>
#include <url_launcher_linux/url_launcher_plugin.h>
void fl_register_plugins(FlPluginRegistry* registry) {
@ -17,6 +18,9 @@ void fl_register_plugins(FlPluginRegistry* registry) {
g_autoptr(FlPluginRegistrar) hotkey_manager_registrar =
fl_plugin_registry_get_registrar_for_plugin(registry, "HotkeyManagerPlugin");
hotkey_manager_plugin_register_with_registrar(hotkey_manager_registrar);
g_autoptr(FlPluginRegistrar) libwinmedia_registrar =
fl_plugin_registry_get_registrar_for_plugin(registry, "LibwinmediaPlugin");
libwinmedia_plugin_register_with_registrar(libwinmedia_registrar);
g_autoptr(FlPluginRegistrar) url_launcher_linux_registrar =
fl_plugin_registry_get_registrar_for_plugin(registry, "UrlLauncherPlugin");
url_launcher_plugin_register_with_registrar(url_launcher_linux_registrar);

View File

@ -5,6 +5,7 @@
list(APPEND FLUTTER_PLUGIN_LIST
bitsdojo_window_linux
hotkey_manager
libwinmedia
url_launcher_linux
)

View File

@ -29,6 +29,13 @@ packages:
url: "https://pub.dartlang.org"
source: hosted
version: "2.8.2"
audio_session:
dependency: transitive
description:
name: audio_session
url: "https://pub.dartlang.org"
source: hosted
version: "0.1.6+1"
bitsdojo_window:
dependency: "direct main"
description:
@ -141,13 +148,6 @@ packages:
url: "https://pub.dartlang.org"
source: hosted
version: "1.0.4"
eventify:
dependency: transitive
description:
name: eventify
url: "https://pub.dartlang.org"
source: hosted
version: "1.0.0"
fake_async:
dependency: transitive
description:
@ -275,6 +275,41 @@ packages:
url: "https://pub.dartlang.org"
source: hosted
version: "4.4.0"
just_audio:
dependency: "direct main"
description:
name: just_audio
url: "https://pub.dartlang.org"
source: hosted
version: "0.9.18"
just_audio_libwinmedia:
dependency: "direct main"
description:
name: just_audio_libwinmedia
url: "https://pub.dartlang.org"
source: hosted
version: "0.0.4"
just_audio_platform_interface:
dependency: transitive
description:
name: just_audio_platform_interface
url: "https://pub.dartlang.org"
source: hosted
version: "4.0.0"
just_audio_web:
dependency: transitive
description:
name: just_audio_web
url: "https://pub.dartlang.org"
source: hosted
version: "0.4.2"
libwinmedia:
dependency: transitive
description:
name: libwinmedia
url: "https://pub.dartlang.org"
source: hosted
version: "0.0.7"
lints:
dependency: transitive
description:
@ -296,13 +331,6 @@ packages:
url: "https://pub.dartlang.org"
source: hosted
version: "1.7.0"
mpv_dart:
dependency: "direct main"
description:
name: mpv_dart
url: "https://pub.dartlang.org"
source: hosted
version: "0.0.1"
msix:
dependency: "direct dev"
description:

View File

@ -42,10 +42,11 @@ dependencies:
spotify: ^0.6.0
url_launcher: ^6.0.17
youtube_explode_dart: ^1.10.8
mpv_dart: ^0.0.1
infinite_scroll_pagination: ^3.1.0
bitsdojo_window: ^0.1.1+1
hotkey_manager: ^0.1.6
just_audio: ^0.9.18
just_audio_libwinmedia: ^0.0.4
dev_dependencies:
flutter_test:

View File

@ -8,6 +8,7 @@
#include <bitsdojo_window_windows/bitsdojo_window_plugin.h>
#include <hotkey_manager/hotkey_manager_plugin.h>
#include <libwinmedia/libwinmedia_plugin.h>
#include <url_launcher_windows/url_launcher_windows.h>
void RegisterPlugins(flutter::PluginRegistry* registry) {
@ -15,6 +16,8 @@ void RegisterPlugins(flutter::PluginRegistry* registry) {
registry->GetRegistrarForPlugin("BitsdojoWindowPlugin"));
HotkeyManagerPluginRegisterWithRegistrar(
registry->GetRegistrarForPlugin("HotkeyManagerPlugin"));
LibwinmediaPluginRegisterWithRegistrar(
registry->GetRegistrarForPlugin("LibwinmediaPlugin"));
UrlLauncherWindowsRegisterWithRegistrar(
registry->GetRegistrarForPlugin("UrlLauncherWindows"));
}

View File

@ -5,6 +5,7 @@
list(APPEND FLUTTER_PLUGIN_LIST
bitsdojo_window_windows
hotkey_manager
libwinmedia
url_launcher_windows
)