mirror of
https://github.com/KRTirtho/spotube.git
synced 2025-09-13 16:05:18 +00:00
feat(metadata): add plugin update checker and dialog for available updates
This commit is contained in:
parent
c9556c2ecb
commit
7309e900bc
@ -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();
|
||||
|
@ -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"),
|
||||
),
|
||||
),
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
|
@ -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'),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
@ -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;
|
||||
|
@ -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;
|
||||
}
|
||||
|
||||
|
@ -6,9 +6,10 @@ 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);
|
||||
final authenticated =
|
||||
await ref.watch(metadataPluginAuthenticatedProvider.future);
|
||||
|
||||
if (metadataPlugin == null) {
|
||||
if (!authenticated || metadataPlugin == null) {
|
||||
return null;
|
||||
}
|
||||
return metadataPlugin.user.me();
|
||||
|
@ -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<MetadataPluginState> {
|
||||
}
|
||||
|
||||
Future<String> _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<Directory> _getPluginDir() async => Directory(
|
||||
/// Root directory where all metadata plugins are stored.
|
||||
Future<Directory> _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<Directory> _getPluginExtractionDir(PluginConfiguration plugin) async {
|
||||
final pluginDir = await _getPluginRootDir();
|
||||
final pluginExtractionDirPath = join(
|
||||
pluginDir.path,
|
||||
"${ServiceUtils.sanitizeFilename(plugin.name)}-${plugin.version}",
|
||||
);
|
||||
return Directory(pluginExtractionDirPath);
|
||||
}
|
||||
|
||||
Future<PluginConfiguration> extractPluginArchive(List<int> 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<MetadataPluginState> {
|
||||
) as Map<String, dynamic>,
|
||||
);
|
||||
|
||||
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<int>;
|
||||
final extractedFile = File(join(
|
||||
pluginExtractionDirPath,
|
||||
pluginExtractionDir.path,
|
||||
filename,
|
||||
));
|
||||
await extractedFile.create(recursive: true);
|
||||
@ -251,12 +264,12 @@ class MetadataPluginNotifier extends AsyncNotifier<MetadataPluginState> {
|
||||
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<MetadataPluginState> {
|
||||
);
|
||||
|
||||
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<void> 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<MetadataPluginState> {
|
||||
.get();
|
||||
|
||||
if (pluginRes.isNotEmpty) {
|
||||
throw Exception("Plugin already exists");
|
||||
throw MetadataPluginException.duplicatePlugin();
|
||||
}
|
||||
|
||||
await database.metadataPluginsTable.insertOne(
|
||||
@ -307,12 +339,8 @@ class MetadataPluginNotifier extends AsyncNotifier<MetadataPluginState> {
|
||||
}
|
||||
|
||||
Future<void> 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<MetadataPluginState> {
|
||||
.deleteWhere((tbl) => tbl.name.equals(plugin.name));
|
||||
}
|
||||
|
||||
Future<void> 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<void> setDefaultPlugin(PluginConfiguration plugin) async {
|
||||
await (database.metadataPluginsTable.update()
|
||||
..where((tbl) => tbl.name.equals(plugin.name)))
|
||||
@ -329,29 +378,21 @@ class MetadataPluginNotifier extends AsyncNotifier<MetadataPluginState> {
|
||||
}
|
||||
|
||||
Future<Uint8List> 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<File?> 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;
|
||||
|
17
lib/provider/metadata_plugin/updater/update_checker.dart
Normal file
17
lib/provider/metadata_plugin/updater/update_checker.dart
Normal file
@ -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<PluginUpdateAvailable?>((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!);
|
||||
});
|
73
lib/services/metadata/errors/exceptions.dart
Normal file
73
lib/services/metadata/errors/exceptions.dart
Normal file
@ -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';
|
||||
}
|
@ -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<MetadataPlugin> 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);
|
||||
}
|
||||
}
|
||||
|
@ -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:
|
||||
|
@ -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
|
||||
|
Loading…
Reference in New Issue
Block a user