diff --git a/lib/cubits/download/download_cubit.dart b/lib/cubits/download/download_cubit.dart index 5d4138e..efc5c6d 100644 --- a/lib/cubits/download/download_cubit.dart +++ b/lib/cubits/download/download_cubit.dart @@ -4,10 +4,14 @@ import 'dart:io'; import 'package:background_downloader/background_downloader.dart'; import 'package:equatable/equatable.dart'; import 'package:hive_flutter/hive_flutter.dart'; +import 'package:path/path.dart' as path; +import 'package:path_provider/path_provider.dart'; import 'package:rxdart/rxdart.dart'; +import 'package:varanasi_mobile_app/features/user-library/data/user_library.dart'; import 'package:varanasi_mobile_app/models/app_config.dart'; import 'package:varanasi_mobile_app/models/download.dart'; import 'package:varanasi_mobile_app/models/download_url.dart'; +import 'package:varanasi_mobile_app/models/image.dart'; import 'package:varanasi_mobile_app/models/media_playlist.dart'; import 'package:varanasi_mobile_app/models/playable_item.dart'; import 'package:varanasi_mobile_app/models/song.dart'; @@ -19,7 +23,7 @@ import 'package:varanasi_mobile_app/utils/logger.dart'; part 'download_state.dart'; class DownloadCubit extends AppCubit { - DownloadCubit() : super(const DownloadState()); + DownloadCubit() : super(DownloadInitial()); late final Box _downloadBox; late final FileDownloader _downloader; @@ -30,11 +34,19 @@ class DownloadCubit extends AppCubit { Box get downloadBox => _downloadBox; + FileDownloader get downloader => _downloader; + + DownloadLoadedState get loadedState => state as DownloadLoadedState; + @override FutureOr init() async { + final baseDir = await getApplicationDocumentsDirectory(); + emit(DownloadLoadedState(downloadDirectory: baseDir)); _songMap = {}; _downloadBox = Hive.box(AppStrings.downloadBoxName); - _downloader = FileDownloader(); + _downloader = FileDownloader() + ..trackTasks() + ..resumeFromBackground(); _downloader.updates.listen((update) { if (update is TaskStatusUpdate) { _handleTaskStatusUpdate(update); @@ -142,12 +154,13 @@ class DownloadCubit extends AppCubit { for (final song in filteredsong) { _songMap[song.itemId] = song; } - emit(state.updateProgress(MapEntry(playlist.id!, 0))); + emit(loadedState.updateProgress(MapEntry(playlist.id!, 0))); await _downloader.downloadBatch( tasks, batchProgressCallback: (succeeded, failed) { final percentComplete = (succeeded + failed) / tasks.length; - emit(state.updateProgress(MapEntry(playlist.id!, percentComplete))); + emit(loadedState + .updateProgress(MapEntry(playlist.id!, percentComplete))); _logger.i('Batch progress: $succeeded, $failed'); }, taskStatusCallback: _handleTaskStatusUpdate, @@ -279,4 +292,34 @@ class DownloadCubit extends AppCubit { ); return AppConfig.effectiveDlQuality; } + + Stream get downloadLibraryStream { + return _downloadBox.watch().map((event) => toUserLibrary()); + } + + UserLibrary toUserLibrary() { + final List values = _downloadBox.values.toList(); + final library = UserLibrary( + id: "downloads", + title: "Downloads", + description: "Your downloaded songs", + mediaItems: values.map((e) => e.media).toList(), + images: const [Image.likedSongs], + type: UserLibraryType.download, + ); + return library; + } + + String getDownloadPath(String id) { + final item = _downloadBox.get(id); + final filename = _fileNameFromSong(item!.media); + return path.join(loadedState.downloadDirectory.path, '', filename); + } + + File? getCacheFile(String itemId, String itemUrl) { + final ext = itemUrl.split('.').last; + final fileName = '$itemId.$ext'; + return File( + path.join(loadedState.downloadDirectory.path, 'cache', fileName)); + } } diff --git a/lib/cubits/download/download_state.dart b/lib/cubits/download/download_state.dart index e68ec64..6ca732c 100644 --- a/lib/cubits/download/download_state.dart +++ b/lib/cubits/download/download_state.dart @@ -1,25 +1,38 @@ // ignore_for_file: public_member_api_docs, sort_constructors_first part of 'download_cubit.dart'; -class DownloadState extends Equatable { +sealed class DownloadState extends Equatable { + const DownloadState(); + + @override + List get props => []; +} + +class DownloadInitial extends DownloadState {} + +class DownloadLoadedState extends DownloadState { final Map playlistProgressMap; + final Directory downloadDirectory; - const DownloadState({ + const DownloadLoadedState({ this.playlistProgressMap = const {}, + required this.downloadDirectory, }); @override - List get props => [playlistProgressMap]; + List get props => [playlistProgressMap, downloadDirectory]; - DownloadState copyWith({ + DownloadLoadedState copyWith({ Map? playlistProgressMap, + Directory? downloadDirectory, }) { - return DownloadState( + return DownloadLoadedState( playlistProgressMap: playlistProgressMap ?? this.playlistProgressMap, + downloadDirectory: downloadDirectory ?? this.downloadDirectory, ); } - DownloadState updateProgress(MapEntry entry) { + DownloadLoadedState updateProgress(MapEntry entry) { final Map oldProgress = Map.from(playlistProgressMap) ..addEntries([entry]); return copyWith( diff --git a/lib/cubits/player/player_cubit.dart b/lib/cubits/player/player_cubit.dart index 31e5f83..efce673 100644 --- a/lib/cubits/player/player_cubit.dart +++ b/lib/cubits/player/player_cubit.dart @@ -62,10 +62,13 @@ class MediaPlayerCubit extends AppCubit Future playFromMediaPlaylist( MediaPlaylist playlist, [ - int? startIndex, + PlayableMedia? initialMedia, bool autoPlay = true, ]) async { if (playlist.id == state.currentPlaylist && !audioHandler.player.playing) { + final startIndex = initialMedia == null + ? null + : state.queueState.queue.indexOf(initialMedia.toMediaItem()); if (startIndex != null) { await skipToIndex(startIndex, autoPlay); } else if (autoPlay) { @@ -76,6 +79,13 @@ class MediaPlayerCubit extends AppCubit unawaited(_configCubit.saveCurrentPlaylist(playlist)); emit(state.copyWith(currentPlaylist: playlist.id)); await audioHandler.updateQueue(playlist.mediaItemsAsMediaItems); + final shuffleModeEnabled = audioHandler.player.shuffleModeEnabled; + if (shuffleModeEnabled) { + await audioHandler.setShuffleMode(AudioServiceShuffleMode.all); + } + final startIndex = initialMedia == null + ? null + : state.queueState.queue.indexOf(initialMedia.toMediaItem()); if (startIndex != null) { await skipToIndex(startIndex, autoPlay); } else if (autoPlay) { diff --git a/lib/features/library/cubit/library_cubit.dart b/lib/features/library/cubit/library_cubit.dart index 023053d..d128512 100644 --- a/lib/features/library/cubit/library_cubit.dart +++ b/lib/features/library/cubit/library_cubit.dart @@ -26,9 +26,19 @@ class LibraryCubit extends Cubit { String link = playlist.images.last.link!; if (!appContext.mounted) return; final configCubit = appContext.read(); - final colorPalette = await configCubit.generatePalleteGenerator(link); + PaletteGenerator.fromColors([]); + if (!appContext.mounted) return; + final colorPalette = playlist.isDownload && appContext.mounted + ? PaletteGenerator.fromColors( + [PaletteColor(appContext.colorScheme.secondaryContainer, 1)]) + : await configCubit.generatePalleteGenerator(link); final image = configCubit.getProvider(link); - emit(LibraryLoaded(playlist.toMediaPlaylist(), colorPalette!, image)); + emit(LibraryLoaded( + playlist.toMediaPlaylist(), + colorPalette!, + image, + sourceLibrary: playlist, + )); } catch (e, s) { emit(LibraryError(e, stackTrace: s)); } diff --git a/lib/features/library/cubit/library_state.dart b/lib/features/library/cubit/library_state.dart index fddfa6b..717aaf3 100644 --- a/lib/features/library/cubit/library_state.dart +++ b/lib/features/library/cubit/library_state.dart @@ -27,16 +27,19 @@ class LibraryLoaded extends LibraryState { final PaletteGenerator colorPalette; final ImageProvider image; final bool showTitleInAppBar; + final UserLibrary? sourceLibrary; const LibraryLoaded( this.playlist, this.colorPalette, this.image, { this.showTitleInAppBar = false, + this.sourceLibrary, }); @override - List get props => [playlist, colorPalette, image, showTitleInAppBar]; + List get props => + [playlist, colorPalette, image, showTitleInAppBar, sourceLibrary]; PaletteColor? get baseColor => colorPalette.dominantColor ?? colorPalette.vibrantColor; @@ -75,12 +78,14 @@ class LibraryLoaded extends LibraryState { PaletteGenerator? colorPalette, ImageProvider? image, bool? showTitleInAppBar, + UserLibrary? sourceLibrary, }) { return LibraryLoaded( playlist ?? this.playlist, colorPalette ?? this.colorPalette, image ?? this.image, showTitleInAppBar: showTitleInAppBar ?? this.showTitleInAppBar, + sourceLibrary: sourceLibrary ?? this.sourceLibrary, ); } diff --git a/lib/features/library/ui/library_widgets/library_app_bar.dart b/lib/features/library/ui/library_widgets/library_app_bar.dart index bb39891..754693a 100644 --- a/lib/features/library/ui/library_widgets/library_app_bar.dart +++ b/lib/features/library/ui/library_widgets/library_app_bar.dart @@ -17,6 +17,11 @@ class LibraryAppBar extends StatelessWidget { final LibraryLoaded state; final EdgeInsets padding; + bool get isFromUserLibrary => + state.sourceLibrary != null && + (state.sourceLibrary!.isDownload == true || + state.sourceLibrary!.isFavorite == true); + @override Widget build(BuildContext context) { return SliverAppBar( @@ -24,7 +29,7 @@ class LibraryAppBar extends StatelessWidget { elevation: 0, scrolledUnderElevation: 10, stretch: true, - expandedHeight: kSliverExpandedHeight, + expandedHeight: isFromUserLibrary ? 130 : kSliverExpandedHeight, pinned: kSliverAppBarPinned, collapsedHeight: kToolbarHeight, flexibleSpace: LayoutBuilder(builder: (context, constraints) { @@ -64,7 +69,7 @@ class LibraryAppBar extends StatelessWidget { children: [ Container( height: 64, - width: context.width * 0.85, + width: context.width * 0.8, alignment: Alignment.topCenter, child: SizedBox( height: 32, @@ -79,17 +84,19 @@ class LibraryAppBar extends StatelessWidget { ), ), ), - AnimatedContainer( - duration: kThemeAnimationDuration, - height: imageHeight, - width: imageHeight, - decoration: BoxDecoration(boxShadow: state.boxShadow), - child: Image( + if (!isFromUserLibrary) + AnimatedContainer( + duration: kThemeAnimationDuration, height: imageHeight, width: imageHeight, - image: state.image, + decoration: BoxDecoration(boxShadow: state.boxShadow), + child: Image( + height: imageHeight, + width: imageHeight, + image: state.image, + fit: BoxFit.cover, + ), ), - ), ], ), ), diff --git a/lib/features/library/ui/library_widgets/page.dart b/lib/features/library/ui/library_widgets/page.dart index 6e1b96c..fc79c11 100644 --- a/lib/features/library/ui/library_widgets/page.dart +++ b/lib/features/library/ui/library_widgets/page.dart @@ -1,3 +1,4 @@ +import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart' hide Typography; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; @@ -10,6 +11,7 @@ import 'package:varanasi_mobile_app/models/playable_item.dart'; import 'package:varanasi_mobile_app/utils/extensions/extensions.dart'; import 'package:varanasi_mobile_app/utils/extensions/media_query.dart'; import 'package:varanasi_mobile_app/widgets/add_to_library.dart'; +import 'package:varanasi_mobile_app/widgets/disable_child.dart'; import 'package:varanasi_mobile_app/widgets/download_button.dart'; import 'package:varanasi_mobile_app/widgets/media_list.dart'; import 'package:varanasi_mobile_app/widgets/play_pause_button.dart'; @@ -128,8 +130,17 @@ class _LibraryContentState extends State { Row( key: titleKey, children: [ - AddToLibrary(state.playlist), - DownloadPlaylist(playlist: state.playlist), + AddToLibrary( + state.playlist, + sourceLibrary: state.sourceLibrary, + ), + DisableChild( + disabled: !kDebugMode && + state.sourceLibrary?.isDownload == true, + child: DownloadPlaylist( + playlist: state.playlist, + ), + ), const Spacer(), const ShuffleModeToggle(iconSize: 24), ], @@ -151,7 +162,7 @@ class _LibraryContentState extends State { } else { context.readMediaPlayerCubit.playFromMediaPlaylist( state.playlist.copyWith(mediaItems: sortedMediaItems), - index, + mediaItem, ); } }, diff --git a/lib/features/library/ui/library_widgets/sort_by_toggle.dart b/lib/features/library/ui/library_widgets/sort_by_toggle.dart index 84eaa2d..29f6151 100644 --- a/lib/features/library/ui/library_widgets/sort_by_toggle.dart +++ b/lib/features/library/ui/library_widgets/sort_by_toggle.dart @@ -1,11 +1,9 @@ -import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:go_router/go_router.dart'; import 'package:varanasi_mobile_app/cubits/config/config_cubit.dart'; import 'package:varanasi_mobile_app/models/sort_type.dart'; +import 'package:varanasi_mobile_app/utils/dialogs/app_dialog.dart'; import 'package:varanasi_mobile_app/utils/extensions/extensions.dart'; -import 'package:varanasi_mobile_app/utils/helpers/show_bottom_sheet.dart'; class SortByToggle extends StatelessWidget { const SortByToggle({super.key}); @@ -31,30 +29,12 @@ class SortByToggle extends StatelessWidget { ), child: const Text('Sort'), onPressed: () async { - final padding = MediaQuery.paddingOf(context); - // show dialog - final value = await showAppBottomSheet( + final value = await AppDialog.showOptionsPicker( context, - builder: (context) => ListView( - padding: padding.copyWith(left: 8, right: 8, top: 16), - children: [ - ListTile( - title: const Text('Sort by'), - titleTextStyle: context.textTheme.bodyLarge - ?.copyWith(fontWeight: FontWeight.bold), - ), - ...SortBy.values.map( - (e) => RadioListTile( - controlAffinity: ListTileControlAffinity.trailing, - groupValue: sortBy, - value: e, - onChanged: (value) => context.pop(value), - title: Text(describeEnum(e).capitalize), - selected: sortBy == e, - ), - ), - ], - ), + sortBy, + SortBy.values, + (e) => e.name.capitalize, + title: "Sort by", ); if (context.mounted && value != null) { context.read().setSortType(value); diff --git a/lib/features/search/data/search_repository.dart b/lib/features/search/data/search_repository.dart index aba75c8..1c4ee2a 100644 --- a/lib/features/search/data/search_repository.dart +++ b/lib/features/search/data/search_repository.dart @@ -2,7 +2,6 @@ import 'package:hive/hive.dart'; import 'package:varanasi_mobile_app/features/search/data/search_data_provider.dart'; import 'package:varanasi_mobile_app/features/search/data/top_search_result/top_search_result.dart'; import 'package:varanasi_mobile_app/utils/constants/strings.dart'; -import 'package:varanasi_mobile_app/utils/convert_nested_map.dart'; import 'package:varanasi_mobile_app/utils/logger.dart'; import 'package:varanasi_mobile_app/utils/mixins/cachable_mixin.dart'; @@ -28,30 +27,8 @@ class SearchRepository with CacheableService { final Map _searchResultsCache = {}; Future fetchTopSearchResults() async { - await initcache().then((value) { - if (value == null) return; - _box = value; - }); - final cached = maybeGetCached(AppStrings.topSearchesCacheKey); - if (cached != null) { - try { - final cachemap = convertNestedMap(cached); - final library = - SearchDataProvider.instance.parseTopSearchResult(cachemap); - return library; - } catch (e) { - /// If the cached data is corrupted, delete it - _logger.e(e); - deleteCache(AppStrings.topSearchesCacheKey); - } - } - final (response, searchResults) = + final (_, searchResults) = await SearchDataProvider.instance.fetchTopSearchResults(); - if (searchResults != null) { - cache(AppStrings.topSearchesCacheKey, response, const Duration(hours: 1)); - } else { - throw Exception('Failed to fetch top searches'); - } return searchResults; } diff --git a/lib/features/search/ui/widgets/trending_searches_carousel.dart b/lib/features/search/ui/widgets/trending_searches_carousel.dart index 29bc1c7..ee5345a 100644 --- a/lib/features/search/ui/widgets/trending_searches_carousel.dart +++ b/lib/features/search/ui/widgets/trending_searches_carousel.dart @@ -3,14 +3,66 @@ import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:varanasi_mobile_app/features/home/ui/home_widgets/media_carousel/media_carousel.dart'; import 'package:varanasi_mobile_app/features/search/cubit/search_cubit.dart'; import 'package:varanasi_mobile_app/models/media_playlist.dart'; +import 'package:varanasi_mobile_app/utils/extensions/theme.dart'; +import 'package:varanasi_mobile_app/widgets/shimmer_loader.dart'; class TrendingSearchesCarousel extends StatelessWidget { const TrendingSearchesCarousel({super.key}); @override Widget build(BuildContext context) { - final trendingSearches = context - .select((SearchCubit cubit) => cubit.state.topSearchResult?.data ?? []); + final (trendingSearches, isFetchingTopSearchResults) = context.select( + (SearchCubit cubit) => ( + cubit.state.topSearchResult?.data ?? [], + cubit.state.isFetchingTopSearchResults + )); + if (isFetchingTopSearchResults) { + return SliverToBoxAdapter( + child: SizedBox( + height: 220, + child: Column( + children: [ + Padding( + padding: const EdgeInsets.only(left: 8), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + 'Trending Searches', + style: context.textTheme.titleLarge + ?.copyWith(fontWeight: FontWeight.bold), + ), + ], + ), + ), + const SizedBox(height: 16), + SizedBox( + height: 140, + child: ListView.separated( + physics: const NeverScrollableScrollPhysics(), + shrinkWrap: true, + itemCount: 10, + itemBuilder: (context, index) => ShimmerLoader( + height: 120, + width: 140, + margin: index == 0 + ? const EdgeInsets.only(left: 8) + : index == 19 + ? const EdgeInsets.only(right: 8) + : EdgeInsets.zero, + decoration: + BoxDecoration(borderRadius: BorderRadius.circular(8)), + ), + separatorBuilder: (context, index) => + const SizedBox(width: 16), + scrollDirection: Axis.horizontal, + ), + ), + ], + ), + ), + ); + } return SliverToBoxAdapter( child: MediaCarousel( playlist: MediaPlaylist( diff --git a/lib/features/user-library/data/user_library.dart b/lib/features/user-library/data/user_library.dart index 570c1cd..d9b0fb2 100644 --- a/lib/features/user-library/data/user_library.dart +++ b/lib/features/user-library/data/user_library.dart @@ -14,7 +14,9 @@ enum UserLibraryType { @HiveField(1) album('album'), @HiveField(2) - playlist('playlist'); + playlist('playlist'), + @HiveField(3) + download('download'); // TODO: Add Artist final String type; @@ -24,10 +26,11 @@ enum UserLibraryType { bool get isFavorite => this == UserLibraryType.favorite; bool get isAlbum => this == UserLibraryType.album; bool get isPlaylist => this == UserLibraryType.playlist; + bool get isDownload => this == UserLibraryType.download; } @HiveType(typeId: 18) -class UserLibrary extends Equatable { +class UserLibrary with EquatableMixin implements Comparable { @HiveField(0) final UserLibraryType type; @HiveField(1) @@ -57,9 +60,10 @@ class UserLibrary extends Equatable { bool get isNotEmpty => mediaItems.isNotEmpty; - bool get isFavorite => type.isFavorite; + bool get isFavorite => type.isFavorite || id == "favorite"; bool get isAlbum => type.isAlbum; bool get isPlaylist => type.isPlaylist; + bool get isDownload => type.isDownload || id == "downloads"; const UserLibrary.empty(this.type) : id = "", @@ -97,6 +101,18 @@ class UserLibrary extends Equatable { images: images ?? this.images, ); } + + @override + int compareTo(UserLibrary other) { + // if id download then it should be first + if (isDownload) return -1; + if (other.isDownload) return 1; + // if id favorite then it should be first + if (isFavorite) return -1; + if (other.isFavorite) return 1; + // else sort by title + return (title ?? "").compareTo(other.title ?? ""); + } } @HiveType(typeId: 19) diff --git a/lib/features/user-library/data/user_library.g.dart b/lib/features/user-library/data/user_library.g.dart index a188d3a..cc10218 100644 --- a/lib/features/user-library/data/user_library.g.dart +++ b/lib/features/user-library/data/user_library.g.dart @@ -208,6 +208,8 @@ class UserLibraryTypeAdapter extends TypeAdapter { return UserLibraryType.album; case 2: return UserLibraryType.playlist; + case 3: + return UserLibraryType.download; default: return UserLibraryType.favorite; } @@ -225,6 +227,9 @@ class UserLibraryTypeAdapter extends TypeAdapter { case UserLibraryType.playlist: writer.writeByte(2); break; + case UserLibraryType.download: + writer.writeByte(3); + break; } } diff --git a/lib/features/user-library/ui/user_library_page.dart b/lib/features/user-library/ui/user_library_page.dart index cf3d084..7980408 100644 --- a/lib/features/user-library/ui/user_library_page.dart +++ b/lib/features/user-library/ui/user_library_page.dart @@ -1,21 +1,34 @@ import 'package:cached_network_image/cached_network_image.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:go_router/go_router.dart'; +import 'package:varanasi_mobile_app/cubits/download/download_cubit.dart'; import 'package:varanasi_mobile_app/features/user-library/cubit/user_library_cubit.dart'; +import 'package:varanasi_mobile_app/features/user-library/data/user_library.dart'; import 'package:varanasi_mobile_app/features/user-library/ui/widgets/add_playlist_button.dart'; import 'package:varanasi_mobile_app/flavors.dart'; import 'package:varanasi_mobile_app/gen/assets.gen.dart'; import 'package:varanasi_mobile_app/utils/extensions/extensions.dart'; +import 'package:varanasi_mobile_app/utils/helpers/get_app_context.dart'; import 'package:varanasi_mobile_app/utils/routes.dart'; +import 'package:varanasi_mobile_app/widgets/downloads_icon.dart'; import 'widgets/empty.dart'; -class UserLibraryPage extends StatelessWidget { +class UserLibraryPage extends HookWidget { const UserLibraryPage({super.key}); + DownloadCubit get downloadCubit => appContext.read(); + Stream get downloadLibraryStream => + downloadCubit.downloadLibraryStream; + @override Widget build(BuildContext context) { + final downloadSnapshot = useStream( + downloadLibraryStream, + initialData: downloadCubit.toUserLibrary(), + ); return Scaffold( appBar: AppBar( title: Row( @@ -38,23 +51,41 @@ class UserLibraryPage extends StatelessWidget { if (state is! UserLibraryLoaded) { return const Center(child: CircularProgressIndicator()); } - final library = state.library; + final library = [...state.library, downloadSnapshot.data] + .whereType() + .where((element) => element.isNotEmpty) + .toList() + ..sort(); if (library.isEmpty) return const EmptyUserLibrary(); return ListView.builder( itemBuilder: (context, index) { final item = library[index]; return ListTile( onTap: () => context.push(AppRoutes.library.path, extra: item), - leading: ClipRRect( - borderRadius: BorderRadius.circular(4), - child: CachedNetworkImage( - imageUrl: item.images.lastOrNull?.link ?? '', - height: 48, - width: 48, + leading: Visibility( + replacement: const DownloadsIcon(), + visible: !item.isDownload, + child: ClipRRect( + borderRadius: BorderRadius.circular(4), + child: CachedNetworkImage( + imageUrl: item.images.lastOrNull?.link ?? '', + height: 48, + width: 48, + ), ), ), title: Text(item.title ?? ''), - subtitle: Text(item.description ?? ''), + subtitle: Row( + children: [ + if (item.isDownload || item.isFavorite) + Icon( + Icons.push_pin_rounded, + size: 12, + color: context.colorScheme.primary, + ), + Expanded(child: Text(item.description ?? '')), + ], + ), ); }, itemCount: library.length, diff --git a/lib/utils/configs.dart b/lib/utils/configs.dart index 19b1b03..2862ad9 100644 --- a/lib/utils/configs.dart +++ b/lib/utils/configs.dart @@ -114,9 +114,9 @@ Config get appConfig { const server = kReleaseMode ? Server('https://saavn.aryak.dev') : Server('https://saavn.aryak.dev'); - return Config( + return const Config( env: kReleaseMode ? 'production' : 'development', - endpoint: const Endpoint( + endpoint: Endpoint( modules: '/modules', playlists: Playlists(id: 'playlists'), albums: Albums(link: 'albums'), @@ -131,6 +131,6 @@ Config get appConfig { ), ), server: server, - placeholderImageLink: '${server.baseUrl}/audio.jpg', + placeholderImageLink: 'http://192.168.31.130:3000/audio.jpg', ); } diff --git a/lib/utils/player/audio_handler_impl.dart b/lib/utils/player/audio_handler_impl.dart index b304dbf..21ce1e1 100644 --- a/lib/utils/player/audio_handler_impl.dart +++ b/lib/utils/player/audio_handler_impl.dart @@ -2,13 +2,16 @@ import 'dart:async'; import 'package:audio_service/audio_service.dart'; import 'package:audio_session/audio_session.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:hive/hive.dart'; import 'package:just_audio/just_audio.dart'; import 'package:rxdart/rxdart.dart'; import 'package:varanasi_mobile_app/cubits/config/config_cubit.dart'; +import 'package:varanasi_mobile_app/cubits/download/download_cubit.dart'; import 'package:varanasi_mobile_app/models/app_config.dart'; import 'package:varanasi_mobile_app/models/download.dart'; import 'package:varanasi_mobile_app/utils/constants/strings.dart'; +import 'package:varanasi_mobile_app/utils/helpers/get_app_context.dart'; import 'typings.dart'; @@ -183,13 +186,14 @@ final class AudioHandlerImpl extends BaseAudioHandler final downloaded = _isSongDownloaded(itemId); if (!downloaded) { final uri = Uri.parse(mediaItem.id); - final audioSource = LockCachingAudioSource(uri); + final cacheFile = + appContext.read().getCacheFile(itemId, mediaItem.id); + final audioSource = LockCachingAudioSource(uri, cacheFile: cacheFile); _mediaItemExpando[audioSource] = mediaItem; return audioSource; } - final box = Hive.box(AppStrings.downloadBoxName); - final downloadedMedia = box.get(itemId); - final audioSource = AudioSource.file(downloadedMedia!.path); + final path = appContext.read().getDownloadPath(itemId); + final audioSource = AudioSource.file(path); _mediaItemExpando[audioSource] = mediaItem; return audioSource; } @@ -199,7 +203,8 @@ final class AudioHandlerImpl extends BaseAudioHandler @override Future addQueueItem(MediaItem mediaItem) async { - await _playlist.add(_itemToSource(mediaItem)); + final source = _itemToSource(mediaItem); + await _playlist.add(source); } @override @@ -209,7 +214,8 @@ final class AudioHandlerImpl extends BaseAudioHandler @override Future insertQueueItem(int index, MediaItem mediaItem) async { - await _playlist.insert(index, _itemToSource(mediaItem)); + final source = _itemToSource(mediaItem); + await _playlist.insert(index, source); } @override diff --git a/lib/widgets/add_to_library.dart b/lib/widgets/add_to_library.dart index ff7d8f6..f40884c 100644 --- a/lib/widgets/add_to_library.dart +++ b/lib/widgets/add_to_library.dart @@ -1,21 +1,25 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:varanasi_mobile_app/features/user-library/cubit/user_library_cubit.dart'; +import 'package:varanasi_mobile_app/features/user-library/data/user_library.dart'; import 'package:varanasi_mobile_app/models/media_playlist.dart'; class AddToLibrary extends StatelessWidget { final MediaPlaylist playlist; - const AddToLibrary(this.playlist, {super.key}); + final UserLibrary? sourceLibrary; + const AddToLibrary(this.playlist, {super.key, this.sourceLibrary}); @override Widget build(BuildContext context) { return BlocBuilder( builder: (context, state) { if (state is! UserLibraryLoaded) return const SizedBox.shrink(); - final isAdded = state.isAdded(playlist); - AnimatedIcons.add_event; + final isInternalLibrary = sourceLibrary?.isDownload == true || + sourceLibrary?.isFavorite == true; + final isAdded = state.isAdded(playlist) || isInternalLibrary; return IconButton( onPressed: () { + if (isInternalLibrary) return; final cubit = context.read(); if (isAdded) { cubit.removeFromLibrary(playlist); diff --git a/lib/widgets/disable_child.dart b/lib/widgets/disable_child.dart new file mode 100644 index 0000000..b637ece --- /dev/null +++ b/lib/widgets/disable_child.dart @@ -0,0 +1,22 @@ +import 'package:flutter/material.dart'; + +class DisableChild extends StatelessWidget { + const DisableChild({ + super.key, + this.disabled = true, + required this.child, + }); + final bool disabled; + + final Widget child; + @override + Widget build(BuildContext context) { + return IgnorePointer( + ignoring: disabled, + child: Opacity( + opacity: disabled ? 0.5 : 1, + child: child, + ), + ); + } +} diff --git a/lib/widgets/download_button.dart b/lib/widgets/download_button.dart index 9fea6fd..29a525a 100644 --- a/lib/widgets/download_button.dart +++ b/lib/widgets/download_button.dart @@ -7,7 +7,6 @@ import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:hive_flutter/hive_flutter.dart'; import 'package:percent_indicator/circular_percent_indicator.dart'; import 'package:varanasi_mobile_app/cubits/download/download_cubit.dart'; -import 'package:varanasi_mobile_app/features/library/cubit/library_cubit.dart'; import 'package:varanasi_mobile_app/features/user-library/cubit/user_library_cubit.dart'; import 'package:varanasi_mobile_app/models/download.dart'; import 'package:varanasi_mobile_app/models/media_playlist.dart'; @@ -38,11 +37,6 @@ class DownloadButton extends StatelessWidget { } else { cubit.downloadSong(media); } - final libraryState = context.read().state; - if (libraryState is LibraryLoaded) { - final playlist = libraryState.playlist; - context.read().addToLibrary(playlist); - } HapticFeedback.mediumImpact(); }, icon: DownloadStatus( @@ -67,7 +61,7 @@ class DownloadPlaylist extends StatelessWidget { @override Widget build(BuildContext context) { final progress = context.select((DownloadCubit value) => - value.state.playlistProgressMap[playlist.id] ?? 0); + value.loadedState.playlistProgressMap[playlist.id] ?? 0); final downloadBox = context.read().downloadBox; final keys = playlist.mediaItems?.map((e) => e.itemId).toList(); return ValueListenableBuilder( diff --git a/lib/widgets/downloads_icon.dart b/lib/widgets/downloads_icon.dart new file mode 100644 index 0000000..06fafad --- /dev/null +++ b/lib/widgets/downloads_icon.dart @@ -0,0 +1,25 @@ +import 'package:flutter/material.dart'; +import 'package:varanasi_mobile_app/utils/extensions/extensions.dart'; + +class DownloadsIcon extends StatelessWidget { + const DownloadsIcon({ + super.key, + this.dimension = 48, + }); + + final double dimension; + + @override + Widget build(BuildContext context) { + return Container( + height: dimension, + width: dimension, + decoration: BoxDecoration( + color: context.colorScheme.secondaryContainer, + borderRadius: BorderRadius.circular(4), + ), + child: Icon(Icons.download_rounded, + color: context.theme.colorScheme.onSecondaryContainer), + ); + } +} diff --git a/pubspec.lock b/pubspec.lock index d65156d..eace605 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -897,7 +897,7 @@ packages: source: hosted version: "0.3.3+3" path: - dependency: transitive + dependency: "direct main" description: name: path sha256: "8829d8a55c13fc0e37127c29fedf290c102f4e40ae94ada574091fe0ff96c917" @@ -913,13 +913,13 @@ packages: source: hosted version: "1.0.1" path_provider: - dependency: transitive + dependency: "direct main" description: name: path_provider - sha256: "3087813781ab814e4157b172f1a11c46be20179fcc9bea043e0fba36bc0acaa2" + sha256: a1aa8aaa2542a6bc57e381f132af822420216c80d4781f7aa085ca3229208aaa url: "https://pub.dev" source: hosted - version: "2.0.15" + version: "2.1.1" path_provider_android: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index 450aa9c..d1bbea3 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -16,7 +16,7 @@ publish_to: "none" # Remove this line if you wish to publish to pub.dev # https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html # In Windows, build-name is used as the major, minor, and patch parts # of the product and file versions while build-number is used as the build suffix. -version: 0.1.3+17 +version: 0.1.4+18 environment: sdk: ">=3.0.6 <4.0.0" @@ -75,6 +75,8 @@ dependencies: background_downloader: ^7.10.1 lottie: ^2.7.0 percent_indicator: ^4.2.3 + path_provider: ^2.1.1 + path: ^1.8.3 dev_dependencies: flutter_test: