✨ 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_inappwebview_ios/Core (0.0.1):
|
||||||
- Flutter
|
- Flutter
|
||||||
- OrderedSet (~> 5.0)
|
- OrderedSet (~> 5.0)
|
||||||
|
- flutter_native_splash (0.0.1):
|
||||||
|
- Flutter
|
||||||
- flutter_secure_storage (6.0.0):
|
- flutter_secure_storage (6.0.0):
|
||||||
- Flutter
|
- Flutter
|
||||||
- media_kit_libs_ios_audio (1.0.4):
|
- media_kit_libs_ios_audio (1.0.4):
|
||||||
@ -61,6 +63,7 @@ DEPENDENCIES:
|
|||||||
- Flutter (from `Flutter`)
|
- Flutter (from `Flutter`)
|
||||||
- flutter_broadcasts (from `.symlinks/plugins/flutter_broadcasts/ios`)
|
- flutter_broadcasts (from `.symlinks/plugins/flutter_broadcasts/ios`)
|
||||||
- flutter_inappwebview_ios (from `.symlinks/plugins/flutter_inappwebview_ios/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`)
|
- 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_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`)
|
- 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"
|
:path: ".symlinks/plugins/flutter_broadcasts/ios"
|
||||||
flutter_inappwebview_ios:
|
flutter_inappwebview_ios:
|
||||||
:path: ".symlinks/plugins/flutter_inappwebview_ios/ios"
|
:path: ".symlinks/plugins/flutter_inappwebview_ios/ios"
|
||||||
|
flutter_native_splash:
|
||||||
|
:path: ".symlinks/plugins/flutter_native_splash/ios"
|
||||||
flutter_secure_storage:
|
flutter_secure_storage:
|
||||||
:path: ".symlinks/plugins/flutter_secure_storage/ios"
|
:path: ".symlinks/plugins/flutter_secure_storage/ios"
|
||||||
media_kit_libs_ios_audio:
|
media_kit_libs_ios_audio:
|
||||||
@ -115,6 +120,7 @@ SPEC CHECKSUMS:
|
|||||||
Flutter: e0871f40cf51350855a761d2e70bf5af5b9b5de7
|
Flutter: e0871f40cf51350855a761d2e70bf5af5b9b5de7
|
||||||
flutter_broadcasts: 3ece15b27d8ccbe2132c3df303e7c3401feab882
|
flutter_broadcasts: 3ece15b27d8ccbe2132c3df303e7c3401feab882
|
||||||
flutter_inappwebview_ios: 97215cf7d4677db55df76782dbd2930c5e1c1ea0
|
flutter_inappwebview_ios: 97215cf7d4677db55df76782dbd2930c5e1c1ea0
|
||||||
|
flutter_native_splash: edf599c81f74d093a4daf8e17bd7a018854bc778
|
||||||
flutter_secure_storage: d33dac7ae2ea08509be337e775f6b59f1ff45f12
|
flutter_secure_storage: d33dac7ae2ea08509be337e775f6b59f1ff45f12
|
||||||
media_kit_libs_ios_audio: 8f39d96a9c630685dfb844c289bd1d114c486fb3
|
media_kit_libs_ios_audio: 8f39d96a9c630685dfb844c289bd1d114c486fb3
|
||||||
media_kit_native_event_loop: 99111eded5acbdc9c2738021ea6550dd36ca8837
|
media_kit_native_event_loop: 99111eded5acbdc9c2738021ea6550dd36ca8837
|
||||||
|
@ -1,12 +1,16 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:gap/gap.dart';
|
import 'package:gap/gap.dart';
|
||||||
import 'package:get/get.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/recent_played.dart';
|
||||||
import 'package:rhythm_box/providers/spotify.dart';
|
import 'package:rhythm_box/providers/spotify.dart';
|
||||||
import 'package:rhythm_box/providers/user_preferences.dart';
|
import 'package:rhythm_box/providers/user_preferences.dart';
|
||||||
import 'package:rhythm_box/services/album.dart';
|
import 'package:rhythm_box/services/album.dart';
|
||||||
import 'package:rhythm_box/services/database/database.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:rhythm_box/widgets/playlist/playlist_section.dart';
|
||||||
|
import 'package:spotify/spotify.dart';
|
||||||
|
|
||||||
class ExploreScreen extends StatefulWidget {
|
class ExploreScreen extends StatefulWidget {
|
||||||
const ExploreScreen({super.key});
|
const ExploreScreen({super.key});
|
||||||
@ -18,19 +22,23 @@ class ExploreScreen extends StatefulWidget {
|
|||||||
class _ExploreScreenState extends State<ExploreScreen> {
|
class _ExploreScreenState extends State<ExploreScreen> {
|
||||||
late final SpotifyProvider _spotify = Get.find();
|
late final SpotifyProvider _spotify = Get.find();
|
||||||
late final RecentlyPlayedProvider _history = Get.find();
|
late final RecentlyPlayedProvider _history = Get.find();
|
||||||
|
late final AuthenticationProvider _auth = Get.find();
|
||||||
|
|
||||||
final Map<String, bool> _isLoading = {
|
final Map<String, bool> _isLoading = {
|
||||||
'featured': true,
|
'featured': true,
|
||||||
'recently': true,
|
'recently': true,
|
||||||
'newReleases': true,
|
'newReleases': true,
|
||||||
|
'forYou': true,
|
||||||
};
|
};
|
||||||
|
|
||||||
List<Object>? _featuredPlaylist;
|
List<Object>? _featuredPlaylist;
|
||||||
List<Object>? _recentlyPlaylist;
|
List<Object>? _recentlyPlaylist;
|
||||||
List<Object>? _newReleasesPlaylist;
|
List<Object>? _newReleasesPlaylist;
|
||||||
|
List<dynamic>? _forYouView;
|
||||||
|
|
||||||
Future<void> _pullPlaylist() async {
|
Future<void> _pullPlaylist() async {
|
||||||
final market = Get.find<UserPreferencesProvider>().state.value.market;
|
final market = Get.find<UserPreferencesProvider>().state.value.market;
|
||||||
|
final locale = Get.find<UserPreferencesProvider>().state.value.locale;
|
||||||
|
|
||||||
_featuredPlaylist =
|
_featuredPlaylist =
|
||||||
(await _spotify.api.playlists.featured.getPage(20)).items!.toList();
|
(await _spotify.api.playlists.featured.getPage(20)).items!.toList();
|
||||||
@ -48,6 +56,16 @@ class _ExploreScreenState extends State<ExploreScreen> {
|
|||||||
?.map((album) => album.toAlbum())
|
?.map((album) => album.toAlbum())
|
||||||
.toList();
|
.toList();
|
||||||
setState(() => _isLoading['newReleases'] = false);
|
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
|
@override
|
||||||
@ -65,28 +83,52 @@ class _ExploreScreenState extends State<ExploreScreen> {
|
|||||||
title: Text('explore'.tr),
|
title: Text('explore'.tr),
|
||||||
centerTitle: MediaQuery.of(context).size.width >= 720,
|
centerTitle: MediaQuery.of(context).size.width >= 720,
|
||||||
),
|
),
|
||||||
body: ListView(
|
body: CustomScrollView(
|
||||||
children: [
|
slivers: [
|
||||||
if (_newReleasesPlaylist?.isNotEmpty ?? false)
|
if (_newReleasesPlaylist?.isNotEmpty ?? false)
|
||||||
PlaylistSection(
|
SliverToBoxAdapter(
|
||||||
isLoading: _isLoading['newReleases']!,
|
child: PlaylistSection(
|
||||||
title: 'New Releases',
|
isLoading: _isLoading['newReleases']!,
|
||||||
list: _newReleasesPlaylist,
|
title: 'New Releases',
|
||||||
|
list: _newReleasesPlaylist,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
if (_newReleasesPlaylist?.isNotEmpty ?? false) const Gap(16),
|
if (_newReleasesPlaylist?.isNotEmpty ?? false) const SliverGap(16),
|
||||||
if (_recentlyPlaylist?.isNotEmpty ?? false)
|
if (_recentlyPlaylist?.isNotEmpty ?? false)
|
||||||
PlaylistSection(
|
SliverToBoxAdapter(
|
||||||
isLoading: _isLoading['recently']!,
|
child: PlaylistSection(
|
||||||
title: 'Recent Played',
|
isLoading: _isLoading['recently']!,
|
||||||
list: _recentlyPlaylist,
|
title: 'Recent Played',
|
||||||
|
list: _recentlyPlaylist,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
if (_recentlyPlaylist?.isNotEmpty ?? false) const Gap(16),
|
if (_recentlyPlaylist?.isNotEmpty ?? false) const SliverGap(16),
|
||||||
PlaylistSection(
|
SliverList.builder(
|
||||||
isLoading: _isLoading['featured']!,
|
itemCount: _forYouView?.length ?? 0,
|
||||||
title: 'Featured',
|
itemBuilder: (context, idx) {
|
||||||
list: _featuredPlaylist,
|
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
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 nativeAppUriMobile => throw _privateConstructorUsedError;
|
||||||
String? get nativeAppUriDesktop => throw _privateConstructorUsedError;
|
String? get nativeAppUriDesktop => throw _privateConstructorUsedError;
|
||||||
|
|
||||||
|
/// Serializes this SongLink to a JSON map.
|
||||||
Map<String, dynamic> toJson() => throw _privateConstructorUsedError;
|
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 =>
|
$SongLinkCopyWith<SongLink> get copyWith =>
|
||||||
throw _privateConstructorUsedError;
|
throw _privateConstructorUsedError;
|
||||||
}
|
}
|
||||||
@ -63,6 +67,8 @@ class _$SongLinkCopyWithImpl<$Res, $Val extends SongLink>
|
|||||||
// ignore: unused_field
|
// ignore: unused_field
|
||||||
final $Res Function($Val) _then;
|
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')
|
@pragma('vm:prefer-inline')
|
||||||
@override
|
@override
|
||||||
$Res call({
|
$Res call({
|
||||||
@ -145,6 +151,8 @@ class __$$SongLinkImplCopyWithImpl<$Res>
|
|||||||
_$SongLinkImpl _value, $Res Function(_$SongLinkImpl) _then)
|
_$SongLinkImpl _value, $Res Function(_$SongLinkImpl) _then)
|
||||||
: super(_value, _then);
|
: super(_value, _then);
|
||||||
|
|
||||||
|
/// Create a copy of SongLink
|
||||||
|
/// with the given fields replaced by the non-null parameter values.
|
||||||
@pragma('vm:prefer-inline')
|
@pragma('vm:prefer-inline')
|
||||||
@override
|
@override
|
||||||
$Res call({
|
$Res call({
|
||||||
@ -261,12 +269,14 @@ class _$SongLinkImpl implements _SongLink {
|
|||||||
other.nativeAppUriDesktop == nativeAppUriDesktop));
|
other.nativeAppUriDesktop == nativeAppUriDesktop));
|
||||||
}
|
}
|
||||||
|
|
||||||
@JsonKey(ignore: true)
|
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||||
@override
|
@override
|
||||||
int get hashCode => Object.hash(runtimeType, displayName, linkId, platform,
|
int get hashCode => Object.hash(runtimeType, displayName, linkId, platform,
|
||||||
show, uniqueId, country, url, nativeAppUriMobile, nativeAppUriDesktop);
|
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
|
@override
|
||||||
@pragma('vm:prefer-inline')
|
@pragma('vm:prefer-inline')
|
||||||
_$$SongLinkImplCopyWith<_$SongLinkImpl> get copyWith =>
|
_$$SongLinkImplCopyWith<_$SongLinkImpl> get copyWith =>
|
||||||
@ -313,8 +323,11 @@ abstract class _SongLink implements SongLink {
|
|||||||
String? get nativeAppUriMobile;
|
String? get nativeAppUriMobile;
|
||||||
@override
|
@override
|
||||||
String? get nativeAppUriDesktop;
|
String? get nativeAppUriDesktop;
|
||||||
|
|
||||||
|
/// Create a copy of SongLink
|
||||||
|
/// with the given fields replaced by the non-null parameter values.
|
||||||
@override
|
@override
|
||||||
@JsonKey(ignore: true)
|
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||||
_$$SongLinkImplCopyWith<_$SongLinkImpl> get copyWith =>
|
_$$SongLinkImplCopyWith<_$SongLinkImpl> get copyWith =>
|
||||||
throw _privateConstructorUsedError;
|
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),
|
).paddingSymmetric(horizontal: 8),
|
||||||
onTap: () {
|
onTap: () {
|
||||||
if (onTap != null) return;
|
if (onTap == null) return;
|
||||||
onTap!();
|
onTap!();
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
|
@ -43,7 +43,7 @@ class PlaylistCard extends StatelessWidget {
|
|||||||
],
|
],
|
||||||
).paddingSymmetric(horizontal: 8),
|
).paddingSymmetric(horizontal: 8),
|
||||||
onTap: () {
|
onTap: () {
|
||||||
if (onTap != null) return;
|
if (onTap == null) return;
|
||||||
onTap!();
|
onTap!();
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
|
18
pubspec.lock
18
pubspec.lock
@ -614,6 +614,14 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.0.0"
|
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:
|
freezed_annotation:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
@ -759,7 +767,7 @@ packages:
|
|||||||
source: hosted
|
source: hosted
|
||||||
version: "4.9.0"
|
version: "4.9.0"
|
||||||
json_serializable:
|
json_serializable:
|
||||||
dependency: "direct main"
|
dependency: "direct dev"
|
||||||
description:
|
description:
|
||||||
name: json_serializable
|
name: json_serializable
|
||||||
sha256: ea1432d167339ea9b5bb153f0571d0039607a873d6e04e0117af043f14a1fd4b
|
sha256: ea1432d167339ea9b5bb153f0571d0039607a873d6e04e0117af043f14a1fd4b
|
||||||
@ -1382,6 +1390,14 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.7.2"
|
version: "0.7.2"
|
||||||
|
timezone:
|
||||||
|
dependency: "direct main"
|
||||||
|
description:
|
||||||
|
name: timezone
|
||||||
|
sha256: "2236ec079a174ce07434e89fcd3fcda430025eb7692244139a9cf54fdcf1fc7d"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "0.9.4"
|
||||||
timing:
|
timing:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
@ -78,7 +78,6 @@ dependencies:
|
|||||||
flutter_secure_storage: ^9.2.2
|
flutter_secure_storage: ^9.2.2
|
||||||
window_manager: ^0.4.2
|
window_manager: ^0.4.2
|
||||||
lrc: ^1.0.2
|
lrc: ^1.0.2
|
||||||
json_serializable: ^6.8.0
|
|
||||||
path: ^1.9.0
|
path: ^1.9.0
|
||||||
path_provider: ^2.1.4
|
path_provider: ^2.1.4
|
||||||
sqlite3_flutter_libs: ^0.5.23
|
sqlite3_flutter_libs: ^0.5.23
|
||||||
@ -100,6 +99,7 @@ dependencies:
|
|||||||
ref: feat/cookies
|
ref: feat/cookies
|
||||||
path: packages/desktop_webview_window
|
path: packages/desktop_webview_window
|
||||||
flutter_inappwebview: ^6.0.0
|
flutter_inappwebview: ^6.0.0
|
||||||
|
timezone: ^0.9.4
|
||||||
|
|
||||||
dev_dependencies:
|
dev_dependencies:
|
||||||
flutter_test:
|
flutter_test:
|
||||||
@ -115,6 +115,8 @@ dev_dependencies:
|
|||||||
build_runner: ^2.4.12
|
build_runner: ^2.4.12
|
||||||
flutter_launcher_icons: ^0.13.1
|
flutter_launcher_icons: ^0.13.1
|
||||||
flutter_native_splash: ^2.4.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
|
# For information on the generic Dart part of this file, see the
|
||||||
# following page: https://dart.dev/tools/pub/pubspec
|
# following page: https://dart.dev/tools/pub/pubspec
|
||||||
|
Loading…
Reference in New Issue
Block a user