✨ Better explore
This commit is contained in:
		@@ -10,6 +10,7 @@ import 'package:rhythm_box/providers/database.dart';
 | 
			
		||||
import 'package:rhythm_box/providers/endless_playback.dart';
 | 
			
		||||
import 'package:rhythm_box/providers/history.dart';
 | 
			
		||||
import 'package:rhythm_box/providers/palette.dart';
 | 
			
		||||
import 'package:rhythm_box/providers/recent_played.dart';
 | 
			
		||||
import 'package:rhythm_box/providers/scrobbler.dart';
 | 
			
		||||
import 'package:rhythm_box/providers/skip_segments.dart';
 | 
			
		||||
import 'package:rhythm_box/providers/spotify.dart';
 | 
			
		||||
@@ -105,6 +106,7 @@ class MyApp extends StatelessWidget {
 | 
			
		||||
    Get.put(SourcedTrackProvider());
 | 
			
		||||
    Get.put(EndlessPlaybackProvider());
 | 
			
		||||
    Get.put(VolumeProvider());
 | 
			
		||||
    Get.put(RecentlyPlayedProvider());
 | 
			
		||||
 | 
			
		||||
    Get.put(ServerPlaybackRoutesProvider());
 | 
			
		||||
    Get.put(PlaybackServerProvider());
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										51
									
								
								lib/providers/recent_played.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										51
									
								
								lib/providers/recent_played.dart
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,51 @@
 | 
			
		||||
import 'dart:async';
 | 
			
		||||
 | 
			
		||||
import 'package:collection/collection.dart';
 | 
			
		||||
import 'package:drift/drift.dart';
 | 
			
		||||
import 'package:get/get.dart';
 | 
			
		||||
import 'package:rhythm_box/providers/database.dart';
 | 
			
		||||
import 'package:rhythm_box/services/database/database.dart';
 | 
			
		||||
 | 
			
		||||
class RecentlyPlayedProvider extends GetxController {
 | 
			
		||||
  Future<List<HistoryTableData>> fetch() async {
 | 
			
		||||
    final database = Get.find<DatabaseProvider>().database;
 | 
			
		||||
 | 
			
		||||
    final uniqueItemIds = await (database.selectOnly(
 | 
			
		||||
      database.historyTable,
 | 
			
		||||
      distinct: true,
 | 
			
		||||
    )
 | 
			
		||||
          ..addColumns([database.historyTable.itemId, database.historyTable.id])
 | 
			
		||||
          ..where(
 | 
			
		||||
            database.historyTable.type.isInValues([
 | 
			
		||||
              HistoryEntryType.playlist,
 | 
			
		||||
              HistoryEntryType.album,
 | 
			
		||||
            ]),
 | 
			
		||||
          )
 | 
			
		||||
          ..limit(10)
 | 
			
		||||
          ..orderBy([
 | 
			
		||||
            OrderingTerm(
 | 
			
		||||
              expression: database.historyTable.createdAt,
 | 
			
		||||
              mode: OrderingMode.desc,
 | 
			
		||||
            ),
 | 
			
		||||
          ]))
 | 
			
		||||
        .map(
 | 
			
		||||
          (row) => row.read(database.historyTable.id),
 | 
			
		||||
        )
 | 
			
		||||
        .get()
 | 
			
		||||
        .then((value) => value.whereNotNull().toList());
 | 
			
		||||
 | 
			
		||||
    final query = database.select(database.historyTable)
 | 
			
		||||
      ..where(
 | 
			
		||||
        (tbl) => tbl.id.isIn(uniqueItemIds),
 | 
			
		||||
      )
 | 
			
		||||
      ..orderBy([
 | 
			
		||||
        (tbl) => OrderingTerm(
 | 
			
		||||
              expression: tbl.createdAt,
 | 
			
		||||
              mode: OrderingMode.desc,
 | 
			
		||||
            ),
 | 
			
		||||
      ]);
 | 
			
		||||
 | 
			
		||||
    final fetchedItems = await query.get();
 | 
			
		||||
    return fetchedItems;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
@@ -1,11 +1,11 @@
 | 
			
		||||
import 'package:flutter/material.dart';
 | 
			
		||||
import 'package:get/get.dart';
 | 
			
		||||
import 'package:go_router/go_router.dart';
 | 
			
		||||
import 'package:rhythm_box/providers/recent_played.dart';
 | 
			
		||||
import 'package:rhythm_box/providers/spotify.dart';
 | 
			
		||||
import 'package:rhythm_box/widgets/playlist/playlist_tile.dart';
 | 
			
		||||
import 'package:rhythm_box/widgets/sized_container.dart';
 | 
			
		||||
import 'package:skeletonizer/skeletonizer.dart';
 | 
			
		||||
import 'package:spotify/spotify.dart';
 | 
			
		||||
import 'package:rhythm_box/providers/user_preferences.dart';
 | 
			
		||||
import 'package:rhythm_box/services/album.dart';
 | 
			
		||||
import 'package:rhythm_box/services/database/database.dart';
 | 
			
		||||
import 'package:rhythm_box/widgets/playlist/playlist_section.dart';
 | 
			
		||||
 | 
			
		||||
class ExploreScreen extends StatefulWidget {
 | 
			
		||||
  const ExploreScreen({super.key});
 | 
			
		||||
@@ -16,15 +16,37 @@ class ExploreScreen extends StatefulWidget {
 | 
			
		||||
 | 
			
		||||
class _ExploreScreenState extends State<ExploreScreen> {
 | 
			
		||||
  late final SpotifyProvider _spotify = Get.find();
 | 
			
		||||
  late final RecentlyPlayedProvider _history = Get.find();
 | 
			
		||||
 | 
			
		||||
  bool _isLoading = true;
 | 
			
		||||
  final Map<String, bool> _isLoading = {
 | 
			
		||||
    'featured': true,
 | 
			
		||||
    'recently': true,
 | 
			
		||||
    'newReleases': true,
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  List<PlaylistSimple>? _featuredPlaylist;
 | 
			
		||||
  List<Object>? _featuredPlaylist;
 | 
			
		||||
  List<Object>? _recentlyPlaylist;
 | 
			
		||||
  List<Object>? _newReleasesPlaylist;
 | 
			
		||||
 | 
			
		||||
  Future<void> _pullPlaylist() async {
 | 
			
		||||
    final market = Get.find<UserPreferencesProvider>().state.value.market;
 | 
			
		||||
 | 
			
		||||
    _featuredPlaylist =
 | 
			
		||||
        (await _spotify.api.playlists.featured.getPage(20)).items!.toList();
 | 
			
		||||
    setState(() => _isLoading = false);
 | 
			
		||||
    setState(() => _isLoading['featured'] = false);
 | 
			
		||||
 | 
			
		||||
    _recentlyPlaylist = (await _history.fetch())
 | 
			
		||||
        .where((x) => x.playlist != null)
 | 
			
		||||
        .map((x) => x.playlist!)
 | 
			
		||||
        .toList();
 | 
			
		||||
    setState(() => _isLoading['recently'] = false);
 | 
			
		||||
 | 
			
		||||
    _newReleasesPlaylist =
 | 
			
		||||
        (await _spotify.api.browse.newReleases(country: market).getPage(20))
 | 
			
		||||
            .items
 | 
			
		||||
            ?.map((album) => album.toAlbum())
 | 
			
		||||
            .toList();
 | 
			
		||||
    setState(() => _isLoading['newReleases'] = false);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  @override
 | 
			
		||||
@@ -42,26 +64,26 @@ class _ExploreScreenState extends State<ExploreScreen> {
 | 
			
		||||
          title: Text('explore'.tr),
 | 
			
		||||
          centerTitle: MediaQuery.of(context).size.width >= 720,
 | 
			
		||||
        ),
 | 
			
		||||
        body: CenteredContainer(
 | 
			
		||||
          child: Skeletonizer(
 | 
			
		||||
            enabled: _isLoading,
 | 
			
		||||
            child: ListView.builder(
 | 
			
		||||
              itemCount: _featuredPlaylist?.length ?? 20,
 | 
			
		||||
              itemBuilder: (context, idx) {
 | 
			
		||||
                final item = _featuredPlaylist?[idx];
 | 
			
		||||
                return PlaylistTile(
 | 
			
		||||
                  item: item,
 | 
			
		||||
                  onTap: () {
 | 
			
		||||
                    if (item == null) return;
 | 
			
		||||
                    GoRouter.of(context).pushNamed(
 | 
			
		||||
                      'playlistView',
 | 
			
		||||
                      pathParameters: {'id': item.id!},
 | 
			
		||||
                    );
 | 
			
		||||
                  },
 | 
			
		||||
                );
 | 
			
		||||
              },
 | 
			
		||||
        body: ListView(
 | 
			
		||||
          children: [
 | 
			
		||||
            if (_newReleasesPlaylist?.isNotEmpty ?? false)
 | 
			
		||||
              PlaylistSection(
 | 
			
		||||
                isLoading: _isLoading['newReleases']!,
 | 
			
		||||
                title: 'New Releases',
 | 
			
		||||
                list: _newReleasesPlaylist,
 | 
			
		||||
              ),
 | 
			
		||||
            if (_recentlyPlaylist?.isNotEmpty ?? false)
 | 
			
		||||
              PlaylistSection(
 | 
			
		||||
                isLoading: _isLoading['recently']!,
 | 
			
		||||
                title: 'Recent Played',
 | 
			
		||||
                list: _recentlyPlaylist,
 | 
			
		||||
              ),
 | 
			
		||||
            PlaylistSection(
 | 
			
		||||
              isLoading: _isLoading['featured']!,
 | 
			
		||||
              title: 'Featured',
 | 
			
		||||
              list: _featuredPlaylist,
 | 
			
		||||
            ),
 | 
			
		||||
          ),
 | 
			
		||||
          ],
 | 
			
		||||
        ),
 | 
			
		||||
      ),
 | 
			
		||||
    );
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										21
									
								
								lib/services/album.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										21
									
								
								lib/services/album.dart
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,21 @@
 | 
			
		||||
import 'package:spotify/spotify.dart';
 | 
			
		||||
 | 
			
		||||
extension AlbumExtensions on AlbumSimple {
 | 
			
		||||
  Album toAlbum() {
 | 
			
		||||
    Album album = Album();
 | 
			
		||||
    album.albumType = albumType;
 | 
			
		||||
    album.artists = artists;
 | 
			
		||||
    album.availableMarkets = availableMarkets;
 | 
			
		||||
    album.externalUrls = externalUrls;
 | 
			
		||||
    album.href = href;
 | 
			
		||||
    album.id = id;
 | 
			
		||||
    album.images = images;
 | 
			
		||||
    album.name = name;
 | 
			
		||||
    album.releaseDate = releaseDate;
 | 
			
		||||
    album.releaseDatePrecision = releaseDatePrecision;
 | 
			
		||||
    album.tracks = tracks;
 | 
			
		||||
    album.type = type;
 | 
			
		||||
    album.uri = uri;
 | 
			
		||||
    return album;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
@@ -250,8 +250,7 @@ class YoutubeSourcedTrack extends SourcedTrack {
 | 
			
		||||
 | 
			
		||||
    final searchResults = await youtubeClient.search.search(
 | 
			
		||||
      query,
 | 
			
		||||
      // '$query - Topic',
 | 
			
		||||
      filter: TypeFilters.video,
 | 
			
		||||
      filter: const SearchFilter('CAMSAhAB'),
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
    if (ServiceUtils.onlyContainsEnglish(query)) {
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										53
									
								
								lib/widgets/album/album_card.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										53
									
								
								lib/widgets/album/album_card.dart
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,53 @@
 | 
			
		||||
import 'package:flutter/material.dart';
 | 
			
		||||
import 'package:get/get.dart';
 | 
			
		||||
import 'package:rhythm_box/widgets/auto_cache_image.dart';
 | 
			
		||||
import 'package:rhythm_box/services/artist.dart';
 | 
			
		||||
import 'package:spotify/spotify.dart';
 | 
			
		||||
 | 
			
		||||
class AlbumCard extends StatelessWidget {
 | 
			
		||||
  final AlbumSimple? item;
 | 
			
		||||
 | 
			
		||||
  final Function? onTap;
 | 
			
		||||
 | 
			
		||||
  const AlbumCard({super.key, required this.item, this.onTap});
 | 
			
		||||
 | 
			
		||||
  @override
 | 
			
		||||
  Widget build(BuildContext context) {
 | 
			
		||||
    return Card(
 | 
			
		||||
      child: InkWell(
 | 
			
		||||
        borderRadius: const BorderRadius.all(Radius.circular(8)),
 | 
			
		||||
        child: Column(
 | 
			
		||||
          crossAxisAlignment: CrossAxisAlignment.start,
 | 
			
		||||
          children: [
 | 
			
		||||
            ClipRRect(
 | 
			
		||||
              borderRadius: const BorderRadius.all(Radius.circular(8)),
 | 
			
		||||
              child: AspectRatio(
 | 
			
		||||
                aspectRatio: 1,
 | 
			
		||||
                child: (item?.images?.isNotEmpty ?? false)
 | 
			
		||||
                    ? AutoCacheImage(item!.images!.first.url!)
 | 
			
		||||
                    : const Center(child: Icon(Icons.image)),
 | 
			
		||||
              ),
 | 
			
		||||
            ).paddingSymmetric(vertical: 8),
 | 
			
		||||
            Text(
 | 
			
		||||
              item?.name ?? 'Loading...',
 | 
			
		||||
              style: Theme.of(context).textTheme.bodyLarge,
 | 
			
		||||
              maxLines: 1,
 | 
			
		||||
              overflow: TextOverflow.ellipsis,
 | 
			
		||||
            ),
 | 
			
		||||
            Expanded(
 | 
			
		||||
              child: Text(
 | 
			
		||||
                item?.artists?.asString() ?? 'Please stand by...',
 | 
			
		||||
                maxLines: 3,
 | 
			
		||||
                overflow: TextOverflow.ellipsis,
 | 
			
		||||
              ),
 | 
			
		||||
            ),
 | 
			
		||||
          ],
 | 
			
		||||
        ).paddingSymmetric(horizontal: 8),
 | 
			
		||||
        onTap: () {
 | 
			
		||||
          if (onTap != null) return;
 | 
			
		||||
          onTap!();
 | 
			
		||||
        },
 | 
			
		||||
      ),
 | 
			
		||||
    );
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
@@ -109,6 +109,7 @@ class _SyncedLyricsState extends State<SyncedLyrics> {
 | 
			
		||||
    final size = MediaQuery.of(context).size;
 | 
			
		||||
 | 
			
		||||
    return CustomScrollView(
 | 
			
		||||
      cacheExtent: 10000,
 | 
			
		||||
      controller: _autoScrollController,
 | 
			
		||||
      slivers: [
 | 
			
		||||
        if (_lyric == null)
 | 
			
		||||
@@ -164,6 +165,9 @@ class _SyncedLyricsState extends State<SyncedLyrics> {
 | 
			
		||||
                          ),
 | 
			
		||||
                          textAlign: TextAlign.center,
 | 
			
		||||
                          child: InkWell(
 | 
			
		||||
                            borderRadius: const BorderRadius.all(
 | 
			
		||||
                              Radius.circular(8),
 | 
			
		||||
                            ),
 | 
			
		||||
                            onTap: () async {
 | 
			
		||||
                              final time = Duration(
 | 
			
		||||
                                seconds: lyricSlice.time.inSeconds -
 | 
			
		||||
@@ -184,7 +188,7 @@ class _SyncedLyricsState extends State<SyncedLyrics> {
 | 
			
		||||
                                      : _unFocusColor,
 | 
			
		||||
                                ),
 | 
			
		||||
                                duration: 500.ms,
 | 
			
		||||
                                curve: Curves.easeInOut,
 | 
			
		||||
                                curve: Curves.decelerate,
 | 
			
		||||
                                child: Text(
 | 
			
		||||
                                  lyricSlice.text,
 | 
			
		||||
                                  textAlign:
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										52
									
								
								lib/widgets/playlist/playlist_card.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										52
									
								
								lib/widgets/playlist/playlist_card.dart
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,52 @@
 | 
			
		||||
import 'package:flutter/material.dart';
 | 
			
		||||
import 'package:get/get.dart';
 | 
			
		||||
import 'package:rhythm_box/widgets/auto_cache_image.dart';
 | 
			
		||||
import 'package:spotify/spotify.dart';
 | 
			
		||||
 | 
			
		||||
class PlaylistCard extends StatelessWidget {
 | 
			
		||||
  final PlaylistSimple? item;
 | 
			
		||||
 | 
			
		||||
  final Function? onTap;
 | 
			
		||||
 | 
			
		||||
  const PlaylistCard({super.key, required this.item, this.onTap});
 | 
			
		||||
 | 
			
		||||
  @override
 | 
			
		||||
  Widget build(BuildContext context) {
 | 
			
		||||
    return Card(
 | 
			
		||||
      child: InkWell(
 | 
			
		||||
        borderRadius: const BorderRadius.all(Radius.circular(8)),
 | 
			
		||||
        child: Column(
 | 
			
		||||
          crossAxisAlignment: CrossAxisAlignment.start,
 | 
			
		||||
          children: [
 | 
			
		||||
            ClipRRect(
 | 
			
		||||
              borderRadius: const BorderRadius.all(Radius.circular(8)),
 | 
			
		||||
              child: AspectRatio(
 | 
			
		||||
                aspectRatio: 1,
 | 
			
		||||
                child: (item?.images?.isNotEmpty ?? false)
 | 
			
		||||
                    ? AutoCacheImage(item!.images!.first.url!)
 | 
			
		||||
                    : const Center(child: Icon(Icons.image)),
 | 
			
		||||
              ),
 | 
			
		||||
            ).paddingSymmetric(vertical: 8),
 | 
			
		||||
            Text(
 | 
			
		||||
              item?.name ?? 'Loading...',
 | 
			
		||||
              style: Theme.of(context).textTheme.bodyLarge,
 | 
			
		||||
              maxLines: 1,
 | 
			
		||||
              overflow: TextOverflow.ellipsis,
 | 
			
		||||
            ),
 | 
			
		||||
            Expanded(
 | 
			
		||||
              child: Text(
 | 
			
		||||
                item?.description ?? 'Please stand by...',
 | 
			
		||||
                maxLines: 3,
 | 
			
		||||
                overflow: TextOverflow.ellipsis,
 | 
			
		||||
              ),
 | 
			
		||||
            ),
 | 
			
		||||
          ],
 | 
			
		||||
        ).paddingSymmetric(horizontal: 8),
 | 
			
		||||
        onTap: () {
 | 
			
		||||
          if (onTap != null) return;
 | 
			
		||||
          onTap!();
 | 
			
		||||
        },
 | 
			
		||||
      ),
 | 
			
		||||
    );
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										74
									
								
								lib/widgets/playlist/playlist_section.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										74
									
								
								lib/widgets/playlist/playlist_section.dart
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,74 @@
 | 
			
		||||
import 'package:flutter/material.dart';
 | 
			
		||||
import 'package:get/get.dart';
 | 
			
		||||
import 'package:go_router/go_router.dart';
 | 
			
		||||
import 'package:rhythm_box/widgets/album/album_card.dart';
 | 
			
		||||
import 'package:rhythm_box/widgets/playlist/playlist_card.dart';
 | 
			
		||||
import 'package:skeletonizer/skeletonizer.dart';
 | 
			
		||||
import 'package:spotify/spotify.dart';
 | 
			
		||||
 | 
			
		||||
class PlaylistSection extends StatelessWidget {
 | 
			
		||||
  final bool isLoading;
 | 
			
		||||
  final String title;
 | 
			
		||||
  final List<Object>? list;
 | 
			
		||||
 | 
			
		||||
  const PlaylistSection({
 | 
			
		||||
    super.key,
 | 
			
		||||
    required this.isLoading,
 | 
			
		||||
    required this.title,
 | 
			
		||||
    required this.list,
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  @override
 | 
			
		||||
  Widget build(BuildContext context) {
 | 
			
		||||
    return Column(
 | 
			
		||||
      crossAxisAlignment: CrossAxisAlignment.start,
 | 
			
		||||
      children: [
 | 
			
		||||
        Text(
 | 
			
		||||
          title,
 | 
			
		||||
          style: Theme.of(context).textTheme.titleLarge,
 | 
			
		||||
        ).paddingOnly(left: 32, right: 32, bottom: 4),
 | 
			
		||||
        SizedBox(
 | 
			
		||||
          height: 280,
 | 
			
		||||
          width: double.infinity,
 | 
			
		||||
          child: Skeletonizer(
 | 
			
		||||
            enabled: isLoading,
 | 
			
		||||
            child: ListView.builder(
 | 
			
		||||
              scrollDirection: Axis.horizontal,
 | 
			
		||||
              padding: const EdgeInsets.symmetric(horizontal: 16),
 | 
			
		||||
              itemCount: list?.length ?? 20,
 | 
			
		||||
              itemBuilder: (context, idx) {
 | 
			
		||||
                final item = list?[idx];
 | 
			
		||||
                return SizedBox(
 | 
			
		||||
                  width: 180,
 | 
			
		||||
                  height: 180,
 | 
			
		||||
                  child: switch (item.runtimeType) {
 | 
			
		||||
                    const (AlbumSimple) || const (Album) => AlbumCard(
 | 
			
		||||
                        item: item as AlbumSimple?,
 | 
			
		||||
                        onTap: () {
 | 
			
		||||
                          if (item == null) return;
 | 
			
		||||
                          GoRouter.of(context).pushNamed(
 | 
			
		||||
                            'playlistView',
 | 
			
		||||
                            pathParameters: {'id': item.id!},
 | 
			
		||||
                          );
 | 
			
		||||
                        },
 | 
			
		||||
                      ),
 | 
			
		||||
                    _ => PlaylistCard(
 | 
			
		||||
                        item: item as PlaylistSimple?,
 | 
			
		||||
                        onTap: () {
 | 
			
		||||
                          if (item == null) return;
 | 
			
		||||
                          GoRouter.of(context).pushNamed(
 | 
			
		||||
                            'playlistView',
 | 
			
		||||
                            pathParameters: {'id': item.id!},
 | 
			
		||||
                          );
 | 
			
		||||
                        },
 | 
			
		||||
                      ),
 | 
			
		||||
                  },
 | 
			
		||||
                );
 | 
			
		||||
              },
 | 
			
		||||
            ),
 | 
			
		||||
          ),
 | 
			
		||||
        ),
 | 
			
		||||
      ],
 | 
			
		||||
    );
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
		Reference in New Issue
	
	Block a user