✨ Better explore ever
This commit is contained in:
		@@ -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(
 | 
				
			||||||
 | 
					                child: PlaylistSection(
 | 
				
			||||||
                  isLoading: _isLoading['newReleases']!,
 | 
					                  isLoading: _isLoading['newReleases']!,
 | 
				
			||||||
                  title: 'New Releases',
 | 
					                  title: 'New Releases',
 | 
				
			||||||
                  list: _newReleasesPlaylist,
 | 
					                  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(
 | 
				
			||||||
 | 
					                child: PlaylistSection(
 | 
				
			||||||
                  isLoading: _isLoading['recently']!,
 | 
					                  isLoading: _isLoading['recently']!,
 | 
				
			||||||
                  title: 'Recent Played',
 | 
					                  title: 'Recent Played',
 | 
				
			||||||
                  list: _recentlyPlaylist,
 | 
					                  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']!,
 | 
					                isLoading: _isLoading['featured']!,
 | 
				
			||||||
                title: 'Featured',
 | 
					                title: 'Featured',
 | 
				
			||||||
                list: _featuredPlaylist,
 | 
					                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 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
 | 
				
			||||||
 
 | 
				
			|||||||
		Reference in New Issue
	
	Block a user