mirror of
https://github.com/KRTirtho/spotube.git
synced 2025-09-12 23:45:18 +00:00
refactor: home browse and browse section
This commit is contained in:
parent
b8cae569b4
commit
7569c37739
@ -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",
|
||||
|
@ -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}';
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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) =>
|
||||
|
@ -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
@ -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) =>
|
||||
|
@ -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));
|
||||
|
@ -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));
|
||||
},
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
46
lib/modules/home/sections/sections.dart
Normal file
46
lib/modules/home/sections/sections.dart
Normal 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,
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
@ -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(),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
@ -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(),
|
||||
],
|
||||
),
|
||||
));
|
||||
|
122
lib/pages/home/sections/section_items.dart
Normal file
122
lib/pages/home/sections/section_items.dart
Normal 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(),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
32
lib/provider/metadata_plugin/browse/section_items.dart
Normal file
32
lib/provider/metadata_plugin/browse/section_items.dart
Normal 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(),
|
||||
);
|
31
lib/provider/metadata_plugin/browse/sections.dart
Normal file
31
lib/provider/metadata_plugin/browse/sections.dart
Normal 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(),
|
||||
);
|
@ -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>> {
|
||||
|
@ -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>(),
|
||||
);
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user