Browse Source

chore: 更新 Flutter Web 插件以使用 js_interop 和 web 包

迁移 Spotify SDK Web 插件从 dart:html 和 js 包到 dart:js_interop 和 web 包,以适配 Dart 3.7 的 JS 互操作改进。主要更改包括:
- 更新 pubspec.yaml 中的 SDK 约束和依赖项
- 使用 extension types 重写 JS 类型定义
- 用新的互操作方法替换 allowInterop 和 promiseToFuture
- 将 DOM 操作迁移到 web 包 API
- 修复类型转换和事件处理逻辑
weicu
lxm 7 months ago
parent
commit
c29822ad85
  1. 243
      local_plugins/spotify_sdk/lib/spotify_sdk_web.dart
  2. 4
      local_plugins/spotify_sdk/pubspec.yaml

243
local_plugins/spotify_sdk/lib/spotify_sdk_web.dart

@ -5,16 +5,15 @@ import 'dart:async';
import 'dart:convert';
import 'dart:core';
import 'dart:developer';
import 'dart:html';
import 'dart:js_interop';
import 'dart:math' as math;
import 'package:crypto/crypto.dart';
import 'package:dio/dio.dart';
import 'package:flutter/services.dart';
import 'package:flutter_web_plugins/flutter_web_plugins.dart';
import 'package:js/js.dart';
import 'package:js/js_util.dart';
import 'package:synchronized/synchronized.dart' as synchronized;
import 'package:web/web.dart' as web;
import 'enums/repeat_mode_enum.dart';
import 'models/album.dart';
@ -52,6 +51,9 @@ class SpotifySdkPlugin {
/// Current Spotify SDK player instance.
Player? _currentPlayer;
/// Current device ID (存储在 Dart 侧,因为 extension type 不支持实例字段)
String? _currentDeviceID;
/// Current Spotify auth token.
SpotifyToken? _spotifyToken;
@ -172,18 +174,19 @@ class SpotifySdkPlugin {
// create player
_currentPlayer = Player(PlayerOptions(
name: playerName,
getOAuthToken: allowInterop((Function callback, t) {
getOAuthToken: ((JSAny? callback, JSAny? t) {
_getSpotifyAuthToken().then((value) {
callback(value);
(callback as JSFunction).callAsFunction(null, value.toJS);
});
})));
}).toJS));
_registerPlayerEvents(_currentPlayer!);
var result = await promiseToFuture(_currentPlayer!.connect());
var result =
(await _currentPlayer!.connect().toDart).toDart;
if (result == true) {
// wait for the confirmation
num time = 0;
while (_currentPlayer!.deviceID == null) {
while (_currentDeviceID == null) {
await Future.delayed(const Duration(milliseconds: 200));
time += 200;
if (time > 10000) {
@ -242,19 +245,19 @@ class SpotifySdkPlugin {
call.arguments[ParamNames.repeatMode] as RepeatMode?);
break;
case MethodNames.resume:
await promiseToFuture(_currentPlayer?.resume());
await _currentPlayer!.resume().toDart;
break;
case MethodNames.pause:
await promiseToFuture(_currentPlayer?.pause());
await _currentPlayer!.pause().toDart;
break;
case MethodNames.skipNext:
await promiseToFuture(_currentPlayer?.nextTrack());
await _currentPlayer!.nextTrack().toDart;
break;
case MethodNames.skipPrevious:
await promiseToFuture(_currentPlayer?.previousTrack());
await _currentPlayer!.previousTrack().toDart;
break;
case MethodNames.getPlayerState:
var stateRaw = await promiseToFuture(_currentPlayer?.getCurrentState())
var stateRaw = (await _currentPlayer!.getCurrentState().toDart)
as WebPlaybackState?;
if (stateRaw == null) return null;
return jsonEncode(toPlayerState(stateRaw)!.toJson());
@ -272,10 +275,13 @@ class SpotifySdkPlugin {
log('Loading Spotify SDK...');
// link spotify ready function
_onSpotifyWebPlaybackSDKReady = allowInterop(_onSpotifyInitialized);
_onSpotifyWebPlaybackSDKReady = _onSpotifyInitialized.toJS;
// load spotify sdk
querySelector('body')!.children.add(ScriptElement()..src = spotifySdkUrl);
final script =
web.document.createElement('script') as web.HTMLScriptElement;
script.src = spotifySdkUrl;
web.document.body!.appendChild(script);
// wait for initialization
while (_sdkLoaded == false) {
@ -294,33 +300,37 @@ class SpotifySdkPlugin {
void _registerPlayerEvents(Player player) {
// player state
player.addListener('player_state_changed',
allowInterop((WebPlaybackState? state) {
if (state == null) return;
((JSAny? stateObj) {
if (stateObj == null) return;
var state = stateObj as WebPlaybackState;
playerStateEventController
.add(jsonEncode(toPlayerState(state)!.toJson()));
playerContextEventController
.add(jsonEncode(toPlayerContext(state)!.toJson()));
}));
}).toJS);
// ready/not ready
player.addListener('ready', allowInterop((WebPlaybackPlayer player) {
player.addListener('ready', ((JSAny? playerObj) {
var p = playerObj as WebPlaybackPlayer;
log('Spotify SDK ready!');
_onSpotifyConnected(player.device_id);
}));
player.addListener('not_ready', allowInterop((event) {
_onSpotifyConnected(p.device_id);
}).toJS);
player.addListener('not_ready', ((JSAny? event) {
_onSpotifyDisconnected(
errorCode: 'Spotify SDK not ready',
errorDetails: 'Spotify SDK is not ready to take requests');
}));
}).toJS);
// errors
player.addListener('initialization_error',
allowInterop((WebPlaybackError error) {
((JSAny? errorObj) {
var error = errorObj as WebPlaybackError;
_onSpotifyDisconnected(
errorCode: 'Initialization Error', errorDetails: error.message);
}));
}).toJS);
player.addListener('authentication_error',
allowInterop((WebPlaybackError error) {
((JSAny? errorObj) {
var error = errorObj as WebPlaybackError;
// If the error is due to browser security, don't disconnect.
// The user needs to interact with the SDK to trigger media activation.
// https://developer.spotify.com/documentation/web-playback-sdk/quick-start/#mobile-support
@ -330,14 +340,16 @@ class SpotifySdkPlugin {
}
_onSpotifyDisconnected(
errorCode: 'Authentication Error', errorDetails: error.message);
}));
player.addListener('account_error', allowInterop((WebPlaybackError error) {
}).toJS);
player.addListener('account_error', ((JSAny? errorObj) {
var error = errorObj as WebPlaybackError;
_onSpotifyDisconnected(
errorCode: 'Account Error', errorDetails: error.message);
}));
player.addListener('playback_error', allowInterop((WebPlaybackError error) {
}).toJS);
player.addListener('playback_error', ((JSAny? errorObj) {
var error = errorObj as WebPlaybackError;
log('playback_error: ${error.message}');
}));
}).toJS);
}
/// Called when the Spotify SDK is first loaded.
@ -347,7 +359,7 @@ class SpotifySdkPlugin {
/// Called when the plugin successfully connects to the spotify web sdk.
void _onSpotifyConnected(String deviceId) {
_currentPlayer!.deviceID = deviceId;
_currentDeviceID = deviceId;
// emit connected event
connectionStatusEventController.add(jsonEncode(ConnectionStatus(
@ -362,6 +374,7 @@ class SpotifySdkPlugin {
void _onSpotifyDisconnected({String? errorCode, String? errorDetails}) {
_unregisterPlayerEvents(_currentPlayer!);
_currentPlayer = null;
_currentDeviceID = null;
if (errorCode != null) {
// disconnected due to error
@ -445,27 +458,32 @@ class SpotifySdkPlugin {
);
// opening auth window
var authPopup = window.open(
var authPopup = web.window.open(
authorizationUri.toString(),
'Spotify Authorization',
);
String? message;
var sub = window.onMessage.listen(allowInterop((event) {
message = event.data.toString();
// ensure the message contains auth code
if (!message!.startsWith('?code=')) {
message = null;
final messageHandler = ((JSAny? e) {
final event = e as web.MessageEvent;
if (event.data != null && event.data!.isA<JSString>()) {
final str = (event.data as JSString).toDart;
// ensure the message contains auth code
if (str.startsWith('?code=')) {
message = str;
}
}
}));
}).toJS;
web.window.addEventListener('message', messageHandler);
// loop and wait for auth
while (authPopup.closed == false && message == null) {
while (authPopup?.closed == false && message == null) {
// await response from the window
await Future.delayed(const Duration(milliseconds: 250));
}
// error if window closed by user
if (message == null) {
web.window.removeEventListener('message', messageHandler);
throw PlatformException(
message: 'User closed authentication window',
code: 'Authentication Error');
@ -476,6 +494,7 @@ class SpotifySdkPlugin {
// check if state is the same
if (state != parsedMessage.queryParameters['state']) {
web.window.removeEventListener('message', messageHandler);
throw PlatformException(
message: 'Invalid state', code: 'Authentication Error');
}
@ -483,16 +502,17 @@ class SpotifySdkPlugin {
// check for error
if (parsedMessage.queryParameters['error'] != null ||
parsedMessage.queryParameters['code'] == null) {
web.window.removeEventListener('message', messageHandler);
throw PlatformException(
message: "${parsedMessage.queryParameters['error']}",
code: 'Authentication Error');
}
// close auth window
if (authPopup.closed == false) {
authPopup.close();
if (authPopup?.closed == false) {
authPopup?.close();
}
await sub.cancel();
web.window.removeEventListener('message', messageHandler);
// exchange auth code for access and refresh tokens
dynamic authResponse;
@ -613,7 +633,7 @@ class SpotifySdkPlugin {
/// Starts track playback on the device.
Future _play(String? uri) async {
if (_currentPlayer?.deviceID == null) {
if (_currentDeviceID == null) {
throw PlatformException(
message: 'Spotify player not connected!', code: 'Playback Error');
}
@ -623,7 +643,7 @@ class SpotifySdkPlugin {
data: {
'uris': [uri]
},
queryParameters: {'device_id': _currentPlayer!.deviceID},
queryParameters: {'device_id': _currentDeviceID},
options: Options(
headers: {
'Content-Type': 'application/json',
@ -635,14 +655,14 @@ class SpotifySdkPlugin {
/// Adds a given track to the playback queue.
Future _queue(String? uri) async {
if (_currentPlayer?.deviceID == null) {
if (_currentDeviceID == null) {
throw PlatformException(
message: 'Spotify player not connected!', code: 'Playback Error');
}
await _dio.post(
'/queue',
queryParameters: {'uri': uri, 'device_id': _currentPlayer!.deviceID},
queryParameters: {'uri': uri, 'device_id': _currentDeviceID},
options: Options(
headers: {
'Content-Type': 'application/json',
@ -654,7 +674,7 @@ class SpotifySdkPlugin {
/// Sets whether shuffle should be enabled.
Future _setShuffle(bool? shuffleEnabled) async {
if (_currentPlayer?.deviceID == null) {
if (_currentDeviceID == null) {
throw PlatformException(
message: 'Spotify player not connected!', code: 'Set Shuffle Error');
}
@ -663,7 +683,7 @@ class SpotifySdkPlugin {
'/shuffle',
queryParameters: {
'state': shuffleEnabled,
'device_id': _currentPlayer!.deviceID
'device_id': _currentDeviceID
},
options: Options(
headers: {
@ -676,7 +696,7 @@ class SpotifySdkPlugin {
/// Sets the repeat mode.
Future _setRepeatMode(RepeatMode? repeatMode) async {
if (_currentPlayer?.deviceID == null) {
if (_currentDeviceID == null) {
throw PlatformException(
message: 'Spotify player not connected!',
code: 'Set Repeat Mode Error');
@ -686,7 +706,7 @@ class SpotifySdkPlugin {
'/repeat',
queryParameters: {
'state': repeatMode.toString().substring(11),
'device_id': _currentPlayer!.deviceID
'device_id': _currentDeviceID
},
options: Options(
headers: {
@ -699,14 +719,14 @@ class SpotifySdkPlugin {
/// Toggles shuffle on the current player.
Future toggleShuffle({bool? state}) async {
if (_currentPlayer?.deviceID == null) {
if (_currentDeviceID == null) {
throw PlatformException(
message: 'Spotify player not connected!', code: 'Playback Error');
}
await _dio.put(
'/shuffle',
queryParameters: {'state': state, 'device_id': _currentPlayer!.deviceID},
queryParameters: {'state': state, 'device_id': _currentDeviceID},
options: Options(
headers: {
'Content-Type': 'application/json',
@ -718,14 +738,14 @@ class SpotifySdkPlugin {
/// Toggles repeat on the current player.
Future toggleRepeat({bool? state}) async {
if (_currentPlayer?.deviceID == null) {
if (_currentDeviceID == null) {
throw PlatformException(
message: 'Spotify player not connected!', code: 'Playback Error');
}
await _dio.put(
'/repeat',
queryParameters: {'state': state, 'device_id': _currentPlayer!.deviceID},
queryParameters: {'state': state, 'device_id': _currentDeviceID},
options: Options(
headers: {
'Content-Type': 'application/json',
@ -744,7 +764,8 @@ class SpotifySdkPlugin {
var artists = <Artist>[];
if (trackRaw != null) {
for (var artist in trackRaw.artists) {
for (var i = 0; i < trackRaw.artists.length; i++) {
var artist = trackRaw.artists[i] as WebPlaybackArtist;
artists.add(Artist(artist.name, artist.uri));
}
}
@ -770,7 +791,7 @@ class SpotifySdkPlugin {
artists[0],
artists,
-1,
ImageUri(albumRaw.images[0].url),
ImageUri((albumRaw.images[0] as WebPlaybackAlbumImage).url),
trackRaw.name,
trackRaw.uri,
trackRaw.linked_from.uri,
@ -806,87 +827,79 @@ class SpotifySdkPlugin {
/// Allows assigning the function onSpotifyWebPlaybackSDKReady
/// to be callable from `window.onSpotifyWebPlaybackSDKReady()`
@JS('onSpotifyWebPlaybackSDKReady')
external set _onSpotifyWebPlaybackSDKReady(void Function()? f);
external set _onSpotifyWebPlaybackSDKReady(JSFunction? f);
/// Allows assigning the function onSpotifyWebPlaybackSDKReady
/// to be callable from `window.onSpotifyWebPlaybackSDKReady()`
/// Allows checking if onSpotifyWebPlaybackSDKReady has been set
@JS('onSpotifyWebPlaybackSDKReady')
external void Function()? get _onSpotifyWebPlaybackSDKReady;
external JSFunction? get _onSpotifyWebPlaybackSDKReady;
/// Spotify Player Object
@JS('Spotify.Player')
class Player {
/// Device id of the player.
String? deviceID;
extension type Player._(JSObject _) implements JSObject {
/// The main constructor for initializing the Web Playback SDK.
/// It should contain an object with the player name, volume and access token.
external Player(PlayerOptions options);
/// Connects Web Playback SDK instance to Spotify
/// with the credentials provided during initialization.
external dynamic connect();
external JSPromise<JSBoolean> connect();
/// Closes the current session that Web Playback SDK has with Spotify.
external void disconnect();
/// Create a new event listener in the Web Playback SDK.
external void addListener(String type, Function callback);
external void addListener(String type, JSFunction callback);
/// Remove an event listener in the Web Playback SDK.
// ignore: non_constant_identifier_names
external void removeListener(String event_name);
/// Collect metadata on local playback.
external dynamic getCurrentState();
external JSPromise<JSAny?> getCurrentState();
/// Rename the Spotify Player device.
/// This is visible across all Spotify Connect devices.
external dynamic setName(String name);
external JSPromise setName(String name);
/// Set the local volume for the Web Playback SDK.
external dynamic setVolume(double volume);
external JSPromise setVolume(double volume);
/// Pause the local playback.
external dynamic pause();
external JSPromise pause();
/// Resume the local playback.
external dynamic resume();
external JSPromise resume();
/// Resume/pause the local playback.
external dynamic togglePlay();
external JSPromise togglePlay();
/// Seek to a position in the current track in local playback.
// ignore: non_constant_identifier_names
external dynamic seek(int position_ms);
external JSPromise seek(int position_ms);
/// Switch to the previous track in local playback.
external dynamic previousTrack();
external JSPromise previousTrack();
/// Skip to the next track in local playback.
external dynamic nextTrack();
external JSPromise nextTrack();
}
/// Spotify player options object
@JS()
@anonymous
class PlayerOptions {
extension type PlayerOptions._(JSObject _) implements JSObject {
// ignore: public_member_api_docs
external String get name;
// ignore: public_member_api_docs
external Function get getOAuthToken;
external JSFunction get getOAuthToken;
// ignore: public_member_api_docs
external double get volume;
// ignore: public_member_api_docs
external factory PlayerOptions(
{String? name, Function? getOAuthToken, double? volume});
{String? name, JSFunction? getOAuthToken, double? volume});
}
/// Spotify playback object
@JS()
@anonymous
class WebPlaybackPlayer {
extension type WebPlaybackPlayer._(JSObject _) implements JSObject {
// ignore: public_member_api_docs, non_constant_identifier_names
external String get device_id;
// ignore: public_member_api_docs, non_constant_identifier_names
@ -894,9 +907,7 @@ class WebPlaybackPlayer {
}
/// Spotify playback state object
@JS()
@anonymous
class WebPlaybackState {
extension type WebPlaybackState._(JSObject _) implements JSObject {
// ignore: public_member_api_docs
external WebPlayerContext get context;
// ignore: public_member_api_docs
@ -916,7 +927,7 @@ class WebPlaybackState {
external factory WebPlaybackState(
{WebPlayerContext? context,
WebPlayerDisallows? disallows,
bool? paysed,
bool? paused,
int? position,
// ignore: non_constant_identifier_names
int? repeat_mode,
@ -926,9 +937,7 @@ class WebPlaybackState {
}
/// Spotify player context object
@JS()
@anonymous
class WebPlayerContext {
extension type WebPlayerContext._(JSObject _) implements JSObject {
// ignore: public_member_api_docs
external String get uri;
// ignore: public_member_api_docs
@ -940,9 +949,7 @@ class WebPlayerContext {
}
/// Spotify player context metadata object
@JS()
@anonymous
class WebPlayerContextMetadata {
extension type WebPlayerContextMetadata._(JSObject _) implements JSObject {
// ignore: public_member_api_docs
external String get title;
// ignore: public_member_api_docs
@ -956,9 +963,7 @@ class WebPlayerContextMetadata {
}
/// Spotify player disallows object
@JS()
@anonymous
class WebPlayerDisallows {
extension type WebPlayerDisallows._(JSObject _) implements JSObject {
// ignore: public_member_api_docs
external bool get pausing;
// ignore: public_member_api_docs, non_constant_identifier_names
@ -990,30 +995,26 @@ class WebPlayerDisallows {
}
/// Spotify player track window object
@JS()
@anonymous
class WebPlayerTrackWindow {
extension type WebPlayerTrackWindow._(JSObject _) implements JSObject {
// ignore: public_member_api_docs, non_constant_identifier_names
external WebPlaybackTrack? get current_track;
// ignore: public_member_api_docs, non_constant_identifier_names
external List<WebPlaybackTrack>? get previous_tracks;
external JSArray? get previous_tracks;
// ignore: public_member_api_docs, non_constant_identifier_names
external List<WebPlaybackTrack>? get next_tracks;
external JSArray? get next_tracks;
// ignore: public_member_api_docs
external factory WebPlayerTrackWindow(
// ignore: non_constant_identifier_names
{WebPlaybackTrack? current_track,
// ignore: non_constant_identifier_names
List<WebPlaybackTrack>? previous_tracks,
JSArray? previous_tracks,
// ignore: non_constant_identifier_names
List<WebPlaybackTrack>? next_tracks});
JSArray? next_tracks});
}
/// Spotify playback track object
@JS()
@anonymous
class WebPlaybackTrack {
extension type WebPlaybackTrack._(JSObject _) implements JSObject {
// ignore: public_member_api_docs
external String get uri;
// ignore: public_member_api_docs
@ -1029,7 +1030,7 @@ class WebPlaybackTrack {
// ignore: public_member_api_docs
external WebPlaybackAlbum get album;
// ignore: public_member_api_docs
external List<WebPlaybackArtist> get artists;
external JSArray get artists;
// ignore: public_member_api_docs
// ignore: non_constant_identifier_names
external WebLinkedFrom get linked_from;
@ -1045,31 +1046,27 @@ class WebPlaybackTrack {
// ignore: non_constant_identifier_names
bool? is_playable,
WebPlaybackAlbum? album,
List<WebPlaybackArtist>? artists,
JSArray? artists,
// ignore: non_constant_identifier_names
WebLinkedFrom? linked_from});
}
/// Spotify playback album object
@JS()
@anonymous
class WebPlaybackAlbum {
extension type WebPlaybackAlbum._(JSObject _) implements JSObject {
// ignore: public_member_api_docs
external String get uri;
// ignore: public_member_api_docs
external String get name;
// ignore: public_member_api_docs
external List<WebPlaybackAlbumImage> get images;
external JSArray get images;
// ignore: public_member_api_docs
external factory WebPlaybackAlbum(
{String? uri, String? name, List<WebPlaybackAlbumImage>? images});
{String? uri, String? name, JSArray? images});
}
/// Spotify playback album object
@JS()
@anonymous
class WebLinkedFrom {
extension type WebLinkedFrom._(JSObject _) implements JSObject {
// ignore: public_member_api_docs
external String get uri;
// ignore: public_member_api_docs
@ -1080,9 +1077,7 @@ class WebLinkedFrom {
}
/// Spotify artist object
@JS()
@anonymous
class WebPlaybackArtist {
extension type WebPlaybackArtist._(JSObject _) implements JSObject {
// ignore: public_member_api_docs
external String get uri;
// ignore: public_member_api_docs
@ -1093,9 +1088,7 @@ class WebPlaybackArtist {
}
/// Spotify album image object
@JS()
@anonymous
class WebPlaybackAlbumImage {
extension type WebPlaybackAlbumImage._(JSObject _) implements JSObject {
// ignore: public_member_api_docs
external String get url;
@ -1104,9 +1097,7 @@ class WebPlaybackAlbumImage {
}
/// Spotify playback error object
@JS()
@anonymous
class WebPlaybackError {
extension type WebPlaybackError._(JSObject _) implements JSObject {
// ignore: public_member_api_docs
external String get message;

4
local_plugins/spotify_sdk/pubspec.yaml

@ -5,7 +5,7 @@ homepage: https://github.com/brim-borium/spotify_sdk
issue_tracker: https://github.com/brim-borium/spotify_sdk/issues
environment:
sdk: ^3.5.0
sdk: ^3.7.0
flutter: '>=3.3.0'
dependencies:
@ -16,7 +16,7 @@ dependencies:
logger: ^2.4.0
dio: ^5.7.0
json_annotation: ^4.9.0
js: ">=0.6.7 <0.8.0"
web: ^1.1.0
crypto: ^3.0.5
synchronized: ^3.3.0+3
http: ^1.2.2

Loading…
Cancel
Save