107 changed files with 6214 additions and 210 deletions
@ -1,4 +1,4 @@ |
|||
class NWApi { |
|||
// static const baseApi = "https://www.deapsound.com"; |
|||
static const baseApi = "https://www.deapsound.com"; |
|||
static const baseApi = "https://api.deapsound.com"; |
|||
} |
|||
|
|||
@ -0,0 +1,62 @@ |
|||
--- |
|||
name: Bug report |
|||
about: Create a report to help us improve |
|||
title: '' |
|||
labels: 'bug' |
|||
assignees: '' |
|||
|
|||
--- |
|||
|
|||
**!! Attention !!** |
|||
FILL IN ALL SECTIONS OF THIS TEMPLATE OR YOUR ISSUE WILL BE CLOSED WITHOUT INVESTIGATION |
|||
This is a flutter package that wraps the native [iOS](https://github.com/spotify/ios-sdk) and [Android](https://github.com/spotify/android-sdk) Spotify "remote" SDKs as well as the [Spotify Web Playback SDK](https://developer.spotify.com/documentation/web-playback-sdk/) for web. Since it wraps the native SDKs it has the same features and limitations. Please use the search functionality in the native SDK repos to find corresponding issues before opening an issue here. If your issue is deriving from the native SDKs you should open an issue there and not in this repo. |
|||
|
|||
**Is this a how to question?** |
|||
Yes/No |
|||
If Yes, please use Stack Overflow instead. |
|||
|
|||
**Is this a native Spotify SDK issue?** |
|||
Yes/No/I don't know |
|||
If you answered "Yes" then your issue belongs to the native SDK repo and not here. |
|||
If you answered "I don't know" then you should visit the corresponding native SDK repo and make sure that this is not a native SDK issue. |
|||
|
|||
**Have you searched for existing issues?** |
|||
Yes/No |
|||
If you answered "No" you should that first before opening a new issue. |
|||
|
|||
**Are you using a Free or Premium Spotify account?** |
|||
Free/Premium |
|||
|
|||
**Are you aware of the limitations of Free Spotify accounts?** |
|||
Yes/No |
|||
If you answered "No" and you are using a Free account then you make sure that this is not behavior that intentionally limited by the native SDK. |
|||
|
|||
**Expected behavior** |
|||
A clear and concise description of what you expected to happen. |
|||
|
|||
**Describe the bug** |
|||
A clear and concise description of what happened instead. |
|||
|
|||
**Steps to Reproduce** |
|||
Steps to reproduce the behavior: |
|||
1. Go to '...' |
|||
2. Click on '....' |
|||
3. Scroll down to '....' |
|||
4. See error |
|||
|
|||
**Screenshots** |
|||
If applicable, add screenshots to help explain your problem. |
|||
|
|||
**Spotify_sdk Flutter package version** |
|||
Check you `pubspeck.lock` file to verify the exact version [e.g 2.1, commit ID] |
|||
|
|||
**Target Platform, Version & Device** |
|||
- OS & Version: [e.g. iOS 8.1, Android 9, Web] |
|||
- Browser & Version (if applicable) [e.g. Safari 15, Chrome 22] |
|||
- Device: [e.g. iPhone 6, Pixel 5, PC] |
|||
|
|||
**Development OS** |
|||
- OS: [e.g. Mac OS, Windows, Linux] |
|||
|
|||
**Additional context** |
|||
Add any other information about the problem here. |
|||
@ -0,0 +1 @@ |
|||
blank_issues_enabled: false |
|||
@ -0,0 +1,20 @@ |
|||
--- |
|||
name: Feature request |
|||
about: Suggest an idea for this project |
|||
title: '' |
|||
labels: 'enhancement' |
|||
assignees: '' |
|||
|
|||
--- |
|||
|
|||
**Is your feature request related to a problem? Please describe.** |
|||
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] |
|||
|
|||
**Describe the solution you'd like** |
|||
A clear and concise description of what you want to happen. |
|||
|
|||
**Describe alternatives you've considered** |
|||
A clear and concise description of any alternative solutions or features you've considered. |
|||
|
|||
**Additional context** |
|||
Add any other context or screenshots about the feature request here. |
|||
@ -0,0 +1,41 @@ |
|||
name: Publish Flutter package to pub.dev |
|||
description: Publish your Flutter package to pub.dev |
|||
|
|||
inputs: |
|||
working-directory: |
|||
description: directory with-in the repository where the package is located (if not in the repository root) |
|||
required: false |
|||
|
|||
runs: |
|||
using: "composite" |
|||
steps: |
|||
- name: 📚 Git Checkout |
|||
uses: actions/checkout@v4 |
|||
- name: 🐦 Setup Flutter |
|||
uses: subosito/flutter-action@v2 |
|||
|
|||
- name: 🪪 Get Id Token |
|||
uses: actions/github-script@v6 |
|||
with: |
|||
script: | |
|||
let pub_token = await core.getIDToken('https://pub.dev') |
|||
core.exportVariable('PUB_TOKEN', pub_token) |
|||
|
|||
- name: 📢 Authenticate |
|||
shell: ${{ inputs.shell }} |
|||
run: flutter pub pub token add https://pub.dev --env-var PUB_TOKEN |
|||
|
|||
- name: 📦 Install dependencies |
|||
shell: ${{ inputs.shell }} |
|||
run: flutter pub get |
|||
working-directory: ${{ inputs.working-directory }} |
|||
|
|||
- name: 🌵 Dry Run |
|||
shell: ${{ inputs.shell }} |
|||
run: flutter pub publish --dry-run |
|||
working-directory: ${{ inputs.working-directory }} |
|||
|
|||
- name: 📢 Publish |
|||
shell: ${{ inputs.shell }} |
|||
run: flutter pub publish -f |
|||
working-directory: ${{ inputs.working-directory }} |
|||
@ -0,0 +1,42 @@ |
|||
# To get started with Dependabot version updates, you'll need to specify which |
|||
# package ecosystems to update and where the package manifests are located. |
|||
# Please see the documentation for all configuration options: |
|||
# https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates |
|||
|
|||
version: 2 |
|||
updates: |
|||
- package-ecosystem: "pub" # See documentation for possible values |
|||
directory: "/" # Location of package manifests |
|||
schedule: |
|||
interval: "weekly" |
|||
commit-message: |
|||
prefix: "chore" |
|||
include: "scope" |
|||
groups: |
|||
spotify_sdk: |
|||
patterns: |
|||
- "*" |
|||
|
|||
- package-ecosystem: "pub" |
|||
directory: "/example" # Location of package manifests in the example directory |
|||
schedule: |
|||
interval: "weekly" |
|||
commit-message: |
|||
prefix: "chore" |
|||
include: "scope" |
|||
groups: |
|||
spotify_sdk_example: |
|||
patterns: |
|||
- "*" |
|||
|
|||
- package-ecosystem: "github-actions" |
|||
directory: "/" |
|||
schedule: |
|||
interval: "weekly" |
|||
commit-message: |
|||
prefix: "chore" |
|||
include: "scope" |
|||
groups: |
|||
github_actions: |
|||
patterns: |
|||
- "*" |
|||
@ -0,0 +1,16 @@ |
|||
#!/bin/bash |
|||
# Runs `pana . --no-warning` and verifies that the package score |
|||
# is greater or equal to the desired score. By default the desired score is |
|||
# a perfect score but it can be overridden by passing the desired score as an argument. |
|||
# |
|||
# Ensure the package has a score of at least a 100 |
|||
# `./verify_pub_score.sh 100` |
|||
# |
|||
# Ensure the package has a perfect score |
|||
# `./verify_pub_score.sh` |
|||
|
|||
PANA=$(pana . --no-warning); PANA_SCORE=$(echo $PANA | sed -n "s/.*Points: \([0-9]*\)\/\([0-9]*\)./\1\/\2/p") |
|||
echo "score: $PANA_SCORE" |
|||
IFS='/'; read -a SCORE_ARR <<< "$PANA_SCORE"; SCORE=SCORE_ARR[0]; TOTAL=SCORE_ARR[1] |
|||
if [ -z "$1" ]; then MINIMUM_SCORE=TOTAL; else MINIMUM_SCORE=$1; fi |
|||
if (( $SCORE < $MINIMUM_SCORE )); then echo "minimum score $MINIMUM_SCORE was not met!"; exit 1; fi |
|||
@ -0,0 +1,38 @@ |
|||
name: spotify_sdk |
|||
on: pull_request |
|||
|
|||
jobs: |
|||
build: |
|||
runs-on: ubuntu-latest |
|||
steps: |
|||
- uses: actions/checkout@v4 |
|||
- uses: subosito/flutter-action@v2 |
|||
with: |
|||
channel: 'stable' |
|||
- name: Install Dependencies |
|||
run: flutter packages get |
|||
- name: Format |
|||
run: dart format --set-exit-if-changed lib test example |
|||
- name: Analyze |
|||
run: flutter analyze lib test example |
|||
- name: Publish dry run |
|||
run: flutter pub publish --dry-run |
|||
- name: Build web version |
|||
run: | |
|||
cd example |
|||
flutter build web |
|||
|
|||
pana: |
|||
runs-on: ubuntu-latest |
|||
steps: |
|||
- uses: actions/checkout@v4 |
|||
- uses: subosito/flutter-action@v2 |
|||
|
|||
- name: Install Dependencies |
|||
run: | |
|||
flutter packages get |
|||
flutter pub global activate pana |
|||
sudo apt-get install webp |
|||
|
|||
- name: Verify Pub Score |
|||
run: ./.github/scripts/verify_pub_score.sh |
|||
@ -0,0 +1,34 @@ |
|||
name: Publish to pub.dev |
|||
on: |
|||
push: |
|||
tags: |
|||
- "[0-9]+.[0-9]+.[0-9]+*" # tag-pattern on pub.dev: '' |
|||
|
|||
jobs: |
|||
pana: |
|||
runs-on: ubuntu-latest |
|||
steps: |
|||
- uses: actions/checkout@v4 |
|||
- uses: subosito/flutter-action@v2 |
|||
|
|||
- name: Install Dependencies |
|||
run: | |
|||
flutter packages get |
|||
flutter pub global activate pana |
|||
sudo apt-get install webp |
|||
|
|||
- name: Verify Pub Score |
|||
run: ./.github/scripts/verify_pub_score.sh |
|||
|
|||
publish: |
|||
needs: pana |
|||
environment: 'pub.dev' |
|||
permissions: |
|||
id-token: write # Required for authentication using OIDC |
|||
runs-on: ubuntu-latest |
|||
steps: |
|||
- name: 📚 Git Checkout |
|||
uses: actions/checkout@v4 |
|||
with: |
|||
submodules: recursive |
|||
- uses: ./.github/actions/publish_flutter_package |
|||
@ -0,0 +1,50 @@ |
|||
.DS_Store |
|||
.atom/ |
|||
.idea/ |
|||
.vscode/ |
|||
|
|||
.packages |
|||
.pub/ |
|||
.dart_tool/ |
|||
pubspec.lock |
|||
flutter_export_environment.sh |
|||
|
|||
examples/all_plugins/pubspec.yaml |
|||
|
|||
Podfile |
|||
Podfile.lock |
|||
Pods/ |
|||
.symlinks/ |
|||
**/Flutter/App.framework/ |
|||
**/Flutter/Flutter.framework/ |
|||
**/Flutter/Generated.xcconfig |
|||
**/Flutter/flutter_assets/ |
|||
ServiceDefinitions.json |
|||
xcuserdata/ |
|||
*.xcworkspace |
|||
**/DerivedData/ |
|||
|
|||
local.properties |
|||
keystore.properties |
|||
.gradle/ |
|||
gradlew |
|||
gradlew.bat |
|||
gradle-wrapper.jar |
|||
.flutter-plugins-dependencies |
|||
*.iml |
|||
|
|||
GeneratedPluginRegistrant.h |
|||
GeneratedPluginRegistrant.m |
|||
GeneratedPluginRegistrant.java |
|||
build/ |
|||
.flutter-plugins |
|||
|
|||
.project |
|||
.classpath |
|||
.settings |
|||
*.sqlite |
|||
/.vs/spotify_sdk/v16/.suo |
|||
|
|||
SpotifyiOS.framework |
|||
**/doc/api/ |
|||
/ios/ios-sdk/ |
|||
@ -0,0 +1,10 @@ |
|||
# This file tracks properties of this Flutter project. |
|||
# Used by Flutter tool to assess capabilities and perform upgrades etc. |
|||
# |
|||
# This file should be version controlled and should not be manually edited. |
|||
|
|||
version: |
|||
revision: 856a90e67c9284124d44d2be6c785bacd3a1c772 |
|||
channel: beta |
|||
|
|||
project_type: plugin |
|||
@ -0,0 +1,184 @@ |
|||
## 3.0.2 |
|||
* chore: update deprications (#224) |
|||
|
|||
## 3.0.1 |
|||
* **BREAKING**: `isSpotifyAppActive` is removed from iOS and Android (#222) and therefore removed from the sdk |
|||
* chore: Bump native iOS SDK to 3.0.0 (#222) |
|||
|
|||
## 3.0.0 |
|||
* All changes from dev builds |
|||
* chore: Bump native iOS SDK to 1.2.4 (#208) |
|||
* chore: Update Spotify iOS SDK to v2.1.7 (#216) |
|||
* chore: Update android versions (#219) |
|||
|
|||
## 3.0.0-dev.3 |
|||
* Fix: prevent multiple iOS initializations (#203) |
|||
* Chore: Update libraries (#202) |
|||
* Feat: Automatic setup of the android integration of the spotify_sdk (#204) |
|||
|
|||
## 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) |
|||
```groovy |
|||
defaultConfig { |
|||
manifestPlaceholders = [redirectSchemeName: "spotify-sdk", redirectHostName: "auth"] |
|||
... |
|||
} |
|||
``` |
|||
* Update android target sdk to 34 |
|||
|
|||
## 2.3.1 |
|||
* Fix: null album when getting advertisement on android (#179) |
|||
* Fix: queue endpoint for web (#167) |
|||
* Update license to apache-2.0 |
|||
|
|||
## 2.3.0 |
|||
* iOS, Android and Web |
|||
* getAuthenticationToken is deprecated in favor of getAccessToken |
|||
* Android: |
|||
* `spotify-auth` SDK is now retrieved via Maven Central instead of being sourced from an AAR file |
|||
* Steps to remove the `spotify-auth` SDK: |
|||
* android/settings.gradle -> remove `':spotify-auth'` |
|||
* android/spotify-auth/build.gradle -> remove file |
|||
* android/spotify-auth/spotify-auth-release-x.x.x.aar -> remove file |
|||
|
|||
## 2.2.0 |
|||
* iOS and Android |
|||
* adds SkipToIndex and getCapabilities |
|||
* adds isSpotifyAppActive |
|||
* adds getLibraryState on iOS |
|||
* Switch to native Spotify iOS XCFramework |
|||
* fixes android json mapping error on release builds |
|||
* fixes skipPrevious on iOS |
|||
* web |
|||
* adds use accessToken which implies reusing _spotifyToken from getAuthenticationToken |
|||
* adds optional tokenSwapURL and tokenRefreshURL for Authorization Code (without PKCE) |
|||
* adds support for track relinking |
|||
* fixes browser autoplay error |
|||
* updates libraries |
|||
* updates documentation |
|||
|
|||
## 2.1.0 |
|||
* BREAKING: setShuffle now does not expect a named argument |
|||
* fixes accessToken being ignored in connectToSpotify() |
|||
* exposes spotifyURI on connectToSpotify() method |
|||
* fixes the web implementation not using the authentication scopes supplied by the user |
|||
* fix a crash when calling subscribeToConnectionStatus |
|||
* fix a bug where the webplayer would not dispose |
|||
* add null safety to example app |
|||
|
|||
## 2.0.0 |
|||
* BREAKING: opt into null safety |
|||
* upgrade Dart SDK constraints to >=2.12.0-0 <3.0.0 |
|||
|
|||
## 1.0.2 |
|||
* fixes image dimension exception on getImage |
|||
|
|||
## 1.0.1 |
|||
* improves error reporting on iOS |
|||
* supports adding additional scopes for iOS |
|||
* offers option to start radio while connecting to Spotify |
|||
* updates packages |
|||
|
|||
## 1.0.0 |
|||
* __adds support for iOS__ 🎉 (thanks [fotiDim](https://github.com/fotiDim)) |
|||
* adapts to breaking changes in the spotify android sdk (thanks [itsMatoosh](https://github.com/itsMatoosh)) |
|||
* implements PKCE auth flow for the web implementation (thanks [itsMatoosh](https://github.com/itsMatoosh)) |
|||
* adds the `setShuffle()` and `setRepeatMode()` APIs for Android (thanks [Joran-Dob](https://github.com/Joran-Dob)) |
|||
* renames logout to disconnect |
|||
* adds `accessToken` as an optional parameter to `connectToSpotifyRemote()` (only supported on iOS) |
|||
* some minor bug fixing |
|||
|
|||
## 0.5.0 |
|||
* adds support for web (thanks [itsMatoosh](https://github.com/itsMatoosh)) |
|||
* adds custom scopes for the web api (thanks [arnav-sh](https://github.com/arnav-sh)) |
|||
* adds logout functionality for android |
|||
* moved from [pedantic](https://pub.dev/packages/pedantic) to [lint](https://pub.dev/packages/lint) for static analyses |
|||
* some minor bug fixing |
|||
|
|||
## 0.3.4 |
|||
|
|||
* adds handling of unexpected disconnects from Spotify via subscribeConnectionStatus()-Stream(thanks [itsMatoosh](https://github.com/itsMatoosh)) |
|||
* adds usage of .env file for the example project |
|||
* fixes some minor error message issues |
|||
|
|||
## 0.3.3 |
|||
|
|||
* adds getImage to get an Image from any spotifyURI (thanks [eddwhite](https://github.com/eddwhite)) |
|||
* fixes some minor issues |
|||
* raised dart-sdk version to 2.7.0 |
|||
|
|||
## 0.3.2 |
|||
|
|||
* fixes compatibility with spotify-auth dependency above version 1.2.0 (thanks [itsMatoosh](https://github.com/itsMatoosh)) |
|||
* spotify introduced some breaking changes: Rename classes from Authentication<code>ClassName</code> to Authorization<code>ClassName</code> |
|||
|
|||
## 0.3.1 |
|||
|
|||
* fixes wrong links and incorrect docs |
|||
|
|||
## 0.3.0 |
|||
|
|||
* android user api implementation finished |
|||
* remove from library |
|||
* subscribe to user status |
|||
* subscribe to capabilities |
|||
* get librarystate |
|||
* updated package references |
|||
|
|||
## 0.2.0 |
|||
|
|||
* android player api implementation subscriptions finished |
|||
* subscribe to playerContext and playerState now possible |
|||
* added more instructions for android |
|||
* code refactoring |
|||
* extended the example |
|||
* extended the documentation |
|||
|
|||
## 0.1.0 |
|||
|
|||
* android player api implementation finished |
|||
* error handling finished |
|||
* setup instructions for android finished |
|||
* naming for modules finished |
|||
|
|||
## 0.0.4 |
|||
|
|||
* added instructions for android |
|||
* fixed naming for modules |
|||
|
|||
## 0.0.3 |
|||
|
|||
* added the following implementations for android: |
|||
* getCrossfadeState |
|||
* getPlayerState |
|||
* general refactoring of the native android implementation |
|||
* adds documentation to all methods |
|||
* adds json_annotation: ^3.0.0, build_runner: ^1.0.0, json_serializable: ^3.2.0 to make use of some json serializing functionality for the crossfadeState and PlayerState |
|||
|
|||
## 0.0.2 |
|||
|
|||
* added the following implementations for android: |
|||
* resume |
|||
* skip next |
|||
* skip previous |
|||
* seek to |
|||
* seek to relative |
|||
* splitted remote authorization and token retrieval in two seperate functions |
|||
* implemented the example project |
|||
* added logger package for prettier logs |
|||
|
|||
## 0.0.1 |
|||
|
|||
* Added latests spotify-app-remote (v7.0.0) and spotify-auth (v1.1.0) from <https://github.com/spotify/android-sdk/releases> |
|||
* finished android native implementation for |
|||
* authorization with token response |
|||
* play |
|||
* pause |
|||
* queue |
|||
* toggleShuffle |
|||
* toggleRepeat |
|||
* addToLibrary |
|||
@ -0,0 +1,76 @@ |
|||
# Contributor Covenant Code of Conduct |
|||
|
|||
## Our Pledge |
|||
|
|||
In the interest of fostering an open and welcoming environment, we as |
|||
contributors and maintainers pledge to making participation in our project and |
|||
our community a harassment-free experience for everyone, regardless of age, body |
|||
size, disability, ethnicity, sex characteristics, gender identity and expression, |
|||
level of experience, education, socio-economic status, nationality, personal |
|||
appearance, race, religion, or sexual identity and orientation. |
|||
|
|||
## Our Standards |
|||
|
|||
Examples of behavior that contributes to creating a positive environment |
|||
include: |
|||
|
|||
* Using welcoming and inclusive language |
|||
* Being respectful of differing viewpoints and experiences |
|||
* Gracefully accepting constructive criticism |
|||
* Focusing on what is best for the community |
|||
* Showing empathy towards other community members |
|||
|
|||
Examples of unacceptable behavior by participants include: |
|||
|
|||
* The use of sexualized language or imagery and unwelcome sexual attention or |
|||
advances |
|||
* Trolling, insulting/derogatory comments, and personal or political attacks |
|||
* Public or private harassment |
|||
* Publishing others' private information, such as a physical or electronic |
|||
address, without explicit permission |
|||
* Other conduct which could reasonably be considered inappropriate in a |
|||
professional setting |
|||
|
|||
## Our Responsibilities |
|||
|
|||
Project maintainers are responsible for clarifying the standards of acceptable |
|||
behavior and are expected to take appropriate and fair corrective action in |
|||
response to any instances of unacceptable behavior. |
|||
|
|||
Project maintainers have the right and responsibility to remove, edit, or |
|||
reject comments, commits, code, wiki edits, issues, and other contributions |
|||
that are not aligned to this Code of Conduct, or to ban temporarily or |
|||
permanently any contributor for other behaviors that they deem inappropriate, |
|||
threatening, offensive, or harmful. |
|||
|
|||
## Scope |
|||
|
|||
This Code of Conduct applies both within project spaces and in public spaces |
|||
when an individual is representing the project or its community. Examples of |
|||
representing a project or community include using an official project e-mail |
|||
address, posting via an official social media account, or acting as an appointed |
|||
representative at an online or offline event. Representation of a project may be |
|||
further defined and clarified by project maintainers. |
|||
|
|||
## Enforcement |
|||
|
|||
Instances of abusive, harassing, or otherwise unacceptable behavior may be |
|||
reported by contacting the project team at tobi@minimalme.de. All |
|||
complaints will be reviewed and investigated and will result in a response that |
|||
is deemed necessary and appropriate to the circumstances. The project team is |
|||
obligated to maintain confidentiality with regard to the reporter of an incident. |
|||
Further details of specific enforcement policies may be posted separately. |
|||
|
|||
Project maintainers who do not follow or enforce the Code of Conduct in good |
|||
faith may face temporary or permanent repercussions as determined by other |
|||
members of the project's leadership. |
|||
|
|||
## Attribution |
|||
|
|||
This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, |
|||
available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html |
|||
|
|||
[homepage]: https://www.contributor-covenant.org |
|||
|
|||
For answers to common questions about this code of conduct, see |
|||
https://www.contributor-covenant.org/faq |
|||
@ -0,0 +1,48 @@ |
|||
# How you can contribute to spotify_sdk |
|||
|
|||
Hi nice to see you here. 🙌🎉 |
|||
|
|||
Thank you for taking the time to contribute to this package ! 👍 |
|||
|
|||
This document should be a set of guidelines that can help you to create a meaningful |
|||
pull request or issue. Don't see this as any kind of rules, this is a living document, |
|||
so if you have an idea how to optimize this propose changes in a pull request. |
|||
|
|||
## How do I propose a change? |
|||
|
|||
Changes to the public API should be done via an issue. So we can discuss the proposed |
|||
changes before you put any work into them. |
|||
|
|||
If you are fixing a bug, you can just submit a pull request. We do recommend filing an issue |
|||
as well to get an overview what it is that you are fixing. |
|||
This is helpful in case we don’t accept that specific fix but want to keep |
|||
track of the issue. |
|||
|
|||
## What do I do before creating a pull request |
|||
|
|||
1. Fork the repository and branch out of `master`, prefixing your branch's name with `feature/`, `bug/` or `task/` to indicate the scope of the PR. |
|||
1. Install all dependencies (`flutter packages get` or `pub get`) |
|||
1. Ensure you have a meaningful PR name using the [imperative mood](https://chris.beams.io/posts/git-commit/#imperative) as all your commits will be squashed upon merge and the PR's name will be used as the merge commit's message. |
|||
1. If the PR can be broken down into multiple meaningful PRs please do so so that it is easier to review. |
|||
1. If you’ve fixed a bug or added code that should be tested, add tests! |
|||
1. If you've changed the public API, make sure to update/add documentation (for now that is the [Readme](README.md)) |
|||
1. If you've made breaking changes give us a heads up in the pull request. Try to provide a compatibility path for the deprecated APIs and if necessary provide migration instructions in the [Readme](README.md). |
|||
1. Format your code (`dartfmt -w .`) |
|||
1. Analyze your code (`flutter analyze`) |
|||
1. Create the Pull Request |
|||
1. Verify that all status checks are passing |
|||
|
|||
While the prerequisites above must be satisfied prior to having your |
|||
pull request reviewed, the reviewer(s) may ask you to complete additional |
|||
design work, tests, or other changes before your pull request can be ultimately |
|||
accepted. |
|||
|
|||
## Getting in Touch |
|||
|
|||
If you want to just ask a question or get feedback on an idea you can post it |
|||
on [Slack](https://join.slack.com/t/spotifysdk/shared_invite/zt-gibgpkf9-o2ZEJBMPqXNTvqqPONYUQA). |
|||
|
|||
## License |
|||
|
|||
By contributing to spotify_sdk, you agree that your contributions will be licensed |
|||
under its [MIT license](LICENSE). |
|||
@ -0,0 +1,201 @@ |
|||
Apache License |
|||
Version 2.0, January 2004 |
|||
http://www.apache.org/licenses/ |
|||
|
|||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION |
|||
|
|||
1. Definitions. |
|||
|
|||
"License" shall mean the terms and conditions for use, reproduction, |
|||
and distribution as defined by Sections 1 through 9 of this document. |
|||
|
|||
"Licensor" shall mean the copyright owner or entity authorized by |
|||
the copyright owner that is granting the License. |
|||
|
|||
"Legal Entity" shall mean the union of the acting entity and all |
|||
other entities that control, are controlled by, or are under common |
|||
control with that entity. For the purposes of this definition, |
|||
"control" means (i) the power, direct or indirect, to cause the |
|||
direction or management of such entity, whether by contract or |
|||
otherwise, or (ii) ownership of fifty percent (50%) or more of the |
|||
outstanding shares, or (iii) beneficial ownership of such entity. |
|||
|
|||
"You" (or "Your") shall mean an individual or Legal Entity |
|||
exercising permissions granted by this License. |
|||
|
|||
"Source" form shall mean the preferred form for making modifications, |
|||
including but not limited to software source code, documentation |
|||
source, and configuration files. |
|||
|
|||
"Object" form shall mean any form resulting from mechanical |
|||
transformation or translation of a Source form, including but |
|||
not limited to compiled object code, generated documentation, |
|||
and conversions to other media types. |
|||
|
|||
"Work" shall mean the work of authorship, whether in Source or |
|||
Object form, made available under the License, as indicated by a |
|||
copyright notice that is included in or attached to the work |
|||
(an example is provided in the Appendix below). |
|||
|
|||
"Derivative Works" shall mean any work, whether in Source or Object |
|||
form, that is based on (or derived from) the Work and for which the |
|||
editorial revisions, annotations, elaborations, or other modifications |
|||
represent, as a whole, an original work of authorship. For the purposes |
|||
of this License, Derivative Works shall not include works that remain |
|||
separable from, or merely link (or bind by name) to the interfaces of, |
|||
the Work and Derivative Works thereof. |
|||
|
|||
"Contribution" shall mean any work of authorship, including |
|||
the original version of the Work and any modifications or additions |
|||
to that Work or Derivative Works thereof, that is intentionally |
|||
submitted to Licensor for inclusion in the Work by the copyright owner |
|||
or by an individual or Legal Entity authorized to submit on behalf of |
|||
the copyright owner. For the purposes of this definition, "submitted" |
|||
means any form of electronic, verbal, or written communication sent |
|||
to the Licensor or its representatives, including but not limited to |
|||
communication on electronic mailing lists, source code control systems, |
|||
and issue tracking systems that are managed by, or on behalf of, the |
|||
Licensor for the purpose of discussing and improving the Work, but |
|||
excluding communication that is conspicuously marked or otherwise |
|||
designated in writing by the copyright owner as "Not a Contribution." |
|||
|
|||
"Contributor" shall mean Licensor and any individual or Legal Entity |
|||
on behalf of whom a Contribution has been received by Licensor and |
|||
subsequently incorporated within the Work. |
|||
|
|||
2. Grant of Copyright License. Subject to the terms and conditions of |
|||
this License, each Contributor hereby grants to You a perpetual, |
|||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable |
|||
copyright license to reproduce, prepare Derivative Works of, |
|||
publicly display, publicly perform, sublicense, and distribute the |
|||
Work and such Derivative Works in Source or Object form. |
|||
|
|||
3. Grant of Patent License. Subject to the terms and conditions of |
|||
this License, each Contributor hereby grants to You a perpetual, |
|||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable |
|||
(except as stated in this section) patent license to make, have made, |
|||
use, offer to sell, sell, import, and otherwise transfer the Work, |
|||
where such license applies only to those patent claims licensable |
|||
by such Contributor that are necessarily infringed by their |
|||
Contribution(s) alone or by combination of their Contribution(s) |
|||
with the Work to which such Contribution(s) was submitted. If You |
|||
institute patent litigation against any entity (including a |
|||
cross-claim or counterclaim in a lawsuit) alleging that the Work |
|||
or a Contribution incorporated within the Work constitutes direct |
|||
or contributory patent infringement, then any patent licenses |
|||
granted to You under this License for that Work shall terminate |
|||
as of the date such litigation is filed. |
|||
|
|||
4. Redistribution. You may reproduce and distribute copies of the |
|||
Work or Derivative Works thereof in any medium, with or without |
|||
modifications, and in Source or Object form, provided that You |
|||
meet the following conditions: |
|||
|
|||
(a) You must give any other recipients of the Work or |
|||
Derivative Works a copy of this License; and |
|||
|
|||
(b) You must cause any modified files to carry prominent notices |
|||
stating that You changed the files; and |
|||
|
|||
(c) You must retain, in the Source form of any Derivative Works |
|||
that You distribute, all copyright, patent, trademark, and |
|||
attribution notices from the Source form of the Work, |
|||
excluding those notices that do not pertain to any part of |
|||
the Derivative Works; and |
|||
|
|||
(d) If the Work includes a "NOTICE" text file as part of its |
|||
distribution, then any Derivative Works that You distribute must |
|||
include a readable copy of the attribution notices contained |
|||
within such NOTICE file, excluding those notices that do not |
|||
pertain to any part of the Derivative Works, in at least one |
|||
of the following places: within a NOTICE text file distributed |
|||
as part of the Derivative Works; within the Source form or |
|||
documentation, if provided along with the Derivative Works; or, |
|||
within a display generated by the Derivative Works, if and |
|||
wherever such third-party notices normally appear. The contents |
|||
of the NOTICE file are for informational purposes only and |
|||
do not modify the License. You may add Your own attribution |
|||
notices within Derivative Works that You distribute, alongside |
|||
or as an addendum to the NOTICE text from the Work, provided |
|||
that such additional attribution notices cannot be construed |
|||
as modifying the License. |
|||
|
|||
You may add Your own copyright statement to Your modifications and |
|||
may provide additional or different license terms and conditions |
|||
for use, reproduction, or distribution of Your modifications, or |
|||
for any such Derivative Works as a whole, provided Your use, |
|||
reproduction, and distribution of the Work otherwise complies with |
|||
the conditions stated in this License. |
|||
|
|||
5. Submission of Contributions. Unless You explicitly state otherwise, |
|||
any Contribution intentionally submitted for inclusion in the Work |
|||
by You to the Licensor shall be under the terms and conditions of |
|||
this License, without any additional terms or conditions. |
|||
Notwithstanding the above, nothing herein shall supersede or modify |
|||
the terms of any separate license agreement you may have executed |
|||
with Licensor regarding such Contributions. |
|||
|
|||
6. Trademarks. This License does not grant permission to use the trade |
|||
names, trademarks, service marks, or product names of the Licensor, |
|||
except as required for reasonable and customary use in describing the |
|||
origin of the Work and reproducing the content of the NOTICE file. |
|||
|
|||
7. Disclaimer of Warranty. Unless required by applicable law or |
|||
agreed to in writing, Licensor provides the Work (and each |
|||
Contributor provides its Contributions) on an "AS IS" BASIS, |
|||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or |
|||
implied, including, without limitation, any warranties or conditions |
|||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A |
|||
PARTICULAR PURPOSE. You are solely responsible for determining the |
|||
appropriateness of using or redistributing the Work and assume any |
|||
risks associated with Your exercise of permissions under this License. |
|||
|
|||
8. Limitation of Liability. In no event and under no legal theory, |
|||
whether in tort (including negligence), contract, or otherwise, |
|||
unless required by applicable law (such as deliberate and grossly |
|||
negligent acts) or agreed to in writing, shall any Contributor be |
|||
liable to You for damages, including any direct, indirect, special, |
|||
incidental, or consequential damages of any character arising as a |
|||
result of this License or out of the use or inability to use the |
|||
Work (including but not limited to damages for loss of goodwill, |
|||
work stoppage, computer failure or malfunction, or any and all |
|||
other commercial damages or losses), even if such Contributor |
|||
has been advised of the possibility of such damages. |
|||
|
|||
9. Accepting Warranty or Additional Liability. While redistributing |
|||
the Work or Derivative Works thereof, You may choose to offer, |
|||
and charge a fee for, acceptance of support, warranty, indemnity, |
|||
or other liability obligations and/or rights consistent with this |
|||
License. However, in accepting such obligations, You may act only |
|||
on Your own behalf and on Your sole responsibility, not on behalf |
|||
of any other Contributor, and only if You agree to indemnify, |
|||
defend, and hold each Contributor harmless for any liability |
|||
incurred by, or claims asserted against, such Contributor by reason |
|||
of your accepting any such warranty or additional liability. |
|||
|
|||
END OF TERMS AND CONDITIONS |
|||
|
|||
APPENDIX: How to apply the Apache License to your work. |
|||
|
|||
To apply the Apache License to your work, attach the following |
|||
boilerplate notice, with the fields enclosed by brackets "[]" |
|||
replaced with your own identifying information. (Don't include |
|||
the brackets!) The text should be enclosed in the appropriate |
|||
comment syntax for the file format. We also recommend that a |
|||
file or class name and description of purpose be included on the |
|||
same "printed page" as the copyright notice for easier |
|||
identification within third-party archives. |
|||
|
|||
Copyright [yyyy] [name of copyright owner] |
|||
|
|||
Licensed under the Apache License, Version 2.0 (the "License"); |
|||
you may not use this file except in compliance with the License. |
|||
You may obtain a copy of the License at |
|||
|
|||
http://www.apache.org/licenses/LICENSE-2.0 |
|||
|
|||
Unless required by applicable law or agreed to in writing, software |
|||
distributed under the License is distributed on an "AS IS" BASIS, |
|||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
|||
See the License for the specific language governing permissions and |
|||
limitations under the License. |
|||
@ -0,0 +1,257 @@ |
|||
# spotify_sdk |
|||
|
|||
<p align="center"> |
|||
<a href="https://pub.dev/packages/spotify_sdk"><img src="https://img.shields.io/badge/pub-3.0.2-green" alt="build"></a> |
|||
<a href="https://github.com/brim-borium/spotify_sdk"><img src="https://img.shields.io/github/stars/brim-borium/spotify_sdk?color=deeppink" alt="build"></a> |
|||
<a href="https://github.com/brim-borium/spotify_sdk/blob/main/LICENSE"><img src="https://img.shields.io/github/license/brim-borium/spotify_sdk?color=blue" alt="build"></a> |
|||
</p> |
|||
|
|||
--- |
|||
|
|||
## Description |
|||
|
|||
This is a flutter package that wraps the native [iOS](https://github.com/spotify/ios-sdk) and [Android](https://github.com/spotify/android-sdk) Spotify "remote" SDKs as well as the [Spotify Web Playback SDK](https://developer.spotify.com/documentation/web-playback-sdk/) for web. Since it wraps the native SDKs it has the same features and limitations. |
|||
|
|||
## Setup |
|||
|
|||
### Android |
|||
|
|||
This package is using both the spotify-app-remote sdk and spotify-auth library. The auth library is needed to get the access token to work with the web api. |
|||
|
|||
From the [Spotify Android SDK Quick Start](https://developer.spotify.com/documentation/android/quick-start/). You need two things: |
|||
|
|||
1. Register your app in the [spotify developer portal](https://developer.spotify.com/dashboard/). You also need to create a sha-1 fingerprint and add this and your package name to the app settings on the dashboard as well as a redirect url. |
|||
2. Follow the steps below for either [Option A: Auto setup](#option-a-auto-setup) or [Option B: Manual setup](#option-b-manual-setup-instructions-for-android-studio-42). |
|||
|
|||
#### Option A: Auto setup |
|||
|
|||
Use the provided setup script to automatically download the latest version of the spotify-app-remote sdk from GitHub and setup the gradle files inside your android project. Run the following command in the root folder of your flutter project. |
|||
|
|||
```bash |
|||
dart run spotify_sdk:android_setup |
|||
``` |
|||
|
|||
Use the `--help` flag to see all available options. |
|||
|
|||
#### Option B: Manual setup, instructions for Android Studio 4.2+ |
|||
|
|||
Download the current [Spotify Android SDK](https://github.com/spotify/android-sdk/releases). Here you need the `spotify-app-remote-\*.aar``. |
|||
|
|||
After you are all setup you need to add the SDKs `\*.aar`` file to your Android Project as Module. See the [Spotify Android SDK Quick Start](https://developer.spotify.com/documentation/android/quick-start/) for detailed information. |
|||
|
|||
Since Android Studio 4.2 you need to manually perform these steps in order to add .jar/.aar files: |
|||
|
|||
1. Open the android folder of your flutter project as an Android Studio project |
|||
2. In the android root folder create a single folder for `spotify-app-remote`, place the corresponding aar file and create an empty build.gradle file, like on the screenshot below: |
|||
 |
|||
|
|||
3. Content of the `spotify-app-remote/build.gradle` file: |
|||
|
|||
```groovy |
|||
configurations.maybeCreate("default") |
|||
artifacts.add("default", file('spotify-app-remote-release-x.x.x.aar')) |
|||
``` |
|||
|
|||
4. In the android root folder find `settings.gradle` file, open it and add the following line at the top of the file: |
|||
|
|||
```groovy |
|||
include ':spotify-app-remote' |
|||
``` |
|||
|
|||
5. In the app/build.gradle add the following to the default config |
|||
|
|||
```groovy |
|||
defaultConfig { |
|||
manifestPlaceholders = [redirectSchemeName: "spotify-sdk", redirectHostName: "auth"] |
|||
... |
|||
} |
|||
``` |
|||
|
|||
### iOS |
|||
|
|||
Register your app in the [spotify developer portal](https://developer.spotify.com/dashboard/). You also need to register your Bundle ID as well as a Redirect URI. |
|||
|
|||
Follow the instructions in the section `Setup the iOS SDK` of [Spotify iOS SDK Quick Start](https://developer.spotify.com/documentation/ios/quick-start/). |
|||
|
|||
### Web |
|||
|
|||
1. Register your app in the [spotify developer portal](https://developer.spotify.com/dashboard/). You need to provide a redirect URL which points to a dedicated page on a website you own. |
|||
|
|||
2. Paste the following onto the webpage, which you linked to in your redirect URL. |
|||
```html |
|||
<!DOCTYPE html> |
|||
<html> |
|||
<head> |
|||
<title>Authenticating Spotify</title> |
|||
</head> |
|||
<body> |
|||
<p>Please wait while we authenticate Spotify...</p> |
|||
<script type="text/javascript"> |
|||
if(window.opener) { |
|||
window.opener.postMessage('?' + window.location.href.split('?')[1], "*"); |
|||
} else { |
|||
window.close(); |
|||
} |
|||
</script> |
|||
</body> |
|||
</html> |
|||
``` |
|||
|
|||
3. Optionally add this to your Flutter app web/index.html to avoid a Javascript `TypeError: r.__extends is not a function` error in development mode. |
|||
|
|||
```html |
|||
<script src="https://sdk.scdn.co/spotify-player.js"></script> |
|||
<script> |
|||
window.onSpotifyWebPlaybackSDKReady = (evt) => {}; |
|||
</script> |
|||
``` |
|||
|
|||
[You need Spotify Premium to access the Web SDK.](https://developer.spotify.com/documentation/web-playback-sdk/quick-start/) |
|||
|
|||
## Usage |
|||
|
|||
To start using this package first import it in your Dart file. |
|||
|
|||
```dart |
|||
import 'package:spotify_sdk/spotify_sdk.dart'; |
|||
``` |
|||
|
|||
To connect to the Spotify app you can call connectToSpotifyRemote(...) or getAccessToken(...). In both of these methods you need the client id, which you will find in the Spotify developer dashboard and the redirect url you set there for that specific client. |
|||
|
|||
```dart |
|||
await SpotifySdk.connectToSpotifyRemote(clientId: "", redirectUrl: "") |
|||
``` |
|||
|
|||
Subscribe to `PlayerState` or `PlayerContext` streams only after connecting successfully |
|||
|
|||
```dart |
|||
SpotifySdk.subscribePlayerState(); |
|||
SpotifySdk.subscribePlayerContext(); |
|||
``` |
|||
|
|||
If you want to use the web api as well you have to use this method to get the access token. |
|||
You can specify multiple scopes by separating them with a comma "," as shown below. For more information on scopes you can refer to [Spotify Authorization Scopes Guide](https://developer.spotify.com/documentation/general/guides/authorization/scopes/) |
|||
|
|||
```dart |
|||
final accessToken = await SpotifySdk.getAccessToken(clientId: "", redirectUrl: "", scope: "app-remote-control,user-modify-playback-state,playlist-read-private"); |
|||
``` |
|||
|
|||
On Web you can use the token that you get from `getAccessToken(...)` and then pass it to `connectToSpotifyRemote(...)`. This will avoid having to send user through two Spotify OAuth prompts. You should not persist this token, nor supply a different token, because the refresh token is only set interally by `getAccessToken` or `connectToSpotifyRemote`. |
|||
|
|||
On iOS you can store the token that you get from `getAccessToken(...)` and then pass it to `connectToSpotifyRemote(...)` during the next session. This will avoid having to switch to the Spotify app for establishing the connection. This library does not handle storing the token. It is up to you to persist it wherever you see fit. Keep in mind that this feature is currently quite buggy in the native iOS SDK and has many side effects like random disconnections. Proceed with caution. |
|||
|
|||
On iOS Spotify starts playing music when attempting connection. This is a default behavior and there is no official way to prevent this with the currently supported authentication flows. You have the option to pass a Spotify URI upon connection or set it to a blank string to play the last played song. There is an undocumented workaround if you don't want music to start playing which is to pass an invalid Spotify URI instead. This is not officially supported by the Spotify SDK or this library and it can fail or stop working at any time! |
|||
|
|||
Have a look [in the example](example/lib/main.dart) for detailed insights on how you can use this package. |
|||
|
|||
### Token Swap |
|||
|
|||
You can optionally specify "token swap" URLs to manage tokens with a backend service that protects your OAuth client secret. For more information refer to the [Spotify Token Swap and Refresh Guide](https://developer.spotify.com/documentation/ios/guides/token-swap-and-refresh/) |
|||
|
|||
```dart |
|||
SpotifySdkPlugin.tokenSwapURL = 'https://example.com/api/spotify/token'; |
|||
SpotifySdkPlugin.tokenRefreshURL = 'https://example.com/api/spotify/refresh'; |
|||
```` |
|||
|
|||
On web, this package will perform an Authorization Code (without PKCE) flow, then exchange the code and refresh the token with a backend service you run at the URLs provided. |
|||
|
|||
Token Swap is for now "web only". While the iOS SDK also supports the "token swap", this flow is not yet supported. |
|||
|
|||
### Api |
|||
|
|||
#### Connecting/Authenticating |
|||
|
|||
| Function | Description| Android | iOS | Web | |
|||
|---|---|---|---|---| |
|||
| connectToSpotifyRemote | Connects the App to Spotify | ✔ | ✔ | ✔ | |
|||
| getAccessToken | Gets the Access Token that you can use to work with the [Web Api](https://developer.spotify.com/documentation/web-api/) | ✔ | ✔ | ✔ | |
|||
| disconnect | Disconnects the app connection | ✔ | ✔ | ✔ | |
|||
| subscribeConnectionStatus | Subscribes to the current player state. | ✔ | ✔ | 🚧 | |
|||
|
|||
#### Player Api |
|||
|
|||
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 | ✔ | ✔ | 🚧 | |
|||
| getCapabilities | Gets the current users capabilities | ✔ | ✔ | 🚧 | |
|||
| getLibraryState | Gets the current library state | ✔ | ✔ | 🚧 | |
|||
| removeFromLibrary | Removes the given spotifyUri to the users library | ✔ | ✔ | 🚧 | |
|||
| subscribeCapabilities | Subscribes to the current users capabilities | ✔ | 🚧 | 🚧 | |
|||
| subscribeUserStatus | Subscribes to the current users status | ✔ | 🚧 | 🚧 | |
|||
|
|||
#### Connect Api |
|||
|
|||
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 | 🚧 | 🚧 | 🚧 | |
|||
| getRecommendedContentItems | tbd | 🚧 | 🚧 | 🚧 | |
|||
| playContentItem | tbd | 🚧 | 🚧 | 🚧 | |
|||
|
|||
## Migration |
|||
|
|||
### spotify-auth: moving from locally sourced aar to maven central |
|||
`spotify-auth` SDK is now retrieved via Maven Central instead of being sourced from an AAR file |
|||
* Steps to remove the locally sourced `spotify-auth` SDK: |
|||
* android/settings.gradle -> remove `':spotify-auth'` |
|||
* android/spotify-auth/build.gradle -> remove file |
|||
* android/spotify-auth/spotify-auth-release-x.x.x.aar -> remove file |
|||
|
|||
## Official Spotify Docs |
|||
|
|||
- [Auth](https://spotify.github.io/android-sdk/auth-lib/docs/index.html) |
|||
- [App Remote](https://spotify.github.io/android-sdk/app-remote-lib/docs/index.html) |
|||
- [Web Playback SDK](https://developer.spotify.com/documentation/web-playback-sdk/) |
|||
|
|||
|
|||
@ -0,0 +1,12 @@ |
|||
include: package:flutter_lints/flutter.yaml |
|||
|
|||
analyzer: |
|||
exclude: |
|||
# Ignore generated files |
|||
- "**/*.g.dart" |
|||
- "lib/generated/**" |
|||
errors: |
|||
close_sinks: ignore |
|||
linter: |
|||
rules: |
|||
unsafe_html: false |
|||
@ -0,0 +1,8 @@ |
|||
*.iml |
|||
.gradle |
|||
/local.properties |
|||
/.idea/workspace.xml |
|||
/.idea/libraries |
|||
.DS_Store |
|||
/build |
|||
/captures |
|||
@ -0,0 +1,74 @@ |
|||
group 'de.minimalme.spotify_sdk' |
|||
version '1.0-SNAPSHOT' |
|||
|
|||
buildscript { |
|||
ext.kotlin_version = '1.8.22' |
|||
repositories { |
|||
google() |
|||
mavenCentral() |
|||
maven { url "https://jitpack.io" } |
|||
} |
|||
|
|||
dependencies { |
|||
classpath 'com.android.tools.build:gradle:8.1.4' |
|||
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" |
|||
} |
|||
} |
|||
|
|||
rootProject.allprojects { |
|||
repositories { |
|||
google() |
|||
mavenCentral() |
|||
maven { url "https://jitpack.io" } |
|||
} |
|||
|
|||
} |
|||
|
|||
apply plugin: 'com.android.library' |
|||
apply plugin: 'kotlin-android' |
|||
|
|||
android { |
|||
if (project.android.hasProperty("namespace")) { |
|||
namespace 'de.minimalme.spotify_sdk' |
|||
} |
|||
|
|||
compileSdk 34 |
|||
|
|||
compileOptions { |
|||
sourceCompatibility = JavaVersion.VERSION_17 |
|||
targetCompatibility = JavaVersion.VERSION_17 |
|||
} |
|||
|
|||
kotlinOptions { |
|||
jvmTarget = JavaVersion.VERSION_17 |
|||
} |
|||
|
|||
sourceSets { |
|||
main.java.srcDirs += 'src/main/kotlin' |
|||
} |
|||
|
|||
defaultConfig { |
|||
minSdkVersion 21 |
|||
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" |
|||
} |
|||
|
|||
lintOptions { |
|||
disable 'InvalidPackage' |
|||
} |
|||
} |
|||
|
|||
dependencies { |
|||
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" |
|||
// -- spotify |
|||
implementation "com.spotify.android:auth:2.1.0" |
|||
implementation(name: 'spotify-app-remote-release-0.8.0', ext: 'aar') |
|||
implementation 'com.google.code.gson:gson:2.10.1' |
|||
// -- events |
|||
implementation "com.github.stuhlmeier:kotlin-events:v2.0" |
|||
} |
|||
|
|||
repositories { |
|||
flatDir { |
|||
dirs 'libs' |
|||
} |
|||
} |
|||
@ -0,0 +1,3 @@ |
|||
org.gradle.jvmargs=-Xmx1536M |
|||
android.useAndroidX=true |
|||
android.enableJetifier=true |
|||
@ -0,0 +1,7 @@ |
|||
distributionBase=GRADLE_USER_HOME |
|||
distributionPath=wrapper/dists |
|||
distributionUrl=https\://services.gradle.org/distributions/gradle-8.9-all.zip |
|||
networkTimeout=10000 |
|||
validateDistributionUrl=true |
|||
zipStoreBase=GRADLE_USER_HOME |
|||
zipStorePath=wrapper/dists |
|||
Binary file not shown.
@ -0,0 +1 @@ |
|||
rootProject.name = 'spotify_sdk' |
|||
@ -0,0 +1,10 @@ |
|||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"> |
|||
<application> |
|||
<activity |
|||
android:name="com.spotify.sdk.android.authentication.LoginActivity" |
|||
android:theme="@android:style/Theme.Translucent.NoTitleBar" /> |
|||
</application> |
|||
<queries> |
|||
<package android:name="com.spotify.music" /> |
|||
</queries> |
|||
</manifest> |
|||
@ -0,0 +1,13 @@ |
|||
package de.minimalme.spotify_sdk |
|||
|
|||
import com.spotify.android.appremote.api.SpotifyAppRemote |
|||
import io.flutter.plugin.common.MethodChannel |
|||
|
|||
open class BaseSpotifyApi(internal val spotifyAppRemote: SpotifyAppRemote?, internal val result: MethodChannel.Result) { |
|||
|
|||
private val errorAppRemoteNull = "spotifyAppRemoteNull" |
|||
|
|||
internal fun spotifyRemoteAppNotSetError() { |
|||
result.error(errorAppRemoteNull, "spotifyAppRemote is null", "") |
|||
} |
|||
} |
|||
@ -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() |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,35 @@ |
|||
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 SpotifyImagesApi(spotifyAppRemote: SpotifyAppRemote?, result: MethodChannel.Result) : BaseSpotifyApi(spotifyAppRemote, result) { |
|||
|
|||
private val errorGetImage = "errorGetImage" |
|||
private val errorImageUri = "errorImageUri" |
|||
private val errorImageDimension = "errorImageDimensionUri" |
|||
|
|||
private val imagesApi = spotifyAppRemote?.imagesApi |
|||
|
|||
fun getImage(imageUri: String?, dimension: Int?) { |
|||
if (imagesApi != null && imageUri != null && dimension != null) { |
|||
imagesApi.getImage(ImageUri(imageUri), Dimension.values().first{it.value == dimension}) |
|||
.setResultCallback { bitmap -> |
|||
val stream = ByteArrayOutputStream() |
|||
bitmap.compress(Bitmap.CompressFormat.PNG, 100, stream) |
|||
result.success(stream.toByteArray()) |
|||
} |
|||
.setErrorCallback { throwable -> result.error(errorGetImage, "error when getting the image", throwable.toString()) } |
|||
} else if (imageUri == null) { |
|||
result.error(errorImageUri, "imageUri has invalid format or is not set", "") |
|||
} else if (dimension == null) { |
|||
result.error(errorImageDimension, "imageDimension has invalid format or is not set", "") |
|||
} else { |
|||
spotifyRemoteAppNotSetError() |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,210 @@ |
|||
package de.minimalme.spotify_sdk |
|||
|
|||
import com.google.gson.Gson |
|||
import com.spotify.android.appremote.api.SpotifyAppRemote |
|||
import com.spotify.protocol.types.PlaybackSpeed |
|||
import io.flutter.plugin.common.MethodChannel |
|||
|
|||
class SpotifyPlayerApi(spotifyAppRemote: SpotifyAppRemote?, result: MethodChannel.Result) : BaseSpotifyApi(spotifyAppRemote, result) { |
|||
|
|||
|
|||
private val errorCrossfadeState = "crossfadeStateError" |
|||
private val errorPlayerState = "PlayerStateError" |
|||
private val errorQue = "queueError" |
|||
private val errorPlay = "playError" |
|||
private val errorPause = "pauseError" |
|||
private val errorResume = "resumeError" |
|||
private val errorSkipNext = "skipNextError" |
|||
private val errorSkipPrevious = "skipPreviousError" |
|||
private val errorSeekTo = "seekToError" |
|||
private val errorSkipToIndex = "skipToIndexError" |
|||
private val errorPodcastPlaybackSpeed = "podcastPlaybackSpeedError" |
|||
private val errorToggleShuffle = "toggleShuffleError" |
|||
private val errorToggleRepeat = "toggleRepeatError" |
|||
|
|||
private val playerApi = spotifyAppRemote?.playerApi |
|||
|
|||
internal fun getCrossfadeState() { |
|||
if (playerApi != null) { |
|||
playerApi.crossfadeState |
|||
.setResultCallback { crossfadeState -> |
|||
result.success(Gson().toJson(crossfadeState)) |
|||
} |
|||
.setErrorCallback { throwable -> result.error(errorCrossfadeState, "error when getting the current state of crossfade setting", throwable.toString()) } |
|||
} else { |
|||
spotifyRemoteAppNotSetError() |
|||
} |
|||
} |
|||
|
|||
internal fun getPlayerState() { |
|||
if (playerApi != null) { |
|||
playerApi.playerState |
|||
.setResultCallback { playerState -> |
|||
result.success(Gson().toJson(playerState)) |
|||
|
|||
} |
|||
.setErrorCallback { throwable -> result.error(errorPlayerState, "error when getting the current state of the player", throwable.toString()) } |
|||
} else { |
|||
spotifyRemoteAppNotSetError() |
|||
} |
|||
} |
|||
|
|||
internal fun queue(spotifyUri: String?) { |
|||
if (playerApi != null && !spotifyUri.isNullOrBlank()) { |
|||
playerApi.queue(spotifyUri) |
|||
.setResultCallback { result.success(true) } |
|||
.setErrorCallback { throwable -> result.error(errorQue, "error when adding uri: $spotifyUri to queue", throwable.toString()) } |
|||
} else if (spotifyUri.isNullOrBlank()) { |
|||
result.error(errorQue, "spotifyUri has invalid format or is not set", "") |
|||
} else { |
|||
spotifyRemoteAppNotSetError() |
|||
} |
|||
} |
|||
|
|||
internal fun play(spotifyUri: String?) { |
|||
if (playerApi != null && !spotifyUri.isNullOrBlank()) { |
|||
playerApi.play(spotifyUri) |
|||
.setResultCallback { result.success(true) } |
|||
.setErrorCallback { throwable -> result.error(errorPlay, "error when playing uri: $spotifyUri", throwable.toString()) } |
|||
} else if (spotifyUri.isNullOrBlank()) { |
|||
result.error(errorPlay, "spotifyUri has invalid format or is not set", "") |
|||
} else { |
|||
spotifyRemoteAppNotSetError() |
|||
} |
|||
} |
|||
|
|||
internal fun pause() { |
|||
if (playerApi != null) { |
|||
playerApi.pause() |
|||
.setResultCallback { result.success(true) } |
|||
.setErrorCallback { throwable -> result.error(errorPause, "error when pausing", throwable.toString()) } |
|||
} else { |
|||
spotifyRemoteAppNotSetError() |
|||
} |
|||
} |
|||
|
|||
internal fun resume() { |
|||
if (playerApi != null) { |
|||
playerApi.resume() |
|||
.setResultCallback { result.success(true) } |
|||
.setErrorCallback { throwable -> result.error(errorResume, "error when resuming", throwable.toString()) } |
|||
} else { |
|||
spotifyRemoteAppNotSetError() |
|||
} |
|||
} |
|||
|
|||
internal fun seekTo(milliseconds: Int?) { |
|||
val castedMilliseconds = milliseconds?.toLong() |
|||
if (playerApi != null && castedMilliseconds != null) { |
|||
playerApi.seekTo(castedMilliseconds) |
|||
.setResultCallback { result.success(true) } |
|||
.setErrorCallback { throwable -> result.error(errorResume, "error when seeking to: $castedMilliseconds", throwable.toString()) } |
|||
} else if (castedMilliseconds == null) { |
|||
result.error(errorSeekTo, "positionMS is not set", "") |
|||
} else { |
|||
spotifyRemoteAppNotSetError() |
|||
} |
|||
} |
|||
|
|||
internal fun seekToRelativePosition(milliseconds: Int?) { |
|||
val castedMilliseconds = milliseconds?.toLong() |
|||
if (playerApi != null && castedMilliseconds != null) { |
|||
playerApi.seekToRelativePosition(castedMilliseconds) |
|||
.setResultCallback { result.success(true) } |
|||
.setErrorCallback { throwable -> result.error(errorResume, "error when seeking relative to: $castedMilliseconds", throwable.toString()) } |
|||
} else if (castedMilliseconds == null) { |
|||
result.error(errorSeekTo, "milliseconds is not set", "") |
|||
} else { |
|||
spotifyRemoteAppNotSetError() |
|||
} |
|||
} |
|||
|
|||
internal fun setPodcastPlaybackSpeed(podcastPlaybackSpeedValue: Int?) { |
|||
if (playerApi != null && podcastPlaybackSpeedValue != null) { |
|||
|
|||
val podcastPlaybackSpeed = PlaybackSpeed.PodcastPlaybackSpeed.values().firstOrNull{ it.value == podcastPlaybackSpeedValue } |
|||
|
|||
playerApi.setPodcastPlaybackSpeed(podcastPlaybackSpeed) |
|||
.setResultCallback { result.success(true) } |
|||
.setErrorCallback { throwable -> result.error(errorPodcastPlaybackSpeed, "error when setting the podcastPlaybackSpeed to: $podcastPlaybackSpeed", throwable.toString()) } |
|||
} else if (podcastPlaybackSpeedValue == null) { |
|||
result.error(errorPodcastPlaybackSpeed, "podcastPlaybackSpeedValue is not set", "") |
|||
} else { |
|||
spotifyRemoteAppNotSetError() |
|||
} |
|||
} |
|||
|
|||
internal fun skipNext() { |
|||
if (playerApi != null) { |
|||
playerApi.skipNext() |
|||
.setResultCallback { result.success(true) } |
|||
.setErrorCallback { throwable -> result.error(errorSkipNext, "error when skipping next", throwable.toString()) } |
|||
} else { |
|||
spotifyRemoteAppNotSetError() |
|||
} |
|||
} |
|||
|
|||
internal fun skipPrevious() { |
|||
if (playerApi != null) { |
|||
playerApi.skipPrevious() |
|||
.setResultCallback { result.success(true) } |
|||
.setErrorCallback { throwable -> result.error(errorSkipPrevious, "error when skipping previous", throwable.toString()) } |
|||
} else { |
|||
spotifyRemoteAppNotSetError() |
|||
} |
|||
} |
|||
|
|||
internal fun skipToIndex(uri: String?, index: Int?) { |
|||
if (playerApi != null && !uri.isNullOrBlank() && index != null) { |
|||
playerApi.skipToIndex(uri, index) |
|||
.setResultCallback { result.success(true) } |
|||
.setErrorCallback { throwable -> result.error(errorSkipToIndex, "error when skipping to index", throwable.toString()) } |
|||
} else { |
|||
spotifyRemoteAppNotSetError() |
|||
} |
|||
} |
|||
|
|||
internal fun toggleShuffle() { |
|||
if (playerApi != null) { |
|||
playerApi.toggleShuffle() |
|||
.setResultCallback { result.success(true) } |
|||
.setErrorCallback { throwable -> result.error(errorToggleShuffle, "error when toggle shuffle", throwable.toString()) } |
|||
} else { |
|||
spotifyRemoteAppNotSetError() |
|||
} |
|||
} |
|||
|
|||
internal fun setShuffle(shuffle: Boolean?) { |
|||
if (playerApi != null && shuffle != null) { |
|||
playerApi.setShuffle(shuffle) |
|||
.setResultCallback { result.success(true) } |
|||
.setErrorCallback { throwable -> result.error(errorToggleRepeat, "error when toggle shuffle", throwable.toString()) } |
|||
} else if (shuffle == null) { |
|||
result.error(errorQue, "shuffle has invalid format or is not set", "") |
|||
} else { |
|||
spotifyRemoteAppNotSetError() |
|||
} |
|||
} |
|||
|
|||
internal fun toggleRepeat() { |
|||
if (playerApi != null) { |
|||
playerApi.toggleRepeat() |
|||
.setResultCallback { result.success(true) } |
|||
.setErrorCallback { throwable -> result.error(errorToggleRepeat, "error when toggle repeat", throwable.toString()) } |
|||
} else { |
|||
spotifyRemoteAppNotSetError() |
|||
} |
|||
} |
|||
|
|||
internal fun setRepeatMode(repeatMode: Int?) { |
|||
if (playerApi != null && repeatMode != null) { |
|||
playerApi.setRepeat(repeatMode) |
|||
.setResultCallback { result.success(true) } |
|||
.setErrorCallback { throwable -> result.error(errorToggleRepeat, "error when toggle repeat", throwable.toString()) } |
|||
} else if (repeatMode == null) { |
|||
result.error(errorQue, "repeatMode has invalid format or is not set", "") |
|||
} else { |
|||
spotifyRemoteAppNotSetError() |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,381 @@ |
|||
package de.minimalme.spotify_sdk |
|||
|
|||
import android.app.Activity |
|||
import android.content.Context |
|||
import android.content.Intent |
|||
import android.util.Log |
|||
import com.spotify.android.appremote.api.ConnectionParams |
|||
import com.spotify.android.appremote.api.Connector.ConnectionListener |
|||
import com.spotify.android.appremote.api.SpotifyAppRemote |
|||
import com.spotify.android.appremote.api.error.* |
|||
import com.spotify.sdk.android.auth.AuthorizationClient |
|||
import com.spotify.sdk.android.auth.AuthorizationRequest |
|||
import com.spotify.sdk.android.auth.AuthorizationResponse |
|||
import de.minimalme.spotify_sdk.subscriptions.* |
|||
import io.flutter.embedding.engine.plugins.FlutterPlugin |
|||
import io.flutter.embedding.engine.plugins.activity.ActivityAware |
|||
import io.flutter.embedding.engine.plugins.activity.ActivityPluginBinding |
|||
import io.flutter.plugin.common.EventChannel |
|||
import io.flutter.plugin.common.MethodCall |
|||
import io.flutter.plugin.common.MethodChannel |
|||
import io.flutter.plugin.common.MethodChannel.MethodCallHandler |
|||
import io.flutter.plugin.common.MethodChannel.Result |
|||
import io.flutter.plugin.common.PluginRegistry |
|||
import kotlinx.event.SetEvent |
|||
import kotlinx.event.event |
|||
|
|||
class SpotifySdkPlugin : MethodCallHandler, FlutterPlugin, ActivityAware, PluginRegistry.ActivityResultListener { |
|||
|
|||
// application context |
|||
private var applicationContext : Context? = null |
|||
private var applicationActivity : Activity? = null |
|||
|
|||
// method channel |
|||
private lateinit var methodChannel : MethodChannel |
|||
private val channelName = "spotify_sdk" |
|||
private val loggingTag = "spotify_sdk" |
|||
|
|||
// event channels |
|||
private var playerContextChannel : EventChannel? = null |
|||
private var playerStateChannel : EventChannel? = null |
|||
private var capabilitiesChannel : EventChannel? = null |
|||
private var userStatusChannel : EventChannel? = null |
|||
private var connectionStatusChannel : EventChannel? = null |
|||
|
|||
private val playerContextSubscription = "player_context_subscription" |
|||
private val playerStateSubscription = "player_state_subscription" |
|||
private val capabilitiesSubscription = "capabilities_subscription" |
|||
private val userStatusSubscription = "user_status_subscription" |
|||
private val connectionStatusSubscription = "connection_status_subscription" |
|||
|
|||
//connecting |
|||
private val methodConnectToSpotify = "connectToSpotify" |
|||
private val methodGetAccessToken = "getAccessToken" |
|||
private val methodDisconnectFromSpotify = "disconnectFromSpotify" |
|||
|
|||
// connectApi |
|||
private val methodSwitchToLocalDevice = "switchToLocalDevice" |
|||
|
|||
//playerApi |
|||
private val methodGetCrossfadeState = "getCrossfadeState" |
|||
private val methodGetPlayerState = "getPlayerState" |
|||
private val methodPlay = "play" |
|||
private val methodPause = "pause" |
|||
private val methodQueueTrack = "queueTrack" |
|||
private val methodResume = "resume" |
|||
private val methodSeekToRelativePosition = "seekToRelativePosition" |
|||
private val methodSetPodcastPlaybackSpeed = "setPodcastPlaybackSpeed" |
|||
private val methodSkipNext = "skipNext" |
|||
private val methodSkipPrevious = "skipPrevious" |
|||
private val methodSkipToIndex = "skipToIndex" |
|||
private val methodSeekTo = "seekTo" |
|||
private val methodToggleRepeat = "toggleRepeat" |
|||
private val methodToggleShuffle = "toggleShuffle" |
|||
private val methodSetShuffle = "setShuffle" |
|||
private val methodSetRepeatMode = "setRepeatMode" |
|||
|
|||
//userApi |
|||
private val methodAddToLibrary = "addToLibrary" |
|||
private val methodRemoveFromLibrary = "removeFromLibrary" |
|||
private val methodGetCapabilities = "getCapabilities" |
|||
private val methodGetLibraryState = "getLibraryState" |
|||
|
|||
//imagesApi |
|||
private val methodGetImage = "getImage" |
|||
|
|||
private val paramClientId = "clientId" |
|||
private val paramRedirectUrl = "redirectUrl" |
|||
private val paramScope = "scope" |
|||
private val paramSpotifyUri = "spotifyUri" |
|||
private val paramImageUri = "imageUri" |
|||
private val paramImageDimension = "imageDimension" |
|||
private val paramPositionedMilliseconds = "positionedMilliseconds" |
|||
private val paramRelativeMilliseconds = "relativeMilliseconds" |
|||
private val paramPodcastPlaybackSpeed = "podcastPlaybackSpeed" |
|||
private val paramTrackIndex = "trackIndex" |
|||
private val paramRepeatMode = "repeatMode" |
|||
private val paramShuffle = "shuffle" |
|||
|
|||
private val errorConnecting = "errorConnecting" |
|||
private val errorDisconnecting = "errorDisconnecting" |
|||
private val errorConnection = "errorConnection" |
|||
private val errorAuthenticationToken = "authenticationTokenError" |
|||
|
|||
private var connStatusEventChannel: SetEvent<ConnectionStatusChannel.ConnectionEvent> = event() |
|||
|
|||
private val requestCodeAuthentication = 1337 |
|||
|
|||
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 |
|||
|
|||
override fun onAttachedToEngine(binding: FlutterPlugin.FlutterPluginBinding) { |
|||
this.applicationContext = binding.applicationContext |
|||
|
|||
methodChannel = MethodChannel(binding.binaryMessenger, channelName) |
|||
methodChannel.setMethodCallHandler(this) |
|||
|
|||
playerContextChannel = EventChannel(binding.binaryMessenger, playerContextSubscription) |
|||
playerStateChannel = EventChannel(binding.binaryMessenger, playerStateSubscription) |
|||
capabilitiesChannel = EventChannel(binding.binaryMessenger, capabilitiesSubscription) |
|||
userStatusChannel = EventChannel(binding.binaryMessenger, userStatusSubscription) |
|||
connectionStatusChannel = EventChannel(binding.binaryMessenger, connectionStatusSubscription) |
|||
|
|||
connectionStatusChannel?.setStreamHandler(ConnectionStatusChannel(connStatusEventChannel)) |
|||
} |
|||
|
|||
override fun onDetachedFromEngine(binding: FlutterPlugin.FlutterPluginBinding) { |
|||
|
|||
applicationContext = null |
|||
|
|||
methodChannel.setMethodCallHandler(null) |
|||
|
|||
playerContextChannel?.setStreamHandler(null) |
|||
playerStateChannel?.setStreamHandler(null) |
|||
capabilitiesChannel?.setStreamHandler(null) |
|||
userStatusChannel?.setStreamHandler(null) |
|||
connectionStatusChannel?.setStreamHandler(null) |
|||
|
|||
playerContextChannel = null |
|||
playerStateChannel = null |
|||
capabilitiesChannel = null |
|||
userStatusChannel = null |
|||
connectionStatusChannel = null |
|||
} |
|||
|
|||
override fun onAttachedToActivity(binding: ActivityPluginBinding) { |
|||
binding.addActivityResultListener(this) |
|||
applicationActivity = binding.activity |
|||
} |
|||
|
|||
override fun onDetachedFromActivityForConfigChanges() { |
|||
applicationActivity = null |
|||
} |
|||
|
|||
override fun onReattachedToActivityForConfigChanges(binding: ActivityPluginBinding) { |
|||
applicationActivity = binding.activity |
|||
} |
|||
|
|||
override fun onDetachedFromActivity() { |
|||
applicationActivity = null |
|||
} |
|||
|
|||
override fun onMethodCall(call: MethodCall, result: Result) { |
|||
|
|||
if (spotifyAppRemote != null) { |
|||
spotifyPlayerApi = SpotifyPlayerApi(spotifyAppRemote, result) |
|||
spotifyUserApi = SpotifyUserApi(spotifyAppRemote, result) |
|||
spotifyImagesApi = SpotifyImagesApi(spotifyAppRemote, result) |
|||
spotifyConnectApi = SpotifyConnectApi(spotifyAppRemote, result) |
|||
} |
|||
|
|||
when (call.method) { |
|||
//connecting to spotify |
|||
methodConnectToSpotify -> connectToSpotify(call.argument(paramClientId), call.argument(paramRedirectUrl), result) |
|||
methodGetAccessToken -> getAccessToken(call.argument(paramClientId), call.argument(paramRedirectUrl), call.argument(paramScope), result) |
|||
methodDisconnectFromSpotify -> disconnectFromSpotify(result) |
|||
//connectApi calls |
|||
methodSwitchToLocalDevice -> spotifyConnectApi?.switchToLocalDevice() |
|||
//playerApi calls |
|||
methodGetCrossfadeState -> spotifyPlayerApi?.getCrossfadeState() |
|||
methodGetPlayerState -> spotifyPlayerApi?.getPlayerState() |
|||
methodPlay -> spotifyPlayerApi?.play(call.argument(paramSpotifyUri)) |
|||
methodPause -> spotifyPlayerApi?.pause() |
|||
methodQueueTrack -> spotifyPlayerApi?.queue(call.argument(paramSpotifyUri)) |
|||
methodResume -> spotifyPlayerApi?.resume() |
|||
methodSeekTo -> spotifyPlayerApi?.seekTo(call.argument(paramPositionedMilliseconds)) |
|||
methodSeekToRelativePosition -> spotifyPlayerApi?.seekToRelativePosition(call.argument(paramRelativeMilliseconds)) |
|||
methodSetPodcastPlaybackSpeed -> spotifyPlayerApi?.setPodcastPlaybackSpeed(call.argument(paramPodcastPlaybackSpeed)) |
|||
methodSkipNext -> spotifyPlayerApi?.skipNext() |
|||
methodSkipPrevious -> spotifyPlayerApi?.skipPrevious() |
|||
methodSkipToIndex -> spotifyPlayerApi?.skipToIndex(call.argument(paramSpotifyUri), call.argument(paramTrackIndex)) |
|||
methodToggleShuffle -> spotifyPlayerApi?.toggleShuffle() |
|||
methodSetShuffle -> spotifyPlayerApi?.setShuffle(call.argument(paramShuffle)) |
|||
methodToggleRepeat -> spotifyPlayerApi?.toggleRepeat() |
|||
methodSetRepeatMode -> spotifyPlayerApi?.setRepeatMode(call.argument(paramRepeatMode)) |
|||
//userApi calls |
|||
methodAddToLibrary -> spotifyUserApi?.addToUserLibrary(call.argument(paramSpotifyUri)) |
|||
methodRemoveFromLibrary -> spotifyUserApi?.removeFromUserLibrary(call.argument(paramSpotifyUri)) |
|||
methodGetCapabilities -> spotifyUserApi?.getCapabilities() |
|||
methodGetLibraryState -> spotifyUserApi?.getLibraryState(call.argument(paramSpotifyUri)) |
|||
//imageApi calls |
|||
methodGetImage -> spotifyImagesApi?.getImage(call.argument(paramImageUri), call.argument(paramImageDimension)) |
|||
// method call is not implemented yet |
|||
else -> result.notImplemented() |
|||
} |
|||
} |
|||
|
|||
//-- Method implementations |
|||
private fun connectToSpotify(clientId: String?, redirectUrl: String?, result: Result) { |
|||
|
|||
if (clientId.isNullOrBlank() || redirectUrl.isNullOrBlank()) { |
|||
result.error(errorConnecting, "client id or redirectUrl are not set or have invalid format", "") |
|||
} else { |
|||
val connectionParams = ConnectionParams.Builder(clientId) |
|||
.setRedirectUri(redirectUrl) |
|||
.showAuthView(true) |
|||
.build() |
|||
SpotifyAppRemote.disconnect(spotifyAppRemote) |
|||
var initiallyConnected = false |
|||
SpotifyAppRemote.connect(applicationContext, connectionParams, |
|||
object : ConnectionListener { |
|||
override fun onConnected(spotifyAppRemoteValue: SpotifyAppRemote) { |
|||
spotifyAppRemote = spotifyAppRemoteValue |
|||
|
|||
playerContextChannel?.setStreamHandler(PlayerContextChannel(spotifyAppRemote!!.playerApi)) |
|||
Log.i(loggingTag, "Set stream handler for PlayerContextChannel") |
|||
playerStateChannel?.setStreamHandler(PlayerStateChannel(spotifyAppRemote!!.playerApi)) |
|||
Log.i(loggingTag, "Set stream handler for PlayerStateChannel") |
|||
capabilitiesChannel?.setStreamHandler(CapabilitiesChannel(spotifyAppRemote!!.userApi)) |
|||
Log.i(loggingTag, "Set stream handler for CapabilitiesChannel") |
|||
userStatusChannel?.setStreamHandler(UserStatusChannel(spotifyAppRemote!!.userApi)) |
|||
Log.i(loggingTag, "Set stream handler for UserStatusChannel") |
|||
initiallyConnected = true |
|||
|
|||
Log.i(loggingTag, "App Remote successfully connected") |
|||
// emit connection established event |
|||
connStatusEventChannel(ConnectionStatusChannel.ConnectionEvent(true, "Successfully connected to Spotify.", null, null)) |
|||
// method success |
|||
result.success(true) |
|||
} |
|||
|
|||
override fun onFailure(throwable: Throwable) { |
|||
val errorDetails = throwable.toString() |
|||
// determine the error |
|||
val errorMessage: String |
|||
val errorCode: String |
|||
var connected = false |
|||
when (throwable) { |
|||
is SpotifyDisconnectedException, is SpotifyConnectionTerminatedException -> { |
|||
// The Spotify app was/is disconnected by the Spotify app. |
|||
// This indicates typically that the Spotify app was closed by the user or for other reasons. |
|||
// You need to reconnect to continue using Spotify App Remote. |
|||
errorMessage = "The Spotify app was/is disconnected by the Spotify app.Reconnect necessary" |
|||
errorCode = "SpotifyDisconnectedException" |
|||
} |
|||
is CouldNotFindSpotifyApp -> { |
|||
errorMessage = "The Spotify app is not installed on the device" |
|||
errorCode = "CouldNotFindSpotifyApp" |
|||
} |
|||
is AuthenticationFailedException -> { |
|||
errorMessage = "Partner app failed to authenticate with Spotify. Check client credentials and make sure your app is registered correctly at developer.spotify.com" |
|||
errorCode = "AuthenticationFailedException" |
|||
} |
|||
is UserNotAuthorizedException -> { |
|||
errorMessage = "Indicates the user did not authorize this client of App Remote to use Spotify on the users behalf." |
|||
errorCode = "UserNotAuthorizedException" |
|||
} |
|||
is UnsupportedFeatureVersionException -> { |
|||
errorMessage = "Spotify app can't support requested features. User should update Spotify app." |
|||
errorCode = "UnsupportedFeatureVersionException" |
|||
connected = true |
|||
} |
|||
is OfflineModeException -> { |
|||
errorMessage = "Spotify user has set their Spotify app to be in offline mode" |
|||
errorCode = "OfflineModeException" |
|||
connected = true |
|||
} |
|||
is NotLoggedInException -> { |
|||
errorMessage = "User has logged out from Spotify." |
|||
errorCode = "NotLoggedInException" |
|||
} |
|||
is SpotifyRemoteServiceException -> { |
|||
errorMessage = "Encapsulates possible SecurityException and IllegalStateException errors." |
|||
errorCode = "SpotifyRemoteServiceException" |
|||
} |
|||
else -> { |
|||
errorMessage = "Something went wrong connecting spotify remote" |
|||
errorCode = errorConnection |
|||
} |
|||
} |
|||
Log.e(loggingTag, errorMessage) |
|||
// notify plugin |
|||
if (initiallyConnected) { |
|||
// emit connection error event |
|||
connStatusEventChannel(ConnectionStatusChannel.ConnectionEvent(connected, errorMessage, errorCode, errorDetails)) |
|||
} else { |
|||
// throw exception as the connect method |
|||
result.error(errorCode, errorMessage, errorDetails) |
|||
} |
|||
} |
|||
}) |
|||
} |
|||
} |
|||
|
|||
private fun getAccessToken(clientId: String?, redirectUrl: String?, scope: String?, result: Result) { |
|||
if (applicationActivity == null) { |
|||
throw IllegalStateException("getAccessToken needs a foreground activity") |
|||
} |
|||
|
|||
if (clientId.isNullOrBlank() || redirectUrl.isNullOrBlank()) { |
|||
result.error(errorConnecting, "client id or redirectUrl are not set or have invalid format", "") |
|||
} else { |
|||
//Convert String? scope to Array. Delimiter set as comma "," |
|||
val scopeArray = scope?.split(",")?.toTypedArray() |
|||
methodConnectToSpotify.checkAndSetPendingOperation(result) |
|||
|
|||
val builder = AuthorizationRequest.Builder(clientId, AuthorizationResponse.Type.TOKEN, redirectUrl) |
|||
builder.setScopes(scopeArray) |
|||
val request = builder.build() |
|||
|
|||
AuthorizationClient.openLoginActivity(applicationActivity, requestCodeAuthentication, request) |
|||
} |
|||
} |
|||
|
|||
private fun disconnectFromSpotify(result: Result) { |
|||
if (spotifyAppRemote != null && spotifyAppRemote!!.isConnected) { |
|||
SpotifyAppRemote.disconnect(spotifyAppRemote) |
|||
|
|||
// emit connection terminated event |
|||
connStatusEventChannel(ConnectionStatusChannel.ConnectionEvent(false, "Successfully disconnected from Spotify.", null, null)) |
|||
// method success |
|||
result.success(true) |
|||
} else if (!spotifyAppRemote!!.isConnected) { |
|||
result.error(errorDisconnecting, "could not disconnect spotify remote", "you are not connected, no need to disconnect") |
|||
} else { |
|||
result.error(errorDisconnecting, "could not disconnect spotify remote", "spotifyAppRemote is not set") |
|||
} |
|||
} |
|||
|
|||
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?): Boolean { |
|||
if (pendingOperation == null) { |
|||
return false |
|||
} |
|||
return when (requestCode) { |
|||
requestCodeAuthentication -> { |
|||
authFlow(resultCode, data) |
|||
return true |
|||
} |
|||
else -> false |
|||
} |
|||
} |
|||
|
|||
private fun authFlow(resultCode: Int, data: Intent?) { |
|||
|
|||
val response: AuthorizationResponse = AuthorizationClient.getResponse(resultCode, data) |
|||
val result = pendingOperation!!.result |
|||
pendingOperation = null |
|||
|
|||
when (response.type) { |
|||
AuthorizationResponse.Type.TOKEN -> { |
|||
result.success(response.accessToken) |
|||
} |
|||
AuthorizationResponse.Type.ERROR -> result.error(errorAuthenticationToken, "Authentication went wrong", response.error) |
|||
else -> result.notImplemented() |
|||
} |
|||
} |
|||
|
|||
private fun String.checkAndSetPendingOperation(result: Result) { |
|||
|
|||
check(pendingOperation == null) |
|||
{ |
|||
"Concurrent operations detected: " + pendingOperation?.method.toString() + ", " + this |
|||
} |
|||
pendingOperation = PendingOperation(this, result) |
|||
} |
|||
} |
|||
|
|||
private class PendingOperation internal constructor(val method: String, val result: Result) |
|||
@ -0,0 +1,59 @@ |
|||
package de.minimalme.spotify_sdk |
|||
|
|||
import com.google.gson.Gson |
|||
import com.spotify.android.appremote.api.SpotifyAppRemote |
|||
import io.flutter.plugin.common.MethodChannel |
|||
|
|||
class SpotifyUserApi(spotifyAppRemote: SpotifyAppRemote?, result: MethodChannel.Result) : BaseSpotifyApi(spotifyAppRemote, result) { |
|||
|
|||
private val errorAddToLibrary = "addToLibraryError" |
|||
private val errorRemoveFromLibrary = "removeFromLibraryError" |
|||
private val errorGettingCapabilities = "getCapabilitiesError" |
|||
private val errorGettingLibraryState = "getLibraryStateError" |
|||
|
|||
private val userApi = spotifyAppRemote?.userApi |
|||
|
|||
fun addToUserLibrary(spotifyUri: String?) { |
|||
if (userApi != null && !spotifyUri.isNullOrBlank()) { |
|||
userApi.addToLibrary(spotifyUri) |
|||
.setResultCallback {result.success(true)} |
|||
.setErrorCallback { throwable -> result.error(errorAddToLibrary, "error when adding uri to user library", throwable.toString()) } |
|||
} else if (spotifyUri.isNullOrBlank()) { |
|||
result.error(errorAddToLibrary, "spotifyUri has invalid format", "") |
|||
} else { |
|||
spotifyRemoteAppNotSetError() |
|||
} |
|||
} |
|||
|
|||
fun removeFromUserLibrary(spotifyUri: String?){ |
|||
if (userApi != null && !spotifyUri.isNullOrBlank()) { |
|||
userApi.removeFromLibrary(spotifyUri) |
|||
.setResultCallback {result.success(true)} |
|||
.setErrorCallback { throwable -> result.error(errorRemoveFromLibrary, "error when removing uri from user library", throwable.toString()) } |
|||
} else if (spotifyUri.isNullOrBlank()) { |
|||
result.error(errorRemoveFromLibrary, "spotifyUri has invalid format", "") |
|||
} else { |
|||
spotifyRemoteAppNotSetError() |
|||
} |
|||
} |
|||
|
|||
fun getCapabilities(){ |
|||
if (userApi != null) { |
|||
userApi.capabilities |
|||
.setResultCallback {capabilities -> result.success(Gson().toJson(capabilities))} |
|||
.setErrorCallback { throwable -> result.error(errorGettingCapabilities, "error when getting capabilities", throwable.toString()) } |
|||
} else { |
|||
spotifyRemoteAppNotSetError() |
|||
} |
|||
} |
|||
|
|||
fun getLibraryState(spotifyUri: String?){ |
|||
if (userApi != null && !spotifyUri.isNullOrBlank()) { |
|||
userApi.getLibraryState(spotifyUri) |
|||
.setResultCallback {libraryState -> result.success(Gson().toJson(libraryState))} |
|||
.setErrorCallback { throwable -> result.error(errorGettingLibraryState, "error when getting the library state", throwable.toString()) } |
|||
} else { |
|||
spotifyRemoteAppNotSetError() |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,23 @@ |
|||
package de.minimalme.spotify_sdk.subscriptions |
|||
|
|||
import com.google.gson.Gson |
|||
import com.spotify.android.appremote.api.PlayerApi |
|||
import com.spotify.android.appremote.api.UserApi |
|||
import io.flutter.plugin.common.EventChannel |
|||
|
|||
class CapabilitiesChannel(private val userApi: UserApi) : EventChannel.StreamHandler { |
|||
|
|||
private val errorSubscribeCapabilities = "subscribeCapabilitiesError" |
|||
override fun onListen(arguments: Any?, events: EventChannel.EventSink?) { |
|||
userApi.subscribeToCapabilities() |
|||
.setEventCallback { |
|||
capabilities -> events?.success(Gson().toJson(capabilities)) |
|||
} |
|||
.setErrorCallback { |
|||
throwable -> events?.error(errorSubscribeCapabilities, "error when subscribing to the users capabilities", throwable.toString()) |
|||
} |
|||
} |
|||
|
|||
override fun onCancel(arguments: Any?) { |
|||
} |
|||
} |
|||
@ -0,0 +1,27 @@ |
|||
package de.minimalme.spotify_sdk.subscriptions |
|||
|
|||
import android.util.Log |
|||
import com.google.gson.Gson |
|||
import com.google.gson.annotations.SerializedName |
|||
import io.flutter.plugin.common.EventChannel |
|||
import kotlinx.event.SetEvent |
|||
|
|||
class ConnectionStatusChannel(private val connStatusStream: SetEvent<ConnectionEvent>) : EventChannel.StreamHandler { |
|||
|
|||
data class ConnectionEvent( |
|||
@SerializedName("connected") val connected: Boolean, |
|||
@SerializedName("message" )val message: String, |
|||
@SerializedName("errorCode") val errorCode: String?, |
|||
@SerializedName("errorDetails") val errorDetails: Any? |
|||
) |
|||
|
|||
override fun onListen(arguments: Any?, events: EventChannel.EventSink?) { |
|||
connStatusStream += { event -> |
|||
// not error |
|||
events?.success(Gson().toJson(event)) |
|||
} |
|||
} |
|||
|
|||
override fun onCancel(arguments: Any?) { |
|||
} |
|||
} |
|||
@ -0,0 +1,22 @@ |
|||
package de.minimalme.spotify_sdk.subscriptions |
|||
|
|||
import com.google.gson.Gson |
|||
import com.spotify.android.appremote.api.PlayerApi |
|||
import io.flutter.plugin.common.EventChannel |
|||
|
|||
class PlayerContextChannel(private val playerApi: PlayerApi) : EventChannel.StreamHandler { |
|||
|
|||
private val errorSubscribePlayerContext = "subscribePlayerContextError" |
|||
override fun onListen(arguments: Any?, events: EventChannel.EventSink?) { |
|||
playerApi.subscribeToPlayerContext() |
|||
.setEventCallback { |
|||
playerContext -> events?.success(Gson().toJson(playerContext)) |
|||
} |
|||
.setErrorCallback { |
|||
throwable -> events?.error(errorSubscribePlayerContext, "error when subscribing to the player context", throwable.toString()) |
|||
} |
|||
} |
|||
|
|||
override fun onCancel(arguments: Any?) { |
|||
} |
|||
} |
|||
@ -0,0 +1,22 @@ |
|||
package de.minimalme.spotify_sdk.subscriptions |
|||
|
|||
import com.google.gson.Gson |
|||
import com.spotify.android.appremote.api.PlayerApi |
|||
import io.flutter.plugin.common.EventChannel |
|||
|
|||
class PlayerStateChannel(private val playerApi: PlayerApi) : EventChannel.StreamHandler { |
|||
|
|||
private val errorSubscribePlayerState = "subscribePlayerStateError" |
|||
override fun onListen(arguments: Any?, events: EventChannel.EventSink?) { |
|||
playerApi.subscribeToPlayerState() |
|||
.setEventCallback { |
|||
playerState -> events?.success(Gson().toJson(playerState)) |
|||
} |
|||
.setErrorCallback { |
|||
throwable -> events?.error(errorSubscribePlayerState, "error when subscribing to the player state", throwable.toString()) |
|||
} |
|||
} |
|||
|
|||
override fun onCancel(arguments: Any?) { |
|||
} |
|||
} |
|||
@ -0,0 +1,23 @@ |
|||
package de.minimalme.spotify_sdk.subscriptions |
|||
|
|||
import com.google.gson.Gson |
|||
import com.spotify.android.appremote.api.PlayerApi |
|||
import com.spotify.android.appremote.api.UserApi |
|||
import io.flutter.plugin.common.EventChannel |
|||
|
|||
class UserStatusChannel(private val userApi: UserApi) : EventChannel.StreamHandler { |
|||
|
|||
private val errorSubscribeUserStatus = "subscribeUserStatusError" |
|||
override fun onListen(arguments: Any?, events: EventChannel.EventSink?) { |
|||
userApi.subscribeToUserStatus() |
|||
.setEventCallback { |
|||
userStatus -> events?.success(Gson().toJson(userStatus)) |
|||
} |
|||
.setErrorCallback { |
|||
throwable -> events?.error(errorSubscribeUserStatus, "error when subscribing to the users status", throwable.toString()) |
|||
} |
|||
} |
|||
|
|||
override fun onCancel(arguments: Any?) { |
|||
} |
|||
} |
|||
@ -0,0 +1,39 @@ |
|||
import 'dart:io'; |
|||
|
|||
import 'android_setup.dart'; |
|||
|
|||
/// Removes all changes made by the [android_setup] script. |
|||
Future<void> main(List<String> args) async { |
|||
logger.i('running android_cleanup script'); |
|||
|
|||
// remove the module directory if it exists |
|||
final moduleDir = Directory('android/$moduleName'); |
|||
if (moduleDir.existsSync()) { |
|||
await moduleDir.delete(recursive: true); |
|||
logger.t('deleted directory ${moduleDir.path}'); |
|||
} |
|||
|
|||
// remove the include statement from settings.gradle |
|||
final settingsFile = await File('android/settings.gradle').readAsString(); |
|||
const includeStatements = [ |
|||
"include ':$moduleName'", |
|||
'include ":$moduleName"' |
|||
]; |
|||
if (includeStatements.any((element) => settingsFile.contains(element))) { |
|||
final newSettingsFile = includeStatements.fold(settingsFile, |
|||
(previousValue, element) => previousValue.replaceAll(element, '')); |
|||
await File('android/settings.gradle').writeAsString(newSettingsFile); |
|||
logger.t('removed include statement from settings.gradle'); |
|||
} |
|||
|
|||
// remove the manifestPlaceholder from app/build.gradle |
|||
final appBuildFile = await File('android/app/build.gradle').readAsString(); |
|||
if (appBuildFile.contains('manifestPlaceholders')) { |
|||
final newAppBuildFile = appBuildFile |
|||
.split('\n') |
|||
.where((element) => !element.contains('manifestPlaceholders')) |
|||
.join('\n'); |
|||
await File('android/app/build.gradle').writeAsString(newAppBuildFile); |
|||
logger.t('removed manifestPlaceholders from app/build.gradle'); |
|||
} |
|||
} |
|||
@ -0,0 +1,96 @@ |
|||
import 'dart:io'; |
|||
|
|||
import 'package:logger/logger.dart'; |
|||
|
|||
import 'android_setup.dart'; |
|||
|
|||
/// Responsible for creating a new android module in the /android directory |
|||
/// of the flutter project. |
|||
class AndroidModuleCreator { |
|||
String moduleName; |
|||
String aarFileName; |
|||
|
|||
AndroidModuleCreator(this.moduleName, this.aarFileName); |
|||
|
|||
String _gradleFileContent(String aarFileName) => ''' |
|||
configurations.maybeCreate("default") |
|||
artifacts.add("default", file('$aarFileName')) |
|||
'''; |
|||
|
|||
String _readmeContent(String moduleName, String aarFileName) => ''' |
|||
# Spotify Android SDK for Flutter |
|||
This module (`:$moduleName`) was generated by running the `android_setup` script of |
|||
the [spotify_sdk](https://pub.dev/packages/spotify_sdk) plugin. |
|||
|
|||
If you want to update the `aar`-library, please run the `android_setup` script using |
|||
one of the following commands: |
|||
``` |
|||
dart run spotify_sdk:android_setup --cleanup |
|||
flutter pub run spotify_sdk:android_setup --cleanup |
|||
``` |
|||
'''; |
|||
|
|||
Future<void> createModuleDirectory() async { |
|||
// create directory in /android |
|||
final aarDir = await Directory('android/$moduleName').create(); |
|||
logger.t('created new directory ${aarDir.path}'); |
|||
|
|||
// create build.gradle file |
|||
final gradleFile = await File('${aarDir.path}/build.gradle').create(); |
|||
logger.t('created new file ${gradleFile.path}'); |
|||
await gradleFile.writeAsString(_gradleFileContent(aarFileName)); |
|||
|
|||
_settingsGradle(logger); |
|||
|
|||
_appBuildGradle(logger); |
|||
|
|||
// create README.md file |
|||
final readmeFile = await File('${aarDir.path}/README.md').create(); |
|||
logger.t('created new file ${readmeFile.path}'); |
|||
await readmeFile.writeAsString(_readmeContent(moduleName, aarFileName)); |
|||
} |
|||
|
|||
void _settingsGradle(Logger logger) async { |
|||
// edit settings.gradle file |
|||
final settingsFile = await File('android/settings.gradle').readAsString(); |
|||
final includeStatement = "include ':$moduleName'"; |
|||
if (settingsFile.contains(includeStatement)) { |
|||
logger.t('settings.gradle already contains $includeStatement'); |
|||
} else { |
|||
final String newSettingsFile; |
|||
if (settingsFile.contains("include ':app'")) { |
|||
newSettingsFile = settingsFile.replaceFirst( |
|||
'include \':app\'', 'include \':app\'\ninclude \':$moduleName\''); |
|||
} else { |
|||
newSettingsFile = settingsFile.replaceFirst( |
|||
'include ":app"', 'include ":app"\ninclude ":$moduleName"'); |
|||
} |
|||
await File('android/settings.gradle').writeAsString(newSettingsFile); |
|||
logger.t('added "$includeStatement" to android/settings.gradle'); |
|||
} |
|||
} |
|||
|
|||
void _appBuildGradle(Logger logger) async { |
|||
// edit app/build.gradle file |
|||
final appBuildFile = await File('android/app/build.gradle').readAsString(); |
|||
if (!appBuildFile.contains('defaultConfig')) { |
|||
logger.e( |
|||
'Error: The file "android/app/build.gradle" does not contain a defaultConfig block. ' |
|||
'Your android project might be misconfigured.'); |
|||
return; |
|||
} |
|||
if (appBuildFile.contains('manifestPlaceholders')) { |
|||
logger.w( |
|||
'android/app/build.gradle already contains manifestPlaceholders. ' |
|||
'Make sure to update the redirect scheme and host name manually if necessary.'); |
|||
return; |
|||
} |
|||
|
|||
final String newAppBuildFile = appBuildFile.replaceFirst( |
|||
'defaultConfig {', |
|||
'defaultConfig {\n' |
|||
' manifestPlaceholders = [redirectSchemeName: "spotify-sdk", redirectHostName: "auth"]'); |
|||
await File('android/app/build.gradle').writeAsString(newAppBuildFile); |
|||
logger.t('added manifestPlaceholders to android/app/build.gradle'); |
|||
} |
|||
} |
|||
@ -0,0 +1,91 @@ |
|||
import 'dart:io'; |
|||
|
|||
import 'package:logger/logger.dart'; |
|||
|
|||
import 'android_module_creator.dart'; |
|||
import 'github_api.dart'; |
|||
import 'precondition_checker.dart'; |
|||
|
|||
import 'android_cleanup.dart' as cleanup; |
|||
|
|||
const String scriptName = 'android_setup'; |
|||
const String moduleName = 'spotify-app-remote'; |
|||
|
|||
final logger = Logger(filter: ScriptLogFilter(), printer: SimplePrinter()); |
|||
Level loglevel = Level.info; |
|||
|
|||
Future<void> main(List<String> args) async { |
|||
if (args.contains('--help')) { |
|||
logger.i(''' |
|||
Usage: $scriptName [options] |
|||
Options: |
|||
--help: show this help message |
|||
--verbose: show all logs |
|||
--cleanup: runs the cleanup script before executing the setup script to remove all previously created changes |
|||
--sdk-version: the version of the Spotify Android SDK, default is the latest release on GitHub (eg. --sdk-version=0.8.0) |
|||
'''); |
|||
return; |
|||
} |
|||
|
|||
if (args.contains('--verbose')) { |
|||
loglevel = Level.trace; |
|||
logger.t('verbose logging enabled'); |
|||
} |
|||
|
|||
if (args.contains('--cleanup')) { |
|||
logger.t('running cleanup script'); |
|||
await cleanup.main(args); |
|||
} |
|||
|
|||
if (!PreconditionChecker.setupConditionsMet()) { |
|||
logger.e('$scriptName can not be executed, ' |
|||
'please make sure to meet all requirements and try again.'); |
|||
} else { |
|||
logger.i('running $scriptName script'); |
|||
String? sdkVersion = args.cast<String?>().firstWhere( |
|||
(element) => element?.startsWith('--sdk-version=') ?? false, |
|||
orElse: () => null); |
|||
_runSetup(sdkVersion: sdkVersion); |
|||
} |
|||
} |
|||
|
|||
/// Runs the setup process. |
|||
void _runSetup({String? sdkVersion}) async { |
|||
Uri url; |
|||
String name; |
|||
try { |
|||
if (sdkVersion == null) { |
|||
(name, url) = |
|||
await GitHubApi.fetchLatestAppRemoteReleaseAssetDownloadUrl(); |
|||
} else { |
|||
(name, url) = |
|||
await GitHubApi.fetchVersionedAppRemoteReleaseAssetDownloadUrl( |
|||
sdkVersion); |
|||
} |
|||
} catch (e) { |
|||
logger.e('Failed to fetch the Spotify Android SDK asset, terminating.'); |
|||
return; |
|||
} |
|||
|
|||
// create the new module directory |
|||
final destination = File('android/$moduleName/$name') |
|||
..createSync(recursive: true); |
|||
logger.t('created new file ${destination.path}'); |
|||
|
|||
// download the aar file to the new destination module |
|||
final client = HttpClient(); |
|||
final request = await client.getUrl(url); |
|||
final response = await request.close(); |
|||
await response.pipe(destination.openWrite()); |
|||
client.close(); |
|||
logger.t('downloaded $name to ${destination.path}'); |
|||
|
|||
// create the new module |
|||
await AndroidModuleCreator(moduleName, name).createModuleDirectory(); |
|||
} |
|||
|
|||
/// Log filter to show all logs when running the script in release mode. |
|||
class ScriptLogFilter extends LogFilter { |
|||
@override |
|||
bool shouldLog(LogEvent event) => event.level.index >= loglevel.index; |
|||
} |
|||
@ -0,0 +1,100 @@ |
|||
import 'dart:convert'; |
|||
|
|||
import 'package:http/http.dart' as http; |
|||
|
|||
import 'android_setup.dart'; |
|||
|
|||
/// [fetchLatestAppRemoteReleaseAssetDownloadUrl] fetches the latest release of the |
|||
/// Spotify Android SDK from GitHub API and returns the name and download url of |
|||
/// the spotify-app-remote-release-*.aar asset. |
|||
/// Throws an exception if the request fails. |
|||
class GitHubApi { |
|||
static const String apiUrl = 'https://api.github.com/repos'; |
|||
static const String spotifyAndroidSdkRepo = '/spotify/android-sdk'; |
|||
static const String latestRelease = '/releases/latest'; |
|||
static const String allReleases = '/releases'; |
|||
|
|||
static Future<(String, Uri)> |
|||
fetchLatestAppRemoteReleaseAssetDownloadUrl() async { |
|||
// fetch the github api to get the latest release |
|||
Uri uri = Uri.parse(apiUrl + spotifyAndroidSdkRepo + latestRelease); |
|||
try { |
|||
final response = await http.get(uri); |
|||
if (response.statusCode == 200) { |
|||
final data = jsonDecode(response.body); |
|||
return _findAsset(data); |
|||
} else { |
|||
logger.e('Failed to fetch data from the API.'); |
|||
} |
|||
} catch (e) { |
|||
logger.e('An error occurred: $e'); |
|||
} |
|||
throw Exception(); |
|||
} |
|||
|
|||
static Future<(String, Uri)> fetchVersionedAppRemoteReleaseAssetDownloadUrl( |
|||
String sdkVersion) async { |
|||
if (sdkVersion.startsWith('--sdk-version=')) { |
|||
sdkVersion = sdkVersion.substring(14); |
|||
} |
|||
if (sdkVersion.startsWith('v')) { |
|||
sdkVersion = sdkVersion.substring(1); |
|||
} |
|||
|
|||
// fetch the github api to get all releases |
|||
Uri uri = Uri.parse(apiUrl + spotifyAndroidSdkRepo + allReleases); |
|||
try { |
|||
final response = await http.get(uri); |
|||
if (response.statusCode == 200) { |
|||
final data = jsonDecode(response.body) as List<dynamic>; |
|||
|
|||
logger.t( |
|||
'Found ${data.length} releases of the Spotify Android SDK on GitHub.'); |
|||
final releaseData = data.firstWhere( |
|||
(element) { |
|||
final tagName = element['tag_name'] as String; |
|||
return tagName.contains('v$sdkVersion'); |
|||
}, |
|||
orElse: () { |
|||
logger.e('Failed to find a release with version "$sdkVersion".'); |
|||
throw Exception(); |
|||
}, |
|||
); |
|||
|
|||
return _findAsset(releaseData); |
|||
} |
|||
} catch (e) { |
|||
logger.e('An error occurred: $e'); |
|||
} |
|||
throw Exception(); |
|||
} |
|||
|
|||
static (String, Uri) _findAsset(Map<String, dynamic> data) { |
|||
final assets = data['assets'] as List<dynamic>; |
|||
|
|||
final assetMap = <int, (String, String)>{}; |
|||
for (var asset in assets) { |
|||
final id = asset['id'] as int; |
|||
final name = asset['name'] as String; |
|||
final url = asset['browser_download_url'] as String; |
|||
assetMap[id] = (name, url); |
|||
} |
|||
|
|||
// find the spotify-app-remote-release-*.aar asset |
|||
final assetId = assetMap.keys.firstWhere( |
|||
(id) => |
|||
assetMap[id]?.$1.startsWith('spotify-app-remote-release-') ?? false, |
|||
orElse: () => -1, |
|||
); |
|||
|
|||
if (assetId == -1) { |
|||
logger.e('Failed to find the Spotify Android SDK asset.'); |
|||
throw Exception(); |
|||
} |
|||
|
|||
// return the download url of the spotify-app-remote-release-*.aar asset |
|||
logger.i('Found the Spotify Android SDK asset: ' |
|||
'${assetMap[assetId]!.$1}'); |
|||
return (assetMap[assetId]!.$1, Uri.parse(assetMap[assetId]!.$2)); |
|||
} |
|||
} |
|||
@ -0,0 +1,52 @@ |
|||
import 'dart:io'; |
|||
|
|||
import 'android_setup.dart'; |
|||
|
|||
/// Checks if all preconditions are met to execute the script. |
|||
/// Returns true if all preconditions are met, false otherwise. |
|||
class PreconditionChecker { |
|||
static bool setupConditionsMet() { |
|||
// check if the script is executed on a supported platform |
|||
if (!Platform.isMacOS && !Platform.isLinux) { |
|||
logger.w( |
|||
'Warning: This script has not been tested on your platform (${Platform.operatingSystem}).'); |
|||
} |
|||
|
|||
// check if flutter is installed |
|||
if (!(Platform.environment['PATH']?.contains("flutter") ?? false) && |
|||
Platform.environment['FLUTTER_ROOT'] == null) { |
|||
logger.e('Error: Flutter is not installed or not in your PATH.'); |
|||
return false; |
|||
} |
|||
|
|||
// check if the script is executed from inside a flutter project |
|||
if (!File('pubspec.yaml').existsSync()) { |
|||
logger.e( |
|||
'Error: The script must be executed from inside a flutter project.'); |
|||
return false; |
|||
} |
|||
|
|||
// check if the necessary android files exist |
|||
if (!File('android/app/build.gradle').existsSync()) { |
|||
logger.e('Error: The file "android/app/build.gradle" does not exist.'); |
|||
return false; |
|||
} |
|||
|
|||
// check if the setup may have already been executed and recommend to run the cleanup script |
|||
bool prevRun = Directory('android/$moduleName').existsSync() || |
|||
File('android/$moduleName/build.gradle').existsSync(); |
|||
if (!prevRun && File('android/settings.gradle').existsSync()) { |
|||
final settingsFile = File('android/settings.gradle').readAsStringSync(); |
|||
prevRun |= settingsFile.contains("include ':$moduleName'"); |
|||
prevRun |= settingsFile.contains('include ":$moduleName"'); |
|||
} |
|||
|
|||
if (prevRun) { |
|||
logger.w('Warning: The setup may have already been executed. ' |
|||
'Please run dart run spotify_sdk:android_setup --cleanup before running this script again.'); |
|||
return false; |
|||
} |
|||
|
|||
return true; |
|||
} |
|||
} |
|||
@ -0,0 +1,37 @@ |
|||
.idea/ |
|||
.vagrant/ |
|||
.sconsign.dblite |
|||
.svn/ |
|||
|
|||
.DS_Store |
|||
*.swp |
|||
profile |
|||
|
|||
DerivedData/ |
|||
build/ |
|||
GeneratedPluginRegistrant.h |
|||
GeneratedPluginRegistrant.m |
|||
|
|||
.generated/ |
|||
|
|||
*.pbxuser |
|||
*.mode1v3 |
|||
*.mode2v3 |
|||
*.perspectivev3 |
|||
|
|||
!default.pbxuser |
|||
!default.mode1v3 |
|||
!default.mode2v3 |
|||
!default.perspectivev3 |
|||
|
|||
xcuserdata |
|||
|
|||
*.moved-aside |
|||
|
|||
*.pyc |
|||
*sync/ |
|||
Icon? |
|||
.tags* |
|||
|
|||
/Flutter/Generated.xcconfig |
|||
/Flutter/flutter_export_environment.sh |
|||
@ -0,0 +1,45 @@ |
|||
import SpotifyiOS |
|||
|
|||
class ConnectionStatusHandler: StatusHandler, SPTAppRemoteDelegate { |
|||
|
|||
var tokenResult: FlutterResult? |
|||
var connectionResult: FlutterResult? |
|||
|
|||
func appRemoteDidEstablishConnection(_ appRemote: SPTAppRemote) { |
|||
connectionResult?(true) |
|||
tokenResult?(appRemote.connectionParameters.accessToken) |
|||
eventSink?("{\"connected\": true}") |
|||
|
|||
connectionResult = nil |
|||
tokenResult = nil |
|||
} |
|||
|
|||
func appRemote(_ appRemote: SPTAppRemote, didFailConnectionAttemptWithError error: Error?) { |
|||
defer { |
|||
connectionResult = nil |
|||
tokenResult = nil |
|||
} |
|||
|
|||
if error != nil { |
|||
// report spotify remote error to plugin |
|||
eventSink?("{\"connected\": false, \"errorCode\": \"\(error!._code)\", \"errorDetails\": \"\(error!.localizedDescription)\"}") |
|||
connectionResult?(FlutterError(code: String(error!._code), message: error!.localizedDescription, details: nil)) |
|||
tokenResult?(FlutterError(code: String(error!._code), message: error!.localizedDescription, details: nil)) |
|||
} else { |
|||
// report disconnection to plugin |
|||
eventSink?("{\"connected\": false}") |
|||
connectionResult?(FlutterError(code: "errorConnection", message: "Failed Connection Attempt", details: nil)) |
|||
tokenResult?(FlutterError(code: "errorConnection", message: "Failed Connection Attempt", details: nil)) |
|||
} |
|||
} |
|||
|
|||
func appRemote(_ appRemote: SPTAppRemote, didDisconnectWithError error: Error?) { |
|||
if error != nil { |
|||
// report spotify remote error to plugin |
|||
eventSink?("{\"connected\": false, \"errorCode\": \"\(error!._code)\", \"errorDetails\": \"\(error!.localizedDescription)\"}") |
|||
} else { |
|||
// report disconnection to plugin |
|||
eventSink?("{\"connected\": false}") |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,13 @@ |
|||
import Foundation |
|||
|
|||
extension Dictionary { |
|||
var json: String { |
|||
let invalidJson = "Invalid JSON" |
|||
do { |
|||
let jsonData = try JSONSerialization.data(withJSONObject: self, options: .prettyPrinted) |
|||
return String(bytes: jsonData, encoding: String.Encoding.utf8) ?? invalidJson |
|||
} catch { |
|||
return invalidJson |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,14 @@ |
|||
import SpotifyiOS |
|||
import Flutter |
|||
|
|||
class PlayerDelegate: NSObject, SPTAppRemotePlayerStateDelegate { |
|||
|
|||
var playerStateSink: FlutterEventSink? |
|||
var playerContextSink: FlutterEventSink? |
|||
|
|||
func playerStateDidChange(_ playerState: SPTAppRemotePlayerState) { |
|||
print("playerStateDidChange") |
|||
playerStateSink?(State.playerStateDictionary(playerState).json) |
|||
playerContextSink?(State.playerContextDictionary(playerState).json) |
|||
} |
|||
} |
|||
@ -0,0 +1,18 @@ |
|||
import SpotifyiOS |
|||
|
|||
class PlayerContextHandler: StatusHandler { |
|||
private let appRemote: SPTAppRemote |
|||
private let playerDelegate: PlayerDelegate |
|||
|
|||
init (appRemote: SPTAppRemote, playerDelegate: PlayerDelegate) { |
|||
self.appRemote = appRemote |
|||
self.playerDelegate = playerDelegate |
|||
super.init() |
|||
} |
|||
|
|||
override func onListen(withArguments arguments: Any?, eventSink events: @escaping FlutterEventSink) -> FlutterError? { |
|||
_ = super.onListen(withArguments: arguments, eventSink: events) |
|||
playerDelegate.playerContextSink = events |
|||
return nil |
|||
} |
|||
} |
|||
@ -0,0 +1,85 @@ |
|||
import SpotifyiOS |
|||
|
|||
struct State { |
|||
static func playerStateDictionary(_ playerState: SPTAppRemotePlayerState) -> [String : Any] { |
|||
let artist = [ |
|||
"name" : playerState.track.artist.name, |
|||
"uri" : playerState.track.artist.uri |
|||
] |
|||
|
|||
let album = [ |
|||
"name" : playerState.track.album.name, |
|||
"uri" : playerState.track.album.uri |
|||
] |
|||
|
|||
let imageIdentifier = [ |
|||
"raw" : playerState.track.imageIdentifier |
|||
|
|||
] |
|||
|
|||
let track: [String : Any] = [ |
|||
"album" : album, |
|||
"artist" : artist, |
|||
"artists" : [artist], |
|||
"duration_ms" : playerState.track.duration, |
|||
"image_id" : imageIdentifier, |
|||
"is_episode" : playerState.track.isEpisode, |
|||
"is_podcast" : playerState.track.isPodcast, |
|||
"name" : playerState.track.name, |
|||
"uri" : playerState.track.uri |
|||
] |
|||
|
|||
let playbackOptions: [String : Any] = [ |
|||
"shuffle" : playerState.playbackOptions.isShuffling, |
|||
"repeat" : playerState.playbackOptions.repeatMode.rawValue |
|||
] |
|||
|
|||
let playbackRestrictions = [ |
|||
"can_skip_next" : playerState.playbackRestrictions.canSkipNext, |
|||
"can_skip_prev" : playerState.playbackRestrictions.canSkipPrevious, |
|||
"can_repeat_track" : playerState.playbackRestrictions.canRepeatTrack, |
|||
"can_repeat_context" : playerState.playbackRestrictions.canRepeatContext, |
|||
"can_toggle_shuffle" : playerState.playbackRestrictions.canToggleShuffle, |
|||
"can_seek" : playerState.playbackRestrictions.canSeek |
|||
] |
|||
|
|||
return [ |
|||
"track" : track, |
|||
"is_paused" : playerState.isPaused, |
|||
"playback_speed" : playerState.playbackSpeed, |
|||
"playback_position" : playerState.playbackPosition, |
|||
"playback_options" : playbackOptions, |
|||
"playback_restrictions" : playbackRestrictions |
|||
] |
|||
} |
|||
|
|||
static func playerContextDictionary(_ playerState: SPTAppRemotePlayerState) -> [String : Any] { |
|||
return [ |
|||
"title" : playerState.contextTitle, |
|||
"subtitle" : playerState.contextTitle, |
|||
"type" : playerState.contextURI.absoluteString.components(separatedBy: ":")[1], |
|||
"uri" : playerState.contextURI.absoluteString |
|||
] |
|||
} |
|||
|
|||
static func crossfadeStateDictionary(_ crossfadeState: SPTAppRemoteCrossfadeState) -> [String : Any] { |
|||
return [ |
|||
"duration": crossfadeState.duration, |
|||
"isEnabled": crossfadeState.isEnabled, |
|||
] |
|||
} |
|||
|
|||
static func userCapabilitiesDictionary(_ userCapabilities: SPTAppRemoteUserCapabilities) -> [String : Any] { |
|||
return [ |
|||
"can_play_on_demand": userCapabilities.canPlayOnDemand |
|||
] |
|||
} |
|||
|
|||
static func libraryStateDictionary(_ libraryState: SPTAppRemoteLibraryState) -> [String : Any] { |
|||
return [ |
|||
"uri": libraryState.uri, |
|||
"saved": libraryState.isAdded, |
|||
"can_save": libraryState.canAdd, |
|||
] |
|||
} |
|||
} |
|||
@ -0,0 +1,22 @@ |
|||
import SpotifyiOS |
|||
|
|||
class PlayerStateHandler: StatusHandler { |
|||
private let appRemote: SPTAppRemote |
|||
private let playerDelegate: PlayerDelegate |
|||
|
|||
init (appRemote: SPTAppRemote, playerDelegate: PlayerDelegate) { |
|||
self.appRemote = appRemote |
|||
self.playerDelegate = playerDelegate |
|||
super.init() |
|||
} |
|||
|
|||
override func onListen(withArguments arguments: Any?, eventSink events: @escaping FlutterEventSink) -> FlutterError? { |
|||
_ = super.onListen(withArguments: arguments, eventSink: events) |
|||
playerDelegate.playerStateSink = events |
|||
appRemote.playerAPI?.delegate = playerDelegate |
|||
appRemote.playerAPI?.subscribe { (_, error) -> Void in |
|||
guard error == nil else { return } |
|||
} |
|||
return nil |
|||
} |
|||
} |
|||
@ -0,0 +1,4 @@ |
|||
enum SpotifyError: Error { |
|||
case spotifyNotInstalledError |
|||
case redirectURLInvalid |
|||
} |
|||
@ -0,0 +1,46 @@ |
|||
import Foundation |
|||
|
|||
public class SpotifySdkConstants |
|||
{ |
|||
//connecting |
|||
public static let methodConnectToSpotify = "connectToSpotify" |
|||
public static let methodGetAccessToken = "getAccessToken" |
|||
public static let methodDisconnectFromSpotify = "disconnectFromSpotify" |
|||
|
|||
//player api |
|||
public static let methodQueueTrack = "queueTrack" |
|||
public static let methodPlay = "play" |
|||
public static let methodPause = "pause" |
|||
public static let methodResume = "resume" |
|||
public static let methodSkipNext = "skipNext" |
|||
public static let methodSkipPrevious = "skipPrevious" |
|||
public static let methodSkipToIndex = "skipToIndex" |
|||
public static let methodSeekTo = "seekTo" |
|||
public static let methodGetPlayerState = "getPlayerState" |
|||
public static let methodGetCrossfadeState = "getCrossfadeState" |
|||
public static let methodSetShuffle = "setShuffle" |
|||
public static let methodSetRepeatMode = "setRepeatMode" |
|||
|
|||
//user api |
|||
public static let methodAddToLibrary = "addToLibrary" |
|||
public static let methodRemoveFromLibrary = "removeFromLibrary" |
|||
public static let methodGetCapabilities = "getCapabilities" |
|||
|
|||
//images api |
|||
public static let methodGetImage = "getImage" |
|||
|
|||
public static let paramClientId = "clientId" |
|||
public static let paramRedirectUrl = "redirectUrl" |
|||
public static let paramSpotifyUri = "spotifyUri" |
|||
public static let paramAsRadio = "asRadio" |
|||
public static let paramImageUri = "imageUri" |
|||
public static let paramImageDimension = "imageDimension" |
|||
public static let paramPositionedMilliseconds = "positionedMilliseconds" |
|||
public static let paramRelativeMilliseconds = "relativeMilliseconds" |
|||
public static let paramAccessToken = "accessToken" |
|||
public static let paramShuffle = "shuffle" |
|||
public static let paramRepeatMode = "repeatMode" |
|||
public static let paramTrackIndex = "trackIndex" |
|||
public static let scope = "scope" |
|||
public static let getLibraryState = "getLibraryState" |
|||
} |
|||
@ -0,0 +1,4 @@ |
|||
#import <Flutter/Flutter.h> |
|||
|
|||
@interface SpotifySdkPlugin : NSObject <FlutterPlugin> |
|||
@end |
|||
@ -0,0 +1,18 @@ |
|||
#import "SpotifySdkPlugin.h" |
|||
#if __has_include(<spotify_sdk/spotify_sdk-Swift.h>) |
|||
#import <spotify_sdk/spotify_sdk-Swift.h> |
|||
#else |
|||
// Support project import fallback if the generated compatibility header |
|||
// is not copied when this plugin is created as a library. |
|||
// https://forums.swift.org/t/swift-static-libraries-dont-copy-generated-objective-c-header/19816 |
|||
#import "spotify_sdk-Swift.h" |
|||
#endif |
|||
|
|||
#import <SpotifyiOS/SpotifyiOS.h> |
|||
|
|||
@implementation SpotifySdkPlugin |
|||
+ (void)registerWithRegistrar:(NSObject<FlutterPluginRegistrar>*)registrar { |
|||
[SwiftSpotifySdkPlugin registerWithRegistrar:registrar]; |
|||
|
|||
} |
|||
@end |
|||
@ -0,0 +1,17 @@ |
|||
import Flutter |
|||
import UIKit |
|||
import SpotifyiOS |
|||
|
|||
class StatusHandler: NSObject, FlutterStreamHandler { |
|||
var eventSink: FlutterEventSink? |
|||
|
|||
func onListen(withArguments arguments: Any?, eventSink events: @escaping FlutterEventSink) -> FlutterError? { |
|||
eventSink = events |
|||
return nil |
|||
} |
|||
|
|||
func onCancel(withArguments arguments: Any?) -> FlutterError? { |
|||
eventSink = nil |
|||
return nil |
|||
} |
|||
} |
|||
@ -0,0 +1,416 @@ |
|||
import Flutter |
|||
import SpotifyiOS |
|||
|
|||
public class SwiftSpotifySdkPlugin: NSObject, FlutterPlugin { |
|||
private static var instance = SwiftSpotifySdkPlugin() |
|||
private var appRemote: SPTAppRemote? |
|||
private var connectionStatusHandler: ConnectionStatusHandler? |
|||
private var playerStateHandler: PlayerStateHandler? |
|||
private var playerContextHandler: PlayerContextHandler? |
|||
private static var playerStateChannel: FlutterEventChannel? |
|||
private static var playerContextChannel: FlutterEventChannel? |
|||
|
|||
public static func register(with registrar: FlutterPluginRegistrar) { |
|||
guard playerStateChannel == nil else { |
|||
// Avoid multiple plugin registations |
|||
return |
|||
} |
|||
let spotifySDKChannel = FlutterMethodChannel(name: "spotify_sdk", binaryMessenger: registrar.messenger()) |
|||
let connectionStatusChannel = FlutterEventChannel(name: "connection_status_subscription", binaryMessenger: registrar.messenger()) |
|||
playerStateChannel = FlutterEventChannel(name: "player_state_subscription", binaryMessenger: registrar.messenger()) |
|||
playerContextChannel = FlutterEventChannel(name: "player_context_subscription", binaryMessenger: registrar.messenger()) |
|||
registrar.addApplicationDelegate(instance) |
|||
registrar.addMethodCallDelegate(instance, channel: spotifySDKChannel) |
|||
instance.connectionStatusHandler = ConnectionStatusHandler() |
|||
connectionStatusChannel.setStreamHandler(instance.connectionStatusHandler) |
|||
} |
|||
|
|||
public func handle(_ call: FlutterMethodCall, result: @escaping FlutterResult) { |
|||
var defaultPlayAPICallback: SPTAppRemoteCallback { |
|||
get { |
|||
return {_, error in |
|||
if let error = error { |
|||
result(FlutterError(code: "PlayerAPI Error", message: error.localizedDescription, details: nil)) |
|||
} else { |
|||
result(true) |
|||
} |
|||
} |
|||
} |
|||
} |
|||
|
|||
switch call.method { |
|||
case SpotifySdkConstants.methodConnectToSpotify: |
|||
guard let swiftArguments = call.arguments as? [String:Any], |
|||
let clientID = swiftArguments[SpotifySdkConstants.paramClientId] as? String, |
|||
!clientID.isEmpty else { |
|||
result(FlutterError(code: "Argument Error", message: "Client ID is not set", details: nil)) |
|||
return |
|||
} |
|||
|
|||
guard let url = swiftArguments[SpotifySdkConstants.paramRedirectUrl] as? String, |
|||
!url.isEmpty else { |
|||
result(FlutterError(code: "Argument Error", message: "Redirect URL is not set", details: nil)) |
|||
return |
|||
} |
|||
|
|||
connectionStatusHandler?.connectionResult = result |
|||
|
|||
|
|||
let accessToken: String? = swiftArguments[SpotifySdkConstants.paramAccessToken] as? String |
|||
let spotifyUri: String = swiftArguments[SpotifySdkConstants.paramSpotifyUri] as? String ?? "" |
|||
|
|||
do { |
|||
try connectToSpotify(clientId: clientID, redirectURL: url, accessToken: accessToken, spotifyUri: spotifyUri, asRadio: swiftArguments[SpotifySdkConstants.paramAsRadio] as? Bool, additionalScopes: swiftArguments[SpotifySdkConstants.scope] as? String) |
|||
} |
|||
catch SpotifyError.redirectURLInvalid { |
|||
result(FlutterError(code: "errorConnecting", message: "Redirect URL is not set or has invalid format", details: nil)) |
|||
} |
|||
catch { |
|||
result(FlutterError(code: "CouldNotFindSpotifyApp", message: "The Spotify app is not installed on the device", details: nil)) |
|||
return |
|||
} |
|||
|
|||
case SpotifySdkConstants.methodGetAccessToken: |
|||
guard let swiftArguments = call.arguments as? [String:Any], |
|||
let clientID = swiftArguments[SpotifySdkConstants.paramClientId] as? String, |
|||
let url = swiftArguments[SpotifySdkConstants.paramRedirectUrl] as? String else { |
|||
result(FlutterError(code: "Arguments Error", message: "One or more arguments are missing", details: nil)) |
|||
return |
|||
} |
|||
connectionStatusHandler?.tokenResult = result |
|||
let spotifyUri: String = swiftArguments[SpotifySdkConstants.paramSpotifyUri] as? String ?? "" |
|||
|
|||
do { |
|||
try connectToSpotify(clientId: clientID, redirectURL: url, spotifyUri: spotifyUri, asRadio: swiftArguments[SpotifySdkConstants.paramAsRadio] as? Bool, additionalScopes: swiftArguments[SpotifySdkConstants.scope] as? String) |
|||
} |
|||
catch SpotifyError.redirectURLInvalid { |
|||
result(FlutterError(code: "errorConnecting", message: "Redirect URL is not set or has invalid format", details: nil)) |
|||
} |
|||
catch { |
|||
result(FlutterError(code: "CouldNotFindSpotifyApp", message: "The Spotify app is not installed on the device", details: nil)) |
|||
return |
|||
} |
|||
case SpotifySdkConstants.methodGetImage: |
|||
guard let appRemote = appRemote else { |
|||
result(FlutterError(code: "Connection Error", message: "AppRemote is null", details: nil)) |
|||
return |
|||
} |
|||
guard let swiftArguments = call.arguments as? [String:Any], |
|||
let paramImageUri = swiftArguments[SpotifySdkConstants.paramImageUri] as? String, |
|||
let paramImageDimension = swiftArguments[SpotifySdkConstants.paramImageDimension] as? Int else { |
|||
result(FlutterError(code: "Arguments Error", message: "One or more arguments are missing", details: nil)) |
|||
return |
|||
} |
|||
|
|||
class ImageObject: NSObject, SPTAppRemoteImageRepresentable { |
|||
var imageIdentifier: String = "" |
|||
} |
|||
|
|||
let imageObject = ImageObject() |
|||
imageObject.imageIdentifier = paramImageUri |
|||
appRemote.imageAPI?.fetchImage(forItem: imageObject, with: CGSize(width: paramImageDimension, height: paramImageDimension), callback: { (image, error) in |
|||
guard error == nil else { |
|||
result(FlutterError(code: "ImageAPI Error", message: error?.localizedDescription, details: nil)) |
|||
return |
|||
} |
|||
guard let imageData = (image as? UIImage)?.pngData() else { |
|||
result(FlutterError(code: "ImageAPI Error", message: "Image is empty", details: nil)) |
|||
return |
|||
} |
|||
result(imageData) |
|||
}) |
|||
case SpotifySdkConstants.methodGetPlayerState: |
|||
guard let appRemote = appRemote else { |
|||
result(FlutterError(code: "Connection Error", message: "AppRemote is null", details: nil)) |
|||
return |
|||
} |
|||
|
|||
appRemote.playerAPI?.getPlayerState({ (playerState, error) in |
|||
guard error == nil else { |
|||
result(FlutterError(code: "PlayerAPI Error", message: error?.localizedDescription, details: nil)) |
|||
return |
|||
} |
|||
guard let playerState = playerState as? SPTAppRemotePlayerState else { |
|||
result(FlutterError(code: "PlayerAPI Error", message: "PlayerState is empty", details: nil)) |
|||
return |
|||
} |
|||
result(State.playerStateDictionary(playerState).json) |
|||
}) |
|||
case SpotifySdkConstants.methodDisconnectFromSpotify: |
|||
appRemote?.disconnect() |
|||
// appRemote?.connectionParameters.accessToken = nil |
|||
result(true) |
|||
case SpotifySdkConstants.methodPlay: |
|||
guard let appRemote = appRemote else { |
|||
result(FlutterError(code: "Connection Error", message: "AppRemote is null", details: nil)) |
|||
return |
|||
} |
|||
guard let swiftArguments = call.arguments as? [String:Any], |
|||
let uri = swiftArguments[SpotifySdkConstants.paramSpotifyUri] as? String else { |
|||
result(FlutterError(code: "URI Error", message: "No URI was specified", details: nil)) |
|||
return |
|||
} |
|||
let asRadio: Bool = (swiftArguments[SpotifySdkConstants.paramAsRadio] as? Bool) ?? false |
|||
appRemote.playerAPI?.play(uri, asRadio: asRadio, callback: defaultPlayAPICallback) |
|||
case SpotifySdkConstants.methodPause: |
|||
guard let appRemote = appRemote else { |
|||
result(FlutterError(code: "Connection Error", message: "AppRemote is null", details: nil)) |
|||
return |
|||
} |
|||
appRemote.playerAPI?.pause(defaultPlayAPICallback) |
|||
case SpotifySdkConstants.methodResume: |
|||
guard let appRemote = appRemote else { |
|||
result(FlutterError(code: "Connection Error", message: "AppRemote is null", details: nil)) |
|||
return |
|||
} |
|||
appRemote.playerAPI?.resume(defaultPlayAPICallback) |
|||
case SpotifySdkConstants.methodSkipNext: |
|||
guard let appRemote = appRemote else { |
|||
result(FlutterError(code: "Connection Error", message: "AppRemote is null", details: nil)) |
|||
return |
|||
} |
|||
appRemote.playerAPI?.skip(toNext: defaultPlayAPICallback) |
|||
case SpotifySdkConstants.methodSkipPrevious: |
|||
guard let appRemote = appRemote else { |
|||
result(FlutterError(code: "Connection Error", message: "AppRemote is null", details: nil)) |
|||
return |
|||
} |
|||
appRemote.playerAPI?.skip(toPrevious: { (spotifyResult, error) in |
|||
if let error = error { |
|||
result(FlutterError(code: "PlayerAPI Error", message: error.localizedDescription, details: nil)) |
|||
return |
|||
} |
|||
result(true) |
|||
}) |
|||
case SpotifySdkConstants.methodSkipToIndex: |
|||
guard let appRemote = appRemote else { |
|||
result(FlutterError(code: "Connection Error", message: "AppRemote is null", details: nil)) |
|||
return |
|||
} |
|||
guard let swiftArguments = call.arguments as? [String:Any], |
|||
let uri = swiftArguments[SpotifySdkConstants.paramSpotifyUri] as? String else { |
|||
result(FlutterError(code: "URI Error", message: "No URI was specified", details: nil)) |
|||
return |
|||
} |
|||
let index = (swiftArguments[SpotifySdkConstants.paramTrackIndex] as? Int) ?? 0 |
|||
|
|||
appRemote.contentAPI?.fetchContentItem(forURI: uri, callback: { (contentItemResult, error) in |
|||
guard error == nil else { |
|||
result(FlutterError(code: "PlayerAPI Error", message: error?.localizedDescription, details: nil)) |
|||
return |
|||
} |
|||
guard let contentItem = contentItemResult as? SPTAppRemoteContentItem else { |
|||
result(FlutterError(code: "URI Error", message: "No URI was specified", details: nil)) |
|||
return |
|||
} |
|||
appRemote.playerAPI?.play(contentItem, skipToTrackIndex: index, callback: defaultPlayAPICallback) |
|||
}) |
|||
|
|||
case SpotifySdkConstants.methodAddToLibrary: |
|||
guard let appRemote = appRemote else { |
|||
result(FlutterError(code: "Connection Error", message: "AppRemote is null", details: nil)) |
|||
return |
|||
} |
|||
guard let swiftArguments = call.arguments as? [String:Any], |
|||
let uri = swiftArguments[SpotifySdkConstants.paramSpotifyUri] as? String else { |
|||
result(FlutterError(code: "URI Error", message: "No URI was specified", details: nil)) |
|||
return |
|||
} |
|||
appRemote.userAPI?.addItemToLibrary(withURI: uri, callback: defaultPlayAPICallback) |
|||
case SpotifySdkConstants.methodRemoveFromLibrary: |
|||
guard let appRemote = appRemote else { |
|||
result(FlutterError(code: "Connection Error", message: "AppRemote is null", details: nil)) |
|||
return |
|||
} |
|||
guard let swiftArguments = call.arguments as? [String:Any], |
|||
let uri = swiftArguments[SpotifySdkConstants.paramSpotifyUri] as? String else { |
|||
result(FlutterError(code: "URI Error", message: "No URI was specified", details: nil)) |
|||
return |
|||
} |
|||
appRemote.userAPI?.removeItemFromLibrary(withURI: uri, callback: defaultPlayAPICallback) |
|||
case SpotifySdkConstants.methodGetCapabilities: |
|||
guard let appRemote = appRemote else { |
|||
result(FlutterError(code: "Connection Error", message: "AppRemote is null", details: nil)) |
|||
return |
|||
} |
|||
appRemote.userAPI?.fetchCapabilities(callback: { (capabilitiesResult, error) in |
|||
guard error == nil else { |
|||
result(FlutterError(code: "getCapabilitiesError", message: error?.localizedDescription, details: nil)) |
|||
return |
|||
} |
|||
guard let userCapabilities = capabilitiesResult as? SPTAppRemoteUserCapabilities else { |
|||
result(FlutterError(code: "getCapabilitiesError", message: error?.localizedDescription, details: nil)) |
|||
return |
|||
} |
|||
|
|||
result(State.userCapabilitiesDictionary(userCapabilities).json) |
|||
}) |
|||
case SpotifySdkConstants.methodQueueTrack: |
|||
guard let appRemote = appRemote else { |
|||
result(FlutterError(code: "Connection Error", message: "AppRemote is null", details: nil)) |
|||
return |
|||
} |
|||
guard let swiftArguments = call.arguments as? [String:Any], |
|||
let uri = swiftArguments[SpotifySdkConstants.paramSpotifyUri] as? String else { |
|||
result(FlutterError(code: "URI Error", message: "No URI was specified", details: nil)) |
|||
return |
|||
} |
|||
appRemote.playerAPI?.enqueueTrackUri(uri, callback: defaultPlayAPICallback) |
|||
case SpotifySdkConstants.methodSeekTo: |
|||
guard let appRemote = appRemote else { |
|||
result(FlutterError(code: "Connection Error", message: "AppRemote is null", details: nil)) |
|||
return |
|||
} |
|||
guard let swiftArguments = call.arguments as? [String:Any], |
|||
let position = swiftArguments[SpotifySdkConstants.paramPositionedMilliseconds] as? Int else { |
|||
result(FlutterError(code: "Position error", message: "No position was specified", details: nil)) |
|||
return |
|||
} |
|||
appRemote.playerAPI?.seek(toPosition: position, callback: defaultPlayAPICallback) |
|||
case SpotifySdkConstants.methodGetCrossfadeState: |
|||
guard let appRemote = appRemote else { |
|||
result(FlutterError(code: "Connection Error", message: "AppRemote is null", details: nil)) |
|||
return |
|||
} |
|||
appRemote.playerAPI?.getCrossfadeState({ (crossfadeState, error) in |
|||
guard error == nil else { |
|||
result(FlutterError(code: "PlayerAPI Error", message: error?.localizedDescription, details: nil)) |
|||
return |
|||
} |
|||
guard let crossfadeState = crossfadeState as? SPTAppRemoteCrossfadeState else { |
|||
result(FlutterError(code: "PlayerAPI Error", message: "PlayerState is empty", details: nil)) |
|||
return |
|||
} |
|||
result(State.crossfadeStateDictionary(crossfadeState).json) |
|||
}) |
|||
case SpotifySdkConstants.methodSetShuffle: |
|||
guard let appRemote = appRemote else { |
|||
result(FlutterError(code: "Connection Error", message: "AppRemote is null", details: nil)) |
|||
return |
|||
} |
|||
guard let swiftArguments = call.arguments as? [String:Any], |
|||
let shuffle = swiftArguments[SpotifySdkConstants.paramShuffle] as? Bool else { |
|||
result(FlutterError(code: "Shuffle mode error", message: "No ShuffleMode was specified", details: nil)) |
|||
return |
|||
} |
|||
appRemote.playerAPI?.setShuffle(shuffle, callback: defaultPlayAPICallback) |
|||
case SpotifySdkConstants.methodSetRepeatMode: |
|||
guard let appRemote = appRemote else { |
|||
result(FlutterError(code: "Connection Error", message: "AppRemote is null", details: nil)) |
|||
return |
|||
} |
|||
guard let swiftArguments = call.arguments as? [String:Any], |
|||
let repeatModeIndex = swiftArguments[SpotifySdkConstants.paramRepeatMode] as? UInt, |
|||
let repeatMode = SPTAppRemotePlaybackOptionsRepeatMode(rawValue: repeatModeIndex) else { |
|||
result(FlutterError(code: "Repeat mode error", message: "No RepeatMode was specified", details: nil)) |
|||
return |
|||
} |
|||
appRemote.playerAPI?.setRepeatMode(repeatMode, callback: defaultPlayAPICallback) |
|||
case SpotifySdkConstants.getLibraryState: |
|||
guard let appRemote = appRemote else { |
|||
result(FlutterError(code: "Connection Error", message: "AppRemote is null", details: nil)) |
|||
return |
|||
} |
|||
guard let swiftArguments = call.arguments as? [String:Any], |
|||
let uri = swiftArguments[SpotifySdkConstants.paramSpotifyUri] as? String else { |
|||
result(FlutterError(code: "URI Error", message: "No URI was specified", details: nil)) |
|||
return |
|||
} |
|||
appRemote.userAPI?.fetchLibraryState(forURI: uri, callback: {libraryStateResult, error in |
|||
guard error == nil else { |
|||
result(FlutterError(code: "fetchLibraryStateError", message: error?.localizedDescription, details: nil)) |
|||
return |
|||
} |
|||
guard let libraryState = libraryStateResult as? SPTAppRemoteLibraryState else { |
|||
result(FlutterError(code: "fetchLibraryStateError", message: error?.localizedDescription, details: nil)) |
|||
return |
|||
} |
|||
|
|||
result(State.libraryStateDictionary(libraryState).json) |
|||
}) |
|||
default: |
|||
result(FlutterMethodNotImplemented) |
|||
} |
|||
} |
|||
|
|||
private func connectToSpotify(clientId: String, redirectURL: String, accessToken: String? = nil, spotifyUri: String = "", asRadio: Bool?, additionalScopes: String? = nil) throws { |
|||
func configureAppRemote(clientID: String, redirectURL: String, accessToken: String? = nil) throws { |
|||
guard let redirectURL = URL(string: redirectURL) else { |
|||
throw SpotifyError.redirectURLInvalid |
|||
} |
|||
let configuration = SPTConfiguration(clientID: clientID, redirectURL: redirectURL) |
|||
let appRemote = SPTAppRemote(configuration: configuration, logLevel: .none) |
|||
appRemote.delegate = connectionStatusHandler |
|||
let playerDelegate = PlayerDelegate() |
|||
playerStateHandler = PlayerStateHandler(appRemote: appRemote, playerDelegate: playerDelegate) |
|||
SwiftSpotifySdkPlugin.playerStateChannel?.setStreamHandler(playerStateHandler) |
|||
|
|||
playerContextHandler = PlayerContextHandler(appRemote: appRemote, playerDelegate: playerDelegate) |
|||
SwiftSpotifySdkPlugin.playerContextChannel?.setStreamHandler(playerContextHandler) |
|||
|
|||
appRemote.connectionParameters.accessToken = accessToken |
|||
self.appRemote = appRemote |
|||
} |
|||
|
|||
try configureAppRemote(clientID: clientId, redirectURL: redirectURL, accessToken: accessToken) |
|||
|
|||
var scopes: [String]? |
|||
if let additionalScopes = additionalScopes { |
|||
scopes = additionalScopes.components(separatedBy: ",") |
|||
} |
|||
|
|||
if accessToken != nil { |
|||
appRemote?.connect() |
|||
} else { |
|||
// Note: A blank string will play the user's last song or pick a random one. |
|||
self.appRemote?.authorizeAndPlayURI(spotifyUri, asRadio: asRadio ?? false, additionalScopes: scopes) { success in |
|||
if (!success) { |
|||
self.connectionStatusHandler?.connectionResult?(FlutterError(code: "spotifyNotInstalled", message: "Spotify app is not installed", details: nil)) |
|||
} |
|||
} |
|||
} |
|||
} |
|||
} |
|||
|
|||
extension SwiftSpotifySdkPlugin { |
|||
public func application(_ application: UIApplication, open url: URL, options: [UIApplication.OpenURLOptionsKey : Any] = [:]) -> Bool { |
|||
setAccessTokenFromURL(url: url) |
|||
return true |
|||
} |
|||
|
|||
public func application(_ application: UIApplication, continue userActivity: NSUserActivity, restorationHandler: @escaping ([Any]) -> Void) -> Bool { |
|||
guard userActivity.activityType == NSUserActivityTypeBrowsingWeb, |
|||
let url = userActivity.webpageURL |
|||
else { |
|||
connectionStatusHandler?.connectionResult?(FlutterError(code: "errorConnecting", message: "client id or redirectUrl is invalid", details: nil)) |
|||
connectionStatusHandler?.tokenResult?(FlutterError(code: "errorConnecting", message: "client id or redirectUrl is invalid", details: nil)) |
|||
connectionStatusHandler?.connectionResult = nil |
|||
connectionStatusHandler?.tokenResult = nil |
|||
return false |
|||
} |
|||
|
|||
setAccessTokenFromURL(url: url) |
|||
return false |
|||
} |
|||
|
|||
private func setAccessTokenFromURL(url: URL) { |
|||
guard let appRemote = appRemote else { |
|||
connectionStatusHandler?.connectionResult?(FlutterError(code: "errorConnection", message: "AppRemote is null", details: nil)) |
|||
connectionStatusHandler?.tokenResult?(FlutterError(code: "errorConnection", message: "AppRemote is null", details: nil)) |
|||
connectionStatusHandler?.connectionResult = nil |
|||
connectionStatusHandler?.tokenResult = nil |
|||
return |
|||
} |
|||
|
|||
guard let token = appRemote.authorizationParameters(from: url)?[SPTAppRemoteAccessTokenKey] else { |
|||
connectionStatusHandler?.connectionResult?(FlutterError(code: "authenticationTokenError", message: appRemote.authorizationParameters(from: url)?[SPTAppRemoteErrorDescriptionKey], details: nil)) |
|||
connectionStatusHandler?.tokenResult?(FlutterError(code: "authenticationTokenError", message: appRemote.authorizationParameters(from: url)?[SPTAppRemoteErrorDescriptionKey], details: nil)) |
|||
connectionStatusHandler?.connectionResult = nil |
|||
connectionStatusHandler?.tokenResult = nil |
|||
return |
|||
} |
|||
|
|||
appRemote.connectionParameters.accessToken = token |
|||
appRemote.connect() |
|||
} |
|||
} |
|||
@ -0,0 +1,10 @@ |
|||
#!/bin/sh |
|||
REPO_NAME="ios-sdk" |
|||
FRAMEWORK_NAME="SpotifyiOS.xcframework" |
|||
|
|||
#!/bin/sh |
|||
rm -fR ${REPO_NAME} |
|||
mkdir ${REPO_NAME} |
|||
git clone https://github.com/spotify/${REPO_NAME} |
|||
git -C ${REPO_NAME} checkout tags/v3.0.0 |
|||
find ./${REPO_NAME} -mindepth 1 -maxdepth 1 -not -name ${FRAMEWORK_NAME} -exec rm -rf '{}' \; # Keep on only the xcframework folder |
|||
@ -0,0 +1,26 @@ |
|||
# |
|||
# To learn more about a Podspec see http://guides.cocoapods.org/syntax/podspec.html. |
|||
# Run `pod lib lint spotify_sdk.podspec' to validate before publishing. |
|||
# |
|||
Pod::Spec.new do |s| |
|||
s.name = 'spotify_sdk' |
|||
s.version = '0.0.1' |
|||
s.summary = 'Unofficial Spotify Flutter SDK.' |
|||
s.description = <<-DESC |
|||
Unofficial Spotify Flutter SDK. |
|||
DESC |
|||
s.homepage = 'https://github.com/brim-borium/spotify_sdk' |
|||
s.license = { :file => '../LICENSE' } |
|||
s.author = { 'Your Company' => 'fdimanidis@gmail.com' } |
|||
s.source = { :path => '.' } |
|||
s.source_files = 'Classes/**/*' |
|||
s.dependency 'Flutter' |
|||
s.platform = :ios, '12.0' |
|||
s.ios.deployment_target = '12.0' |
|||
# Flutter.framework does not contain a i386 slice. Only x86_64 simulators are supported. |
|||
s.pod_target_xcconfig = { 'DEFINES_MODULE' => 'YES', 'VALID_ARCHS[sdk=iphonesimulator*]' => 'x86_64' } |
|||
s.swift_version = '5.0' |
|||
s.prepare_command = './prepare-iOS-SDK.sh' |
|||
s.vendored_frameworks = 'ios-sdk/SpotifyiOS.xcframework' |
|||
s.preserve_paths = 'ios-sdk/SpotifyiOS.xcframework' |
|||
end |
|||
@ -0,0 +1,17 @@ |
|||
/// Holds the values from the spotify api for supported Image dimensions |
|||
enum ImageDimension { |
|||
/// large image |
|||
large, |
|||
|
|||
///medium image |
|||
medium, |
|||
|
|||
/// small image |
|||
small, |
|||
|
|||
///xsmall image |
|||
xSmall, |
|||
|
|||
/// thumbnail image |
|||
thumbnail |
|||
} |
|||
@ -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, |
|||
} |
|||
@ -0,0 +1,11 @@ |
|||
/// Holds the values from the spotify api for RepeatModes |
|||
enum RepeatMode { |
|||
/// repeat is off |
|||
off, |
|||
|
|||
/// repeats the current track |
|||
track, |
|||
|
|||
/// repeats the current context |
|||
context, |
|||
} |
|||
@ -0,0 +1,19 @@ |
|||
import 'package:spotify_sdk/enums/image_dimension_enum.dart'; |
|||
|
|||
///Extension for formatting the ImageDimension enum to value |
|||
///@nodoc |
|||
extension ImageDimensionExtension on ImageDimension { |
|||
///maps the value to the specified enum |
|||
///@nodoc |
|||
static const values = { |
|||
ImageDimension.large: 720, |
|||
ImageDimension.medium: 480, |
|||
ImageDimension.small: 360, |
|||
ImageDimension.xSmall: 240, |
|||
ImageDimension.thumbnail: 144, |
|||
}; |
|||
|
|||
/// returns the value |
|||
///@nodoc |
|||
int get value => values[this]!; |
|||
} |
|||
@ -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]!; |
|||
} |
|||
@ -0,0 +1,8 @@ |
|||
import 'package:logger/logger.dart'; |
|||
|
|||
class CustomLogFilter extends LogFilter { |
|||
@override |
|||
bool shouldLog(LogEvent event) { |
|||
return true; |
|||
} |
|||
} |
|||
@ -0,0 +1,15 @@ |
|||
import 'package:json_annotation/json_annotation.dart'; |
|||
|
|||
part 'album.g.dart'; |
|||
|
|||
@JsonSerializable() |
|||
class Album { |
|||
Album(this.name, this.uri); |
|||
|
|||
final String? name; |
|||
final String? uri; |
|||
|
|||
factory Album.fromJson(Map<String, dynamic> json) => _$AlbumFromJson(json); |
|||
|
|||
Map<String, dynamic> toJson() => _$AlbumToJson(this); |
|||
} |
|||
@ -0,0 +1,17 @@ |
|||
// GENERATED CODE - DO NOT MODIFY BY HAND |
|||
|
|||
part of 'album.dart'; |
|||
|
|||
// ************************************************************************** |
|||
// JsonSerializableGenerator |
|||
// ************************************************************************** |
|||
|
|||
Album _$AlbumFromJson(Map<String, dynamic> json) => Album( |
|||
json['name'] as String?, |
|||
json['uri'] as String?, |
|||
); |
|||
|
|||
Map<String, dynamic> _$AlbumToJson(Album instance) => <String, dynamic>{ |
|||
'name': instance.name, |
|||
'uri': instance.uri, |
|||
}; |
|||
@ -0,0 +1,15 @@ |
|||
import 'package:json_annotation/json_annotation.dart'; |
|||
|
|||
part 'artist.g.dart'; |
|||
|
|||
@JsonSerializable() |
|||
class Artist { |
|||
Artist(this.name, this.uri); |
|||
|
|||
final String? name; |
|||
final String? uri; |
|||
|
|||
factory Artist.fromJson(Map<String, dynamic> json) => _$ArtistFromJson(json); |
|||
|
|||
Map<String, dynamic> toJson() => _$ArtistToJson(this); |
|||
} |
|||
@ -0,0 +1,17 @@ |
|||
// GENERATED CODE - DO NOT MODIFY BY HAND |
|||
|
|||
part of 'artist.dart'; |
|||
|
|||
// ************************************************************************** |
|||
// JsonSerializableGenerator |
|||
// ************************************************************************** |
|||
|
|||
Artist _$ArtistFromJson(Map<String, dynamic> json) => Artist( |
|||
json['name'] as String?, |
|||
json['uri'] as String?, |
|||
); |
|||
|
|||
Map<String, dynamic> _$ArtistToJson(Artist instance) => <String, dynamic>{ |
|||
'name': instance.name, |
|||
'uri': instance.uri, |
|||
}; |
|||
@ -0,0 +1,18 @@ |
|||
import 'package:json_annotation/json_annotation.dart'; |
|||
|
|||
part 'capabilities.g.dart'; |
|||
|
|||
@JsonSerializable() |
|||
class Capabilities { |
|||
Capabilities({ |
|||
required this.canPlayOnDemand, |
|||
}); |
|||
|
|||
@JsonKey(name: 'can_play_on_demand') |
|||
final bool canPlayOnDemand; |
|||
|
|||
factory Capabilities.fromJson(Map<String, dynamic> json) => |
|||
_$CapabilitiesFromJson(json); |
|||
|
|||
Map<String, dynamic> toJson() => _$CapabilitiesToJson(this); |
|||
} |
|||
@ -0,0 +1,16 @@ |
|||
// GENERATED CODE - DO NOT MODIFY BY HAND |
|||
|
|||
part of 'capabilities.dart'; |
|||
|
|||
// ************************************************************************** |
|||
// JsonSerializableGenerator |
|||
// ************************************************************************** |
|||
|
|||
Capabilities _$CapabilitiesFromJson(Map<String, dynamic> json) => Capabilities( |
|||
canPlayOnDemand: json['can_play_on_demand'] as bool, |
|||
); |
|||
|
|||
Map<String, dynamic> _$CapabilitiesToJson(Capabilities instance) => |
|||
<String, dynamic>{ |
|||
'can_play_on_demand': instance.canPlayOnDemand, |
|||
}; |
|||
@ -0,0 +1,31 @@ |
|||
import 'package:json_annotation/json_annotation.dart'; |
|||
|
|||
part 'connection_status.g.dart'; |
|||
|
|||
@JsonSerializable() |
|||
class ConnectionStatus { |
|||
ConnectionStatus( |
|||
this.message, |
|||
this.errorCode, |
|||
this.errorDetails, { |
|||
required this.connected, |
|||
}); |
|||
|
|||
@JsonKey(name: 'connected') |
|||
final bool connected; |
|||
@JsonKey(name: 'message') |
|||
final String? message; |
|||
@JsonKey(name: 'errorCode') |
|||
final String? errorCode; |
|||
@JsonKey(name: 'errorDetails') |
|||
final String? errorDetails; |
|||
|
|||
bool hasError() { |
|||
return errorCode?.isNotEmpty == true; |
|||
} |
|||
|
|||
factory ConnectionStatus.fromJson(Map<String, dynamic> json) => |
|||
_$ConnectionStatusFromJson(json); |
|||
|
|||
Map<String, dynamic> toJson() => _$ConnectionStatusToJson(this); |
|||
} |
|||
@ -0,0 +1,23 @@ |
|||
// GENERATED CODE - DO NOT MODIFY BY HAND |
|||
|
|||
part of 'connection_status.dart'; |
|||
|
|||
// ************************************************************************** |
|||
// JsonSerializableGenerator |
|||
// ************************************************************************** |
|||
|
|||
ConnectionStatus _$ConnectionStatusFromJson(Map<String, dynamic> json) => |
|||
ConnectionStatus( |
|||
json['message'] as String?, |
|||
json['errorCode'] as String?, |
|||
json['errorDetails'] as String?, |
|||
connected: json['connected'] as bool, |
|||
); |
|||
|
|||
Map<String, dynamic> _$ConnectionStatusToJson(ConnectionStatus instance) => |
|||
<String, dynamic>{ |
|||
'connected': instance.connected, |
|||
'message': instance.message, |
|||
'errorCode': instance.errorCode, |
|||
'errorDetails': instance.errorDetails, |
|||
}; |
|||
@ -0,0 +1,19 @@ |
|||
import 'package:json_annotation/json_annotation.dart'; |
|||
|
|||
part 'crossfade_state.g.dart'; |
|||
|
|||
@JsonSerializable() |
|||
class CrossfadeState { |
|||
CrossfadeState( |
|||
this.duration, { |
|||
required this.isEnabled, |
|||
}); |
|||
|
|||
final bool isEnabled; |
|||
final int duration; |
|||
|
|||
factory CrossfadeState.fromJson(Map<String, dynamic> json) => |
|||
_$CrossfadeStateFromJson(json); |
|||
|
|||
Map<String, dynamic> toJson() => _$CrossfadeStateToJson(this); |
|||
} |
|||
@ -0,0 +1,19 @@ |
|||
// GENERATED CODE - DO NOT MODIFY BY HAND |
|||
|
|||
part of 'crossfade_state.dart'; |
|||
|
|||
// ************************************************************************** |
|||
// JsonSerializableGenerator |
|||
// ************************************************************************** |
|||
|
|||
CrossfadeState _$CrossfadeStateFromJson(Map<String, dynamic> json) => |
|||
CrossfadeState( |
|||
json['duration'] as int, |
|||
isEnabled: json['isEnabled'] as bool, |
|||
); |
|||
|
|||
Map<String, dynamic> _$CrossfadeStateToJson(CrossfadeState instance) => |
|||
<String, dynamic>{ |
|||
'isEnabled': instance.isEnabled, |
|||
'duration': instance.duration, |
|||
}; |
|||
@ -0,0 +1,15 @@ |
|||
import 'package:json_annotation/json_annotation.dart'; |
|||
|
|||
part 'image_uri.g.dart'; |
|||
|
|||
@JsonSerializable() |
|||
class ImageUri { |
|||
ImageUri(this.raw); |
|||
|
|||
final String raw; |
|||
|
|||
factory ImageUri.fromJson(Map<String, dynamic> json) => |
|||
_$ImageUriFromJson(json); |
|||
|
|||
Map<String, dynamic> toJson() => _$ImageUriToJson(this); |
|||
} |
|||
@ -0,0 +1,15 @@ |
|||
// GENERATED CODE - DO NOT MODIFY BY HAND |
|||
|
|||
part of 'image_uri.dart'; |
|||
|
|||
// ************************************************************************** |
|||
// JsonSerializableGenerator |
|||
// ************************************************************************** |
|||
|
|||
ImageUri _$ImageUriFromJson(Map<String, dynamic> json) => ImageUri( |
|||
json['raw'] as String, |
|||
); |
|||
|
|||
Map<String, dynamic> _$ImageUriToJson(ImageUri instance) => <String, dynamic>{ |
|||
'raw': instance.raw, |
|||
}; |
|||
@ -0,0 +1,24 @@ |
|||
import 'package:json_annotation/json_annotation.dart'; |
|||
|
|||
part 'library_state.g.dart'; |
|||
|
|||
@JsonSerializable() |
|||
class LibraryState { |
|||
LibraryState( |
|||
this.uri, { |
|||
required this.isSaved, |
|||
required this.canSave, |
|||
}); |
|||
|
|||
@JsonKey(name: 'uri') |
|||
final String uri; |
|||
@JsonKey(name: 'saved') |
|||
final bool isSaved; |
|||
@JsonKey(name: 'can_save') |
|||
final bool canSave; |
|||
|
|||
factory LibraryState.fromJson(Map<String, dynamic> json) => |
|||
_$LibraryStateFromJson(json); |
|||
|
|||
Map<String, dynamic> toJson() => _$LibraryStateToJson(this); |
|||
} |
|||
@ -0,0 +1,20 @@ |
|||
// GENERATED CODE - DO NOT MODIFY BY HAND |
|||
|
|||
part of 'library_state.dart'; |
|||
|
|||
// ************************************************************************** |
|||
// JsonSerializableGenerator |
|||
// ************************************************************************** |
|||
|
|||
LibraryState _$LibraryStateFromJson(Map<String, dynamic> json) => LibraryState( |
|||
json['uri'] as String, |
|||
isSaved: json['saved'] as bool, |
|||
canSave: json['can_save'] as bool, |
|||
); |
|||
|
|||
Map<String, dynamic> _$LibraryStateToJson(LibraryState instance) => |
|||
<String, dynamic>{ |
|||
'uri': instance.uri, |
|||
'saved': instance.isSaved, |
|||
'can_save': instance.canSave, |
|||
}; |
|||
@ -0,0 +1,18 @@ |
|||
import 'package:json_annotation/json_annotation.dart'; |
|||
|
|||
part 'player_context.g.dart'; |
|||
|
|||
@JsonSerializable() |
|||
class PlayerContext { |
|||
PlayerContext(this.title, this.subtitle, this.type, this.uri); |
|||
|
|||
final String title; |
|||
final String subtitle; |
|||
final String type; |
|||
final String uri; |
|||
|
|||
factory PlayerContext.fromJson(Map<String, dynamic> json) => |
|||
_$PlayerContextFromJson(json); |
|||
|
|||
Map<String, dynamic> toJson() => _$PlayerContextToJson(this); |
|||
} |
|||
@ -0,0 +1,23 @@ |
|||
// GENERATED CODE - DO NOT MODIFY BY HAND |
|||
|
|||
part of 'player_context.dart'; |
|||
|
|||
// ************************************************************************** |
|||
// JsonSerializableGenerator |
|||
// ************************************************************************** |
|||
|
|||
PlayerContext _$PlayerContextFromJson(Map<String, dynamic> json) => |
|||
PlayerContext( |
|||
json['title'] as String, |
|||
json['subtitle'] as String, |
|||
json['type'] as String, |
|||
json['uri'] as String, |
|||
); |
|||
|
|||
Map<String, dynamic> _$PlayerContextToJson(PlayerContext instance) => |
|||
<String, dynamic>{ |
|||
'title': instance.title, |
|||
'subtitle': instance.subtitle, |
|||
'type': instance.type, |
|||
'uri': instance.uri, |
|||
}; |
|||
@ -0,0 +1,30 @@ |
|||
import 'package:json_annotation/json_annotation.dart'; |
|||
|
|||
part 'player_options.g.dart'; |
|||
|
|||
@JsonSerializable() |
|||
class PlayerOptions { |
|||
PlayerOptions( |
|||
this.repeatMode, { |
|||
required this.isShuffling, |
|||
}); |
|||
|
|||
@JsonKey(name: 'shuffle') |
|||
final bool isShuffling; |
|||
@JsonKey(name: 'repeat') |
|||
final RepeatMode repeatMode; |
|||
|
|||
factory PlayerOptions.fromJson(Map<String, dynamic> json) => |
|||
_$PlayerOptionsFromJson(json); |
|||
|
|||
Map<String, dynamic> toJson() => _$PlayerOptionsToJson(this); |
|||
} |
|||
|
|||
enum RepeatMode { |
|||
@JsonValue(0) |
|||
off, |
|||
@JsonValue(1) |
|||
track, |
|||
@JsonValue(2) |
|||
context |
|||
} |
|||
@ -0,0 +1,25 @@ |
|||
// GENERATED CODE - DO NOT MODIFY BY HAND |
|||
|
|||
part of 'player_options.dart'; |
|||
|
|||
// ************************************************************************** |
|||
// JsonSerializableGenerator |
|||
// ************************************************************************** |
|||
|
|||
PlayerOptions _$PlayerOptionsFromJson(Map<String, dynamic> json) => |
|||
PlayerOptions( |
|||
$enumDecode(_$RepeatModeEnumMap, json['repeat']), |
|||
isShuffling: json['shuffle'] as bool, |
|||
); |
|||
|
|||
Map<String, dynamic> _$PlayerOptionsToJson(PlayerOptions instance) => |
|||
<String, dynamic>{ |
|||
'shuffle': instance.isShuffling, |
|||
'repeat': _$RepeatModeEnumMap[instance.repeatMode]!, |
|||
}; |
|||
|
|||
const _$RepeatModeEnumMap = { |
|||
RepeatMode.off: 0, |
|||
RepeatMode.track: 1, |
|||
RepeatMode.context: 2, |
|||
}; |
|||
@ -0,0 +1,33 @@ |
|||
import 'package:json_annotation/json_annotation.dart'; |
|||
|
|||
part 'player_restrictions.g.dart'; |
|||
|
|||
@JsonSerializable() |
|||
class PlayerRestrictions { |
|||
PlayerRestrictions({ |
|||
required this.canSkipNext, |
|||
required this.canSkipPrevious, |
|||
required this.canRepeatTrack, |
|||
required this.canRepeatContext, |
|||
required this.canToggleShuffle, |
|||
required this.canSeek, |
|||
}); |
|||
|
|||
@JsonKey(name: 'can_skip_next') |
|||
final bool canSkipNext; |
|||
@JsonKey(name: 'can_skip_prev') |
|||
final bool canSkipPrevious; |
|||
@JsonKey(name: 'can_repeat_track') |
|||
final bool canRepeatTrack; |
|||
@JsonKey(name: 'can_repeat_context') |
|||
final bool canRepeatContext; |
|||
@JsonKey(name: 'can_toggle_shuffle') |
|||
final bool canToggleShuffle; |
|||
@JsonKey(name: 'can_seek') |
|||
final bool canSeek; |
|||
|
|||
factory PlayerRestrictions.fromJson(Map<String, dynamic> json) => |
|||
_$PlayerRestrictionsFromJson(json); |
|||
|
|||
Map<String, dynamic> toJson() => _$PlayerRestrictionsToJson(this); |
|||
} |
|||
@ -0,0 +1,27 @@ |
|||
// GENERATED CODE - DO NOT MODIFY BY HAND |
|||
|
|||
part of 'player_restrictions.dart'; |
|||
|
|||
// ************************************************************************** |
|||
// JsonSerializableGenerator |
|||
// ************************************************************************** |
|||
|
|||
PlayerRestrictions _$PlayerRestrictionsFromJson(Map<String, dynamic> json) => |
|||
PlayerRestrictions( |
|||
canSkipNext: json['can_skip_next'] as bool, |
|||
canSkipPrevious: json['can_skip_prev'] as bool, |
|||
canRepeatTrack: json['can_repeat_track'] as bool, |
|||
canRepeatContext: json['can_repeat_context'] as bool, |
|||
canToggleShuffle: json['can_toggle_shuffle'] as bool, |
|||
canSeek: json['can_seek'] as bool, |
|||
); |
|||
|
|||
Map<String, dynamic> _$PlayerRestrictionsToJson(PlayerRestrictions instance) => |
|||
<String, dynamic>{ |
|||
'can_skip_next': instance.canSkipNext, |
|||
'can_skip_prev': instance.canSkipPrevious, |
|||
'can_repeat_track': instance.canRepeatTrack, |
|||
'can_repeat_context': instance.canRepeatContext, |
|||
'can_toggle_shuffle': instance.canToggleShuffle, |
|||
'can_seek': instance.canSeek, |
|||
}; |
|||
@ -0,0 +1,36 @@ |
|||
import 'package:json_annotation/json_annotation.dart'; |
|||
|
|||
import 'player_options.dart'; |
|||
import 'player_restrictions.dart'; |
|||
import 'track.dart'; |
|||
|
|||
part 'player_state.g.dart'; |
|||
|
|||
@JsonSerializable() |
|||
class PlayerState { |
|||
PlayerState( |
|||
this.track, |
|||
this.playbackSpeed, |
|||
this.playbackPosition, |
|||
this.playbackOptions, |
|||
this.playbackRestrictions, { |
|||
required this.isPaused, |
|||
}); |
|||
|
|||
final Track? track; |
|||
@JsonKey(name: 'is_paused') |
|||
final bool isPaused; |
|||
@JsonKey(name: 'playback_speed') |
|||
final double playbackSpeed; |
|||
@JsonKey(name: 'playback_position') |
|||
final int playbackPosition; |
|||
@JsonKey(name: 'playback_options') |
|||
final PlayerOptions playbackOptions; |
|||
@JsonKey(name: 'playback_restrictions') |
|||
final PlayerRestrictions playbackRestrictions; |
|||
|
|||
factory PlayerState.fromJson(Map<String, dynamic> json) => |
|||
_$PlayerStateFromJson(json); |
|||
|
|||
Map<String, dynamic> toJson() => _$PlayerStateToJson(this); |
|||
} |
|||
@ -0,0 +1,29 @@ |
|||
// GENERATED CODE - DO NOT MODIFY BY HAND |
|||
|
|||
part of 'player_state.dart'; |
|||
|
|||
// ************************************************************************** |
|||
// JsonSerializableGenerator |
|||
// ************************************************************************** |
|||
|
|||
PlayerState _$PlayerStateFromJson(Map<String, dynamic> json) => PlayerState( |
|||
json['track'] == null |
|||
? null |
|||
: Track.fromJson(json['track'] as Map<String, dynamic>), |
|||
(json['playback_speed'] as num).toDouble(), |
|||
json['playback_position'] as int, |
|||
PlayerOptions.fromJson(json['playback_options'] as Map<String, dynamic>), |
|||
PlayerRestrictions.fromJson( |
|||
json['playback_restrictions'] as Map<String, dynamic>), |
|||
isPaused: json['is_paused'] as bool, |
|||
); |
|||
|
|||
Map<String, dynamic> _$PlayerStateToJson(PlayerState instance) => |
|||
<String, dynamic>{ |
|||
'track': instance.track, |
|||
'is_paused': instance.isPaused, |
|||
'playback_speed': instance.playbackSpeed, |
|||
'playback_position': instance.playbackPosition, |
|||
'playback_options': instance.playbackOptions, |
|||
'playback_restrictions': instance.playbackRestrictions, |
|||
}; |
|||
@ -0,0 +1,43 @@ |
|||
import 'package:json_annotation/json_annotation.dart'; |
|||
|
|||
import 'album.dart'; |
|||
import 'artist.dart'; |
|||
import 'image_uri.dart'; |
|||
|
|||
part 'track.g.dart'; |
|||
|
|||
@JsonSerializable() |
|||
class Track { |
|||
Track( |
|||
this.album, |
|||
this.artist, |
|||
this.artists, |
|||
this.duration, |
|||
this.imageUri, |
|||
this.name, |
|||
this.uri, |
|||
this.linkedFromUri, { |
|||
required this.isEpisode, |
|||
required this.isPodcast, |
|||
}); |
|||
|
|||
final Album album; |
|||
final Artist artist; |
|||
final List<Artist> artists; |
|||
@JsonKey(name: 'duration_ms') |
|||
final int duration; |
|||
@JsonKey(name: 'image_id') |
|||
final ImageUri imageUri; |
|||
@JsonKey(name: 'is_episode') |
|||
final bool isEpisode; |
|||
@JsonKey(name: 'is_podcast') |
|||
final bool isPodcast; |
|||
final String name; |
|||
final String uri; |
|||
@JsonKey(name: 'linked_from_uri') |
|||
final String? linkedFromUri; |
|||
|
|||
factory Track.fromJson(Map<String, dynamic> json) => _$TrackFromJson(json); |
|||
|
|||
Map<String, dynamic> toJson() => _$TrackToJson(this); |
|||
} |
|||
@ -0,0 +1,35 @@ |
|||
// GENERATED CODE - DO NOT MODIFY BY HAND |
|||
|
|||
part of 'track.dart'; |
|||
|
|||
// ************************************************************************** |
|||
// JsonSerializableGenerator |
|||
// ************************************************************************** |
|||
|
|||
Track _$TrackFromJson(Map<String, dynamic> json) => Track( |
|||
Album.fromJson(json['album'] as Map<String, dynamic>), |
|||
Artist.fromJson(json['artist'] as Map<String, dynamic>), |
|||
(json['artists'] as List<dynamic>) |
|||
.map((e) => Artist.fromJson(e as Map<String, dynamic>)) |
|||
.toList(), |
|||
json['duration_ms'] as int, |
|||
ImageUri.fromJson(json['image_id'] as Map<String, dynamic>), |
|||
json['name'] as String, |
|||
json['uri'] as String, |
|||
json['linked_from_uri'] as String?, |
|||
isEpisode: json['is_episode'] as bool, |
|||
isPodcast: json['is_podcast'] as bool, |
|||
); |
|||
|
|||
Map<String, dynamic> _$TrackToJson(Track instance) => <String, dynamic>{ |
|||
'album': instance.album, |
|||
'artist': instance.artist, |
|||
'artists': instance.artists, |
|||
'duration_ms': instance.duration, |
|||
'image_id': instance.imageUri, |
|||
'is_episode': instance.isEpisode, |
|||
'is_podcast': instance.isPodcast, |
|||
'name': instance.name, |
|||
'uri': instance.uri, |
|||
'linked_from_uri': instance.linkedFromUri, |
|||
}; |
|||
@ -0,0 +1,26 @@ |
|||
import 'package:json_annotation/json_annotation.dart'; |
|||
|
|||
part 'user_status.g.dart'; |
|||
|
|||
@JsonSerializable() |
|||
class UserStatus { |
|||
UserStatus(this.code, this.shortMessage, this.longMessage); |
|||
|
|||
final int statusCodeOk = 0; |
|||
final int statusCodeNotLoggedIn = 1; |
|||
@JsonKey(name: 'code') |
|||
final int code; |
|||
@JsonKey(name: 'short_text') |
|||
final String shortMessage; |
|||
@JsonKey(name: 'long_text') |
|||
final String longMessage; |
|||
|
|||
bool isLoggedIn() { |
|||
return code == 0; |
|||
} |
|||
|
|||
factory UserStatus.fromJson(Map<String, dynamic> json) => |
|||
_$UserStatusFromJson(json); |
|||
|
|||
Map<String, dynamic> toJson() => _$UserStatusToJson(this); |
|||
} |
|||
Some files were not shown because too many files changed in this diff
Loading…
Reference in new issue