From 7309e900bc1540f719520e931d8daa21ee03f6f1 Mon Sep 17 00:00:00 2001 From: Kingkor Roy Tirtho Date: Tue, 22 Jul 2025 00:11:20 +0600 Subject: [PATCH] feat(metadata): add plugin update checker and dialog for available updates --- lib/main.dart | 2 + .../metadata_plugins/installed_plugin.dart | 46 +++++++- .../plugin_update_available_dialog.dart | 99 ++++++++++++++++ .../root/use_global_subscriptions.dart | 19 +++ .../audio_player/audio_player_streams.dart | 6 +- lib/provider/metadata_plugin/core/user.dart | 5 +- .../metadata_plugin_provider.dart | 111 ++++++++++++------ .../updater/update_checker.dart | 17 +++ lib/services/metadata/errors/exceptions.dart | 73 ++++++++++++ lib/services/metadata/metadata.dart | 6 + pubspec.lock | 6 +- pubspec.yaml | 1 + 12 files changed, 343 insertions(+), 48 deletions(-) create mode 100644 lib/modules/metadata_plugins/plugin_update_available_dialog.dart create mode 100644 lib/provider/metadata_plugin/updater/update_checker.dart create mode 100644 lib/services/metadata/errors/exceptions.dart diff --git a/lib/main.dart b/lib/main.dart index d575eda1..8f02371d 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -32,6 +32,7 @@ import 'package:spotube/provider/audio_player/audio_player_streams.dart'; import 'package:spotube/provider/database/database.dart'; import 'package:spotube/provider/glance/glance.dart'; import 'package:spotube/provider/metadata_plugin/metadata_plugin_provider.dart'; +import 'package:spotube/provider/metadata_plugin/updater/update_checker.dart'; import 'package:spotube/provider/server/bonsoir.dart'; import 'package:spotube/provider/server/server.dart'; import 'package:spotube/provider/tray_manager/tray_manager.dart'; @@ -153,6 +154,7 @@ class Spotube extends HookConsumerWidget { ref.listen(metadataPluginProvider, (_, __) {}); ref.listen(serverProvider, (_, __) {}); ref.listen(trayManagerProvider, (_, __) {}); + ref.listen(metadataPluginUpdateCheckerProvider, (_, __) {}); useFixWindowStretching(); useDisableBatteryOptimizations(); diff --git a/lib/modules/metadata_plugins/installed_plugin.dart b/lib/modules/metadata_plugins/installed_plugin.dart index 7fa21306..f21d87a2 100644 --- a/lib/modules/metadata_plugins/installed_plugin.dart +++ b/lib/modules/metadata_plugins/installed_plugin.dart @@ -3,8 +3,10 @@ import 'package:shadcn_flutter/shadcn_flutter.dart'; import 'package:shadcn_flutter/shadcn_flutter_extension.dart'; import 'package:spotube/collections/spotube_icons.dart'; import 'package:spotube/models/metadata/metadata.dart'; +import 'package:spotube/modules/metadata_plugins/plugin_update_available_dialog.dart'; import 'package:spotube/provider/metadata_plugin/core/auth.dart'; import 'package:spotube/provider/metadata_plugin/metadata_plugin_provider.dart'; +import 'package:spotube/provider/metadata_plugin/updater/update_checker.dart'; import 'package:url_launcher/url_launcher.dart'; class MetadataInstalledPluginItem extends HookConsumerWidget { @@ -23,6 +25,9 @@ class MetadataInstalledPluginItem extends HookConsumerWidget { final pluginsNotifier = ref.watch(metadataPluginsProvider.notifier); final requiresAuth = isDefault && plugin.abilities.contains(PluginAbilities.authentication); + final updateAvailable = + isDefault ? ref.watch(metadataPluginUpdateCheckerProvider) : null; + final hasUpdate = isDefault && updateAvailable?.asData?.value != null; return Card( child: Column( @@ -105,19 +110,48 @@ class MetadataInstalledPluginItem extends HookConsumerWidget { ); }, ), - if (plugin.abilities.contains(PluginAbilities.authentication) && - isDefault) + if (requiresAuth || hasUpdate) Container( decoration: BoxDecoration( color: context.theme.colorScheme.secondary, borderRadius: BorderRadius.circular(8), ), padding: const EdgeInsets.all(12), - child: const Row( - spacing: 8, + child: Column( + spacing: 12, children: [ - Icon(SpotubeIcons.warning, color: Colors.yellow), - Text("Plugin requires authentication"), + if (requiresAuth) + const Row( + spacing: 8, + children: [ + Icon(SpotubeIcons.warning, color: Colors.yellow), + Text("Plugin requires authentication"), + ], + ), + if (hasUpdate) + SizedBox( + width: double.infinity, + child: Basic( + leading: const Icon(SpotubeIcons.update), + title: const Text("Update available"), + subtitle: Text( + updateAvailable!.asData!.value!.version, + ), + trailing: Button.primary( + onPressed: () { + showDialog( + context: context, + builder: (context) => + MetadataPluginUpdateAvailableDialog( + plugin: plugin, + update: updateAvailable.asData!.value!, + ), + ); + }, + child: const Text("Update"), + ), + ), + ) ], ), ), diff --git a/lib/modules/metadata_plugins/plugin_update_available_dialog.dart b/lib/modules/metadata_plugins/plugin_update_available_dialog.dart new file mode 100644 index 00000000..4bd6c16e --- /dev/null +++ b/lib/modules/metadata_plugins/plugin_update_available_dialog.dart @@ -0,0 +1,99 @@ +import 'package:flutter_hooks/flutter_hooks.dart'; +import 'package:flutter_markdown_plus/flutter_markdown_plus.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:shadcn_flutter/shadcn_flutter.dart'; +import 'package:spotube/collections/spotube_icons.dart'; +import 'package:spotube/models/metadata/metadata.dart'; +import 'package:spotube/provider/metadata_plugin/metadata_plugin_provider.dart'; +import 'package:url_launcher/url_launcher_string.dart'; + +class MetadataPluginUpdateAvailableDialog extends HookConsumerWidget { + final PluginConfiguration plugin; + final PluginUpdateAvailable update; + const MetadataPluginUpdateAvailableDialog({ + super.key, + required this.plugin, + required this.update, + }); + + @override + Widget build(BuildContext context, ref) { + final isUpdating = useState(false); + + final showErrorSnackbar = useCallback( + (BuildContext context, String message) { + showToast( + context: context, + builder: (context, overlay) { + return SurfaceCard( + child: Basic( + leading: const Icon(SpotubeIcons.error, color: Colors.red), + title: Text(message), + leadingAlignment: Alignment.center, + trailing: IconButton.ghost( + size: ButtonSize.small, + icon: const Icon(SpotubeIcons.close), + onPressed: () { + overlay.close(); + }, + ), + ), + ); + }); + }, + [], + ); + + return AlertDialog( + title: const Text('Plugin update available'), + content: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + spacing: 8, + children: [ + Text('${plugin.name} (${update.version}) available.'), + if (update.changelog != null && update.changelog!.isNotEmpty) + MarkdownBody( + data: '### Changelog: \n\n${update.changelog}', + onTapLink: (text, href, title) { + if (href != null) { + launchUrlString(href); + } + }, + ), + ], + ), + actions: [ + SecondaryButton( + onPressed: () { + Navigator.of(context).pop(); + }, + child: const Text('Dismiss'), + ), + PrimaryButton( + enabled: !isUpdating.value, + onPressed: () async { + isUpdating.value = true; + try { + await ref + .read(metadataPluginsProvider.notifier) + .updatePlugin(plugin, update); + if (context.mounted) { + Navigator.of(context).pop(); + } + } catch (e) { + if (context.mounted) { + showErrorSnackbar(context, e.toString()); + } + } finally { + if (context.mounted) { + isUpdating.value = false; + } + } + }, + child: const Text('Update'), + ), + ], + ); + } +} diff --git a/lib/modules/root/use_global_subscriptions.dart b/lib/modules/root/use_global_subscriptions.dart index e0e4dae7..68f70b5a 100644 --- a/lib/modules/root/use_global_subscriptions.dart +++ b/lib/modules/root/use_global_subscriptions.dart @@ -5,6 +5,9 @@ import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:shadcn_flutter/shadcn_flutter.dart'; import 'package:spotube/collections/spotube_icons.dart'; import 'package:spotube/extensions/context.dart'; +import 'package:spotube/modules/metadata_plugins/plugin_update_available_dialog.dart'; +import 'package:spotube/provider/metadata_plugin/metadata_plugin_provider.dart'; +import 'package:spotube/provider/metadata_plugin/updater/update_checker.dart'; import 'package:spotube/provider/server/routes/connect.dart'; import 'package:spotube/services/audio_player/audio_player.dart'; import 'package:spotube/services/connectivity_adapter.dart'; @@ -18,6 +21,22 @@ void useGlobalSubscriptions(WidgetRef ref) { useEffect(() { WidgetsBinding.instance.addPostFrameCallback((_) async { ServiceUtils.checkForUpdates(context, ref); + + final pluginUpdate = + await ref.read(metadataPluginUpdateCheckerProvider.future); + + if (pluginUpdate != null) { + final pluginConfig = await ref.read(metadataPluginsProvider.future); + if (context.mounted) { + showDialog( + context: context, + builder: (context) => MetadataPluginUpdateAvailableDialog( + plugin: pluginConfig.defaultPluginConfig!, + update: pluginUpdate, + ), + ); + } + } }); StreamSubscription? audioPlayerSubscription; diff --git a/lib/provider/audio_player/audio_player_streams.dart b/lib/provider/audio_player/audio_player_streams.dart index 5b9731c5..49568b21 100644 --- a/lib/provider/audio_player/audio_player_streams.dart +++ b/lib/provider/audio_player/audio_player_streams.dart @@ -135,9 +135,11 @@ class AudioPlayerStreamListeners { return; } final nextTrack = audioPlayerState.tracks - .elementAt(audioPlayerState.currentIndex + 1); + .elementAtOrNull(audioPlayerState.currentIndex + 1); - if (lastTrack == nextTrack.id || nextTrack is SpotubeLocalTrackObject) { + if (nextTrack == null || + lastTrack == nextTrack.id || + nextTrack is SpotubeLocalTrackObject) { return; } diff --git a/lib/provider/metadata_plugin/core/user.dart b/lib/provider/metadata_plugin/core/user.dart index 4acfb8f7..3ad46d63 100644 --- a/lib/provider/metadata_plugin/core/user.dart +++ b/lib/provider/metadata_plugin/core/user.dart @@ -6,9 +6,10 @@ import 'package:spotube/provider/metadata_plugin/metadata_plugin_provider.dart'; final metadataPluginUserProvider = FutureProvider( (ref) async { final metadataPlugin = await ref.watch(metadataPluginProvider.future); - ref.watch(metadataPluginAuthenticatedProvider); + final authenticated = + await ref.watch(metadataPluginAuthenticatedProvider.future); - if (metadataPlugin == null) { + if (!authenticated || metadataPlugin == null) { return null; } return metadataPlugin.user.me(); diff --git a/lib/provider/metadata_plugin/metadata_plugin_provider.dart b/lib/provider/metadata_plugin/metadata_plugin_provider.dart index a47d052b..5eef608e 100644 --- a/lib/provider/metadata_plugin/metadata_plugin_provider.dart +++ b/lib/provider/metadata_plugin/metadata_plugin_provider.dart @@ -11,10 +11,13 @@ import 'package:spotube/models/database/database.dart'; import 'package:spotube/models/metadata/metadata.dart'; import 'package:spotube/provider/database/database.dart'; import 'package:spotube/services/dio/dio.dart'; +import 'package:spotube/services/logger/logger.dart'; +import 'package:spotube/services/metadata/errors/exceptions.dart'; import 'package:spotube/services/metadata/metadata.dart'; import 'package:spotube/utils/service_utils.dart'; import 'package:uuid/uuid.dart'; import 'package:archive/archive.dart'; +import 'package:pub_semver/pub_semver.dart'; final allowedDomainsRegex = RegExp( r"^(https?:\/\/)?(www\.)?(github\.com|codeberg\.org)\/.+", @@ -166,43 +169,56 @@ class MetadataPluginNotifier extends AsyncNotifier { } Future _getPluginDownloadUrl(Uri uri) async { - print("Getting plugin download URL from: $uri"); + AppLogger.log.i("Getting plugin download URL from: $uri"); final res = await globalDio.getUri( uri, options: Options(responseType: ResponseType.json), ); if (res.statusCode != 200) { - throw Exception("Failed to get releases"); + throw MetadataPluginException.failedToGetRelease(); } final releases = res.data as List; if (releases.isEmpty) { - throw Exception("No releases found"); + throw MetadataPluginException.noReleasesFound(); } final latestRelease = releases.first; final downloadUrl = (latestRelease["assets"] as List).firstWhere( (asset) => (asset["name"] as String).endsWith(".smplug"), )["browser_download_url"]; if (downloadUrl == null) { - throw Exception("No download URL found"); + throw MetadataPluginException.assetUrlNotFound(); } return downloadUrl; } - Future _getPluginDir() async => Directory( + /// Root directory where all metadata plugins are stored. + Future _getPluginRootDir() async => Directory( join( (await getApplicationCacheDirectory()).path, "metadata-plugins", ), ); + /// Directory where the plugin will be extracted. + /// This is a unique directory for each plugin version. + /// It is used to avoid conflicts when multiple versions of the same plugin are installed + Future _getPluginExtractionDir(PluginConfiguration plugin) async { + final pluginDir = await _getPluginRootDir(); + final pluginExtractionDirPath = join( + pluginDir.path, + "${ServiceUtils.sanitizeFilename(plugin.name)}-${plugin.version}", + ); + return Directory(pluginExtractionDirPath); + } + Future extractPluginArchive(List bytes) async { final archive = ZipDecoder().decodeBytes(bytes); final pluginJson = archive .firstWhereOrNull((file) => file.isFile && file.name == "plugin.json"); if (pluginJson == null) { - throw Exception("No plugin.json found"); + throw MetadataPluginException.pluginConfigJsonNotFound(); } final pluginConfig = PluginConfiguration.fromJson( jsonDecode( @@ -210,20 +226,17 @@ class MetadataPluginNotifier extends AsyncNotifier { ) as Map, ); - final pluginDir = await _getPluginDir(); + final pluginDir = await _getPluginRootDir(); await pluginDir.create(recursive: true); - final pluginExtractionDirPath = join( - pluginDir.path, - ServiceUtils.sanitizeFilename(pluginConfig.name), - ); + final pluginExtractionDir = await _getPluginExtractionDir(pluginConfig); for (final file in archive) { if (file.isFile) { final filename = file.name; final data = file.content as List; final extractedFile = File(join( - pluginExtractionDirPath, + pluginExtractionDir.path, filename, )); await extractedFile.create(recursive: true); @@ -251,12 +264,12 @@ class MetadataPluginNotifier extends AsyncNotifier { final uri = _getCodebergeReleasesUrl(url); pluginDownloadUrl = await _getPluginDownloadUrl(uri); } else { - throw Exception("Unsupported website"); + throw MetadataPluginException.unsupportedPluginDownloadWebsite(); } } // Now let's download, extract and cache the plugin - final pluginDir = await _getPluginDir(); + final pluginDir = await _getPluginRootDir(); await pluginDir.create(recursive: true); final tempPluginName = "${const Uuid().v4()}.smplug"; @@ -273,13 +286,32 @@ class MetadataPluginNotifier extends AsyncNotifier { ); if ((pluginRes.statusCode ?? 500) > 299) { - throw Exception("Failed to download plugin"); + throw MetadataPluginException.pluginDownloadFailed(); } return await extractPluginArchive(await pluginFile.readAsBytes()); } + bool validatePluginApiCompatibility(PluginConfiguration plugin) { + final configPluginApiVersion = Version.parse(plugin.pluginApiVersion); + final appPluginApiVersion = MetadataPlugin.pluginApiVersion; + + // Plugin API's major version must match the app's major version + if (configPluginApiVersion.major != appPluginApiVersion.major) { + return false; + } + return configPluginApiVersion >= appPluginApiVersion; + } + + void _assertPluginApiCompatibility(PluginConfiguration plugin) { + if (!validatePluginApiCompatibility(plugin)) { + throw MetadataPluginException.pluginApiVersionMismatch(); + } + } + Future addPlugin(PluginConfiguration plugin) async { + _assertPluginApiCompatibility(plugin); + final pluginRes = await (database.metadataPluginsTable.select() ..where( (tbl) => tbl.name.equals(plugin.name), @@ -288,7 +320,7 @@ class MetadataPluginNotifier extends AsyncNotifier { .get(); if (pluginRes.isNotEmpty) { - throw Exception("Plugin already exists"); + throw MetadataPluginException.duplicatePlugin(); } await database.metadataPluginsTable.insertOne( @@ -307,12 +339,8 @@ class MetadataPluginNotifier extends AsyncNotifier { } Future removePlugin(PluginConfiguration plugin) async { - final pluginDir = await _getPluginDir(); - final pluginExtractionDirPath = join( - pluginDir.path, - ServiceUtils.sanitizeFilename(plugin.name), - ); - final pluginExtractionDir = Directory(pluginExtractionDirPath); + final pluginExtractionDir = await _getPluginExtractionDir(plugin); + if (pluginExtractionDir.existsSync()) { await pluginExtractionDir.delete(recursive: true); } @@ -320,6 +348,27 @@ class MetadataPluginNotifier extends AsyncNotifier { .deleteWhere((tbl) => tbl.name.equals(plugin.name)); } + Future updatePlugin( + PluginConfiguration plugin, + PluginUpdateAvailable update, + ) async { + final isDefault = plugin == state.valueOrNull?.defaultPluginConfig; + final pluginUpdatedConfig = + await downloadAndCachePlugin(update.downloadUrl); + + if (pluginUpdatedConfig.name != plugin.name) { + throw MetadataPluginException.invalidPluginConfiguration(); + } + _assertPluginApiCompatibility(pluginUpdatedConfig); + + await removePlugin(plugin); + await addPlugin(pluginUpdatedConfig); + + if (isDefault) { + await setDefaultPlugin(pluginUpdatedConfig); + } + } + Future setDefaultPlugin(PluginConfiguration plugin) async { await (database.metadataPluginsTable.update() ..where((tbl) => tbl.name.equals(plugin.name))) @@ -329,29 +378,21 @@ class MetadataPluginNotifier extends AsyncNotifier { } Future getPluginByteCode(PluginConfiguration plugin) async { - final pluginDir = await _getPluginDir(); - final pluginExtractionDirPath = join( - pluginDir.path, - ServiceUtils.sanitizeFilename(plugin.name), - ); + final pluginExtractionDirPath = await _getPluginExtractionDir(plugin); - final libraryFile = File(join(pluginExtractionDirPath, "plugin.out")); + final libraryFile = File(join(pluginExtractionDirPath.path, "plugin.out")); if (!libraryFile.existsSync()) { - throw Exception("No plugin.out (Bytecode) file found"); + throw MetadataPluginException.pluginByteCodeFileNotFound(); } return await libraryFile.readAsBytes(); } Future getLogoPath(PluginConfiguration plugin) async { - final pluginDir = await _getPluginDir(); - final pluginExtractionDirPath = join( - pluginDir.path, - ServiceUtils.sanitizeFilename(plugin.name), - ); + final pluginExtractionDirPath = await _getPluginExtractionDir(plugin); - final logoFile = File(join(pluginExtractionDirPath, "logo.png")); + final logoFile = File(join(pluginExtractionDirPath.path, "logo.png")); if (!logoFile.existsSync()) { return null; diff --git a/lib/provider/metadata_plugin/updater/update_checker.dart b/lib/provider/metadata_plugin/updater/update_checker.dart new file mode 100644 index 00000000..dc906936 --- /dev/null +++ b/lib/provider/metadata_plugin/updater/update_checker.dart @@ -0,0 +1,17 @@ +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:spotube/models/metadata/metadata.dart'; +import 'package:spotube/provider/metadata_plugin/metadata_plugin_provider.dart'; + +final metadataPluginUpdateCheckerProvider = + FutureProvider((ref) async { + final metadataPluginConfigs = await ref.watch(metadataPluginsProvider.future); + final metadataPlugin = await ref.watch(metadataPluginProvider.future); + + if (metadataPlugin == null || + metadataPluginConfigs.defaultPluginConfig == null) { + return null; + } + + return metadataPlugin.updater + .check(metadataPluginConfigs.defaultPluginConfig!); +}); diff --git a/lib/services/metadata/errors/exceptions.dart b/lib/services/metadata/errors/exceptions.dart new file mode 100644 index 00000000..be460745 --- /dev/null +++ b/lib/services/metadata/errors/exceptions.dart @@ -0,0 +1,73 @@ +enum MetadataPluginErrorCode { + pluginApiVersionMismatch, + invalidPluginConfiguration, + failedToGetReleaseInfo, + noReleasesFound, + assetUrlNotFound, + pluginConfigJsonNotFound, + unsupportedPluginDownloadWebsite, + pluginDownloadFailed, + duplicatePlugin, + pluginByteCodeFileNotFound, +} + +class MetadataPluginException implements Exception { + final String message; + final MetadataPluginErrorCode errorCode; + + MetadataPluginException._(this.message, {required this.errorCode}); + MetadataPluginException.pluginApiVersionMismatch() + : this._( + 'Plugin API version mismatch', + errorCode: MetadataPluginErrorCode.pluginApiVersionMismatch, + ); + MetadataPluginException.invalidPluginConfiguration() + : this._( + 'Invalid plugin configuration', + errorCode: MetadataPluginErrorCode.invalidPluginConfiguration, + ); + MetadataPluginException.failedToGetRelease() + : this._( + 'Failed to get release information', + errorCode: MetadataPluginErrorCode.failedToGetReleaseInfo, + ); + MetadataPluginException.noReleasesFound() + : this._( + 'No releases found for the plugin', + errorCode: MetadataPluginErrorCode.noReleasesFound, + ); + + MetadataPluginException.assetUrlNotFound() + : this._( + 'No asset URL found for the plugin release', + errorCode: MetadataPluginErrorCode.assetUrlNotFound, + ); + MetadataPluginException.pluginConfigJsonNotFound() + : this._( + 'Plugin configuration JSON, plugin.json file not found', + errorCode: MetadataPluginErrorCode.pluginConfigJsonNotFound, + ); + MetadataPluginException.unsupportedPluginDownloadWebsite() + : this._( + 'Unsupported plugin download website. Please use GitHub or Codeberg.', + errorCode: MetadataPluginErrorCode.unsupportedPluginDownloadWebsite, + ); + MetadataPluginException.pluginDownloadFailed() + : this._( + 'Failed to download the plugin. Please check your internet connection or try again later.', + errorCode: MetadataPluginErrorCode.pluginDownloadFailed, + ); + MetadataPluginException.duplicatePlugin() + : this._( + 'Same plugin already exists with the same name and version.', + errorCode: MetadataPluginErrorCode.duplicatePlugin, + ); + MetadataPluginException.pluginByteCodeFileNotFound() + : this._( + 'Plugin byte code file, plugin.out not found. Please ensure the plugin is correctly packaged.', + errorCode: MetadataPluginErrorCode.pluginByteCodeFileNotFound, + ); + + @override + String toString() => 'MetadataPluginException: $message'; +} diff --git a/lib/services/metadata/metadata.dart b/lib/services/metadata/metadata.dart index 14132cf3..8916d8d2 100644 --- a/lib/services/metadata/metadata.dart +++ b/lib/services/metadata/metadata.dart @@ -5,6 +5,7 @@ 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:pub_semver/pub_semver.dart'; import 'package:shadcn_flutter/shadcn_flutter.dart'; import 'package:shared_preferences/shared_preferences.dart'; import 'package:spotube/collections/routes.dart'; @@ -19,11 +20,14 @@ import 'package:spotube/services/metadata/endpoints/browse.dart'; import 'package:spotube/services/metadata/endpoints/playlist.dart'; import 'package:spotube/services/metadata/endpoints/search.dart'; import 'package:spotube/services/metadata/endpoints/track.dart'; +import 'package:spotube/services/metadata/endpoints/updater.dart'; import 'package:spotube/services/metadata/endpoints/user.dart'; const defaultMetadataLimit = "20"; class MetadataPlugin { + static final pluginApiVersion = Version.parse("1.0.0"); + static Future create( PluginConfiguration config, Uint8List byteCode, @@ -101,6 +105,7 @@ class MetadataPlugin { late final MetadataPluginPlaylistEndpoint playlist; late final MetadataPluginTrackEndpoint track; late final MetadataPluginUserEndpoint user; + late final MetadataPluginUpdaterEndpoint updater; MetadataPlugin._(this.hetu) { auth = MetadataAuthEndpoint(hetu); @@ -112,5 +117,6 @@ class MetadataPlugin { playlist = MetadataPluginPlaylistEndpoint(hetu); track = MetadataPluginTrackEndpoint(hetu); user = MetadataPluginUserEndpoint(hetu); + updater = MetadataPluginUpdaterEndpoint(hetu); } } diff --git a/pubspec.lock b/pubspec.lock index 2effc2da..f1c65465 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -1988,13 +1988,13 @@ packages: source: hosted version: "3.0.0" pub_semver: - dependency: transitive + dependency: "direct main" description: name: pub_semver - sha256: "7b3cfbf654f3edd0c6298ecd5be782ce997ddf0e00531b9464b55245185bbbbd" + sha256: "5bfcf68ca79ef689f8990d1160781b4bad40a3bd5e5218ad4076ddb7f4081585" url: "https://pub.dev" source: hosted - version: "2.1.5" + version: "2.2.0" pubspec_parse: dependency: "direct dev" description: diff --git a/pubspec.yaml b/pubspec.yaml index 1de34389..ed05a226 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -157,6 +157,7 @@ dependencies: ref: main get_it: ^8.0.3 flutter_markdown_plus: ^1.0.3 + pub_semver: ^2.2.0 dev_dependencies: build_runner: ^2.4.13