mirror of
https://github.com/KRTirtho/spotube.git
synced 2025-09-13 07:55: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,
|
page: GenrePlaylistsRoute.page,
|
||||||
),
|
),
|
||||||
AutoRoute(
|
AutoRoute(
|
||||||
path: "home/feeds/:feedId",
|
path: "home/sections/:sectionId",
|
||||||
page: HomeFeedSectionRoute.page,
|
page: HomeBrowseSectionItemsRoute.page,
|
||||||
),
|
),
|
||||||
AutoRoute(
|
AutoRoute(
|
||||||
path: "search",
|
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/connect.dart' as _i6;
|
||||||
import 'package:spotube/pages/connect/control/control.dart' as _i5;
|
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/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/genre_playlists.dart' as _i8;
|
||||||
import 'package:spotube/pages/home/genres/genres.dart' as _i7;
|
import 'package:spotube/pages/home/genres/genres.dart' as _i7;
|
||||||
import 'package:spotube/pages/home/home.dart' as _i11;
|
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/lastfm_login/lastfm_login.dart' as _i12;
|
||||||
import 'package:spotube/pages/library/library.dart' as _i13;
|
import 'package:spotube/pages/library/library.dart' as _i13;
|
||||||
import 'package:spotube/pages/library/playlist_generate/playlist_generate.dart'
|
import 'package:spotube/pages/library/playlist_generate/playlist_generate.dart'
|
||||||
@ -332,53 +332,56 @@ class GettingStartedRoute extends _i44.PageRouteInfo<void> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// generated route for
|
/// generated route for
|
||||||
/// [_i10.HomeFeedSectionPage]
|
/// [_i10.HomeBrowseSectionItemsPage]
|
||||||
class HomeFeedSectionRoute
|
class HomeBrowseSectionItemsRoute
|
||||||
extends _i44.PageRouteInfo<HomeFeedSectionRouteArgs> {
|
extends _i44.PageRouteInfo<HomeBrowseSectionItemsRouteArgs> {
|
||||||
HomeFeedSectionRoute({
|
HomeBrowseSectionItemsRoute({
|
||||||
_i48.Key? key,
|
_i48.Key? key,
|
||||||
required String sectionUri,
|
required String sectionId,
|
||||||
|
required _i46.SpotubeBrowseSectionObject<Object> section,
|
||||||
List<_i44.PageRouteInfo>? children,
|
List<_i44.PageRouteInfo>? children,
|
||||||
}) : super(
|
}) : super(
|
||||||
HomeFeedSectionRoute.name,
|
HomeBrowseSectionItemsRoute.name,
|
||||||
args: HomeFeedSectionRouteArgs(
|
args: HomeBrowseSectionItemsRouteArgs(
|
||||||
key: key,
|
key: key,
|
||||||
sectionUri: sectionUri,
|
sectionId: sectionId,
|
||||||
|
section: section,
|
||||||
),
|
),
|
||||||
rawPathParams: {'feedId': sectionUri},
|
rawPathParams: {'sectionId': sectionId},
|
||||||
initialChildren: children,
|
initialChildren: children,
|
||||||
);
|
);
|
||||||
|
|
||||||
static const String name = 'HomeFeedSectionRoute';
|
static const String name = 'HomeBrowseSectionItemsRoute';
|
||||||
|
|
||||||
static _i44.PageInfo page = _i44.PageInfo(
|
static _i44.PageInfo page = _i44.PageInfo(
|
||||||
name,
|
name,
|
||||||
builder: (data) {
|
builder: (data) {
|
||||||
final pathParams = data.inheritedPathParams;
|
final args = data.argsAs<HomeBrowseSectionItemsRouteArgs>();
|
||||||
final args = data.argsAs<HomeFeedSectionRouteArgs>(
|
return _i10.HomeBrowseSectionItemsPage(
|
||||||
orElse: () => HomeFeedSectionRouteArgs(
|
|
||||||
sectionUri: pathParams.getString('feedId')));
|
|
||||||
return _i10.HomeFeedSectionPage(
|
|
||||||
key: args.key,
|
key: args.key,
|
||||||
sectionUri: args.sectionUri,
|
sectionId: args.sectionId,
|
||||||
|
section: args.section,
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
class HomeFeedSectionRouteArgs {
|
class HomeBrowseSectionItemsRouteArgs {
|
||||||
const HomeFeedSectionRouteArgs({
|
const HomeBrowseSectionItemsRouteArgs({
|
||||||
this.key,
|
this.key,
|
||||||
required this.sectionUri,
|
required this.sectionId,
|
||||||
|
required this.section,
|
||||||
});
|
});
|
||||||
|
|
||||||
final _i48.Key? key;
|
final _i48.Key? key;
|
||||||
|
|
||||||
final String sectionUri;
|
final String sectionId;
|
||||||
|
|
||||||
|
final _i46.SpotubeBrowseSectionObject<Object> section;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String toString() {
|
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 String externalUri,
|
||||||
required List<SpotubeSimpleArtistObject> artists,
|
required List<SpotubeSimpleArtistObject> artists,
|
||||||
@Default([]) List<SpotubeImageObject> images,
|
@Default([]) List<SpotubeImageObject> images,
|
||||||
required String releaseDate,
|
|
||||||
required SpotubeAlbumType albumType,
|
required SpotubeAlbumType albumType,
|
||||||
|
String? releaseDate,
|
||||||
}) = _SpotubeSimpleAlbumObject;
|
}) = _SpotubeSimpleAlbumObject;
|
||||||
|
|
||||||
factory SpotubeSimpleAlbumObject.fromJson(Map<String, dynamic> json) =>
|
factory SpotubeSimpleAlbumObject.fromJson(Map<String, dynamic> json) =>
|
||||||
|
@ -1,43 +1,21 @@
|
|||||||
part of 'metadata.dart';
|
part of 'metadata.dart';
|
||||||
|
|
||||||
enum SectionItemType {
|
@Freezed(genericArgumentFactories: true)
|
||||||
@JsonValue("Playlist")
|
class SpotubeBrowseSectionObject<T> with _$SpotubeBrowseSectionObject<T> {
|
||||||
playlist,
|
factory SpotubeBrowseSectionObject({
|
||||||
@JsonValue("Album")
|
required String id,
|
||||||
album,
|
required String title,
|
||||||
@JsonValue("Artist")
|
required String externalUri,
|
||||||
artist
|
required bool browseMore,
|
||||||
}
|
required List<T> items,
|
||||||
|
}) = _SpotubeBrowseSectionObject<T>;
|
||||||
@Freezed(unionKey: "itemType")
|
|
||||||
class SpotubeBrowseSectionObject with _$SpotubeBrowseSectionObject {
|
factory SpotubeBrowseSectionObject.fromJson(
|
||||||
@FreezedUnionValue("Album")
|
Map<String, Object?> json,
|
||||||
factory SpotubeBrowseSectionObject.album({
|
T Function(Map<String, dynamic> json) fromJsonT,
|
||||||
required String id,
|
) =>
|
||||||
required String title,
|
_$SpotubeBrowseSectionObjectFromJson<T>(
|
||||||
required String externalUri,
|
json,
|
||||||
required SectionItemType itemType,
|
(json) => fromJsonT(json as Map<String, dynamic>),
|
||||||
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);
|
|
||||||
}
|
}
|
||||||
|
File diff suppressed because it is too large
Load Diff
@ -64,8 +64,8 @@ _$SpotubeSimpleAlbumObjectImpl _$$SpotubeSimpleAlbumObjectImplFromJson(
|
|||||||
Map<String, dynamic>.from(e as Map)))
|
Map<String, dynamic>.from(e as Map)))
|
||||||
.toList() ??
|
.toList() ??
|
||||||
const [],
|
const [],
|
||||||
releaseDate: json['releaseDate'] as String,
|
|
||||||
albumType: $enumDecode(_$SpotubeAlbumTypeEnumMap, json['albumType']),
|
albumType: $enumDecode(_$SpotubeAlbumTypeEnumMap, json['albumType']),
|
||||||
|
releaseDate: json['releaseDate'] as String?,
|
||||||
);
|
);
|
||||||
|
|
||||||
Map<String, dynamic> _$$SpotubeSimpleAlbumObjectImplToJson(
|
Map<String, dynamic> _$$SpotubeSimpleAlbumObjectImplToJson(
|
||||||
@ -76,8 +76,8 @@ Map<String, dynamic> _$$SpotubeSimpleAlbumObjectImplToJson(
|
|||||||
'externalUri': instance.externalUri,
|
'externalUri': instance.externalUri,
|
||||||
'artists': instance.artists.map((e) => e.toJson()).toList(),
|
'artists': instance.artists.map((e) => e.toJson()).toList(),
|
||||||
'images': instance.images.map((e) => e.toJson()).toList(),
|
'images': instance.images.map((e) => e.toJson()).toList(),
|
||||||
'releaseDate': instance.releaseDate,
|
|
||||||
'albumType': _$SpotubeAlbumTypeEnumMap[instance.albumType]!,
|
'albumType': _$SpotubeAlbumTypeEnumMap[instance.albumType]!,
|
||||||
|
'releaseDate': instance.releaseDate,
|
||||||
};
|
};
|
||||||
|
|
||||||
_$SpotubeFullArtistObjectImpl _$$SpotubeFullArtistObjectImplFromJson(
|
_$SpotubeFullArtistObjectImpl _$$SpotubeFullArtistObjectImplFromJson(
|
||||||
@ -123,79 +123,29 @@ Map<String, dynamic> _$$SpotubeSimpleArtistObjectImplToJson(
|
|||||||
'externalUri': instance.externalUri,
|
'externalUri': instance.externalUri,
|
||||||
};
|
};
|
||||||
|
|
||||||
_$SpotubeBrowseAlbumSectionObjectImpl
|
_$SpotubeBrowseSectionObjectImpl<T>
|
||||||
_$$SpotubeBrowseAlbumSectionObjectImplFromJson(Map json) =>
|
_$$SpotubeBrowseSectionObjectImplFromJson<T>(
|
||||||
_$SpotubeBrowseAlbumSectionObjectImpl(
|
Map json,
|
||||||
|
T Function(Object? json) fromJsonT,
|
||||||
|
) =>
|
||||||
|
_$SpotubeBrowseSectionObjectImpl<T>(
|
||||||
id: json['id'] as String,
|
id: json['id'] as String,
|
||||||
title: json['title'] as String,
|
title: json['title'] as String,
|
||||||
externalUri: json['externalUri'] as String,
|
externalUri: json['externalUri'] as String,
|
||||||
itemType: $enumDecode(_$SectionItemTypeEnumMap, json['itemType']),
|
browseMore: json['browseMore'] as bool,
|
||||||
items: (json['items'] as List<dynamic>)
|
items: (json['items'] as List<dynamic>).map(fromJsonT).toList(),
|
||||||
.map((e) => SpotubeSimpleAlbumObject.fromJson(
|
|
||||||
Map<String, dynamic>.from(e as Map)))
|
|
||||||
.toList(),
|
|
||||||
);
|
);
|
||||||
|
|
||||||
Map<String, dynamic> _$$SpotubeBrowseAlbumSectionObjectImplToJson(
|
Map<String, dynamic> _$$SpotubeBrowseSectionObjectImplToJson<T>(
|
||||||
_$SpotubeBrowseAlbumSectionObjectImpl instance) =>
|
_$SpotubeBrowseSectionObjectImpl<T> instance,
|
||||||
|
Object? Function(T value) toJsonT,
|
||||||
|
) =>
|
||||||
<String, dynamic>{
|
<String, dynamic>{
|
||||||
'id': instance.id,
|
'id': instance.id,
|
||||||
'title': instance.title,
|
'title': instance.title,
|
||||||
'externalUri': instance.externalUri,
|
'externalUri': instance.externalUri,
|
||||||
'itemType': _$SectionItemTypeEnumMap[instance.itemType]!,
|
'browseMore': instance.browseMore,
|
||||||
'items': instance.items.map((e) => e.toJson()).toList(),
|
'items': instance.items.map(toJsonT).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(),
|
|
||||||
};
|
};
|
||||||
|
|
||||||
_$SpotubeImageObjectImpl _$$SpotubeImageObjectImplFromJson(Map json) =>
|
_$SpotubeImageObjectImpl _$$SpotubeImageObjectImplFromJson(Map json) =>
|
||||||
|
@ -63,7 +63,7 @@ class AlbumCard extends HookConsumerWidget {
|
|||||||
);
|
);
|
||||||
var isLoading =
|
var isLoading =
|
||||||
(isPlaylistPlaying && isFetchingActiveTrack) || updating.value;
|
(isPlaylistPlaying && isFetchingActiveTrack) || updating.value;
|
||||||
var description = "${album.albumType} • ${album.artists.asString()}";
|
var description = "${album.albumType.name} • ${album.artists.asString()}";
|
||||||
|
|
||||||
void onTap() {
|
void onTap() {
|
||||||
context.navigateTo(AlbumRoute(id: album.id, album: album));
|
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/models/database/database.dart';
|
||||||
import 'package:spotube/modules/connect/connect_device.dart';
|
import 'package:spotube/modules/connect/connect_device.dart';
|
||||||
import 'package:spotube/modules/home/sections/featured.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/friends.dart';
|
||||||
import 'package:spotube/modules/home/sections/genres/genres.dart';
|
import 'package:spotube/modules/home/sections/genres/genres.dart';
|
||||||
import 'package:spotube/modules/home/sections/made_for_user.dart';
|
import 'package:spotube/modules/home/sections/made_for_user.dart';
|
||||||
@ -76,20 +76,19 @@ class HomePage extends HookConsumerWidget {
|
|||||||
else if (kIsMacOS)
|
else if (kIsMacOS)
|
||||||
const SliverGap(10),
|
const SliverGap(10),
|
||||||
const SliverGap(10),
|
const SliverGap(10),
|
||||||
SliverList.builder(
|
// SliverList.builder(
|
||||||
itemCount: 5,
|
// itemCount: 5,
|
||||||
itemBuilder: (context, index) {
|
// itemBuilder: (context, index) {
|
||||||
return switch (index) {
|
// return switch (index) {
|
||||||
0 => const HomeGenresSection(),
|
// 0 => const HomeGenresSection(),
|
||||||
1 => const HomeRecentlyPlayedSection(),
|
// 1 => const HomeRecentlyPlayedSection(),
|
||||||
2 => const HomeFeaturedSection(),
|
// 2 => const HomeFeaturedSection(),
|
||||||
3 => const HomePageFriendsSection(),
|
// 3 => const HomePageFriendsSection(),
|
||||||
_ => const HomeNewReleasesSection()
|
// _ => const HomeNewReleasesSection()
|
||||||
};
|
// };
|
||||||
},
|
// },
|
||||||
),
|
// ),
|
||||||
const HomePageFeedSection(),
|
const HomePageBrowseSection(),
|
||||||
const SliverSafeArea(sliver: HomeMadeForUserSection()),
|
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
));
|
));
|
||||||
|
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/endpoints/error.dart';
|
||||||
import 'package:spotube/services/metadata/metadata.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>
|
mixin MetadataPluginMixin<K>
|
||||||
// ignore: invalid_use_of_internal_member
|
// ignore: invalid_use_of_internal_member
|
||||||
on AsyncNotifierBase<SpotubePaginationResponseObject<K>> {
|
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/album.dart';
|
||||||
import 'package:spotube/services/metadata/endpoints/artist.dart';
|
import 'package:spotube/services/metadata/endpoints/artist.dart';
|
||||||
import 'package:spotube/services/metadata/endpoints/auth.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/playlist.dart';
|
||||||
import 'package:spotube/services/metadata/endpoints/user.dart';
|
import 'package:spotube/services/metadata/endpoints/user.dart';
|
||||||
|
|
||||||
@ -78,6 +79,7 @@ class MetadataPlugin {
|
|||||||
|
|
||||||
late final MetadataPluginAlbumEndpoint album;
|
late final MetadataPluginAlbumEndpoint album;
|
||||||
late final MetadataPluginArtistEndpoint artist;
|
late final MetadataPluginArtistEndpoint artist;
|
||||||
|
late final MetadataPluginBrowseEndpoint browse;
|
||||||
late final MetadataPluginPlaylistEndpoint playlist;
|
late final MetadataPluginPlaylistEndpoint playlist;
|
||||||
late final MetadataPluginUserEndpoint user;
|
late final MetadataPluginUserEndpoint user;
|
||||||
|
|
||||||
@ -86,6 +88,7 @@ class MetadataPlugin {
|
|||||||
|
|
||||||
artist = MetadataPluginArtistEndpoint(hetu);
|
artist = MetadataPluginArtistEndpoint(hetu);
|
||||||
album = MetadataPluginAlbumEndpoint(hetu);
|
album = MetadataPluginAlbumEndpoint(hetu);
|
||||||
|
browse = MetadataPluginBrowseEndpoint(hetu);
|
||||||
playlist = MetadataPluginPlaylistEndpoint(hetu);
|
playlist = MetadataPluginPlaylistEndpoint(hetu);
|
||||||
user = MetadataPluginUserEndpoint(hetu);
|
user = MetadataPluginUserEndpoint(hetu);
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user