Better explore ever

This commit is contained in:
LittleSheep 2024-08-30 13:43:29 +08:00
parent 07a86c32a0
commit a95292a9ef
11 changed files with 2510 additions and 25 deletions

View File

@ -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

View File

@ -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(
isLoading: _isLoading['newReleases']!,
title: 'New Releases',
list: _newReleasesPlaylist,
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(
isLoading: _isLoading['recently']!,
title: 'Recent Played',
list: _recentlyPlaylist,
SliverToBoxAdapter(
child: PlaylistSection(
isLoading: _isLoading['recently']!,
title: 'Recent Played',
list: _recentlyPlaylist,
),
),
if (_recentlyPlaylist?.isNotEmpty ?? false) const Gap(16),
PlaylistSection(
isLoading: _isLoading['featured']!,
title: 'Featured',
list: _featuredPlaylist,
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);
},
),
const Gap(16),
SliverToBoxAdapter(
child: PlaylistSection(
isLoading: _isLoading['featured']!,
title: 'Featured',
list: _featuredPlaylist,
),
),
const SliverGap(16),
],
),
),

21
lib/services/song_link/song_link.freezed.dart Executable file → Normal file
View 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;
}

View 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;
}
}

View 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>>()
};
}

File diff suppressed because it is too large Load Diff

View 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,
};

View File

@ -44,7 +44,7 @@ class AlbumCard extends StatelessWidget {
],
).paddingSymmetric(horizontal: 8),
onTap: () {
if (onTap != null) return;
if (onTap == null) return;
onTap!();
},
),

View File

@ -43,7 +43,7 @@ class PlaylistCard extends StatelessWidget {
],
).paddingSymmetric(horizontal: 8),
onTap: () {
if (onTap != null) return;
if (onTap == null) return;
onTap!();
},
),

View File

@ -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:

View File

@ -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