Browse Source

上传音乐播放和facntion call 的结果补充

newdev_shunjiawei
liwei1dao 2 years ago
parent
commit
d6720d0036
  1. 11
      android/app/build.gradle.kts
  2. 4
      android/app/src/main/AndroidManifest.xml
  3. 4
      android/app/src/main/kotlin/com/yunqiinnovation/deepsound/VoiceInteractionHandler.kt
  4. 7
      android/settings.gradle.kts
  5. 2
      lib/data/services/deapsound_ai_service.dart
  6. 2
      lib/data/services/network/nw_api.dart
  7. 95
      lib/data/services/open_ai_service_adapter.dart
  8. 34
      lib/modules/chat/controllers/chat_controller.dart
  9. 17
      lib/modules/profile/controllers/profile_controller.dart
  10. 27
      local_plugins/open_ai_service/android/src/main/kotlin/com/yunqiinnovation/open_ai_service/MCPClient.kt
  11. 13
      local_plugins/open_ai_service/android/src/main/kotlin/com/yunqiinnovation/open_ai_service/OpenAIService.kt
  12. 27
      local_plugins/open_ai_service/android/src/main/kotlin/com/yunqiinnovation/open_ai_service/OpenAIServicePlugin.kt
  13. 2
      local_plugins/open_ai_service/android/src/main/kotlin/com/yunqiinnovation/open_ai_service/mcp/CustomSseClientTransport.kt
  14. 16
      local_plugins/open_ai_service/lib/open_ai_service.dart
  15. 62
      local_plugins/spotify_sdk/.github/ISSUE_TEMPLATE/bug_report.md
  16. 1
      local_plugins/spotify_sdk/.github/ISSUE_TEMPLATE/config.yml
  17. 20
      local_plugins/spotify_sdk/.github/ISSUE_TEMPLATE/feature_request.md
  18. 41
      local_plugins/spotify_sdk/.github/actions/publish_flutter_package/action.yaml
  19. 42
      local_plugins/spotify_sdk/.github/dependabot.yml
  20. 16
      local_plugins/spotify_sdk/.github/scripts/verify_pub_score.sh
  21. 38
      local_plugins/spotify_sdk/.github/workflows/pull_request.yml
  22. 34
      local_plugins/spotify_sdk/.github/workflows/release.yml
  23. 50
      local_plugins/spotify_sdk/.gitignore
  24. 10
      local_plugins/spotify_sdk/.metadata
  25. 184
      local_plugins/spotify_sdk/CHANGELOG.md
  26. 76
      local_plugins/spotify_sdk/CODE_OF_CONDUCT.md
  27. 48
      local_plugins/spotify_sdk/CONTRIBUTING.md
  28. 201
      local_plugins/spotify_sdk/LICENSE
  29. 257
      local_plugins/spotify_sdk/README.md
  30. 12
      local_plugins/spotify_sdk/analysis_options.yaml
  31. 8
      local_plugins/spotify_sdk/android/.gitignore
  32. 74
      local_plugins/spotify_sdk/android/build.gradle
  33. 3
      local_plugins/spotify_sdk/android/gradle.properties
  34. 7
      local_plugins/spotify_sdk/android/gradle/wrapper/gradle-wrapper.properties
  35. BIN
      local_plugins/spotify_sdk/android/libs/spotify-app-remote-release-0.8.0.aar
  36. 1
      local_plugins/spotify_sdk/android/settings.gradle
  37. 10
      local_plugins/spotify_sdk/android/src/main/AndroidManifest.xml
  38. 13
      local_plugins/spotify_sdk/android/src/main/kotlin/de/minimalme/spotify_sdk/BaseSpotifyApi.kt
  39. 25
      local_plugins/spotify_sdk/android/src/main/kotlin/de/minimalme/spotify_sdk/SpotifyConnectApi.kt
  40. 35
      local_plugins/spotify_sdk/android/src/main/kotlin/de/minimalme/spotify_sdk/SpotifyImagesApi.kt
  41. 210
      local_plugins/spotify_sdk/android/src/main/kotlin/de/minimalme/spotify_sdk/SpotifyPlayerApi.kt
  42. 381
      local_plugins/spotify_sdk/android/src/main/kotlin/de/minimalme/spotify_sdk/SpotifySdkPlugin.kt
  43. 59
      local_plugins/spotify_sdk/android/src/main/kotlin/de/minimalme/spotify_sdk/SpotifyUserApi.kt
  44. 23
      local_plugins/spotify_sdk/android/src/main/kotlin/de/minimalme/spotify_sdk/subscriptions/CapabilitiesChannel.kt
  45. 27
      local_plugins/spotify_sdk/android/src/main/kotlin/de/minimalme/spotify_sdk/subscriptions/ConnectionStatusChannel.kt
  46. 22
      local_plugins/spotify_sdk/android/src/main/kotlin/de/minimalme/spotify_sdk/subscriptions/PlayerContextChannel.kt
  47. 22
      local_plugins/spotify_sdk/android/src/main/kotlin/de/minimalme/spotify_sdk/subscriptions/PlayerStateChannel.kt
  48. 23
      local_plugins/spotify_sdk/android/src/main/kotlin/de/minimalme/spotify_sdk/subscriptions/UserStatusChannel.kt
  49. 39
      local_plugins/spotify_sdk/bin/android_cleanup.dart
  50. 96
      local_plugins/spotify_sdk/bin/android_module_creator.dart
  51. 91
      local_plugins/spotify_sdk/bin/android_setup.dart
  52. 100
      local_plugins/spotify_sdk/bin/github_api.dart
  53. 52
      local_plugins/spotify_sdk/bin/precondition_checker.dart
  54. 37
      local_plugins/spotify_sdk/ios/.gitignore
  55. 0
      local_plugins/spotify_sdk/ios/Assets/.gitkeep
  56. 45
      local_plugins/spotify_sdk/ios/Classes/ConnectionStatusHandler.swift
  57. 13
      local_plugins/spotify_sdk/ios/Classes/Extension/Dictionary.swift
  58. 14
      local_plugins/spotify_sdk/ios/Classes/PlayerDelegate.swift
  59. 18
      local_plugins/spotify_sdk/ios/Classes/PlayerState/PlayerContextHandler.swift
  60. 85
      local_plugins/spotify_sdk/ios/Classes/PlayerState/PlayerState.swift
  61. 22
      local_plugins/spotify_sdk/ios/Classes/PlayerState/PlayerStateHandler.swift
  62. 4
      local_plugins/spotify_sdk/ios/Classes/SpotifyError.swift
  63. 46
      local_plugins/spotify_sdk/ios/Classes/SpotifySdkConstants.swift
  64. 4
      local_plugins/spotify_sdk/ios/Classes/SpotifySdkPlugin.h
  65. 18
      local_plugins/spotify_sdk/ios/Classes/SpotifySdkPlugin.m
  66. 17
      local_plugins/spotify_sdk/ios/Classes/StatusHandler.swift
  67. 416
      local_plugins/spotify_sdk/ios/Classes/SwiftSpotifySdkPlugin.swift
  68. 10
      local_plugins/spotify_sdk/ios/prepare-iOS-SDK.sh
  69. 26
      local_plugins/spotify_sdk/ios/spotify_sdk.podspec
  70. 17
      local_plugins/spotify_sdk/lib/enums/image_dimension_enum.dart
  71. 23
      local_plugins/spotify_sdk/lib/enums/podcast_playback_speed.dart
  72. 11
      local_plugins/spotify_sdk/lib/enums/repeat_mode_enum.dart
  73. 19
      local_plugins/spotify_sdk/lib/extensions/image_dimension_extension.dart
  74. 21
      local_plugins/spotify_sdk/lib/extensions/podcast_playback_speed_extension.dart
  75. 8
      local_plugins/spotify_sdk/lib/logging/custom_log_filter.dart
  76. 15
      local_plugins/spotify_sdk/lib/models/album.dart
  77. 17
      local_plugins/spotify_sdk/lib/models/album.g.dart
  78. 15
      local_plugins/spotify_sdk/lib/models/artist.dart
  79. 17
      local_plugins/spotify_sdk/lib/models/artist.g.dart
  80. 18
      local_plugins/spotify_sdk/lib/models/capabilities.dart
  81. 16
      local_plugins/spotify_sdk/lib/models/capabilities.g.dart
  82. 31
      local_plugins/spotify_sdk/lib/models/connection_status.dart
  83. 23
      local_plugins/spotify_sdk/lib/models/connection_status.g.dart
  84. 19
      local_plugins/spotify_sdk/lib/models/crossfade_state.dart
  85. 19
      local_plugins/spotify_sdk/lib/models/crossfade_state.g.dart
  86. 15
      local_plugins/spotify_sdk/lib/models/image_uri.dart
  87. 15
      local_plugins/spotify_sdk/lib/models/image_uri.g.dart
  88. 24
      local_plugins/spotify_sdk/lib/models/library_state.dart
  89. 20
      local_plugins/spotify_sdk/lib/models/library_state.g.dart
  90. 18
      local_plugins/spotify_sdk/lib/models/player_context.dart
  91. 23
      local_plugins/spotify_sdk/lib/models/player_context.g.dart
  92. 30
      local_plugins/spotify_sdk/lib/models/player_options.dart
  93. 25
      local_plugins/spotify_sdk/lib/models/player_options.g.dart
  94. 33
      local_plugins/spotify_sdk/lib/models/player_restrictions.dart
  95. 27
      local_plugins/spotify_sdk/lib/models/player_restrictions.g.dart
  96. 36
      local_plugins/spotify_sdk/lib/models/player_state.dart
  97. 29
      local_plugins/spotify_sdk/lib/models/player_state.g.dart
  98. 43
      local_plugins/spotify_sdk/lib/models/track.dart
  99. 35
      local_plugins/spotify_sdk/lib/models/track.g.dart
  100. 26
      local_plugins/spotify_sdk/lib/models/user_status.dart

11
android/app/build.gradle.kts

@ -3,6 +3,9 @@ import java.io.FileInputStream
repositories {
google()
mavenCentral()
flatDir {
dirs(project(":spotify_sdk").projectDir.resolve("libs"))
}
}
plugins {
@ -54,6 +57,12 @@ android {
}
defaultConfig {
manifestPlaceholders.putAll(
mapOf(
"redirectSchemeName" to "spotify-sdk",
"redirectHostName" to "auth"
)
)
// TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html).
applicationId = "com.yunqiinnovation.deepsound"
// You can update the following values to match your application needs.
@ -110,6 +119,7 @@ dependencies {
implementation(project(":volcano_speech"))
implementation(project(":chat_storage")) // 添加新创建的SQLite聊天存储插件
implementation(project(":jieli")) // 添加新创建的SQLite聊天存储插件
implementation(project(":spotify_sdk"))
// 添加OkHttp依赖
implementation("com.squareup.okhttp3:okhttp:4.9.3")
@ -126,7 +136,6 @@ dependencies {
// 添加 AndroidX Security 加密 SharedPreferences 依赖
implementation("androidx.security:security-crypto:1.1.0-alpha06")
}
flutter {

4
android/app/src/main/AndroidManifest.xml

@ -92,6 +92,10 @@
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>
<action android:name="android.intent.action.VIEW"/>
<category android:name="android.intent.category.DEFAULT"/>
<category android:name="android.intent.category.BROWSABLE"/>
<data android:scheme="yqcxdeepsound"/>
</intent-filter>
</activity>
<!-- Don't delete the meta-data below.

4
android/app/src/main/kotlin/com/yunqiinnovation/deepsound/VoiceInteractionHandler.kt

@ -230,7 +230,7 @@ class VoiceInteractionHandler(
private fun processWithOpenAI(text: String) {
// 保存当前用户输入,用于后续同步聊天记录
currentUserInput = text
Log.d(TAG, "用户 问题: $text")
Thread {
try {
val messages = JSONArray().apply {
@ -269,8 +269,6 @@ class VoiceInteractionHandler(
override fun onFunctionCall(call: JSONObject) {
FileLogger.d(TAG, "processWithOpenAI 收到函数调用请求: ${call.getString("name")}")
}
}
)

7
android/settings.gradle.kts

@ -25,8 +25,8 @@ pluginManagement {
plugins {
id("dev.flutter.flutter-plugin-loader")
id("com.android.application") version "8.7.0" apply false
id("com.android.library") version "8.7.0" apply false
id("com.android.application") version "8.8.0" apply false
id("com.android.library") version "8.8.0" apply false
id("org.jetbrains.kotlin.android") version "2.1.10" apply false
}
@ -36,10 +36,11 @@ include(":open_ai_service")
include(":volcano_speech")
include(":chat_storage")
include(":jieli")
include(":spotify_sdk")
// 设置azure_speech项目的路径
project(":azure_speech").projectDir = file("../local_plugins/azure_speech/android")
project(":open_ai_service").projectDir = file("../local_plugins/open_ai_service/android")
project(":volcano_speech").projectDir = file("../local_plugins/volcano_speech/android")
project(":chat_storage").projectDir = file("../local_plugins/chat_storage/android")
project(":jieli").projectDir = file("../local_plugins/jieli/android")
project(":spotify_sdk").projectDir = file("../local_plugins/spotify_sdk/android")

2
lib/data/services/deapsound_ai_service.dart

@ -28,7 +28,7 @@ class DeapsoundAIService implements AiService {
required String systemPrompt,
}) async* {
final client = http.Client();
final url = Uri.parse('https://www.deapsound.com/ai/funcchat');
final url = Uri.parse('https://api.deapsound.com/ai/funcchat');
List contextsList = [];
String msg = '';

2
lib/data/services/network/nw_api.dart

@ -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";
}

95
lib/data/services/open_ai_service_adapter.dart

@ -23,7 +23,8 @@ class StreamEvent {
class OpenAIServiceAdapter implements AiService {
final OpenAIService _openAIService = OpenAIService();
StreamSubscription<OpenAIEvent>? _eventSubscription;
final StreamController<StreamEvent> _tokenStreamController = StreamController<StreamEvent>.broadcast();
final StreamController<StreamEvent> _tokenStreamController =
StreamController<StreamEvent>.broadcast();
/// 构造函数
OpenAIServiceAdapter() {
@ -44,44 +45,40 @@ class OpenAIServiceAdapter implements AiService {
switch (event.type) {
case OpenAIEventType.token:
if (event.content is String) {
_tokenStreamController.add(StreamEvent(
StreamEventType.token,
content: event.content as String
));
_tokenStreamController.add(StreamEvent(StreamEventType.token,
content: event.content as String));
} else {
printInfo(info: '收到非字符串类型的token: ${event.content}');
}
break;
case OpenAIEventType.complete:
_tokenStreamController.add(StreamEvent(StreamEventType.complete));
_tokenStreamController
.add(StreamEvent(StreamEventType.complete));
break;
case OpenAIEventType.error:
_tokenStreamController.add(StreamEvent(
StreamEventType.error,
content: '未知错误: ${event.content}'
));
_tokenStreamController.add(StreamEvent(StreamEventType.error,
content: '未知错误: ${event.content}'));
break;
case OpenAIEventType.functionCall:
_tokenStreamController.add(StreamEvent(
StreamEventType.error,
content: '收到函数调用,该流仅支持文本响应'
));
printInfo(
info:
'接收到 functionCall content: ${event.content} meta: ${event.meta}');
// _tokenStreamController.add(StreamEvent(
// StreamEventType.error,
// content: '收到函数调用,该流仅支持文本响应'
// ));
break;
}
} catch (e) {
printError(info: '处理事件出错: $e');
_tokenStreamController.add(StreamEvent(
StreamEventType.error,
content: '处理事件失败: $e'
));
_tokenStreamController
.add(StreamEvent(StreamEventType.error, content: '处理事件失败: $e'));
}
},
onError: (error) {
printError(info: '事件流错误: $error');
_tokenStreamController.add(StreamEvent(
StreamEventType.error,
content: '事件流错误: $error'
));
_tokenStreamController.add(
StreamEvent(StreamEventType.error, content: '事件流错误: $error'));
},
onDone: () {
printInfo(info: '事件流已关闭');
@ -125,7 +122,6 @@ class OpenAIServiceAdapter implements AiService {
}
} catch (e) {
printError(info: '加载.mcp.json资源文件时出错: $e');
}
// 初始化OpenAI服务
@ -157,8 +153,8 @@ class OpenAIServiceAdapter implements AiService {
}) async {
try {
// 在方法内直接转换
final convertedMessages = messages.map((m) =>
Map<String, dynamic>.from(m)).toList();
final convertedMessages =
messages.map((m) => Map<String, dynamic>.from(m)).toList();
// 发送消息并获取回复
final response = await _openAIService.sendMessage(
@ -180,36 +176,34 @@ class OpenAIServiceAdapter implements AiService {
}) async* {
try {
// 在方法内直接转换
final convertedMessages = messages.map((m) =>
Map<String, dynamic>.from(m)).toList();
final convertedMessages =
messages.map((m) => Map<String, dynamic>.from(m)).toList();
// 创建用于接收token的控制器
final localController = StreamController<String>();
// 添加从广播流到本地流的订阅
final subscription = _tokenStreamController.stream.listen(
(streamEvent) {
switch (streamEvent.type) {
case StreamEventType.token:
if (streamEvent.content != null && !localController.isClosed) {
localController.add(streamEvent.content!);
}
break;
case StreamEventType.complete:
if (!localController.isClosed) {
localController.close();
}
break;
case StreamEventType.error:
if (!localController.isClosed) {
printError(info: '令牌流错误: ${streamEvent.content}');
localController.addError(streamEvent.content ?? '未知错误');
localController.close();
}
break;
}
final subscription = _tokenStreamController.stream.listen((streamEvent) {
switch (streamEvent.type) {
case StreamEventType.token:
if (streamEvent.content != null && !localController.isClosed) {
localController.add(streamEvent.content!);
}
break;
case StreamEventType.complete:
if (!localController.isClosed) {
localController.close();
}
break;
case StreamEventType.error:
if (!localController.isClosed) {
printError(info: '令牌流错误: ${streamEvent.content}');
localController.addError(streamEvent.content ?? '未知错误');
localController.close();
}
break;
}
);
});
// 当本地控制器关闭时,取消订阅
localController.onCancel = () {
@ -249,7 +243,8 @@ class OpenAIServiceAdapter implements AiService {
}
/// 注册函数
Future<bool> registerFunction(String name, String description, Map<String, dynamic> parameters) async {
Future<bool> registerFunction(
String name, String description, Map<String, dynamic> parameters) async {
try {
final result = await _openAIService.registerFunction(
name: name,

34
lib/modules/chat/controllers/chat_controller.dart

@ -109,9 +109,8 @@ class ChatController extends GetxController {
break;
default:
// 默认使用OpenAIServiceAdapter
_aiService = Get.find<OpenAIServiceAdapter>();
Logger.info('使用OpenAIServiceAdapter');
_aiService = Get.find<OpenAIServiceAdapter>();
Logger.info('使用OpenAIServiceAdapter');
}
// 使用克隆音色语音合成
@ -293,7 +292,8 @@ class ChatController extends GetxController {
_aiResponseSubscription = null;
// 将AI中间消息转换为正常消息,保留已输出的内容
final index = messages.indexWhere((m) => m.isIntermediate && m.role == 'assistant');
final index =
messages.indexWhere((m) => m.isIntermediate && m.role == 'assistant');
if (index != -1) {
messages[index] = messages[index].copyWith(
isIntermediate: false,
@ -413,7 +413,8 @@ class ChatController extends GetxController {
// 更新中间状态的消息
void _updateIntermediateMessage(String content) {
final index = messages.indexWhere((m) => m.isIntermediate && m.role == 'assistant');
final index =
messages.indexWhere((m) => m.isIntermediate && m.role == 'assistant');
if (index != -1) {
messages[index] = messages[index].copyWith(
content: content,
@ -426,7 +427,8 @@ class ChatController extends GetxController {
// 更新或添加最终响应
void _finalizeResponse(String content) {
// 查找AI的中间状态消息
final index = messages.indexWhere((m) => m.isIntermediate && m.role == 'assistant');
final index =
messages.indexWhere((m) => m.isIntermediate && m.role == 'assistant');
if (index != -1) {
// 更新中间状态消息为最终状态
messages[index] = messages[index].copyWith(
@ -461,7 +463,8 @@ class ChatController extends GetxController {
// 处理响应错误
void _handleResponseError() {
// 查找AI的中间状态消息
final index = messages.indexWhere((m) => m.isIntermediate && m.role == 'assistant');
final index =
messages.indexWhere((m) => m.isIntermediate && m.role == 'assistant');
if (index != -1) {
messages[index] = messages[index].copyWith(
content: '抱歉,我遇到了一些问题,无法回应您的请求。',
@ -590,7 +593,8 @@ class ChatController extends GetxController {
}
// 查找或创建用户的中间状态消息
final intermediateIndex = messages.indexWhere((m) => m.isIntermediate && m.role == 'user');
final intermediateIndex =
messages.indexWhere((m) => m.isIntermediate && m.role == 'user');
if (intermediateIndex >= 0) {
// 更新已有的中间状态消息
@ -622,7 +626,8 @@ class ChatController extends GetxController {
_updateActivityTime();
// 查找并移除用户的中间状态消息
final intermediateIndex = messages.indexWhere((m) => m.isIntermediate && m.role == 'user');
final intermediateIndex =
messages.indexWhere((m) => m.isIntermediate && m.role == 'user');
if (intermediateIndex >= 0) {
messages.removeAt(intermediateIndex);
}
@ -654,7 +659,8 @@ class ChatController extends GetxController {
// 清理语音识别资源
void _cleanupRecognition() {
// 查找并移除用户的中间状态消息
final intermediateIndex = messages.indexWhere((m) => m.isIntermediate && m.role == 'user');
final intermediateIndex =
messages.indexWhere((m) => m.isIntermediate && m.role == 'user');
if (intermediateIndex >= 0) {
if (messages[intermediateIndex].content.trim().isEmpty) {
messages.removeAt(intermediateIndex);
@ -689,7 +695,8 @@ class ChatController extends GetxController {
isSpeechDetected.value = false;
// 查找并处理用户的中间状态消息
final intermediateIndex = messages.indexWhere((m) => m.isIntermediate && m.role == 'user');
final intermediateIndex =
messages.indexWhere((m) => m.isIntermediate && m.role == 'user');
if (intermediateIndex >= 0) {
if (messages[intermediateIndex].content.trim().isEmpty) {
// 如果没有内容则移除
@ -728,7 +735,8 @@ class ChatController extends GetxController {
// 滚动监听,用于触发加载更多历史记录
void _scrollListener() {
// 当滚动到顶部并且有更多历史记录且不在加载中时,加载更多
if (scrollController.position.pixels == scrollController.position.minScrollExtent &&
if (scrollController.position.pixels ==
scrollController.position.minScrollExtent &&
_hasMoreHistory.value &&
!_isLoadingMore.value) {
_loadMoreHistory();
@ -767,7 +775,6 @@ class ChatController extends GetxController {
scrollController.jumpTo(scrollController.position.maxScrollExtent);
}
});
} catch (e) {
Logger.error('加载聊天历史失败', e);
} finally {
@ -802,7 +809,6 @@ class ChatController extends GetxController {
// 将旧消息插入到列表顶部
messages.insertAll(0, olderMessages.reversed);
} catch (e) {
Logger.error('加载更多聊天历史失败', e);
_currentPage.value--; // 加载失败,恢复页码

17
lib/modules/profile/controllers/profile_controller.dart

@ -13,7 +13,6 @@ import 'package:path_provider/path_provider.dart';
import '../../../data/services/network/api.dart';
import 'package:dio/dio.dart';
class ProfileController extends GetxController {
static ProfileController get to => Get.find();
@ -118,7 +117,7 @@ class ProfileController extends GetxController {
// _isEditing.value = !_isEditing.value;
// }
// 验证base64图片数据
// 验证base64图片数据
// Future<void> _verifyBase64Image(String base64String) async {
// try {
// // 解码base64数据
@ -171,8 +170,8 @@ class ProfileController extends GetxController {
// 准备数据
Map<String, dynamic> data = {
"image": "", // 默认空字符串
"language": currentLanguage.value,//当前用户语言
"name": userInfo.value.name,//当前用户名
"language": currentLanguage.value, //当前用户语言
"name": userInfo.value.name, //当前用户名
};
// 如果有头像,转换为base64
@ -196,7 +195,7 @@ class ProfileController extends GetxController {
}
final dio = Dio(BaseOptions(
baseUrl: 'https://www.deapsound.com:443',
baseUrl: 'https://api.deapsound.com:443',
headers: {
'accept': 'application/json',
'Authorization': _storage.read<String>("logintoken"),
@ -212,7 +211,6 @@ class ProfileController extends GetxController {
data: data,
);
// 调用API
//final response = await Api.updateUserSetting(data);
@ -254,7 +252,7 @@ class ProfileController extends GetxController {
}
// 下载并保存远程头像
Future<void> _downloadAndSaveAvatar(String url) async {
Future<void> _downloadAndSaveAvatar(String url) async {
try {
Logger().d('开始下载头像: $url');
@ -300,7 +298,7 @@ class ProfileController extends GetxController {
// 更新用户头像
Future<void> updateUserAvatar() async {
final XFile? image = await _picker.pickImage(
source: ImageSource.gallery,// 从相册选择
source: ImageSource.gallery, // 从相册选择
maxWidth: 800,
maxHeight: 800,
imageQuality: 85,
@ -374,7 +372,8 @@ class ProfileController extends GetxController {
if (Get.isRegistered<SettingsController>()) {
final settingsController = Get.find<SettingsController>();
settingsController.userInfo.value.name = userInfo.value.name;
settingsController.userInfo.value.avatar = '${userInfo.value.avatar}?time=${DateTime.now().millisecondsSinceEpoch}';
settingsController.userInfo.value.avatar =
'${userInfo.value.avatar}?time=${DateTime.now().millisecondsSinceEpoch}';
//settingsController.loadUserInfo();
settingsController.userInfo.refresh();
}

27
local_plugins/open_ai_service/android/src/main/kotlin/com/yunqiinnovation/open_ai_service/MCPClient.kt

@ -33,6 +33,7 @@ import okhttp3.Interceptor
import io.ktor.client.request.*
import io.ktor.http.*
import com.yunqiinnovation.open_ai_service.mcp.CustomSseClientTransport
import io.ktor.util.valuesOf
/**
* 工具类型枚举
@ -367,10 +368,10 @@ class MCPSubClient(
/**
* 调用工具
*/
suspend fun callTool(name: String, arguments: Map<String, Any>): String? {
suspend fun callTool(name: String, arguments: Map<String, Any>): JSONObject? {
if (!isConnected) {
Log.e(TAG, "未连接到MCP服务器", null)
return "未连接到MCP服务器"
return JSONObject().apply { put("context","未连接到MCP服务器") }
}
if (!containsTool(name)) {
@ -383,17 +384,17 @@ class MCPSubClient(
name = name,
arguments = arguments
)
// 处理工具调用结果
result?.content?.joinToString("\n") { content ->
val context = result?.content?.joinToString("\n") { content ->
when (content) {
is TextContent -> content.text ?: ""
else -> ""
}
}
JSONObject().apply {put("meta",result?._meta?.toMap()); put("context",context) }
} catch (e: Exception) {
Log.e(TAG, "调用MCP工具失败: ${e.message}", e)
"调用MCP工具失败: ${e.message}"
JSONObject().apply { put("context", "调用MCP工具失败: ${e.message}") }
}
}
@ -714,7 +715,7 @@ class MCPClient(private val context: Context? = null) : AutoCloseable {
* @param arguments 参数
* @return 调用结果
*/
suspend fun callTool(name: String, arguments: Map<String, Any>): String? {
suspend fun callTool(name: String, arguments: Map<String, Any>): JSONObject? {
val toolType = getToolType(name)
return when (toolType) {
@ -734,28 +735,30 @@ class MCPClient(private val context: Context? = null) : AutoCloseable {
/**
* 调用本地函数
*/
private suspend fun callLocalFunction(name: String, arguments: Map<String, Any>): String? {
private suspend fun callLocalFunction(name: String, arguments: Map<String, Any>): JSONObject? {
return try {
val handler = localFunctions[name]
if (handler != null) {
handler.handle(arguments)
val context = handler.handle(arguments)
JSONObject().apply { put("context", context) }
} else {
Log.e(TAG, "未找到本地函数: $name")
null
}
} catch (e: Exception) {
Log.e(TAG, "调用本地函数失败: ${e.message}", e)
"调用本地函数失败: ${e.message}"
JSONObject().apply { put("context", "调用本地函数失败: ${e.message}") }
}
}
/**
* 调用MCP工具
*/
private suspend fun callMcpTool(name: String, arguments: Map<String, Any>): String? {
private suspend fun callMcpTool(name: String, arguments: Map<String, Any>): JSONObject? {
if (!isConnected) {
Log.e(TAG, "未连接到MCP服务器", null)
return "未连接到MCP服务器"
return JSONObject().apply { put("context", "未连接到MCP服务器") }
}
// 查找包含此工具的子客户端
@ -769,7 +772,7 @@ class MCPClient(private val context: Context? = null) : AutoCloseable {
// 没有找到包含此工具的客户端
Log.e(TAG, "没有子客户端包含工具: $name")
return "没有可用的服务器提供工具: $name"
return JSONObject().apply { put("context", "没有可用的服务器提供工具: $name") }
}
/**

13
local_plugins/open_ai_service/android/src/main/kotlin/com/yunqiinnovation/open_ai_service/OpenAIService.kt

@ -164,9 +164,9 @@ class OpenAIService(private val context: Context? = null) {
* @param functionCall 函数调用JSON对象
* @return 处理结果,如果处理失败则返回null
*/
suspend fun handleMcpToolCall(functionCall: JSONObject): String? {
suspend fun handleMcpToolCall(functionCall: JSONObject): JSONObject? {
if (mcpClient == null || !isMcpInitialized) {
return "MCP客户端未初始化"
return JSONObject().apply { put("context", "MCP客户端未初始化") }
}
try {
@ -181,7 +181,7 @@ class OpenAIService(private val context: Context? = null) {
return mcpClient?.callTool(name, arguments)
} catch (e: Exception) {
Log.e(TAG, "处理MCP工具调用失败: ${e.message}", e)
return "处理MCP工具调用失败: ${e.message}"
return JSONObject().apply { put("context", "处理MCP工具调用失败: ${e.message}") }
}
}
@ -226,12 +226,17 @@ class OpenAIService(private val context: Context? = null) {
// 调用工具(统一通过MCPClient处理)
val result = mcpClient?.callTool(name, arguments)
var context = "工具调用失败";
if (result != null){
callback.onFunctionCall(result)
context = "${result["context"]}"
}
// 不论是本地函数还是MCP工具,都直接将结果发送回OpenAI
sendFunctionCallResult(
messages = messages,
functionCall = functionCall,
functionResult = result ?: "工具调用失败",
functionResult = context,
callback = callback
)
} catch (e: Exception) {

27
local_plugins/open_ai_service/android/src/main/kotlin/com/yunqiinnovation/open_ai_service/OpenAIServicePlugin.kt

@ -4,6 +4,7 @@ import android.content.Context
import android.os.Handler
import android.os.Looper
import android.util.Log
import kotlinx.serialization.json.Json
import androidx.annotation.NonNull
import io.flutter.embedding.engine.plugins.FlutterPlugin
import io.flutter.plugin.common.MethodCall
@ -163,10 +164,23 @@ class OpenAIServicePlugin : FlutterPlugin, MethodCallHandler, StreamHandler {
}
override fun onFunctionCall(functionCall: JSONObject) {
Log.e("OpenAIServicePlugin", "工具调用结果: ${functionCall.toString()}")
val context = functionCall["context"]
// val meta = functionCall["meta"]
val meta: Map<String, Any>? = functionCall.optJSONObject("meta")?.let { metaJson ->
metaJson.keys().asSequence().associateWith { key ->
metaJson.get(key) // 或 opt(key),更安全
}
}
val card_spotifyKey = meta?.containsKey("card_spotify") ?: false
if (card_spotifyKey) { //是否是音乐卡片
}
// 发送函数调用事件
mainHandler.post {
val functionCallMap = functionCall.toMap()
eventSink?.success(mapOf("type" to "functionCall", "content" to functionCallMap))
// val functionCallMap = functionCall.toMap()
eventSink?.success(mapOf("type" to "functionCall", "content" to context,"meta" to meta))
}
}
}
@ -229,11 +243,12 @@ class OpenAIServicePlugin : FlutterPlugin, MethodCallHandler, StreamHandler {
}
override fun onFunctionCall(nestedFunctionCall: JSONObject) {
Log.e("OpenAIServicePlugin", "工具调用结果: ${nestedFunctionCall.toString()}")
// 发送函数调用事件
mainHandler.post {
val functionCallMap = nestedFunctionCall.toMap()
eventSink?.success(mapOf("type" to "functionCall", "content" to functionCallMap))
}
// mainHandler.post {
// val functionCallMap = nestedFunctionCall.toMap()
// eventSink?.success(mapOf("type" to "functionCall", "content" to functionCallMap))
// }
}
}
)

2
local_plugins/open_ai_service/android/src/main/kotlin/com/yunqiinnovation/open_ai_service/mcp/CustomSseClientTransport.kt

@ -124,7 +124,7 @@ class CustomSseClientTransport(
val fullEndpoint = if (eventData.startsWith("/")) {
"$hostPart$eventData"
} else {
"$hostPart/$eventData"
"$eventData"
}
Log.d(TAG, "构建的endpoint路径(不含参数): $fullEndpoint")

16
local_plugins/open_ai_service/lib/open_ai_service.dart

@ -25,8 +25,9 @@ enum OpenAIEventType {
class OpenAIEvent {
final OpenAIEventType type;
final dynamic content;
final dynamic meta;
OpenAIEvent({required this.type, this.content});
OpenAIEvent({required this.type, this.content, this.meta});
factory OpenAIEvent.fromMap(Map<String, dynamic> map) {
final typeStr = map['type'] as String;
@ -56,8 +57,10 @@ class OpenAIEvent {
/// OpenAI服务插件
class OpenAIService {
static const MethodChannel _channel = MethodChannel('com.yunqiinnovation.open_ai_service/methods');
static const EventChannel _eventChannel = EventChannel('com.yunqiinnovation.open_ai_service/events');
static const MethodChannel _channel =
MethodChannel('com.yunqiinnovation.open_ai_service/methods');
static const EventChannel _eventChannel =
EventChannel('com.yunqiinnovation.open_ai_service/events');
/// 事件流控制器
StreamController<OpenAIEvent>? _eventStreamController;
@ -258,7 +261,6 @@ class OpenAIService {
}
}
/// 从JSON字符串解析函数调用
Map<String, dynamic> parseFunctionCall(String functionCallJson) {
try {
@ -327,7 +329,7 @@ class OpenAIService {
/// 处理函数调用(自动处理本地函数和MCP工具)
///
/// 这是一个辅助方法,用于简化函数调用处理流程:
/// 1. MCP工具会被自动处理,不会通过onFunctionCall回调返回
/// 1. MCP工具会被自动处理,不会通过回调返回
/// 2. 只有本地函数才需要手动处理
/// 3. 提供统一的处理接口
///
@ -341,7 +343,9 @@ class OpenAIService {
Future<bool> handleFunctionCall({
required Map<String, dynamic> functionCall,
required List<Map<String, dynamic>> messages,
required Future<String> Function(String name, Map<String, dynamic> arguments) localFunctionHandler,
required Future<String> Function(
String name, Map<String, dynamic> arguments)
localFunctionHandler,
}) async {
final name = functionCall['name'] as String;
final argumentsJson = functionCall['arguments'] as String;

62
local_plugins/spotify_sdk/.github/ISSUE_TEMPLATE/bug_report.md

@ -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.

1
local_plugins/spotify_sdk/.github/ISSUE_TEMPLATE/config.yml

@ -0,0 +1 @@
blank_issues_enabled: false

20
local_plugins/spotify_sdk/.github/ISSUE_TEMPLATE/feature_request.md

@ -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.

41
local_plugins/spotify_sdk/.github/actions/publish_flutter_package/action.yaml

@ -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 }}

42
local_plugins/spotify_sdk/.github/dependabot.yml

@ -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:
- "*"

16
local_plugins/spotify_sdk/.github/scripts/verify_pub_score.sh

@ -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

38
local_plugins/spotify_sdk/.github/workflows/pull_request.yml

@ -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

34
local_plugins/spotify_sdk/.github/workflows/release.yml

@ -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

50
local_plugins/spotify_sdk/.gitignore

@ -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/

10
local_plugins/spotify_sdk/.metadata

@ -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

184
local_plugins/spotify_sdk/CHANGELOG.md

@ -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

76
local_plugins/spotify_sdk/CODE_OF_CONDUCT.md

@ -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

48
local_plugins/spotify_sdk/CONTRIBUTING.md

@ -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).

201
local_plugins/spotify_sdk/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.

257
local_plugins/spotify_sdk/README.md

@ -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:
![image](https://user-images.githubusercontent.com/42183561/125422846-24e03bf0-ec7f-409f-b382-0ef2d0213d08.png)
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/)

12
local_plugins/spotify_sdk/analysis_options.yaml

@ -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

8
local_plugins/spotify_sdk/android/.gitignore

@ -0,0 +1,8 @@
*.iml
.gradle
/local.properties
/.idea/workspace.xml
/.idea/libraries
.DS_Store
/build
/captures

74
local_plugins/spotify_sdk/android/build.gradle

@ -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'
}
}

3
local_plugins/spotify_sdk/android/gradle.properties

@ -0,0 +1,3 @@
org.gradle.jvmargs=-Xmx1536M
android.useAndroidX=true
android.enableJetifier=true

7
local_plugins/spotify_sdk/android/gradle/wrapper/gradle-wrapper.properties

@ -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

BIN
local_plugins/spotify_sdk/android/libs/spotify-app-remote-release-0.8.0.aar

Binary file not shown.

1
local_plugins/spotify_sdk/android/settings.gradle

@ -0,0 +1 @@
rootProject.name = 'spotify_sdk'

10
local_plugins/spotify_sdk/android/src/main/AndroidManifest.xml

@ -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>

13
local_plugins/spotify_sdk/android/src/main/kotlin/de/minimalme/spotify_sdk/BaseSpotifyApi.kt

@ -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", "")
}
}

25
local_plugins/spotify_sdk/android/src/main/kotlin/de/minimalme/spotify_sdk/SpotifyConnectApi.kt

@ -0,0 +1,25 @@
package de.minimalme.spotify_sdk
import com.spotify.android.appremote.api.SpotifyAppRemote
import com.spotify.protocol.types.Image.Dimension
import com.spotify.protocol.types.ImageUri
import io.flutter.plugin.common.MethodChannel
import java.io.ByteArrayOutputStream
import android.graphics.Bitmap
class SpotifyConnectApi(spotifyAppRemote: SpotifyAppRemote?, result: MethodChannel.Result) : BaseSpotifyApi(spotifyAppRemote, result) {
private val errorConnectSwitchToLocalDevice = "errorConnectSwitchToLocalDevice"
private val connectApi = spotifyAppRemote?.connectApi
fun switchToLocalDevice() {
if (connectApi != null) {
connectApi.connectSwitchToLocalDevice()
.setResultCallback { result.success(true) }
.setErrorCallback { throwable -> result.error(errorConnectSwitchToLocalDevice, "error when switching to local device", throwable.toString()) }
} else {
spotifyRemoteAppNotSetError()
}
}
}

35
local_plugins/spotify_sdk/android/src/main/kotlin/de/minimalme/spotify_sdk/SpotifyImagesApi.kt

@ -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()
}
}
}

210
local_plugins/spotify_sdk/android/src/main/kotlin/de/minimalme/spotify_sdk/SpotifyPlayerApi.kt

@ -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()
}
}
}

381
local_plugins/spotify_sdk/android/src/main/kotlin/de/minimalme/spotify_sdk/SpotifySdkPlugin.kt

@ -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)

59
local_plugins/spotify_sdk/android/src/main/kotlin/de/minimalme/spotify_sdk/SpotifyUserApi.kt

@ -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()
}
}
}

23
local_plugins/spotify_sdk/android/src/main/kotlin/de/minimalme/spotify_sdk/subscriptions/CapabilitiesChannel.kt

@ -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?) {
}
}

27
local_plugins/spotify_sdk/android/src/main/kotlin/de/minimalme/spotify_sdk/subscriptions/ConnectionStatusChannel.kt

@ -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?) {
}
}

22
local_plugins/spotify_sdk/android/src/main/kotlin/de/minimalme/spotify_sdk/subscriptions/PlayerContextChannel.kt

@ -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?) {
}
}

22
local_plugins/spotify_sdk/android/src/main/kotlin/de/minimalme/spotify_sdk/subscriptions/PlayerStateChannel.kt

@ -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?) {
}
}

23
local_plugins/spotify_sdk/android/src/main/kotlin/de/minimalme/spotify_sdk/subscriptions/UserStatusChannel.kt

@ -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?) {
}
}

39
local_plugins/spotify_sdk/bin/android_cleanup.dart

@ -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');
}
}

96
local_plugins/spotify_sdk/bin/android_module_creator.dart

@ -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');
}
}

91
local_plugins/spotify_sdk/bin/android_setup.dart

@ -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;
}

100
local_plugins/spotify_sdk/bin/github_api.dart

@ -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));
}
}

52
local_plugins/spotify_sdk/bin/precondition_checker.dart

@ -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;
}
}

37
local_plugins/spotify_sdk/ios/.gitignore

@ -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
local_plugins/spotify_sdk/ios/Assets/.gitkeep

45
local_plugins/spotify_sdk/ios/Classes/ConnectionStatusHandler.swift

@ -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}")
}
}
}

13
local_plugins/spotify_sdk/ios/Classes/Extension/Dictionary.swift

@ -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
}
}
}

14
local_plugins/spotify_sdk/ios/Classes/PlayerDelegate.swift

@ -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)
}
}

18
local_plugins/spotify_sdk/ios/Classes/PlayerState/PlayerContextHandler.swift

@ -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
}
}

85
local_plugins/spotify_sdk/ios/Classes/PlayerState/PlayerState.swift

@ -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,
]
}
}

22
local_plugins/spotify_sdk/ios/Classes/PlayerState/PlayerStateHandler.swift

@ -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
}
}

4
local_plugins/spotify_sdk/ios/Classes/SpotifyError.swift

@ -0,0 +1,4 @@
enum SpotifyError: Error {
case spotifyNotInstalledError
case redirectURLInvalid
}

46
local_plugins/spotify_sdk/ios/Classes/SpotifySdkConstants.swift

@ -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"
}

4
local_plugins/spotify_sdk/ios/Classes/SpotifySdkPlugin.h

@ -0,0 +1,4 @@
#import <Flutter/Flutter.h>
@interface SpotifySdkPlugin : NSObject <FlutterPlugin>
@end

18
local_plugins/spotify_sdk/ios/Classes/SpotifySdkPlugin.m

@ -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

17
local_plugins/spotify_sdk/ios/Classes/StatusHandler.swift

@ -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
}
}

416
local_plugins/spotify_sdk/ios/Classes/SwiftSpotifySdkPlugin.swift

@ -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()
}
}

10
local_plugins/spotify_sdk/ios/prepare-iOS-SDK.sh

@ -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

26
local_plugins/spotify_sdk/ios/spotify_sdk.podspec

@ -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

17
local_plugins/spotify_sdk/lib/enums/image_dimension_enum.dart

@ -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
}

23
local_plugins/spotify_sdk/lib/enums/podcast_playback_speed.dart

@ -0,0 +1,23 @@
/// Holds the values from the spotify api for supported Podcast playback speeds
enum PodcastPlaybackSpeed {
/// 0.5 x playback speed
playbackSpeed_50,
/// 0.8 x playback speed
playbackSpeed_80,
/// 1 x playback speed
playbackSpeed_100,
/// 1.2 x playback speed
playbackSpeed_120,
/// 1.5 x playback speed
playbackSpeed_150,
/// 2 x playback speed
playbackSpeed_200,
/// 3 x playback speed
playbackSpeed_300,
}

11
local_plugins/spotify_sdk/lib/enums/repeat_mode_enum.dart

@ -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,
}

19
local_plugins/spotify_sdk/lib/extensions/image_dimension_extension.dart

@ -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]!;
}

21
local_plugins/spotify_sdk/lib/extensions/podcast_playback_speed_extension.dart

@ -0,0 +1,21 @@
import 'package:spotify_sdk/enums/podcast_playback_speed.dart';
///Extension for formatting the PodcastPlaybackSpeed enum to value
///@nodoc
extension PodcastPlaybackSpeedExtension on PodcastPlaybackSpeed {
///maps the value to the specified enum
///@nodoc
static const values = {
PodcastPlaybackSpeed.playbackSpeed_50: 50,
PodcastPlaybackSpeed.playbackSpeed_80: 80,
PodcastPlaybackSpeed.playbackSpeed_100: 100,
PodcastPlaybackSpeed.playbackSpeed_120: 120,
PodcastPlaybackSpeed.playbackSpeed_150: 150,
PodcastPlaybackSpeed.playbackSpeed_200: 200,
PodcastPlaybackSpeed.playbackSpeed_300: 300,
};
/// returns the value
///@nodoc
int get value => values[this]!;
}

8
local_plugins/spotify_sdk/lib/logging/custom_log_filter.dart

@ -0,0 +1,8 @@
import 'package:logger/logger.dart';
class CustomLogFilter extends LogFilter {
@override
bool shouldLog(LogEvent event) {
return true;
}
}

15
local_plugins/spotify_sdk/lib/models/album.dart

@ -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);
}

17
local_plugins/spotify_sdk/lib/models/album.g.dart

@ -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,
};

15
local_plugins/spotify_sdk/lib/models/artist.dart

@ -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);
}

17
local_plugins/spotify_sdk/lib/models/artist.g.dart

@ -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,
};

18
local_plugins/spotify_sdk/lib/models/capabilities.dart

@ -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);
}

16
local_plugins/spotify_sdk/lib/models/capabilities.g.dart

@ -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,
};

31
local_plugins/spotify_sdk/lib/models/connection_status.dart

@ -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);
}

23
local_plugins/spotify_sdk/lib/models/connection_status.g.dart

@ -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,
};

19
local_plugins/spotify_sdk/lib/models/crossfade_state.dart

@ -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);
}

19
local_plugins/spotify_sdk/lib/models/crossfade_state.g.dart

@ -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,
};

15
local_plugins/spotify_sdk/lib/models/image_uri.dart

@ -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);
}

15
local_plugins/spotify_sdk/lib/models/image_uri.g.dart

@ -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,
};

24
local_plugins/spotify_sdk/lib/models/library_state.dart

@ -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);
}

20
local_plugins/spotify_sdk/lib/models/library_state.g.dart

@ -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,
};

18
local_plugins/spotify_sdk/lib/models/player_context.dart

@ -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);
}

23
local_plugins/spotify_sdk/lib/models/player_context.g.dart

@ -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,
};

30
local_plugins/spotify_sdk/lib/models/player_options.dart

@ -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
}

25
local_plugins/spotify_sdk/lib/models/player_options.g.dart

@ -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,
};

33
local_plugins/spotify_sdk/lib/models/player_restrictions.dart

@ -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);
}

27
local_plugins/spotify_sdk/lib/models/player_restrictions.g.dart

@ -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,
};

36
local_plugins/spotify_sdk/lib/models/player_state.dart

@ -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);
}

29
local_plugins/spotify_sdk/lib/models/player_state.g.dart

@ -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,
};

43
local_plugins/spotify_sdk/lib/models/track.dart

@ -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);
}

35
local_plugins/spotify_sdk/lib/models/track.g.dart

@ -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,
};

26
local_plugins/spotify_sdk/lib/models/user_status.dart

@ -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…
Cancel
Save