✨ Better explore ever
This commit is contained in:
parent
07a86c32a0
commit
a95292a9ef
@ -15,6 +15,8 @@ PODS:
|
||||
- flutter_inappwebview_ios/Core (0.0.1):
|
||||
- Flutter
|
||||
- OrderedSet (~> 5.0)
|
||||
- flutter_native_splash (0.0.1):
|
||||
- Flutter
|
||||
- flutter_secure_storage (6.0.0):
|
||||
- Flutter
|
||||
- media_kit_libs_ios_audio (1.0.4):
|
||||
@ -61,6 +63,7 @@ DEPENDENCIES:
|
||||
- Flutter (from `Flutter`)
|
||||
- flutter_broadcasts (from `.symlinks/plugins/flutter_broadcasts/ios`)
|
||||
- flutter_inappwebview_ios (from `.symlinks/plugins/flutter_inappwebview_ios/ios`)
|
||||
- flutter_native_splash (from `.symlinks/plugins/flutter_native_splash/ios`)
|
||||
- flutter_secure_storage (from `.symlinks/plugins/flutter_secure_storage/ios`)
|
||||
- media_kit_libs_ios_audio (from `.symlinks/plugins/media_kit_libs_ios_audio/ios`)
|
||||
- media_kit_native_event_loop (from `.symlinks/plugins/media_kit_native_event_loop/ios`)
|
||||
@ -89,6 +92,8 @@ EXTERNAL SOURCES:
|
||||
:path: ".symlinks/plugins/flutter_broadcasts/ios"
|
||||
flutter_inappwebview_ios:
|
||||
:path: ".symlinks/plugins/flutter_inappwebview_ios/ios"
|
||||
flutter_native_splash:
|
||||
:path: ".symlinks/plugins/flutter_native_splash/ios"
|
||||
flutter_secure_storage:
|
||||
:path: ".symlinks/plugins/flutter_secure_storage/ios"
|
||||
media_kit_libs_ios_audio:
|
||||
@ -115,6 +120,7 @@ SPEC CHECKSUMS:
|
||||
Flutter: e0871f40cf51350855a761d2e70bf5af5b9b5de7
|
||||
flutter_broadcasts: 3ece15b27d8ccbe2132c3df303e7c3401feab882
|
||||
flutter_inappwebview_ios: 97215cf7d4677db55df76782dbd2930c5e1c1ea0
|
||||
flutter_native_splash: edf599c81f74d093a4daf8e17bd7a018854bc778
|
||||
flutter_secure_storage: d33dac7ae2ea08509be337e775f6b59f1ff45f12
|
||||
media_kit_libs_ios_audio: 8f39d96a9c630685dfb844c289bd1d114c486fb3
|
||||
media_kit_native_event_loop: 99111eded5acbdc9c2738021ea6550dd36ca8837
|
||||
|
@ -1,12 +1,16 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:gap/gap.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
import 'package:rhythm_box/providers/auth.dart';
|
||||
import 'package:rhythm_box/providers/recent_played.dart';
|
||||
import 'package:rhythm_box/providers/spotify.dart';
|
||||
import 'package:rhythm_box/providers/user_preferences.dart';
|
||||
import 'package:rhythm_box/services/album.dart';
|
||||
import 'package:rhythm_box/services/database/database.dart';
|
||||
import 'package:rhythm_box/services/spotify/spotify_endpoints.dart';
|
||||
import 'package:rhythm_box/widgets/playlist/playlist_section.dart';
|
||||
import 'package:spotify/spotify.dart';
|
||||
|
||||
class ExploreScreen extends StatefulWidget {
|
||||
const ExploreScreen({super.key});
|
||||
@ -18,19 +22,23 @@ class ExploreScreen extends StatefulWidget {
|
||||
class _ExploreScreenState extends State<ExploreScreen> {
|
||||
late final SpotifyProvider _spotify = Get.find();
|
||||
late final RecentlyPlayedProvider _history = Get.find();
|
||||
late final AuthenticationProvider _auth = Get.find();
|
||||
|
||||
final Map<String, bool> _isLoading = {
|
||||
'featured': true,
|
||||
'recently': true,
|
||||
'newReleases': true,
|
||||
'forYou': true,
|
||||
};
|
||||
|
||||
List<Object>? _featuredPlaylist;
|
||||
List<Object>? _recentlyPlaylist;
|
||||
List<Object>? _newReleasesPlaylist;
|
||||
List<dynamic>? _forYouView;
|
||||
|
||||
Future<void> _pullPlaylist() async {
|
||||
final market = Get.find<UserPreferencesProvider>().state.value.market;
|
||||
final locale = Get.find<UserPreferencesProvider>().state.value.locale;
|
||||
|
||||
_featuredPlaylist =
|
||||
(await _spotify.api.playlists.featured.getPage(20)).items!.toList();
|
||||
@ -48,6 +56,16 @@ class _ExploreScreenState extends State<ExploreScreen> {
|
||||
?.map((album) => album.toAlbum())
|
||||
.toList();
|
||||
setState(() => _isLoading['newReleases'] = false);
|
||||
|
||||
final customEndpoint =
|
||||
CustomSpotifyEndpoints(_auth.auth.value?.accessToken.value ?? '');
|
||||
final forYouView = await customEndpoint.getView(
|
||||
'made-for-x-hub',
|
||||
market: market,
|
||||
locale: Intl.canonicalizedLocale(locale.toString()),
|
||||
);
|
||||
_forYouView = forYouView['content']?['items'];
|
||||
setState(() => _isLoading['forYou'] = false);
|
||||
}
|
||||
|
||||
@override
|
||||
@ -65,28 +83,52 @@ class _ExploreScreenState extends State<ExploreScreen> {
|
||||
title: Text('explore'.tr),
|
||||
centerTitle: MediaQuery.of(context).size.width >= 720,
|
||||
),
|
||||
body: ListView(
|
||||
children: [
|
||||
body: CustomScrollView(
|
||||
slivers: [
|
||||
if (_newReleasesPlaylist?.isNotEmpty ?? false)
|
||||
PlaylistSection(
|
||||
SliverToBoxAdapter(
|
||||
child: PlaylistSection(
|
||||
isLoading: _isLoading['newReleases']!,
|
||||
title: 'New Releases',
|
||||
list: _newReleasesPlaylist,
|
||||
),
|
||||
if (_newReleasesPlaylist?.isNotEmpty ?? false) const Gap(16),
|
||||
),
|
||||
if (_newReleasesPlaylist?.isNotEmpty ?? false) const SliverGap(16),
|
||||
if (_recentlyPlaylist?.isNotEmpty ?? false)
|
||||
PlaylistSection(
|
||||
SliverToBoxAdapter(
|
||||
child: PlaylistSection(
|
||||
isLoading: _isLoading['recently']!,
|
||||
title: 'Recent Played',
|
||||
list: _recentlyPlaylist,
|
||||
),
|
||||
if (_recentlyPlaylist?.isNotEmpty ?? false) const Gap(16),
|
||||
PlaylistSection(
|
||||
),
|
||||
if (_recentlyPlaylist?.isNotEmpty ?? false) const SliverGap(16),
|
||||
SliverList.builder(
|
||||
itemCount: _forYouView?.length ?? 0,
|
||||
itemBuilder: (context, idx) {
|
||||
final item = _forYouView![idx];
|
||||
final playlists = item['content']?['items']
|
||||
?.where((itemL2) => itemL2['type'] == 'playlist')
|
||||
.map((itemL2) => PlaylistSimple.fromJson(itemL2))
|
||||
.toList()
|
||||
.cast<PlaylistSimple>() ??
|
||||
<PlaylistSimple>[];
|
||||
if (playlists.isEmpty) return const SizedBox.shrink();
|
||||
return PlaylistSection(
|
||||
isLoading: false,
|
||||
title: item['name'] ?? '',
|
||||
list: playlists,
|
||||
).paddingOnly(bottom: 16);
|
||||
},
|
||||
),
|
||||
SliverToBoxAdapter(
|
||||
child: PlaylistSection(
|
||||
isLoading: _isLoading['featured']!,
|
||||
title: 'Featured',
|
||||
list: _featuredPlaylist,
|
||||
),
|
||||
const Gap(16),
|
||||
),
|
||||
const SliverGap(16),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
21
lib/services/song_link/song_link.freezed.dart
Executable file → Normal file
21
lib/services/song_link/song_link.freezed.dart
Executable file → Normal file
@ -30,8 +30,12 @@ mixin _$SongLink {
|
||||
String? get nativeAppUriMobile => throw _privateConstructorUsedError;
|
||||
String? get nativeAppUriDesktop => throw _privateConstructorUsedError;
|
||||
|
||||
/// Serializes this SongLink to a JSON map.
|
||||
Map<String, dynamic> toJson() => throw _privateConstructorUsedError;
|
||||
@JsonKey(ignore: true)
|
||||
|
||||
/// Create a copy of SongLink
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
$SongLinkCopyWith<SongLink> get copyWith =>
|
||||
throw _privateConstructorUsedError;
|
||||
}
|
||||
@ -63,6 +67,8 @@ class _$SongLinkCopyWithImpl<$Res, $Val extends SongLink>
|
||||
// ignore: unused_field
|
||||
final $Res Function($Val) _then;
|
||||
|
||||
/// Create a copy of SongLink
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@pragma('vm:prefer-inline')
|
||||
@override
|
||||
$Res call({
|
||||
@ -145,6 +151,8 @@ class __$$SongLinkImplCopyWithImpl<$Res>
|
||||
_$SongLinkImpl _value, $Res Function(_$SongLinkImpl) _then)
|
||||
: super(_value, _then);
|
||||
|
||||
/// Create a copy of SongLink
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@pragma('vm:prefer-inline')
|
||||
@override
|
||||
$Res call({
|
||||
@ -261,12 +269,14 @@ class _$SongLinkImpl implements _SongLink {
|
||||
other.nativeAppUriDesktop == nativeAppUriDesktop));
|
||||
}
|
||||
|
||||
@JsonKey(ignore: true)
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@override
|
||||
int get hashCode => Object.hash(runtimeType, displayName, linkId, platform,
|
||||
show, uniqueId, country, url, nativeAppUriMobile, nativeAppUriDesktop);
|
||||
|
||||
@JsonKey(ignore: true)
|
||||
/// Create a copy of SongLink
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@override
|
||||
@pragma('vm:prefer-inline')
|
||||
_$$SongLinkImplCopyWith<_$SongLinkImpl> get copyWith =>
|
||||
@ -313,8 +323,11 @@ abstract class _SongLink implements SongLink {
|
||||
String? get nativeAppUriMobile;
|
||||
@override
|
||||
String? get nativeAppUriDesktop;
|
||||
|
||||
/// Create a copy of SongLink
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@override
|
||||
@JsonKey(ignore: true)
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
_$$SongLinkImplCopyWith<_$SongLinkImpl> get copyWith =>
|
||||
throw _privateConstructorUsedError;
|
||||
}
|
||||
|
214
lib/services/spotify/spotify_endpoints.dart
Normal file
214
lib/services/spotify/spotify_endpoints.dart
Normal file
@ -0,0 +1,214 @@
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:dio/dio.dart';
|
||||
import 'package:rhythm_box/services/spotify/spotify_feed.dart';
|
||||
import 'package:spotify/spotify.dart';
|
||||
import 'package:timezone/timezone.dart' as tz;
|
||||
|
||||
class CustomSpotifyEndpoints {
|
||||
static const _baseUrl = 'https://api.spotify.com/v1';
|
||||
final String accessToken;
|
||||
final Dio _client;
|
||||
|
||||
CustomSpotifyEndpoints(this.accessToken)
|
||||
: _client = Dio(
|
||||
BaseOptions(
|
||||
baseUrl: _baseUrl,
|
||||
responseType: ResponseType.json,
|
||||
headers: {
|
||||
'content-type': 'application/json',
|
||||
if (accessToken.isNotEmpty)
|
||||
'authorization': 'Bearer $accessToken',
|
||||
'accept': 'application/json',
|
||||
},
|
||||
),
|
||||
);
|
||||
|
||||
// views API
|
||||
|
||||
/// Get a single view of given genre
|
||||
///
|
||||
/// Currently known genres are:
|
||||
/// - new-releases-page
|
||||
/// - made-for-x-hub (it requires authentication)
|
||||
/// - my-mix-genres (it requires authentication)
|
||||
/// - artist-seed-mixes (it requires authentication)
|
||||
/// - my-mix-decades (it requires authentication)
|
||||
/// - my-mix-moods (it requires authentication)
|
||||
/// - podcasts-and-more (it requires authentication)
|
||||
/// - uniquely-yours-in-hub (it requires authentication)
|
||||
/// - made-for-x-dailymix (it requires authentication)
|
||||
/// - made-for-x-discovery (it requires authentication)
|
||||
Future<Map<String, dynamic>> getView(
|
||||
String view, {
|
||||
int limit = 20,
|
||||
int contentLimit = 10,
|
||||
List<String> types = const [
|
||||
'album',
|
||||
'playlist',
|
||||
'artist',
|
||||
'show',
|
||||
'station',
|
||||
'episode',
|
||||
'merch',
|
||||
'artist_concerts',
|
||||
'uri_link'
|
||||
],
|
||||
String imageStyle = 'gradient_overlay',
|
||||
String includeExternal = 'audio',
|
||||
String? locale,
|
||||
Market? market,
|
||||
Market? country,
|
||||
}) async {
|
||||
if (accessToken.isEmpty) {
|
||||
throw Exception('[CustomSpotifyEndpoints.getView]: accessToken is empty');
|
||||
}
|
||||
|
||||
final queryParams = {
|
||||
'limit': limit.toString(),
|
||||
'content_limit': contentLimit.toString(),
|
||||
'types': types.join(','),
|
||||
'image_style': imageStyle,
|
||||
'include_external': includeExternal,
|
||||
'timestamp': DateTime.now().toUtc().toIso8601String(),
|
||||
if (locale != null) 'locale': locale,
|
||||
if (market != null) 'market': market.name,
|
||||
if (country != null) 'country': country.name,
|
||||
}.entries.map((e) => '${e.key}=${e.value}').join('&');
|
||||
|
||||
final res = await _client.getUri(
|
||||
Uri.parse('$_baseUrl/views/$view?$queryParams'),
|
||||
);
|
||||
|
||||
if (res.statusCode == 200) {
|
||||
return res.data;
|
||||
} else {
|
||||
throw Exception(
|
||||
'[CustomSpotifyEndpoints.getView]: Failed to get view'
|
||||
'\nStatus code: ${res.statusCode}'
|
||||
'\nBody: ${res.data}',
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Future<List<String>> listGenreSeeds() async {
|
||||
final res = await _client.getUri(
|
||||
Uri.parse('$_baseUrl/recommendations/available-genre-seeds'),
|
||||
);
|
||||
|
||||
if (res.statusCode == 200) {
|
||||
final body = res.data;
|
||||
return List<String>.from(body['genres'] ?? []);
|
||||
} else {
|
||||
throw Exception(
|
||||
'[CustomSpotifyEndpoints.listGenreSeeds]: Failed to get genre seeds'
|
||||
'\nStatus code: ${res.statusCode}'
|
||||
'\nBody: ${res.data}',
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Future<SpotifyHomeFeed> getHomeFeed({
|
||||
required String spTCookie,
|
||||
required Market country,
|
||||
}) async {
|
||||
final headers = {
|
||||
'app-platform': 'WebPlayer',
|
||||
'authorization': 'Bearer $accessToken',
|
||||
'content-type': 'application/json;charset=UTF-8',
|
||||
'dnt': '1',
|
||||
'origin': 'https://open.spotify.com',
|
||||
'referer': 'https://open.spotify.com/'
|
||||
};
|
||||
final response = await _client.getUri(
|
||||
Uri(
|
||||
scheme: 'https',
|
||||
host: 'api-partner.spotify.com',
|
||||
path: '/pathfinder/v1/query',
|
||||
queryParameters: {
|
||||
'operationName': 'home',
|
||||
'variables': jsonEncode({
|
||||
'timeZone': tz.local.name,
|
||||
'sp_t': spTCookie,
|
||||
'country': country.name,
|
||||
'facet': null,
|
||||
'sectionItemsLimit': 10
|
||||
}),
|
||||
'extensions': jsonEncode(
|
||||
{
|
||||
'persistedQuery': {
|
||||
'version': 1,
|
||||
|
||||
/// GraphQL persisted Query hash
|
||||
/// This can change overtime. We've to lookout for it
|
||||
/// Docs: https://www.apollographql.com/docs/graphos/operations/persisted-queries/
|
||||
'sha256Hash':
|
||||
'eb3fba2d388cf4fc4d696b1757a58584e9538a3b515ea742e9cc9465807340be',
|
||||
}
|
||||
},
|
||||
),
|
||||
},
|
||||
),
|
||||
options: Options(headers: headers),
|
||||
);
|
||||
|
||||
final data = SpotifyHomeFeed.fromJson(
|
||||
transformHomeFeedJsonMap(response.data),
|
||||
);
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
Future<SpotifyHomeFeedSection> getHomeFeedSection(
|
||||
String sectionUri, {
|
||||
required String spTCookie,
|
||||
required Market country,
|
||||
}) async {
|
||||
final headers = {
|
||||
'app-platform': 'WebPlayer',
|
||||
'authorization': 'Bearer $accessToken',
|
||||
'content-type': 'application/json;charset=UTF-8',
|
||||
'dnt': '1',
|
||||
'origin': 'https://open.spotify.com',
|
||||
'referer': 'https://open.spotify.com/'
|
||||
};
|
||||
final response = await _client.getUri(
|
||||
Uri(
|
||||
scheme: 'https',
|
||||
host: 'api-partner.spotify.com',
|
||||
path: '/pathfinder/v1/query',
|
||||
queryParameters: {
|
||||
'operationName': 'homeSection',
|
||||
'variables': jsonEncode({
|
||||
'timeZone': tz.local.name,
|
||||
'sp_t': spTCookie,
|
||||
'country': country.name,
|
||||
'uri': sectionUri
|
||||
}),
|
||||
'extensions': jsonEncode(
|
||||
{
|
||||
'persistedQuery': {
|
||||
'version': 1,
|
||||
|
||||
/// GraphQL persisted Query hash
|
||||
/// This can change overtime. We've to lookout for it
|
||||
/// Docs: https://www.apollographql.com/docs/graphos/operations/persisted-queries/
|
||||
'sha256Hash':
|
||||
'eb3fba2d388cf4fc4d696b1757a58584e9538a3b515ea742e9cc9465807340be',
|
||||
}
|
||||
},
|
||||
),
|
||||
},
|
||||
),
|
||||
options: Options(headers: headers),
|
||||
);
|
||||
|
||||
final data = SpotifyHomeFeedSection.fromJson(
|
||||
transformSectionItemJsonMap(
|
||||
response.data['data']['homeSections']['sections'][0],
|
||||
),
|
||||
);
|
||||
|
||||
return data;
|
||||
}
|
||||
}
|
247
lib/services/spotify/spotify_feed.dart
Normal file
247
lib/services/spotify/spotify_feed.dart
Normal file
@ -0,0 +1,247 @@
|
||||
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||
import 'package:spotify/spotify.dart';
|
||||
|
||||
part 'spotify_feed.freezed.dart';
|
||||
part 'spotify_feed.g.dart';
|
||||
|
||||
@freezed
|
||||
class SpotifySectionPlaylist with _$SpotifySectionPlaylist {
|
||||
const SpotifySectionPlaylist._();
|
||||
|
||||
const factory SpotifySectionPlaylist({
|
||||
required String description,
|
||||
required String format,
|
||||
required List<SpotifySectionItemImage> images,
|
||||
required String name,
|
||||
required String owner,
|
||||
required String uri,
|
||||
}) = _SpotifySectionPlaylist;
|
||||
|
||||
factory SpotifySectionPlaylist.fromJson(Map<String, dynamic> json) =>
|
||||
_$SpotifySectionPlaylistFromJson(json);
|
||||
|
||||
String get id => uri.split(':').last;
|
||||
|
||||
Playlist get asPlaylist {
|
||||
return Playlist()
|
||||
..id = id
|
||||
..name = name
|
||||
..description = description
|
||||
..collaborative = false
|
||||
..images = images.map((e) => e.asImage).toList()
|
||||
..owner = (User()..displayName = 'Spotify')
|
||||
..uri = uri
|
||||
..type = 'playlist';
|
||||
}
|
||||
}
|
||||
|
||||
@freezed
|
||||
class SpotifySectionArtist with _$SpotifySectionArtist {
|
||||
const SpotifySectionArtist._();
|
||||
|
||||
const factory SpotifySectionArtist({
|
||||
required String name,
|
||||
required String uri,
|
||||
required List<SpotifySectionItemImage> images,
|
||||
}) = _SpotifySectionArtist;
|
||||
|
||||
factory SpotifySectionArtist.fromJson(Map<String, dynamic> json) =>
|
||||
_$SpotifySectionArtistFromJson(json);
|
||||
|
||||
String get id => uri.split(':').last;
|
||||
|
||||
Artist get asArtist {
|
||||
return Artist()
|
||||
..id = id
|
||||
..name = name
|
||||
..images = images.map((e) => e.asImage).toList()
|
||||
..type = 'artist'
|
||||
..uri = uri;
|
||||
}
|
||||
}
|
||||
|
||||
@freezed
|
||||
class SpotifySectionAlbum with _$SpotifySectionAlbum {
|
||||
const SpotifySectionAlbum._();
|
||||
|
||||
const factory SpotifySectionAlbum({
|
||||
required List<SpotifySectionAlbumArtist> artists,
|
||||
required List<SpotifySectionItemImage> images,
|
||||
required String name,
|
||||
required String uri,
|
||||
}) = _SpotifySectionAlbum;
|
||||
|
||||
factory SpotifySectionAlbum.fromJson(Map<String, dynamic> json) =>
|
||||
_$SpotifySectionAlbumFromJson(json);
|
||||
|
||||
String get id => uri.split(':').last;
|
||||
|
||||
Album get asAlbum {
|
||||
return Album()
|
||||
..id = id
|
||||
..name = name
|
||||
..artists = artists.map((a) => a.asArtist).toList()
|
||||
..albumType = AlbumType.album
|
||||
..images = images.map((e) => e.asImage).toList()
|
||||
..uri = uri;
|
||||
}
|
||||
}
|
||||
|
||||
@freezed
|
||||
class SpotifySectionAlbumArtist with _$SpotifySectionAlbumArtist {
|
||||
const SpotifySectionAlbumArtist._();
|
||||
|
||||
const factory SpotifySectionAlbumArtist({
|
||||
required String name,
|
||||
required String uri,
|
||||
}) = _SpotifySectionAlbumArtist;
|
||||
|
||||
factory SpotifySectionAlbumArtist.fromJson(Map<String, dynamic> json) =>
|
||||
_$SpotifySectionAlbumArtistFromJson(json);
|
||||
|
||||
String get id => uri.split(':').last;
|
||||
|
||||
Artist get asArtist {
|
||||
return Artist()
|
||||
..id = id
|
||||
..name = name
|
||||
..type = 'artist'
|
||||
..uri = uri;
|
||||
}
|
||||
}
|
||||
|
||||
@freezed
|
||||
class SpotifySectionItemImage with _$SpotifySectionItemImage {
|
||||
const SpotifySectionItemImage._();
|
||||
|
||||
const factory SpotifySectionItemImage({
|
||||
required num? height,
|
||||
required String url,
|
||||
required num? width,
|
||||
}) = _SpotifySectionItemImage;
|
||||
|
||||
factory SpotifySectionItemImage.fromJson(Map<String, dynamic> json) =>
|
||||
_$SpotifySectionItemImageFromJson(json);
|
||||
|
||||
Image get asImage {
|
||||
return Image()
|
||||
..height = height?.toInt()
|
||||
..width = width?.toInt()
|
||||
..url = url;
|
||||
}
|
||||
}
|
||||
|
||||
@freezed
|
||||
class SpotifyHomeFeedSectionItem with _$SpotifyHomeFeedSectionItem {
|
||||
factory SpotifyHomeFeedSectionItem({
|
||||
required String typename,
|
||||
SpotifySectionPlaylist? playlist,
|
||||
SpotifySectionArtist? artist,
|
||||
SpotifySectionAlbum? album,
|
||||
}) = _SpotifyHomeFeedSectionItem;
|
||||
|
||||
factory SpotifyHomeFeedSectionItem.fromJson(Map<String, dynamic> json) =>
|
||||
_$SpotifyHomeFeedSectionItemFromJson(json);
|
||||
}
|
||||
|
||||
@freezed
|
||||
class SpotifyHomeFeedSection with _$SpotifyHomeFeedSection {
|
||||
factory SpotifyHomeFeedSection({
|
||||
required String typename,
|
||||
String? title,
|
||||
required String uri,
|
||||
required List<SpotifyHomeFeedSectionItem> items,
|
||||
}) = _SpotifyHomeFeedSection;
|
||||
|
||||
factory SpotifyHomeFeedSection.fromJson(Map<String, dynamic> json) =>
|
||||
_$SpotifyHomeFeedSectionFromJson(json);
|
||||
}
|
||||
|
||||
@freezed
|
||||
class SpotifyHomeFeed with _$SpotifyHomeFeed {
|
||||
factory SpotifyHomeFeed({
|
||||
required String greeting,
|
||||
required List<SpotifyHomeFeedSection> sections,
|
||||
}) = _SpotifyHomeFeed;
|
||||
|
||||
factory SpotifyHomeFeed.fromJson(Map<String, dynamic> json) =>
|
||||
_$SpotifyHomeFeedFromJson(json);
|
||||
}
|
||||
|
||||
Map<String, dynamic> transformSectionItemTypeJsonMap(
|
||||
Map<String, dynamic> json) {
|
||||
final data = json['content']['data'];
|
||||
final objType = json['content']['data']['__typename'];
|
||||
return {
|
||||
'typename': json['content']['__typename'],
|
||||
if (objType == 'Playlist')
|
||||
'playlist': {
|
||||
'name': data['name'],
|
||||
'description': data['description'],
|
||||
'format': data['format'],
|
||||
'images': (data['images']['items'] as List)
|
||||
.expand((j) => j['sources'] as dynamic)
|
||||
.toList()
|
||||
.cast<Map<String, dynamic>>(),
|
||||
'owner': data['ownerV2']['data']['name'],
|
||||
'uri': data['uri']
|
||||
},
|
||||
if (objType == 'Artist')
|
||||
'artist': {
|
||||
'name': data['profile']['name'],
|
||||
'uri': data['uri'],
|
||||
'images': data['visuals']['avatarImage']['sources'],
|
||||
},
|
||||
if (objType == 'Album')
|
||||
'album': {
|
||||
'name': data['name'],
|
||||
'uri': data['uri'],
|
||||
'images': data['coverArt']['sources'],
|
||||
'artists': data['artists']['items']
|
||||
.map(
|
||||
(artist) => {
|
||||
'name': artist['profile']['name'],
|
||||
'uri': artist['uri'],
|
||||
},
|
||||
)
|
||||
.toList()
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
Map<String, dynamic> transformSectionItemJsonMap(Map<String, dynamic> json) {
|
||||
return {
|
||||
'typename': json['data']['__typename'],
|
||||
'title': json['data']?['title']?['text'],
|
||||
'uri': json['uri'],
|
||||
'items': (json['sectionItems']['items'] as List)
|
||||
.map(
|
||||
(data) =>
|
||||
transformSectionItemTypeJsonMap(data as Map<String, dynamic>)
|
||||
as dynamic,
|
||||
)
|
||||
.where(
|
||||
(w) =>
|
||||
w['playlist'] != null ||
|
||||
w['artist'] != null ||
|
||||
w['album'] != null,
|
||||
)
|
||||
.toList()
|
||||
.cast<Map<String, dynamic>>()
|
||||
};
|
||||
}
|
||||
|
||||
Map<String, dynamic> transformHomeFeedJsonMap(Map<String, dynamic> json) {
|
||||
return {
|
||||
'greeting': json['data']['home']['greeting']['text'],
|
||||
'sections':
|
||||
(json['data']['home']['sectionContainer']['sections']['items'] as List)
|
||||
.map(
|
||||
(item) =>
|
||||
transformSectionItemJsonMap(item as Map<String, dynamic>)
|
||||
as dynamic,
|
||||
)
|
||||
.toList()
|
||||
.cast<Map<String, dynamic>>()
|
||||
};
|
||||
}
|
1776
lib/services/spotify/spotify_feed.freezed.dart
Normal file
1776
lib/services/spotify/spotify_feed.freezed.dart
Normal file
File diff suppressed because it is too large
Load Diff
169
lib/services/spotify/spotify_feed.g.dart
Normal file
169
lib/services/spotify/spotify_feed.g.dart
Normal file
@ -0,0 +1,169 @@
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'spotify_feed.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// JsonSerializableGenerator
|
||||
// **************************************************************************
|
||||
|
||||
_$SpotifySectionPlaylistImpl _$$SpotifySectionPlaylistImplFromJson(
|
||||
Map<String, dynamic> json) =>
|
||||
_$SpotifySectionPlaylistImpl(
|
||||
description: json['description'] as String,
|
||||
format: json['format'] as String,
|
||||
images: (json['images'] as List<dynamic>)
|
||||
.map((e) =>
|
||||
SpotifySectionItemImage.fromJson(e as Map<String, dynamic>))
|
||||
.toList(),
|
||||
name: json['name'] as String,
|
||||
owner: json['owner'] as String,
|
||||
uri: json['uri'] as String,
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$$SpotifySectionPlaylistImplToJson(
|
||||
_$SpotifySectionPlaylistImpl instance) =>
|
||||
<String, dynamic>{
|
||||
'description': instance.description,
|
||||
'format': instance.format,
|
||||
'images': instance.images,
|
||||
'name': instance.name,
|
||||
'owner': instance.owner,
|
||||
'uri': instance.uri,
|
||||
};
|
||||
|
||||
_$SpotifySectionArtistImpl _$$SpotifySectionArtistImplFromJson(
|
||||
Map<String, dynamic> json) =>
|
||||
_$SpotifySectionArtistImpl(
|
||||
name: json['name'] as String,
|
||||
uri: json['uri'] as String,
|
||||
images: (json['images'] as List<dynamic>)
|
||||
.map((e) =>
|
||||
SpotifySectionItemImage.fromJson(e as Map<String, dynamic>))
|
||||
.toList(),
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$$SpotifySectionArtistImplToJson(
|
||||
_$SpotifySectionArtistImpl instance) =>
|
||||
<String, dynamic>{
|
||||
'name': instance.name,
|
||||
'uri': instance.uri,
|
||||
'images': instance.images,
|
||||
};
|
||||
|
||||
_$SpotifySectionAlbumImpl _$$SpotifySectionAlbumImplFromJson(
|
||||
Map<String, dynamic> json) =>
|
||||
_$SpotifySectionAlbumImpl(
|
||||
artists: (json['artists'] as List<dynamic>)
|
||||
.map((e) =>
|
||||
SpotifySectionAlbumArtist.fromJson(e as Map<String, dynamic>))
|
||||
.toList(),
|
||||
images: (json['images'] as List<dynamic>)
|
||||
.map((e) =>
|
||||
SpotifySectionItemImage.fromJson(e as Map<String, dynamic>))
|
||||
.toList(),
|
||||
name: json['name'] as String,
|
||||
uri: json['uri'] as String,
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$$SpotifySectionAlbumImplToJson(
|
||||
_$SpotifySectionAlbumImpl instance) =>
|
||||
<String, dynamic>{
|
||||
'artists': instance.artists,
|
||||
'images': instance.images,
|
||||
'name': instance.name,
|
||||
'uri': instance.uri,
|
||||
};
|
||||
|
||||
_$SpotifySectionAlbumArtistImpl _$$SpotifySectionAlbumArtistImplFromJson(
|
||||
Map<String, dynamic> json) =>
|
||||
_$SpotifySectionAlbumArtistImpl(
|
||||
name: json['name'] as String,
|
||||
uri: json['uri'] as String,
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$$SpotifySectionAlbumArtistImplToJson(
|
||||
_$SpotifySectionAlbumArtistImpl instance) =>
|
||||
<String, dynamic>{
|
||||
'name': instance.name,
|
||||
'uri': instance.uri,
|
||||
};
|
||||
|
||||
_$SpotifySectionItemImageImpl _$$SpotifySectionItemImageImplFromJson(
|
||||
Map<String, dynamic> json) =>
|
||||
_$SpotifySectionItemImageImpl(
|
||||
height: json['height'] as num?,
|
||||
url: json['url'] as String,
|
||||
width: json['width'] as num?,
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$$SpotifySectionItemImageImplToJson(
|
||||
_$SpotifySectionItemImageImpl instance) =>
|
||||
<String, dynamic>{
|
||||
'height': instance.height,
|
||||
'url': instance.url,
|
||||
'width': instance.width,
|
||||
};
|
||||
|
||||
_$SpotifyHomeFeedSectionItemImpl _$$SpotifyHomeFeedSectionItemImplFromJson(
|
||||
Map<String, dynamic> json) =>
|
||||
_$SpotifyHomeFeedSectionItemImpl(
|
||||
typename: json['typename'] as String,
|
||||
playlist: json['playlist'] == null
|
||||
? null
|
||||
: SpotifySectionPlaylist.fromJson(
|
||||
json['playlist'] as Map<String, dynamic>),
|
||||
artist: json['artist'] == null
|
||||
? null
|
||||
: SpotifySectionArtist.fromJson(
|
||||
json['artist'] as Map<String, dynamic>),
|
||||
album: json['album'] == null
|
||||
? null
|
||||
: SpotifySectionAlbum.fromJson(json['album'] as Map<String, dynamic>),
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$$SpotifyHomeFeedSectionItemImplToJson(
|
||||
_$SpotifyHomeFeedSectionItemImpl instance) =>
|
||||
<String, dynamic>{
|
||||
'typename': instance.typename,
|
||||
'playlist': instance.playlist,
|
||||
'artist': instance.artist,
|
||||
'album': instance.album,
|
||||
};
|
||||
|
||||
_$SpotifyHomeFeedSectionImpl _$$SpotifyHomeFeedSectionImplFromJson(
|
||||
Map<String, dynamic> json) =>
|
||||
_$SpotifyHomeFeedSectionImpl(
|
||||
typename: json['typename'] as String,
|
||||
title: json['title'] as String?,
|
||||
uri: json['uri'] as String,
|
||||
items: (json['items'] as List<dynamic>)
|
||||
.map((e) =>
|
||||
SpotifyHomeFeedSectionItem.fromJson(e as Map<String, dynamic>))
|
||||
.toList(),
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$$SpotifyHomeFeedSectionImplToJson(
|
||||
_$SpotifyHomeFeedSectionImpl instance) =>
|
||||
<String, dynamic>{
|
||||
'typename': instance.typename,
|
||||
'title': instance.title,
|
||||
'uri': instance.uri,
|
||||
'items': instance.items,
|
||||
};
|
||||
|
||||
_$SpotifyHomeFeedImpl _$$SpotifyHomeFeedImplFromJson(
|
||||
Map<String, dynamic> json) =>
|
||||
_$SpotifyHomeFeedImpl(
|
||||
greeting: json['greeting'] as String,
|
||||
sections: (json['sections'] as List<dynamic>)
|
||||
.map(
|
||||
(e) => SpotifyHomeFeedSection.fromJson(e as Map<String, dynamic>))
|
||||
.toList(),
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$$SpotifyHomeFeedImplToJson(
|
||||
_$SpotifyHomeFeedImpl instance) =>
|
||||
<String, dynamic>{
|
||||
'greeting': instance.greeting,
|
||||
'sections': instance.sections,
|
||||
};
|
@ -44,7 +44,7 @@ class AlbumCard extends StatelessWidget {
|
||||
],
|
||||
).paddingSymmetric(horizontal: 8),
|
||||
onTap: () {
|
||||
if (onTap != null) return;
|
||||
if (onTap == null) return;
|
||||
onTap!();
|
||||
},
|
||||
),
|
||||
|
@ -43,7 +43,7 @@ class PlaylistCard extends StatelessWidget {
|
||||
],
|
||||
).paddingSymmetric(horizontal: 8),
|
||||
onTap: () {
|
||||
if (onTap != null) return;
|
||||
if (onTap == null) return;
|
||||
onTap!();
|
||||
},
|
||||
),
|
||||
|
18
pubspec.lock
18
pubspec.lock
@ -614,6 +614,14 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.0.0"
|
||||
freezed:
|
||||
dependency: "direct dev"
|
||||
description:
|
||||
name: freezed
|
||||
sha256: "44c19278dd9d89292cf46e97dc0c1e52ce03275f40a97c5a348e802a924bf40e"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.5.7"
|
||||
freezed_annotation:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
@ -759,7 +767,7 @@ packages:
|
||||
source: hosted
|
||||
version: "4.9.0"
|
||||
json_serializable:
|
||||
dependency: "direct main"
|
||||
dependency: "direct dev"
|
||||
description:
|
||||
name: json_serializable
|
||||
sha256: ea1432d167339ea9b5bb153f0571d0039607a873d6e04e0117af043f14a1fd4b
|
||||
@ -1382,6 +1390,14 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.7.2"
|
||||
timezone:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: timezone
|
||||
sha256: "2236ec079a174ce07434e89fcd3fcda430025eb7692244139a9cf54fdcf1fc7d"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.9.4"
|
||||
timing:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
@ -78,7 +78,6 @@ dependencies:
|
||||
flutter_secure_storage: ^9.2.2
|
||||
window_manager: ^0.4.2
|
||||
lrc: ^1.0.2
|
||||
json_serializable: ^6.8.0
|
||||
path: ^1.9.0
|
||||
path_provider: ^2.1.4
|
||||
sqlite3_flutter_libs: ^0.5.23
|
||||
@ -100,6 +99,7 @@ dependencies:
|
||||
ref: feat/cookies
|
||||
path: packages/desktop_webview_window
|
||||
flutter_inappwebview: ^6.0.0
|
||||
timezone: ^0.9.4
|
||||
|
||||
dev_dependencies:
|
||||
flutter_test:
|
||||
@ -115,6 +115,8 @@ dev_dependencies:
|
||||
build_runner: ^2.4.12
|
||||
flutter_launcher_icons: ^0.13.1
|
||||
flutter_native_splash: ^2.4.1
|
||||
freezed: ^2.5.7
|
||||
json_serializable: ^6.8.0
|
||||
|
||||
# For information on the generic Dart part of this file, see the
|
||||
# following page: https://dart.dev/tools/pub/pubspec
|
||||
|
Loading…
Reference in New Issue
Block a user