refactor: home browse and browse section

This commit is contained in:
Kingkor Roy Tirtho 2025-06-15 22:14:04 +06:00
parent b8cae569b4
commit 7569c37739
17 changed files with 530 additions and 1097 deletions

View File

@ -48,8 +48,8 @@ class AppRouter extends RootStackRouter {
page: GenrePlaylistsRoute.page,
),
AutoRoute(
path: "home/feeds/:feedId",
page: HomeFeedSectionRoute.page,
path: "home/sections/:sectionId",
page: HomeBrowseSectionItemsRoute.page,
),
AutoRoute(
path: "search",

View File

@ -19,10 +19,10 @@ import 'package:spotube/pages/artist/artist.dart' as _i3;
import 'package:spotube/pages/connect/connect.dart' as _i6;
import 'package:spotube/pages/connect/control/control.dart' as _i5;
import 'package:spotube/pages/getting_started/getting_started.dart' as _i9;
import 'package:spotube/pages/home/feed/feed_section.dart' as _i10;
import 'package:spotube/pages/home/genres/genre_playlists.dart' as _i8;
import 'package:spotube/pages/home/genres/genres.dart' as _i7;
import 'package:spotube/pages/home/home.dart' as _i11;
import 'package:spotube/pages/home/sections/section_items.dart' as _i10;
import 'package:spotube/pages/lastfm_login/lastfm_login.dart' as _i12;
import 'package:spotube/pages/library/library.dart' as _i13;
import 'package:spotube/pages/library/playlist_generate/playlist_generate.dart'
@ -332,53 +332,56 @@ class GettingStartedRoute extends _i44.PageRouteInfo<void> {
}
/// generated route for
/// [_i10.HomeFeedSectionPage]
class HomeFeedSectionRoute
extends _i44.PageRouteInfo<HomeFeedSectionRouteArgs> {
HomeFeedSectionRoute({
/// [_i10.HomeBrowseSectionItemsPage]
class HomeBrowseSectionItemsRoute
extends _i44.PageRouteInfo<HomeBrowseSectionItemsRouteArgs> {
HomeBrowseSectionItemsRoute({
_i48.Key? key,
required String sectionUri,
required String sectionId,
required _i46.SpotubeBrowseSectionObject<Object> section,
List<_i44.PageRouteInfo>? children,
}) : super(
HomeFeedSectionRoute.name,
args: HomeFeedSectionRouteArgs(
HomeBrowseSectionItemsRoute.name,
args: HomeBrowseSectionItemsRouteArgs(
key: key,
sectionUri: sectionUri,
sectionId: sectionId,
section: section,
),
rawPathParams: {'feedId': sectionUri},
rawPathParams: {'sectionId': sectionId},
initialChildren: children,
);
static const String name = 'HomeFeedSectionRoute';
static const String name = 'HomeBrowseSectionItemsRoute';
static _i44.PageInfo page = _i44.PageInfo(
name,
builder: (data) {
final pathParams = data.inheritedPathParams;
final args = data.argsAs<HomeFeedSectionRouteArgs>(
orElse: () => HomeFeedSectionRouteArgs(
sectionUri: pathParams.getString('feedId')));
return _i10.HomeFeedSectionPage(
final args = data.argsAs<HomeBrowseSectionItemsRouteArgs>();
return _i10.HomeBrowseSectionItemsPage(
key: args.key,
sectionUri: args.sectionUri,
sectionId: args.sectionId,
section: args.section,
);
},
);
}
class HomeFeedSectionRouteArgs {
const HomeFeedSectionRouteArgs({
class HomeBrowseSectionItemsRouteArgs {
const HomeBrowseSectionItemsRouteArgs({
this.key,
required this.sectionUri,
required this.sectionId,
required this.section,
});
final _i48.Key? key;
final String sectionUri;
final String sectionId;
final _i46.SpotubeBrowseSectionObject<Object> section;
@override
String toString() {
return 'HomeFeedSectionRouteArgs{key: $key, sectionUri: $sectionUri}';
return 'HomeBrowseSectionItemsRouteArgs{key: $key, sectionId: $sectionId, section: $section}';
}
}

View File

@ -33,8 +33,8 @@ class SpotubeSimpleAlbumObject with _$SpotubeSimpleAlbumObject {
required String externalUri,
required List<SpotubeSimpleArtistObject> artists,
@Default([]) List<SpotubeImageObject> images,
required String releaseDate,
required SpotubeAlbumType albumType,
String? releaseDate,
}) = _SpotubeSimpleAlbumObject;
factory SpotubeSimpleAlbumObject.fromJson(Map<String, dynamic> json) =>

View File

@ -1,43 +1,21 @@
part of 'metadata.dart';
enum SectionItemType {
@JsonValue("Playlist")
playlist,
@JsonValue("Album")
album,
@JsonValue("Artist")
artist
}
@Freezed(unionKey: "itemType")
class SpotubeBrowseSectionObject with _$SpotubeBrowseSectionObject {
@FreezedUnionValue("Album")
factory SpotubeBrowseSectionObject.album({
required String id,
required String title,
required String externalUri,
required SectionItemType itemType,
required List<SpotubeSimpleAlbumObject> items,
}) = SpotubeBrowseAlbumSectionObject;
@FreezedUnionValue("Artist")
factory SpotubeBrowseSectionObject.artist({
required String id,
required String title,
required String externalUri,
required SectionItemType itemType,
required List<SpotubeSimpleArtistObject> items,
}) = SpotubeBrowseArtistSectionObject;
@FreezedUnionValue("Playlist")
factory SpotubeBrowseSectionObject.playlist({
required String id,
required String title,
required String externalUri,
required SectionItemType itemType,
required List<SpotubeSimplePlaylistObject> items,
}) = SpotubeBrowsePlaylistSectionObject;
factory SpotubeBrowseSectionObject.fromJson(Map<String, Object?> json) =>
_$SpotubeBrowseSectionObjectFromJson(json);
@Freezed(genericArgumentFactories: true)
class SpotubeBrowseSectionObject<T> with _$SpotubeBrowseSectionObject<T> {
factory SpotubeBrowseSectionObject({
required String id,
required String title,
required String externalUri,
required bool browseMore,
required List<T> items,
}) = _SpotubeBrowseSectionObject<T>;
factory SpotubeBrowseSectionObject.fromJson(
Map<String, Object?> json,
T Function(Map<String, dynamic> json) fromJsonT,
) =>
_$SpotubeBrowseSectionObjectFromJson<T>(
json,
(json) => fromJsonT(json as Map<String, dynamic>),
);
}

File diff suppressed because it is too large Load Diff

View File

@ -64,8 +64,8 @@ _$SpotubeSimpleAlbumObjectImpl _$$SpotubeSimpleAlbumObjectImplFromJson(
Map<String, dynamic>.from(e as Map)))
.toList() ??
const [],
releaseDate: json['releaseDate'] as String,
albumType: $enumDecode(_$SpotubeAlbumTypeEnumMap, json['albumType']),
releaseDate: json['releaseDate'] as String?,
);
Map<String, dynamic> _$$SpotubeSimpleAlbumObjectImplToJson(
@ -76,8 +76,8 @@ Map<String, dynamic> _$$SpotubeSimpleAlbumObjectImplToJson(
'externalUri': instance.externalUri,
'artists': instance.artists.map((e) => e.toJson()).toList(),
'images': instance.images.map((e) => e.toJson()).toList(),
'releaseDate': instance.releaseDate,
'albumType': _$SpotubeAlbumTypeEnumMap[instance.albumType]!,
'releaseDate': instance.releaseDate,
};
_$SpotubeFullArtistObjectImpl _$$SpotubeFullArtistObjectImplFromJson(
@ -123,79 +123,29 @@ Map<String, dynamic> _$$SpotubeSimpleArtistObjectImplToJson(
'externalUri': instance.externalUri,
};
_$SpotubeBrowseAlbumSectionObjectImpl
_$$SpotubeBrowseAlbumSectionObjectImplFromJson(Map json) =>
_$SpotubeBrowseAlbumSectionObjectImpl(
_$SpotubeBrowseSectionObjectImpl<T>
_$$SpotubeBrowseSectionObjectImplFromJson<T>(
Map json,
T Function(Object? json) fromJsonT,
) =>
_$SpotubeBrowseSectionObjectImpl<T>(
id: json['id'] as String,
title: json['title'] as String,
externalUri: json['externalUri'] as String,
itemType: $enumDecode(_$SectionItemTypeEnumMap, json['itemType']),
items: (json['items'] as List<dynamic>)
.map((e) => SpotubeSimpleAlbumObject.fromJson(
Map<String, dynamic>.from(e as Map)))
.toList(),
browseMore: json['browseMore'] as bool,
items: (json['items'] as List<dynamic>).map(fromJsonT).toList(),
);
Map<String, dynamic> _$$SpotubeBrowseAlbumSectionObjectImplToJson(
_$SpotubeBrowseAlbumSectionObjectImpl instance) =>
Map<String, dynamic> _$$SpotubeBrowseSectionObjectImplToJson<T>(
_$SpotubeBrowseSectionObjectImpl<T> instance,
Object? Function(T value) toJsonT,
) =>
<String, dynamic>{
'id': instance.id,
'title': instance.title,
'externalUri': instance.externalUri,
'itemType': _$SectionItemTypeEnumMap[instance.itemType]!,
'items': instance.items.map((e) => e.toJson()).toList(),
};
const _$SectionItemTypeEnumMap = {
SectionItemType.playlist: 'Playlist',
SectionItemType.album: 'Album',
SectionItemType.artist: 'Artist',
};
_$SpotubeBrowseArtistSectionObjectImpl
_$$SpotubeBrowseArtistSectionObjectImplFromJson(Map json) =>
_$SpotubeBrowseArtistSectionObjectImpl(
id: json['id'] as String,
title: json['title'] as String,
externalUri: json['externalUri'] as String,
itemType: $enumDecode(_$SectionItemTypeEnumMap, json['itemType']),
items: (json['items'] as List<dynamic>)
.map((e) => SpotubeSimpleArtistObject.fromJson(
Map<String, dynamic>.from(e as Map)))
.toList(),
);
Map<String, dynamic> _$$SpotubeBrowseArtistSectionObjectImplToJson(
_$SpotubeBrowseArtistSectionObjectImpl instance) =>
<String, dynamic>{
'id': instance.id,
'title': instance.title,
'externalUri': instance.externalUri,
'itemType': _$SectionItemTypeEnumMap[instance.itemType]!,
'items': instance.items.map((e) => e.toJson()).toList(),
};
_$SpotubeBrowsePlaylistSectionObjectImpl
_$$SpotubeBrowsePlaylistSectionObjectImplFromJson(Map json) =>
_$SpotubeBrowsePlaylistSectionObjectImpl(
id: json['id'] as String,
title: json['title'] as String,
externalUri: json['externalUri'] as String,
itemType: $enumDecode(_$SectionItemTypeEnumMap, json['itemType']),
items: (json['items'] as List<dynamic>)
.map((e) => SpotubeSimplePlaylistObject.fromJson(
Map<String, dynamic>.from(e as Map)))
.toList(),
);
Map<String, dynamic> _$$SpotubeBrowsePlaylistSectionObjectImplToJson(
_$SpotubeBrowsePlaylistSectionObjectImpl instance) =>
<String, dynamic>{
'id': instance.id,
'title': instance.title,
'externalUri': instance.externalUri,
'itemType': _$SectionItemTypeEnumMap[instance.itemType]!,
'items': instance.items.map((e) => e.toJson()).toList(),
'browseMore': instance.browseMore,
'items': instance.items.map(toJsonT).toList(),
};
_$SpotubeImageObjectImpl _$$SpotubeImageObjectImplFromJson(Map json) =>

View File

@ -63,7 +63,7 @@ class AlbumCard extends HookConsumerWidget {
);
var isLoading =
(isPlaylistPlaying && isFetchingActiveTrack) || updating.value;
var description = "${album.albumType}${album.artists.asString()}";
var description = "${album.albumType.name}${album.artists.asString()}";
void onTap() {
context.navigateTo(AlbumRoute(id: album.id, album: album));

View File

@ -1,50 +0,0 @@
import 'package:auto_route/auto_route.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:shadcn_flutter/shadcn_flutter.dart';
import 'package:spotube/collections/routes.gr.dart';
import 'package:spotube/components/horizontal_playbutton_card_view/horizontal_playbutton_card_view.dart';
import 'package:spotube/extensions/context.dart';
import 'package:spotube/provider/spotify/views/home.dart';
class HomePageFeedSection extends HookConsumerWidget {
const HomePageFeedSection({super.key});
@override
Widget build(BuildContext context, ref) {
final homeFeed = ref.watch(homeViewProvider);
final nonShortSections = homeFeed.asData?.value.sections
.where((s) => s.typename == "HomeGenericSectionData")
.toList() ??
[];
return SliverList.builder(
itemCount: nonShortSections.length,
itemBuilder: (context, index) {
final section = nonShortSections[index];
if (section.items.isEmpty) return const SizedBox.shrink();
return HorizontalPlaybuttonCardView(
items: [
for (final item in section.items)
if (item.album != null)
item.album!.asAlbum
else if (item.artist != null)
item.artist!.asArtist
else if (item.playlist != null)
item.playlist!.asPlaylist
],
title: Text(section.title ?? context.l10n.no_title),
hasNextPage: false,
isLoadingNextPage: false,
onFetchMore: () {},
titleTrailing: Button.text(
child: Text(context.l10n.browse_all),
onPressed: () {
context.navigateTo(HomeFeedSectionRoute(sectionUri: section.uri));
},
),
);
},
);
}
}

View File

@ -0,0 +1,46 @@
import 'package:auto_route/auto_route.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:shadcn_flutter/shadcn_flutter.dart';
import 'package:spotube/collections/routes.gr.dart';
import 'package:spotube/components/horizontal_playbutton_card_view/horizontal_playbutton_card_view.dart';
import 'package:spotube/extensions/context.dart';
import 'package:spotube/provider/metadata_plugin/browse/sections.dart';
class HomePageBrowseSection extends HookConsumerWidget {
const HomePageBrowseSection({super.key});
@override
Widget build(BuildContext context, ref) {
final browseSections = ref.watch(metadataPluginBrowseSectionsProvider);
final sections = browseSections.asData?.value.items;
return SliverList.builder(
itemCount: sections?.length ?? 0,
itemBuilder: (context, index) {
final section = sections![index];
if (section.items.isEmpty) return const SizedBox.shrink();
return HorizontalPlaybuttonCardView(
items: section.items,
title: Text(section.title),
hasNextPage: false,
isLoadingNextPage: false,
onFetchMore: () {},
titleTrailing: section.browseMore
? Button.text(
child: Text(context.l10n.browse_all),
onPressed: () {
context.navigateTo(
HomeBrowseSectionItemsRoute(
sectionId: section.id,
section: section,
),
);
},
)
: null,
);
},
);
}
}

View File

@ -1,101 +0,0 @@
import 'package:auto_route/auto_route.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:shadcn_flutter/shadcn_flutter.dart';
import 'package:skeletonizer/skeletonizer.dart';
import 'package:spotube/collections/fake.dart';
import 'package:spotube/components/playbutton_view/playbutton_view.dart';
import 'package:spotube/modules/album/album_card.dart';
import 'package:spotube/modules/artist/artist_card.dart';
import 'package:spotube/modules/playlist/playlist_card.dart';
import 'package:spotube/components/titlebar/titlebar.dart';
import 'package:spotube/provider/spotify/views/home_section.dart';
@RoutePage()
class HomeFeedSectionPage extends HookConsumerWidget {
static const name = "home_feed_section";
final String sectionUri;
const HomeFeedSectionPage({
super.key,
@PathParam("feedId") required this.sectionUri,
});
@override
Widget build(BuildContext context, ref) {
final homeFeedSection = ref.watch(homeSectionViewProvider(sectionUri));
final section = homeFeedSection.asData?.value ?? FakeData.feedSection;
final controller = useScrollController();
final isArtist = section.items.every((item) => item.artist != null);
return SafeArea(
bottom: false,
child: Skeletonizer(
enabled: homeFeedSection.isLoading,
child: Scaffold(
headers: [
TitleBar(
title: Text(section.title ?? ""),
)
],
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 8.0),
child: CustomScrollView(
controller: controller,
slivers: [
// if (isArtist)
// SliverGrid.builder(
// gridDelegate:
// const SliverGridDelegateWithMaxCrossAxisExtent(
// maxCrossAxisExtent: 200,
// mainAxisExtent: 250,
// crossAxisSpacing: 8,
// mainAxisSpacing: 8,
// ),
// itemCount: section.items.length,
// itemBuilder: (context, index) {
// final item = section.items[index];
// return ArtistCard(item.artist!.asArtist);
// },
// )
// else
// PlaybuttonView(
// controller: controller,
// itemCount: section.items.length,
// hasMore: false,
// isLoading: false,
// onRequestMore: () => {},
// listItemBuilder: (context, index) {
// final item = section.items[index];
// if (item.album != null) {
// return AlbumCard.tile(item.album!.asAlbum);
// }
// if (item.playlist != null) {
// return PlaylistCard.tile(item.playlist!.asPlaylist);
// }
// return const SizedBox.shrink();
// },
// gridItemBuilder: (context, index) {
// final item = section.items[index];
// if (item.album != null) {
// return AlbumCard(item.album!.asAlbum);
// }
// if (item.playlist != null) {
// return PlaylistCard(item.playlist!.asPlaylist);
// }
// return const SizedBox.shrink();
// },
// ),
const SliverToBoxAdapter(
child: SafeArea(
child: SizedBox(),
),
),
],
),
),
),
),
);
}
}

View File

@ -12,7 +12,7 @@ import 'package:spotube/collections/spotube_icons.dart';
import 'package:spotube/models/database/database.dart';
import 'package:spotube/modules/connect/connect_device.dart';
import 'package:spotube/modules/home/sections/featured.dart';
import 'package:spotube/modules/home/sections/feed.dart';
import 'package:spotube/modules/home/sections/sections.dart';
import 'package:spotube/modules/home/sections/friends.dart';
import 'package:spotube/modules/home/sections/genres/genres.dart';
import 'package:spotube/modules/home/sections/made_for_user.dart';
@ -76,20 +76,19 @@ class HomePage extends HookConsumerWidget {
else if (kIsMacOS)
const SliverGap(10),
const SliverGap(10),
SliverList.builder(
itemCount: 5,
itemBuilder: (context, index) {
return switch (index) {
0 => const HomeGenresSection(),
1 => const HomeRecentlyPlayedSection(),
2 => const HomeFeaturedSection(),
3 => const HomePageFriendsSection(),
_ => const HomeNewReleasesSection()
};
},
),
const HomePageFeedSection(),
const SliverSafeArea(sliver: HomeMadeForUserSection()),
// SliverList.builder(
// itemCount: 5,
// itemBuilder: (context, index) {
// return switch (index) {
// 0 => const HomeGenresSection(),
// 1 => const HomeRecentlyPlayedSection(),
// 2 => const HomeFeaturedSection(),
// 3 => const HomePageFriendsSection(),
// _ => const HomeNewReleasesSection()
// };
// },
// ),
const HomePageBrowseSection(),
],
),
));

View File

@ -0,0 +1,122 @@
import 'package:auto_route/auto_route.dart';
import 'package:flutter_hooks/flutter_hooks.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/components/playbutton_view/playbutton_card.dart';
import 'package:spotube/components/waypoint.dart';
import 'package:spotube/models/metadata/metadata.dart';
import 'package:spotube/modules/album/album_card.dart';
import 'package:spotube/modules/artist/artist_card.dart';
import 'package:spotube/modules/playlist/playlist_card.dart';
import 'package:spotube/components/titlebar/titlebar.dart';
import 'package:spotube/provider/metadata_plugin/browse/section_items.dart';
import 'package:spotube/provider/metadata_plugin/utils/common.dart';
const _dummyPlaybuttonCard = PlaybuttonCard(
imageUrl: 'https://placehold.co/150x150.png',
isLoading: false,
isPlaying: false,
title: "Playbutton",
description: "A really cool playbutton",
isOwner: false,
);
@RoutePage()
class HomeBrowseSectionItemsPage extends HookConsumerWidget {
static const name = "home_browse_section_items";
final String sectionId;
final SpotubeBrowseSectionObject<Object> section;
const HomeBrowseSectionItemsPage({
super.key,
@PathParam("sectionId") required this.sectionId,
required this.section,
});
@override
Widget build(BuildContext context, ref) {
final scale = context.theme.scaling;
final sectionItems =
ref.watch(metadataPluginBrowseSectionItemsProvider(sectionId));
final sectionItemsNotifier =
ref.watch(metadataPluginBrowseSectionItemsProvider(sectionId).notifier);
final items = sectionItems.asData?.value.items ?? [];
final controller = useScrollController();
final isLoading = sectionItems.isLoading || sectionItems.isLoadingNextPage;
final itemCount = items.length;
final hasMore = sectionItems.asData?.value.hasMore ?? false;
return SafeArea(
bottom: false,
child: Skeletonizer(
enabled: sectionItems.isLoading,
child: Scaffold(
headers: [
TitleBar(
title: Text(section.title),
)
],
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 8.0),
child: CustomScrollView(
controller: controller,
slivers: [
SliverGrid.builder(
itemCount: isLoading ? 6 : itemCount + 1,
gridDelegate: SliverGridDelegateWithMaxCrossAxisExtent(
maxCrossAxisExtent: 150 * scale,
mainAxisExtent: 225 * scale,
crossAxisSpacing: 12 * scale,
mainAxisSpacing: 12 * scale,
),
itemBuilder: (context, index) {
if (isLoading) {
return const Skeletonizer(
enabled: true,
child: _dummyPlaybuttonCard,
);
}
if (index == itemCount) {
if (!hasMore) return const SizedBox.shrink();
return Waypoint(
controller: controller,
isGrid: true,
onTouchEdge: () async {
await sectionItemsNotifier.fetchMore();
},
child: const Skeletonizer(
enabled: true,
child: _dummyPlaybuttonCard,
),
);
}
final item = items[index];
return switch (item) {
SpotubeFullArtistObject() => ArtistCard(item),
SpotubeSimplePlaylistObject() => PlaylistCard(item),
SpotubeSimpleAlbumObject() => AlbumCard(item),
_ => throw Exception(
"Unsupported item type: ${item.runtimeType}",
),
};
},
),
const SliverToBoxAdapter(
child: SafeArea(
child: SizedBox(),
),
),
],
),
),
),
),
);
}
}

View File

@ -0,0 +1,32 @@
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:spotube/models/metadata/metadata.dart';
import 'package:spotube/provider/metadata_plugin/metadata_plugin_provider.dart';
import 'package:spotube/provider/metadata_plugin/utils/family_paginated.dart';
class MetadataPluginBrowseSectionItemsNotifier
extends FamilyPaginatedAsyncNotifier<Object, String> {
@override
Future<SpotubePaginationResponseObject<Object>> fetch(
int offset,
int limit,
) async {
return await (await metadataPlugin).browse.sectionItems(
arg,
limit: limit,
offset: offset,
);
}
@override
build(arg) async {
ref.watch(metadataPluginProvider);
return await fetch(0, 20);
}
}
final metadataPluginBrowseSectionItemsProvider = AsyncNotifierProviderFamily<
MetadataPluginBrowseSectionItemsNotifier,
SpotubePaginationResponseObject<Object>,
String>(
() => MetadataPluginBrowseSectionItemsNotifier(),
);

View File

@ -0,0 +1,31 @@
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:spotube/models/metadata/metadata.dart';
import 'package:spotube/provider/metadata_plugin/metadata_plugin_provider.dart';
import 'package:spotube/provider/metadata_plugin/utils/paginated.dart';
class MetadataPluginBrowseSectionsNotifier
extends PaginatedAsyncNotifier<SpotubeBrowseSectionObject<Object>> {
@override
Future<SpotubePaginationResponseObject<SpotubeBrowseSectionObject<Object>>>
fetch(
int offset,
int limit,
) async {
return await (await metadataPlugin).browse.sections(
limit: limit,
offset: offset,
);
}
@override
build() async {
ref.watch(metadataPluginProvider);
return await fetch(0, 20);
}
}
final metadataPluginBrowseSectionsProvider = AsyncNotifierProvider<
MetadataPluginBrowseSectionsNotifier,
SpotubePaginationResponseObject<SpotubeBrowseSectionObject<Object>>>(
() => MetadataPluginBrowseSectionsNotifier(),
);

View File

@ -9,6 +9,10 @@ import 'package:spotube/provider/metadata_plugin/metadata_plugin_provider.dart';
import 'package:spotube/services/metadata/endpoints/error.dart';
import 'package:spotube/services/metadata/metadata.dart';
extension PaginationExtension<T> on AsyncValue<T> {
bool get isLoadingNextPage => this is AsyncData && this is AsyncLoadingNext;
}
mixin MetadataPluginMixin<K>
// ignore: invalid_use_of_internal_member
on AsyncNotifierBase<SpotubePaginationResponseObject<K>> {

View File

@ -0,0 +1,87 @@
import 'package:hetu_script/hetu_script.dart';
import 'package:hetu_script/values.dart';
import 'package:spotube/models/metadata/metadata.dart';
class MetadataPluginBrowseEndpoint {
final Hetu hetu;
MetadataPluginBrowseEndpoint(this.hetu);
HTInstance get hetuMetadataBrowse =>
(hetu.fetch("metadataPlugin") as HTInstance).memberGet("browse")
as HTInstance;
Future<SpotubePaginationResponseObject<SpotubeBrowseSectionObject<Object>>>
sections({
int? offset,
int? limit,
}) async {
final raw = await hetuMetadataBrowse.invoke(
"sections",
namedArgs: {
"offset": offset,
"limit": limit,
}..removeWhere((key, value) => value == null),
) as Map;
return SpotubePaginationResponseObject<
SpotubeBrowseSectionObject<Object>>.fromJson(
raw.cast<String, dynamic>(),
(Map json) => SpotubeBrowseSectionObject<Object>.fromJson(
json.cast<String, dynamic>(),
(json) {
final isPlaylist = json["owner"] != null;
final isAlbum = json["artists"] != null;
if (isPlaylist) {
return SpotubeSimplePlaylistObject.fromJson(
json.cast<String, dynamic>(),
);
} else if (isAlbum) {
return SpotubeSimpleAlbumObject.fromJson(
json.cast<String, dynamic>(),
);
} else {
return SpotubeFullArtistObject.fromJson(
json.cast<String, dynamic>(),
);
}
},
),
);
}
Future<SpotubePaginationResponseObject<Object>> sectionItems(
String id, {
int? offset,
int? limit,
}) async {
final raw = await hetuMetadataBrowse.invoke(
"sectionItems",
positionalArgs: [id],
namedArgs: {
"offset": offset,
"limit": limit,
}..removeWhere((key, value) => value == null),
) as Map;
return SpotubePaginationResponseObject<Object>.fromJson(
raw.cast<String, dynamic>(),
(json) {
final isPlaylist = json["owner"] != null;
final isAlbum = json["artists"] != null;
if (isPlaylist) {
return SpotubeSimplePlaylistObject.fromJson(
json.cast<String, dynamic>(),
);
} else if (isAlbum) {
return SpotubeSimpleAlbumObject.fromJson(
json.cast<String, dynamic>(),
);
} else {
return SpotubeFullArtistObject.fromJson(
json.cast<String, dynamic>(),
);
}
},
);
}
}

View File

@ -14,6 +14,7 @@ import 'package:spotube/services/metadata/apis/localstorage.dart';
import 'package:spotube/services/metadata/endpoints/album.dart';
import 'package:spotube/services/metadata/endpoints/artist.dart';
import 'package:spotube/services/metadata/endpoints/auth.dart';
import 'package:spotube/services/metadata/endpoints/browse.dart';
import 'package:spotube/services/metadata/endpoints/playlist.dart';
import 'package:spotube/services/metadata/endpoints/user.dart';
@ -78,6 +79,7 @@ class MetadataPlugin {
late final MetadataPluginAlbumEndpoint album;
late final MetadataPluginArtistEndpoint artist;
late final MetadataPluginBrowseEndpoint browse;
late final MetadataPluginPlaylistEndpoint playlist;
late final MetadataPluginUserEndpoint user;
@ -86,6 +88,7 @@ class MetadataPlugin {
artist = MetadataPluginArtistEndpoint(hetu);
album = MetadataPluginAlbumEndpoint(hetu);
browse = MetadataPluginBrowseEndpoint(hetu);
playlist = MetadataPluginPlaylistEndpoint(hetu);
user = MetadataPluginUserEndpoint(hetu);
}