mirror of
https://github.com/KRTirtho/spotube.git
synced 2025-09-13 07:55:18 +00:00
Got playlist caching working again with proper volume controls
This commit is contained in:
parent
f07a142274
commit
bc1334dd6d
5
.vscode/settings.json
vendored
5
.vscode/settings.json
vendored
@ -1,3 +1,6 @@
|
|||||||
{
|
{
|
||||||
"cmake.configureOnOpen": false
|
"cmake.configureOnOpen": false,
|
||||||
|
"cSpell.words": [
|
||||||
|
"Mpris"
|
||||||
|
]
|
||||||
}
|
}
|
@ -1,20 +1,14 @@
|
|||||||
import 'dart:async';
|
|
||||||
|
|
||||||
import 'package:audioplayers/audioplayers.dart';
|
|
||||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:shared_preferences/shared_preferences.dart';
|
|
||||||
import 'package:spotube/components/Player/PlayerActions.dart';
|
import 'package:spotube/components/Player/PlayerActions.dart';
|
||||||
import 'package:spotube/components/Player/PlayerOverlay.dart';
|
import 'package:spotube/components/Player/PlayerOverlay.dart';
|
||||||
import 'package:spotube/components/Player/PlayerTrackDetails.dart';
|
import 'package:spotube/components/Player/PlayerTrackDetails.dart';
|
||||||
import 'package:spotube/components/Player/PlayerControls.dart';
|
import 'package:spotube/components/Player/PlayerControls.dart';
|
||||||
import 'package:spotube/helpers/image-to-url-string.dart';
|
import 'package:spotube/helpers/image-to-url-string.dart';
|
||||||
import 'package:spotube/hooks/useBreakpoints.dart';
|
import 'package:spotube/hooks/useBreakpoints.dart';
|
||||||
import 'package:spotube/models/LocalStorageKeys.dart';
|
|
||||||
import 'package:spotube/models/Logger.dart';
|
import 'package:spotube/models/Logger.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:spotube/utils/AudioPlayerHandler.dart';
|
|
||||||
|
|
||||||
class Player extends HookConsumerWidget {
|
class Player extends HookConsumerWidget {
|
||||||
Player({Key? key}) : super(key: key);
|
Player({Key? key}) : super(key: key);
|
||||||
@ -26,11 +20,6 @@ class Player extends HookConsumerWidget {
|
|||||||
|
|
||||||
final breakpoint = useBreakpoints();
|
final breakpoint = useBreakpoints();
|
||||||
|
|
||||||
final Future<SharedPreferences> future =
|
|
||||||
useMemoized(SharedPreferences.getInstance);
|
|
||||||
final AsyncSnapshot<SharedPreferences?> localStorage =
|
|
||||||
useFuture(future, initialData: null);
|
|
||||||
|
|
||||||
String albumArt = useMemoized(
|
String albumArt = useMemoized(
|
||||||
() => imageToUrlString(
|
() => imageToUrlString(
|
||||||
playback.track?.album?.images,
|
playback.track?.album?.images,
|
||||||
@ -114,16 +103,29 @@ class Player extends HookConsumerWidget {
|
|||||||
Container(
|
Container(
|
||||||
height: 20,
|
height: 20,
|
||||||
constraints: const BoxConstraints(maxWidth: 200),
|
constraints: const BoxConstraints(maxWidth: 200),
|
||||||
child: Slider.adaptive(
|
child: HookBuilder(builder: (context) {
|
||||||
value: playback.volume,
|
final volume = useState(
|
||||||
onChanged: (value) async {
|
useMemoized(() => playback.volume, []),
|
||||||
|
);
|
||||||
|
return Slider.adaptive(
|
||||||
|
min: 0,
|
||||||
|
max: 1,
|
||||||
|
value: volume.value,
|
||||||
|
onChanged: (v) {
|
||||||
|
volume.value = v;
|
||||||
|
},
|
||||||
|
onChangeEnd: (value) async {
|
||||||
try {
|
try {
|
||||||
|
// You don't really need to know why but this
|
||||||
|
// way it works only
|
||||||
|
await playback.setVolume(value);
|
||||||
await playback.setVolume(value);
|
await playback.setVolume(value);
|
||||||
} catch (e, stack) {
|
} catch (e, stack) {
|
||||||
logger.e("onChange", e, stack);
|
logger.e("onChange", e, stack);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
),
|
);
|
||||||
|
}),
|
||||||
),
|
),
|
||||||
PlayerActions()
|
PlayerActions()
|
||||||
],
|
],
|
||||||
|
@ -1,10 +1,10 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:spotube/helpers/zero-pad-num-str.dart';
|
import 'package:spotube/helpers/zero-pad-num-str.dart';
|
||||||
import 'package:spotube/hooks/playback.dart';
|
import 'package:spotube/hooks/playback.dart';
|
||||||
import 'package:spotube/models/Logger.dart';
|
import 'package:spotube/models/Logger.dart';
|
||||||
import 'package:spotube/provider/Playback.dart';
|
import 'package:spotube/provider/Playback.dart';
|
||||||
import 'package:spotube/utils/AudioPlayerHandler.dart';
|
|
||||||
|
|
||||||
class PlayerControls extends HookConsumerWidget {
|
class PlayerControls extends HookConsumerWidget {
|
||||||
final Color? iconColor;
|
final Color? iconColor;
|
||||||
@ -47,18 +47,32 @@ class PlayerControls extends HookConsumerWidget {
|
|||||||
|
|
||||||
final sliderMax = duration.inSeconds;
|
final sliderMax = duration.inSeconds;
|
||||||
final sliderValue = snapshot.data?.inSeconds ?? 0;
|
final sliderValue = snapshot.data?.inSeconds ?? 0;
|
||||||
final value = (sliderMax == 0 || sliderValue > sliderMax)
|
|
||||||
|
return HookBuilder(builder: (context) {
|
||||||
|
final progressStatic =
|
||||||
|
(sliderMax == 0 || sliderValue > sliderMax)
|
||||||
? 0
|
? 0
|
||||||
: sliderValue / sliderMax;
|
: sliderValue / sliderMax;
|
||||||
|
|
||||||
|
final progress = useState<num>(
|
||||||
|
useMemoized(() => progressStatic, []),
|
||||||
|
);
|
||||||
|
|
||||||
|
useEffect(() {
|
||||||
|
progress.value = progressStatic;
|
||||||
|
return null;
|
||||||
|
}, [progressStatic]);
|
||||||
|
|
||||||
return Column(
|
return Column(
|
||||||
children: [
|
children: [
|
||||||
Slider.adaptive(
|
Slider.adaptive(
|
||||||
// cannot divide by zero
|
// cannot divide by zero
|
||||||
// there's an edge case for value being bigger
|
// there's an edge case for value being bigger
|
||||||
// than total duration. Keeping it resolved
|
// than total duration. Keeping it resolved
|
||||||
value: value.toDouble(),
|
value: progress.value.toDouble(),
|
||||||
onChanged: (_) {},
|
onChanged: (v) {
|
||||||
|
progress.value = v;
|
||||||
|
},
|
||||||
onChangeEnd: (value) async {
|
onChangeEnd: (value) async {
|
||||||
await playback.seekPosition(
|
await playback.seekPosition(
|
||||||
Duration(
|
Duration(
|
||||||
@ -82,6 +96,7 @@ class PlayerControls extends HookConsumerWidget {
|
|||||||
),
|
),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
|
});
|
||||||
}),
|
}),
|
||||||
Row(
|
Row(
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
|
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
|
||||||
|
@ -59,7 +59,6 @@ class PlayerView extends HookConsumerWidget {
|
|||||||
),
|
),
|
||||||
backgroundColor: paletteColor.color,
|
backgroundColor: paletteColor.color,
|
||||||
body: Column(
|
body: Column(
|
||||||
// mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
|
||||||
children: [
|
children: [
|
||||||
Padding(
|
Padding(
|
||||||
padding: const EdgeInsets.all(10),
|
padding: const EdgeInsets.all(10),
|
||||||
|
@ -30,3 +30,71 @@ extension VideoFromCacheTrackExtension on Video {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
extension ThumbnailSetJson on ThumbnailSet {
|
||||||
|
static ThumbnailSet fromJson(Map<String, dynamic> map) {
|
||||||
|
return ThumbnailSet(map["videoId"]);
|
||||||
|
}
|
||||||
|
|
||||||
|
Map<String, dynamic> toJson() {
|
||||||
|
return {
|
||||||
|
"videoId": videoId,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension EngagementJson on Engagement {
|
||||||
|
static Engagement fromJson(Map<String, dynamic> map) {
|
||||||
|
return Engagement(
|
||||||
|
map["viewCount"],
|
||||||
|
map["likeCount"],
|
||||||
|
map["dislikeCount"],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Map<String, dynamic> toJson() {
|
||||||
|
return {
|
||||||
|
"dislikeCount": dislikeCount,
|
||||||
|
"likeCount": likeCount,
|
||||||
|
"viewCount": viewCount,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension VideoToJson on Video {
|
||||||
|
static Video fromJson(Map<String, dynamic> map) {
|
||||||
|
return Video(
|
||||||
|
VideoId(map["id"]),
|
||||||
|
map["title"],
|
||||||
|
map["author"],
|
||||||
|
ChannelId(map["channelId"]),
|
||||||
|
DateTime.tryParse(map["uploadDate"]),
|
||||||
|
DateTime.tryParse(map["publishDate"]),
|
||||||
|
map["description"],
|
||||||
|
parseDuration(map["duration"]),
|
||||||
|
ThumbnailSetJson.fromJson(map["thumbnails"]),
|
||||||
|
List.castFrom<dynamic, String>(map["keywords"]),
|
||||||
|
EngagementJson.fromJson(map["engagement"]),
|
||||||
|
map["isLive"],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Map<String, dynamic> toJson() {
|
||||||
|
return {
|
||||||
|
"hasWatchPage": hasWatchPage,
|
||||||
|
"url": url,
|
||||||
|
"author": author,
|
||||||
|
"channelId": channelId.value,
|
||||||
|
"description": description,
|
||||||
|
"duration": duration.toString(),
|
||||||
|
"engagement": engagement.toJson(),
|
||||||
|
"id": id.value,
|
||||||
|
"isLive": isLive,
|
||||||
|
"keywords": keywords.toList(),
|
||||||
|
"publishDate": publishDate.toString(),
|
||||||
|
"thumbnails": thumbnails.toJson(),
|
||||||
|
"title": title,
|
||||||
|
"uploadDate": uploadDate.toString(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -30,8 +30,15 @@ Future<void> Function() usePreviousTrack(Playback playback) {
|
|||||||
Future<void> Function([dynamic]) useTogglePlayPause(Playback playback) {
|
Future<void> Function([dynamic]) useTogglePlayPause(Playback playback) {
|
||||||
return ([key]) async {
|
return ([key]) async {
|
||||||
try {
|
try {
|
||||||
if (playback.track == null) return;
|
if (playback.track == null) {
|
||||||
|
return;
|
||||||
|
} else if (playback.track != null &&
|
||||||
|
playback.currentDuration == Duration.zero &&
|
||||||
|
await playback.player.getCurrentPosition() == Duration.zero) {
|
||||||
|
await playback.play();
|
||||||
|
} else {
|
||||||
await playback.togglePlayPause();
|
await playback.togglePlayPause();
|
||||||
|
}
|
||||||
} catch (e, stack) {
|
} catch (e, stack) {
|
||||||
logger.e("useTogglePlayPause", e, stack);
|
logger.e("useTogglePlayPause", e, stack);
|
||||||
}
|
}
|
||||||
|
@ -1,214 +0,0 @@
|
|||||||
// This file was generated using the following command and may be overwritten.
|
|
||||||
// dart-dbus generate-object defs/org.mpris.MediaPlayer2.xml
|
|
||||||
|
|
||||||
import 'package:bitsdojo_window/bitsdojo_window.dart';
|
|
||||||
import 'package:dbus/dbus.dart';
|
|
||||||
import 'package:spotube/provider/DBus.dart';
|
|
||||||
|
|
||||||
class Media_Player extends DBusObject {
|
|
||||||
/// Creates a new object to expose on [path].
|
|
||||||
Media_Player() : super(DBusObjectPath('/org/mpris/MediaPlayer2')) {
|
|
||||||
dbus.registerObject(this);
|
|
||||||
}
|
|
||||||
|
|
||||||
void dispose() {
|
|
||||||
dbus.unregisterObject(this);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Gets value of property org.mpris.MediaPlayer2.CanQuit
|
|
||||||
Future<DBusMethodResponse> getCanQuit() async {
|
|
||||||
return DBusMethodSuccessResponse([const DBusBoolean(true)]);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Gets value of property org.mpris.MediaPlayer2.Fullscreen
|
|
||||||
Future<DBusMethodResponse> getFullscreen() async {
|
|
||||||
return DBusMethodSuccessResponse([const DBusBoolean(false)]);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Sets property org.mpris.MediaPlayer2.Fullscreen
|
|
||||||
Future<DBusMethodResponse> setFullscreen(bool value) async {
|
|
||||||
return DBusMethodSuccessResponse();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Gets value of property org.mpris.MediaPlayer2.CanSetFullscreen
|
|
||||||
Future<DBusMethodResponse> getCanSetFullscreen() async {
|
|
||||||
return DBusMethodSuccessResponse([const DBusBoolean(false)]);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Gets value of property org.mpris.MediaPlayer2.CanRaise
|
|
||||||
Future<DBusMethodResponse> getCanRaise() async {
|
|
||||||
return DBusMethodSuccessResponse([const DBusBoolean(false)]);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Gets value of property org.mpris.MediaPlayer2.HasTrackList
|
|
||||||
Future<DBusMethodResponse> getHasTrackList() async {
|
|
||||||
return DBusMethodSuccessResponse([const DBusBoolean(false)]);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Gets value of property org.mpris.MediaPlayer2.Identity
|
|
||||||
Future<DBusMethodResponse> getIdentity() async {
|
|
||||||
return DBusMethodSuccessResponse([const DBusString("Spotube")]);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Gets value of property org.mpris.MediaPlayer2.DesktopEntry
|
|
||||||
Future<DBusMethodResponse> getDesktopEntry() async {
|
|
||||||
return DBusMethodSuccessResponse([const DBusString("spotube")]);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Gets value of property org.mpris.MediaPlayer2.SupportedUriSchemes
|
|
||||||
Future<DBusMethodResponse> getSupportedUriSchemes() async {
|
|
||||||
return DBusMethodSuccessResponse([
|
|
||||||
DBusArray.string(["http"])
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Gets value of property org.mpris.MediaPlayer2.SupportedMimeTypes
|
|
||||||
Future<DBusMethodResponse> getSupportedMimeTypes() async {
|
|
||||||
return DBusMethodSuccessResponse([
|
|
||||||
DBusArray.string(["audio/mpeg"])
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Implementation of org.mpris.MediaPlayer2.Raise()
|
|
||||||
Future<DBusMethodResponse> doRaise() async {
|
|
||||||
return DBusMethodSuccessResponse();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Implementation of org.mpris.MediaPlayer2.Quit()
|
|
||||||
Future<DBusMethodResponse> doQuit() async {
|
|
||||||
appWindow.close();
|
|
||||||
return DBusMethodSuccessResponse();
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
List<DBusIntrospectInterface> introspect() {
|
|
||||||
return [
|
|
||||||
DBusIntrospectInterface('org.mpris.MediaPlayer2', methods: [
|
|
||||||
DBusIntrospectMethod('Raise'),
|
|
||||||
DBusIntrospectMethod('Quit')
|
|
||||||
], properties: [
|
|
||||||
DBusIntrospectProperty('CanQuit', DBusSignature('b'),
|
|
||||||
access: DBusPropertyAccess.read),
|
|
||||||
DBusIntrospectProperty('Fullscreen', DBusSignature('b'),
|
|
||||||
access: DBusPropertyAccess.readwrite),
|
|
||||||
DBusIntrospectProperty('CanSetFullscreen', DBusSignature('b'),
|
|
||||||
access: DBusPropertyAccess.read),
|
|
||||||
DBusIntrospectProperty('CanRaise', DBusSignature('b'),
|
|
||||||
access: DBusPropertyAccess.read),
|
|
||||||
DBusIntrospectProperty('HasTrackList', DBusSignature('b'),
|
|
||||||
access: DBusPropertyAccess.read),
|
|
||||||
DBusIntrospectProperty('Identity', DBusSignature('s'),
|
|
||||||
access: DBusPropertyAccess.read),
|
|
||||||
DBusIntrospectProperty('DesktopEntry', DBusSignature('s'),
|
|
||||||
access: DBusPropertyAccess.read),
|
|
||||||
DBusIntrospectProperty('SupportedUriSchemes', DBusSignature('as'),
|
|
||||||
access: DBusPropertyAccess.read),
|
|
||||||
DBusIntrospectProperty('SupportedMimeTypes', DBusSignature('as'),
|
|
||||||
access: DBusPropertyAccess.read)
|
|
||||||
])
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
Future<DBusMethodResponse> handleMethodCall(DBusMethodCall methodCall) async {
|
|
||||||
if (methodCall.interface == 'org.mpris.MediaPlayer2') {
|
|
||||||
if (methodCall.name == 'Raise') {
|
|
||||||
if (methodCall.values.isNotEmpty) {
|
|
||||||
return DBusMethodErrorResponse.invalidArgs();
|
|
||||||
}
|
|
||||||
return doRaise();
|
|
||||||
} else if (methodCall.name == 'Quit') {
|
|
||||||
if (methodCall.values.isNotEmpty) {
|
|
||||||
return DBusMethodErrorResponse.invalidArgs();
|
|
||||||
}
|
|
||||||
return doQuit();
|
|
||||||
} else {
|
|
||||||
return DBusMethodErrorResponse.unknownMethod();
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
return DBusMethodErrorResponse.unknownInterface();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
Future<DBusMethodResponse> getProperty(String interface, String name) async {
|
|
||||||
if (interface == 'org.mpris.MediaPlayer2') {
|
|
||||||
if (name == 'CanQuit') {
|
|
||||||
return getCanQuit();
|
|
||||||
} else if (name == 'Fullscreen') {
|
|
||||||
return getFullscreen();
|
|
||||||
} else if (name == 'CanSetFullscreen') {
|
|
||||||
return getCanSetFullscreen();
|
|
||||||
} else if (name == 'CanRaise') {
|
|
||||||
return getCanRaise();
|
|
||||||
} else if (name == 'HasTrackList') {
|
|
||||||
return getHasTrackList();
|
|
||||||
} else if (name == 'Identity') {
|
|
||||||
return getIdentity();
|
|
||||||
} else if (name == 'DesktopEntry') {
|
|
||||||
return getDesktopEntry();
|
|
||||||
} else if (name == 'SupportedUriSchemes') {
|
|
||||||
return getSupportedUriSchemes();
|
|
||||||
} else if (name == 'SupportedMimeTypes') {
|
|
||||||
return getSupportedMimeTypes();
|
|
||||||
} else {
|
|
||||||
return DBusMethodErrorResponse.unknownProperty();
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
return DBusMethodErrorResponse.unknownProperty();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
Future<DBusMethodResponse> setProperty(
|
|
||||||
String interface, String name, DBusValue value) async {
|
|
||||||
if (interface == 'org.mpris.MediaPlayer2') {
|
|
||||||
if (name == 'CanQuit') {
|
|
||||||
return DBusMethodErrorResponse.propertyReadOnly();
|
|
||||||
} else if (name == 'Fullscreen') {
|
|
||||||
if (value.signature != DBusSignature('b')) {
|
|
||||||
return DBusMethodErrorResponse.invalidArgs();
|
|
||||||
}
|
|
||||||
return setFullscreen((value as DBusBoolean).value);
|
|
||||||
} else if (name == 'CanSetFullscreen') {
|
|
||||||
return DBusMethodErrorResponse.propertyReadOnly();
|
|
||||||
} else if (name == 'CanRaise') {
|
|
||||||
return DBusMethodErrorResponse.propertyReadOnly();
|
|
||||||
} else if (name == 'HasTrackList') {
|
|
||||||
return DBusMethodErrorResponse.propertyReadOnly();
|
|
||||||
} else if (name == 'Identity') {
|
|
||||||
return DBusMethodErrorResponse.propertyReadOnly();
|
|
||||||
} else if (name == 'DesktopEntry') {
|
|
||||||
return DBusMethodErrorResponse.propertyReadOnly();
|
|
||||||
} else if (name == 'SupportedUriSchemes') {
|
|
||||||
return DBusMethodErrorResponse.propertyReadOnly();
|
|
||||||
} else if (name == 'SupportedMimeTypes') {
|
|
||||||
return DBusMethodErrorResponse.propertyReadOnly();
|
|
||||||
} else {
|
|
||||||
return DBusMethodErrorResponse.unknownProperty();
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
return DBusMethodErrorResponse.unknownProperty();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
Future<DBusMethodResponse> getAllProperties(String interface) async {
|
|
||||||
var properties = <String, DBusValue>{};
|
|
||||||
if (interface == 'org.mpris.MediaPlayer2') {
|
|
||||||
properties['CanQuit'] = (await getCanQuit()).returnValues[0];
|
|
||||||
properties['Fullscreen'] = (await getFullscreen()).returnValues[0];
|
|
||||||
properties['CanSetFullscreen'] =
|
|
||||||
(await getCanSetFullscreen()).returnValues[0];
|
|
||||||
properties['CanRaise'] = (await getCanRaise()).returnValues[0];
|
|
||||||
properties['HasTrackList'] = (await getHasTrackList()).returnValues[0];
|
|
||||||
properties['Identity'] = (await getIdentity()).returnValues[0];
|
|
||||||
properties['DesktopEntry'] = (await getDesktopEntry()).returnValues[0];
|
|
||||||
properties['SupportedUriSchemes'] =
|
|
||||||
(await getSupportedUriSchemes()).returnValues[0];
|
|
||||||
properties['SupportedMimeTypes'] =
|
|
||||||
(await getSupportedMimeTypes()).returnValues[0];
|
|
||||||
}
|
|
||||||
return DBusMethodSuccessResponse([DBusDict.stringVariant(properties)]);
|
|
||||||
}
|
|
||||||
}
|
|
@ -12,9 +12,9 @@ import 'package:spotube/provider/AudioPlayer.dart';
|
|||||||
import 'package:spotube/provider/Playback.dart';
|
import 'package:spotube/provider/Playback.dart';
|
||||||
import 'package:spotube/provider/UserPreferences.dart';
|
import 'package:spotube/provider/UserPreferences.dart';
|
||||||
import 'package:spotube/provider/YouTube.dart';
|
import 'package:spotube/provider/YouTube.dart';
|
||||||
|
import 'package:spotube/services/MobileAudioService.dart';
|
||||||
import 'package:spotube/themes/dark-theme.dart';
|
import 'package:spotube/themes/dark-theme.dart';
|
||||||
import 'package:spotube/themes/light-theme.dart';
|
import 'package:spotube/themes/light-theme.dart';
|
||||||
import 'package:spotube/utils/AudioPlayerHandler.dart';
|
|
||||||
import 'package:spotube/utils/platform.dart';
|
import 'package:spotube/utils/platform.dart';
|
||||||
|
|
||||||
void main() async {
|
void main() async {
|
||||||
@ -33,7 +33,7 @@ void main() async {
|
|||||||
appWindow.show();
|
appWindow.show();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
AudioPlayerHandler? audioServiceHandler;
|
MobileAudioService? audioServiceHandler;
|
||||||
runApp(ProviderScope(
|
runApp(ProviderScope(
|
||||||
child: Spotube(),
|
child: Spotube(),
|
||||||
overrides: [
|
overrides: [
|
||||||
@ -50,7 +50,7 @@ void main() async {
|
|||||||
|
|
||||||
if (audioServiceHandler == null) {
|
if (audioServiceHandler == null) {
|
||||||
AudioService.init(
|
AudioService.init(
|
||||||
builder: () => AudioPlayerHandler(playback),
|
builder: () => MobileAudioService(playback),
|
||||||
config: const AudioServiceConfig(
|
config: const AudioServiceConfig(
|
||||||
androidNotificationChannelId: 'com.krtirtho.Spotube',
|
androidNotificationChannelId: 'com.krtirtho.Spotube',
|
||||||
androidNotificationChannelName: 'Spotube',
|
androidNotificationChannelName: 'Spotube',
|
||||||
|
@ -1,4 +1,6 @@
|
|||||||
import 'package:spotify/spotify.dart';
|
import 'package:spotify/spotify.dart';
|
||||||
|
import 'package:spotube/models/CurrentPlaylist.dart';
|
||||||
|
import 'package:spotube/extensions/yt-video-from-cache-track.dart';
|
||||||
import 'package:youtube_explode_dart/youtube_explode_dart.dart';
|
import 'package:youtube_explode_dart/youtube_explode_dart.dart';
|
||||||
|
|
||||||
enum SpotubeTrackMatchAlgorithm {
|
enum SpotubeTrackMatchAlgorithm {
|
||||||
@ -14,11 +16,16 @@ class SpotubeTrack extends Track {
|
|||||||
Video ytTrack;
|
Video ytTrack;
|
||||||
String ytUri;
|
String ytUri;
|
||||||
|
|
||||||
|
SpotubeTrack(
|
||||||
|
this.ytTrack,
|
||||||
|
this.ytUri,
|
||||||
|
) : super();
|
||||||
|
|
||||||
SpotubeTrack.fromTrack({
|
SpotubeTrack.fromTrack({
|
||||||
required Track track,
|
required Track track,
|
||||||
required this.ytTrack,
|
required this.ytTrack,
|
||||||
required this.ytUri,
|
required this.ytUri,
|
||||||
}) {
|
}) : super() {
|
||||||
album = track.album;
|
album = track.album;
|
||||||
artists = track.artists;
|
artists = track.artists;
|
||||||
availableMarkets = track.availableMarkets;
|
availableMarkets = track.availableMarkets;
|
||||||
@ -38,4 +45,38 @@ class SpotubeTrack extends Track {
|
|||||||
type = track.type;
|
type = track.type;
|
||||||
uri = track.uri;
|
uri = track.uri;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static SpotubeTrack fromJson(Map<String, dynamic> map) {
|
||||||
|
return SpotubeTrack.fromTrack(
|
||||||
|
track: Track.fromJson(map),
|
||||||
|
ytTrack: VideoToJson.fromJson(map["ytTrack"]),
|
||||||
|
ytUri: map["ytUri"],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Map<String, dynamic> toJson() {
|
||||||
|
return {
|
||||||
|
"album": album?.toJson(),
|
||||||
|
"artists": artists?.map((artist) => artist.toJson()).toList(),
|
||||||
|
"availableMarkets": availableMarkets,
|
||||||
|
"discNumber": discNumber,
|
||||||
|
"duration": duration.toString(),
|
||||||
|
"durationMs": durationMs,
|
||||||
|
"explicit": explicit,
|
||||||
|
// "externalIds": externalIds,
|
||||||
|
// "externalUrls": externalUrls,
|
||||||
|
"href": href,
|
||||||
|
"id": id,
|
||||||
|
"isPlayable": isPlayable,
|
||||||
|
// "linkedFrom": linkedFrom,
|
||||||
|
"name": name,
|
||||||
|
"popularity": popularity,
|
||||||
|
"previewUrl": previewUrl,
|
||||||
|
"trackNumber": trackNumber,
|
||||||
|
"type": type,
|
||||||
|
"uri": uri,
|
||||||
|
"ytTrack": ytTrack.toJson(),
|
||||||
|
"ytUri": ytUri,
|
||||||
|
};
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,325 +0,0 @@
|
|||||||
import 'dart:async';
|
|
||||||
import 'dart:convert';
|
|
||||||
import 'dart:io';
|
|
||||||
|
|
||||||
import 'package:audio_service/audio_service.dart';
|
|
||||||
import 'package:audioplayers/audioplayers.dart';
|
|
||||||
import 'package:dbus/dbus.dart';
|
|
||||||
import 'package:flutter/widgets.dart';
|
|
||||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
|
||||||
import 'package:hive/hive.dart';
|
|
||||||
import 'package:spotify/spotify.dart';
|
|
||||||
import 'package:spotube/entities/CacheTrack.dart';
|
|
||||||
import 'package:spotube/helpers/artist-to-string.dart';
|
|
||||||
import 'package:spotube/helpers/image-to-url-string.dart';
|
|
||||||
import 'package:spotube/helpers/search-youtube.dart';
|
|
||||||
import 'package:spotube/interfaces/media_player2.dart';
|
|
||||||
import 'package:spotube/interfaces/media_player2_player.dart';
|
|
||||||
import 'package:spotube/models/CurrentPlaylist.dart';
|
|
||||||
import 'package:spotube/models/Logger.dart';
|
|
||||||
import 'package:spotube/provider/DBus.dart';
|
|
||||||
import 'package:spotube/provider/UserPreferences.dart';
|
|
||||||
import 'package:spotube/provider/YouTube.dart';
|
|
||||||
import 'package:spotube/utils/AudioPlayerHandler.dart';
|
|
||||||
import 'package:spotube/utils/PersistedChangeNotifier.dart';
|
|
||||||
import 'package:youtube_explode_dart/youtube_explode_dart.dart';
|
|
||||||
|
|
||||||
class LegacyPlayback extends PersistedChangeNotifier {
|
|
||||||
UrlSource? _currentAudioSource;
|
|
||||||
final _logger = getLogger(LegacyPlayback);
|
|
||||||
CurrentPlaylist? _currentPlaylist;
|
|
||||||
Track? _currentTrack;
|
|
||||||
|
|
||||||
// states
|
|
||||||
bool _isPlaying = false;
|
|
||||||
Duration? duration;
|
|
||||||
|
|
||||||
bool _shuffled = false;
|
|
||||||
|
|
||||||
AudioPlayerHandler player;
|
|
||||||
YoutubeExplode youtube;
|
|
||||||
Ref ref;
|
|
||||||
|
|
||||||
LazyBox<CacheTrack>? cacheTrackBox;
|
|
||||||
|
|
||||||
@protected
|
|
||||||
final DBusClient? dbus;
|
|
||||||
Media_Player? _media_player;
|
|
||||||
Player_Interface? _mpris;
|
|
||||||
|
|
||||||
double volume = 1;
|
|
||||||
|
|
||||||
LegacyPlayback({
|
|
||||||
required this.player,
|
|
||||||
required this.youtube,
|
|
||||||
required this.ref,
|
|
||||||
required this.dbus,
|
|
||||||
CurrentPlaylist? currentPlaylist,
|
|
||||||
Track? currentTrack,
|
|
||||||
}) : _currentPlaylist = currentPlaylist,
|
|
||||||
_currentTrack = currentTrack,
|
|
||||||
super() {
|
|
||||||
player.onNextRequest = () {
|
|
||||||
movePlaylistPositionBy(1);
|
|
||||||
};
|
|
||||||
player.onPreviousRequest = () {
|
|
||||||
movePlaylistPositionBy(-1);
|
|
||||||
};
|
|
||||||
|
|
||||||
_init();
|
|
||||||
}
|
|
||||||
|
|
||||||
StreamSubscription<Duration?>? _durationStream;
|
|
||||||
StreamSubscription<PlayerState>? _playingStream;
|
|
||||||
StreamSubscription<Duration>? _positionStream;
|
|
||||||
|
|
||||||
void _init() async {
|
|
||||||
// dbus m.p.r.i.s stuff
|
|
||||||
if (Platform.isLinux) {
|
|
||||||
try {
|
|
||||||
_media_player = Media_Player();
|
|
||||||
_mpris = Player_Interface(player: player.core, playback: this);
|
|
||||||
await dbus?.registerObject(_media_player!);
|
|
||||||
await dbus?.registerObject(_mpris!);
|
|
||||||
} catch (e) {
|
|
||||||
logger.e("[MPRIS initialization error]", e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
cacheTrackBox = await Hive.openLazyBox<CacheTrack>("track-cache");
|
|
||||||
|
|
||||||
_playingStream = player.core.onPlayerStateChanged.listen(
|
|
||||||
(state) async {
|
|
||||||
_isPlaying = state == PlayerState.playing;
|
|
||||||
if (state == PlayerState.completed) {
|
|
||||||
if (_currentTrack?.id != null) {
|
|
||||||
movePlaylistPositionBy(1);
|
|
||||||
} else {
|
|
||||||
_isPlaying = false;
|
|
||||||
duration = null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
notifyListeners();
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
_durationStream = player.core.onDurationChanged.listen((event) {
|
|
||||||
duration = event;
|
|
||||||
notifyListeners();
|
|
||||||
});
|
|
||||||
|
|
||||||
_positionStream = player.core.onPositionChanged.listen((pos) async {
|
|
||||||
if (pos > Duration.zero &&
|
|
||||||
(duration == null || duration == Duration.zero)) {
|
|
||||||
duration = await player.core.getDuration();
|
|
||||||
notifyListeners();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
void dispose() {
|
|
||||||
_playingStream?.cancel();
|
|
||||||
_durationStream?.cancel();
|
|
||||||
_positionStream?.cancel();
|
|
||||||
cacheTrackBox?.close();
|
|
||||||
if (Platform.isLinux && _media_player != null && _mpris != null) {
|
|
||||||
dbus?.unregisterObject(_media_player!);
|
|
||||||
dbus?.unregisterObject(_mpris!);
|
|
||||||
}
|
|
||||||
super.dispose();
|
|
||||||
}
|
|
||||||
|
|
||||||
bool get shuffled => _shuffled;
|
|
||||||
CurrentPlaylist? get currentPlaylist => _currentPlaylist;
|
|
||||||
Track? get currentTrack => _currentTrack;
|
|
||||||
bool get isPlaying => _isPlaying;
|
|
||||||
|
|
||||||
set setCurrentTrack(Track track) {
|
|
||||||
_logger.v("[Setting Current Track] ${track.name} - ${track.id}");
|
|
||||||
_currentTrack = track;
|
|
||||||
notifyListeners();
|
|
||||||
updatePersistence();
|
|
||||||
}
|
|
||||||
|
|
||||||
set setCurrentPlaylist(CurrentPlaylist playlist) {
|
|
||||||
_logger.v("[Current Playlist Changed] ${playlist.name} - ${playlist.id}");
|
|
||||||
_currentPlaylist = playlist;
|
|
||||||
notifyListeners();
|
|
||||||
updatePersistence();
|
|
||||||
}
|
|
||||||
|
|
||||||
void reset() {
|
|
||||||
_logger.v("Playback Reset");
|
|
||||||
_isPlaying = false;
|
|
||||||
_shuffled = false;
|
|
||||||
duration = null;
|
|
||||||
_currentPlaylist = null;
|
|
||||||
_currentTrack = null;
|
|
||||||
notifyListeners();
|
|
||||||
updatePersistence(clearNullEntries: true);
|
|
||||||
}
|
|
||||||
|
|
||||||
void setVolume(double newVolume) {
|
|
||||||
volume = newVolume;
|
|
||||||
notifyListeners();
|
|
||||||
updatePersistence();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// 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;
|
|
||||||
updatePersistence();
|
|
||||||
return _currentPlaylist!.tracks[index].uri == uri;
|
|
||||||
} catch (e) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void movePlaylistPositionBy(int pos) {
|
|
||||||
_logger.v("[Playlist Position Move] $pos");
|
|
||||||
if (_currentTrack != null && _currentPlaylist != null) {
|
|
||||||
final int index =
|
|
||||||
_currentPlaylist!.trackIds.indexOf(_currentTrack!.id!) + pos;
|
|
||||||
|
|
||||||
final safeIndex = index > _currentPlaylist!.trackIds.length - 1
|
|
||||||
? 0
|
|
||||||
: index < 0
|
|
||||||
? _currentPlaylist!.trackIds.length
|
|
||||||
: index;
|
|
||||||
Track? track = _currentPlaylist!.tracks.asMap().containsKey(safeIndex)
|
|
||||||
? _currentPlaylist!.tracks.elementAt(safeIndex)
|
|
||||||
: null;
|
|
||||||
if (track != null) {
|
|
||||||
duration = null;
|
|
||||||
_currentTrack = track;
|
|
||||||
notifyListeners();
|
|
||||||
updatePersistence();
|
|
||||||
// starts to play the newly entered next/prev track
|
|
||||||
startPlaying();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> startPlaying([Track? track]) async {
|
|
||||||
_logger.v("[Track Playing] ${track?.name} - ${track?.id}");
|
|
||||||
try {
|
|
||||||
// the track is already playing so no need to change that
|
|
||||||
if (track != null && track.id == _currentTrack?.id) return;
|
|
||||||
track ??= _currentTrack;
|
|
||||||
if (track != null) {
|
|
||||||
Uri? parsedUri = Uri.tryParse(track.uri ?? "");
|
|
||||||
final tag = MediaItem(
|
|
||||||
id: track.id!,
|
|
||||||
title: track.name!,
|
|
||||||
album: track.album?.name,
|
|
||||||
artist: artistsToString(track.artists ?? <ArtistSimple>[]),
|
|
||||||
artUri: Uri.parse(imageToUrlString(track.album?.images)),
|
|
||||||
);
|
|
||||||
player.addItem(tag);
|
|
||||||
if (parsedUri != null && parsedUri.hasAbsolutePath) {
|
|
||||||
_currentAudioSource = UrlSource(parsedUri.toString());
|
|
||||||
await player.core
|
|
||||||
.play(
|
|
||||||
_currentAudioSource!,
|
|
||||||
)
|
|
||||||
.then((value) async {
|
|
||||||
_currentTrack = track;
|
|
||||||
notifyListeners();
|
|
||||||
updatePersistence();
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
final preferences = ref.read(userPreferencesProvider);
|
|
||||||
final spotubeTrack = await toSpotubeTrack(
|
|
||||||
youtube: youtube,
|
|
||||||
track: track,
|
|
||||||
format: preferences.ytSearchFormat,
|
|
||||||
matchAlgorithm: preferences.trackMatchAlgorithm,
|
|
||||||
audioQuality: preferences.audioQuality,
|
|
||||||
box: cacheTrackBox,
|
|
||||||
);
|
|
||||||
if (setTrackUriById(track.id!, spotubeTrack.ytUri)) {
|
|
||||||
logger.v("[Track Direct Source] - ${spotubeTrack.ytUri}");
|
|
||||||
_currentAudioSource = UrlSource(spotubeTrack.ytUri);
|
|
||||||
await player.core
|
|
||||||
.play(
|
|
||||||
_currentAudioSource!,
|
|
||||||
)
|
|
||||||
.then((value) {
|
|
||||||
_currentTrack = spotubeTrack;
|
|
||||||
notifyListeners();
|
|
||||||
updatePersistence();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (e, stack) {
|
|
||||||
_logger.e("startPlaying", e, stack);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void shuffle() {
|
|
||||||
if (currentPlaylist?.shuffle() == true) {
|
|
||||||
_shuffled = true;
|
|
||||||
notifyListeners();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void unshuffle() {
|
|
||||||
if (currentPlaylist?.unshuffle() == true) {
|
|
||||||
_shuffled = false;
|
|
||||||
notifyListeners();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
FutureOr<void> loadFromLocal(Map<String, dynamic> map) {
|
|
||||||
if (map["currentPlaylist"] != null) {
|
|
||||||
_currentPlaylist =
|
|
||||||
CurrentPlaylist.fromJson(jsonDecode(map["currentPlaylist"]));
|
|
||||||
}
|
|
||||||
if (map["currentTrack"] != null) {
|
|
||||||
_currentTrack = Track.fromJson(jsonDecode(map["currentTrack"]));
|
|
||||||
startPlaying().then((_) {
|
|
||||||
Timer.periodic(const Duration(milliseconds: 100), (timer) {
|
|
||||||
if (player.core.state == PlayerState.playing) {
|
|
||||||
player.pause();
|
|
||||||
timer.cancel();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
volume = map["volume"] ?? volume;
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
FutureOr<Map<String, dynamic>> toMap() {
|
|
||||||
return {
|
|
||||||
"currentPlaylist": currentPlaylist != null
|
|
||||||
? jsonEncode(currentPlaylist?.toJson())
|
|
||||||
: null,
|
|
||||||
"currentTrack":
|
|
||||||
currentTrack != null ? jsonEncode(currentTrack?.toJson()) : null,
|
|
||||||
"volume": volume,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
final legacyPlaybackProvider = ChangeNotifierProvider<LegacyPlayback>((ref) {
|
|
||||||
final player = AudioPlayerHandler();
|
|
||||||
final youtube = ref.watch(youtubeProvider);
|
|
||||||
final dbus = ref.watch(dbusClientProvider);
|
|
||||||
return LegacyPlayback(
|
|
||||||
player: player,
|
|
||||||
youtube: youtube,
|
|
||||||
ref: ref,
|
|
||||||
dbus: dbus,
|
|
||||||
);
|
|
||||||
});
|
|
@ -1,9 +1,9 @@
|
|||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
|
import 'dart:convert';
|
||||||
import 'dart:io';
|
import 'dart:io';
|
||||||
|
|
||||||
import 'package:audio_service/audio_service.dart';
|
import 'package:audio_service/audio_service.dart';
|
||||||
import 'package:audioplayers/audioplayers.dart';
|
import 'package:audioplayers/audioplayers.dart';
|
||||||
import 'package:flutter/widgets.dart';
|
|
||||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
import 'package:hive/hive.dart';
|
import 'package:hive/hive.dart';
|
||||||
import 'package:spotify/spotify.dart';
|
import 'package:spotify/spotify.dart';
|
||||||
@ -14,20 +14,20 @@ import 'package:spotube/helpers/contains-text-in-bracket.dart';
|
|||||||
import 'package:spotube/helpers/getLyrics.dart';
|
import 'package:spotube/helpers/getLyrics.dart';
|
||||||
import 'package:spotube/helpers/image-to-url-string.dart';
|
import 'package:spotube/helpers/image-to-url-string.dart';
|
||||||
import 'package:spotube/helpers/search-youtube.dart';
|
import 'package:spotube/helpers/search-youtube.dart';
|
||||||
import 'package:spotube/interfaces/media_player2.dart';
|
|
||||||
import 'package:spotube/interfaces/media_player2_player.dart';
|
|
||||||
import 'package:spotube/models/CurrentPlaylist.dart';
|
import 'package:spotube/models/CurrentPlaylist.dart';
|
||||||
import 'package:spotube/models/Logger.dart';
|
import 'package:spotube/models/Logger.dart';
|
||||||
import 'package:spotube/models/SpotubeTrack.dart';
|
import 'package:spotube/models/SpotubeTrack.dart';
|
||||||
import 'package:spotube/provider/AudioPlayer.dart';
|
import 'package:spotube/provider/AudioPlayer.dart';
|
||||||
import 'package:spotube/provider/UserPreferences.dart';
|
import 'package:spotube/provider/UserPreferences.dart';
|
||||||
import 'package:spotube/provider/YouTube.dart';
|
import 'package:spotube/provider/YouTube.dart';
|
||||||
import 'package:spotube/utils/AudioPlayerHandler.dart';
|
import 'package:spotube/services/LinuxAudioService.dart';
|
||||||
|
import 'package:spotube/services/MobileAudioService.dart';
|
||||||
|
import 'package:spotube/utils/PersistedChangeNotifier.dart';
|
||||||
import 'package:youtube_explode_dart/youtube_explode_dart.dart' hide Playlist;
|
import 'package:youtube_explode_dart/youtube_explode_dart.dart' hide Playlist;
|
||||||
import 'package:collection/collection.dart';
|
import 'package:collection/collection.dart';
|
||||||
import 'package:spotube/extensions/list-sort-multiple.dart';
|
import 'package:spotube/extensions/list-sort-multiple.dart';
|
||||||
|
|
||||||
class Playback with ChangeNotifier {
|
class Playback extends PersistedChangeNotifier {
|
||||||
// player properties
|
// player properties
|
||||||
bool isShuffled;
|
bool isShuffled;
|
||||||
bool isPlaying;
|
bool isPlaying;
|
||||||
@ -35,9 +35,8 @@ class Playback with ChangeNotifier {
|
|||||||
double volume;
|
double volume;
|
||||||
|
|
||||||
// class dependencies
|
// class dependencies
|
||||||
Media_Player? linuxMPRIS;
|
LinuxAudioService? _linuxAudioService;
|
||||||
Player_Interface? linuxMPRIS_Player;
|
MobileAudioService? mobileAudioService;
|
||||||
AudioPlayerHandler? mobileAudioService;
|
|
||||||
|
|
||||||
// foreign/passed properties
|
// foreign/passed properties
|
||||||
AudioPlayer player;
|
AudioPlayer player;
|
||||||
@ -66,8 +65,7 @@ class Playback with ChangeNotifier {
|
|||||||
_subscriptions = [],
|
_subscriptions = [],
|
||||||
super() {
|
super() {
|
||||||
if (Platform.isLinux) {
|
if (Platform.isLinux) {
|
||||||
linuxMPRIS = Media_Player();
|
_linuxAudioService = LinuxAudioService(this);
|
||||||
linuxMPRIS_Player = Player_Interface(playback: this);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
(() async {
|
(() async {
|
||||||
@ -89,8 +87,10 @@ class Playback with ChangeNotifier {
|
|||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
player.onDurationChanged.listen((event) {
|
player.onDurationChanged.listen((event) {
|
||||||
|
if (event != currentDuration) {
|
||||||
currentDuration = event;
|
currentDuration = event;
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
|
}
|
||||||
}),
|
}),
|
||||||
player.onPositionChanged.listen((pos) async {
|
player.onPositionChanged.listen((pos) async {
|
||||||
if (pos > Duration.zero && currentDuration == Duration.zero) {
|
if (pos > Duration.zero && currentDuration == Duration.zero) {
|
||||||
@ -104,8 +104,7 @@ class Playback with ChangeNotifier {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
void dispose() {
|
void dispose() {
|
||||||
linuxMPRIS?.dispose();
|
_linuxAudioService?.dispose();
|
||||||
linuxMPRIS_Player?.dispose();
|
|
||||||
for (var subscription in _subscriptions) {
|
for (var subscription in _subscriptions) {
|
||||||
subscription.cancel();
|
subscription.cancel();
|
||||||
}
|
}
|
||||||
@ -151,6 +150,7 @@ class Playback with ChangeNotifier {
|
|||||||
await player.play(UrlSource(track.ytUri)).then((_) {
|
await player.play(UrlSource(track.ytUri)).then((_) {
|
||||||
this.track = track as SpotubeTrack;
|
this.track = track as SpotubeTrack;
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
|
updatePersistence();
|
||||||
});
|
});
|
||||||
} catch (e, stack) {
|
} catch (e, stack) {
|
||||||
_logger.e("play", e, stack);
|
_logger.e("play", e, stack);
|
||||||
@ -191,6 +191,7 @@ class Playback with ChangeNotifier {
|
|||||||
await player.setVolume(volume);
|
await player.setVolume(volume);
|
||||||
volume = newVolume;
|
volume = newVolume;
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
|
updatePersistence();
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> stop() async {
|
Future<void> stop() async {
|
||||||
@ -202,9 +203,13 @@ class Playback with ChangeNotifier {
|
|||||||
track = null;
|
track = null;
|
||||||
currentDuration = Duration.zero;
|
currentDuration = Duration.zero;
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
|
updatePersistence(clearNullEntries: true);
|
||||||
}
|
}
|
||||||
|
|
||||||
destroy() {}
|
void destroy() {
|
||||||
|
stop();
|
||||||
|
player.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
// playlist & track list methods
|
// playlist & track list methods
|
||||||
Future<SpotubeTrack> toSpotubeTrack(Track track) async {
|
Future<SpotubeTrack> toSpotubeTrack(Track track) async {
|
||||||
@ -351,6 +356,26 @@ class Playback with ChangeNotifier {
|
|||||||
if (prevTrackIndex < 0) return;
|
if (prevTrackIndex < 0) return;
|
||||||
await play(playlist!.tracks.elementAt(prevTrackIndex));
|
await play(playlist!.tracks.elementAt(prevTrackIndex));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
FutureOr<void> loadFromLocal(Map<String, dynamic> map) async {
|
||||||
|
if (map["playlist"] != null) {
|
||||||
|
playlist = CurrentPlaylist.fromJson(jsonDecode(map["playlist"]));
|
||||||
|
}
|
||||||
|
if (map["track"] != null) {
|
||||||
|
track = SpotubeTrack.fromJson(jsonDecode(map["track"]));
|
||||||
|
}
|
||||||
|
volume = map["volume"] ?? volume;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
FutureOr<Map<String, dynamic>> toMap() {
|
||||||
|
return {
|
||||||
|
"playlist": playlist != null ? jsonEncode(playlist?.toJson()) : null,
|
||||||
|
"track": track != null ? jsonEncode(track?.toJson()) : null,
|
||||||
|
"volume": volume,
|
||||||
|
};
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
final playbackProvider = ChangeNotifierProvider<Playback>((ref) {
|
final playbackProvider = ChangeNotifierProvider<Playback>((ref) {
|
||||||
|
@ -1,19 +1,226 @@
|
|||||||
// This file was generated using the following command and may be overwritten.
|
|
||||||
// dart-dbus generate-object defs/org.mpris.MediaPlayer2.Player.xml
|
|
||||||
|
|
||||||
import 'dart:io';
|
import 'dart:io';
|
||||||
|
|
||||||
|
import 'package:bitsdojo_window/bitsdojo_window.dart';
|
||||||
import 'package:dbus/dbus.dart';
|
import 'package:dbus/dbus.dart';
|
||||||
|
|
||||||
|
import 'package:spotube/provider/DBus.dart';
|
||||||
import 'package:spotube/helpers/image-to-url-string.dart';
|
import 'package:spotube/helpers/image-to-url-string.dart';
|
||||||
import 'package:spotube/models/SpotubeTrack.dart';
|
import 'package:spotube/models/SpotubeTrack.dart';
|
||||||
import 'package:spotube/provider/Playback.dart';
|
import 'package:spotube/provider/Playback.dart';
|
||||||
import 'package:spotube/provider/DBus.dart';
|
|
||||||
|
|
||||||
class Player_Interface extends DBusObject {
|
class _MprisMediaPlayer2 extends DBusObject {
|
||||||
|
/// Creates a new object to expose on [path].
|
||||||
|
_MprisMediaPlayer2() : super(DBusObjectPath('/org/mpris/MediaPlayer2')) {
|
||||||
|
dbus.registerObject(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
void dispose() {
|
||||||
|
dbus.unregisterObject(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Gets value of property org.mpris.MediaPlayer2.CanQuit
|
||||||
|
Future<DBusMethodResponse> getCanQuit() async {
|
||||||
|
return DBusMethodSuccessResponse([const DBusBoolean(true)]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Gets value of property org.mpris.MediaPlayer2.Fullscreen
|
||||||
|
Future<DBusMethodResponse> getFullscreen() async {
|
||||||
|
return DBusMethodSuccessResponse([const DBusBoolean(false)]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sets property org.mpris.MediaPlayer2.Fullscreen
|
||||||
|
Future<DBusMethodResponse> setFullscreen(bool value) async {
|
||||||
|
return DBusMethodSuccessResponse();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Gets value of property org.mpris.MediaPlayer2.CanSetFullscreen
|
||||||
|
Future<DBusMethodResponse> getCanSetFullscreen() async {
|
||||||
|
return DBusMethodSuccessResponse([const DBusBoolean(false)]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Gets value of property org.mpris.MediaPlayer2.CanRaise
|
||||||
|
Future<DBusMethodResponse> getCanRaise() async {
|
||||||
|
return DBusMethodSuccessResponse([const DBusBoolean(false)]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Gets value of property org.mpris.MediaPlayer2.HasTrackList
|
||||||
|
Future<DBusMethodResponse> getHasTrackList() async {
|
||||||
|
return DBusMethodSuccessResponse([const DBusBoolean(false)]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Gets value of property org.mpris.MediaPlayer2.Identity
|
||||||
|
Future<DBusMethodResponse> getIdentity() async {
|
||||||
|
return DBusMethodSuccessResponse([const DBusString("Spotube")]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Gets value of property org.mpris.MediaPlayer2.DesktopEntry
|
||||||
|
Future<DBusMethodResponse> getDesktopEntry() async {
|
||||||
|
return DBusMethodSuccessResponse([const DBusString("spotube")]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Gets value of property org.mpris.MediaPlayer2.SupportedUriSchemes
|
||||||
|
Future<DBusMethodResponse> getSupportedUriSchemes() async {
|
||||||
|
return DBusMethodSuccessResponse([
|
||||||
|
DBusArray.string(["http"])
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Gets value of property org.mpris.MediaPlayer2.SupportedMimeTypes
|
||||||
|
Future<DBusMethodResponse> getSupportedMimeTypes() async {
|
||||||
|
return DBusMethodSuccessResponse([
|
||||||
|
DBusArray.string(["audio/mpeg"])
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Implementation of org.mpris.MediaPlayer2.Raise()
|
||||||
|
Future<DBusMethodResponse> doRaise() async {
|
||||||
|
return DBusMethodSuccessResponse();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Implementation of org.mpris.MediaPlayer2.Quit()
|
||||||
|
Future<DBusMethodResponse> doQuit() async {
|
||||||
|
appWindow.close();
|
||||||
|
return DBusMethodSuccessResponse();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
List<DBusIntrospectInterface> introspect() {
|
||||||
|
return [
|
||||||
|
DBusIntrospectInterface('org.mpris.MediaPlayer2', methods: [
|
||||||
|
DBusIntrospectMethod('Raise'),
|
||||||
|
DBusIntrospectMethod('Quit')
|
||||||
|
], properties: [
|
||||||
|
DBusIntrospectProperty('CanQuit', DBusSignature('b'),
|
||||||
|
access: DBusPropertyAccess.read),
|
||||||
|
DBusIntrospectProperty('Fullscreen', DBusSignature('b'),
|
||||||
|
access: DBusPropertyAccess.readwrite),
|
||||||
|
DBusIntrospectProperty('CanSetFullscreen', DBusSignature('b'),
|
||||||
|
access: DBusPropertyAccess.read),
|
||||||
|
DBusIntrospectProperty('CanRaise', DBusSignature('b'),
|
||||||
|
access: DBusPropertyAccess.read),
|
||||||
|
DBusIntrospectProperty('HasTrackList', DBusSignature('b'),
|
||||||
|
access: DBusPropertyAccess.read),
|
||||||
|
DBusIntrospectProperty('Identity', DBusSignature('s'),
|
||||||
|
access: DBusPropertyAccess.read),
|
||||||
|
DBusIntrospectProperty('DesktopEntry', DBusSignature('s'),
|
||||||
|
access: DBusPropertyAccess.read),
|
||||||
|
DBusIntrospectProperty('SupportedUriSchemes', DBusSignature('as'),
|
||||||
|
access: DBusPropertyAccess.read),
|
||||||
|
DBusIntrospectProperty('SupportedMimeTypes', DBusSignature('as'),
|
||||||
|
access: DBusPropertyAccess.read)
|
||||||
|
])
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<DBusMethodResponse> handleMethodCall(DBusMethodCall methodCall) async {
|
||||||
|
if (methodCall.interface == 'org.mpris.MediaPlayer2') {
|
||||||
|
if (methodCall.name == 'Raise') {
|
||||||
|
if (methodCall.values.isNotEmpty) {
|
||||||
|
return DBusMethodErrorResponse.invalidArgs();
|
||||||
|
}
|
||||||
|
return doRaise();
|
||||||
|
} else if (methodCall.name == 'Quit') {
|
||||||
|
if (methodCall.values.isNotEmpty) {
|
||||||
|
return DBusMethodErrorResponse.invalidArgs();
|
||||||
|
}
|
||||||
|
return doQuit();
|
||||||
|
} else {
|
||||||
|
return DBusMethodErrorResponse.unknownMethod();
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return DBusMethodErrorResponse.unknownInterface();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<DBusMethodResponse> getProperty(String interface, String name) async {
|
||||||
|
if (interface == 'org.mpris.MediaPlayer2') {
|
||||||
|
if (name == 'CanQuit') {
|
||||||
|
return getCanQuit();
|
||||||
|
} else if (name == 'Fullscreen') {
|
||||||
|
return getFullscreen();
|
||||||
|
} else if (name == 'CanSetFullscreen') {
|
||||||
|
return getCanSetFullscreen();
|
||||||
|
} else if (name == 'CanRaise') {
|
||||||
|
return getCanRaise();
|
||||||
|
} else if (name == 'HasTrackList') {
|
||||||
|
return getHasTrackList();
|
||||||
|
} else if (name == 'Identity') {
|
||||||
|
return getIdentity();
|
||||||
|
} else if (name == 'DesktopEntry') {
|
||||||
|
return getDesktopEntry();
|
||||||
|
} else if (name == 'SupportedUriSchemes') {
|
||||||
|
return getSupportedUriSchemes();
|
||||||
|
} else if (name == 'SupportedMimeTypes') {
|
||||||
|
return getSupportedMimeTypes();
|
||||||
|
} else {
|
||||||
|
return DBusMethodErrorResponse.unknownProperty();
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return DBusMethodErrorResponse.unknownProperty();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<DBusMethodResponse> setProperty(
|
||||||
|
String interface, String name, DBusValue value) async {
|
||||||
|
if (interface == 'org.mpris.MediaPlayer2') {
|
||||||
|
if (name == 'CanQuit') {
|
||||||
|
return DBusMethodErrorResponse.propertyReadOnly();
|
||||||
|
} else if (name == 'Fullscreen') {
|
||||||
|
if (value.signature != DBusSignature('b')) {
|
||||||
|
return DBusMethodErrorResponse.invalidArgs();
|
||||||
|
}
|
||||||
|
return setFullscreen((value as DBusBoolean).value);
|
||||||
|
} else if (name == 'CanSetFullscreen') {
|
||||||
|
return DBusMethodErrorResponse.propertyReadOnly();
|
||||||
|
} else if (name == 'CanRaise') {
|
||||||
|
return DBusMethodErrorResponse.propertyReadOnly();
|
||||||
|
} else if (name == 'HasTrackList') {
|
||||||
|
return DBusMethodErrorResponse.propertyReadOnly();
|
||||||
|
} else if (name == 'Identity') {
|
||||||
|
return DBusMethodErrorResponse.propertyReadOnly();
|
||||||
|
} else if (name == 'DesktopEntry') {
|
||||||
|
return DBusMethodErrorResponse.propertyReadOnly();
|
||||||
|
} else if (name == 'SupportedUriSchemes') {
|
||||||
|
return DBusMethodErrorResponse.propertyReadOnly();
|
||||||
|
} else if (name == 'SupportedMimeTypes') {
|
||||||
|
return DBusMethodErrorResponse.propertyReadOnly();
|
||||||
|
} else {
|
||||||
|
return DBusMethodErrorResponse.unknownProperty();
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return DBusMethodErrorResponse.unknownProperty();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<DBusMethodResponse> getAllProperties(String interface) async {
|
||||||
|
var properties = <String, DBusValue>{};
|
||||||
|
if (interface == 'org.mpris.MediaPlayer2') {
|
||||||
|
properties['CanQuit'] = (await getCanQuit()).returnValues[0];
|
||||||
|
properties['Fullscreen'] = (await getFullscreen()).returnValues[0];
|
||||||
|
properties['CanSetFullscreen'] =
|
||||||
|
(await getCanSetFullscreen()).returnValues[0];
|
||||||
|
properties['CanRaise'] = (await getCanRaise()).returnValues[0];
|
||||||
|
properties['HasTrackList'] = (await getHasTrackList()).returnValues[0];
|
||||||
|
properties['Identity'] = (await getIdentity()).returnValues[0];
|
||||||
|
properties['DesktopEntry'] = (await getDesktopEntry()).returnValues[0];
|
||||||
|
properties['SupportedUriSchemes'] =
|
||||||
|
(await getSupportedUriSchemes()).returnValues[0];
|
||||||
|
properties['SupportedMimeTypes'] =
|
||||||
|
(await getSupportedMimeTypes()).returnValues[0];
|
||||||
|
}
|
||||||
|
return DBusMethodSuccessResponse([DBusDict.stringVariant(properties)]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _MprisMediaPlayer2Player extends DBusObject {
|
||||||
final Playback playback;
|
final Playback playback;
|
||||||
|
|
||||||
/// Creates a new object to expose on [path].
|
/// Creates a new object to expose on [path].
|
||||||
Player_Interface({
|
_MprisMediaPlayer2Player({
|
||||||
required this.playback,
|
required this.playback,
|
||||||
}) : super(DBusObjectPath("/org/mpris/MediaPlayer2")) {
|
}) : super(DBusObjectPath("/org/mpris/MediaPlayer2")) {
|
||||||
(() async {
|
(() async {
|
||||||
@ -474,3 +681,17 @@ class Player_Interface extends DBusObject {
|
|||||||
return DBusMethodSuccessResponse([DBusDict.stringVariant(properties)]);
|
return DBusMethodSuccessResponse([DBusDict.stringVariant(properties)]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class LinuxAudioService {
|
||||||
|
_MprisMediaPlayer2 mp2;
|
||||||
|
_MprisMediaPlayer2Player player;
|
||||||
|
|
||||||
|
LinuxAudioService(Playback playback)
|
||||||
|
: mp2 = _MprisMediaPlayer2(),
|
||||||
|
player = _MprisMediaPlayer2Player(playback: playback);
|
||||||
|
|
||||||
|
void dispose() {
|
||||||
|
mp2.dispose();
|
||||||
|
player.dispose();
|
||||||
|
}
|
||||||
|
}
|
84
lib/services/MobileAudioService.dart
Normal file
84
lib/services/MobileAudioService.dart
Normal file
@ -0,0 +1,84 @@
|
|||||||
|
import 'dart:async';
|
||||||
|
|
||||||
|
import 'package:audio_service/audio_service.dart';
|
||||||
|
import 'package:audioplayers/audioplayers.dart';
|
||||||
|
import 'package:spotube/provider/Playback.dart';
|
||||||
|
|
||||||
|
class MobileAudioService extends BaseAudioHandler {
|
||||||
|
final Playback playback;
|
||||||
|
|
||||||
|
MobileAudioService(this.playback) {
|
||||||
|
final _player = playback.player;
|
||||||
|
_player.onPlayerStateChanged.listen((state) async {
|
||||||
|
if (state != PlayerState.completed) {
|
||||||
|
playbackState.add(await _transformEvent());
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
_player.onPlayerComplete.listen((_) {
|
||||||
|
if (playback.playlist == null && playback.track == null) {
|
||||||
|
playbackState.add(
|
||||||
|
PlaybackState(
|
||||||
|
processingState: AudioProcessingState.completed,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
void addItem(MediaItem item) {
|
||||||
|
mediaItem.add(item);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<void> play() => playback.resume();
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<void> pause() => playback.pause();
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<void> seek(Duration position) => playback.seekPosition(position);
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<void> stop() => playback.stop();
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<void> skipToNext() async {
|
||||||
|
playback.seekForward();
|
||||||
|
await super.skipToNext();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<void> skipToPrevious() async {
|
||||||
|
playback.seekBackward();
|
||||||
|
await super.skipToPrevious();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<void> onTaskRemoved() {
|
||||||
|
playback.destroy();
|
||||||
|
return super.onTaskRemoved();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<PlaybackState> _transformEvent() async {
|
||||||
|
return PlaybackState(
|
||||||
|
controls: [
|
||||||
|
MediaControl.skipToPrevious,
|
||||||
|
playback.player.state == PlayerState.playing
|
||||||
|
? MediaControl.pause
|
||||||
|
: MediaControl.play,
|
||||||
|
MediaControl.skipToNext,
|
||||||
|
MediaControl.stop,
|
||||||
|
],
|
||||||
|
androidCompactActionIndices: const [0, 1, 2],
|
||||||
|
playing: playback.player.state == PlayerState.playing,
|
||||||
|
updatePosition:
|
||||||
|
(await playback.player.getCurrentPosition()) ?? Duration.zero,
|
||||||
|
processingState: playback.player.state == PlayerState.paused
|
||||||
|
? AudioProcessingState.buffering
|
||||||
|
: playback.player.state == PlayerState.playing
|
||||||
|
? AudioProcessingState.ready
|
||||||
|
: AudioProcessingState.idle,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -1,86 +0,0 @@
|
|||||||
import 'dart:async';
|
|
||||||
|
|
||||||
import 'package:audio_service/audio_service.dart';
|
|
||||||
import 'package:spotube/provider/Playback.dart';
|
|
||||||
|
|
||||||
/// An [AudioHandler] for playing a single item.
|
|
||||||
class AudioPlayerHandler extends BaseAudioHandler {
|
|
||||||
final Playback playback;
|
|
||||||
|
|
||||||
/// Initialise our audio handler.
|
|
||||||
AudioPlayerHandler(this.playback) {
|
|
||||||
final _player = playback.player;
|
|
||||||
// So that our clients (the Flutter UI and the system notification) know
|
|
||||||
// what state to display, here we set up our audio handler to broadcast all
|
|
||||||
// playback state changes as they happen via playbackState...
|
|
||||||
// _player.
|
|
||||||
_player.onPlayerStateChanged.listen((state) async {
|
|
||||||
playbackState.add(await _transformEvent());
|
|
||||||
});
|
|
||||||
_player.onDurationChanged.listen((duration) async {
|
|
||||||
playbackState.add(await _transformEvent());
|
|
||||||
});
|
|
||||||
_player.onPositionChanged.listen((state) async {
|
|
||||||
playbackState.add(await _transformEvent());
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
void addItem(MediaItem item) {
|
|
||||||
mediaItem.add(item);
|
|
||||||
}
|
|
||||||
|
|
||||||
// In this simple example, we handle only 4 actions: play, pause, seek and
|
|
||||||
// stop. Any button press from the Flutter UI, notification, lock screen or
|
|
||||||
// headset will be routed through to these 4 methods so that you can handle
|
|
||||||
// your audio playback logic in one place.
|
|
||||||
|
|
||||||
@override
|
|
||||||
Future<void> play() => playback.resume();
|
|
||||||
|
|
||||||
@override
|
|
||||||
Future<void> pause() => playback.pause();
|
|
||||||
|
|
||||||
@override
|
|
||||||
Future<void> seek(Duration position) => playback.seekPosition(position);
|
|
||||||
|
|
||||||
@override
|
|
||||||
Future<void> stop() => playback.stop();
|
|
||||||
|
|
||||||
@override
|
|
||||||
Future<void> skipToNext() async {
|
|
||||||
playback.seekForward();
|
|
||||||
await super.skipToNext();
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
Future<void> skipToPrevious() async {
|
|
||||||
playback.seekBackward();
|
|
||||||
await super.skipToPrevious();
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
Future<void> onTaskRemoved() {
|
|
||||||
playback.destroy();
|
|
||||||
return super.onTaskRemoved();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Transform a just_audio event into an audio_service state.
|
|
||||||
///
|
|
||||||
/// This method is used from the constructor. Every event received from the
|
|
||||||
/// just_audio player will be transformed into an audio_service state so that
|
|
||||||
/// it can be broadcast to audio_service clients.
|
|
||||||
Future<PlaybackState> _transformEvent() async {
|
|
||||||
return PlaybackState(
|
|
||||||
controls: [
|
|
||||||
MediaControl.skipToPrevious,
|
|
||||||
if (playback.isPlaying) MediaControl.pause else MediaControl.play,
|
|
||||||
MediaControl.skipToNext,
|
|
||||||
MediaControl.stop,
|
|
||||||
],
|
|
||||||
androidCompactActionIndices: const [0, 1, 2],
|
|
||||||
playing: playback.isPlaying,
|
|
||||||
updatePosition:
|
|
||||||
(await playback.player.getCurrentPosition()) ?? Duration.zero,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
Loading…
Reference in New Issue
Block a user