feat: implement metadata plugins based on hetu

This commit is contained in:
Kingkor Roy Tirtho 2025-06-14 08:41:38 +06:00
parent 69c0333327
commit 7a6821f28d
32 changed files with 577 additions and 1397 deletions

View File

@ -235,9 +235,5 @@ class AppRouter extends RootStackRouter {
page: LastFMLoginRoute.page, page: LastFMLoginRoute.page,
// parentNavigatorKey: rootNavigatorKey, // parentNavigatorKey: rootNavigatorKey,
), ),
AutoRoute(
path: "/webview",
page: WebviewRoute.page,
),
]; ];
} }

File diff suppressed because it is too large Load Diff

View File

@ -147,7 +147,7 @@ class Spotube extends HookConsumerWidget {
ref.listen(bonsoirProvider, (_, __) {}); ref.listen(bonsoirProvider, (_, __) {});
ref.listen(connectClientsProvider, (_, __) {}); ref.listen(connectClientsProvider, (_, __) {});
ref.listen(metadataPluginsProvider, (_, __) {}); ref.listen(metadataPluginsProvider, (_, __) {});
ref.listen(metadataPluginApiProvider, (_, __) {}); ref.listen(metadataPluginProvider, (_, __) {});
ref.listen(serverProvider, (_, __) {}); ref.listen(serverProvider, (_, __) {});
ref.listen(trayManagerProvider, (_, __) {}); ref.listen(trayManagerProvider, (_, __) {});

View File

@ -3,9 +3,9 @@ part of 'metadata.dart';
@freezed @freezed
class SpotubeImageObject with _$SpotubeImageObject { class SpotubeImageObject with _$SpotubeImageObject {
factory SpotubeImageObject({ factory SpotubeImageObject({
required final String url, required String url,
required final int width, int? width,
required final int height, int? height,
}) = _SpotubeImageObject; }) = _SpotubeImageObject;
factory SpotubeImageObject.fromJson(Map<String, dynamic> json) => factory SpotubeImageObject.fromJson(Map<String, dynamic> json) =>

View File

@ -775,8 +775,8 @@ SpotubeImageObject _$SpotubeImageObjectFromJson(Map<String, dynamic> json) {
/// @nodoc /// @nodoc
mixin _$SpotubeImageObject { mixin _$SpotubeImageObject {
String get url => throw _privateConstructorUsedError; String get url => throw _privateConstructorUsedError;
int get width => throw _privateConstructorUsedError; int? get width => throw _privateConstructorUsedError;
int get height => throw _privateConstructorUsedError; int? get height => throw _privateConstructorUsedError;
/// Serializes this SpotubeImageObject to a JSON map. /// Serializes this SpotubeImageObject to a JSON map.
Map<String, dynamic> toJson() => throw _privateConstructorUsedError; Map<String, dynamic> toJson() => throw _privateConstructorUsedError;
@ -794,7 +794,7 @@ abstract class $SpotubeImageObjectCopyWith<$Res> {
SpotubeImageObject value, $Res Function(SpotubeImageObject) then) = SpotubeImageObject value, $Res Function(SpotubeImageObject) then) =
_$SpotubeImageObjectCopyWithImpl<$Res, SpotubeImageObject>; _$SpotubeImageObjectCopyWithImpl<$Res, SpotubeImageObject>;
@useResult @useResult
$Res call({String url, int width, int height}); $Res call({String url, int? width, int? height});
} }
/// @nodoc /// @nodoc
@ -813,22 +813,22 @@ class _$SpotubeImageObjectCopyWithImpl<$Res, $Val extends SpotubeImageObject>
@override @override
$Res call({ $Res call({
Object? url = null, Object? url = null,
Object? width = null, Object? width = freezed,
Object? height = null, Object? height = freezed,
}) { }) {
return _then(_value.copyWith( return _then(_value.copyWith(
url: null == url url: null == url
? _value.url ? _value.url
: url // ignore: cast_nullable_to_non_nullable : url // ignore: cast_nullable_to_non_nullable
as String, as String,
width: null == width width: freezed == width
? _value.width ? _value.width
: width // ignore: cast_nullable_to_non_nullable : width // ignore: cast_nullable_to_non_nullable
as int, as int?,
height: null == height height: freezed == height
? _value.height ? _value.height
: height // ignore: cast_nullable_to_non_nullable : height // ignore: cast_nullable_to_non_nullable
as int, as int?,
) as $Val); ) as $Val);
} }
} }
@ -841,7 +841,7 @@ abstract class _$$SpotubeImageObjectImplCopyWith<$Res>
__$$SpotubeImageObjectImplCopyWithImpl<$Res>; __$$SpotubeImageObjectImplCopyWithImpl<$Res>;
@override @override
@useResult @useResult
$Res call({String url, int width, int height}); $Res call({String url, int? width, int? height});
} }
/// @nodoc /// @nodoc
@ -858,22 +858,22 @@ class __$$SpotubeImageObjectImplCopyWithImpl<$Res>
@override @override
$Res call({ $Res call({
Object? url = null, Object? url = null,
Object? width = null, Object? width = freezed,
Object? height = null, Object? height = freezed,
}) { }) {
return _then(_$SpotubeImageObjectImpl( return _then(_$SpotubeImageObjectImpl(
url: null == url url: null == url
? _value.url ? _value.url
: url // ignore: cast_nullable_to_non_nullable : url // ignore: cast_nullable_to_non_nullable
as String, as String,
width: null == width width: freezed == width
? _value.width ? _value.width
: width // ignore: cast_nullable_to_non_nullable : width // ignore: cast_nullable_to_non_nullable
as int, as int?,
height: null == height height: freezed == height
? _value.height ? _value.height
: height // ignore: cast_nullable_to_non_nullable : height // ignore: cast_nullable_to_non_nullable
as int, as int?,
)); ));
} }
} }
@ -881,8 +881,7 @@ class __$$SpotubeImageObjectImplCopyWithImpl<$Res>
/// @nodoc /// @nodoc
@JsonSerializable() @JsonSerializable()
class _$SpotubeImageObjectImpl implements _SpotubeImageObject { class _$SpotubeImageObjectImpl implements _SpotubeImageObject {
_$SpotubeImageObjectImpl( _$SpotubeImageObjectImpl({required this.url, this.width, this.height});
{required this.url, required this.width, required this.height});
factory _$SpotubeImageObjectImpl.fromJson(Map<String, dynamic> json) => factory _$SpotubeImageObjectImpl.fromJson(Map<String, dynamic> json) =>
_$$SpotubeImageObjectImplFromJson(json); _$$SpotubeImageObjectImplFromJson(json);
@ -890,9 +889,9 @@ class _$SpotubeImageObjectImpl implements _SpotubeImageObject {
@override @override
final String url; final String url;
@override @override
final int width; final int? width;
@override @override
final int height; final int? height;
@override @override
String toString() { String toString() {
@ -933,8 +932,8 @@ class _$SpotubeImageObjectImpl implements _SpotubeImageObject {
abstract class _SpotubeImageObject implements SpotubeImageObject { abstract class _SpotubeImageObject implements SpotubeImageObject {
factory _SpotubeImageObject( factory _SpotubeImageObject(
{required final String url, {required final String url,
required final int width, final int? width,
required final int height}) = _$SpotubeImageObjectImpl; final int? height}) = _$SpotubeImageObjectImpl;
factory _SpotubeImageObject.fromJson(Map<String, dynamic> json) = factory _SpotubeImageObject.fromJson(Map<String, dynamic> json) =
_$SpotubeImageObjectImpl.fromJson; _$SpotubeImageObjectImpl.fromJson;
@ -942,9 +941,9 @@ abstract class _SpotubeImageObject implements SpotubeImageObject {
@override @override
String get url; String get url;
@override @override
int get width; int? get width;
@override @override
int get height; int? get height;
/// Create a copy of SpotubeImageObject /// Create a copy of SpotubeImageObject
/// with the given fields replaced by the non-null parameter values. /// with the given fields replaced by the non-null parameter values.
@ -2192,11 +2191,10 @@ SpotubeUserObject _$SpotubeUserObjectFromJson(Map<String, dynamic> json) {
/// @nodoc /// @nodoc
mixin _$SpotubeUserObject { mixin _$SpotubeUserObject {
String get uid => throw _privateConstructorUsedError; String get id => throw _privateConstructorUsedError;
String get name => throw _privateConstructorUsedError; String get name => throw _privateConstructorUsedError;
List<SpotubeImageObject> get avatars => throw _privateConstructorUsedError; List<SpotubeImageObject> get images => throw _privateConstructorUsedError;
String get externalUrl => throw _privateConstructorUsedError; String get externalUri => throw _privateConstructorUsedError;
String get displayName => throw _privateConstructorUsedError;
/// Serializes this SpotubeUserObject to a JSON map. /// Serializes this SpotubeUserObject to a JSON map.
Map<String, dynamic> toJson() => throw _privateConstructorUsedError; Map<String, dynamic> toJson() => throw _privateConstructorUsedError;
@ -2215,11 +2213,10 @@ abstract class $SpotubeUserObjectCopyWith<$Res> {
_$SpotubeUserObjectCopyWithImpl<$Res, SpotubeUserObject>; _$SpotubeUserObjectCopyWithImpl<$Res, SpotubeUserObject>;
@useResult @useResult
$Res call( $Res call(
{String uid, {String id,
String name, String name,
List<SpotubeImageObject> avatars, List<SpotubeImageObject> images,
String externalUrl, String externalUri});
String displayName});
} }
/// @nodoc /// @nodoc
@ -2237,32 +2234,27 @@ class _$SpotubeUserObjectCopyWithImpl<$Res, $Val extends SpotubeUserObject>
@pragma('vm:prefer-inline') @pragma('vm:prefer-inline')
@override @override
$Res call({ $Res call({
Object? uid = null, Object? id = null,
Object? name = null, Object? name = null,
Object? avatars = null, Object? images = null,
Object? externalUrl = null, Object? externalUri = null,
Object? displayName = null,
}) { }) {
return _then(_value.copyWith( return _then(_value.copyWith(
uid: null == uid id: null == id
? _value.uid ? _value.id
: uid // ignore: cast_nullable_to_non_nullable : id // ignore: cast_nullable_to_non_nullable
as String, as String,
name: null == name name: null == name
? _value.name ? _value.name
: name // ignore: cast_nullable_to_non_nullable : name // ignore: cast_nullable_to_non_nullable
as String, as String,
avatars: null == avatars images: null == images
? _value.avatars ? _value.images
: avatars // ignore: cast_nullable_to_non_nullable : images // ignore: cast_nullable_to_non_nullable
as List<SpotubeImageObject>, as List<SpotubeImageObject>,
externalUrl: null == externalUrl externalUri: null == externalUri
? _value.externalUrl ? _value.externalUri
: externalUrl // ignore: cast_nullable_to_non_nullable : externalUri // ignore: cast_nullable_to_non_nullable
as String,
displayName: null == displayName
? _value.displayName
: displayName // ignore: cast_nullable_to_non_nullable
as String, as String,
) as $Val); ) as $Val);
} }
@ -2277,11 +2269,10 @@ abstract class _$$SpotubeUserObjectImplCopyWith<$Res>
@override @override
@useResult @useResult
$Res call( $Res call(
{String uid, {String id,
String name, String name,
List<SpotubeImageObject> avatars, List<SpotubeImageObject> images,
String externalUrl, String externalUri});
String displayName});
} }
/// @nodoc /// @nodoc
@ -2297,32 +2288,27 @@ class __$$SpotubeUserObjectImplCopyWithImpl<$Res>
@pragma('vm:prefer-inline') @pragma('vm:prefer-inline')
@override @override
$Res call({ $Res call({
Object? uid = null, Object? id = null,
Object? name = null, Object? name = null,
Object? avatars = null, Object? images = null,
Object? externalUrl = null, Object? externalUri = null,
Object? displayName = null,
}) { }) {
return _then(_$SpotubeUserObjectImpl( return _then(_$SpotubeUserObjectImpl(
uid: null == uid id: null == id
? _value.uid ? _value.id
: uid // ignore: cast_nullable_to_non_nullable : id // ignore: cast_nullable_to_non_nullable
as String, as String,
name: null == name name: null == name
? _value.name ? _value.name
: name // ignore: cast_nullable_to_non_nullable : name // ignore: cast_nullable_to_non_nullable
as String, as String,
avatars: null == avatars images: null == images
? _value._avatars ? _value._images
: avatars // ignore: cast_nullable_to_non_nullable : images // ignore: cast_nullable_to_non_nullable
as List<SpotubeImageObject>, as List<SpotubeImageObject>,
externalUrl: null == externalUrl externalUri: null == externalUri
? _value.externalUrl ? _value.externalUri
: externalUrl // ignore: cast_nullable_to_non_nullable : externalUri // ignore: cast_nullable_to_non_nullable
as String,
displayName: null == displayName
? _value.displayName
: displayName // ignore: cast_nullable_to_non_nullable
as String, as String,
)); ));
} }
@ -2332,37 +2318,34 @@ class __$$SpotubeUserObjectImplCopyWithImpl<$Res>
@JsonSerializable() @JsonSerializable()
class _$SpotubeUserObjectImpl implements _SpotubeUserObject { class _$SpotubeUserObjectImpl implements _SpotubeUserObject {
_$SpotubeUserObjectImpl( _$SpotubeUserObjectImpl(
{required this.uid, {required this.id,
required this.name, required this.name,
final List<SpotubeImageObject> avatars = const [], final List<SpotubeImageObject> images = const [],
required this.externalUrl, required this.externalUri})
required this.displayName}) : _images = images;
: _avatars = avatars;
factory _$SpotubeUserObjectImpl.fromJson(Map<String, dynamic> json) => factory _$SpotubeUserObjectImpl.fromJson(Map<String, dynamic> json) =>
_$$SpotubeUserObjectImplFromJson(json); _$$SpotubeUserObjectImplFromJson(json);
@override @override
final String uid; final String id;
@override @override
final String name; final String name;
final List<SpotubeImageObject> _avatars; final List<SpotubeImageObject> _images;
@override @override
@JsonKey() @JsonKey()
List<SpotubeImageObject> get avatars { List<SpotubeImageObject> get images {
if (_avatars is EqualUnmodifiableListView) return _avatars; if (_images is EqualUnmodifiableListView) return _images;
// ignore: implicit_dynamic_type // ignore: implicit_dynamic_type
return EqualUnmodifiableListView(_avatars); return EqualUnmodifiableListView(_images);
} }
@override @override
final String externalUrl; final String externalUri;
@override
final String displayName;
@override @override
String toString() { String toString() {
return 'SpotubeUserObject(uid: $uid, name: $name, avatars: $avatars, externalUrl: $externalUrl, displayName: $displayName)'; return 'SpotubeUserObject(id: $id, name: $name, images: $images, externalUri: $externalUri)';
} }
@override @override
@ -2370,19 +2353,17 @@ class _$SpotubeUserObjectImpl implements _SpotubeUserObject {
return identical(this, other) || return identical(this, other) ||
(other.runtimeType == runtimeType && (other.runtimeType == runtimeType &&
other is _$SpotubeUserObjectImpl && other is _$SpotubeUserObjectImpl &&
(identical(other.uid, uid) || other.uid == uid) && (identical(other.id, id) || other.id == id) &&
(identical(other.name, name) || other.name == name) && (identical(other.name, name) || other.name == name) &&
const DeepCollectionEquality().equals(other._avatars, _avatars) && const DeepCollectionEquality().equals(other._images, _images) &&
(identical(other.externalUrl, externalUrl) || (identical(other.externalUri, externalUri) ||
other.externalUrl == externalUrl) && other.externalUri == externalUri));
(identical(other.displayName, displayName) ||
other.displayName == displayName));
} }
@JsonKey(includeFromJson: false, includeToJson: false) @JsonKey(includeFromJson: false, includeToJson: false)
@override @override
int get hashCode => Object.hash(runtimeType, uid, name, int get hashCode => Object.hash(runtimeType, id, name,
const DeepCollectionEquality().hash(_avatars), externalUrl, displayName); const DeepCollectionEquality().hash(_images), externalUri);
/// Create a copy of SpotubeUserObject /// Create a copy of SpotubeUserObject
/// with the given fields replaced by the non-null parameter values. /// with the given fields replaced by the non-null parameter values.
@ -2403,25 +2384,22 @@ class _$SpotubeUserObjectImpl implements _SpotubeUserObject {
abstract class _SpotubeUserObject implements SpotubeUserObject { abstract class _SpotubeUserObject implements SpotubeUserObject {
factory _SpotubeUserObject( factory _SpotubeUserObject(
{required final String uid, {required final String id,
required final String name, required final String name,
final List<SpotubeImageObject> avatars, final List<SpotubeImageObject> images,
required final String externalUrl, required final String externalUri}) = _$SpotubeUserObjectImpl;
required final String displayName}) = _$SpotubeUserObjectImpl;
factory _SpotubeUserObject.fromJson(Map<String, dynamic> json) = factory _SpotubeUserObject.fromJson(Map<String, dynamic> json) =
_$SpotubeUserObjectImpl.fromJson; _$SpotubeUserObjectImpl.fromJson;
@override @override
String get uid; String get id;
@override @override
String get name; String get name;
@override @override
List<SpotubeImageObject> get avatars; List<SpotubeImageObject> get images;
@override @override
String get externalUrl; String get externalUri;
@override
String get displayName;
/// Create a copy of SpotubeUserObject /// Create a copy of SpotubeUserObject
/// with the given fields replaced by the non-null parameter values. /// with the given fields replaced by the non-null parameter values.

View File

@ -84,8 +84,8 @@ Map<String, dynamic> _$$SpotubeFeedObjectImplToJson(
_$SpotubeImageObjectImpl _$$SpotubeImageObjectImplFromJson(Map json) => _$SpotubeImageObjectImpl _$$SpotubeImageObjectImplFromJson(Map json) =>
_$SpotubeImageObjectImpl( _$SpotubeImageObjectImpl(
url: json['url'] as String, url: json['url'] as String,
width: (json['width'] as num).toInt(), width: (json['width'] as num?)?.toInt(),
height: (json['height'] as num).toInt(), height: (json['height'] as num?)?.toInt(),
); );
Map<String, dynamic> _$$SpotubeImageObjectImplToJson( Map<String, dynamic> _$$SpotubeImageObjectImplToJson(
@ -203,25 +203,23 @@ Map<String, dynamic> _$$SpotubeTrackObjectImplToJson(
_$SpotubeUserObjectImpl _$$SpotubeUserObjectImplFromJson(Map json) => _$SpotubeUserObjectImpl _$$SpotubeUserObjectImplFromJson(Map json) =>
_$SpotubeUserObjectImpl( _$SpotubeUserObjectImpl(
uid: json['uid'] as String, id: json['id'] as String,
name: json['name'] as String, name: json['name'] as String,
avatars: (json['avatars'] as List<dynamic>?) images: (json['images'] as List<dynamic>?)
?.map((e) => SpotubeImageObject.fromJson( ?.map((e) => SpotubeImageObject.fromJson(
Map<String, dynamic>.from(e as Map))) Map<String, dynamic>.from(e as Map)))
.toList() ?? .toList() ??
const [], const [],
externalUrl: json['externalUrl'] as String, externalUri: json['externalUri'] as String,
displayName: json['displayName'] as String,
); );
Map<String, dynamic> _$$SpotubeUserObjectImplToJson( Map<String, dynamic> _$$SpotubeUserObjectImplToJson(
_$SpotubeUserObjectImpl instance) => _$SpotubeUserObjectImpl instance) =>
<String, dynamic>{ <String, dynamic>{
'uid': instance.uid, 'id': instance.id,
'name': instance.name, 'name': instance.name,
'avatars': instance.avatars.map((e) => e.toJson()).toList(), 'images': instance.images.map((e) => e.toJson()).toList(),
'externalUrl': instance.externalUrl, 'externalUri': instance.externalUri,
'displayName': instance.displayName,
}; };
_$PluginConfigurationImpl _$$PluginConfigurationImplFromJson(Map json) => _$PluginConfigurationImpl _$$PluginConfigurationImplFromJson(Map json) =>

View File

@ -3,11 +3,10 @@ part of 'metadata.dart';
@freezed @freezed
class SpotubeUserObject with _$SpotubeUserObject { class SpotubeUserObject with _$SpotubeUserObject {
factory SpotubeUserObject({ factory SpotubeUserObject({
required final String uid, required final String id,
required final String name, required final String name,
@Default([]) final List<SpotubeImageObject> avatars, @Default([]) final List<SpotubeImageObject> images,
required final String externalUrl, required final String externalUri,
required final String displayName,
}) = _SpotubeUserObject; }) = _SpotubeUserObject;
factory SpotubeUserObject.fromJson(Map<String, dynamic> json) => factory SpotubeUserObject.fromJson(Map<String, dynamic> json) =>

View File

@ -7,6 +7,7 @@ import 'package:shadcn_flutter/shadcn_flutter.dart';
import 'package:spotube/collections/spotube_icons.dart'; import 'package:spotube/collections/spotube_icons.dart';
import 'package:spotube/components/form/text_form_field.dart'; import 'package:spotube/components/form/text_form_field.dart';
import 'package:spotube/components/titlebar/titlebar.dart'; import 'package:spotube/components/titlebar/titlebar.dart';
import 'package:spotube/models/metadata/metadata.dart';
import 'package:spotube/provider/metadata_plugin/auth.dart'; import 'package:spotube/provider/metadata_plugin/auth.dart';
import 'package:spotube/provider/metadata_plugin/metadata_plugin_provider.dart'; import 'package:spotube/provider/metadata_plugin/metadata_plugin_provider.dart';
import 'package:file_picker/file_picker.dart'; import 'package:file_picker/file_picker.dart';
@ -21,10 +22,8 @@ class SettingsMetadataProviderPage extends HookConsumerWidget {
final plugins = ref.watch(metadataPluginsProvider); final plugins = ref.watch(metadataPluginsProvider);
final pluginsNotifier = ref.watch(metadataPluginsProvider.notifier); final pluginsNotifier = ref.watch(metadataPluginsProvider.notifier);
final metadataApi = ref.watch(metadataPluginApiProvider); final metadataPlugin = ref.watch(metadataPluginProvider);
final isAuthenticated = ref.watch(metadataAuthenticatedProvider); final isAuthenticated = ref.watch(metadataPluginAuthenticatedProvider);
final artists = ref.watch(metadataUserArtistsProvider);
return Scaffold( return Scaffold(
headers: const [ headers: const [
@ -111,9 +110,7 @@ class SettingsMetadataProviderPage extends HookConsumerWidget {
final plugin = plugins.asData!.value.plugins[index]; final plugin = plugins.asData!.value.plugins[index];
final isDefault = plugins.asData!.value.defaultPlugin == index; final isDefault = plugins.asData!.value.defaultPlugin == index;
final requiresAuth = isDefault && final requiresAuth = isDefault &&
metadataApi.hasValue && plugin.abilities.contains(PluginAbilities.authentication);
metadataApi.asData?.value?.signatureFlags.requiresAuth ==
true;
return Card( return Card(
child: Column( child: Column(
spacing: 8, spacing: 8,
@ -153,8 +150,8 @@ class SettingsMetadataProviderPage extends HookConsumerWidget {
if (isAuthenticated.asData?.value != true) if (isAuthenticated.asData?.value != true)
Button.primary( Button.primary(
onPressed: () async { onPressed: () async {
await metadataApi.asData?.value await metadataPlugin.asData?.value?.auth
?.authenticate(); .authenticate();
}, },
leading: const Icon(SpotubeIcons.login), leading: const Icon(SpotubeIcons.login),
child: const Text("Login"), child: const Text("Login"),
@ -162,7 +159,8 @@ class SettingsMetadataProviderPage extends HookConsumerWidget {
else else
Button.destructive( Button.destructive(
onPressed: () async { onPressed: () async {
await metadataApi.asData?.value?.logout(); await metadataPlugin.asData?.value?.auth
.logout();
}, },
leading: const Icon(SpotubeIcons.logout), leading: const Icon(SpotubeIcons.logout),
child: const Text("Logout"), child: const Text("Logout"),

View File

@ -1,56 +0,0 @@
import 'package:auto_route/auto_route.dart';
import 'package:flutter_inappwebview/flutter_inappwebview.dart';
import 'package:shadcn_flutter/shadcn_flutter.dart';
import 'package:spotube/components/button/back_button.dart';
import 'package:spotube/components/titlebar/titlebar.dart';
import 'package:spotube/services/metadata/apis/webview.dart';
@RoutePage()
class WebviewPage extends StatelessWidget {
final WebviewInitialSettings? initialSettings;
final String? url;
final void Function(InAppWebViewController controller, WebUri? url)?
onLoadStop;
const WebviewPage({
super.key,
this.initialSettings,
this.url,
this.onLoadStop,
});
@override
Widget build(BuildContext context) {
return SafeArea(
bottom: false,
child: Scaffold(
headers: const [
TitleBar(
leading: [BackButton(color: Colors.white)],
backgroundColor: Colors.transparent,
),
],
floatingHeader: true,
child: InAppWebView(
initialSettings: InAppWebViewSettings(
userAgent: initialSettings?.userAgent ??
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/105.0.0.0 safari/537.36",
incognito: initialSettings?.incognito ?? false,
clearCache: initialSettings?.clearCache ?? false,
clearSessionCache: initialSettings?.clearSessionCache ?? false,
),
initialUrlRequest: URLRequest(
url: WebUri("https://accounts.spotify.com/"),
),
onPermissionRequest: (controller, permissionRequest) async {
return PermissionResponse(
resources: permissionRequest.resources,
action: PermissionResponseAction.GRANT,
);
},
onLoadStop: onLoadStop,
),
),
);
}
}

View File

@ -1,49 +1,37 @@
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'dart:async';
import 'package:riverpod/riverpod.dart';
import 'package:spotube/models/metadata/metadata.dart';
import 'package:spotube/provider/metadata_plugin/metadata_plugin_provider.dart'; import 'package:spotube/provider/metadata_plugin/metadata_plugin_provider.dart';
class MetadataAuthenticationNotifier extends AsyncNotifier<bool> { class MetadataPluginAuthenticatedNotifier extends AsyncNotifier<bool> {
MetadataAuthenticationNotifier();
@override @override
build() async { FutureOr<bool> build() async {
final metadataApi = await ref.watch(metadataPluginApiProvider.future); final defaultPluginConfig = ref.watch(metadataPluginsProvider);
if (defaultPluginConfig.asData?.value.defaultPluginConfig?.abilities
if (metadataApi?.signatureFlags.requiresAuth != true) { .contains(PluginAbilities.authentication) !=
true) {
return false; return false;
} }
final subscription = metadataApi?.authenticatedStream.listen((event) { final defaultPlugin = await ref.watch(metadataPluginProvider.future);
state = AsyncValue.data(event); if (defaultPlugin == null) {
return false;
}
final sub = defaultPlugin.auth.authStateStream.listen((event) {
state = AsyncData(defaultPlugin.auth.isAuthenticated());
}); });
ref.onDispose(() { ref.onDispose(() {
subscription?.cancel(); sub.cancel();
}); });
return await metadataApi?.isAuthenticated() ?? false; return defaultPlugin.auth.isAuthenticated();
}
Future<void> login() async {
final metadataApi = await ref.read(metadataPluginApiProvider.future);
if (metadataApi == null || !metadataApi.signatureFlags.requiresAuth) {
return;
}
await metadataApi.authenticate();
}
Future<void> logout() async {
final metadataApi = await ref.read(metadataPluginApiProvider.future);
if (metadataApi == null || !metadataApi.signatureFlags.requiresAuth) {
return;
}
await metadataApi.logout();
} }
} }
final metadataAuthenticatedProvider = final metadataPluginAuthenticatedProvider =
AsyncNotifierProvider<MetadataAuthenticationNotifier, bool>( AsyncNotifierProvider<MetadataPluginAuthenticatedNotifier, bool>(
() => MetadataAuthenticationNotifier(), MetadataPluginAuthenticatedNotifier.new,
); );

View File

@ -10,7 +10,6 @@ import 'package:path_provider/path_provider.dart';
import 'package:spotube/models/database/database.dart'; import 'package:spotube/models/database/database.dart';
import 'package:spotube/models/metadata/metadata.dart'; import 'package:spotube/models/metadata/metadata.dart';
import 'package:spotube/provider/database/database.dart'; import 'package:spotube/provider/database/database.dart';
import 'package:spotube/provider/metadata_plugin/auth.dart';
import 'package:spotube/services/dio/dio.dart'; import 'package:spotube/services/dio/dio.dart';
import 'package:spotube/services/metadata/metadata.dart'; import 'package:spotube/services/metadata/metadata.dart';
import 'package:spotube/utils/service_utils.dart'; import 'package:spotube/utils/service_utils.dart';
@ -315,20 +314,20 @@ class MetadataPluginNotifier extends AsyncNotifier<MetadataPluginState> {
); );
} }
Future<String> getPluginLibraryCode(PluginConfiguration plugin) async { Future<Uint8List> getPluginByteCode(PluginConfiguration plugin) async {
final pluginDir = await _getPluginDir(); final pluginDir = await _getPluginDir();
final pluginExtractionDirPath = join( final pluginExtractionDirPath = join(
pluginDir.path, pluginDir.path,
ServiceUtils.sanitizeFilename(plugin.name), ServiceUtils.sanitizeFilename(plugin.name),
); );
final libraryFile = File(join(pluginExtractionDirPath, "dist", "index.js")); final libraryFile = File(join(pluginExtractionDirPath, "plugin.out"));
if (!libraryFile.existsSync()) { if (!libraryFile.existsSync()) {
throw Exception("No dist/index.js found"); throw Exception("No plugin.out (Bytecode) file found");
} }
return await libraryFile.readAsString(); return await libraryFile.readAsBytes();
} }
} }
@ -337,7 +336,7 @@ final metadataPluginsProvider =
MetadataPluginNotifier.new, MetadataPluginNotifier.new,
); );
final metadataPluginApiProvider = FutureProvider<MetadataApiSignature?>( final metadataPluginProvider = FutureProvider<MetadataPlugin?>(
(ref) async { (ref) async {
final defaultPlugin = await ref.watch( final defaultPlugin = await ref.watch(
metadataPluginsProvider.selectAsync((data) => data.defaultPluginConfig), metadataPluginsProvider.selectAsync((data) => data.defaultPluginConfig),
@ -348,38 +347,9 @@ final metadataPluginApiProvider = FutureProvider<MetadataApiSignature?>(
} }
final pluginsNotifier = ref.read(metadataPluginsProvider.notifier); final pluginsNotifier = ref.read(metadataPluginsProvider.notifier);
final libraryCode = final pluginByteCode =
await pluginsNotifier.getPluginLibraryCode(defaultPlugin); await pluginsNotifier.getPluginByteCode(defaultPlugin);
return MetadataApiSignature.init(libraryCode, defaultPlugin); return await MetadataPlugin.create(defaultPlugin, pluginByteCode);
}, },
); );
final metadataProviderUserProvider = FutureProvider(
(ref) async {
final metadataApi = await ref.watch(metadataPluginApiProvider.future);
ref.watch(metadataAuthenticatedProvider);
if (metadataApi == null) {
return null;
}
return metadataApi.getMe();
},
);
final metadataUserArtistsProvider =
FutureProvider<List<SpotubeArtistObject>>((ref) async {
final metadataApi = await ref.watch(metadataPluginApiProvider.future);
ref.watch(metadataAuthenticatedProvider);
final userId = await ref.watch(
metadataProviderUserProvider.selectAsync((data) => data?.uid),
);
if (metadataApi == null || userId == null) {
return [];
}
final res = await metadataApi.listUserSavedArtists(userId);
return res.items as List<SpotubeArtistObject>;
});

View File

@ -0,0 +1,16 @@
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:spotube/models/metadata/metadata.dart';
import 'package:spotube/provider/metadata_plugin/auth.dart';
import 'package:spotube/provider/metadata_plugin/metadata_plugin_provider.dart';
final metadataPluginUserProvider = FutureProvider<SpotubeUserObject?>(
(ref) async {
final metadataPlugin = await ref.watch(metadataPluginProvider.future);
ref.watch(metadataPluginAuthenticatedProvider);
if (metadataPlugin == null) {
return null;
}
return metadataPlugin.user.me();
},
);

View File

@ -1,60 +1,78 @@
import 'package:flutter_js/flutter_js.dart'; import 'package:hetu_spotube_plugin/hetu_spotube_plugin.dart';
import 'package:shared_preferences/shared_preferences.dart'; import 'package:shared_preferences/shared_preferences.dart';
class PluginLocalStorageApi { class SharedPreferencesLocalStorage implements Localstorage {
final JavascriptRuntime runtime; final SharedPreferences _prefs;
final SharedPreferences sharedPreferences; final String pluginSlug;
final String pluginName; SharedPreferencesLocalStorage(this._prefs, this.pluginSlug);
PluginLocalStorageApi({ String prefix(String key) {
required this.runtime, return 'spotube_plugin.$pluginSlug.$key';
required this.sharedPreferences,
required this.pluginName,
}) {
runtime.onMessage("LocalStorage.getItem", (args) {
final key = args[0]["key"];
final value = getItem(key);
runtime.evaluate(
"""
eventEmitter.emit('LocalStorage.getItem', ${value != null ? "'$value'" : "null"});
""",
);
});
runtime.onMessage("LocalStorage.setItem", (args) {
final map = args[0] as Map<String, dynamic>;
setItem(map["key"], map["value"]);
});
runtime.onMessage("LocalStorage.removeItem", (args) {
final map = args[0];
removeItem(map["key"]);
});
runtime.onMessage("LocalStorage.clear", (args) {
clear();
});
} }
void setItem(String key, String value) async { @override
await sharedPreferences.setString("plugin.$pluginName.$key", value); Future<void> clear() {
return _prefs.clear();
} }
String? getItem(String key) { @override
return sharedPreferences.getString("plugin.$pluginName.$key"); Future<bool> containsKey(String key) async {
return _prefs.containsKey(prefix(key));
} }
void removeItem(String key) async { @override
await sharedPreferences.remove("plugin.$pluginName.$key"); Future<bool?> getBool(String key) async {
return _prefs.getBool(prefix(key));
} }
void clear() async { @override
final keys = sharedPreferences.getKeys(); Future<double?> getDouble(String key) async {
for (String key in keys) { return _prefs.getDouble(prefix(key));
if (key.startsWith("plugin.$pluginName.")) {
await sharedPreferences.remove(key);
} }
@override
Future<int?> getInt(String key) async {
return _prefs.getInt(prefix(key));
} }
@override
Future<String?> getString(String key) async {
return _prefs.getString(prefix(key));
}
@override
Future<List<String>?> getStringList(String key) async {
return _prefs.getStringList(prefix(key));
}
@override
Future<void> remove(String key) async {
await _prefs.remove(prefix(key));
}
@override
Future<void> setBool(String key, bool value) async {
await _prefs.setBool(prefix(key), value);
}
@override
Future<void> setDouble(String key, double value) async {
await _prefs.setDouble(prefix(key), value);
}
@override
Future<void> setInt(String key, int value) async {
await _prefs.setInt(prefix(key), value);
}
@override
Future<void> setString(String key, String value) async {
await _prefs.setString(prefix(key), value);
}
@override
Future<void> setStringList(String key, List<String> value) async {
await _prefs.setStringList(prefix(key), value);
} }
} }

View File

@ -1,80 +0,0 @@
import 'dart:async';
import 'package:flutter_js/flutter_js.dart';
class PluginSetIntervalApi {
final JavascriptRuntime runtime;
final Map<String, Timer> _timers = {};
PluginSetIntervalApi(this.runtime) {
runtime.evaluate(
"""
var __NATIVE_FLUTTER_JS__setIntervalCount = -1;
var __NATIVE_FLUTTER_JS__setIntervalCallbacks = {};
function setInterval(fnInterval, interval) {
try {
__NATIVE_FLUTTER_JS__setIntervalCount += 1;
var intervalIndex = '' + __NATIVE_FLUTTER_JS__setIntervalCount;
__NATIVE_FLUTTER_JS__setIntervalCallbacks[intervalIndex] = fnInterval;
;
sendMessage('PluginSetIntervalApi.setInterval', JSON.stringify({ intervalIndex, interval}));
return intervalIndex;
} catch (e) {
console.error('ERROR HERE',e.message);
}
};
function clearInterval(intervalIndex) {
try {
delete __NATIVE_FLUTTER_JS__setIntervalCallbacks[intervalIndex];
sendMessage('PluginSetIntervalApi.clearInterval', JSON.stringify({ intervalIndex}));
} catch (e) {
console.error('ERROR HERE',e.message);
}
};
1
""",
);
runtime.onMessage('PluginSetIntervalApi.setInterval', (dynamic args) {
try {
int duration = args['interval'] ?? 0;
String idx = args['intervalIndex'];
_timers[idx] =
Timer.periodic(Duration(milliseconds: duration), (timer) {
runtime.evaluate("""
__NATIVE_FLUTTER_JS__setIntervalCallbacks[$idx].call();
delete __NATIVE_FLUTTER_JS__setIntervalCallbacks[$idx];
""");
});
} on Exception catch (e) {
print('Exception no setInterval: $e');
} on Error catch (e) {
print('Erro no setInterval: $e');
}
});
runtime.onMessage('PluginSetIntervalApi.clearInterval', (dynamic args) {
try {
String idx = args['intervalIndex'];
if (_timers.containsKey(idx)) {
_timers[idx]?.cancel();
_timers.remove(idx);
}
} on Exception catch (e) {
print('Exception no clearInterval: $e');
} on Error catch (e) {
print('Error no clearInterval: $e');
}
});
}
void dispose() {
for (var timer in _timers.values) {
timer.cancel();
}
_timers.clear();
}
}

View File

@ -1,40 +0,0 @@
import 'package:flutter_js/javascript_runtime.dart';
import 'package:otp_util/otp_util.dart';
// ignore: implementation_imports
import 'package:otp_util/src/utils/generic_util.dart';
class PluginTotpGenerator {
final JavascriptRuntime runtime;
PluginTotpGenerator(this.runtime) {
runtime.onMessage("TotpGenerator.generate", (args) {
final opts = args[0];
if (opts is! Map) {
return;
}
final totp = TOTP(
secret: opts["secret"] as String,
algorithm: OTPAlgorithm.values.firstWhere(
(e) => e.name == opts["algorithm"],
orElse: () => OTPAlgorithm.SHA1,
),
digits: opts["digits"] as int? ?? 6,
interval: opts["interval"] as int? ?? 30,
);
final otp = totp.generateOTP(
input: Util.timeFormat(
time: DateTime.fromMillisecondsSinceEpoch(opts["period"]),
interval: 30,
),
);
runtime.evaluate(
"""
eventEmitter.emit('TotpGenerator.generate', '$otp');
""",
);
});
}
}

View File

@ -1,171 +0,0 @@
import 'dart:convert';
import 'dart:io';
import 'package:auto_route/auto_route.dart';
import 'package:desktop_webview_window/desktop_webview_window.dart';
import 'package:flutter/services.dart';
import 'package:flutter_inappwebview/flutter_inappwebview.dart';
import 'package:flutter_js/flutter_js.dart';
import 'package:path/path.dart';
import 'package:path_provider/path_provider.dart';
import 'package:shadcn_flutter/shadcn_flutter.dart' hide join;
import 'package:spotube/collections/routes.dart';
import 'package:spotube/collections/routes.gr.dart';
import 'package:spotube/pages/mobile_login/no_webview_runtime_dialog.dart';
import 'package:spotube/utils/platform.dart';
class WebviewInitialSettings {
final String? userAgent;
final bool? incognito;
final bool? clearCache;
final bool? clearSessionCache;
WebviewInitialSettings({
this.userAgent,
this.incognito,
this.clearCache,
this.clearSessionCache,
});
factory WebviewInitialSettings.fromJson(Map<String, dynamic> json) {
return WebviewInitialSettings(
userAgent: json["userAgent"],
incognito: json["incognito"],
clearCache: json["clearCache"],
clearSessionCache: json["clearSessionCache"],
);
}
}
class PluginWebViewApi {
JavascriptRuntime runtime;
PluginWebViewApi({
required this.runtime,
}) {
runtime.onMessage("WebView.show", (args) {
if (args[0] is! Map) {
return;
}
showWebView(
url: args[0]["url"] as String,
initialSettings: args[0]["initialSettings"] != null
? WebviewInitialSettings.fromJson(
args[0]["initialSettings"],
)
: null,
);
});
}
Webview? webviewWindow;
Future showWebView({
required String url,
WebviewInitialSettings? initialSettings,
}) async {
if (rootNavigatorKey.currentContext == null) {
return;
}
final context = rootNavigatorKey.currentContext!;
final theme = Theme.of(context);
if (kIsMobile || kIsMacOS) {
context.pushRoute(WebviewRoute(
initialSettings: initialSettings,
url: url,
onLoadStop: (controller, uri) async {
if (uri == null) return;
final cookies = await CookieManager().getAllCookies();
final jsonCookies = cookies.map((e) {
return {
"name": e.name,
"value": e.value,
"domain": e.domain,
"path": e.path,
};
});
runtime.onMessage("WebView.close", (args) {
context.back();
});
runtime.evaluate(
"""
eventEmitter.emit('WebView.onLoadFinish', {url: '${uri.toString()}', cookies: ${jsonEncode(jsonCookies)}});
""",
);
},
));
return;
}
try {
final applicationSupportDir = await getApplicationSupportDirectory();
final userDataFolder = Directory(
join(applicationSupportDir.path, "webview_window_Webview2"),
);
if (!await userDataFolder.exists()) {
await userDataFolder.create();
}
final webview = await WebviewWindow.create(
configuration: CreateConfiguration(
title: "Webview",
titleBarTopPadding: kIsMacOS ? 20 : 0,
windowHeight: 720,
windowWidth: 1280,
userDataFolderWindows: userDataFolder.path,
),
);
webviewWindow = webview;
runtime.onMessage("WebView.close", (args) {
webview.close();
});
webview
..setBrightness(theme.colorScheme.brightness)
..launch(url)
..setOnUrlRequestCallback((url) {
() async {
final cookies = await webview.getAllCookies();
final jsonCookies = cookies.map((e) {
return {
"name": e.name,
"value": e.value,
"domain": e.domain,
"path": e.path,
};
}).toList();
runtime.evaluate(
"""
eventEmitter.emit('WebView.onLoadFinish', {url: '$url', cookies: ${jsonEncode(jsonCookies)}});
""",
);
}();
return false;
});
} on PlatformException catch (_) {
if (!await WebviewWindow.isWebviewAvailable()) {
WidgetsBinding.instance.addPostFrameCallback((timeStamp) {
showDialog(
context: context,
builder: (context) {
return const NoWebviewRuntimeDialog();
},
);
});
}
}
}
void dispose() {
webviewWindow?.close();
webviewWindow = null;
}
}

View File

@ -0,0 +1,33 @@
import 'package:desktop_webview_window/desktop_webview_window.dart';
import 'package:flutter_inappwebview/flutter_inappwebview.dart';
import 'package:hetu_script/hetu_script.dart';
import 'package:hetu_std/hetu_std.dart';
import 'package:spotube/utils/platform.dart';
class MetadataAuthEndpoint {
final Hetu hetu;
MetadataAuthEndpoint(this.hetu);
Stream get authStateStream =>
hetu.eval("metadataPlugin.auth.authStateStream");
Future<void> authenticate() async {
await hetu.eval("metadataPlugin.auth.authenticate()");
}
bool isAuthenticated() {
return hetu.eval("metadataPlugin.auth.isAuthenticated()") as bool;
}
Future<void> logout() async {
await hetu.eval("metadataPlugin.auth.logout()");
if (kIsMobile) {
WebStorageManager.instance().deleteAllData();
CookieManager.instance().deleteAllCookies();
}
if (kIsDesktop) {
await WebviewWindow.clearAll();
}
}
}

View File

@ -0,0 +1,15 @@
import 'package:hetu_script/hetu_script.dart';
import 'package:spotube/models/metadata/metadata.dart';
class MetadataPluginUserEndpoint {
final Hetu hetu;
MetadataPluginUserEndpoint(this.hetu);
Future<SpotubeUserObject> me() async {
final raw = await hetu.eval("metadataPlugin.user.me()") as Map;
return SpotubeUserObject.fromJson(
raw.cast<String, dynamic>(),
);
}
}

View File

@ -1,564 +1,81 @@
import 'dart:async'; import 'dart:typed_data';
import 'dart:convert';
import 'package:flutter_js/extensions/fetch.dart'; import 'package:auto_route/auto_route.dart';
import 'package:flutter_js/extensions/xhr.dart'; import 'package:hetu_otp_util/hetu_otp_util.dart';
import 'package:flutter_js/flutter_js.dart'; import 'package:hetu_script/hetu_script.dart';
import 'package:hetu_spotube_plugin/hetu_spotube_plugin.dart';
import 'package:hetu_std/hetu_std.dart';
import 'package:shadcn_flutter/shadcn_flutter.dart';
import 'package:shared_preferences/shared_preferences.dart'; import 'package:shared_preferences/shared_preferences.dart';
import 'package:spotube/collections/routes.dart';
import 'package:spotube/components/titlebar/titlebar.dart';
import 'package:spotube/models/metadata/metadata.dart'; import 'package:spotube/models/metadata/metadata.dart';
import 'package:spotube/services/logger/logger.dart';
import 'package:spotube/services/metadata/apis/localstorage.dart'; import 'package:spotube/services/metadata/apis/localstorage.dart';
import 'package:spotube/services/metadata/apis/set_interval.dart'; import 'package:spotube/services/metadata/endpoints/auth.dart';
import 'package:spotube/services/metadata/apis/totp.dart'; import 'package:spotube/services/metadata/endpoints/user.dart';
import 'package:spotube/services/metadata/apis/webview.dart';
const defaultMetadataLimit = "20"; const defaultMetadataLimit = "20";
class MetadataSignatureFlags { class MetadataPlugin {
final bool requiresAuth; static Future<MetadataPlugin> create(
const MetadataSignatureFlags({
this.requiresAuth = false,
});
factory MetadataSignatureFlags.fromJson(Map<String, dynamic> json) {
return MetadataSignatureFlags(
requiresAuth: json["requiresAuth"] ?? false,
);
}
}
/// Signature for metadata and related methods that will return Spotube native
/// objects e.g. SpotubeTrack, SpotubePlaylist, etc.
class MetadataApiSignature {
final JavascriptRuntime runtime;
final PluginLocalStorageApi localStorageApi;
final PluginWebViewApi webViewApi;
final PluginTotpGenerator totpGenerator;
final PluginSetIntervalApi setIntervalApi;
late MetadataSignatureFlags _signatureFlags;
final StreamController<bool> _authenticatedStreamController;
Stream<bool> get authenticatedStream => _authenticatedStreamController.stream;
MetadataSignatureFlags get signatureFlags => _signatureFlags;
MetadataApiSignature._(
this.runtime,
this.localStorageApi,
this.webViewApi,
this.totpGenerator,
this.setIntervalApi,
) : _authenticatedStreamController = StreamController<bool>.broadcast() {
runtime.onMessage("authenticatedStatus", (args) {
if (args[0] is Map && (args[0] as Map).containsKey("authenticated")) {
final authenticated = args[0]["authenticated"] as bool;
_authenticatedStreamController.add(authenticated);
}
});
}
static Future<MetadataApiSignature> init(
String libraryCode,
PluginConfiguration config, PluginConfiguration config,
Uint8List byteCode,
) async { ) async {
final runtime = getJavascriptRuntime(xhr: true).enableXhr(); final sharedPreferences = await SharedPreferences.getInstance();
runtime.enableHandlePromises(); BuildContext? pageContext;
await runtime.enableFetch();
Timer.periodic( final hetu = Hetu();
const Duration(milliseconds: 100), hetu.init();
(timer) {
runtime.executePendingJob(); HetuStdLoader.loadBindings(hetu);
HetuSpotubePluginLoader.loadBindings(
hetu,
localStorageImpl: SharedPreferencesLocalStorage(
sharedPreferences,
config.slug,
),
onNavigatorPush: (route) {
return rootNavigatorKey.currentContext?.router
.pushWidget(Builder(builder: (context) {
pageContext = context;
return Scaffold(
headers: const [
TitleBar(
automaticallyImplyLeading: true,
)
],
child: route,
);
}));
},
onNavigatorPop: () {
pageContext?.maybePop();
}, },
); );
// Create all the PluginAPIs after library code is evaluated await HetuStdLoader.loadBytecodeFlutter(hetu);
final localStorageApi = PluginLocalStorageApi( await HetuOtpUtilLoader.loadBytecodeFlutter(hetu);
runtime: runtime, await HetuSpotubePluginLoader.loadBytecodeFlutter(hetu);
sharedPreferences: await SharedPreferences.getInstance(),
pluginName: config.slug,
);
final webViewApi = PluginWebViewApi(runtime: runtime); hetu.loadBytecode(bytes: byteCode, moduleName: "plugin");
final totpGenerator = PluginTotpGenerator(runtime); hetu.eval("""
final setIntervalApi = PluginSetIntervalApi(runtime); import "module:plugin" as plugin
final metadataApi = MetadataApiSignature._( var Plugin = plugin.${config.entryPoint}
runtime,
localStorageApi,
webViewApi,
totpGenerator,
setIntervalApi,
);
final res = runtime.evaluate( var metadataPlugin = Plugin()
""" """);
;$libraryCode;
const metadataApi = new MetadataApi();
""",
);
metadataApi._signatureFlags = await metadataApi._getSignatureFlags();
if (res.isError) { return MetadataPlugin._(hetu);
AppLogger.reportError(
"Error evaluating code: $libraryCode\n${res.rawResult}",
);
} }
return metadataApi; final Hetu hetu;
}
void dispose() { late final MetadataAuthEndpoint auth;
setIntervalApi.dispose(); late final MetadataPluginUserEndpoint user;
webViewApi.dispose();
runtime.dispose();
}
Future invoke(String method, [List? args]) async { MetadataPlugin._(this.hetu) {
final completer = Completer(); auth = MetadataAuthEndpoint(hetu);
runtime.onMessage(method, (result) { user = MetadataPluginUserEndpoint(hetu);
if (completer.isCompleted) return;
try {
if (result is Map && result.containsKey("error")) {
completer.completeError(result["error"]);
} else {
completer.complete(result is String ? jsonDecode(result) : result);
}
} catch (e, stack) {
AppLogger.reportError(
"[MetadataApiSignature][invoke] Error in $method: $e",
stack,
);
}
});
final code = """
$method(...${args != null ? jsonEncode(args) : "[]"})
.then((res) => {
try {
sendMessage("$method", res ? JSON.stringify(res) : "[]");
} catch (e) {
console.error("Failed to send message in $method.then: ", `\${e.toString()}\n\${e.stack.toString()}`);
}
}).catch((e) => {
try {
console.error("Error in $method: ", `\${e.toString()}\n\${e.stack.toString()}`);
sendMessage("$method", JSON.stringify({error: `\${e.toString()}\n\${e.stack.toString()}`}));
} catch (e) {
console.error("Failed to send message in $method.catch: ", `\${e.toString()}\n\${e.stack.toString()}`);
}
});
""";
final res = await runtime.evaluateAsync(code);
if (res.isError) {
AppLogger.reportError("Error evaluating code: $code\n${res.rawResult}");
completer.completeError("Error evaluating code: $code\n${res.rawResult}");
return completer.future;
}
return completer.future;
}
Future<MetadataSignatureFlags> _getSignatureFlags() async {
final res = await invoke("metadataApi.getSignatureFlags");
return MetadataSignatureFlags.fromJson(res);
}
// ----- Authentication ------
Future<void> authenticate() async {
await invoke("metadataApi.authenticate");
}
Future<bool> isAuthenticated() async {
final res = await invoke("metadataApi.isAuthenticated");
return res as bool;
}
Future<void> logout() async {
await invoke("metadataApi.logout");
}
// ----- Track ------
Future<SpotubeTrackObject> getTrack(String id) async {
final result = await invoke("metadataApi.getTrack", [id]);
return SpotubeTrackObject.fromJson(result);
}
Future<SpotubePaginationResponseObject<SpotubeTrackObject>> listTracks({
List<String>? ids,
String limit = defaultMetadataLimit,
String? cursor,
}) async {
final result = await invoke(
"metadataApi.listTracks",
[
ids,
limit,
cursor,
],
);
return SpotubePaginationResponseObject<SpotubeTrackObject>.fromJson(
result,
SpotubeTrackObject.fromJson,
);
}
Future<SpotubePaginationResponseObject<SpotubeTrackObject>> listTracksByAlbum(
String albumId, {
String limit = defaultMetadataLimit,
String? cursor,
}) async {
final res = await invoke(
"metadataApi.listTracksByAlbum",
[albumId, limit, cursor],
);
return SpotubePaginationResponseObject<SpotubeTrackObject>.fromJson(
res,
SpotubeTrackObject.fromJson,
);
}
Future<SpotubePaginationResponseObject<SpotubeTrackObject>>
listTopTracksByArtist(
String artistId, {
String limit = defaultMetadataLimit,
String? cursor,
}) async {
final res = await invoke(
"metadataApi.listTopTracksByArtist",
[artistId, limit, cursor],
);
return SpotubePaginationResponseObject<SpotubeTrackObject>.fromJson(
res,
SpotubeTrackObject.fromJson,
);
}
Future<SpotubePaginationResponseObject<SpotubeTrackObject>>
listTracksByPlaylist(
String playlistId, {
String limit = defaultMetadataLimit,
String? cursor,
}) async {
final res = await invoke(
"metadataApi.listTracksByPlaylist",
[playlistId, limit, cursor],
);
return SpotubePaginationResponseObject<SpotubeTrackObject>.fromJson(
res,
SpotubeTrackObject.fromJson,
);
}
Future<SpotubePaginationResponseObject<SpotubeTrackObject>>
listUserSavedTracks(
String userId, {
String limit = defaultMetadataLimit,
String? cursor,
}) async {
final res = await invoke(
"metadataApi.listUserSavedTracks",
[userId, limit, cursor],
);
return SpotubePaginationResponseObject<SpotubeTrackObject>.fromJson(
res,
SpotubeTrackObject.fromJson,
);
}
// ----- Album ------
Future<SpotubeAlbumObject> getAlbum(String id) async {
final res = await invoke("metadataApi.getAlbum", [id]);
return SpotubeAlbumObject.fromJson(res);
}
Future<SpotubePaginationResponseObject<SpotubeAlbumObject>> listAlbums({
List<String>? ids,
String limit = defaultMetadataLimit,
String? cursor,
}) async {
final res = await invoke(
"metadataApi.listAlbums",
[ids, limit, cursor],
);
return SpotubePaginationResponseObject<SpotubeAlbumObject>.fromJson(
res,
SpotubeAlbumObject.fromJson,
);
}
Future<SpotubePaginationResponseObject<SpotubeAlbumObject>>
listAlbumsByArtist(
String artistId, {
String limit = defaultMetadataLimit,
String? cursor,
}) async {
final res = await invoke(
"metadataApi.listAlbumsByArtist",
[artistId, limit, cursor],
);
return SpotubePaginationResponseObject.fromJson(
res,
SpotubeAlbumObject.fromJson,
);
}
Future<SpotubePaginationResponseObject<SpotubeAlbumObject>>
listUserSavedAlbums(
String userId, {
String limit = defaultMetadataLimit,
String? cursor,
}) async {
final res = await invoke(
"metadataApi.listUserSavedAlbums",
[userId, limit, cursor],
);
return SpotubePaginationResponseObject.fromJson(
res,
SpotubeAlbumObject.fromJson,
);
}
// ----- Playlist ------
Future<SpotubePlaylistObject> getPlaylist(String id) async {
final res = await invoke("metadataApi.getPlaylist", [id]);
return SpotubePlaylistObject.fromJson(res);
}
Future<SpotubePaginationResponseObject<SpotubePlaylistObject>>
listFeedPlaylists(
String feedId, {
String limit = defaultMetadataLimit,
String? cursor,
}) async {
final res = await invoke(
"metadataApi.listFeedPlaylists",
[feedId, limit, cursor],
);
return SpotubePaginationResponseObject.fromJson(
res,
SpotubePlaylistObject.fromJson,
);
}
Future<SpotubePaginationResponseObject<SpotubePlaylistObject>>
listUserSavedPlaylists(
String userId, {
String limit = defaultMetadataLimit,
String? cursor,
}) async {
final res = await invoke(
"metadataApi.listUserSavedPlaylists",
[userId, limit, cursor],
);
return SpotubePaginationResponseObject.fromJson(
res,
SpotubePlaylistObject.fromJson,
);
}
Future<SpotubePlaylistObject> createPlaylist(
String userId,
String name, {
String? description,
bool? public,
bool? collaborative,
String? imageBase64,
}) async {
final res = await invoke(
"metadataApi.createPlaylist",
[
userId,
name,
description,
public,
collaborative,
imageBase64,
],
);
return SpotubePlaylistObject.fromJson(res);
}
Future<void> updatePlaylist(
String playlistId, {
String? name,
String? description,
bool? public,
bool? collaborative,
String? imageBase64,
}) async {
await invoke(
"metadataApi.updatePlaylist",
[
playlistId,
name,
description,
public,
collaborative,
imageBase64,
],
);
}
Future<void> deletePlaylist(String userId, String playlistId) async {
await unsavePlaylist(userId, playlistId);
}
Future<void> addTracksToPlaylist(
String playlistId,
List<String> trackIds, {
int? position,
}) async {
await invoke(
"metadataApi.addTracksToPlaylist",
[
playlistId,
trackIds,
position,
],
);
}
Future<void> removeTracksFromPlaylist(
String playlistId,
List<String> trackIds,
) async {
await invoke(
"metadataApi.removeTracksFromPlaylist",
[
playlistId,
trackIds,
],
);
}
// ----- Artist ------
Future<SpotubeArtistObject> getArtist(String id) async {
final res = await invoke("metadataApi.getArtist", [id]);
return SpotubeArtistObject.fromJson(res);
}
Future<SpotubePaginationResponseObject<SpotubeArtistObject>> listArtists({
List<String>? ids,
String limit = defaultMetadataLimit,
String? cursor,
}) async {
final res = await invoke(
"metadataApi.listArtists",
[ids, limit, cursor],
);
return SpotubePaginationResponseObject.fromJson(
res,
SpotubeArtistObject.fromJson,
);
}
Future<SpotubePaginationResponseObject<SpotubeArtistObject>>
listUserSavedArtists(
String userId, {
String limit = defaultMetadataLimit,
String? cursor,
}) async {
final res = await invoke(
"metadataApi.listUserSavedArtists",
[userId, limit, cursor],
);
return SpotubePaginationResponseObject.fromJson(
res,
SpotubeArtistObject.fromJson,
);
}
// ----- Search ------
Future<SpotubeSearchResponseObject> search(
String query, {
String limit = defaultMetadataLimit,
String? cursor,
}) async {
final res = await invoke(
"metadataApi.search",
[query, limit, cursor],
);
return SpotubeSearchResponseObject.fromJson(res);
}
// ----- Feed ------
Future<SpotubeFeedObject> getFeed(String id) async {
final res = await invoke("metadataApi.getFeed", [id]);
return SpotubeFeedObject.fromJson(res);
}
Future<SpotubePaginationResponseObject<SpotubeFeedObject>> listFeeds({
String limit = defaultMetadataLimit,
String? cursor,
}) async {
final res = await invoke("metadataApi.listFeeds", [limit, cursor]);
return SpotubePaginationResponseObject.fromJson(
res,
SpotubeFeedObject.fromJson,
);
}
// ----- User ------
Future<SpotubeUserObject> getMe() async {
final res = await invoke("metadataApi.getMe");
return SpotubeUserObject.fromJson(res);
}
Future<void> followArtist(String userId, String artistId) async {
await invoke("metadataApi.followArtist", [userId, artistId]);
}
Future<void> unfollowArtist(String userId, String artistId) async {
await invoke("metadataApi.unfollowArtist", [userId, artistId]);
}
Future<void> savePlaylist(String userId, String playlistId) async {
await invoke("metadataApi.savePlaylist", [userId, playlistId]);
}
Future<void> unsavePlaylist(String userId, String playlistId) async {
await invoke("metadataApi.unsavePlaylist", [userId, playlistId]);
}
Future<void> saveAlbum(String userId, String albumId) async {
await invoke("metadataApi.saveAlbum", [userId, albumId]);
}
Future<void> unsaveAlbum(String userId, String albumId) async {
await invoke("metadataApi.unsaveAlbum", [userId, albumId]);
}
Future<void> saveTrack(String userId, String trackId) async {
await invoke("metadataApi.saveTrack", [userId, trackId]);
}
Future<void> unsaveTrack(String userId, String trackId) async {
await invoke("metadataApi.unsaveTrack", [userId, trackId]);
} }
} }

View File

@ -8,8 +8,8 @@
#include <desktop_webview_window/desktop_webview_window_plugin.h> #include <desktop_webview_window/desktop_webview_window_plugin.h>
#include <file_selector_linux/file_selector_plugin.h> #include <file_selector_linux/file_selector_plugin.h>
#include <flutter_js/flutter_js_plugin.h>
#include <flutter_secure_storage_linux/flutter_secure_storage_linux_plugin.h> #include <flutter_secure_storage_linux/flutter_secure_storage_linux_plugin.h>
#include <flutter_timezone/flutter_timezone_plugin.h>
#include <gtk/gtk_plugin.h> #include <gtk/gtk_plugin.h>
#include <local_notifier/local_notifier_plugin.h> #include <local_notifier/local_notifier_plugin.h>
#include <media_kit_libs_linux/media_kit_libs_linux_plugin.h> #include <media_kit_libs_linux/media_kit_libs_linux_plugin.h>
@ -28,12 +28,12 @@ void fl_register_plugins(FlPluginRegistry* registry) {
g_autoptr(FlPluginRegistrar) file_selector_linux_registrar = g_autoptr(FlPluginRegistrar) file_selector_linux_registrar =
fl_plugin_registry_get_registrar_for_plugin(registry, "FileSelectorPlugin"); fl_plugin_registry_get_registrar_for_plugin(registry, "FileSelectorPlugin");
file_selector_plugin_register_with_registrar(file_selector_linux_registrar); file_selector_plugin_register_with_registrar(file_selector_linux_registrar);
g_autoptr(FlPluginRegistrar) flutter_js_registrar =
fl_plugin_registry_get_registrar_for_plugin(registry, "FlutterJsPlugin");
flutter_js_plugin_register_with_registrar(flutter_js_registrar);
g_autoptr(FlPluginRegistrar) flutter_secure_storage_linux_registrar = g_autoptr(FlPluginRegistrar) flutter_secure_storage_linux_registrar =
fl_plugin_registry_get_registrar_for_plugin(registry, "FlutterSecureStorageLinuxPlugin"); fl_plugin_registry_get_registrar_for_plugin(registry, "FlutterSecureStorageLinuxPlugin");
flutter_secure_storage_linux_plugin_register_with_registrar(flutter_secure_storage_linux_registrar); flutter_secure_storage_linux_plugin_register_with_registrar(flutter_secure_storage_linux_registrar);
g_autoptr(FlPluginRegistrar) flutter_timezone_registrar =
fl_plugin_registry_get_registrar_for_plugin(registry, "FlutterTimezonePlugin");
flutter_timezone_plugin_register_with_registrar(flutter_timezone_registrar);
g_autoptr(FlPluginRegistrar) gtk_registrar = g_autoptr(FlPluginRegistrar) gtk_registrar =
fl_plugin_registry_get_registrar_for_plugin(registry, "GtkPlugin"); fl_plugin_registry_get_registrar_for_plugin(registry, "GtkPlugin");
gtk_plugin_register_with_registrar(gtk_registrar); gtk_plugin_register_with_registrar(gtk_registrar);

View File

@ -5,8 +5,8 @@
list(APPEND FLUTTER_PLUGIN_LIST list(APPEND FLUTTER_PLUGIN_LIST
desktop_webview_window desktop_webview_window
file_selector_linux file_selector_linux
flutter_js
flutter_secure_storage_linux flutter_secure_storage_linux
flutter_timezone
gtk gtk
local_notifier local_notifier
media_kit_libs_linux media_kit_libs_linux

View File

@ -14,8 +14,8 @@ import desktop_webview_window
import device_info_plus import device_info_plus
import file_selector_macos import file_selector_macos
import flutter_inappwebview_macos import flutter_inappwebview_macos
import flutter_js
import flutter_secure_storage_macos import flutter_secure_storage_macos
import flutter_timezone
import local_notifier import local_notifier
import media_kit_libs_macos_audio import media_kit_libs_macos_audio
import open_file_mac import open_file_mac
@ -40,8 +40,8 @@ func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
DeviceInfoPlusMacosPlugin.register(with: registry.registrar(forPlugin: "DeviceInfoPlusMacosPlugin")) DeviceInfoPlusMacosPlugin.register(with: registry.registrar(forPlugin: "DeviceInfoPlusMacosPlugin"))
FileSelectorPlugin.register(with: registry.registrar(forPlugin: "FileSelectorPlugin")) FileSelectorPlugin.register(with: registry.registrar(forPlugin: "FileSelectorPlugin"))
InAppWebViewFlutterPlugin.register(with: registry.registrar(forPlugin: "InAppWebViewFlutterPlugin")) InAppWebViewFlutterPlugin.register(with: registry.registrar(forPlugin: "InAppWebViewFlutterPlugin"))
FlutterJsPlugin.register(with: registry.registrar(forPlugin: "FlutterJsPlugin"))
FlutterSecureStoragePlugin.register(with: registry.registrar(forPlugin: "FlutterSecureStoragePlugin")) FlutterSecureStoragePlugin.register(with: registry.registrar(forPlugin: "FlutterSecureStoragePlugin"))
FlutterTimezonePlugin.register(with: registry.registrar(forPlugin: "FlutterTimezonePlugin"))
LocalNotifierPlugin.register(with: registry.registrar(forPlugin: "LocalNotifierPlugin")) LocalNotifierPlugin.register(with: registry.registrar(forPlugin: "LocalNotifierPlugin"))
MediaKitLibsMacosAudioPlugin.register(with: registry.registrar(forPlugin: "MediaKitLibsMacosAudioPlugin")) MediaKitLibsMacosAudioPlugin.register(with: registry.registrar(forPlugin: "MediaKitLibsMacosAudioPlugin"))
OpenFilePlugin.register(with: registry.registrar(forPlugin: "OpenFilePlugin")) OpenFilePlugin.register(with: registry.registrar(forPlugin: "OpenFilePlugin"))

View File

@ -540,10 +540,10 @@ packages:
dependency: "direct main" dependency: "direct main"
description: description:
name: dio name: dio
sha256: "5598aa796bbf4699afd5c67c0f5f6e2ed542afc956884b9cd58c306966efc260" sha256: "253a18bbd4851fecba42f7343a1df3a9a4c1d31a2c1b37e221086b4fa8c8dbc9"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "5.7.0" version: "5.8.0+1"
dio_http2_adapter: dio_http2_adapter:
dependency: "direct main" dependency: "direct main"
description: description:
@ -658,6 +658,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.3.2" version: "1.3.2"
fast_noise:
dependency: transitive
description:
name: fast_noise
sha256: "271031cebf1602fc064472970e658fa7ff2f3b55a979bb430337b715bd55690b"
url: "https://pub.dev"
source: hosted
version: "1.0.1"
ffi: ffi:
dependency: transitive dependency: transitive
description: description:
@ -909,14 +917,6 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "0.6.0" version: "0.6.0"
flutter_js:
dependency: "direct main"
description:
name: flutter_js
sha256: "6b777cd4e468546f046a2f114d078a4596143269f6fa6bad5c29611d5b896369"
url: "https://pub.dev"
source: hosted
version: "0.8.2"
flutter_launcher_icons: flutter_launcher_icons:
dependency: "direct dev" dependency: "direct dev"
description: description:
@ -1048,6 +1048,14 @@ packages:
description: flutter description: flutter
source: sdk source: sdk
version: "0.0.0" version: "0.0.0"
flutter_timezone:
dependency: transitive
description:
name: flutter_timezone
sha256: "13b2109ad75651faced4831bf262e32559e44aa549426eab8a597610d385d934"
url: "https://pub.dev"
source: hosted
version: "4.1.1"
flutter_undraw: flutter_undraw:
dependency: "direct main" dependency: "direct main"
description: description:
@ -1122,6 +1130,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "3.0.1" version: "3.0.1"
get_it:
dependency: "direct main"
description:
name: get_it
sha256: f126a3e286b7f5b578bf436d5592968706c4c1de28a228b870ce375d9f743103
url: "https://pub.dev"
source: hosted
version: "8.0.3"
glob: glob:
dependency: transitive dependency: transitive
description: description:
@ -1170,6 +1186,41 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.0.0" version: "2.0.0"
hetu_otp_util:
dependency: "direct main"
description:
path: "."
ref: main
resolved-ref: "7790606751828758769596e809ba6cbb756f9b9a"
url: "https://github.com/hetu-community/hetu_otp_util.git"
source: git
version: "1.0.0"
hetu_script:
dependency: "direct main"
description:
name: hetu_script
sha256: a9a3f4f510ae10188d352b79e8fc45e27134cfa81ee1691004067395f45f17cb
url: "https://pub.dev"
source: hosted
version: "0.4.2+1"
hetu_spotube_plugin:
dependency: "direct main"
description:
path: "."
ref: main
resolved-ref: "1aa924281d2dbe09aab27d8c2de1cffc853b0d16"
url: "https://github.com/KRTirtho/hetu_spotube_plugin.git"
source: git
version: "0.0.1"
hetu_std:
dependency: "direct main"
description:
path: "."
ref: main
resolved-ref: d3720be2a92022f7b95a3082d40322d8458c70da
url: "https://github.com/hetu-community/hetu_std.git"
source: git
version: "1.0.0"
home_widget: home_widget:
dependency: "direct main" dependency: "direct main"
description: description:
@ -2783,5 +2834,5 @@ packages:
source: git source: git
version: "1.0.0" version: "1.0.0"
sdks: sdks:
dart: ">=3.7.0-0 <4.0.0" dart: ">=3.7.2 <4.0.0"
flutter: ">=3.29.0" flutter: ">=3.29.0"

View File

@ -142,8 +142,21 @@ dependencies:
collection: any collection: any
otp_util: ^1.0.2 otp_util: ^1.0.2
dio_http2_adapter: ^2.6.0 dio_http2_adapter: ^2.6.0
flutter_js: ^0.8.2
archive: ^4.0.7 archive: ^4.0.7
hetu_script: ^0.4.2+1
hetu_std:
git:
url: https://github.com/hetu-community/hetu_std.git
ref: main
hetu_otp_util:
git:
url: https://github.com/hetu-community/hetu_otp_util.git
ref: main
hetu_spotube_plugin:
git:
url: https://github.com/KRTirtho/hetu_spotube_plugin.git
ref: main
get_it: ^8.0.3
dev_dependencies: dev_dependencies:
build_runner: ^2.4.13 build_runner: ^2.4.13
@ -170,9 +183,10 @@ dependency_overrides:
git: git:
url: https://github.com/KRTirtho/Bonsoir.git url: https://github.com/KRTirtho/Bonsoir.git
path: packages/bonsoir_android path: packages/bonsoir_android
web: ^1.1.0
meta: 1.16.0 meta: 1.16.0
web: ^1.1.0
flutter_svg: ^2.0.17 flutter_svg: ^2.0.17
intl: any
collection: any collection: any
flutter: flutter:
@ -195,6 +209,10 @@ flutter:
- packages/flutter_undraw/assets/undraw/taken.svg - packages/flutter_undraw/assets/undraw/taken.svg
- packages/flutter_undraw/assets/undraw/empty.svg - packages/flutter_undraw/assets/undraw/empty.svg
- packages/flutter_undraw/assets/undraw/no_data.svg - packages/flutter_undraw/assets/undraw/no_data.svg
# hetu script bytecode
- packages/hetu_std/assets/bytecode/std.out
- packages/hetu_otp_util/assets/bytecode/otp_util.out
- packages/hetu_spotube_plugin/assets/bytecode/spotube_plugin.out
fonts: fonts:
- family: RadixIcons - family: RadixIcons
fonts: fonts:

View File

@ -12,8 +12,8 @@
#include <desktop_webview_window/desktop_webview_window_plugin.h> #include <desktop_webview_window/desktop_webview_window_plugin.h>
#include <file_selector_windows/file_selector_windows.h> #include <file_selector_windows/file_selector_windows.h>
#include <flutter_inappwebview_windows/flutter_inappwebview_windows_plugin_c_api.h> #include <flutter_inappwebview_windows/flutter_inappwebview_windows_plugin_c_api.h>
#include <flutter_js/flutter_js_plugin.h>
#include <flutter_secure_storage_windows/flutter_secure_storage_windows_plugin.h> #include <flutter_secure_storage_windows/flutter_secure_storage_windows_plugin.h>
#include <flutter_timezone/flutter_timezone_plugin_c_api.h>
#include <local_notifier/local_notifier_plugin.h> #include <local_notifier/local_notifier_plugin.h>
#include <media_kit_libs_windows_audio/media_kit_libs_windows_audio_plugin_c_api.h> #include <media_kit_libs_windows_audio/media_kit_libs_windows_audio_plugin_c_api.h>
#include <permission_handler_windows/permission_handler_windows_plugin.h> #include <permission_handler_windows/permission_handler_windows_plugin.h>
@ -37,10 +37,10 @@ void RegisterPlugins(flutter::PluginRegistry* registry) {
registry->GetRegistrarForPlugin("FileSelectorWindows")); registry->GetRegistrarForPlugin("FileSelectorWindows"));
FlutterInappwebviewWindowsPluginCApiRegisterWithRegistrar( FlutterInappwebviewWindowsPluginCApiRegisterWithRegistrar(
registry->GetRegistrarForPlugin("FlutterInappwebviewWindowsPluginCApi")); registry->GetRegistrarForPlugin("FlutterInappwebviewWindowsPluginCApi"));
FlutterJsPluginRegisterWithRegistrar(
registry->GetRegistrarForPlugin("FlutterJsPlugin"));
FlutterSecureStorageWindowsPluginRegisterWithRegistrar( FlutterSecureStorageWindowsPluginRegisterWithRegistrar(
registry->GetRegistrarForPlugin("FlutterSecureStorageWindowsPlugin")); registry->GetRegistrarForPlugin("FlutterSecureStorageWindowsPlugin"));
FlutterTimezonePluginCApiRegisterWithRegistrar(
registry->GetRegistrarForPlugin("FlutterTimezonePluginCApi"));
LocalNotifierPluginRegisterWithRegistrar( LocalNotifierPluginRegisterWithRegistrar(
registry->GetRegistrarForPlugin("LocalNotifierPlugin")); registry->GetRegistrarForPlugin("LocalNotifierPlugin"));
MediaKitLibsWindowsAudioPluginCApiRegisterWithRegistrar( MediaKitLibsWindowsAudioPluginCApiRegisterWithRegistrar(

View File

@ -9,8 +9,8 @@ list(APPEND FLUTTER_PLUGIN_LIST
desktop_webview_window desktop_webview_window
file_selector_windows file_selector_windows
flutter_inappwebview_windows flutter_inappwebview_windows
flutter_js
flutter_secure_storage_windows flutter_secure_storage_windows
flutter_timezone
local_notifier local_notifier
media_kit_libs_windows_audio media_kit_libs_windows_audio
permission_handler_windows permission_handler_windows