mirror of
https://github.com/KRTirtho/spotube.git
synced 2025-09-13 07:55:18 +00:00
feat: add support for automatic plugin repository from github and codeberg
This commit is contained in:
parent
90f9cc28eb
commit
e83a4bb388
@ -3,7 +3,7 @@ import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:spotube/collections/routes.gr.dart';
|
||||
import 'package:spotube/provider/metadata_plugin/auth.dart';
|
||||
import 'package:spotube/provider/metadata_plugin/core/auth.dart';
|
||||
import 'package:spotube/services/kv_store/kv_store.dart';
|
||||
|
||||
final rootNavigatorKey = GlobalKey<NavigatorState>();
|
||||
|
@ -137,4 +137,6 @@ abstract class SpotubeIcons {
|
||||
static const extensions = FeatherIcons.package;
|
||||
static const message = FeatherIcons.send;
|
||||
static const upload = FeatherIcons.uploadCloud;
|
||||
static const plugin = Icons.extension_outlined;
|
||||
static const warning = FeatherIcons.alertTriangle;
|
||||
}
|
||||
|
@ -7,7 +7,7 @@ import 'package:spotube/modules/playlist/playlist_create_dialog.dart';
|
||||
import 'package:spotube/components/image/universal_image.dart';
|
||||
import 'package:spotube/extensions/context.dart';
|
||||
import 'package:spotube/provider/metadata_plugin/library/playlists.dart';
|
||||
import 'package:spotube/provider/metadata_plugin/user.dart';
|
||||
import 'package:spotube/provider/metadata_plugin/core/user.dart';
|
||||
|
||||
class PlaylistAddTrackDialog extends HookConsumerWidget {
|
||||
/// The id of the playlist this dialog was opened from
|
||||
|
@ -5,7 +5,7 @@ import 'package:shadcn_flutter/shadcn_flutter.dart';
|
||||
import 'package:shadcn_flutter/shadcn_flutter_extension.dart';
|
||||
import 'package:spotube/collections/routes.gr.dart';
|
||||
import 'package:spotube/extensions/context.dart';
|
||||
import 'package:spotube/provider/metadata_plugin/auth.dart';
|
||||
import 'package:spotube/provider/metadata_plugin/core/auth.dart';
|
||||
|
||||
import 'package:spotube/utils/platform.dart';
|
||||
|
||||
|
@ -4,9 +4,9 @@ import 'package:shadcn_flutter/shadcn_flutter.dart';
|
||||
import 'package:spotube/components/heart_button/use_track_toggle_like.dart';
|
||||
import 'package:spotube/extensions/context.dart';
|
||||
import 'package:spotube/models/metadata/metadata.dart';
|
||||
import 'package:spotube/provider/metadata_plugin/auth.dart';
|
||||
import 'package:spotube/provider/metadata_plugin/core/auth.dart';
|
||||
import 'package:spotube/provider/metadata_plugin/library/tracks.dart';
|
||||
import 'package:spotube/provider/metadata_plugin/user.dart';
|
||||
import 'package:spotube/provider/metadata_plugin/core/user.dart';
|
||||
|
||||
class HeartButton extends HookConsumerWidget {
|
||||
final bool isLiked;
|
||||
|
@ -1,7 +1,7 @@
|
||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:spotube/provider/metadata_plugin/library/playlists.dart';
|
||||
import 'package:spotube/provider/metadata_plugin/user.dart';
|
||||
import 'package:spotube/provider/metadata_plugin/core/user.dart';
|
||||
|
||||
bool useIsUserPlaylist(WidgetRef ref, String playlistId) {
|
||||
final userPlaylistsQuery = ref.watch(metadataPluginSavedPlaylistsProvider);
|
||||
|
@ -26,11 +26,11 @@ import 'package:spotube/provider/blacklist_provider.dart';
|
||||
import 'package:spotube/provider/download_manager_provider.dart';
|
||||
import 'package:spotube/provider/local_tracks/local_tracks_provider.dart';
|
||||
import 'package:spotube/provider/audio_player/audio_player.dart';
|
||||
import 'package:spotube/provider/metadata_plugin/auth.dart';
|
||||
import 'package:spotube/provider/metadata_plugin/core/auth.dart';
|
||||
import 'package:spotube/provider/metadata_plugin/library/playlists.dart';
|
||||
import 'package:spotube/provider/metadata_plugin/metadata_plugin_provider.dart';
|
||||
import 'package:spotube/provider/metadata_plugin/tracks/playlist.dart';
|
||||
import 'package:spotube/provider/metadata_plugin/user.dart';
|
||||
import 'package:spotube/provider/metadata_plugin/core/user.dart';
|
||||
import 'package:spotube/services/metadata/endpoints/error.dart';
|
||||
|
||||
import 'package:url_launcher/url_launcher_string.dart';
|
||||
|
@ -27,3 +27,4 @@ part 'track.dart';
|
||||
part 'user.dart';
|
||||
|
||||
part 'plugin.dart';
|
||||
part 'repository.dart';
|
||||
|
@ -4815,3 +4815,216 @@ abstract class _PluginConfiguration extends PluginConfiguration {
|
||||
_$$PluginConfigurationImplCopyWith<_$PluginConfigurationImpl> get copyWith =>
|
||||
throw _privateConstructorUsedError;
|
||||
}
|
||||
|
||||
MetadataPluginRepository _$MetadataPluginRepositoryFromJson(
|
||||
Map<String, dynamic> json) {
|
||||
return _MetadataPluginRepository.fromJson(json);
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
mixin _$MetadataPluginRepository {
|
||||
String get name => throw _privateConstructorUsedError;
|
||||
String get owner => throw _privateConstructorUsedError;
|
||||
String get description => throw _privateConstructorUsedError;
|
||||
String get repoUrl => throw _privateConstructorUsedError;
|
||||
|
||||
/// Serializes this MetadataPluginRepository to a JSON map.
|
||||
Map<String, dynamic> toJson() => throw _privateConstructorUsedError;
|
||||
|
||||
/// Create a copy of MetadataPluginRepository
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
$MetadataPluginRepositoryCopyWith<MetadataPluginRepository> get copyWith =>
|
||||
throw _privateConstructorUsedError;
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
abstract class $MetadataPluginRepositoryCopyWith<$Res> {
|
||||
factory $MetadataPluginRepositoryCopyWith(MetadataPluginRepository value,
|
||||
$Res Function(MetadataPluginRepository) then) =
|
||||
_$MetadataPluginRepositoryCopyWithImpl<$Res, MetadataPluginRepository>;
|
||||
@useResult
|
||||
$Res call({String name, String owner, String description, String repoUrl});
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
class _$MetadataPluginRepositoryCopyWithImpl<$Res,
|
||||
$Val extends MetadataPluginRepository>
|
||||
implements $MetadataPluginRepositoryCopyWith<$Res> {
|
||||
_$MetadataPluginRepositoryCopyWithImpl(this._value, this._then);
|
||||
|
||||
// ignore: unused_field
|
||||
final $Val _value;
|
||||
// ignore: unused_field
|
||||
final $Res Function($Val) _then;
|
||||
|
||||
/// Create a copy of MetadataPluginRepository
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@pragma('vm:prefer-inline')
|
||||
@override
|
||||
$Res call({
|
||||
Object? name = null,
|
||||
Object? owner = null,
|
||||
Object? description = null,
|
||||
Object? repoUrl = null,
|
||||
}) {
|
||||
return _then(_value.copyWith(
|
||||
name: null == name
|
||||
? _value.name
|
||||
: name // ignore: cast_nullable_to_non_nullable
|
||||
as String,
|
||||
owner: null == owner
|
||||
? _value.owner
|
||||
: owner // ignore: cast_nullable_to_non_nullable
|
||||
as String,
|
||||
description: null == description
|
||||
? _value.description
|
||||
: description // ignore: cast_nullable_to_non_nullable
|
||||
as String,
|
||||
repoUrl: null == repoUrl
|
||||
? _value.repoUrl
|
||||
: repoUrl // ignore: cast_nullable_to_non_nullable
|
||||
as String,
|
||||
) as $Val);
|
||||
}
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
abstract class _$$MetadataPluginRepositoryImplCopyWith<$Res>
|
||||
implements $MetadataPluginRepositoryCopyWith<$Res> {
|
||||
factory _$$MetadataPluginRepositoryImplCopyWith(
|
||||
_$MetadataPluginRepositoryImpl value,
|
||||
$Res Function(_$MetadataPluginRepositoryImpl) then) =
|
||||
__$$MetadataPluginRepositoryImplCopyWithImpl<$Res>;
|
||||
@override
|
||||
@useResult
|
||||
$Res call({String name, String owner, String description, String repoUrl});
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
class __$$MetadataPluginRepositoryImplCopyWithImpl<$Res>
|
||||
extends _$MetadataPluginRepositoryCopyWithImpl<$Res,
|
||||
_$MetadataPluginRepositoryImpl>
|
||||
implements _$$MetadataPluginRepositoryImplCopyWith<$Res> {
|
||||
__$$MetadataPluginRepositoryImplCopyWithImpl(
|
||||
_$MetadataPluginRepositoryImpl _value,
|
||||
$Res Function(_$MetadataPluginRepositoryImpl) _then)
|
||||
: super(_value, _then);
|
||||
|
||||
/// Create a copy of MetadataPluginRepository
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@pragma('vm:prefer-inline')
|
||||
@override
|
||||
$Res call({
|
||||
Object? name = null,
|
||||
Object? owner = null,
|
||||
Object? description = null,
|
||||
Object? repoUrl = null,
|
||||
}) {
|
||||
return _then(_$MetadataPluginRepositoryImpl(
|
||||
name: null == name
|
||||
? _value.name
|
||||
: name // ignore: cast_nullable_to_non_nullable
|
||||
as String,
|
||||
owner: null == owner
|
||||
? _value.owner
|
||||
: owner // ignore: cast_nullable_to_non_nullable
|
||||
as String,
|
||||
description: null == description
|
||||
? _value.description
|
||||
: description // ignore: cast_nullable_to_non_nullable
|
||||
as String,
|
||||
repoUrl: null == repoUrl
|
||||
? _value.repoUrl
|
||||
: repoUrl // ignore: cast_nullable_to_non_nullable
|
||||
as String,
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
@JsonSerializable()
|
||||
class _$MetadataPluginRepositoryImpl implements _MetadataPluginRepository {
|
||||
_$MetadataPluginRepositoryImpl(
|
||||
{required this.name,
|
||||
required this.owner,
|
||||
required this.description,
|
||||
required this.repoUrl});
|
||||
|
||||
factory _$MetadataPluginRepositoryImpl.fromJson(Map<String, dynamic> json) =>
|
||||
_$$MetadataPluginRepositoryImplFromJson(json);
|
||||
|
||||
@override
|
||||
final String name;
|
||||
@override
|
||||
final String owner;
|
||||
@override
|
||||
final String description;
|
||||
@override
|
||||
final String repoUrl;
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'MetadataPluginRepository(name: $name, owner: $owner, description: $description, repoUrl: $repoUrl)';
|
||||
}
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
return identical(this, other) ||
|
||||
(other.runtimeType == runtimeType &&
|
||||
other is _$MetadataPluginRepositoryImpl &&
|
||||
(identical(other.name, name) || other.name == name) &&
|
||||
(identical(other.owner, owner) || other.owner == owner) &&
|
||||
(identical(other.description, description) ||
|
||||
other.description == description) &&
|
||||
(identical(other.repoUrl, repoUrl) || other.repoUrl == repoUrl));
|
||||
}
|
||||
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@override
|
||||
int get hashCode =>
|
||||
Object.hash(runtimeType, name, owner, description, repoUrl);
|
||||
|
||||
/// Create a copy of MetadataPluginRepository
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@override
|
||||
@pragma('vm:prefer-inline')
|
||||
_$$MetadataPluginRepositoryImplCopyWith<_$MetadataPluginRepositoryImpl>
|
||||
get copyWith => __$$MetadataPluginRepositoryImplCopyWithImpl<
|
||||
_$MetadataPluginRepositoryImpl>(this, _$identity);
|
||||
|
||||
@override
|
||||
Map<String, dynamic> toJson() {
|
||||
return _$$MetadataPluginRepositoryImplToJson(
|
||||
this,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
abstract class _MetadataPluginRepository implements MetadataPluginRepository {
|
||||
factory _MetadataPluginRepository(
|
||||
{required final String name,
|
||||
required final String owner,
|
||||
required final String description,
|
||||
required final String repoUrl}) = _$MetadataPluginRepositoryImpl;
|
||||
|
||||
factory _MetadataPluginRepository.fromJson(Map<String, dynamic> json) =
|
||||
_$MetadataPluginRepositoryImpl.fromJson;
|
||||
|
||||
@override
|
||||
String get name;
|
||||
@override
|
||||
String get owner;
|
||||
@override
|
||||
String get description;
|
||||
@override
|
||||
String get repoUrl;
|
||||
|
||||
/// Create a copy of MetadataPluginRepository
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@override
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
_$$MetadataPluginRepositoryImplCopyWith<_$MetadataPluginRepositoryImpl>
|
||||
get copyWith => throw _privateConstructorUsedError;
|
||||
}
|
||||
|
@ -462,3 +462,21 @@ const _$PluginApisEnumMap = {
|
||||
const _$PluginAbilitiesEnumMap = {
|
||||
PluginAbilities.authentication: 'authentication',
|
||||
};
|
||||
|
||||
_$MetadataPluginRepositoryImpl _$$MetadataPluginRepositoryImplFromJson(
|
||||
Map json) =>
|
||||
_$MetadataPluginRepositoryImpl(
|
||||
name: json['name'] as String,
|
||||
owner: json['owner'] as String,
|
||||
description: json['description'] as String,
|
||||
repoUrl: json['repoUrl'] as String,
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$$MetadataPluginRepositoryImplToJson(
|
||||
_$MetadataPluginRepositoryImpl instance) =>
|
||||
<String, dynamic>{
|
||||
'name': instance.name,
|
||||
'owner': instance.owner,
|
||||
'description': instance.description,
|
||||
'repoUrl': instance.repoUrl,
|
||||
};
|
||||
|
14
lib/models/metadata/repository.dart
Normal file
14
lib/models/metadata/repository.dart
Normal file
@ -0,0 +1,14 @@
|
||||
part of './metadata.dart';
|
||||
|
||||
@freezed
|
||||
class MetadataPluginRepository with _$MetadataPluginRepository {
|
||||
factory MetadataPluginRepository({
|
||||
required String name,
|
||||
required String owner,
|
||||
required String description,
|
||||
required String repoUrl,
|
||||
}) = _MetadataPluginRepository;
|
||||
|
||||
factory MetadataPluginRepository.fromJson(Map<String, dynamic> json) =>
|
||||
_$MetadataPluginRepositoryFromJson(json);
|
||||
}
|
@ -4,7 +4,7 @@ import 'package:spotube/components/horizontal_playbutton_card_view/horizontal_pl
|
||||
import 'package:spotube/extensions/context.dart';
|
||||
import 'package:spotube/models/metadata/metadata.dart';
|
||||
import 'package:spotube/provider/metadata_plugin/album/releases.dart';
|
||||
import 'package:spotube/provider/metadata_plugin/auth.dart';
|
||||
import 'package:spotube/provider/metadata_plugin/core/auth.dart';
|
||||
import 'package:spotube/provider/metadata_plugin/utils/common.dart';
|
||||
|
||||
class HomeNewReleasesSection extends HookConsumerWidget {
|
||||
|
@ -21,7 +21,7 @@ import 'package:spotube/extensions/constrains.dart';
|
||||
import 'package:spotube/extensions/context.dart';
|
||||
import 'package:spotube/modules/root/spotube_navigation_bar.dart';
|
||||
import 'package:spotube/provider/audio_player/audio_player.dart';
|
||||
import 'package:spotube/provider/metadata_plugin/auth.dart';
|
||||
import 'package:spotube/provider/metadata_plugin/core/auth.dart';
|
||||
import 'package:spotube/provider/server/active_track_sources.dart';
|
||||
import 'package:spotube/provider/volume_provider.dart';
|
||||
import 'package:spotube/services/sourced_track/sources/youtube.dart';
|
||||
|
@ -18,7 +18,7 @@ import 'package:spotube/extensions/duration.dart';
|
||||
import 'package:spotube/provider/download_manager_provider.dart';
|
||||
import 'package:spotube/provider/audio_player/audio_player.dart';
|
||||
import 'package:spotube/provider/local_tracks/local_tracks_provider.dart';
|
||||
import 'package:spotube/provider/metadata_plugin/auth.dart';
|
||||
import 'package:spotube/provider/metadata_plugin/core/auth.dart';
|
||||
import 'package:spotube/provider/sleep_timer_provider.dart';
|
||||
|
||||
class PlayerActions extends HookConsumerWidget {
|
||||
|
@ -15,7 +15,7 @@ import 'package:spotube/provider/history/history.dart';
|
||||
import 'package:spotube/provider/audio_player/audio_player.dart';
|
||||
import 'package:spotube/provider/metadata_plugin/library/tracks.dart';
|
||||
import 'package:spotube/provider/metadata_plugin/tracks/playlist.dart';
|
||||
import 'package:spotube/provider/metadata_plugin/user.dart';
|
||||
import 'package:spotube/provider/metadata_plugin/core/user.dart';
|
||||
import 'package:spotube/services/audio_player/audio_player.dart';
|
||||
|
||||
class PlaylistCard extends HookConsumerWidget {
|
||||
|
@ -11,8 +11,8 @@ import 'package:spotube/extensions/context.dart';
|
||||
import 'package:spotube/models/metadata/metadata.dart';
|
||||
import 'package:spotube/modules/connect/connect_device.dart';
|
||||
import 'package:spotube/provider/download_manager_provider.dart';
|
||||
import 'package:spotube/provider/metadata_plugin/auth.dart';
|
||||
import 'package:spotube/provider/metadata_plugin/user.dart';
|
||||
import 'package:spotube/provider/metadata_plugin/core/auth.dart';
|
||||
import 'package:spotube/provider/metadata_plugin/core/user.dart';
|
||||
|
||||
class SidebarFooter extends HookConsumerWidget implements NavigationBarItem {
|
||||
const SidebarFooter({
|
||||
|
@ -12,7 +12,7 @@ import 'package:spotube/models/database/database.dart';
|
||||
import 'package:spotube/models/metadata/metadata.dart';
|
||||
import 'package:spotube/provider/blacklist_provider.dart';
|
||||
import 'package:spotube/provider/metadata_plugin/artist/artist.dart';
|
||||
import 'package:spotube/provider/metadata_plugin/auth.dart';
|
||||
import 'package:spotube/provider/metadata_plugin/core/auth.dart';
|
||||
import 'package:spotube/provider/metadata_plugin/library/artists.dart';
|
||||
import 'package:spotube/utils/primitive_utils.dart';
|
||||
|
||||
|
@ -14,7 +14,7 @@ import 'package:spotube/modules/album/album_card.dart';
|
||||
import 'package:spotube/components/inter_scrollbar/inter_scrollbar.dart';
|
||||
import 'package:spotube/components/fallbacks/anonymous_fallback.dart';
|
||||
import 'package:spotube/extensions/context.dart';
|
||||
import 'package:spotube/provider/metadata_plugin/auth.dart';
|
||||
import 'package:spotube/provider/metadata_plugin/core/auth.dart';
|
||||
import 'package:spotube/provider/metadata_plugin/library/albums.dart';
|
||||
import 'package:auto_route/auto_route.dart';
|
||||
|
||||
|
@ -17,7 +17,7 @@ import 'package:spotube/components/inter_scrollbar/inter_scrollbar.dart';
|
||||
import 'package:spotube/components/waypoint.dart';
|
||||
import 'package:spotube/extensions/constrains.dart';
|
||||
import 'package:spotube/extensions/context.dart';
|
||||
import 'package:spotube/provider/metadata_plugin/auth.dart';
|
||||
import 'package:spotube/provider/metadata_plugin/core/auth.dart';
|
||||
import 'package:spotube/provider/metadata_plugin/library/artists.dart';
|
||||
import 'package:auto_route/auto_route.dart';
|
||||
|
||||
|
@ -15,9 +15,9 @@ import 'package:spotube/components/inter_scrollbar/inter_scrollbar.dart';
|
||||
import 'package:spotube/components/fallbacks/anonymous_fallback.dart';
|
||||
import 'package:spotube/modules/playlist/playlist_card.dart';
|
||||
import 'package:spotube/extensions/context.dart';
|
||||
import 'package:spotube/provider/metadata_plugin/auth.dart';
|
||||
import 'package:spotube/provider/metadata_plugin/core/auth.dart';
|
||||
import 'package:spotube/provider/metadata_plugin/library/playlists.dart';
|
||||
import 'package:spotube/provider/metadata_plugin/user.dart';
|
||||
import 'package:spotube/provider/metadata_plugin/core/user.dart';
|
||||
import 'package:auto_route/auto_route.dart';
|
||||
|
||||
@RoutePage()
|
||||
|
@ -8,7 +8,7 @@ import 'package:spotube/components/image/universal_image.dart';
|
||||
import 'package:spotube/components/titlebar/titlebar.dart';
|
||||
import 'package:spotube/extensions/context.dart';
|
||||
import 'package:spotube/models/metadata/metadata.dart';
|
||||
import 'package:spotube/provider/metadata_plugin/user.dart';
|
||||
import 'package:spotube/provider/metadata_plugin/core/user.dart';
|
||||
import 'package:url_launcher/url_launcher_string.dart';
|
||||
import 'package:auto_route/auto_route.dart';
|
||||
|
||||
|
@ -17,7 +17,7 @@ import 'package:spotube/pages/search/tabs/all.dart';
|
||||
import 'package:spotube/pages/search/tabs/artists.dart';
|
||||
import 'package:spotube/pages/search/tabs/playlists.dart';
|
||||
import 'package:spotube/pages/search/tabs/tracks.dart';
|
||||
import 'package:spotube/provider/metadata_plugin/auth.dart';
|
||||
import 'package:spotube/provider/metadata_plugin/core/auth.dart';
|
||||
import 'package:spotube/provider/metadata_plugin/search/all.dart';
|
||||
import 'package:spotube/services/kv_store/kv_store.dart';
|
||||
import 'package:auto_route/auto_route.dart';
|
||||
|
@ -4,14 +4,20 @@ import 'package:flutter_hooks/flutter_hooks.dart';
|
||||
import 'package:form_builder_validators/form_builder_validators.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:shadcn_flutter/shadcn_flutter.dart';
|
||||
import 'package:shadcn_flutter/shadcn_flutter_extension.dart';
|
||||
import 'package:skeletonizer/skeletonizer.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/core/auth.dart';
|
||||
import 'package:spotube/provider/metadata_plugin/core/repositories.dart';
|
||||
import 'package:spotube/provider/metadata_plugin/metadata_plugin_provider.dart';
|
||||
import 'package:file_picker/file_picker.dart';
|
||||
import 'package:spotube/provider/metadata_plugin/utils/common.dart';
|
||||
import 'package:spotube/utils/platform.dart';
|
||||
import 'package:url_launcher/url_launcher_string.dart';
|
||||
import 'package:very_good_infinite_list/very_good_infinite_list.dart';
|
||||
|
||||
@RoutePage()
|
||||
class SettingsMetadataProviderPage extends HookConsumerWidget {
|
||||
@ -26,6 +32,11 @@ class SettingsMetadataProviderPage extends HookConsumerWidget {
|
||||
final metadataPlugin = ref.watch(metadataPluginProvider);
|
||||
final isAuthenticated = ref.watch(metadataPluginAuthenticatedProvider);
|
||||
|
||||
final pluginReposSnapshot = ref.watch(metadataPluginRepositoriesProvider);
|
||||
final pluginReposNotifier =
|
||||
ref.watch(metadataPluginRepositoriesProvider.notifier);
|
||||
final pluginRepos = pluginReposSnapshot.asData?.value.items ?? [];
|
||||
|
||||
return Scaffold(
|
||||
headers: const [
|
||||
TitleBar(
|
||||
@ -103,76 +114,203 @@ class SettingsMetadataProviderPage extends HookConsumerWidget {
|
||||
],
|
||||
),
|
||||
),
|
||||
const SliverGap(12),
|
||||
SliverToBoxAdapter(
|
||||
child: Row(
|
||||
children: [
|
||||
const Gap(8),
|
||||
const Text("Installed").h4,
|
||||
const Gap(8),
|
||||
const Expanded(child: Divider()),
|
||||
const Gap(8),
|
||||
],
|
||||
),
|
||||
),
|
||||
const SliverGap(20),
|
||||
SliverList.separated(
|
||||
itemCount: plugins.asData?.value.plugins.length ?? 0,
|
||||
separatorBuilder: (context, index) => const Divider(),
|
||||
separatorBuilder: (context, index) => const Gap(12),
|
||||
itemBuilder: (context, index) {
|
||||
final plugin = plugins.asData!.value.plugins[index];
|
||||
final isDefault = plugins.asData!.value.defaultPlugin == index;
|
||||
final requiresAuth = isDefault &&
|
||||
plugin.abilities.contains(PluginAbilities.authentication);
|
||||
|
||||
return Card(
|
||||
child: Column(
|
||||
spacing: 8,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
mainAxisAlignment: MainAxisAlignment.start,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
spacing: 12,
|
||||
children: [
|
||||
Basic(
|
||||
title: Text(plugin.name),
|
||||
subtitle: Text(plugin.description),
|
||||
trailing: Row(
|
||||
spacing: 8,
|
||||
children: [
|
||||
Button.primary(
|
||||
enabled: !isDefault,
|
||||
onPressed: () async {
|
||||
await pluginsNotifier.setDefaultPlugin(plugin);
|
||||
},
|
||||
child: isDefault
|
||||
? const Text("Default")
|
||||
: const Text("Make default"),
|
||||
),
|
||||
IconButton.destructive(
|
||||
FutureBuilder(
|
||||
future: pluginsNotifier.getLogoPath(plugin),
|
||||
builder: (context, snapshot) {
|
||||
return Basic(
|
||||
leading: snapshot.hasData
|
||||
? Image.file(
|
||||
snapshot.data!,
|
||||
width: 36,
|
||||
height: 36,
|
||||
)
|
||||
: Container(
|
||||
height: 36,
|
||||
width: 36,
|
||||
alignment: Alignment.center,
|
||||
decoration: BoxDecoration(
|
||||
color:
|
||||
context.theme.colorScheme.secondary,
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: const Icon(SpotubeIcons.plugin),
|
||||
),
|
||||
title: Text(plugin.name),
|
||||
subtitle: Text(plugin.description),
|
||||
trailing: IconButton.ghost(
|
||||
onPressed: () async {
|
||||
await pluginsNotifier.removePlugin(plugin);
|
||||
},
|
||||
icon: const Icon(SpotubeIcons.trash),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
if (requiresAuth)
|
||||
Row(
|
||||
children: [
|
||||
const Text("Plugin requires authentication"),
|
||||
const Spacer(),
|
||||
if (isAuthenticated.asData?.value != true)
|
||||
Button.primary(
|
||||
onPressed: () async {
|
||||
await metadataPlugin.asData?.value?.auth
|
||||
.authenticate();
|
||||
},
|
||||
leading: const Icon(SpotubeIcons.login),
|
||||
child: const Text("Login"),
|
||||
)
|
||||
else
|
||||
Button.destructive(
|
||||
onPressed: () async {
|
||||
await metadataPlugin.asData?.value?.auth
|
||||
.logout();
|
||||
},
|
||||
leading: const Icon(SpotubeIcons.logout),
|
||||
child: const Text("Logout"),
|
||||
icon: const Icon(
|
||||
SpotubeIcons.trash,
|
||||
color: Colors.red,
|
||||
),
|
||||
],
|
||||
)
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
if (plugin.abilities
|
||||
.contains(PluginAbilities.authentication) &&
|
||||
isDefault)
|
||||
Container(
|
||||
decoration: BoxDecoration(
|
||||
color: context.theme.colorScheme.secondary,
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
padding: const EdgeInsets.all(12),
|
||||
child: const Row(
|
||||
spacing: 8,
|
||||
children: [
|
||||
Icon(SpotubeIcons.warning, color: Colors.yellow),
|
||||
Text("Plugin requires authentication"),
|
||||
],
|
||||
),
|
||||
),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Button.secondary(
|
||||
enabled: !isDefault,
|
||||
onPressed: () async {
|
||||
await pluginsNotifier.setDefaultPlugin(plugin);
|
||||
},
|
||||
child: isDefault
|
||||
? const Text("Default")
|
||||
: const Text("Set default"),
|
||||
),
|
||||
if (isAuthenticated.asData?.value != true &&
|
||||
requiresAuth &&
|
||||
isDefault)
|
||||
Button.primary(
|
||||
onPressed: () async {
|
||||
await metadataPlugin.asData?.value?.auth
|
||||
.authenticate();
|
||||
},
|
||||
leading: const Icon(SpotubeIcons.login),
|
||||
child: const Text("Login"),
|
||||
)
|
||||
else if (isAuthenticated.asData?.value == true &&
|
||||
requiresAuth &&
|
||||
isDefault)
|
||||
Button.destructive(
|
||||
onPressed: () async {
|
||||
await metadataPlugin.asData?.value?.auth
|
||||
.logout();
|
||||
},
|
||||
leading: const Icon(SpotubeIcons.logout),
|
||||
child: const Text("Logout"),
|
||||
)
|
||||
],
|
||||
)
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
const SliverGap(12),
|
||||
SliverToBoxAdapter(
|
||||
child: Row(
|
||||
children: [
|
||||
const Gap(8),
|
||||
const Text("Available plugins").h4,
|
||||
const Gap(8),
|
||||
const Expanded(child: Divider()),
|
||||
const Gap(8),
|
||||
],
|
||||
),
|
||||
),
|
||||
const SliverGap(12),
|
||||
Skeletonizer.sliver(
|
||||
enabled: pluginReposSnapshot.isLoading,
|
||||
child: SliverInfiniteList(
|
||||
isLoading: pluginReposSnapshot.isLoading &&
|
||||
!pluginReposSnapshot.isLoadingNextPage,
|
||||
itemCount: pluginRepos.length,
|
||||
onFetchData: pluginReposNotifier.fetchMore,
|
||||
itemBuilder: (context, index) {
|
||||
final pluginRepo = pluginRepos[index];
|
||||
final host = Uri.parse(pluginRepo.repoUrl).host;
|
||||
|
||||
return Card(
|
||||
child: Basic(
|
||||
title: Text(pluginRepo.name),
|
||||
subtitle: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
spacing: 8,
|
||||
children: [
|
||||
Text(pluginRepo.description),
|
||||
Row(
|
||||
spacing: 8,
|
||||
children: [
|
||||
if (pluginRepo.owner == "KRTirtho") ...[
|
||||
const PrimaryBadge(
|
||||
leading: Icon(SpotubeIcons.done),
|
||||
child: Text("Official"),
|
||||
),
|
||||
SecondaryBadge(
|
||||
leading: host == "github.com"
|
||||
? const Icon(SpotubeIcons.github)
|
||||
: null,
|
||||
child: Text(host),
|
||||
onPressed: () {
|
||||
launchUrlString(pluginRepo.repoUrl);
|
||||
},
|
||||
),
|
||||
] else ...[
|
||||
Text("Author: ${pluginRepo.owner}"),
|
||||
const DestructiveBadge(
|
||||
leading: Icon(SpotubeIcons.warning),
|
||||
child: Text("Third-party"),
|
||||
)
|
||||
]
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
trailing: Button.primary(
|
||||
onPressed: () async {
|
||||
final pluginConfig = await pluginsNotifier
|
||||
.downloadAndCachePlugin(pluginRepo.repoUrl);
|
||||
|
||||
await pluginsNotifier.addPlugin(pluginConfig);
|
||||
},
|
||||
leading: const Icon(SpotubeIcons.add),
|
||||
child: const Text("Install"),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
89
lib/provider/metadata_plugin/core/repositories.dart
Normal file
89
lib/provider/metadata_plugin/core/repositories.dart
Normal file
@ -0,0 +1,89 @@
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:spotube/models/metadata/metadata.dart';
|
||||
import 'package:spotube/provider/metadata_plugin/utils/paginated.dart';
|
||||
import 'package:spotube/services/dio/dio.dart';
|
||||
|
||||
class MetadataPluginRepositoriesNotifier
|
||||
extends PaginatedAsyncNotifier<MetadataPluginRepository> {
|
||||
MetadataPluginRepositoriesNotifier() : super();
|
||||
|
||||
Map<String, bool> _hasMore = {};
|
||||
|
||||
@override
|
||||
fetch(int offset, int limit) async {
|
||||
final gitubSearch = globalDio.get(
|
||||
"https://api.github.com/search/repositories",
|
||||
queryParameters: {
|
||||
"q": "topic:spotube-plugin",
|
||||
"sort": "stars",
|
||||
"order": "desc",
|
||||
"page": offset,
|
||||
"per_page": limit,
|
||||
},
|
||||
);
|
||||
|
||||
final codebergSearch = globalDio.get(
|
||||
"https://codeberg.org/api/v1/repos/search",
|
||||
queryParameters: {
|
||||
"q": "spotube-plugin",
|
||||
"topic": "true",
|
||||
"sort": "stars",
|
||||
"order": "desc",
|
||||
"page": offset,
|
||||
"limit": limit,
|
||||
},
|
||||
);
|
||||
|
||||
final responses = await Future.wait([
|
||||
if (_hasMore["github.com"] ?? true) gitubSearch,
|
||||
if (_hasMore["codeberg.org"] ?? true) codebergSearch,
|
||||
]);
|
||||
|
||||
final repos = responses
|
||||
.expand(
|
||||
(response) => response.data["data"] ?? response.data["items"] ?? [],
|
||||
)
|
||||
.map((repo) {
|
||||
return MetadataPluginRepository(
|
||||
name: repo["name"] ?? "",
|
||||
owner: repo["owner"]["login"] ?? "",
|
||||
description: repo["description"] ?? "",
|
||||
repoUrl: repo["html_url"] ?? "",
|
||||
);
|
||||
}).toList();
|
||||
|
||||
final hasMore = responses.any((response) {
|
||||
final items =
|
||||
(response.data["data"] ?? response.data["items"] ?? []) as List;
|
||||
_hasMore[response.requestOptions.uri.host] =
|
||||
items.length >= limit && items.isNotEmpty;
|
||||
|
||||
return _hasMore[response.requestOptions.uri.host] ?? false;
|
||||
});
|
||||
|
||||
return SpotubePaginationResponseObject(
|
||||
items: repos,
|
||||
total: responses.fold<int>(
|
||||
0,
|
||||
(previousValue, response) => previousValue +
|
||||
(response.data["total_count"] ??
|
||||
int.tryParse(response.headers["x-total-count"]?[0] ?? "") ??
|
||||
0) as int,
|
||||
),
|
||||
hasMore: hasMore,
|
||||
nextOffset: hasMore ? offset + 1 : null,
|
||||
limit: limit,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
build() async {
|
||||
return await fetch(0, 10);
|
||||
}
|
||||
}
|
||||
|
||||
final metadataPluginRepositoriesProvider = AsyncNotifierProvider<
|
||||
MetadataPluginRepositoriesNotifier,
|
||||
SpotubePaginationResponseObject<MetadataPluginRepository>>(
|
||||
() => MetadataPluginRepositoriesNotifier(),
|
||||
);
|
@ -1,6 +1,6 @@
|
||||
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/core/auth.dart';
|
||||
import 'package:spotube/provider/metadata_plugin/metadata_plugin_provider.dart';
|
||||
|
||||
final metadataPluginUserProvider = FutureProvider<SpotubeUserObject?>(
|
@ -130,7 +130,11 @@ class MetadataPluginNotifier extends AsyncNotifier<MetadataPluginState> {
|
||||
final parsedUri = Uri.parse(repoUrl);
|
||||
final uri = parsedUri.replace(
|
||||
host: "api.github.com",
|
||||
path: "/repos/${parsedUri.path}/releases",
|
||||
pathSegments: [
|
||||
"repos",
|
||||
...parsedUri.pathSegments,
|
||||
"releases",
|
||||
],
|
||||
queryParameters: {
|
||||
"per_page": "1",
|
||||
"page": "1",
|
||||
@ -143,7 +147,13 @@ class MetadataPluginNotifier extends AsyncNotifier<MetadataPluginState> {
|
||||
Uri _getCodebergeReleasesUrl(String repoUrl) {
|
||||
final parsedUri = Uri.parse(repoUrl);
|
||||
final uri = parsedUri.replace(
|
||||
path: "/api/v1/repos/${parsedUri.path}/releases",
|
||||
pathSegments: [
|
||||
"api",
|
||||
"v1",
|
||||
"repos",
|
||||
...parsedUri.pathSegments,
|
||||
"releases",
|
||||
],
|
||||
queryParameters: {
|
||||
"limit": "1",
|
||||
"page": "1",
|
||||
@ -154,6 +164,7 @@ class MetadataPluginNotifier extends AsyncNotifier<MetadataPluginState> {
|
||||
}
|
||||
|
||||
Future<String> _getPluginDownloadUrl(Uri uri) async {
|
||||
print("Getting plugin download URL from: $uri");
|
||||
final res = await globalDio.getUri(
|
||||
uri,
|
||||
options: Options(responseType: ResponseType.json),
|
||||
@ -227,8 +238,7 @@ class MetadataPluginNotifier extends AsyncNotifier<MetadataPluginState> {
|
||||
Future<PluginConfiguration> downloadAndCachePlugin(String url) async {
|
||||
final res = await globalDio.head(url);
|
||||
final isSupportedWebsite =
|
||||
(res.headers["Content-Type"] as String?)?.startsWith("text/html") ==
|
||||
true &&
|
||||
(res.headers["Content-Type"]?.first)?.startsWith("text/html") == true &&
|
||||
allowedDomainsRegex.hasMatch(url);
|
||||
String pluginDownloadUrl = url;
|
||||
if (isSupportedWebsite) {
|
||||
@ -329,6 +339,22 @@ class MetadataPluginNotifier extends AsyncNotifier<MetadataPluginState> {
|
||||
|
||||
return await libraryFile.readAsBytes();
|
||||
}
|
||||
|
||||
Future<File?> getLogoPath(PluginConfiguration plugin) async {
|
||||
final pluginDir = await _getPluginDir();
|
||||
final pluginExtractionDirPath = join(
|
||||
pluginDir.path,
|
||||
ServiceUtils.sanitizeFilename(plugin.name),
|
||||
);
|
||||
|
||||
final logoFile = File(join(pluginExtractionDirPath, "logo.png"));
|
||||
|
||||
if (!logoFile.existsSync()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return logoFile;
|
||||
}
|
||||
}
|
||||
|
||||
final metadataPluginsProvider =
|
||||
|
@ -2,7 +2,7 @@ import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:spotube/models/metadata/metadata.dart';
|
||||
import 'package:spotube/provider/metadata_plugin/library/playlists.dart';
|
||||
import 'package:spotube/provider/metadata_plugin/metadata_plugin_provider.dart';
|
||||
import 'package:spotube/provider/metadata_plugin/user.dart';
|
||||
import 'package:spotube/provider/metadata_plugin/core/user.dart';
|
||||
import 'package:spotube/provider/metadata_plugin/utils/common.dart';
|
||||
import 'package:spotube/services/metadata/endpoints/error.dart';
|
||||
import 'package:spotube/services/metadata/metadata.dart';
|
||||
|
@ -27,8 +27,7 @@ mixin PaginatedAsyncNotifierMixin<K>
|
||||
state.value!.items.isEmpty ? <K>[] : state.value!.items.cast<K>();
|
||||
final items = newState.items.isEmpty ? <K>[] : newState.items.cast<K>();
|
||||
|
||||
return newState.copyWith(items: <K>[...oldItems, ...items])
|
||||
as SpotubePaginationResponseObject<K>;
|
||||
return newState.copyWith(items: <K>[...oldItems, ...items]);
|
||||
},
|
||||
);
|
||||
}
|
||||
@ -48,8 +47,7 @@ mixin PaginatedAsyncNotifierMixin<K>
|
||||
hasMore = newState.hasMore;
|
||||
final oldItems = state.items.isEmpty ? <K>[] : state.items.cast<K>();
|
||||
final items = newState.items.isEmpty ? <K>[] : newState.items.cast<K>();
|
||||
return newState.copyWith(items: <K>[...oldItems, ...items])
|
||||
as SpotubePaginationResponseObject<K>;
|
||||
return newState.copyWith(items: <K>[...oldItems, ...items]);
|
||||
});
|
||||
}
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user