From 44ae5c40bb87e0f450f5a50220f16381470ccc45 Mon Sep 17 00:00:00 2001 From: Tobias Busch Date: Wed, 16 Aug 2023 14:05:46 +0200 Subject: [PATCH] feat: Add playback speed control for podcasts and switch to local device (#160) --- CHANGELOG.md | 3 + README.md | 62 +++-- .../spotify_sdk/SpotifyConnectApi.kt | 25 ++ .../minimalme/spotify_sdk/SpotifyPlayerApi.kt | 4 +- .../minimalme/spotify_sdk/SpotifySdkPlugin.kt | 19 +- example/.env | 4 +- example/lib/main.dart | 256 ++++++++++++++---- lib/enums/podcast_playback_speed.dart | 23 ++ .../podcast_playback_speed_extension.dart | 21 ++ lib/platform_channels.dart | 9 + lib/spotify_sdk.dart | 37 +++ pubspec.yaml | 2 +- 12 files changed, 374 insertions(+), 91 deletions(-) create mode 100644 android/src/main/kotlin/de/minimalme/spotify_sdk/SpotifyConnectApi.kt create mode 100644 lib/enums/podcast_playback_speed.dart create mode 100644 lib/extensions/podcast_playback_speed_extension.dart diff --git a/CHANGELOG.md b/CHANGELOG.md index 5dbfaa76..8f6482d5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,6 @@ +## 3.0.0-dev.2 +* Feat: add set podcastPlaybackSpeed and switchToLocalDevice for android (#160) + ## 3.0.0-dev.1 * **BREAKINg**:feat: update spotify.android:auth from 1.2.6 to 2.1.0 and spotify.app.remote from 0.7.2 to 0.8.0 In the app/build.gradle add the following to the default config for auth to work as described [here](https://github.com/spotify/android-auth#integrating-the-library-into-your-project) diff --git a/README.md b/README.md index 1c8bb518..56cedf34 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ # spotify_sdk

-build +build build build build @@ -153,36 +153,44 @@ Token Swap is for now "web only". While the iOS SDK also supports the "token swa #### Player Api -| Function | Description | Android | iOS | Web | -|---|---|---|---|---| -| getPlayerState | Gets the current player state |✔ | ✔ | ✔ | -| pause | Pauses the current track |✔ | ✔ | ✔ | -| play | Plays the given spotifyUri |✔ | ✔ | ✔ | -| queue | Queues given spotifyUri |✔ | ✔ | ✔ | -| resume | Resumes the current track |✔ | ✔ | ✔ | -| skipNext | Skips to next track | ✔ | ✔ | ✔ | -| skipPrevious | Skips to previous track |✔ | ✔ | ✔ | -| skipToIndex | Skips to track at specified index in album or playlist |✔ | ✔ | 🚧 | -| seekTo | Seeks the current track to the given position in milliseconds | ✔ | ✔ | 🚧 | -| seekToRelativePosition | Adds to the current position of the track the given milliseconds | ✔ | ❌ | 🚧 | -| subscribePlayerContext | Subscribes to the current player context | ✔ | ✔ | ✔ | -| subscribePlayerState| Subscribes to the current player state | ✔ | ✔ | ✔ | -| getCrossfadeState | Gets the current crossfade state | ✔ | ✔ | ❌ | -| toggleShuffle | Cycles through the shuffle modes | ✔ | ❌ | ❌ | -| setShuffle | Set the shuffle mode | ✔ | ✔ | ✔ | -| toggleRepeat | Cycles through the repeat modes | ✔ | ✔ | ❌ | -| setRepeatMode | Set the repeat mode | ✔ | ✔ | ✔ | +The playerApi as described [here](https://spotify.github.io/android-sdk/app-remote-lib/docs/com/spotify/android/appremote/api/PlayerApi.html). + +| Function | Description | Android | iOS | Web | +|-------------------------|---|--|---|---| +| getCrossfadeState | Gets the current crossfade state | ✔ | ✔ | ❌ | +| getPlayerState | Gets the current player state |✔ | ✔ | ✔ | +| pause | Pauses the current track |✔ | ✔ | ✔ | +| play | Plays the given spotifyUri |✔ | ✔ | ✔ | +| playWithStreamType | Play the given Spotify uri with specific behaviour for that streamtype | 🚧 | 🚧 | 🚧 | +| queue | Queues given spotifyUri |✔ | ✔ | ✔ | +| resume | Resumes the current track |✔ | ✔ | ✔ | +| seekTo | Seeks the current track to the given position in milliseconds | ✔ | ✔ | 🚧 | +| seekToRelativePosition | Adds to the current position of the track the given milliseconds | ✔ | ❌ | 🚧 | +| setPodcastPlaybackSpeed | Set playback speed for Podcast | ✔ | 🚧 | 🚧 | +| setRepeatMode | Set the repeat mode | ✔ | ✔ | ✔ | +| setShuffle | Set the shuffle mode | ✔ | ✔ | ✔ | +| skipNext | Skips to next track | ✔ | ✔ | ✔ | +| skipPrevious | Skips to previous track |✔ | ✔ | ✔ | +| skipToIndex | Skips to track at specified index in album or playlist |✔ | ✔ | 🚧 | +| subscribePlayerContext | Subscribes to the current player context | ✔ | ✔ | ✔ | +| subscribePlayerState | Subscribes to the current player state | ✔ | ✔ | ✔ | +| toggleRepeat | Cycles through the repeat modes | ✔ | ✔ | ❌ | +| toggleShuffle | Cycles through the shuffle modes | ✔ | ❌ | ❌ | On Web, an automatic call to play may not work due to media activation policies which send an error: "Authentication Error: Browser prevented autoplay due to lack of interaction". This error is ignored by the SDK so you can still present a button for the user to click to `play` or `resume` to start playback. See the [Web SDK Troubleshooting guide](https://developer.spotify.com/documentation/web-playback-sdk/reference/#troubleshooting) for more details. #### Images Api +The imagesApi as described [here](https://spotify.github.io/android-sdk/app-remote-lib/docs/com/spotify/android/appremote/api/ImagesApi.html). + | Function | Description| Android | iOS | Web | |---|---|---|---|---| | getImage | Get the image from the given spotifyUri | ✔ | ✔ | 🚧 | #### User Api +The userApi as described [here](https://spotify.github.io/android-sdk/app-remote-lib/docs/com/spotify/android/appremote/api/UserApi.html). + | Function | Description| Android | iOS | Web | |---|---|---|---|---| | addToLibrary | Adds the given spotifyUri to the users library | ✔ | ✔ | 🚧 | @@ -194,12 +202,20 @@ On Web, an automatic call to play may not work due to media activation policies #### Connect Api -| Function | Description| Android | iOS | Web | -|---|---|---|---|---| -| connectSwitchToLocalDevice | Switch to play music on this (local) device | 🚧 | 🚧 | 🚧 | +The connectApi as described [here](https://spotify.github.io/android-sdk/app-remote-lib/docs/com/spotify/android/appremote/api/ConnectApi.html). + +| Function | Description | Android | iOS | Web | +|----------------------------|--------------------------------------------|---|---|---| +| connectDecreaseVolume | Decrease volume by a step size determined | 🚧 | 🚧 | 🚧 | +| connectIncreaseVolume | Increase volume by a step size determined | 🚧 | 🚧 | 🚧 | +| connectSetVolume | Set a volume on the currently active device | 🚧 | 🚧 | 🚧 | +| connectSwitchToLocalDevice | Switch to play music on this (local) device | ✔ | 🚧 | 🚧 | +| subscribeToVolumeState | Subscribe to volume state | 🚧 | 🚧 | 🚧 | #### Content Api +The contentApi as described [here](https://spotify.github.io/android-sdk/app-remote-lib/docs/com/spotify/android/appremote/api/ContentApi.html). + | Function | Description| Android | iOS | Web | |---|---|---|---|---| | getChildrenOfItem | tbd | 🚧 | 🚧 | 🚧 | diff --git a/android/src/main/kotlin/de/minimalme/spotify_sdk/SpotifyConnectApi.kt b/android/src/main/kotlin/de/minimalme/spotify_sdk/SpotifyConnectApi.kt new file mode 100644 index 00000000..615f890c --- /dev/null +++ b/android/src/main/kotlin/de/minimalme/spotify_sdk/SpotifyConnectApi.kt @@ -0,0 +1,25 @@ +package de.minimalme.spotify_sdk + +import com.spotify.android.appremote.api.SpotifyAppRemote +import com.spotify.protocol.types.Image.Dimension +import com.spotify.protocol.types.ImageUri +import io.flutter.plugin.common.MethodChannel +import java.io.ByteArrayOutputStream +import android.graphics.Bitmap + +class SpotifyConnectApi(spotifyAppRemote: SpotifyAppRemote?, result: MethodChannel.Result) : BaseSpotifyApi(spotifyAppRemote, result) { + + private val errorConnectSwitchToLocalDevice = "errorConnectSwitchToLocalDevice" + + private val connectApi = spotifyAppRemote?.connectApi + + fun switchToLocalDevice() { + if (connectApi != null) { + connectApi.connectSwitchToLocalDevice() + .setResultCallback { result.success(true) } + .setErrorCallback { throwable -> result.error(errorConnectSwitchToLocalDevice, "error when switching to local device", throwable.toString()) } + } else { + spotifyRemoteAppNotSetError() + } + } +} \ No newline at end of file diff --git a/android/src/main/kotlin/de/minimalme/spotify_sdk/SpotifyPlayerApi.kt b/android/src/main/kotlin/de/minimalme/spotify_sdk/SpotifyPlayerApi.kt index 7cee7a48..9f434d8d 100644 --- a/android/src/main/kotlin/de/minimalme/spotify_sdk/SpotifyPlayerApi.kt +++ b/android/src/main/kotlin/de/minimalme/spotify_sdk/SpotifyPlayerApi.kt @@ -123,7 +123,7 @@ class SpotifyPlayerApi(spotifyAppRemote: SpotifyAppRemote?, result: MethodChanne internal fun setPodcastPlaybackSpeed(podcastPlaybackSpeedValue: Int?) { if (playerApi != null && podcastPlaybackSpeedValue != null) { - val podcastPlaybackSpeed = PlaybackSpeed.PodcastPlaybackSpeed.values()[podcastPlaybackSpeedValue] + val podcastPlaybackSpeed = PlaybackSpeed.PodcastPlaybackSpeed.values().firstOrNull{ it.value == podcastPlaybackSpeedValue } playerApi.setPodcastPlaybackSpeed(podcastPlaybackSpeed) .setResultCallback { result.success(true) } @@ -221,4 +221,4 @@ class SpotifyPlayerApi(spotifyAppRemote: SpotifyAppRemote?, result: MethodChanne spotifyRemoteAppNotSetError() } } -} \ No newline at end of file +} diff --git a/android/src/main/kotlin/de/minimalme/spotify_sdk/SpotifySdkPlugin.kt b/android/src/main/kotlin/de/minimalme/spotify_sdk/SpotifySdkPlugin.kt index 11f295ff..cd83605d 100644 --- a/android/src/main/kotlin/de/minimalme/spotify_sdk/SpotifySdkPlugin.kt +++ b/android/src/main/kotlin/de/minimalme/spotify_sdk/SpotifySdkPlugin.kt @@ -65,7 +65,10 @@ class SpotifySdkPlugin : MethodCallHandler, FlutterPlugin, ActivityAware, Plugin private val methodGetAccessToken = "getAccessToken" private val methodDisconnectFromSpotify = "disconnectFromSpotify" - //player api + // connectApi + private val methodSwitchToLocalDevice = "switchToLocalDevice" + + //playerApi private val methodGetCrossfadeState = "getCrossfadeState" private val methodGetPlayerState = "getPlayerState" private val methodPlay = "play" @@ -84,13 +87,13 @@ class SpotifySdkPlugin : MethodCallHandler, FlutterPlugin, ActivityAware, Plugin private val methodSetRepeatMode = "setRepeatMode" private val methodIsSpotifyAppActive = "isSpotifyAppActive" - //user api + //userApi private val methodAddToLibrary = "addToLibrary" private val methodRemoveFromLibrary = "removeFromLibrary" private val methodGetCapabilities = "getCapabilities" private val methodGetLibraryState = "getLibraryState" - //images api + //imagesApi private val methodGetImage = "getImage" private val paramClientId = "clientId" @@ -118,6 +121,7 @@ class SpotifySdkPlugin : MethodCallHandler, FlutterPlugin, ActivityAware, Plugin private var pendingOperation: PendingOperation? = null private var spotifyAppRemote: SpotifyAppRemote? = null private var spotifyPlayerApi: SpotifyPlayerApi? = null + private var spotifyConnectApi: SpotifyConnectApi? = null private var spotifyUserApi: SpotifyUserApi? = null private var spotifyImagesApi: SpotifyImagesApi? = null @@ -168,6 +172,7 @@ class SpotifySdkPlugin : MethodCallHandler, FlutterPlugin, ActivityAware, Plugin spotifyPlayerApi = SpotifyPlayerApi(spotifyAppRemote, result) spotifyUserApi = SpotifyUserApi(spotifyAppRemote, result) spotifyImagesApi = SpotifyImagesApi(spotifyAppRemote, result) + spotifyConnectApi = SpotifyConnectApi(spotifyAppRemote, result) } when (call.method) { @@ -175,7 +180,9 @@ class SpotifySdkPlugin : MethodCallHandler, FlutterPlugin, ActivityAware, Plugin methodConnectToSpotify -> connectToSpotify(call.argument(paramClientId), call.argument(paramRedirectUrl), result) methodGetAccessToken -> getAccessToken(call.argument(paramClientId), call.argument(paramRedirectUrl), call.argument(paramScope), result) methodDisconnectFromSpotify -> disconnectFromSpotify(result) - //player api calls + //connectApi calls + methodSwitchToLocalDevice -> spotifyConnectApi?.switchToLocalDevice() + //playerApi calls methodGetCrossfadeState -> spotifyPlayerApi?.getCrossfadeState() methodGetPlayerState -> spotifyPlayerApi?.getPlayerState() methodPlay -> spotifyPlayerApi?.play(call.argument(paramSpotifyUri)) @@ -193,12 +200,12 @@ class SpotifySdkPlugin : MethodCallHandler, FlutterPlugin, ActivityAware, Plugin methodToggleRepeat -> spotifyPlayerApi?.toggleRepeat() methodSetRepeatMode -> spotifyPlayerApi?.setRepeatMode(call.argument(paramRepeatMode)) methodIsSpotifyAppActive -> spotifyPlayerApi?.isSpotifyAppActive() - //user api calls + //userApi calls methodAddToLibrary -> spotifyUserApi?.addToUserLibrary(call.argument(paramSpotifyUri)) methodRemoveFromLibrary -> spotifyUserApi?.removeFromUserLibrary(call.argument(paramSpotifyUri)) methodGetCapabilities -> spotifyUserApi?.getCapabilities() methodGetLibraryState -> spotifyUserApi?.getLibraryState(call.argument(paramSpotifyUri)) - //image api calls + //imageApi calls methodGetImage -> spotifyImagesApi?.getImage(call.argument(paramImageUri), call.argument(paramImageDimension)) // method call is not implemented yet else -> result.notImplemented() diff --git a/example/.env b/example/.env index 20ea1353..6bea5464 100644 --- a/example/.env +++ b/example/.env @@ -1,4 +1,4 @@ ```sh -CLIENT_ID= -REDIRECT_URL= +CLIENT_ID=61b2332ab76d45918a33f91c3268ec1e +REDIRECT_URL=comspotifytestsdk://callback ``` diff --git a/example/lib/main.dart b/example/lib/main.dart index 78f8423b..672ac494 100644 --- a/example/lib/main.dart +++ b/example/lib/main.dart @@ -49,6 +49,9 @@ class HomeState extends State { @override Widget build(BuildContext context) { return MaterialApp( + theme: ThemeData( + useMaterial3: true, + ), home: StreamBuilder( stream: SpotifySdk.subscribeConnectionStatus(), builder: (context, snapshot) { @@ -79,6 +82,7 @@ class HomeState extends State { Widget _buildBottomBar(BuildContext context) { return BottomAppBar( + height: 125, child: Column( mainAxisSize: MainAxisSize.min, children: [ @@ -131,7 +135,7 @@ class HomeState extends State { return Stack( children: [ ListView( - padding: const EdgeInsets.all(8), + padding: const EdgeInsets.all(16), children: [ Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, @@ -147,12 +151,9 @@ class HomeState extends State { ], ), const Divider(), - const Text( + Text( 'Player State', - style: TextStyle( - fontSize: 16, - fontWeight: FontWeight.bold, - ), + style: Theme.of(context).textTheme.headlineSmall, ), _connected ? _buildPlayerStateWidget() @@ -160,12 +161,9 @@ class HomeState extends State { child: Text('Not connected'), ), const Divider(), - const Text( + Text( 'Player Context', - style: TextStyle( - fontSize: 16, - fontWeight: FontWeight.bold, - ), + style: Theme.of(context).textTheme.headlineSmall, ), _connected ? _buildPlayerContextWidget() @@ -173,43 +171,74 @@ class HomeState extends State { child: Text('Not connected'), ), const Divider(), - const Text( + Text( 'Player Api', - style: TextStyle( - fontSize: 16, - fontWeight: FontWeight.bold, - ), + style: Theme.of(context).textTheme.headlineSmall, ), - Row( + Column( + mainAxisAlignment: MainAxisAlignment.start, + crossAxisAlignment: CrossAxisAlignment.start, children: [ - TextButton( + ElevatedButton( onPressed: seekTo, child: const Text('seek to 20000ms'), ), - TextButton( + ElevatedButton( onPressed: seekToRelative, child: const Text('seek to relative 20000ms'), ), ], ), const Divider(), - const Text( + Text( + 'Connect Api', + style: Theme.of(context).textTheme.headlineSmall, + ), + Column( + mainAxisAlignment: MainAxisAlignment.start, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + ElevatedButton( + onPressed: switchToLocalDevice, + child: const Text('switch to local device'), + ), + ], + ), + const Divider(), + Text( 'Crossfade State', - style: TextStyle( - fontSize: 16, - fontWeight: FontWeight.bold, - ), + style: Theme.of(context).textTheme.headlineSmall, ), - ElevatedButton( - onPressed: getCrossfadeState, - child: const Text( - 'get crossfade state', - ), + Column( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Text( + 'Status', + style: Theme.of(context).textTheme.titleSmall, + ), + Text( + crossfadeState?.isEnabled == true ? 'Enabled' : 'Disabled'), + Padding( + padding: const EdgeInsets.only(top: 8.0), + child: Text( + 'Duration', + style: Theme.of(context).textTheme.titleSmall, + ), + ), + Text(crossfadeState?.duration.toString() ?? 'Unknown'), + Row( + children: [ + ElevatedButton( + onPressed: getCrossfadeState, + child: const Text( + 'get crossfade state', + ), + ), + ], + ), + ], ), - // ignore: prefer_single_quotes - Text("Is enabled: ${crossfadeState?.isEnabled}"), - // ignore: prefer_single_quotes - Text("Duration: ${crossfadeState?.duration}"), ], ), _loading @@ -236,8 +265,8 @@ class HomeState extends State { } return Column( - mainAxisAlignment: MainAxisAlignment.start, - crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.center, children: [ Row( mainAxisAlignment: MainAxisAlignment.spaceEvenly, @@ -265,8 +294,61 @@ class HomeState extends State { ), ], ), + track.isPodcast + ? Row( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + children: [ + TextButton( + child: const SizedBox( + width: 50, + child: Text("x0.5"), + ), + onPressed: () => setPlaybackSpeed( + PodcastPlaybackSpeed.playbackSpeed_50), + ), + TextButton( + child: const SizedBox( + width: 50, + child: Text("x1"), + ), + onPressed: () => setPlaybackSpeed( + PodcastPlaybackSpeed.playbackSpeed_100), + ), + TextButton( + child: const SizedBox( + width: 50, + child: Text("x1.5"), + ), + onPressed: () => setPlaybackSpeed( + PodcastPlaybackSpeed.playbackSpeed_150), + ), + TextButton( + child: const SizedBox( + width: 50, + child: Text("x3.0"), + ), + onPressed: () => setPlaybackSpeed( + PodcastPlaybackSpeed.playbackSpeed_300), + ), + ], + ) + : Container(), + Text( + 'Track', + style: Theme.of(context).textTheme.titleSmall, + ), Text( - '${track.name} by ${track.artist.name} from the album ${track.album.name}'), + '${track.name} by ${track.artist.name} from the album ${track.album.name}', + maxLines: 2, + ), + Padding( + padding: const EdgeInsets.only(top: 8.0), + child: Text( + 'Playback', + textAlign: TextAlign.center, + style: Theme.of(context).textTheme.titleSmall, + ), + ), Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ @@ -282,21 +364,21 @@ class HomeState extends State { Text('Shuffling: ${playerState.playbackOptions.isShuffling}'), ], ), - Text('RepeatMode: ${playerState.playbackOptions.repeatMode}'), - Text('Image URI: ${track.imageUri.raw}'), - Text('Is episode? ${track.isEpisode}'), - Text('Is podcast? ${track.isPodcast}'), - _connected - ? spotifyImageWidget(track.imageUri) - : const Text('Connect to see an image...'), - Column( - crossAxisAlignment: CrossAxisAlignment.start, + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text('Is episode: ${track.isEpisode}'), + Text('Is podcast: ${track.isPodcast}'), + ], + ), + Row( + children: [ + Text('RepeatMode: ${playerState.playbackOptions.repeatMode}'), + ], + ), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ - const Divider(), - const Text( - 'Set Shuffle and Repeat', - style: TextStyle(fontSize: 16), - ), Row( children: [ const Text( @@ -325,7 +407,7 @@ class HomeState extends State { ), Row( children: [ - const Text('Set shuffle: '), + const Text('Switch shuffle: '), Switch.adaptive( value: playerState.playbackOptions.isShuffling, onChanged: (bool shuffle) => setShuffle( @@ -336,6 +418,16 @@ class HomeState extends State { ), ], ), + _connected + ? Padding( + padding: const EdgeInsets.only(top: 16.0, bottom: 8.0), + child: spotifyImageWidget(track.imageUri), + ) + : const Text('Connect to see an image...'), + Text( + track.imageUri.raw, + style: Theme.of(context).textTheme.labelSmall, + ), ], ); }, @@ -355,13 +447,41 @@ class HomeState extends State { } return Column( - mainAxisAlignment: MainAxisAlignment.start, - crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.center, children: [ - Text('Title: ${playerContext.title}'), - Text('Subtitle: ${playerContext.subtitle}'), - Text('Type: ${playerContext.type}'), - Text('Uri: ${playerContext.uri}'), + Text( + 'Title', + style: Theme.of(context).textTheme.titleSmall, + ), + Text(playerContext.title), + Padding( + padding: const EdgeInsets.only(top: 8.0), + child: Text( + 'Subtitle', + style: Theme.of(context).textTheme.titleSmall, + ), + ), + Text(playerContext.subtitle), + Padding( + padding: const EdgeInsets.only(top: 8.0), + child: Text( + 'Type', + style: Theme.of(context).textTheme.titleSmall, + ), + ), + Text(playerContext.type), + Padding( + padding: const EdgeInsets.only(top: 8.0), + child: Text( + 'Uri', + style: Theme.of(context).textTheme.titleSmall, + ), + ), + Text( + playerContext.uri, + style: Theme.of(context).textTheme.labelSmall, + ), ], ); }, @@ -542,6 +662,18 @@ class HomeState extends State { } } + Future setPlaybackSpeed( + PodcastPlaybackSpeed podcastPlaybackSpeed) async { + try { + await SpotifySdk.setPodcastPlaybackSpeed( + podcastPlaybackSpeed: podcastPlaybackSpeed); + } on PlatformException catch (e) { + setStatus(e.code, message: e.message); + } on MissingPluginException { + setStatus('not implemented'); + } + } + Future play() async { try { await SpotifySdk.play(spotifyUri: 'spotify:track:58kNJana4w5BIjlZE2wq5m'); @@ -612,6 +744,16 @@ class HomeState extends State { } } + Future switchToLocalDevice() async { + try { + await SpotifySdk.switchToLocalDevice(); + } on PlatformException catch (e) { + setStatus(e.code, message: e.message); + } on MissingPluginException { + setStatus('not implemented'); + } + } + Future addToLibrary() async { try { await SpotifySdk.addToLibrary( diff --git a/lib/enums/podcast_playback_speed.dart b/lib/enums/podcast_playback_speed.dart new file mode 100644 index 00000000..01f27bc1 --- /dev/null +++ b/lib/enums/podcast_playback_speed.dart @@ -0,0 +1,23 @@ +/// Holds the values from the spotify api for supported Podcast playback speeds +enum PodcastPlaybackSpeed { + /// 0.5 x playback speed + playbackSpeed_50, + + /// 0.8 x playback speed + playbackSpeed_80, + + /// 1 x playback speed + playbackSpeed_100, + + /// 1.2 x playback speed + playbackSpeed_120, + + /// 1.5 x playback speed + playbackSpeed_150, + + /// 2 x playback speed + playbackSpeed_200, + + /// 3 x playback speed + playbackSpeed_300, +} diff --git a/lib/extensions/podcast_playback_speed_extension.dart b/lib/extensions/podcast_playback_speed_extension.dart new file mode 100644 index 00000000..346cca9e --- /dev/null +++ b/lib/extensions/podcast_playback_speed_extension.dart @@ -0,0 +1,21 @@ +import 'package:spotify_sdk/enums/podcast_playback_speed.dart'; + +///Extension for formatting the PodcastPlaybackSpeed enum to value +///@nodoc +extension PodcastPlaybackSpeedExtension on PodcastPlaybackSpeed { + ///maps the value to the specified enum + ///@nodoc + static const values = { + PodcastPlaybackSpeed.playbackSpeed_50: 50, + PodcastPlaybackSpeed.playbackSpeed_80: 80, + PodcastPlaybackSpeed.playbackSpeed_100: 100, + PodcastPlaybackSpeed.playbackSpeed_120: 120, + PodcastPlaybackSpeed.playbackSpeed_150: 150, + PodcastPlaybackSpeed.playbackSpeed_200: 200, + PodcastPlaybackSpeed.playbackSpeed_300: 300, + }; + + /// returns the value + ///@nodoc + int get value => values[this]!; +} diff --git a/lib/platform_channels.dart b/lib/platform_channels.dart index 52a5d649..6b8c77a9 100644 --- a/lib/platform_channels.dart +++ b/lib/platform_channels.dart @@ -54,6 +54,9 @@ class MethodNames { /// method name for [resume] static const String resume = 'resume'; + /// method name for [podcastPlaybackSpeed] + static const String setPodcastPlaybackSpeed = 'setPodcastPlaybackSpeed'; + /// method name for [skipToIndex] static const String skipToIndex = 'skipToIndex'; @@ -104,6 +107,9 @@ class MethodNames { /// method name for [setRepeatMode] static const String setRepeatMode = 'setRepeatMode'; + + /// method name for [switchToLocalDevice] + static const String switchToLocalDevice = "switchToLocalDevice"; } /// Holds the names for all parameters that are used in the package @@ -147,6 +153,9 @@ class ParamNames { /// param name for [repeatMode] static const String repeatMode = 'repeatMode'; + /// param name for [podcastPlaybackSpeed] + static const String podcastPlaybackSpeed = 'podcastPlaybackSpeed'; + /// param name for [uri] static const String uri = 'uri'; diff --git a/lib/spotify_sdk.dart b/lib/spotify_sdk.dart index d3add386..82364a33 100644 --- a/lib/spotify_sdk.dart +++ b/lib/spotify_sdk.dart @@ -5,8 +5,10 @@ import 'package:flutter/services.dart'; import 'package:logger/logger.dart'; import 'enums/image_dimension_enum.dart'; +import 'enums/podcast_playback_speed.dart'; import 'enums/repeat_mode_enum.dart'; import 'extensions/image_dimension_extension.dart'; +import 'extensions/podcast_playback_speed_extension.dart'; import 'models/capabilities.dart'; import 'models/connection_status.dart'; import 'models/crossfade_state.dart'; @@ -18,8 +20,10 @@ import 'models/user_status.dart'; import 'platform_channels.dart'; export 'package:spotify_sdk/enums/image_dimension_enum.dart'; +export 'package:spotify_sdk/enums/podcast_playback_speed.dart'; export 'package:spotify_sdk/enums/repeat_mode_enum.dart'; export 'package:spotify_sdk/extensions/image_dimension_extension.dart'; +export 'package:spotify_sdk/extensions/podcast_playback_speed_extension.dart'; /// /// [SpotifySdk] holds the functionality to connect via spotify remote or @@ -283,6 +287,25 @@ class SpotifySdk { } } + /// Sets the playbackSpeed of the Podcast + /// + /// The podcast playback speed can be controlled via [podcastPlaybackSpeed]. + /// This can only be set if the podcast is played on the local device + /// the same device the app is running on. + /// Throws a [PlatformException] if resuming failed + /// Throws a [MissingPluginException] if the method is not implemented on + /// the native platforms. + static Future setPodcastPlaybackSpeed( + {required PodcastPlaybackSpeed podcastPlaybackSpeed}) async { + try { + await _channel.invokeMethod(MethodNames.setPodcastPlaybackSpeed, + {ParamNames.podcastPlaybackSpeed: podcastPlaybackSpeed.value}); + } on Exception catch (e) { + _logException(MethodNames.resume, e); + rethrow; + } + } + /// Skips to the next track /// /// Throws a [PlatformException] if skipping failed @@ -427,6 +450,20 @@ class SpotifySdk { } } + /// Switch to local device for playback + /// + /// Throws a [PlatformException] if switching to local device failed + /// Throws a [MissingPluginException] if the method is not implemented on + /// the native platforms. + static Future switchToLocalDevice() async { + try { + await _channel.invokeMethod(MethodNames.switchToLocalDevice); + } on Exception catch (e) { + _logException(MethodNames.switchToLocalDevice, e); + rethrow; + } + } + /// Toggles shuffle /// /// Throws a [PlatformException] if toggling shuffle failed diff --git a/pubspec.yaml b/pubspec.yaml index c8e4490b..991e13b1 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,6 +1,6 @@ name: spotify_sdk description: A flutter plugin that let's you communicate with the spotify sdk and auth lib -version: 3.0.0-dev.1 +version: 3.0.0-dev.2 homepage: https://github.com/brim-borium/spotify_sdk issue_tracker: https://github.com/brim-borium/spotify_sdk/issues