mirror of
https://github.com/KRTirtho/spotube.git
synced 2025-09-12 23:45:18 +00:00
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:
parent
f9d3d58398
commit
07b1891cb4
@ -1,15 +1,11 @@
|
|||||||
import 'package:bitsdojo_window/bitsdojo_window.dart';
|
import 'package:bitsdojo_window/bitsdojo_window.dart';
|
||||||
import 'package:flutter/material.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 {
|
class TitleBarActionButtons extends StatelessWidget {
|
||||||
const TitleBarActionButtons({Key? key}) : super(key: key);
|
const TitleBarActionButtons({Key? key}) : super(key: key);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
MPVPlayer player = context.watch<PlayerDI>().player;
|
|
||||||
return Row(
|
return Row(
|
||||||
children: [
|
children: [
|
||||||
TextButton(
|
TextButton(
|
||||||
@ -32,7 +28,6 @@ class TitleBarActionButtons extends StatelessWidget {
|
|||||||
child: const Icon(Icons.crop_square_rounded)),
|
child: const Icon(Icons.crop_square_rounded)),
|
||||||
TextButton(
|
TextButton(
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
player.stop();
|
|
||||||
appWindow.close();
|
appWindow.close();
|
||||||
},
|
},
|
||||||
style: ButtonStyle(
|
style: ButtonStyle(
|
||||||
|
@ -1,16 +1,16 @@
|
|||||||
import 'dart:io';
|
import 'dart:async';
|
||||||
|
|
||||||
import 'package:cached_network_image/cached_network_image.dart';
|
import 'package:cached_network_image/cached_network_image.dart';
|
||||||
import 'package:flutter/foundation.dart';
|
|
||||||
import 'package:hotkey_manager/hotkey_manager.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/components/PlayerControls.dart';
|
||||||
import 'package:spotube/helpers/artist-to-string.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/models/GlobalKeyActions.dart';
|
||||||
import 'package:spotube/provider/Playback.dart';
|
import 'package:spotube/provider/Playback.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:mpv_dart/mpv_dart.dart';
|
|
||||||
import 'package:provider/provider.dart';
|
import 'package:provider/provider.dart';
|
||||||
import 'package:spotube/provider/PlayerDI.dart';
|
|
||||||
import 'package:spotube/provider/SpotifyDI.dart';
|
import 'package:spotube/provider/SpotifyDI.dart';
|
||||||
|
|
||||||
class Player extends StatefulWidget {
|
class Player extends StatefulWidget {
|
||||||
@ -20,169 +20,200 @@ class Player extends StatefulWidget {
|
|||||||
_PlayerState createState() => _PlayerState();
|
_PlayerState createState() => _PlayerState();
|
||||||
}
|
}
|
||||||
|
|
||||||
class _PlayerState extends State<Player> {
|
class _PlayerState extends State<Player> with WidgetsBindingObserver {
|
||||||
|
late AudioPlayer player;
|
||||||
bool _isPlaying = false;
|
bool _isPlaying = false;
|
||||||
bool _shuffled = false;
|
bool _shuffled = false;
|
||||||
double _duration = 0;
|
Duration? _duration;
|
||||||
|
|
||||||
String? _currentPlaylistId;
|
String? _currentTrackId;
|
||||||
|
|
||||||
double _volume = 0;
|
double _volume = 0;
|
||||||
|
|
||||||
List<HotKey> _hotKeys = [];
|
final List<HotKey> _hotKeys = [];
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
player = AudioPlayer();
|
||||||
|
WidgetsBinding.instance?.addObserver(this);
|
||||||
WidgetsBinding.instance?.addPostFrameCallback((timeStamp) async {
|
WidgetsBinding.instance?.addPostFrameCallback((timeStamp) async {
|
||||||
try {
|
try {
|
||||||
MPVPlayer player = context.read<PlayerDI>().player;
|
|
||||||
if (!player.isRunning()) {
|
|
||||||
await player.start();
|
|
||||||
}
|
|
||||||
double volume = await player.getProperty<double>("volume");
|
|
||||||
setState(() {
|
setState(() {
|
||||||
_volume = volume / 100;
|
_volume = player.volume;
|
||||||
});
|
});
|
||||||
player.on(MPVEvents.paused, null, (ev, context) {
|
player.playingStream.listen((playing) async {
|
||||||
setState(() {
|
setState(() {
|
||||||
_isPlaying = false;
|
_isPlaying = playing;
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
player.on(MPVEvents.resumed, null, (ev, context) {
|
player.durationStream.listen((duration) async {
|
||||||
setState(() {
|
if (duration != null) {
|
||||||
_isPlaying = true;
|
// 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
|
||||||
player.on(MPVEvents.status, null, (ev, _) async {
|
// been loaded thus indicating buffering started
|
||||||
Map data = ev.eventData as Map;
|
if (duration != Duration.zero && duration != _duration) {
|
||||||
Playback playback = context.read<Playback>();
|
// this line is for prev/next or already playing playlist
|
||||||
|
if (player.playing) await player.pause();
|
||||||
if (data["property"] == "media-title" && data["value"] != null) {
|
await player.play();
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
if (data["property"] == "duration" && data["value"] != null) {
|
|
||||||
setState(() {
|
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) {
|
player.processingStateStream.listen((event) async {
|
||||||
print("[PLAYER]: $e");
|
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
|
@override
|
||||||
void dispose() async {
|
void dispose() {
|
||||||
MPVPlayer player = context.read<PlayerDI>().player;
|
WidgetsBinding.instance?.removeObserver(this);
|
||||||
player.removeAllByEvent(MPVEvents.paused);
|
player.dispose();
|
||||||
player.removeAllByEvent(MPVEvents.resumed);
|
Future.wait(_hotKeys.map((e) => hotKeyManager.unregister(e)));
|
||||||
player.removeAllByEvent(MPVEvents.status);
|
|
||||||
await Future.wait(_hotKeys.map((e) => hotKeyManager.unregister(e)));
|
|
||||||
super.dispose();
|
super.dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
String playlistToStr(CurrentPlaylist playlist) {
|
@override
|
||||||
var tracks = playlist.tracks.map((track) {
|
void didChangeAppLifecycleState(AppLifecycleState state) {
|
||||||
var artists = artistsToString(track.artists ?? []);
|
if (state == AppLifecycleState.paused) {
|
||||||
var title = track.name?.replaceAll("-", " ");
|
// 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
|
||||||
return "ytdl://ytsearch:$artists - $title";
|
// resume from.
|
||||||
}).toList();
|
player.stop();
|
||||||
|
}
|
||||||
return tracks.join("\n");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Future playPlaylist(MPVPlayer player, CurrentPlaylist playlist) async {
|
void _movePlaylistPositionBy(int pos) {
|
||||||
try {
|
Playback playback = context.read<Playback>();
|
||||||
if (player.isRunning() && playlist.id != _currentPlaylistId) {
|
if (playback.currentTrack != null && playback.currentPlaylist != null) {
|
||||||
var playlistPath = "/tmp/playlist-${playlist.id}.txt";
|
int index = playback.currentPlaylist!.trackIds
|
||||||
File file = File(playlistPath);
|
.indexOf(playback.currentTrack!.id!) +
|
||||||
var newPlaylist = playlistToStr(playlist);
|
pos;
|
||||||
|
|
||||||
if (!await file.exists()) {
|
var safeIndex = index > playback.currentPlaylist!.trackIds.length - 1
|
||||||
await file.create();
|
? 0
|
||||||
}
|
: index < 0
|
||||||
|
? playback.currentPlaylist!.trackIds.length
|
||||||
await file.writeAsString(newPlaylist);
|
: index;
|
||||||
|
Track? track =
|
||||||
await player.loadPlaylist(playlistPath);
|
playback.currentPlaylist!.tracks.asMap().containsKey(safeIndex)
|
||||||
|
? playback.currentPlaylist!.tracks.elementAt(safeIndex)
|
||||||
|
: null;
|
||||||
|
if (track != null) {
|
||||||
|
playback.setCurrentTrack = track;
|
||||||
setState(() {
|
setState(() {
|
||||||
_currentPlaylistId = playlist.id;
|
_duration = null;
|
||||||
_shuffled = false;
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
} 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
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
MPVPlayer player = context.watch<PlayerDI>().player;
|
|
||||||
|
|
||||||
return Container(
|
return Container(
|
||||||
color: Theme.of(context).backgroundColor,
|
color: Theme.of(context).backgroundColor,
|
||||||
child: Consumer<Playback>(
|
child: Consumer<Playback>(
|
||||||
builder: (context, playback, widget) {
|
builder: (context, playback, widget) {
|
||||||
if (playback.currentPlaylist != null) {
|
if (playback.currentPlaylist != null &&
|
||||||
playPlaylist(player, playback.currentPlaylist!);
|
playback.currentTrack != null) {
|
||||||
|
_playTrack(playback.currentTrack!, playback);
|
||||||
}
|
}
|
||||||
|
|
||||||
String? albumArt = playback.currentTrack?.album?.images?.last.url;
|
String? albumArt = playback.currentTrack?.album?.images?.last.url;
|
||||||
@ -223,33 +254,89 @@ class _PlayerState extends State<Player> {
|
|||||||
Flexible(
|
Flexible(
|
||||||
flex: 3,
|
flex: 3,
|
||||||
child: PlayerControls(
|
child: PlayerControls(
|
||||||
player: player,
|
positionStream: player.positionStream,
|
||||||
isPlaying: _isPlaying,
|
isPlaying: _isPlaying,
|
||||||
duration: _duration,
|
duration: _duration ?? Duration.zero,
|
||||||
shuffled: _shuffled,
|
shuffled: _shuffled,
|
||||||
onShuffle: () {
|
onNext: () async {
|
||||||
if (!_shuffled) {
|
try {
|
||||||
player.shuffle().then(
|
await player.pause();
|
||||||
(value) => setState(() {
|
await player.seek(Duration.zero);
|
||||||
_shuffled = true;
|
_movePlaylistPositionBy(1);
|
||||||
}),
|
} catch (e, stack) {
|
||||||
);
|
print("[PlayerControls.onNext()] $e");
|
||||||
} else {
|
print(stack);
|
||||||
player.unshuffle().then(
|
|
||||||
(value) => setState(() {
|
|
||||||
_shuffled = false;
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
onStop: () {
|
onPrevious: () async {
|
||||||
setState(() {
|
try {
|
||||||
_isPlaying = false;
|
await player.pause();
|
||||||
_currentPlaylistId = null;
|
await player.seek(Duration.zero);
|
||||||
_duration = 0;
|
_movePlaylistPositionBy(-1);
|
||||||
_shuffled = false;
|
} catch (e, stack) {
|
||||||
});
|
print("[PlayerControls.onPrevious()] $e");
|
||||||
playback.reset();
|
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),
|
constraints: const BoxConstraints(maxWidth: 200),
|
||||||
child: Slider.adaptive(
|
child: Slider.adaptive(
|
||||||
value: _volume,
|
value: _volume,
|
||||||
onChanged: (value) {
|
onChanged: (value) async {
|
||||||
player.volume(value * 100).then((_) {
|
try {
|
||||||
setState(() {
|
await player.setVolume(value).then((_) {
|
||||||
_volume = value;
|
setState(() {
|
||||||
|
_volume = value;
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
} catch (e, stack) {
|
||||||
|
print("[VolumeSlider.onChange()] $e");
|
||||||
|
print(stack);
|
||||||
|
}
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
@ -1,22 +1,32 @@
|
|||||||
|
import 'dart:async';
|
||||||
|
|
||||||
import 'package:flutter/material.dart';
|
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';
|
import 'package:spotube/helpers/zero-pad-num-str.dart';
|
||||||
|
|
||||||
class PlayerControls extends StatefulWidget {
|
class PlayerControls extends StatefulWidget {
|
||||||
final MPVPlayer player;
|
final Stream<Duration> positionStream;
|
||||||
final bool isPlaying;
|
final bool isPlaying;
|
||||||
final double duration;
|
final Duration duration;
|
||||||
final bool shuffled;
|
final bool shuffled;
|
||||||
final Function? onStop;
|
final Function? onStop;
|
||||||
final Function? onShuffle;
|
final Function? onShuffle;
|
||||||
|
final Function(double value)? onSeek;
|
||||||
|
final Function? onNext;
|
||||||
|
final Function? onPrevious;
|
||||||
|
final Function? onPlay;
|
||||||
|
final Function? onPause;
|
||||||
const PlayerControls({
|
const PlayerControls({
|
||||||
required this.player,
|
required this.positionStream,
|
||||||
required this.isPlaying,
|
required this.isPlaying,
|
||||||
required this.duration,
|
required this.duration,
|
||||||
required this.shuffled,
|
required this.shuffled,
|
||||||
this.onShuffle,
|
this.onShuffle,
|
||||||
this.onStop,
|
this.onStop,
|
||||||
|
this.onSeek,
|
||||||
|
this.onNext,
|
||||||
|
this.onPrevious,
|
||||||
|
this.onPlay,
|
||||||
|
this.onPause,
|
||||||
Key? key,
|
Key? key,
|
||||||
}) : super(key: key);
|
}) : super(key: key);
|
||||||
|
|
||||||
@ -25,64 +35,58 @@ class PlayerControls extends StatefulWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class _PlayerControlsState extends State<PlayerControls> {
|
class _PlayerControlsState extends State<PlayerControls> {
|
||||||
double currentPos = 0;
|
StreamSubscription? _timePositionListener;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void dispose() async {
|
||||||
super.initState();
|
await _timePositionListener?.cancel();
|
||||||
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);
|
|
||||||
super.dispose();
|
super.dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
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(
|
return Container(
|
||||||
constraints: const BoxConstraints(maxWidth: 700),
|
constraints: const BoxConstraints(maxWidth: 700),
|
||||||
child: Column(
|
child: Column(
|
||||||
children: [
|
children: [
|
||||||
Row(
|
StreamBuilder<Duration>(
|
||||||
children: [
|
stream: widget.positionStream,
|
||||||
Expanded(
|
builder: (context, snapshot) {
|
||||||
child: Slider.adaptive(
|
var totalMinutes =
|
||||||
value: currentPos,
|
zeroPadNumStr(widget.duration.inMinutes.remainder(60));
|
||||||
onChanged: (value) async {
|
var totalSeconds =
|
||||||
try {
|
zeroPadNumStr(widget.duration.inSeconds.remainder(60));
|
||||||
setState(() {
|
var currentMinutes = snapshot.hasData
|
||||||
currentPos = value;
|
? zeroPadNumStr(snapshot.data!.inMinutes.remainder(60))
|
||||||
});
|
: "00";
|
||||||
await widget.player.goToPosition(value * widget.duration);
|
var currentSeconds = snapshot.hasData
|
||||||
} catch (e) {
|
? zeroPadNumStr(snapshot.data!.inSeconds.remainder(60))
|
||||||
print("[PlayerControls]: $e");
|
: "00";
|
||||||
}
|
|
||||||
},
|
var sliderMax = widget.duration.inSeconds;
|
||||||
),
|
var sliderValue = snapshot.data?.inSeconds ?? 0;
|
||||||
),
|
return Row(
|
||||||
Text(
|
children: [
|
||||||
"$currentMinutes:$currentSeconds/$totalMinutes:$totalSeconds",
|
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(
|
Row(
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
|
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
|
||||||
children: [
|
children: [
|
||||||
@ -95,13 +99,8 @@ class _PlayerControlsState extends State<PlayerControls> {
|
|||||||
}),
|
}),
|
||||||
IconButton(
|
IconButton(
|
||||||
icon: const Icon(Icons.skip_previous_rounded),
|
icon: const Icon(Icons.skip_previous_rounded),
|
||||||
onPressed: () async {
|
onPressed: () {
|
||||||
bool moved = await widget.player.prev();
|
widget.onPrevious?.call();
|
||||||
if (moved) {
|
|
||||||
setState(() {
|
|
||||||
currentPos = 0;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}),
|
}),
|
||||||
IconButton(
|
IconButton(
|
||||||
icon: Icon(
|
icon: Icon(
|
||||||
@ -109,30 +108,17 @@ class _PlayerControlsState extends State<PlayerControls> {
|
|||||||
? Icons.pause_rounded
|
? Icons.pause_rounded
|
||||||
: Icons.play_arrow_rounded,
|
: Icons.play_arrow_rounded,
|
||||||
),
|
),
|
||||||
onPressed: () async {
|
onPressed: () {
|
||||||
widget.isPlaying
|
widget.isPlaying
|
||||||
? await widget.player.pause()
|
? widget.onPause?.call()
|
||||||
: await widget.player.play();
|
: widget.onPlay?.call();
|
||||||
}),
|
}),
|
||||||
IconButton(
|
IconButton(
|
||||||
icon: const Icon(Icons.skip_next_rounded),
|
icon: const Icon(Icons.skip_next_rounded),
|
||||||
onPressed: () async {
|
onPressed: () => widget.onNext?.call()),
|
||||||
bool moved = await widget.player.next();
|
|
||||||
if (moved) {
|
|
||||||
setState(() {
|
|
||||||
currentPos = 0;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
IconButton(
|
IconButton(
|
||||||
icon: const Icon(Icons.stop_rounded),
|
icon: const Icon(Icons.stop_rounded),
|
||||||
onPressed: () async {
|
onPressed: () => widget.onStop?.call(),
|
||||||
await widget.player.stop();
|
|
||||||
widget.onStop?.call();
|
|
||||||
setState(() {
|
|
||||||
currentPos = 0;
|
|
||||||
});
|
|
||||||
},
|
|
||||||
)
|
)
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
|
@ -87,6 +87,7 @@ class _PlaylistCardState extends State<PlaylistCard> {
|
|||||||
name: widget.playlist.name!,
|
name: widget.playlist.name!,
|
||||||
thumbnail: widget.playlist.images!.first.url!,
|
thumbnail: widget.playlist.images!.first.url!,
|
||||||
);
|
);
|
||||||
|
playback.setCurrentTrack = tracks.first;
|
||||||
},
|
},
|
||||||
child: Icon(
|
child: Icon(
|
||||||
isPlaylistPlaying
|
isPlaylistPlaying
|
||||||
|
@ -155,6 +155,7 @@ class _PlaylistViewState extends State<PlaylistView> {
|
|||||||
.images![0]
|
.images![0]
|
||||||
.url!,
|
.url!,
|
||||||
);
|
);
|
||||||
|
playback.setCurrentTrack = tracks.first;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
: null,
|
: null,
|
||||||
|
24
lib/helpers/search-youtube.dart
Normal file
24
lib/helpers/search-youtube.dart
Normal 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;
|
||||||
|
}
|
@ -1,7 +1,6 @@
|
|||||||
import 'package:bitsdojo_window/bitsdojo_window.dart';
|
import 'package:bitsdojo_window/bitsdojo_window.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:hotkey_manager/hotkey_manager.dart';
|
import 'package:hotkey_manager/hotkey_manager.dart';
|
||||||
import 'package:mpv_dart/mpv_dart.dart';
|
|
||||||
import 'package:provider/provider.dart';
|
import 'package:provider/provider.dart';
|
||||||
import 'package:shared_preferences/shared_preferences.dart';
|
import 'package:shared_preferences/shared_preferences.dart';
|
||||||
import 'package:spotify/spotify.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/models/LocalStorageKeys.dart';
|
||||||
import 'package:spotube/provider/Auth.dart';
|
import 'package:spotube/provider/Auth.dart';
|
||||||
import 'package:spotube/provider/Playback.dart';
|
import 'package:spotube/provider/Playback.dart';
|
||||||
import 'package:spotube/provider/PlayerDI.dart';
|
|
||||||
import 'package:spotube/provider/SpotifyDI.dart';
|
import 'package:spotube/provider/SpotifyDI.dart';
|
||||||
import 'package:spotube/provider/UserPreferences.dart';
|
import 'package:spotube/provider/UserPreferences.dart';
|
||||||
|
|
||||||
void main() async {
|
void main() async {
|
||||||
// Must add this line.
|
|
||||||
WidgetsFlutterBinding.ensureInitialized();
|
WidgetsFlutterBinding.ensureInitialized();
|
||||||
// For hot reload, `unregisterAll()` needs to be called.
|
|
||||||
await hotKeyManager.unregisterAll();
|
await hotKeyManager.unregisterAll();
|
||||||
runApp(MyApp());
|
runApp(MyApp());
|
||||||
doWhenWindowReady(() {
|
doWhenWindowReady(() {
|
||||||
appWindow.minSize = const Size(900, 700);
|
appWindow.minSize = const Size(900, 700);
|
||||||
|
appWindow.size = const Size(900, 700);
|
||||||
appWindow.alignment = Alignment.center;
|
appWindow.alignment = Alignment.center;
|
||||||
appWindow.maximize();
|
appWindow.maximize();
|
||||||
appWindow.show();
|
appWindow.show();
|
||||||
@ -67,15 +64,6 @@ class MyApp extends StatelessWidget {
|
|||||||
);
|
);
|
||||||
}),
|
}),
|
||||||
ChangeNotifierProvider<Playback>(create: (context) => Playback()),
|
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>(
|
ChangeNotifierProvider<UserPreferences>(
|
||||||
create: (context) {
|
create: (context) {
|
||||||
return UserPreferences();
|
return UserPreferences();
|
||||||
|
@ -12,6 +12,8 @@ class CurrentPlaylist {
|
|||||||
required String this.name,
|
required String this.name,
|
||||||
required String this.thumbnail,
|
required String this.thumbnail,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
List<String> get trackIds => tracks.map((e) => e.id!).toList();
|
||||||
}
|
}
|
||||||
|
|
||||||
class Playback extends ChangeNotifier {
|
class Playback extends ChangeNotifier {
|
||||||
@ -40,6 +42,22 @@ class Playback extends ChangeNotifier {
|
|||||||
_currentTrack = null;
|
_currentTrack = null;
|
||||||
notifyListeners();
|
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();
|
var x = Playback();
|
||||||
|
@ -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();
|
|
||||||
}
|
|
||||||
}
|
|
@ -8,6 +8,7 @@
|
|||||||
|
|
||||||
#include <bitsdojo_window_linux/bitsdojo_window_plugin.h>
|
#include <bitsdojo_window_linux/bitsdojo_window_plugin.h>
|
||||||
#include <hotkey_manager/hotkey_manager_plugin.h>
|
#include <hotkey_manager/hotkey_manager_plugin.h>
|
||||||
|
#include <libwinmedia/libwinmedia_plugin.h>
|
||||||
#include <url_launcher_linux/url_launcher_plugin.h>
|
#include <url_launcher_linux/url_launcher_plugin.h>
|
||||||
|
|
||||||
void fl_register_plugins(FlPluginRegistry* registry) {
|
void fl_register_plugins(FlPluginRegistry* registry) {
|
||||||
@ -17,6 +18,9 @@ void fl_register_plugins(FlPluginRegistry* registry) {
|
|||||||
g_autoptr(FlPluginRegistrar) hotkey_manager_registrar =
|
g_autoptr(FlPluginRegistrar) hotkey_manager_registrar =
|
||||||
fl_plugin_registry_get_registrar_for_plugin(registry, "HotkeyManagerPlugin");
|
fl_plugin_registry_get_registrar_for_plugin(registry, "HotkeyManagerPlugin");
|
||||||
hotkey_manager_plugin_register_with_registrar(hotkey_manager_registrar);
|
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 =
|
g_autoptr(FlPluginRegistrar) url_launcher_linux_registrar =
|
||||||
fl_plugin_registry_get_registrar_for_plugin(registry, "UrlLauncherPlugin");
|
fl_plugin_registry_get_registrar_for_plugin(registry, "UrlLauncherPlugin");
|
||||||
url_launcher_plugin_register_with_registrar(url_launcher_linux_registrar);
|
url_launcher_plugin_register_with_registrar(url_launcher_linux_registrar);
|
||||||
|
@ -5,6 +5,7 @@
|
|||||||
list(APPEND FLUTTER_PLUGIN_LIST
|
list(APPEND FLUTTER_PLUGIN_LIST
|
||||||
bitsdojo_window_linux
|
bitsdojo_window_linux
|
||||||
hotkey_manager
|
hotkey_manager
|
||||||
|
libwinmedia
|
||||||
url_launcher_linux
|
url_launcher_linux
|
||||||
)
|
)
|
||||||
|
|
||||||
|
56
pubspec.lock
56
pubspec.lock
@ -29,6 +29,13 @@ packages:
|
|||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.8.2"
|
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:
|
bitsdojo_window:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
@ -141,13 +148,6 @@ packages:
|
|||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.0.4"
|
version: "1.0.4"
|
||||||
eventify:
|
|
||||||
dependency: transitive
|
|
||||||
description:
|
|
||||||
name: eventify
|
|
||||||
url: "https://pub.dartlang.org"
|
|
||||||
source: hosted
|
|
||||||
version: "1.0.0"
|
|
||||||
fake_async:
|
fake_async:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@ -275,6 +275,41 @@ packages:
|
|||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "4.4.0"
|
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:
|
lints:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@ -296,13 +331,6 @@ packages:
|
|||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.7.0"
|
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:
|
msix:
|
||||||
dependency: "direct dev"
|
dependency: "direct dev"
|
||||||
description:
|
description:
|
||||||
|
@ -42,10 +42,11 @@ dependencies:
|
|||||||
spotify: ^0.6.0
|
spotify: ^0.6.0
|
||||||
url_launcher: ^6.0.17
|
url_launcher: ^6.0.17
|
||||||
youtube_explode_dart: ^1.10.8
|
youtube_explode_dart: ^1.10.8
|
||||||
mpv_dart: ^0.0.1
|
|
||||||
infinite_scroll_pagination: ^3.1.0
|
infinite_scroll_pagination: ^3.1.0
|
||||||
bitsdojo_window: ^0.1.1+1
|
bitsdojo_window: ^0.1.1+1
|
||||||
hotkey_manager: ^0.1.6
|
hotkey_manager: ^0.1.6
|
||||||
|
just_audio: ^0.9.18
|
||||||
|
just_audio_libwinmedia: ^0.0.4
|
||||||
|
|
||||||
dev_dependencies:
|
dev_dependencies:
|
||||||
flutter_test:
|
flutter_test:
|
||||||
|
@ -8,6 +8,7 @@
|
|||||||
|
|
||||||
#include <bitsdojo_window_windows/bitsdojo_window_plugin.h>
|
#include <bitsdojo_window_windows/bitsdojo_window_plugin.h>
|
||||||
#include <hotkey_manager/hotkey_manager_plugin.h>
|
#include <hotkey_manager/hotkey_manager_plugin.h>
|
||||||
|
#include <libwinmedia/libwinmedia_plugin.h>
|
||||||
#include <url_launcher_windows/url_launcher_windows.h>
|
#include <url_launcher_windows/url_launcher_windows.h>
|
||||||
|
|
||||||
void RegisterPlugins(flutter::PluginRegistry* registry) {
|
void RegisterPlugins(flutter::PluginRegistry* registry) {
|
||||||
@ -15,6 +16,8 @@ void RegisterPlugins(flutter::PluginRegistry* registry) {
|
|||||||
registry->GetRegistrarForPlugin("BitsdojoWindowPlugin"));
|
registry->GetRegistrarForPlugin("BitsdojoWindowPlugin"));
|
||||||
HotkeyManagerPluginRegisterWithRegistrar(
|
HotkeyManagerPluginRegisterWithRegistrar(
|
||||||
registry->GetRegistrarForPlugin("HotkeyManagerPlugin"));
|
registry->GetRegistrarForPlugin("HotkeyManagerPlugin"));
|
||||||
|
LibwinmediaPluginRegisterWithRegistrar(
|
||||||
|
registry->GetRegistrarForPlugin("LibwinmediaPlugin"));
|
||||||
UrlLauncherWindowsRegisterWithRegistrar(
|
UrlLauncherWindowsRegisterWithRegistrar(
|
||||||
registry->GetRegistrarForPlugin("UrlLauncherWindows"));
|
registry->GetRegistrarForPlugin("UrlLauncherWindows"));
|
||||||
}
|
}
|
||||||
|
@ -5,6 +5,7 @@
|
|||||||
list(APPEND FLUTTER_PLUGIN_LIST
|
list(APPEND FLUTTER_PLUGIN_LIST
|
||||||
bitsdojo_window_windows
|
bitsdojo_window_windows
|
||||||
hotkey_manager
|
hotkey_manager
|
||||||
|
libwinmedia
|
||||||
url_launcher_windows
|
url_launcher_windows
|
||||||
)
|
)
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user