mirror of
https://github.com/KRTirtho/spotube.git
synced 2025-09-12 23:45:18 +00:00
feat: implement metadata plugins based on hetu
This commit is contained in:
parent
69c0333327
commit
7a6821f28d
@ -235,9 +235,5 @@ class AppRouter extends RootStackRouter {
|
||||
page: LastFMLoginRoute.page,
|
||||
// parentNavigatorKey: rootNavigatorKey,
|
||||
),
|
||||
AutoRoute(
|
||||
path: "/webview",
|
||||
page: WebviewRoute.page,
|
||||
),
|
||||
];
|
||||
}
|
||||
|
File diff suppressed because it is too large
Load Diff
@ -147,7 +147,7 @@ class Spotube extends HookConsumerWidget {
|
||||
ref.listen(bonsoirProvider, (_, __) {});
|
||||
ref.listen(connectClientsProvider, (_, __) {});
|
||||
ref.listen(metadataPluginsProvider, (_, __) {});
|
||||
ref.listen(metadataPluginApiProvider, (_, __) {});
|
||||
ref.listen(metadataPluginProvider, (_, __) {});
|
||||
ref.listen(serverProvider, (_, __) {});
|
||||
ref.listen(trayManagerProvider, (_, __) {});
|
||||
|
||||
|
@ -3,9 +3,9 @@ part of 'metadata.dart';
|
||||
@freezed
|
||||
class SpotubeImageObject with _$SpotubeImageObject {
|
||||
factory SpotubeImageObject({
|
||||
required final String url,
|
||||
required final int width,
|
||||
required final int height,
|
||||
required String url,
|
||||
int? width,
|
||||
int? height,
|
||||
}) = _SpotubeImageObject;
|
||||
|
||||
factory SpotubeImageObject.fromJson(Map<String, dynamic> json) =>
|
||||
|
@ -775,8 +775,8 @@ SpotubeImageObject _$SpotubeImageObjectFromJson(Map<String, dynamic> json) {
|
||||
/// @nodoc
|
||||
mixin _$SpotubeImageObject {
|
||||
String get url => throw _privateConstructorUsedError;
|
||||
int get width => throw _privateConstructorUsedError;
|
||||
int get height => throw _privateConstructorUsedError;
|
||||
int? get width => throw _privateConstructorUsedError;
|
||||
int? get height => throw _privateConstructorUsedError;
|
||||
|
||||
/// Serializes this SpotubeImageObject to a JSON map.
|
||||
Map<String, dynamic> toJson() => throw _privateConstructorUsedError;
|
||||
@ -794,7 +794,7 @@ abstract class $SpotubeImageObjectCopyWith<$Res> {
|
||||
SpotubeImageObject value, $Res Function(SpotubeImageObject) then) =
|
||||
_$SpotubeImageObjectCopyWithImpl<$Res, SpotubeImageObject>;
|
||||
@useResult
|
||||
$Res call({String url, int width, int height});
|
||||
$Res call({String url, int? width, int? height});
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
@ -813,22 +813,22 @@ class _$SpotubeImageObjectCopyWithImpl<$Res, $Val extends SpotubeImageObject>
|
||||
@override
|
||||
$Res call({
|
||||
Object? url = null,
|
||||
Object? width = null,
|
||||
Object? height = null,
|
||||
Object? width = freezed,
|
||||
Object? height = freezed,
|
||||
}) {
|
||||
return _then(_value.copyWith(
|
||||
url: null == url
|
||||
? _value.url
|
||||
: url // ignore: cast_nullable_to_non_nullable
|
||||
as String,
|
||||
width: null == width
|
||||
width: freezed == width
|
||||
? _value.width
|
||||
: width // ignore: cast_nullable_to_non_nullable
|
||||
as int,
|
||||
height: null == height
|
||||
as int?,
|
||||
height: freezed == height
|
||||
? _value.height
|
||||
: height // ignore: cast_nullable_to_non_nullable
|
||||
as int,
|
||||
as int?,
|
||||
) as $Val);
|
||||
}
|
||||
}
|
||||
@ -841,7 +841,7 @@ abstract class _$$SpotubeImageObjectImplCopyWith<$Res>
|
||||
__$$SpotubeImageObjectImplCopyWithImpl<$Res>;
|
||||
@override
|
||||
@useResult
|
||||
$Res call({String url, int width, int height});
|
||||
$Res call({String url, int? width, int? height});
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
@ -858,22 +858,22 @@ class __$$SpotubeImageObjectImplCopyWithImpl<$Res>
|
||||
@override
|
||||
$Res call({
|
||||
Object? url = null,
|
||||
Object? width = null,
|
||||
Object? height = null,
|
||||
Object? width = freezed,
|
||||
Object? height = freezed,
|
||||
}) {
|
||||
return _then(_$SpotubeImageObjectImpl(
|
||||
url: null == url
|
||||
? _value.url
|
||||
: url // ignore: cast_nullable_to_non_nullable
|
||||
as String,
|
||||
width: null == width
|
||||
width: freezed == width
|
||||
? _value.width
|
||||
: width // ignore: cast_nullable_to_non_nullable
|
||||
as int,
|
||||
height: null == height
|
||||
as int?,
|
||||
height: freezed == height
|
||||
? _value.height
|
||||
: height // ignore: cast_nullable_to_non_nullable
|
||||
as int,
|
||||
as int?,
|
||||
));
|
||||
}
|
||||
}
|
||||
@ -881,8 +881,7 @@ class __$$SpotubeImageObjectImplCopyWithImpl<$Res>
|
||||
/// @nodoc
|
||||
@JsonSerializable()
|
||||
class _$SpotubeImageObjectImpl implements _SpotubeImageObject {
|
||||
_$SpotubeImageObjectImpl(
|
||||
{required this.url, required this.width, required this.height});
|
||||
_$SpotubeImageObjectImpl({required this.url, this.width, this.height});
|
||||
|
||||
factory _$SpotubeImageObjectImpl.fromJson(Map<String, dynamic> json) =>
|
||||
_$$SpotubeImageObjectImplFromJson(json);
|
||||
@ -890,9 +889,9 @@ class _$SpotubeImageObjectImpl implements _SpotubeImageObject {
|
||||
@override
|
||||
final String url;
|
||||
@override
|
||||
final int width;
|
||||
final int? width;
|
||||
@override
|
||||
final int height;
|
||||
final int? height;
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
@ -933,8 +932,8 @@ class _$SpotubeImageObjectImpl implements _SpotubeImageObject {
|
||||
abstract class _SpotubeImageObject implements SpotubeImageObject {
|
||||
factory _SpotubeImageObject(
|
||||
{required final String url,
|
||||
required final int width,
|
||||
required final int height}) = _$SpotubeImageObjectImpl;
|
||||
final int? width,
|
||||
final int? height}) = _$SpotubeImageObjectImpl;
|
||||
|
||||
factory _SpotubeImageObject.fromJson(Map<String, dynamic> json) =
|
||||
_$SpotubeImageObjectImpl.fromJson;
|
||||
@ -942,9 +941,9 @@ abstract class _SpotubeImageObject implements SpotubeImageObject {
|
||||
@override
|
||||
String get url;
|
||||
@override
|
||||
int get width;
|
||||
int? get width;
|
||||
@override
|
||||
int get height;
|
||||
int? get height;
|
||||
|
||||
/// Create a copy of SpotubeImageObject
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@ -2192,11 +2191,10 @@ SpotubeUserObject _$SpotubeUserObjectFromJson(Map<String, dynamic> json) {
|
||||
|
||||
/// @nodoc
|
||||
mixin _$SpotubeUserObject {
|
||||
String get uid => throw _privateConstructorUsedError;
|
||||
String get id => throw _privateConstructorUsedError;
|
||||
String get name => throw _privateConstructorUsedError;
|
||||
List<SpotubeImageObject> get avatars => throw _privateConstructorUsedError;
|
||||
String get externalUrl => throw _privateConstructorUsedError;
|
||||
String get displayName => throw _privateConstructorUsedError;
|
||||
List<SpotubeImageObject> get images => throw _privateConstructorUsedError;
|
||||
String get externalUri => throw _privateConstructorUsedError;
|
||||
|
||||
/// Serializes this SpotubeUserObject to a JSON map.
|
||||
Map<String, dynamic> toJson() => throw _privateConstructorUsedError;
|
||||
@ -2215,11 +2213,10 @@ abstract class $SpotubeUserObjectCopyWith<$Res> {
|
||||
_$SpotubeUserObjectCopyWithImpl<$Res, SpotubeUserObject>;
|
||||
@useResult
|
||||
$Res call(
|
||||
{String uid,
|
||||
{String id,
|
||||
String name,
|
||||
List<SpotubeImageObject> avatars,
|
||||
String externalUrl,
|
||||
String displayName});
|
||||
List<SpotubeImageObject> images,
|
||||
String externalUri});
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
@ -2237,32 +2234,27 @@ class _$SpotubeUserObjectCopyWithImpl<$Res, $Val extends SpotubeUserObject>
|
||||
@pragma('vm:prefer-inline')
|
||||
@override
|
||||
$Res call({
|
||||
Object? uid = null,
|
||||
Object? id = null,
|
||||
Object? name = null,
|
||||
Object? avatars = null,
|
||||
Object? externalUrl = null,
|
||||
Object? displayName = null,
|
||||
Object? images = null,
|
||||
Object? externalUri = null,
|
||||
}) {
|
||||
return _then(_value.copyWith(
|
||||
uid: null == uid
|
||||
? _value.uid
|
||||
: uid // ignore: cast_nullable_to_non_nullable
|
||||
id: null == id
|
||||
? _value.id
|
||||
: id // ignore: cast_nullable_to_non_nullable
|
||||
as String,
|
||||
name: null == name
|
||||
? _value.name
|
||||
: name // ignore: cast_nullable_to_non_nullable
|
||||
as String,
|
||||
avatars: null == avatars
|
||||
? _value.avatars
|
||||
: avatars // ignore: cast_nullable_to_non_nullable
|
||||
images: null == images
|
||||
? _value.images
|
||||
: images // ignore: cast_nullable_to_non_nullable
|
||||
as List<SpotubeImageObject>,
|
||||
externalUrl: null == externalUrl
|
||||
? _value.externalUrl
|
||||
: externalUrl // ignore: cast_nullable_to_non_nullable
|
||||
as String,
|
||||
displayName: null == displayName
|
||||
? _value.displayName
|
||||
: displayName // ignore: cast_nullable_to_non_nullable
|
||||
externalUri: null == externalUri
|
||||
? _value.externalUri
|
||||
: externalUri // ignore: cast_nullable_to_non_nullable
|
||||
as String,
|
||||
) as $Val);
|
||||
}
|
||||
@ -2277,11 +2269,10 @@ abstract class _$$SpotubeUserObjectImplCopyWith<$Res>
|
||||
@override
|
||||
@useResult
|
||||
$Res call(
|
||||
{String uid,
|
||||
{String id,
|
||||
String name,
|
||||
List<SpotubeImageObject> avatars,
|
||||
String externalUrl,
|
||||
String displayName});
|
||||
List<SpotubeImageObject> images,
|
||||
String externalUri});
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
@ -2297,32 +2288,27 @@ class __$$SpotubeUserObjectImplCopyWithImpl<$Res>
|
||||
@pragma('vm:prefer-inline')
|
||||
@override
|
||||
$Res call({
|
||||
Object? uid = null,
|
||||
Object? id = null,
|
||||
Object? name = null,
|
||||
Object? avatars = null,
|
||||
Object? externalUrl = null,
|
||||
Object? displayName = null,
|
||||
Object? images = null,
|
||||
Object? externalUri = null,
|
||||
}) {
|
||||
return _then(_$SpotubeUserObjectImpl(
|
||||
uid: null == uid
|
||||
? _value.uid
|
||||
: uid // ignore: cast_nullable_to_non_nullable
|
||||
id: null == id
|
||||
? _value.id
|
||||
: id // ignore: cast_nullable_to_non_nullable
|
||||
as String,
|
||||
name: null == name
|
||||
? _value.name
|
||||
: name // ignore: cast_nullable_to_non_nullable
|
||||
as String,
|
||||
avatars: null == avatars
|
||||
? _value._avatars
|
||||
: avatars // ignore: cast_nullable_to_non_nullable
|
||||
images: null == images
|
||||
? _value._images
|
||||
: images // ignore: cast_nullable_to_non_nullable
|
||||
as List<SpotubeImageObject>,
|
||||
externalUrl: null == externalUrl
|
||||
? _value.externalUrl
|
||||
: externalUrl // ignore: cast_nullable_to_non_nullable
|
||||
as String,
|
||||
displayName: null == displayName
|
||||
? _value.displayName
|
||||
: displayName // ignore: cast_nullable_to_non_nullable
|
||||
externalUri: null == externalUri
|
||||
? _value.externalUri
|
||||
: externalUri // ignore: cast_nullable_to_non_nullable
|
||||
as String,
|
||||
));
|
||||
}
|
||||
@ -2332,37 +2318,34 @@ class __$$SpotubeUserObjectImplCopyWithImpl<$Res>
|
||||
@JsonSerializable()
|
||||
class _$SpotubeUserObjectImpl implements _SpotubeUserObject {
|
||||
_$SpotubeUserObjectImpl(
|
||||
{required this.uid,
|
||||
{required this.id,
|
||||
required this.name,
|
||||
final List<SpotubeImageObject> avatars = const [],
|
||||
required this.externalUrl,
|
||||
required this.displayName})
|
||||
: _avatars = avatars;
|
||||
final List<SpotubeImageObject> images = const [],
|
||||
required this.externalUri})
|
||||
: _images = images;
|
||||
|
||||
factory _$SpotubeUserObjectImpl.fromJson(Map<String, dynamic> json) =>
|
||||
_$$SpotubeUserObjectImplFromJson(json);
|
||||
|
||||
@override
|
||||
final String uid;
|
||||
final String id;
|
||||
@override
|
||||
final String name;
|
||||
final List<SpotubeImageObject> _avatars;
|
||||
final List<SpotubeImageObject> _images;
|
||||
@override
|
||||
@JsonKey()
|
||||
List<SpotubeImageObject> get avatars {
|
||||
if (_avatars is EqualUnmodifiableListView) return _avatars;
|
||||
List<SpotubeImageObject> get images {
|
||||
if (_images is EqualUnmodifiableListView) return _images;
|
||||
// ignore: implicit_dynamic_type
|
||||
return EqualUnmodifiableListView(_avatars);
|
||||
return EqualUnmodifiableListView(_images);
|
||||
}
|
||||
|
||||
@override
|
||||
final String externalUrl;
|
||||
@override
|
||||
final String displayName;
|
||||
final String externalUri;
|
||||
|
||||
@override
|
||||
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
|
||||
@ -2370,19 +2353,17 @@ class _$SpotubeUserObjectImpl implements _SpotubeUserObject {
|
||||
return identical(this, other) ||
|
||||
(other.runtimeType == runtimeType &&
|
||||
other is _$SpotubeUserObjectImpl &&
|
||||
(identical(other.uid, uid) || other.uid == uid) &&
|
||||
(identical(other.id, id) || other.id == id) &&
|
||||
(identical(other.name, name) || other.name == name) &&
|
||||
const DeepCollectionEquality().equals(other._avatars, _avatars) &&
|
||||
(identical(other.externalUrl, externalUrl) ||
|
||||
other.externalUrl == externalUrl) &&
|
||||
(identical(other.displayName, displayName) ||
|
||||
other.displayName == displayName));
|
||||
const DeepCollectionEquality().equals(other._images, _images) &&
|
||||
(identical(other.externalUri, externalUri) ||
|
||||
other.externalUri == externalUri));
|
||||
}
|
||||
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@override
|
||||
int get hashCode => Object.hash(runtimeType, uid, name,
|
||||
const DeepCollectionEquality().hash(_avatars), externalUrl, displayName);
|
||||
int get hashCode => Object.hash(runtimeType, id, name,
|
||||
const DeepCollectionEquality().hash(_images), externalUri);
|
||||
|
||||
/// Create a copy of SpotubeUserObject
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@ -2403,25 +2384,22 @@ class _$SpotubeUserObjectImpl implements _SpotubeUserObject {
|
||||
|
||||
abstract class _SpotubeUserObject implements SpotubeUserObject {
|
||||
factory _SpotubeUserObject(
|
||||
{required final String uid,
|
||||
{required final String id,
|
||||
required final String name,
|
||||
final List<SpotubeImageObject> avatars,
|
||||
required final String externalUrl,
|
||||
required final String displayName}) = _$SpotubeUserObjectImpl;
|
||||
final List<SpotubeImageObject> images,
|
||||
required final String externalUri}) = _$SpotubeUserObjectImpl;
|
||||
|
||||
factory _SpotubeUserObject.fromJson(Map<String, dynamic> json) =
|
||||
_$SpotubeUserObjectImpl.fromJson;
|
||||
|
||||
@override
|
||||
String get uid;
|
||||
String get id;
|
||||
@override
|
||||
String get name;
|
||||
@override
|
||||
List<SpotubeImageObject> get avatars;
|
||||
List<SpotubeImageObject> get images;
|
||||
@override
|
||||
String get externalUrl;
|
||||
@override
|
||||
String get displayName;
|
||||
String get externalUri;
|
||||
|
||||
/// Create a copy of SpotubeUserObject
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
|
@ -84,8 +84,8 @@ Map<String, dynamic> _$$SpotubeFeedObjectImplToJson(
|
||||
_$SpotubeImageObjectImpl _$$SpotubeImageObjectImplFromJson(Map json) =>
|
||||
_$SpotubeImageObjectImpl(
|
||||
url: json['url'] as String,
|
||||
width: (json['width'] as num).toInt(),
|
||||
height: (json['height'] as num).toInt(),
|
||||
width: (json['width'] as num?)?.toInt(),
|
||||
height: (json['height'] as num?)?.toInt(),
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$$SpotubeImageObjectImplToJson(
|
||||
@ -203,25 +203,23 @@ Map<String, dynamic> _$$SpotubeTrackObjectImplToJson(
|
||||
|
||||
_$SpotubeUserObjectImpl _$$SpotubeUserObjectImplFromJson(Map json) =>
|
||||
_$SpotubeUserObjectImpl(
|
||||
uid: json['uid'] as String,
|
||||
id: json['id'] as String,
|
||||
name: json['name'] as String,
|
||||
avatars: (json['avatars'] as List<dynamic>?)
|
||||
images: (json['images'] as List<dynamic>?)
|
||||
?.map((e) => SpotubeImageObject.fromJson(
|
||||
Map<String, dynamic>.from(e as Map)))
|
||||
.toList() ??
|
||||
const [],
|
||||
externalUrl: json['externalUrl'] as String,
|
||||
displayName: json['displayName'] as String,
|
||||
externalUri: json['externalUri'] as String,
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$$SpotubeUserObjectImplToJson(
|
||||
_$SpotubeUserObjectImpl instance) =>
|
||||
<String, dynamic>{
|
||||
'uid': instance.uid,
|
||||
'id': instance.id,
|
||||
'name': instance.name,
|
||||
'avatars': instance.avatars.map((e) => e.toJson()).toList(),
|
||||
'externalUrl': instance.externalUrl,
|
||||
'displayName': instance.displayName,
|
||||
'images': instance.images.map((e) => e.toJson()).toList(),
|
||||
'externalUri': instance.externalUri,
|
||||
};
|
||||
|
||||
_$PluginConfigurationImpl _$$PluginConfigurationImplFromJson(Map json) =>
|
||||
|
@ -3,11 +3,10 @@ part of 'metadata.dart';
|
||||
@freezed
|
||||
class SpotubeUserObject with _$SpotubeUserObject {
|
||||
factory SpotubeUserObject({
|
||||
required final String uid,
|
||||
required final String id,
|
||||
required final String name,
|
||||
@Default([]) final List<SpotubeImageObject> avatars,
|
||||
required final String externalUrl,
|
||||
required final String displayName,
|
||||
@Default([]) final List<SpotubeImageObject> images,
|
||||
required final String externalUri,
|
||||
}) = _SpotubeUserObject;
|
||||
|
||||
factory SpotubeUserObject.fromJson(Map<String, dynamic> json) =>
|
||||
|
@ -7,6 +7,7 @@ import 'package:shadcn_flutter/shadcn_flutter.dart';
|
||||
import 'package:spotube/collections/spotube_icons.dart';
|
||||
import 'package:spotube/components/form/text_form_field.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/metadata_plugin_provider.dart';
|
||||
import 'package:file_picker/file_picker.dart';
|
||||
@ -21,10 +22,8 @@ class SettingsMetadataProviderPage extends HookConsumerWidget {
|
||||
|
||||
final plugins = ref.watch(metadataPluginsProvider);
|
||||
final pluginsNotifier = ref.watch(metadataPluginsProvider.notifier);
|
||||
final metadataApi = ref.watch(metadataPluginApiProvider);
|
||||
final isAuthenticated = ref.watch(metadataAuthenticatedProvider);
|
||||
|
||||
final artists = ref.watch(metadataUserArtistsProvider);
|
||||
final metadataPlugin = ref.watch(metadataPluginProvider);
|
||||
final isAuthenticated = ref.watch(metadataPluginAuthenticatedProvider);
|
||||
|
||||
return Scaffold(
|
||||
headers: const [
|
||||
@ -111,9 +110,7 @@ class SettingsMetadataProviderPage extends HookConsumerWidget {
|
||||
final plugin = plugins.asData!.value.plugins[index];
|
||||
final isDefault = plugins.asData!.value.defaultPlugin == index;
|
||||
final requiresAuth = isDefault &&
|
||||
metadataApi.hasValue &&
|
||||
metadataApi.asData?.value?.signatureFlags.requiresAuth ==
|
||||
true;
|
||||
plugin.abilities.contains(PluginAbilities.authentication);
|
||||
return Card(
|
||||
child: Column(
|
||||
spacing: 8,
|
||||
@ -153,8 +150,8 @@ class SettingsMetadataProviderPage extends HookConsumerWidget {
|
||||
if (isAuthenticated.asData?.value != true)
|
||||
Button.primary(
|
||||
onPressed: () async {
|
||||
await metadataApi.asData?.value
|
||||
?.authenticate();
|
||||
await metadataPlugin.asData?.value?.auth
|
||||
.authenticate();
|
||||
},
|
||||
leading: const Icon(SpotubeIcons.login),
|
||||
child: const Text("Login"),
|
||||
@ -162,7 +159,8 @@ class SettingsMetadataProviderPage extends HookConsumerWidget {
|
||||
else
|
||||
Button.destructive(
|
||||
onPressed: () async {
|
||||
await metadataApi.asData?.value?.logout();
|
||||
await metadataPlugin.asData?.value?.auth
|
||||
.logout();
|
||||
},
|
||||
leading: const Icon(SpotubeIcons.logout),
|
||||
child: const Text("Logout"),
|
||||
|
@ -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,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
@ -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';
|
||||
|
||||
class MetadataAuthenticationNotifier extends AsyncNotifier<bool> {
|
||||
MetadataAuthenticationNotifier();
|
||||
class MetadataPluginAuthenticatedNotifier extends AsyncNotifier<bool> {
|
||||
@override
|
||||
build() async {
|
||||
final metadataApi = await ref.watch(metadataPluginApiProvider.future);
|
||||
|
||||
if (metadataApi?.signatureFlags.requiresAuth != true) {
|
||||
FutureOr<bool> build() async {
|
||||
final defaultPluginConfig = ref.watch(metadataPluginsProvider);
|
||||
if (defaultPluginConfig.asData?.value.defaultPluginConfig?.abilities
|
||||
.contains(PluginAbilities.authentication) !=
|
||||
true) {
|
||||
return false;
|
||||
}
|
||||
|
||||
final subscription = metadataApi?.authenticatedStream.listen((event) {
|
||||
state = AsyncValue.data(event);
|
||||
final defaultPlugin = await ref.watch(metadataPluginProvider.future);
|
||||
if (defaultPlugin == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
final sub = defaultPlugin.auth.authStateStream.listen((event) {
|
||||
state = AsyncData(defaultPlugin.auth.isAuthenticated());
|
||||
});
|
||||
|
||||
ref.onDispose(() {
|
||||
subscription?.cancel();
|
||||
sub.cancel();
|
||||
});
|
||||
|
||||
return await metadataApi?.isAuthenticated() ?? false;
|
||||
}
|
||||
|
||||
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();
|
||||
return defaultPlugin.auth.isAuthenticated();
|
||||
}
|
||||
}
|
||||
|
||||
final metadataAuthenticatedProvider =
|
||||
AsyncNotifierProvider<MetadataAuthenticationNotifier, bool>(
|
||||
() => MetadataAuthenticationNotifier(),
|
||||
final metadataPluginAuthenticatedProvider =
|
||||
AsyncNotifierProvider<MetadataPluginAuthenticatedNotifier, bool>(
|
||||
MetadataPluginAuthenticatedNotifier.new,
|
||||
);
|
||||
|
@ -10,7 +10,6 @@ import 'package:path_provider/path_provider.dart';
|
||||
import 'package:spotube/models/database/database.dart';
|
||||
import 'package:spotube/models/metadata/metadata.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/metadata/metadata.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 pluginExtractionDirPath = join(
|
||||
pluginDir.path,
|
||||
ServiceUtils.sanitizeFilename(plugin.name),
|
||||
);
|
||||
|
||||
final libraryFile = File(join(pluginExtractionDirPath, "dist", "index.js"));
|
||||
final libraryFile = File(join(pluginExtractionDirPath, "plugin.out"));
|
||||
|
||||
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,
|
||||
);
|
||||
|
||||
final metadataPluginApiProvider = FutureProvider<MetadataApiSignature?>(
|
||||
final metadataPluginProvider = FutureProvider<MetadataPlugin?>(
|
||||
(ref) async {
|
||||
final defaultPlugin = await ref.watch(
|
||||
metadataPluginsProvider.selectAsync((data) => data.defaultPluginConfig),
|
||||
@ -348,38 +347,9 @@ final metadataPluginApiProvider = FutureProvider<MetadataApiSignature?>(
|
||||
}
|
||||
|
||||
final pluginsNotifier = ref.read(metadataPluginsProvider.notifier);
|
||||
final libraryCode =
|
||||
await pluginsNotifier.getPluginLibraryCode(defaultPlugin);
|
||||
final pluginByteCode =
|
||||
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>;
|
||||
});
|
||||
|
16
lib/provider/metadata_plugin/user.dart
Normal file
16
lib/provider/metadata_plugin/user.dart
Normal 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();
|
||||
},
|
||||
);
|
@ -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';
|
||||
|
||||
class PluginLocalStorageApi {
|
||||
final JavascriptRuntime runtime;
|
||||
final SharedPreferences sharedPreferences;
|
||||
class SharedPreferencesLocalStorage implements Localstorage {
|
||||
final SharedPreferences _prefs;
|
||||
final String pluginSlug;
|
||||
|
||||
final String pluginName;
|
||||
SharedPreferencesLocalStorage(this._prefs, this.pluginSlug);
|
||||
|
||||
PluginLocalStorageApi({
|
||||
required this.runtime,
|
||||
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();
|
||||
});
|
||||
String prefix(String key) {
|
||||
return 'spotube_plugin.$pluginSlug.$key';
|
||||
}
|
||||
|
||||
void setItem(String key, String value) async {
|
||||
await sharedPreferences.setString("plugin.$pluginName.$key", value);
|
||||
@override
|
||||
Future<void> clear() {
|
||||
return _prefs.clear();
|
||||
}
|
||||
|
||||
String? getItem(String key) {
|
||||
return sharedPreferences.getString("plugin.$pluginName.$key");
|
||||
@override
|
||||
Future<bool> containsKey(String key) async {
|
||||
return _prefs.containsKey(prefix(key));
|
||||
}
|
||||
|
||||
void removeItem(String key) async {
|
||||
await sharedPreferences.remove("plugin.$pluginName.$key");
|
||||
@override
|
||||
Future<bool?> getBool(String key) async {
|
||||
return _prefs.getBool(prefix(key));
|
||||
}
|
||||
|
||||
void clear() async {
|
||||
final keys = sharedPreferences.getKeys();
|
||||
for (String key in keys) {
|
||||
if (key.startsWith("plugin.$pluginName.")) {
|
||||
await sharedPreferences.remove(key);
|
||||
}
|
||||
}
|
||||
@override
|
||||
Future<double?> getDouble(String key) async {
|
||||
return _prefs.getDouble(prefix(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);
|
||||
}
|
||||
}
|
||||
|
@ -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();
|
||||
}
|
||||
}
|
@ -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');
|
||||
""",
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
0
lib/services/metadata/endpoints/album.dart
Normal file
0
lib/services/metadata/endpoints/album.dart
Normal file
0
lib/services/metadata/endpoints/artist.dart
Normal file
0
lib/services/metadata/endpoints/artist.dart
Normal file
33
lib/services/metadata/endpoints/auth.dart
Normal file
33
lib/services/metadata/endpoints/auth.dart
Normal 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();
|
||||
}
|
||||
}
|
||||
}
|
0
lib/services/metadata/endpoints/browse.dart
Normal file
0
lib/services/metadata/endpoints/browse.dart
Normal file
0
lib/services/metadata/endpoints/playlist.dart
Normal file
0
lib/services/metadata/endpoints/playlist.dart
Normal file
0
lib/services/metadata/endpoints/search.dart
Normal file
0
lib/services/metadata/endpoints/search.dart
Normal file
0
lib/services/metadata/endpoints/track.dart
Normal file
0
lib/services/metadata/endpoints/track.dart
Normal file
15
lib/services/metadata/endpoints/user.dart
Normal file
15
lib/services/metadata/endpoints/user.dart
Normal 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>(),
|
||||
);
|
||||
}
|
||||
}
|
@ -1,564 +1,81 @@
|
||||
import 'dart:async';
|
||||
import 'dart:convert';
|
||||
import 'dart:typed_data';
|
||||
|
||||
import 'package:flutter_js/extensions/fetch.dart';
|
||||
import 'package:flutter_js/extensions/xhr.dart';
|
||||
import 'package:flutter_js/flutter_js.dart';
|
||||
import 'package:auto_route/auto_route.dart';
|
||||
import 'package:hetu_otp_util/hetu_otp_util.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:spotube/collections/routes.dart';
|
||||
import 'package:spotube/components/titlebar/titlebar.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/set_interval.dart';
|
||||
import 'package:spotube/services/metadata/apis/totp.dart';
|
||||
import 'package:spotube/services/metadata/apis/webview.dart';
|
||||
import 'package:spotube/services/metadata/endpoints/auth.dart';
|
||||
import 'package:spotube/services/metadata/endpoints/user.dart';
|
||||
|
||||
const defaultMetadataLimit = "20";
|
||||
|
||||
class MetadataSignatureFlags {
|
||||
final bool requiresAuth;
|
||||
|
||||
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,
|
||||
class MetadataPlugin {
|
||||
static Future<MetadataPlugin> create(
|
||||
PluginConfiguration config,
|
||||
Uint8List byteCode,
|
||||
) async {
|
||||
final runtime = getJavascriptRuntime(xhr: true).enableXhr();
|
||||
runtime.enableHandlePromises();
|
||||
await runtime.enableFetch();
|
||||
final sharedPreferences = await SharedPreferences.getInstance();
|
||||
BuildContext? pageContext;
|
||||
|
||||
Timer.periodic(
|
||||
const Duration(milliseconds: 100),
|
||||
(timer) {
|
||||
runtime.executePendingJob();
|
||||
final hetu = Hetu();
|
||||
hetu.init();
|
||||
|
||||
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
|
||||
final localStorageApi = PluginLocalStorageApi(
|
||||
runtime: runtime,
|
||||
sharedPreferences: await SharedPreferences.getInstance(),
|
||||
pluginName: config.slug,
|
||||
);
|
||||
await HetuStdLoader.loadBytecodeFlutter(hetu);
|
||||
await HetuOtpUtilLoader.loadBytecodeFlutter(hetu);
|
||||
await HetuSpotubePluginLoader.loadBytecodeFlutter(hetu);
|
||||
|
||||
final webViewApi = PluginWebViewApi(runtime: runtime);
|
||||
final totpGenerator = PluginTotpGenerator(runtime);
|
||||
final setIntervalApi = PluginSetIntervalApi(runtime);
|
||||
hetu.loadBytecode(bytes: byteCode, moduleName: "plugin");
|
||||
hetu.eval("""
|
||||
import "module:plugin" as plugin
|
||||
|
||||
final metadataApi = MetadataApiSignature._(
|
||||
runtime,
|
||||
localStorageApi,
|
||||
webViewApi,
|
||||
totpGenerator,
|
||||
setIntervalApi,
|
||||
);
|
||||
var Plugin = plugin.${config.entryPoint}
|
||||
|
||||
final res = runtime.evaluate(
|
||||
"""
|
||||
;$libraryCode;
|
||||
const metadataApi = new MetadataApi();
|
||||
""",
|
||||
);
|
||||
metadataApi._signatureFlags = await metadataApi._getSignatureFlags();
|
||||
var metadataPlugin = Plugin()
|
||||
""");
|
||||
|
||||
if (res.isError) {
|
||||
AppLogger.reportError(
|
||||
"Error evaluating code: $libraryCode\n${res.rawResult}",
|
||||
);
|
||||
}
|
||||
|
||||
return metadataApi;
|
||||
return MetadataPlugin._(hetu);
|
||||
}
|
||||
|
||||
void dispose() {
|
||||
setIntervalApi.dispose();
|
||||
webViewApi.dispose();
|
||||
runtime.dispose();
|
||||
}
|
||||
final Hetu hetu;
|
||||
|
||||
Future invoke(String method, [List? args]) async {
|
||||
final completer = Completer();
|
||||
runtime.onMessage(method, (result) {
|
||||
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()}`);
|
||||
}
|
||||
});
|
||||
""";
|
||||
late final MetadataAuthEndpoint auth;
|
||||
late final MetadataPluginUserEndpoint user;
|
||||
|
||||
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]);
|
||||
MetadataPlugin._(this.hetu) {
|
||||
auth = MetadataAuthEndpoint(hetu);
|
||||
user = MetadataPluginUserEndpoint(hetu);
|
||||
}
|
||||
}
|
||||
|
@ -8,8 +8,8 @@
|
||||
|
||||
#include <desktop_webview_window/desktop_webview_window_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_timezone/flutter_timezone_plugin.h>
|
||||
#include <gtk/gtk_plugin.h>
|
||||
#include <local_notifier/local_notifier_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 =
|
||||
fl_plugin_registry_get_registrar_for_plugin(registry, "FileSelectorPlugin");
|
||||
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 =
|
||||
fl_plugin_registry_get_registrar_for_plugin(registry, "FlutterSecureStorageLinuxPlugin");
|
||||
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 =
|
||||
fl_plugin_registry_get_registrar_for_plugin(registry, "GtkPlugin");
|
||||
gtk_plugin_register_with_registrar(gtk_registrar);
|
||||
|
@ -5,8 +5,8 @@
|
||||
list(APPEND FLUTTER_PLUGIN_LIST
|
||||
desktop_webview_window
|
||||
file_selector_linux
|
||||
flutter_js
|
||||
flutter_secure_storage_linux
|
||||
flutter_timezone
|
||||
gtk
|
||||
local_notifier
|
||||
media_kit_libs_linux
|
||||
|
@ -14,8 +14,8 @@ import desktop_webview_window
|
||||
import device_info_plus
|
||||
import file_selector_macos
|
||||
import flutter_inappwebview_macos
|
||||
import flutter_js
|
||||
import flutter_secure_storage_macos
|
||||
import flutter_timezone
|
||||
import local_notifier
|
||||
import media_kit_libs_macos_audio
|
||||
import open_file_mac
|
||||
@ -40,8 +40,8 @@ func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
|
||||
DeviceInfoPlusMacosPlugin.register(with: registry.registrar(forPlugin: "DeviceInfoPlusMacosPlugin"))
|
||||
FileSelectorPlugin.register(with: registry.registrar(forPlugin: "FileSelectorPlugin"))
|
||||
InAppWebViewFlutterPlugin.register(with: registry.registrar(forPlugin: "InAppWebViewFlutterPlugin"))
|
||||
FlutterJsPlugin.register(with: registry.registrar(forPlugin: "FlutterJsPlugin"))
|
||||
FlutterSecureStoragePlugin.register(with: registry.registrar(forPlugin: "FlutterSecureStoragePlugin"))
|
||||
FlutterTimezonePlugin.register(with: registry.registrar(forPlugin: "FlutterTimezonePlugin"))
|
||||
LocalNotifierPlugin.register(with: registry.registrar(forPlugin: "LocalNotifierPlugin"))
|
||||
MediaKitLibsMacosAudioPlugin.register(with: registry.registrar(forPlugin: "MediaKitLibsMacosAudioPlugin"))
|
||||
OpenFilePlugin.register(with: registry.registrar(forPlugin: "OpenFilePlugin"))
|
||||
|
73
pubspec.lock
73
pubspec.lock
@ -540,10 +540,10 @@ packages:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: dio
|
||||
sha256: "5598aa796bbf4699afd5c67c0f5f6e2ed542afc956884b9cd58c306966efc260"
|
||||
sha256: "253a18bbd4851fecba42f7343a1df3a9a4c1d31a2c1b37e221086b4fa8c8dbc9"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "5.7.0"
|
||||
version: "5.8.0+1"
|
||||
dio_http2_adapter:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
@ -658,6 +658,14 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
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:
|
||||
dependency: transitive
|
||||
description:
|
||||
@ -909,14 +917,6 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
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:
|
||||
dependency: "direct dev"
|
||||
description:
|
||||
@ -1048,6 +1048,14 @@ packages:
|
||||
description: flutter
|
||||
source: sdk
|
||||
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:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
@ -1122,6 +1130,14 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
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:
|
||||
dependency: transitive
|
||||
description:
|
||||
@ -1170,6 +1186,41 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
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:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
@ -2783,5 +2834,5 @@ packages:
|
||||
source: git
|
||||
version: "1.0.0"
|
||||
sdks:
|
||||
dart: ">=3.7.0-0 <4.0.0"
|
||||
dart: ">=3.7.2 <4.0.0"
|
||||
flutter: ">=3.29.0"
|
||||
|
22
pubspec.yaml
22
pubspec.yaml
@ -142,8 +142,21 @@ dependencies:
|
||||
collection: any
|
||||
otp_util: ^1.0.2
|
||||
dio_http2_adapter: ^2.6.0
|
||||
flutter_js: ^0.8.2
|
||||
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:
|
||||
build_runner: ^2.4.13
|
||||
@ -170,9 +183,10 @@ dependency_overrides:
|
||||
git:
|
||||
url: https://github.com/KRTirtho/Bonsoir.git
|
||||
path: packages/bonsoir_android
|
||||
web: ^1.1.0
|
||||
meta: 1.16.0
|
||||
web: ^1.1.0
|
||||
flutter_svg: ^2.0.17
|
||||
intl: any
|
||||
collection: any
|
||||
|
||||
flutter:
|
||||
@ -195,6 +209,10 @@ flutter:
|
||||
- packages/flutter_undraw/assets/undraw/taken.svg
|
||||
- packages/flutter_undraw/assets/undraw/empty.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:
|
||||
- family: RadixIcons
|
||||
fonts:
|
||||
|
@ -12,8 +12,8 @@
|
||||
#include <desktop_webview_window/desktop_webview_window_plugin.h>
|
||||
#include <file_selector_windows/file_selector_windows.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_timezone/flutter_timezone_plugin_c_api.h>
|
||||
#include <local_notifier/local_notifier_plugin.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>
|
||||
@ -37,10 +37,10 @@ void RegisterPlugins(flutter::PluginRegistry* registry) {
|
||||
registry->GetRegistrarForPlugin("FileSelectorWindows"));
|
||||
FlutterInappwebviewWindowsPluginCApiRegisterWithRegistrar(
|
||||
registry->GetRegistrarForPlugin("FlutterInappwebviewWindowsPluginCApi"));
|
||||
FlutterJsPluginRegisterWithRegistrar(
|
||||
registry->GetRegistrarForPlugin("FlutterJsPlugin"));
|
||||
FlutterSecureStorageWindowsPluginRegisterWithRegistrar(
|
||||
registry->GetRegistrarForPlugin("FlutterSecureStorageWindowsPlugin"));
|
||||
FlutterTimezonePluginCApiRegisterWithRegistrar(
|
||||
registry->GetRegistrarForPlugin("FlutterTimezonePluginCApi"));
|
||||
LocalNotifierPluginRegisterWithRegistrar(
|
||||
registry->GetRegistrarForPlugin("LocalNotifierPlugin"));
|
||||
MediaKitLibsWindowsAudioPluginCApiRegisterWithRegistrar(
|
||||
|
@ -9,8 +9,8 @@ list(APPEND FLUTTER_PLUGIN_LIST
|
||||
desktop_webview_window
|
||||
file_selector_windows
|
||||
flutter_inappwebview_windows
|
||||
flutter_js
|
||||
flutter_secure_storage_windows
|
||||
flutter_timezone
|
||||
local_notifier
|
||||
media_kit_libs_windows_audio
|
||||
permission_handler_windows
|
||||
|
Loading…
Reference in New Issue
Block a user