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:flutter/material.dart';
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:spotube/collections/routes.gr.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';
|
import 'package:spotube/services/kv_store/kv_store.dart';
|
||||||
|
|
||||||
final rootNavigatorKey = GlobalKey<NavigatorState>();
|
final rootNavigatorKey = GlobalKey<NavigatorState>();
|
||||||
|
@ -137,4 +137,6 @@ abstract class SpotubeIcons {
|
|||||||
static const extensions = FeatherIcons.package;
|
static const extensions = FeatherIcons.package;
|
||||||
static const message = FeatherIcons.send;
|
static const message = FeatherIcons.send;
|
||||||
static const upload = FeatherIcons.uploadCloud;
|
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/components/image/universal_image.dart';
|
||||||
import 'package:spotube/extensions/context.dart';
|
import 'package:spotube/extensions/context.dart';
|
||||||
import 'package:spotube/provider/metadata_plugin/library/playlists.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 {
|
class PlaylistAddTrackDialog extends HookConsumerWidget {
|
||||||
/// The id of the playlist this dialog was opened from
|
/// 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:shadcn_flutter/shadcn_flutter_extension.dart';
|
||||||
import 'package:spotube/collections/routes.gr.dart';
|
import 'package:spotube/collections/routes.gr.dart';
|
||||||
import 'package:spotube/extensions/context.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';
|
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/components/heart_button/use_track_toggle_like.dart';
|
||||||
import 'package:spotube/extensions/context.dart';
|
import 'package:spotube/extensions/context.dart';
|
||||||
import 'package:spotube/models/metadata/metadata.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/library/tracks.dart';
|
||||||
import 'package:spotube/provider/metadata_plugin/user.dart';
|
import 'package:spotube/provider/metadata_plugin/core/user.dart';
|
||||||
|
|
||||||
class HeartButton extends HookConsumerWidget {
|
class HeartButton extends HookConsumerWidget {
|
||||||
final bool isLiked;
|
final bool isLiked;
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:spotube/provider/metadata_plugin/library/playlists.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) {
|
bool useIsUserPlaylist(WidgetRef ref, String playlistId) {
|
||||||
final userPlaylistsQuery = ref.watch(metadataPluginSavedPlaylistsProvider);
|
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/download_manager_provider.dart';
|
||||||
import 'package:spotube/provider/local_tracks/local_tracks_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/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/library/playlists.dart';
|
||||||
import 'package:spotube/provider/metadata_plugin/metadata_plugin_provider.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/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:spotube/services/metadata/endpoints/error.dart';
|
||||||
|
|
||||||
import 'package:url_launcher/url_launcher_string.dart';
|
import 'package:url_launcher/url_launcher_string.dart';
|
||||||
|
@ -27,3 +27,4 @@ part 'track.dart';
|
|||||||
part 'user.dart';
|
part 'user.dart';
|
||||||
|
|
||||||
part 'plugin.dart';
|
part 'plugin.dart';
|
||||||
|
part 'repository.dart';
|
||||||
|
@ -4815,3 +4815,216 @@ abstract class _PluginConfiguration extends PluginConfiguration {
|
|||||||
_$$PluginConfigurationImplCopyWith<_$PluginConfigurationImpl> get copyWith =>
|
_$$PluginConfigurationImplCopyWith<_$PluginConfigurationImpl> get copyWith =>
|
||||||
throw _privateConstructorUsedError;
|
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 = {
|
const _$PluginAbilitiesEnumMap = {
|
||||||
PluginAbilities.authentication: 'authentication',
|
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/extensions/context.dart';
|
||||||
import 'package:spotube/models/metadata/metadata.dart';
|
import 'package:spotube/models/metadata/metadata.dart';
|
||||||
import 'package:spotube/provider/metadata_plugin/album/releases.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';
|
import 'package:spotube/provider/metadata_plugin/utils/common.dart';
|
||||||
|
|
||||||
class HomeNewReleasesSection extends HookConsumerWidget {
|
class HomeNewReleasesSection extends HookConsumerWidget {
|
||||||
|
@ -21,7 +21,7 @@ import 'package:spotube/extensions/constrains.dart';
|
|||||||
import 'package:spotube/extensions/context.dart';
|
import 'package:spotube/extensions/context.dart';
|
||||||
import 'package:spotube/modules/root/spotube_navigation_bar.dart';
|
import 'package:spotube/modules/root/spotube_navigation_bar.dart';
|
||||||
import 'package:spotube/provider/audio_player/audio_player.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/server/active_track_sources.dart';
|
||||||
import 'package:spotube/provider/volume_provider.dart';
|
import 'package:spotube/provider/volume_provider.dart';
|
||||||
import 'package:spotube/services/sourced_track/sources/youtube.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/download_manager_provider.dart';
|
||||||
import 'package:spotube/provider/audio_player/audio_player.dart';
|
import 'package:spotube/provider/audio_player/audio_player.dart';
|
||||||
import 'package:spotube/provider/local_tracks/local_tracks_provider.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';
|
import 'package:spotube/provider/sleep_timer_provider.dart';
|
||||||
|
|
||||||
class PlayerActions extends HookConsumerWidget {
|
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/audio_player/audio_player.dart';
|
||||||
import 'package:spotube/provider/metadata_plugin/library/tracks.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/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';
|
import 'package:spotube/services/audio_player/audio_player.dart';
|
||||||
|
|
||||||
class PlaylistCard extends HookConsumerWidget {
|
class PlaylistCard extends HookConsumerWidget {
|
||||||
|
@ -11,8 +11,8 @@ import 'package:spotube/extensions/context.dart';
|
|||||||
import 'package:spotube/models/metadata/metadata.dart';
|
import 'package:spotube/models/metadata/metadata.dart';
|
||||||
import 'package:spotube/modules/connect/connect_device.dart';
|
import 'package:spotube/modules/connect/connect_device.dart';
|
||||||
import 'package:spotube/provider/download_manager_provider.dart';
|
import 'package:spotube/provider/download_manager_provider.dart';
|
||||||
import 'package:spotube/provider/metadata_plugin/auth.dart';
|
import 'package:spotube/provider/metadata_plugin/core/auth.dart';
|
||||||
import 'package:spotube/provider/metadata_plugin/user.dart';
|
import 'package:spotube/provider/metadata_plugin/core/user.dart';
|
||||||
|
|
||||||
class SidebarFooter extends HookConsumerWidget implements NavigationBarItem {
|
class SidebarFooter extends HookConsumerWidget implements NavigationBarItem {
|
||||||
const SidebarFooter({
|
const SidebarFooter({
|
||||||
|
@ -12,7 +12,7 @@ import 'package:spotube/models/database/database.dart';
|
|||||||
import 'package:spotube/models/metadata/metadata.dart';
|
import 'package:spotube/models/metadata/metadata.dart';
|
||||||
import 'package:spotube/provider/blacklist_provider.dart';
|
import 'package:spotube/provider/blacklist_provider.dart';
|
||||||
import 'package:spotube/provider/metadata_plugin/artist/artist.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/provider/metadata_plugin/library/artists.dart';
|
||||||
import 'package:spotube/utils/primitive_utils.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/inter_scrollbar/inter_scrollbar.dart';
|
||||||
import 'package:spotube/components/fallbacks/anonymous_fallback.dart';
|
import 'package:spotube/components/fallbacks/anonymous_fallback.dart';
|
||||||
import 'package:spotube/extensions/context.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:spotube/provider/metadata_plugin/library/albums.dart';
|
||||||
import 'package:auto_route/auto_route.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/components/waypoint.dart';
|
||||||
import 'package:spotube/extensions/constrains.dart';
|
import 'package:spotube/extensions/constrains.dart';
|
||||||
import 'package:spotube/extensions/context.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:spotube/provider/metadata_plugin/library/artists.dart';
|
||||||
import 'package:auto_route/auto_route.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/components/fallbacks/anonymous_fallback.dart';
|
||||||
import 'package:spotube/modules/playlist/playlist_card.dart';
|
import 'package:spotube/modules/playlist/playlist_card.dart';
|
||||||
import 'package:spotube/extensions/context.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/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';
|
import 'package:auto_route/auto_route.dart';
|
||||||
|
|
||||||
@RoutePage()
|
@RoutePage()
|
||||||
|
@ -8,7 +8,7 @@ import 'package:spotube/components/image/universal_image.dart';
|
|||||||
import 'package:spotube/components/titlebar/titlebar.dart';
|
import 'package:spotube/components/titlebar/titlebar.dart';
|
||||||
import 'package:spotube/extensions/context.dart';
|
import 'package:spotube/extensions/context.dart';
|
||||||
import 'package:spotube/models/metadata/metadata.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:url_launcher/url_launcher_string.dart';
|
||||||
import 'package:auto_route/auto_route.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/artists.dart';
|
||||||
import 'package:spotube/pages/search/tabs/playlists.dart';
|
import 'package:spotube/pages/search/tabs/playlists.dart';
|
||||||
import 'package:spotube/pages/search/tabs/tracks.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/provider/metadata_plugin/search/all.dart';
|
||||||
import 'package:spotube/services/kv_store/kv_store.dart';
|
import 'package:spotube/services/kv_store/kv_store.dart';
|
||||||
import 'package:auto_route/auto_route.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:form_builder_validators/form_builder_validators.dart';
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:shadcn_flutter/shadcn_flutter.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/collections/spotube_icons.dart';
|
||||||
import 'package:spotube/components/form/text_form_field.dart';
|
import 'package:spotube/components/form/text_form_field.dart';
|
||||||
import 'package:spotube/components/titlebar/titlebar.dart';
|
import 'package:spotube/components/titlebar/titlebar.dart';
|
||||||
import 'package:spotube/models/metadata/metadata.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:spotube/provider/metadata_plugin/metadata_plugin_provider.dart';
|
||||||
import 'package:file_picker/file_picker.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:spotube/utils/platform.dart';
|
||||||
|
import 'package:url_launcher/url_launcher_string.dart';
|
||||||
|
import 'package:very_good_infinite_list/very_good_infinite_list.dart';
|
||||||
|
|
||||||
@RoutePage()
|
@RoutePage()
|
||||||
class SettingsMetadataProviderPage extends HookConsumerWidget {
|
class SettingsMetadataProviderPage extends HookConsumerWidget {
|
||||||
@ -26,6 +32,11 @@ class SettingsMetadataProviderPage extends HookConsumerWidget {
|
|||||||
final metadataPlugin = ref.watch(metadataPluginProvider);
|
final metadataPlugin = ref.watch(metadataPluginProvider);
|
||||||
final isAuthenticated = ref.watch(metadataPluginAuthenticatedProvider);
|
final isAuthenticated = ref.watch(metadataPluginAuthenticatedProvider);
|
||||||
|
|
||||||
|
final pluginReposSnapshot = ref.watch(metadataPluginRepositoriesProvider);
|
||||||
|
final pluginReposNotifier =
|
||||||
|
ref.watch(metadataPluginRepositoriesProvider.notifier);
|
||||||
|
final pluginRepos = pluginReposSnapshot.asData?.value.items ?? [];
|
||||||
|
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
headers: const [
|
headers: const [
|
||||||
TitleBar(
|
TitleBar(
|
||||||
@ -103,52 +114,101 @@ 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),
|
const SliverGap(20),
|
||||||
SliverList.separated(
|
SliverList.separated(
|
||||||
itemCount: plugins.asData?.value.plugins.length ?? 0,
|
itemCount: plugins.asData?.value.plugins.length ?? 0,
|
||||||
separatorBuilder: (context, index) => const Divider(),
|
separatorBuilder: (context, index) => const Gap(12),
|
||||||
itemBuilder: (context, index) {
|
itemBuilder: (context, index) {
|
||||||
final plugin = plugins.asData!.value.plugins[index];
|
final plugin = plugins.asData!.value.plugins[index];
|
||||||
final isDefault = plugins.asData!.value.defaultPlugin == index;
|
final isDefault = plugins.asData!.value.defaultPlugin == index;
|
||||||
final requiresAuth = isDefault &&
|
final requiresAuth = isDefault &&
|
||||||
plugin.abilities.contains(PluginAbilities.authentication);
|
plugin.abilities.contains(PluginAbilities.authentication);
|
||||||
|
|
||||||
return Card(
|
return Card(
|
||||||
child: Column(
|
child: Column(
|
||||||
spacing: 8,
|
|
||||||
mainAxisSize: MainAxisSize.min,
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||||
mainAxisAlignment: MainAxisAlignment.start,
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
spacing: 12,
|
||||||
children: [
|
children: [
|
||||||
Basic(
|
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),
|
title: Text(plugin.name),
|
||||||
subtitle: Text(plugin.description),
|
subtitle: Text(plugin.description),
|
||||||
trailing: Row(
|
trailing: IconButton.ghost(
|
||||||
|
onPressed: () async {
|
||||||
|
await pluginsNotifier.removePlugin(plugin);
|
||||||
|
},
|
||||||
|
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,
|
spacing: 8,
|
||||||
children: [
|
children: [
|
||||||
Button.primary(
|
Icon(SpotubeIcons.warning, color: Colors.yellow),
|
||||||
|
Text("Plugin requires authentication"),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
|
children: [
|
||||||
|
Button.secondary(
|
||||||
enabled: !isDefault,
|
enabled: !isDefault,
|
||||||
onPressed: () async {
|
onPressed: () async {
|
||||||
await pluginsNotifier.setDefaultPlugin(plugin);
|
await pluginsNotifier.setDefaultPlugin(plugin);
|
||||||
},
|
},
|
||||||
child: isDefault
|
child: isDefault
|
||||||
? const Text("Default")
|
? const Text("Default")
|
||||||
: const Text("Make default"),
|
: const Text("Set default"),
|
||||||
),
|
),
|
||||||
IconButton.destructive(
|
if (isAuthenticated.asData?.value != true &&
|
||||||
onPressed: () async {
|
requiresAuth &&
|
||||||
await pluginsNotifier.removePlugin(plugin);
|
isDefault)
|
||||||
},
|
|
||||||
icon: const Icon(SpotubeIcons.trash),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
if (requiresAuth)
|
|
||||||
Row(
|
|
||||||
children: [
|
|
||||||
const Text("Plugin requires authentication"),
|
|
||||||
const Spacer(),
|
|
||||||
if (isAuthenticated.asData?.value != true)
|
|
||||||
Button.primary(
|
Button.primary(
|
||||||
onPressed: () async {
|
onPressed: () async {
|
||||||
await metadataPlugin.asData?.value?.auth
|
await metadataPlugin.asData?.value?.auth
|
||||||
@ -157,7 +217,9 @@ class SettingsMetadataProviderPage extends HookConsumerWidget {
|
|||||||
leading: const Icon(SpotubeIcons.login),
|
leading: const Icon(SpotubeIcons.login),
|
||||||
child: const Text("Login"),
|
child: const Text("Login"),
|
||||||
)
|
)
|
||||||
else
|
else if (isAuthenticated.asData?.value == true &&
|
||||||
|
requiresAuth &&
|
||||||
|
isDefault)
|
||||||
Button.destructive(
|
Button.destructive(
|
||||||
onPressed: () async {
|
onPressed: () async {
|
||||||
await metadataPlugin.asData?.value?.auth
|
await metadataPlugin.asData?.value?.auth
|
||||||
@ -165,7 +227,7 @@ class SettingsMetadataProviderPage extends HookConsumerWidget {
|
|||||||
},
|
},
|
||||||
leading: const Icon(SpotubeIcons.logout),
|
leading: const Icon(SpotubeIcons.logout),
|
||||||
child: const Text("Logout"),
|
child: const Text("Logout"),
|
||||||
),
|
)
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
],
|
],
|
||||||
@ -173,6 +235,82 @@ class SettingsMetadataProviderPage extends HookConsumerWidget {
|
|||||||
);
|
);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
|
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:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:spotube/models/metadata/metadata.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';
|
import 'package:spotube/provider/metadata_plugin/metadata_plugin_provider.dart';
|
||||||
|
|
||||||
final metadataPluginUserProvider = FutureProvider<SpotubeUserObject?>(
|
final metadataPluginUserProvider = FutureProvider<SpotubeUserObject?>(
|
@ -130,7 +130,11 @@ class MetadataPluginNotifier extends AsyncNotifier<MetadataPluginState> {
|
|||||||
final parsedUri = Uri.parse(repoUrl);
|
final parsedUri = Uri.parse(repoUrl);
|
||||||
final uri = parsedUri.replace(
|
final uri = parsedUri.replace(
|
||||||
host: "api.github.com",
|
host: "api.github.com",
|
||||||
path: "/repos/${parsedUri.path}/releases",
|
pathSegments: [
|
||||||
|
"repos",
|
||||||
|
...parsedUri.pathSegments,
|
||||||
|
"releases",
|
||||||
|
],
|
||||||
queryParameters: {
|
queryParameters: {
|
||||||
"per_page": "1",
|
"per_page": "1",
|
||||||
"page": "1",
|
"page": "1",
|
||||||
@ -143,7 +147,13 @@ class MetadataPluginNotifier extends AsyncNotifier<MetadataPluginState> {
|
|||||||
Uri _getCodebergeReleasesUrl(String repoUrl) {
|
Uri _getCodebergeReleasesUrl(String repoUrl) {
|
||||||
final parsedUri = Uri.parse(repoUrl);
|
final parsedUri = Uri.parse(repoUrl);
|
||||||
final uri = parsedUri.replace(
|
final uri = parsedUri.replace(
|
||||||
path: "/api/v1/repos/${parsedUri.path}/releases",
|
pathSegments: [
|
||||||
|
"api",
|
||||||
|
"v1",
|
||||||
|
"repos",
|
||||||
|
...parsedUri.pathSegments,
|
||||||
|
"releases",
|
||||||
|
],
|
||||||
queryParameters: {
|
queryParameters: {
|
||||||
"limit": "1",
|
"limit": "1",
|
||||||
"page": "1",
|
"page": "1",
|
||||||
@ -154,6 +164,7 @@ class MetadataPluginNotifier extends AsyncNotifier<MetadataPluginState> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Future<String> _getPluginDownloadUrl(Uri uri) async {
|
Future<String> _getPluginDownloadUrl(Uri uri) async {
|
||||||
|
print("Getting plugin download URL from: $uri");
|
||||||
final res = await globalDio.getUri(
|
final res = await globalDio.getUri(
|
||||||
uri,
|
uri,
|
||||||
options: Options(responseType: ResponseType.json),
|
options: Options(responseType: ResponseType.json),
|
||||||
@ -227,8 +238,7 @@ class MetadataPluginNotifier extends AsyncNotifier<MetadataPluginState> {
|
|||||||
Future<PluginConfiguration> downloadAndCachePlugin(String url) async {
|
Future<PluginConfiguration> downloadAndCachePlugin(String url) async {
|
||||||
final res = await globalDio.head(url);
|
final res = await globalDio.head(url);
|
||||||
final isSupportedWebsite =
|
final isSupportedWebsite =
|
||||||
(res.headers["Content-Type"] as String?)?.startsWith("text/html") ==
|
(res.headers["Content-Type"]?.first)?.startsWith("text/html") == true &&
|
||||||
true &&
|
|
||||||
allowedDomainsRegex.hasMatch(url);
|
allowedDomainsRegex.hasMatch(url);
|
||||||
String pluginDownloadUrl = url;
|
String pluginDownloadUrl = url;
|
||||||
if (isSupportedWebsite) {
|
if (isSupportedWebsite) {
|
||||||
@ -329,6 +339,22 @@ class MetadataPluginNotifier extends AsyncNotifier<MetadataPluginState> {
|
|||||||
|
|
||||||
return await libraryFile.readAsBytes();
|
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 =
|
final metadataPluginsProvider =
|
||||||
|
@ -2,7 +2,7 @@ import 'package:flutter_riverpod/flutter_riverpod.dart';
|
|||||||
import 'package:spotube/models/metadata/metadata.dart';
|
import 'package:spotube/models/metadata/metadata.dart';
|
||||||
import 'package:spotube/provider/metadata_plugin/library/playlists.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/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/provider/metadata_plugin/utils/common.dart';
|
||||||
import 'package:spotube/services/metadata/endpoints/error.dart';
|
import 'package:spotube/services/metadata/endpoints/error.dart';
|
||||||
import 'package:spotube/services/metadata/metadata.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>();
|
state.value!.items.isEmpty ? <K>[] : state.value!.items.cast<K>();
|
||||||
final items = newState.items.isEmpty ? <K>[] : newState.items.cast<K>();
|
final items = newState.items.isEmpty ? <K>[] : newState.items.cast<K>();
|
||||||
|
|
||||||
return newState.copyWith(items: <K>[...oldItems, ...items])
|
return newState.copyWith(items: <K>[...oldItems, ...items]);
|
||||||
as SpotubePaginationResponseObject<K>;
|
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -48,8 +47,7 @@ mixin PaginatedAsyncNotifierMixin<K>
|
|||||||
hasMore = newState.hasMore;
|
hasMore = newState.hasMore;
|
||||||
final oldItems = state.items.isEmpty ? <K>[] : state.items.cast<K>();
|
final oldItems = state.items.isEmpty ? <K>[] : state.items.cast<K>();
|
||||||
final items = newState.items.isEmpty ? <K>[] : newState.items.cast<K>();
|
final items = newState.items.isEmpty ? <K>[] : newState.items.cast<K>();
|
||||||
return newState.copyWith(items: <K>[...oldItems, ...items])
|
return newState.copyWith(items: <K>[...oldItems, ...items]);
|
||||||
as SpotubePaginationResponseObject<K>;
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user