diff --git a/lib/models/metadata/metadata.dart b/lib/models/metadata/metadata.dart index 0e17d796..99f7c687 100644 --- a/lib/models/metadata/metadata.dart +++ b/lib/models/metadata/metadata.dart @@ -14,3 +14,5 @@ part 'playlist.dart'; part 'search.dart'; part 'track.dart'; part 'user.dart'; + +part 'plugin.dart'; diff --git a/lib/models/metadata/metadata.freezed.dart b/lib/models/metadata/metadata.freezed.dart index c7777b76..b5a2599f 100644 --- a/lib/models/metadata/metadata.freezed.dart +++ b/lib/models/metadata/metadata.freezed.dart @@ -2430,3 +2430,241 @@ abstract class _SpotubeUserObject implements SpotubeUserObject { _$$SpotubeUserObjectImplCopyWith<_$SpotubeUserObjectImpl> get copyWith => throw _privateConstructorUsedError; } + +PluginConfiguration _$PluginConfigurationFromJson(Map json) { + return _PluginConfiguration.fromJson(json); +} + +/// @nodoc +mixin _$PluginConfiguration { + PluginType get type => throw _privateConstructorUsedError; + String get name => throw _privateConstructorUsedError; + String get description => throw _privateConstructorUsedError; + String get version => throw _privateConstructorUsedError; + String get author => throw _privateConstructorUsedError; + + /// Serializes this PluginConfiguration to a JSON map. + Map toJson() => throw _privateConstructorUsedError; + + /// Create a copy of PluginConfiguration + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + $PluginConfigurationCopyWith get copyWith => + throw _privateConstructorUsedError; +} + +/// @nodoc +abstract class $PluginConfigurationCopyWith<$Res> { + factory $PluginConfigurationCopyWith( + PluginConfiguration value, $Res Function(PluginConfiguration) then) = + _$PluginConfigurationCopyWithImpl<$Res, PluginConfiguration>; + @useResult + $Res call( + {PluginType type, + String name, + String description, + String version, + String author}); +} + +/// @nodoc +class _$PluginConfigurationCopyWithImpl<$Res, $Val extends PluginConfiguration> + implements $PluginConfigurationCopyWith<$Res> { + _$PluginConfigurationCopyWithImpl(this._value, this._then); + + // ignore: unused_field + final $Val _value; + // ignore: unused_field + final $Res Function($Val) _then; + + /// Create a copy of PluginConfiguration + /// with the given fields replaced by the non-null parameter values. + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? type = null, + Object? name = null, + Object? description = null, + Object? version = null, + Object? author = null, + }) { + return _then(_value.copyWith( + type: null == type + ? _value.type + : type // ignore: cast_nullable_to_non_nullable + as PluginType, + name: null == name + ? _value.name + : name // ignore: cast_nullable_to_non_nullable + as String, + description: null == description + ? _value.description + : description // ignore: cast_nullable_to_non_nullable + as String, + version: null == version + ? _value.version + : version // ignore: cast_nullable_to_non_nullable + as String, + author: null == author + ? _value.author + : author // ignore: cast_nullable_to_non_nullable + as String, + ) as $Val); + } +} + +/// @nodoc +abstract class _$$PluginConfigurationImplCopyWith<$Res> + implements $PluginConfigurationCopyWith<$Res> { + factory _$$PluginConfigurationImplCopyWith(_$PluginConfigurationImpl value, + $Res Function(_$PluginConfigurationImpl) then) = + __$$PluginConfigurationImplCopyWithImpl<$Res>; + @override + @useResult + $Res call( + {PluginType type, + String name, + String description, + String version, + String author}); +} + +/// @nodoc +class __$$PluginConfigurationImplCopyWithImpl<$Res> + extends _$PluginConfigurationCopyWithImpl<$Res, _$PluginConfigurationImpl> + implements _$$PluginConfigurationImplCopyWith<$Res> { + __$$PluginConfigurationImplCopyWithImpl(_$PluginConfigurationImpl _value, + $Res Function(_$PluginConfigurationImpl) _then) + : super(_value, _then); + + /// Create a copy of PluginConfiguration + /// with the given fields replaced by the non-null parameter values. + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? type = null, + Object? name = null, + Object? description = null, + Object? version = null, + Object? author = null, + }) { + return _then(_$PluginConfigurationImpl( + type: null == type + ? _value.type + : type // ignore: cast_nullable_to_non_nullable + as PluginType, + name: null == name + ? _value.name + : name // ignore: cast_nullable_to_non_nullable + as String, + description: null == description + ? _value.description + : description // ignore: cast_nullable_to_non_nullable + as String, + version: null == version + ? _value.version + : version // ignore: cast_nullable_to_non_nullable + as String, + author: null == author + ? _value.author + : author // ignore: cast_nullable_to_non_nullable + as String, + )); + } +} + +/// @nodoc +@JsonSerializable() +class _$PluginConfigurationImpl extends _PluginConfiguration { + _$PluginConfigurationImpl( + {required this.type, + required this.name, + required this.description, + required this.version, + required this.author}) + : super._(); + + factory _$PluginConfigurationImpl.fromJson(Map json) => + _$$PluginConfigurationImplFromJson(json); + + @override + final PluginType type; + @override + final String name; + @override + final String description; + @override + final String version; + @override + final String author; + + @override + String toString() { + return 'PluginConfiguration(type: $type, name: $name, description: $description, version: $version, author: $author)'; + } + + @override + bool operator ==(Object other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is _$PluginConfigurationImpl && + (identical(other.type, type) || other.type == type) && + (identical(other.name, name) || other.name == name) && + (identical(other.description, description) || + other.description == description) && + (identical(other.version, version) || other.version == version) && + (identical(other.author, author) || other.author == author)); + } + + @JsonKey(includeFromJson: false, includeToJson: false) + @override + int get hashCode => + Object.hash(runtimeType, type, name, description, version, author); + + /// Create a copy of PluginConfiguration + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + @override + @pragma('vm:prefer-inline') + _$$PluginConfigurationImplCopyWith<_$PluginConfigurationImpl> get copyWith => + __$$PluginConfigurationImplCopyWithImpl<_$PluginConfigurationImpl>( + this, _$identity); + + @override + Map toJson() { + return _$$PluginConfigurationImplToJson( + this, + ); + } +} + +abstract class _PluginConfiguration extends PluginConfiguration { + factory _PluginConfiguration( + {required final PluginType type, + required final String name, + required final String description, + required final String version, + required final String author}) = _$PluginConfigurationImpl; + _PluginConfiguration._() : super._(); + + factory _PluginConfiguration.fromJson(Map json) = + _$PluginConfigurationImpl.fromJson; + + @override + PluginType get type; + @override + String get name; + @override + String get description; + @override + String get version; + @override + String get author; + + /// Create a copy of PluginConfiguration + /// with the given fields replaced by the non-null parameter values. + @override + @JsonKey(includeFromJson: false, includeToJson: false) + _$$PluginConfigurationImplCopyWith<_$PluginConfigurationImpl> get copyWith => + throw _privateConstructorUsedError; +} diff --git a/lib/models/metadata/metadata.g.dart b/lib/models/metadata/metadata.g.dart index 5e76a653..96c8119c 100644 --- a/lib/models/metadata/metadata.g.dart +++ b/lib/models/metadata/metadata.g.dart @@ -223,3 +223,26 @@ Map _$$SpotubeUserObjectImplToJson( 'externalUrl': instance.externalUrl, 'displayName': instance.displayName, }; + +_$PluginConfigurationImpl _$$PluginConfigurationImplFromJson(Map json) => + _$PluginConfigurationImpl( + type: $enumDecode(_$PluginTypeEnumMap, json['type']), + name: json['name'] as String, + description: json['description'] as String, + version: json['version'] as String, + author: json['author'] as String, + ); + +Map _$$PluginConfigurationImplToJson( + _$PluginConfigurationImpl instance) => + { + 'type': _$PluginTypeEnumMap[instance.type]!, + 'name': instance.name, + 'description': instance.description, + 'version': instance.version, + 'author': instance.author, + }; + +const _$PluginTypeEnumMap = { + PluginType.metadata: 'metadata', +}; diff --git a/lib/models/metadata/plugin.dart b/lib/models/metadata/plugin.dart new file mode 100644 index 00000000..d6254168 --- /dev/null +++ b/lib/models/metadata/plugin.dart @@ -0,0 +1,21 @@ +part of 'metadata.dart'; + +enum PluginType { metadata } + +@freezed +class PluginConfiguration with _$PluginConfiguration { + const PluginConfiguration._(); + + factory PluginConfiguration({ + required PluginType type, + required String name, + required String description, + required String version, + required String author, + }) = _PluginConfiguration; + + factory PluginConfiguration.fromJson(Map json) => + _$PluginConfigurationFromJson(json); + + String get slug => name.toLowerCase().replaceAll(RegExp(r'[^a-z0-9]+'), '-'); +} diff --git a/lib/services/metadata/apis/localstorage.dart b/lib/services/metadata/apis/localstorage.dart new file mode 100644 index 00000000..3c354d69 --- /dev/null +++ b/lib/services/metadata/apis/localstorage.dart @@ -0,0 +1,60 @@ +import 'package:flutter_js/flutter_js.dart'; +import 'package:shared_preferences/shared_preferences.dart'; + +class PluginLocalStorageApi { + final JavascriptRuntime runtime; + final SharedPreferences sharedPreferences; + + final String pluginName; + + PluginLocalStorageApi({ + required this.runtime, + required this.sharedPreferences, + required this.pluginName, + }) { + runtime.onMessage("LocalStorage.getItem", (args) { + final key = args[0]; + 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; + 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 { + await sharedPreferences.setString("plugin.$pluginName.$key", value); + } + + String? getItem(String key) { + return sharedPreferences.getString("plugin.$pluginName.$key"); + } + + void removeItem(String key) async { + await sharedPreferences.remove("plugin.$pluginName.$key"); + } + + void clear() async { + final keys = sharedPreferences.getKeys(); + for (String key in keys) { + if (key.startsWith("plugin.$pluginName.")) { + await sharedPreferences.remove(key); + } + } + } +} diff --git a/lib/services/metadata/metadata.dart b/lib/services/metadata/metadata.dart index 611f4720..1188f342 100644 --- a/lib/services/metadata/metadata.dart +++ b/lib/services/metadata/metadata.dart @@ -4,8 +4,10 @@ import 'dart:convert'; import 'package:flutter_js/extensions/fetch.dart'; import 'package:flutter_js/extensions/xhr.dart'; import 'package:flutter_js/flutter_js.dart'; +import 'package:shared_preferences/shared_preferences.dart'; import 'package:spotube/models/metadata/metadata.dart'; import 'package:spotube/services/logger/logger.dart'; +import 'package:spotube/services/metadata/apis/localstorage.dart'; const defaultMetadataLimit = "20"; @@ -13,10 +15,12 @@ const defaultMetadataLimit = "20"; /// objects e.g. SpotubeTrack, SpotubePlaylist, etc. class MetadataApiSignature { final JavascriptRuntime runtime; + final PluginLocalStorageApi localStorageApi; - MetadataApiSignature._(this.runtime); + MetadataApiSignature._(this.runtime, this.localStorageApi); - static Future init(String libraryCode) async { + static Future init( + String libraryCode, PluginConfiguration config) async { final runtime = getJavascriptRuntime(xhr: true).enableXhr(); runtime.enableHandlePromises(); await runtime.enableFetch(); @@ -41,7 +45,17 @@ class MetadataApiSignature { ); } - return MetadataApiSignature._(runtime); + // Create all the PluginAPIs after library code is evaluated + final localStorageApi = PluginLocalStorageApi( + runtime: runtime, + sharedPreferences: await SharedPreferences.getInstance(), + pluginName: config.slug, + ); + + return MetadataApiSignature._( + runtime, + localStorageApi, + ); } void dispose() {