From 5cea80b04a73d5be8afbd31d82522ecbdee832b1 Mon Sep 17 00:00:00 2001 From: devaryakjha Date: Wed, 18 Oct 2023 04:33:09 +0530 Subject: [PATCH 01/18] =?UTF-8?q?=F0=9F=90=9B=20Fix:=20download=20box=20ca?= =?UTF-8?q?using=20issues?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/cubits/download/download_cubit.dart | 3 +-- lib/main.dart | 2 ++ lib/utils/constants/strings.dart | 1 - 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/cubits/download/download_cubit.dart b/lib/cubits/download/download_cubit.dart index a6badf6..10794c5 100644 --- a/lib/cubits/download/download_cubit.dart +++ b/lib/cubits/download/download_cubit.dart @@ -27,8 +27,7 @@ class DownloadCubit extends AppCubit { @override FutureOr init() async { _songMap = {}; - _downloadBox = - await Hive.openBox(AppStrings.downloadBoxName); + _downloadBox = Hive.box(AppStrings.downloadBoxName); _downloader = FileDownloader(); _downloader.updates.listen((update) { if (update is TaskStatusUpdate) { diff --git a/lib/main.dart b/lib/main.dart index aa06b95..64b5a0d 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -3,6 +3,7 @@ import 'package:flutter/services.dart'; import 'package:flutter_native_splash/flutter_native_splash.dart'; import 'package:hive_flutter/hive_flutter.dart'; import 'package:varanasi_mobile_app/features/home/data/models/adapters.dart'; +import 'package:varanasi_mobile_app/models/download.dart'; import 'app.dart'; import 'models/adapters.dart'; @@ -16,6 +17,7 @@ Future main() async { registerHomePageTypeAdapters(); await AppConfig.openBox(); await Hive.openBox(AppStrings.commonCacheBoxName); + await Hive.openBox(AppStrings.downloadBoxName); FlutterNativeSplash.remove(); runApp(const Varanasi()); } diff --git a/lib/utils/constants/strings.dart b/lib/utils/constants/strings.dart index 39aa1df..829490e 100644 --- a/lib/utils/constants/strings.dart +++ b/lib/utils/constants/strings.dart @@ -3,7 +3,6 @@ class AppStrings { static const String configBoxName = 'config'; static const String commonCacheBoxName = 'project_varanasi_cache_box'; static const String downloadBoxName = 'download_box'; - static const String downloadRecordsBoxName = 'download_records_box'; static const String currentPlaylistCacheKey = 'current_playlist'; static const String currentPlaylistIndexCacheKey = 'current_playlist_index'; static const String currentPlaylistPositionKey = 'current_playlist_position'; From 84b6fdd55f639aa783844a75d86000bc48357d89 Mon Sep 17 00:00:00 2001 From: devaryakjha Date: Sun, 22 Oct 2023 13:49:51 +0530 Subject: [PATCH 02/18] =?UTF-8?q?=E2=9C=A8=20Add:=20Download=20icons?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- assets/icon/download.svg | 11 +++++++++++ assets/icon/download_filled.svg | 10 ++++++++++ assets/icon/downloading.svg | 9 +++++++++ 3 files changed, 30 insertions(+) create mode 100644 assets/icon/download.svg create mode 100644 assets/icon/download_filled.svg create mode 100644 assets/icon/downloading.svg diff --git a/assets/icon/download.svg b/assets/icon/download.svg new file mode 100644 index 0000000..04e1c3c --- /dev/null +++ b/assets/icon/download.svg @@ -0,0 +1,11 @@ + + + + + + + + + \ No newline at end of file diff --git a/assets/icon/download_filled.svg b/assets/icon/download_filled.svg new file mode 100644 index 0000000..119b5d0 --- /dev/null +++ b/assets/icon/download_filled.svg @@ -0,0 +1,10 @@ + + + + + + + + \ No newline at end of file diff --git a/assets/icon/downloading.svg b/assets/icon/downloading.svg new file mode 100644 index 0000000..801c418 --- /dev/null +++ b/assets/icon/downloading.svg @@ -0,0 +1,9 @@ + + + + + + + + + \ No newline at end of file From 5ad1bc4a4ef7809793ee1e5b839a98dc733b0752 Mon Sep 17 00:00:00 2001 From: devaryakjha Date: Sun, 22 Oct 2023 13:50:18 +0530 Subject: [PATCH 03/18] =?UTF-8?q?=E2=9C=A8=20Add:=20User=20Library?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/app.dart | 5 + .../cubit/user_library_cubit.dart | 41 ++- .../cubit/user_library_state.dart | 25 ++ .../user-library/data/user_library.dart | 133 ++++++++++ .../user-library/data/user_library.g.dart | 240 ++++++++++++++++++ .../data/user_library_repository.dart | 55 ++++ .../user-library/ui/user_library_page.dart | 27 +- 7 files changed, 523 insertions(+), 3 deletions(-) create mode 100644 lib/features/user-library/data/user_library.dart create mode 100644 lib/features/user-library/data/user_library.g.dart create mode 100644 lib/features/user-library/data/user_library_repository.dart diff --git a/lib/app.dart b/lib/app.dart index af5abb0..a972f01 100644 --- a/lib/app.dart +++ b/lib/app.dart @@ -1,5 +1,6 @@ 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/utils/constants/constants.dart'; import 'package:varanasi_mobile_app/utils/router.dart'; import 'package:varanasi_mobile_app/widgets/responsive_sizer.dart'; @@ -21,6 +22,10 @@ class Varanasi extends StatelessWidget { return MultiBlocProvider( providers: [ BlocProvider(lazy: false, create: (_) => DownloadCubit()..init()), + BlocProvider( + lazy: false, + create: (_) => UserLibraryCubit()..init(), + ), BlocProvider( lazy: false, create: (context) => ConfigCubit()..init(), diff --git a/lib/features/user-library/cubit/user_library_cubit.dart b/lib/features/user-library/cubit/user_library_cubit.dart index 19812e1..d6bd887 100644 --- a/lib/features/user-library/cubit/user_library_cubit.dart +++ b/lib/features/user-library/cubit/user_library_cubit.dart @@ -1,8 +1,45 @@ -import 'package:bloc/bloc.dart'; +import 'dart:async'; + import 'package:equatable/equatable.dart'; +import 'package:varanasi_mobile_app/features/user-library/data/user_library.dart'; +import 'package:varanasi_mobile_app/features/user-library/data/user_library_repository.dart'; +import 'package:varanasi_mobile_app/models/media_playlist.dart'; +import 'package:varanasi_mobile_app/models/song.dart'; +import 'package:varanasi_mobile_app/utils/app_cubit.dart'; +import 'package:varanasi_mobile_app/utils/app_snackbar.dart'; part 'user_library_state.dart'; -class UserLibraryCubit extends Cubit { +class UserLibraryCubit extends AppCubit { UserLibraryCubit() : super(UserLibraryInitial()); + + UserLibraryRepository get _repository => UserLibraryRepository(); + + @override + FutureOr init() async { + await _repository.init(); + final library = _repository.getLibraries(); + emit(UserLibraryLoaded(library: library)); + } + + Future favoriteSong(Song song) async { + await _repository.favoriteSong(song); + final library = _repository.getLibraries(); + emit(UserLibraryLoaded(library: library)); + AppSnackbar.show("Added to favorites"); + } + + Future unfavoriteSong(Song song) async { + await _repository.unfavoriteSong(song); + final library = _repository.getLibraries(); + emit(UserLibraryLoaded(library: library)); + AppSnackbar.show("Removed from favorites"); + } + + Future addToLibrary(MediaPlaylist playlist) async { + _repository.addLibrary(playlist.toUserLibrary()); + final library = _repository.getLibraries(); + emit(UserLibraryLoaded(library: library)); + AppSnackbar.show("Added to library"); + } } diff --git a/lib/features/user-library/cubit/user_library_state.dart b/lib/features/user-library/cubit/user_library_state.dart index 232dfcd..f587519 100644 --- a/lib/features/user-library/cubit/user_library_state.dart +++ b/lib/features/user-library/cubit/user_library_state.dart @@ -1,3 +1,4 @@ +// ignore_for_file: public_member_api_docs, sort_constructors_first part of 'user_library_cubit.dart'; sealed class UserLibraryState extends Equatable { @@ -8,3 +9,27 @@ sealed class UserLibraryState extends Equatable { } final class UserLibraryInitial extends UserLibraryState {} + +class UserLibraryLoaded extends UserLibraryState { + final List library; + + const UserLibraryLoaded({ + required this.library, + }); + + UserLibraryLoaded copyWith({ + List? library, + }) { + return UserLibraryLoaded( + library: library ?? this.library, + ); + } + + @override + List get props => [library]; + + Favorite get favorite => library.firstWhere( + (library) => library is Favorite, + orElse: () => const Favorite.empty(), + ) as Favorite; +} diff --git a/lib/features/user-library/data/user_library.dart b/lib/features/user-library/data/user_library.dart new file mode 100644 index 0000000..a4d0721 --- /dev/null +++ b/lib/features/user-library/data/user_library.dart @@ -0,0 +1,133 @@ +import 'package:equatable/equatable.dart'; +import 'package:hive/hive.dart'; +import 'package:varanasi_mobile_app/models/image.dart'; +import 'package:varanasi_mobile_app/models/media_playlist.dart'; +import 'package:varanasi_mobile_app/models/song.dart'; + +part 'user_library.g.dart'; + +@HiveType(typeId: 22) +enum UserLibraryType { + @HiveField(0) + favorite('favorite'), + @HiveField(1) + album('album'), + @HiveField(2) + playlist('playlist'); + // TODO: Add Artist + + final String type; + + const UserLibraryType(this.type); + + bool get isFavorite => this == UserLibraryType.favorite; + bool get isAlbum => this == UserLibraryType.album; + bool get isPlaylist => this == UserLibraryType.playlist; +} + +@HiveType(typeId: 18) +class UserLibrary extends Equatable { + @HiveField(0) + final UserLibraryType type; + @HiveField(1) + final String id; + @HiveField(2) + final String? title; + @HiveField(3) + final String? description; + @HiveField(4, defaultValue: []) + final List mediaItems; + @HiveField(5, defaultValue: []) + final List images; + + const UserLibrary({ + required this.id, + this.title, + this.description, + this.mediaItems = const [], + this.images = const [], + required this.type, + }); + + @override + List get props => [id, title, description, mediaItems, images]; + + bool get isEmpty => mediaItems.isEmpty; + + bool get isNotEmpty => mediaItems.isNotEmpty; + + bool get isFavorite => type.isFavorite; + bool get isAlbum => type.isAlbum; + bool get isPlaylist => type.isPlaylist; + + const UserLibrary.empty(this.type) + : id = "", + title = null, + description = null, + mediaItems = const [], + images = const []; + + @override + bool? get stringify => true; + + MediaPlaylist toMediaPlaylist() { + return MediaPlaylist( + id: id, + title: title, + description: description, + images: images, + mediaItems: mediaItems, + ); + } +} + +@HiveType(typeId: 19) +final class Favorite extends UserLibrary { + const Favorite({ + required super.mediaItems, + }) : super( + type: UserLibraryType.favorite, + images: const [Image.likedSongs], + id: "favorite", + title: "Liked Songs", + description: "Songs you liked", + ); + + static const String boxKey = "favorite"; + + const Favorite.empty() : super.empty(UserLibraryType.favorite); + + Favorite copyWith({ + List? mediaItems, + }) { + return Favorite( + mediaItems: mediaItems ?? this.mediaItems, + ); + } +} + +@HiveType(typeId: 20) +final class AlbumLibrary extends UserLibrary { + const AlbumLibrary({ + required super.id, + super.title, + super.description, + required super.mediaItems, + required super.images, + }) : super(type: UserLibraryType.album); + + const AlbumLibrary.empty() : super.empty(UserLibraryType.album); +} + +@HiveType(typeId: 21) +final class PlaylistLibrary extends UserLibrary { + const PlaylistLibrary({ + required super.id, + super.title, + super.description, + required super.mediaItems, + required super.images, + }) : super(type: UserLibraryType.playlist); + + const PlaylistLibrary.empty() : super.empty(UserLibraryType.playlist); +} diff --git a/lib/features/user-library/data/user_library.g.dart b/lib/features/user-library/data/user_library.g.dart new file mode 100644 index 0000000..a188d3a --- /dev/null +++ b/lib/features/user-library/data/user_library.g.dart @@ -0,0 +1,240 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'user_library.dart'; + +// ************************************************************************** +// TypeAdapterGenerator +// ************************************************************************** + +class UserLibraryAdapter extends TypeAdapter { + @override + final int typeId = 18; + + @override + UserLibrary read(BinaryReader reader) { + final numOfFields = reader.readByte(); + final fields = { + for (int i = 0; i < numOfFields; i++) reader.readByte(): reader.read(), + }; + return UserLibrary( + id: fields[1] as String, + title: fields[2] as String?, + description: fields[3] as String?, + mediaItems: fields[4] == null ? [] : (fields[4] as List).cast(), + images: fields[5] == null ? [] : (fields[5] as List).cast(), + type: fields[0] as UserLibraryType, + ); + } + + @override + void write(BinaryWriter writer, UserLibrary obj) { + writer + ..writeByte(6) + ..writeByte(0) + ..write(obj.type) + ..writeByte(1) + ..write(obj.id) + ..writeByte(2) + ..write(obj.title) + ..writeByte(3) + ..write(obj.description) + ..writeByte(4) + ..write(obj.mediaItems) + ..writeByte(5) + ..write(obj.images); + } + + @override + int get hashCode => typeId.hashCode; + + @override + bool operator ==(Object other) => + identical(this, other) || + other is UserLibraryAdapter && + runtimeType == other.runtimeType && + typeId == other.typeId; +} + +class FavoriteAdapter extends TypeAdapter { + @override + final int typeId = 19; + + @override + Favorite read(BinaryReader reader) { + final numOfFields = reader.readByte(); + final fields = { + for (int i = 0; i < numOfFields; i++) reader.readByte(): reader.read(), + }; + return Favorite( + mediaItems: fields[4] == null ? [] : (fields[4] as List).cast(), + ); + } + + @override + void write(BinaryWriter writer, Favorite obj) { + writer + ..writeByte(6) + ..writeByte(0) + ..write(obj.type) + ..writeByte(1) + ..write(obj.id) + ..writeByte(2) + ..write(obj.title) + ..writeByte(3) + ..write(obj.description) + ..writeByte(4) + ..write(obj.mediaItems) + ..writeByte(5) + ..write(obj.images); + } + + @override + int get hashCode => typeId.hashCode; + + @override + bool operator ==(Object other) => + identical(this, other) || + other is FavoriteAdapter && + runtimeType == other.runtimeType && + typeId == other.typeId; +} + +class AlbumLibraryAdapter extends TypeAdapter { + @override + final int typeId = 20; + + @override + AlbumLibrary read(BinaryReader reader) { + final numOfFields = reader.readByte(); + final fields = { + for (int i = 0; i < numOfFields; i++) reader.readByte(): reader.read(), + }; + return AlbumLibrary( + id: fields[1] as String, + title: fields[2] as String?, + description: fields[3] as String?, + mediaItems: fields[4] == null ? [] : (fields[4] as List).cast(), + images: fields[5] == null ? [] : (fields[5] as List).cast(), + ); + } + + @override + void write(BinaryWriter writer, AlbumLibrary obj) { + writer + ..writeByte(6) + ..writeByte(0) + ..write(obj.type) + ..writeByte(1) + ..write(obj.id) + ..writeByte(2) + ..write(obj.title) + ..writeByte(3) + ..write(obj.description) + ..writeByte(4) + ..write(obj.mediaItems) + ..writeByte(5) + ..write(obj.images); + } + + @override + int get hashCode => typeId.hashCode; + + @override + bool operator ==(Object other) => + identical(this, other) || + other is AlbumLibraryAdapter && + runtimeType == other.runtimeType && + typeId == other.typeId; +} + +class PlaylistLibraryAdapter extends TypeAdapter { + @override + final int typeId = 21; + + @override + PlaylistLibrary read(BinaryReader reader) { + final numOfFields = reader.readByte(); + final fields = { + for (int i = 0; i < numOfFields; i++) reader.readByte(): reader.read(), + }; + return PlaylistLibrary( + id: fields[1] as String, + title: fields[2] as String?, + description: fields[3] as String?, + mediaItems: fields[4] == null ? [] : (fields[4] as List).cast(), + images: fields[5] == null ? [] : (fields[5] as List).cast(), + ); + } + + @override + void write(BinaryWriter writer, PlaylistLibrary obj) { + writer + ..writeByte(6) + ..writeByte(0) + ..write(obj.type) + ..writeByte(1) + ..write(obj.id) + ..writeByte(2) + ..write(obj.title) + ..writeByte(3) + ..write(obj.description) + ..writeByte(4) + ..write(obj.mediaItems) + ..writeByte(5) + ..write(obj.images); + } + + @override + int get hashCode => typeId.hashCode; + + @override + bool operator ==(Object other) => + identical(this, other) || + other is PlaylistLibraryAdapter && + runtimeType == other.runtimeType && + typeId == other.typeId; +} + +class UserLibraryTypeAdapter extends TypeAdapter { + @override + final int typeId = 22; + + @override + UserLibraryType read(BinaryReader reader) { + switch (reader.readByte()) { + case 0: + return UserLibraryType.favorite; + case 1: + return UserLibraryType.album; + case 2: + return UserLibraryType.playlist; + default: + return UserLibraryType.favorite; + } + } + + @override + void write(BinaryWriter writer, UserLibraryType obj) { + switch (obj) { + case UserLibraryType.favorite: + writer.writeByte(0); + break; + case UserLibraryType.album: + writer.writeByte(1); + break; + case UserLibraryType.playlist: + writer.writeByte(2); + break; + } + } + + @override + int get hashCode => typeId.hashCode; + + @override + bool operator ==(Object other) => + identical(this, other) || + other is UserLibraryTypeAdapter && + runtimeType == other.runtimeType && + typeId == other.typeId; +} diff --git a/lib/features/user-library/data/user_library_repository.dart b/lib/features/user-library/data/user_library_repository.dart new file mode 100644 index 0000000..ed15306 --- /dev/null +++ b/lib/features/user-library/data/user_library_repository.dart @@ -0,0 +1,55 @@ +import 'package:hive_flutter/adapters.dart'; +import 'package:varanasi_mobile_app/features/user-library/data/user_library.dart'; +import 'package:varanasi_mobile_app/models/song.dart'; +import 'package:varanasi_mobile_app/utils/constants/strings.dart'; + +class UserLibraryRepository { + UserLibraryRepository._(); + + static final UserLibraryRepository _instance = UserLibraryRepository._(); + + factory UserLibraryRepository() => _instance; + + late final Box _box; + + Box get box => _box; + + Future init() async { + _box = await Hive.openBox(AppStrings.userLibraryCacheKey); + } + + List getLibraries() => _box.values.toList(); + + Future addLibrary(E library) => + _box.add(library); + + Future updateLibrary(E library) => + _box.put(library.id, library); + + Future deleteLibrary(E library) => + _box.delete(library.id); + + Future clearLibrary() => _box.clear(); + + Future favoriteSong(Song song) async { + final favourites = _box.get( + Favorite.boxKey, + defaultValue: const Favorite.empty(), + ) as Favorite; + final newFavourites = favourites.copyWith( + mediaItems: [...favourites.mediaItems, song], + ); + await _box.put(Favorite.boxKey, newFavourites); + } + + Future unfavoriteSong(Song song) async { + final favourites = _box.get( + Favorite.boxKey, + defaultValue: const Favorite.empty(), + ) as Favorite; + final newFavourites = favourites.copyWith( + mediaItems: favourites.mediaItems.where((e) => e.id != song.id).toList(), + ); + await _box.put(Favorite.boxKey, newFavourites); + } +} diff --git a/lib/features/user-library/ui/user_library_page.dart b/lib/features/user-library/ui/user_library_page.dart index 6d51fb3..ce56d79 100644 --- a/lib/features/user-library/ui/user_library_page.dart +++ b/lib/features/user-library/ui/user_library_page.dart @@ -1,10 +1,35 @@ 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'; class UserLibraryPage extends StatelessWidget { const UserLibraryPage({super.key}); @override Widget build(BuildContext context) { - return const Placeholder(); + return Scaffold( + appBar: AppBar( + title: const Text('Your Library'), + centerTitle: false, + ), + body: BlocBuilder( + builder: (context, state) { + if (state is! UserLibraryLoaded) { + return const Center(child: CircularProgressIndicator()); + } + final library = + state.library.map((e) => e.toMediaPlaylist()).toList(); + return ListView.builder( + itemBuilder: (context, index) { + final item = library[index]; + return ListTile( + leading: Text(item.title ?? ''), + ); + }, + itemCount: library.length, + ); + }, + ), + ); } } From 1e166be19fb548b01f2402e10ebba2365a31af35 Mon Sep 17 00:00:00 2001 From: devaryakjha Date: Sun, 22 Oct 2023 13:50:39 +0530 Subject: [PATCH 04/18] =?UTF-8?q?=F0=9F=9A=A7=20WIP:=20User=20Library=20an?= =?UTF-8?q?d=20downloads?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/cubits/config/config_cubit.dart | 21 ++++++ lib/cubits/download/download_cubit.dart | 52 +++++++++++++- lib/gen/assets.gen.dart | 14 +++- lib/models/adapters.dart | 6 ++ lib/models/image.dart | 8 ++- lib/models/media_playlist.dart | 12 ++++ lib/utils/app_snackbar.dart | 34 ++++++--- lib/utils/constants/strings.dart | 1 + lib/widgets/download_button.dart | 69 +++++++++++-------- lib/widgets/favorite_button.dart | 41 +++++++++++ lib/widgets/media_tile.dart | 8 +-- .../full_screen_player.dart | 31 +++++++-- .../player/full_screen_player/media_info.dart | 15 ++-- 13 files changed, 255 insertions(+), 57 deletions(-) create mode 100644 lib/widgets/favorite_button.dart diff --git a/lib/cubits/config/config_cubit.dart b/lib/cubits/config/config_cubit.dart index 669f5eb..b9d1297 100644 --- a/lib/cubits/config/config_cubit.dart +++ b/lib/cubits/config/config_cubit.dart @@ -5,9 +5,11 @@ import 'package:equatable/equatable.dart'; import 'package:hive_flutter/adapters.dart'; import 'package:package_info_plus/package_info_plus.dart'; import 'package:palette_generator/palette_generator.dart'; +import 'package:rxdart/rxdart.dart'; import 'package:sliding_up_panel/sliding_up_panel.dart'; import 'package:varanasi_mobile_app/models/app_config.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/sort_type.dart'; import 'package:varanasi_mobile_app/utils/app_cubit.dart'; import 'package:varanasi_mobile_app/utils/constants/strings.dart'; @@ -24,6 +26,8 @@ class ConfigCubit extends AppCubit { ConfigCubit() : super(ConfigInitial()); + Box get cacheBox => _cacheBox; + @override void init() async { final packageInfo = await PackageInfo.fromPlatform(); @@ -118,6 +122,10 @@ class ConfigCubit extends AppCubit { return _cacheBox.get(AppStrings.currentPlaylistCacheKey); } + Stream get savedPlaylistStream => _cacheBox + .watch(key: AppStrings.currentPlaylistCacheKey) + .map((event) => event.value); + Future saveCurrentPlaylistIndex(int index) { return _cacheBox.put(AppStrings.currentPlaylistIndexCacheKey, index); } @@ -130,6 +138,10 @@ class ConfigCubit extends AppCubit { return _cacheBox.get(AppStrings.currentPlaylistIndexCacheKey); } + Stream get savedPlaylistIndexStream => _cacheBox + .watch(key: AppStrings.currentPlaylistIndexCacheKey) + .map((event) => event.value); + Future saveCurrentPosition(Duration duration) { return _cacheBox.put( AppStrings.currentPlaylistPositionKey, @@ -165,4 +177,13 @@ class ConfigCubit extends AppCubit { ConfigLoaded? get configOrNull => state is ConfigLoaded ? state as ConfigLoaded : null; + + PlayableMedia? get currentMedia => + savedPlaylist?.mediaItems?[savedPlaylistIndex ?? 0]; + + Stream get currentMediaStream => + Rx.combineLatest2(savedPlaylistStream, savedPlaylistIndexStream, (a, b) { + if (a == null || b == null) return null; + return a.mediaItems?[b]; + }); } diff --git a/lib/cubits/download/download_cubit.dart b/lib/cubits/download/download_cubit.dart index 10794c5..a7a1d1f 100644 --- a/lib/cubits/download/download_cubit.dart +++ b/lib/cubits/download/download_cubit.dart @@ -1,4 +1,5 @@ import 'dart:async'; +import 'dart:io'; import 'package:background_downloader/background_downloader.dart'; import 'package:equatable/equatable.dart'; @@ -24,6 +25,8 @@ class DownloadCubit extends AppCubit { Logger get _logger => Logger.instance; + Box get downloadBox => _downloadBox; + @override FutureOr init() async { _songMap = {}; @@ -88,6 +91,9 @@ class DownloadCubit extends AppCubit { ), ); _logger.i('Download complete for ${item.id} path: $path'); + } else if (update.status == TaskStatus.canceled) { + _downloadBox.delete(item.id); + _logger.i('Download canceled for ${item.id}'); } else if (update.status.isFinalState) { _downloadBox.delete(item.id); _logger.i('Download failed for ${item.id}'); @@ -141,6 +147,12 @@ class DownloadCubit extends AppCubit { Future cancelDownload(PlayableMedia media) => _downloader.cancelTaskWithId(media.itemId); + Future batchCancel(MediaPlaylist media) async { + await _downloader.cancelTasksWithIds( + media.mediaItems?.map((e) => e.itemId).toList() ?? []); + await batchDelete(media); + } + /// Returns a stream of [DownloadedMedia] for the given [song]. /// /// The stream will emit the current download status of the song and @@ -170,6 +182,42 @@ class DownloadCubit extends AppCubit { DownloadedMedia? getDownloadedMedia(PlayableMedia song) => _downloadBox.get(song.itemId); - Future deleteDownloadedMedia(PlayableMedia song) => - _downloadBox.delete(song.itemId); + Future deleteDownloadedMedia(PlayableMedia song) { + final item = _downloadBox.get(song.itemId); + if (item?.path.isNotEmpty ?? false) { + _deleteFile(item!.path); + } + return _downloadBox.delete(song.itemId); + } + + Future batchDelete(MediaPlaylist song) { + final keys = song.mediaItems?.map((e) => e.itemId) ?? []; + final values = keys + .map((e) => _downloadBox.get(e)) + .whereType() + .toList(); + // delete files from disk + for (final value in values) { + if (value.path.isNotEmpty) { + _deleteFile(value.path); + } + } + return _downloadBox.deleteAll(song.mediaItems?.map((e) => e.itemId) ?? []); + } + + Future _deleteFile(String path) async { + try { + final file = File(path); + final exists = file.existsSync(); + if (!exists) { + _logger.i('File does not exist: $path'); + return Future.value(null); + } + await file.delete(recursive: true); + _logger.i('Deleted file: $path'); + } catch (e) { + _logger.e('Error deleting file: $e'); + return Future.value(null); + } + } } diff --git a/lib/gen/assets.gen.dart b/lib/gen/assets.gen.dart index 25e8b24..0d77156 100644 --- a/lib/gen/assets.gen.dart +++ b/lib/gen/assets.gen.dart @@ -21,10 +21,22 @@ class $AssetsIconGen { SvgGenImage get appIconMonotone => const SvgGenImage('assets/icon/app_icon_monotone.svg'); + /// File path: assets/icon/download.svg + SvgGenImage get download => const SvgGenImage('assets/icon/download.svg'); + + /// File path: assets/icon/download_filled.svg + SvgGenImage get downloadFilled => + const SvgGenImage('assets/icon/download_filled.svg'); + + /// File path: assets/icon/downloading.svg + SvgGenImage get downloading => + const SvgGenImage('assets/icon/downloading.svg'); + $AssetsIconNavGen get nav => const $AssetsIconNavGen(); /// List of all assets - List get values => [appIcon, appIconMonotone]; + List get values => + [appIcon, appIconMonotone, download, downloadFilled, downloading]; } class $AssetsImagesGen { diff --git a/lib/models/adapters.dart b/lib/models/adapters.dart index b2a5a4a..6b84042 100644 --- a/lib/models/adapters.dart +++ b/lib/models/adapters.dart @@ -1,4 +1,5 @@ import 'package:hive/hive.dart'; +import 'package:varanasi_mobile_app/features/user-library/data/user_library.dart'; import 'package:varanasi_mobile_app/models/download.dart'; import 'package:varanasi_mobile_app/models/sort_type.dart'; @@ -25,4 +26,9 @@ void registerCommonTypeAdapters() { Hive.registerAdapter(DownloadedMediaAdapter()); Hive.registerAdapter(DownloadUrlAdapter()); Hive.registerAdapter(MediaPlaylistAdapter()); + Hive.registerAdapter(UserLibraryTypeAdapter()); + Hive.registerAdapter(UserLibraryAdapter()); + Hive.registerAdapter(FavoriteAdapter()); + Hive.registerAdapter(AlbumLibraryAdapter()); + Hive.registerAdapter(PlaylistLibraryAdapter()); } diff --git a/lib/models/image.dart b/lib/models/image.dart index 59f018a..9d5de4d 100644 --- a/lib/models/image.dart +++ b/lib/models/image.dart @@ -1,5 +1,5 @@ -import 'package:hive/hive.dart'; import 'package:equatable/equatable.dart'; +import 'package:hive/hive.dart'; import 'package:json_annotation/json_annotation.dart'; part 'image.g.dart'; @@ -33,4 +33,10 @@ class Image extends Equatable { @override List get props => [quality, link]; + + static const Image likedSongs = Image( + quality: '500x500', + link: + 'https://community.spotify.com/t5/image/serverpage/image-id/104727iC92B541DB372FBC7/image-dimensions/2500?v=v2&px=-1', + ); } diff --git a/lib/models/media_playlist.dart b/lib/models/media_playlist.dart index 989e410..2a5d2f6 100644 --- a/lib/models/media_playlist.dart +++ b/lib/models/media_playlist.dart @@ -2,8 +2,10 @@ import 'package:audio_service/audio_service.dart'; import 'package:equatable/equatable.dart'; import 'package:hive_flutter/hive_flutter.dart'; +import 'package:varanasi_mobile_app/features/user-library/data/user_library.dart'; import 'package:varanasi_mobile_app/models/image.dart'; import 'package:varanasi_mobile_app/models/playable_item.dart'; +import 'package:varanasi_mobile_app/models/song.dart'; part 'media_playlist.g.dart'; @@ -104,4 +106,14 @@ class MediaPlaylist extends Equatable { images: images ?? this.images, ); } + + PlaylistLibrary toUserLibrary() { + return PlaylistLibrary( + id: id!, + title: title, + description: description, + mediaItems: mediaItems?.whereType().toList() ?? [], + images: images, + ); + } } diff --git a/lib/utils/app_snackbar.dart b/lib/utils/app_snackbar.dart index c8d64ed..0eaac87 100644 --- a/lib/utils/app_snackbar.dart +++ b/lib/utils/app_snackbar.dart @@ -3,22 +3,36 @@ import 'package:flutter/material.dart'; import 'package:varanasi_mobile_app/utils/helpers/get_app_context.dart'; class AppSnackbar { - static Flushbar _createFlushBar(String message) => Flushbar( - message: message, - duration: const Duration(seconds: 3), - animationDuration: const Duration(milliseconds: 500), - margin: const EdgeInsets.symmetric(horizontal: 12, vertical: 8), - borderRadius: BorderRadius.circular(8), - backgroundColor: Colors.white, - messageColor: Colors.black, - flushbarStyle: FlushbarStyle.FLOATING, - ); + static Flushbar? flush; + + static Flushbar _createFlushBar(String message) { + return flush = Flushbar( + message: message, + duration: const Duration(seconds: 3), + animationDuration: const Duration(milliseconds: 500), + margin: const EdgeInsets.symmetric(horizontal: 12, vertical: 8), + borderRadius: BorderRadius.circular(8), + backgroundColor: Colors.white, + messageColor: Colors.black, + flushbarStyle: FlushbarStyle.FLOATING, + ); + } /// Shows a [Flushbar] with the given [message] and [context]. /// /// If [context] is not provided, [appContext] is used. static void show(String message, [BuildContext? context]) { + dismiss(); context ??= appContext; _createFlushBar(message).show(context); } + + /// Dismisses the [Flushbar] if it is visible. + /// If [context] is not provided, [appContext] is used. + static Future dismiss([T? result]) { + if (flush != null && flush!.isShowing()) { + flush!.dismiss(result); + } + return Future.value(); + } } diff --git a/lib/utils/constants/strings.dart b/lib/utils/constants/strings.dart index 829490e..9518ea0 100644 --- a/lib/utils/constants/strings.dart +++ b/lib/utils/constants/strings.dart @@ -6,6 +6,7 @@ class AppStrings { static const String currentPlaylistCacheKey = 'current_playlist'; static const String currentPlaylistIndexCacheKey = 'current_playlist_index'; static const String currentPlaylistPositionKey = 'current_playlist_position'; + static const String userLibraryCacheKey = 'user_library'; static const String topSearchesCacheKey = 'top_searches'; diff --git a/lib/widgets/download_button.dart b/lib/widgets/download_button.dart index d6df598..f6305e0 100644 --- a/lib/widgets/download_button.dart +++ b/lib/widgets/download_button.dart @@ -1,19 +1,22 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:hive_flutter/hive_flutter.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/gen/assets.gen.dart'; import 'package:varanasi_mobile_app/models/download.dart'; import 'package:varanasi_mobile_app/models/media_playlist.dart'; import 'package:varanasi_mobile_app/models/playable_item.dart'; import 'package:varanasi_mobile_app/utils/extensions/extensions.dart'; class DownloadButton extends StatelessWidget { - const DownloadButton(this.song, {super.key}); + const DownloadButton(this.media, {super.key}); - final PlayableMedia song; + final PlayableMedia media; @override Widget build(BuildContext context) { return StreamBuilder( - stream: context.select((DownloadCubit value) => value.listen(song)), + stream: context.select((DownloadCubit value) => value.listen(media)), builder: (context, snapshot) { final data = snapshot.data; final downloaded = data?.downloadComplete ?? false; @@ -23,20 +26,18 @@ class DownloadButton extends StatelessWidget { onPressed: () { final cubit = context.read(); if (downloaded) { - cubit.deleteDownloadedMedia(song); + cubit.deleteDownloadedMedia(media); } else if (downloading) { - cubit.cancelDownload(song); + cubit.cancelDownload(media); } else { - cubit.downloadSong(song); + cubit.downloadSong(media); } }, - icon: Icon( - downloading - ? Icons.downloading_rounded - : downloaded - ? Icons.delete_outline_rounded - : Icons.download_outlined, - ), + icon: switch (downloaded) { + true => Assets.icon.downloadFilled, + false => Assets.icon.download, + } + .svg(color: Colors.white, width: 24), color: context.colorScheme.onBackground, ); }); @@ -52,24 +53,36 @@ class DownloadPlaylist extends StatelessWidget { @override Widget build(BuildContext context) { - final stream = context - .select((DownloadCubit cubit) => cubit.listenToPlaylist(playlist)); - return StreamBuilder( - stream: stream, - builder: (context, snapshot) { - final downloading = snapshot.data?.downloading ?? false; - final downloaded = snapshot.data?.downloaded ?? false; + final downloadBox = context.read().downloadBox; + final keys = playlist.mediaItems?.map((e) => e.itemId).toList(); + return ValueListenableBuilder( + valueListenable: downloadBox.listenable(keys: keys), + builder: (context, box, _) { + final values = keys?.map((key) => box.get(key)).toList() ?? []; + final downloading = + values.any((element) => element?.downloading ?? false); + final downloaded = + !values.any((element) => !(element?.downloadComplete ?? false)); return IconButton( onPressed: () { - context.read().batchDownload(playlist); + final cubit = context.read(); + if (downloading) { + cubit.batchCancel(playlist); + return; + } + if (downloaded) { + cubit.batchDelete(playlist); + return; + } + context.read().addToLibrary(playlist); + cubit.batchDownload(playlist); }, - icon: Icon( - downloading - ? Icons.downloading_rounded - : downloaded - ? Icons.delete_outline_rounded - : Icons.download_outlined, - ), + icon: switch (downloaded) { + true => Assets.icon.downloadFilled, + false when !downloading => Assets.icon.download, + false => Assets.icon.downloading, + } + .svg(color: Colors.white, width: 24), ); }); } diff --git a/lib/widgets/favorite_button.dart b/lib/widgets/favorite_button.dart new file mode 100644 index 0000000..d07a9a1 --- /dev/null +++ b/lib/widgets/favorite_button.dart @@ -0,0 +1,41 @@ +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/models/playable_item.dart'; +import 'package:varanasi_mobile_app/models/song.dart'; + +class FavoriteButton extends StatelessWidget { + const FavoriteButton({ + super.key, + required this.media, + }); + + final PlayableMedia? media; + + @override + Widget build(BuildContext context) { + return BlocBuilder( + builder: (context, state) { + final source = media; + if (state is! UserLibraryLoaded || source == null || source is! Song) { + return const SizedBox.shrink(); + } + final isAdded = + state.favorite.mediaItems.any((item) => item.id == source.id); + return IconButton( + onPressed: () { + final cubit = context.read(); + if (isAdded) { + cubit.unfavoriteSong(source); + } else { + cubit.favoriteSong(source); + } + }, + icon: isAdded + ? const Icon(Icons.favorite) + : const Icon(Icons.favorite_border_rounded), + ); + }, + ); + } +} diff --git a/lib/widgets/media_tile.dart b/lib/widgets/media_tile.dart index ff1c346..fdd9aac 100644 --- a/lib/widgets/media_tile.dart +++ b/lib/widgets/media_tile.dart @@ -411,13 +411,7 @@ class MediaTile extends StatelessWidget { Widget _buildTrailing() { return Row( mainAxisSize: MainAxisSize.min, - children: [ - DownloadButton(media), - // IconButton( - // onPressed: () {}, - // icon: const Icon(Icons.more_horiz), - // ), - ], + children: [DownloadButton(media)], ); } diff --git a/lib/widgets/player/full_screen_player/full_screen_player.dart b/lib/widgets/player/full_screen_player/full_screen_player.dart index 74d7549..5c945bb 100644 --- a/lib/widgets/player/full_screen_player/full_screen_player.dart +++ b/lib/widgets/player/full_screen_player/full_screen_player.dart @@ -2,9 +2,12 @@ import 'package:cached_network_image/cached_network_image.dart'; import 'package:carousel_slider/carousel_slider.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:hive_flutter/hive_flutter.dart'; import 'package:sliding_up_panel/sliding_up_panel.dart'; import 'package:varanasi_mobile_app/cubits/config/config_cubit.dart'; import 'package:varanasi_mobile_app/cubits/player/player_cubit.dart'; +import 'package:varanasi_mobile_app/models/media_playlist.dart'; +import 'package:varanasi_mobile_app/utils/constants/strings.dart'; import 'package:varanasi_mobile_app/utils/extensions/media_query.dart'; import 'package:varanasi_mobile_app/widgets/play_pause_button.dart'; @@ -26,10 +29,10 @@ class Player extends StatefulWidget { class _PlayerState extends State { @override Widget build(BuildContext context) { - final controller = context.select((ConfigCubit cubit) => + final (controller, configBox) = context.select((ConfigCubit cubit) => cubit.state is ConfigLoaded - ? (cubit.state as ConfigLoaded).playerPageController - : null); + ? (cubit.configLoadedState.playerPageController, cubit.cacheBox) + : (null, cubit.cacheBox)); final (state, audioHandler) = context .select((MediaPlayerCubit cubit) => (cubit.state, cubit.audioHandler)); final queueState = state.queueState; @@ -82,7 +85,27 @@ class _PlayerState extends State { ), ), ), - MediaInfo(mediaItem: mediaItem), + ValueListenableBuilder( + valueListenable: configBox.listenable(keys: [ + AppStrings.currentPlaylistCacheKey, + AppStrings.currentPlaylistIndexCacheKey + ]), + builder: (context, box, _) { + final MediaPlaylist currentPlaylist = box.get( + AppStrings.currentPlaylistCacheKey, + defaultValue: const MediaPlaylist.empty()); + final int currentPlaylistIndex = box.get( + AppStrings.currentPlaylistIndexCacheKey, + defaultValue: 0, + ); + final mediaItems = currentPlaylist.mediaItems ?? []; + final currentMedia = mediaItems[currentPlaylistIndex]; + return MediaInfo( + mediaItem: mediaItem, + currentMedia: currentMedia, + ); + }, + ), AudioSeekbar(audioHandler: audioHandler), const SizedBox(height: 16), Row( diff --git a/lib/widgets/player/full_screen_player/media_info.dart b/lib/widgets/player/full_screen_player/media_info.dart index 6d22a3c..6273103 100644 --- a/lib/widgets/player/full_screen_player/media_info.dart +++ b/lib/widgets/player/full_screen_player/media_info.dart @@ -1,15 +1,20 @@ import 'package:audio_service/audio_service.dart'; import 'package:flutter/material.dart'; +import 'package:varanasi_mobile_app/models/playable_item.dart'; import 'package:varanasi_mobile_app/utils/extensions/media_query.dart'; import 'package:varanasi_mobile_app/utils/extensions/theme.dart'; +import 'package:varanasi_mobile_app/widgets/download_button.dart'; +import 'package:varanasi_mobile_app/widgets/favorite_button.dart'; class MediaInfo extends StatelessWidget { const MediaInfo({ super.key, required this.mediaItem, + required this.currentMedia, }); final MediaItem mediaItem; + final PlayableMedia? currentMedia; @override Widget build(BuildContext context) { @@ -30,10 +35,12 @@ class MediaInfo extends StatelessWidget { maxLines: 1, overflow: TextOverflow.ellipsis, ), - trailing: IconButton( - onPressed: () {}, - icon: const Icon(Icons.add_circle_outline_rounded), - iconSize: 32, + trailing: Row( + mainAxisSize: MainAxisSize.min, + children: [ + if (currentMedia != null) DownloadButton(currentMedia!), + FavoriteButton(media: currentMedia), + ], ), ); } From 35c3214223ef253d3a619c37729c3c535a1e6f43 Mon Sep 17 00:00:00 2001 From: devaryakjha Date: Sun, 22 Oct 2023 15:14:20 +0530 Subject: [PATCH 05/18] =?UTF-8?q?=E2=9C=A8=20Add:=20delete=20all=20method?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/cubits/download/download_cubit.dart | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/lib/cubits/download/download_cubit.dart b/lib/cubits/download/download_cubit.dart index a7a1d1f..0a5456e 100644 --- a/lib/cubits/download/download_cubit.dart +++ b/lib/cubits/download/download_cubit.dart @@ -220,4 +220,15 @@ class DownloadCubit extends AppCubit { return Future.value(null); } } + + Future deleteAll() async { + final keys = _downloadBox.keys.toList(); + for (final key in keys) { + final item = _downloadBox.get(key); + if (item?.path.isNotEmpty ?? false) { + await _deleteFile(item!.path); + } + } + await _downloadBox.clear(); + } } From 3902bc1c95c8acec8645cbd87a4a2334b8e92ec4 Mon Sep 17 00:00:00 2001 From: devaryakjha Date: Sun, 22 Oct 2023 16:49:57 +0530 Subject: [PATCH 06/18] =?UTF-8?q?=F0=9F=90=9B=20Fix:=20navigation=20for=20?= =?UTF-8?q?carousel=20when=20not=20mounted?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/app.dart | 3 +- lib/cubits/config/config_cubit.dart | 36 +++++++++-------- lib/cubits/config/config_state.dart | 24 ++++++++++-- lib/cubits/player/player_cubit.dart | 21 +++++----- lib/utils/safe_animate_to_pageview.dart | 12 ++++-- .../full_screen_player.dart | 39 ++++++------------- 6 files changed, 70 insertions(+), 65 deletions(-) diff --git a/lib/app.dart b/lib/app.dart index a972f01..c8f213e 100644 --- a/lib/app.dart +++ b/lib/app.dart @@ -32,8 +32,7 @@ class Varanasi extends StatelessWidget { ), BlocProvider( lazy: false, - create: (ctx) => - MediaPlayerCubit(() => ctx.read())..init(), + create: (ctx) => MediaPlayerCubit()..init(), ), ], child: Builder(builder: (context) { diff --git a/lib/cubits/config/config_cubit.dart b/lib/cubits/config/config_cubit.dart index b9d1297..39dc437 100644 --- a/lib/cubits/config/config_cubit.dart +++ b/lib/cubits/config/config_cubit.dart @@ -2,10 +2,10 @@ import 'package:audio_service/audio_service.dart'; import 'package:cached_network_image/cached_network_image.dart'; import 'package:carousel_slider/carousel_controller.dart'; import 'package:equatable/equatable.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:hive_flutter/adapters.dart'; import 'package:package_info_plus/package_info_plus.dart'; import 'package:palette_generator/palette_generator.dart'; -import 'package:rxdart/rxdart.dart'; import 'package:sliding_up_panel/sliding_up_panel.dart'; import 'package:varanasi_mobile_app/models/app_config.dart'; import 'package:varanasi_mobile_app/models/media_playlist.dart'; @@ -13,19 +13,23 @@ import 'package:varanasi_mobile_app/models/playable_item.dart'; import 'package:varanasi_mobile_app/models/sort_type.dart'; import 'package:varanasi_mobile_app/utils/app_cubit.dart'; import 'package:varanasi_mobile_app/utils/constants/strings.dart'; +import 'package:varanasi_mobile_app/utils/helpers/get_app_context.dart'; import 'package:varanasi_mobile_app/utils/logger.dart'; part 'config_state.dart'; class ConfigCubit extends AppCubit { - final _configBox = AppConfig.getBox; - final _cacheBox = Hive.box(AppStrings.commonCacheBoxName); final Map _imageProviderCache = {}; late final Expando _paletteGeneratorExpando; Logger logger = Logger.instance; + Box get _configBox => AppConfig.getBox; + Box get _cacheBox => Hive.box(AppStrings.commonCacheBoxName); + ConfigCubit() : super(ConfigInitial()); + static ConfigCubit read() => appContext.read(); + Box get cacheBox => _cacheBox; @override @@ -38,6 +42,16 @@ class ConfigCubit extends AppCubit { } else { logger.i('Config box is not empty'); } + _cacheBox.watch(key: AppStrings.currentPlaylistCacheKey).listen((event) { + logger.i('Current playlist is changed to ${event.value}'); + emit(configLoadedState.copyWith(currentPlaylist: event.value)); + }); + _cacheBox + .watch(key: AppStrings.currentPlaylistIndexCacheKey) + .listen((event) { + logger.i('Current playlist index is changed to ${event.value}'); + emit(configLoadedState.copyWith(currentQueueIndex: event.value)); + }); _configBox.watch().listen((event) { final config = event.value; if (config is! AppConfig) return; @@ -60,6 +74,8 @@ class ConfigCubit extends AppCubit { playerPageController: CarouselController(), miniPlayerPageController: CarouselController(), packageInfo: packageInfo, + currentQueueIndex: savedPlaylistIndex, + currentPlaylist: savedPlaylist, )); } @@ -122,10 +138,6 @@ class ConfigCubit extends AppCubit { return _cacheBox.get(AppStrings.currentPlaylistCacheKey); } - Stream get savedPlaylistStream => _cacheBox - .watch(key: AppStrings.currentPlaylistCacheKey) - .map((event) => event.value); - Future saveCurrentPlaylistIndex(int index) { return _cacheBox.put(AppStrings.currentPlaylistIndexCacheKey, index); } @@ -138,10 +150,6 @@ class ConfigCubit extends AppCubit { return _cacheBox.get(AppStrings.currentPlaylistIndexCacheKey); } - Stream get savedPlaylistIndexStream => _cacheBox - .watch(key: AppStrings.currentPlaylistIndexCacheKey) - .map((event) => event.value); - Future saveCurrentPosition(Duration duration) { return _cacheBox.put( AppStrings.currentPlaylistPositionKey, @@ -180,10 +188,4 @@ class ConfigCubit extends AppCubit { PlayableMedia? get currentMedia => savedPlaylist?.mediaItems?[savedPlaylistIndex ?? 0]; - - Stream get currentMediaStream => - Rx.combineLatest2(savedPlaylistStream, savedPlaylistIndexStream, (a, b) { - if (a == null || b == null) return null; - return a.mediaItems?[b]; - }); } diff --git a/lib/cubits/config/config_state.dart b/lib/cubits/config/config_state.dart index 03fbe4d..1c7482e 100644 --- a/lib/cubits/config/config_state.dart +++ b/lib/cubits/config/config_state.dart @@ -12,9 +12,12 @@ final class ConfigInitial extends ConfigState {} final class ConfigLoaded extends ConfigState { final AppConfig config; - final CarouselController? miniPlayerPageController, playerPageController; + final CarouselController? miniPlayerPageController; + final CarouselController? playerPageController; final PanelController panelController; final PackageInfo packageInfo; + final MediaPlaylist? currentPlaylist; + final int? currentQueueIndex; const ConfigLoaded({ required this.config, @@ -22,6 +25,8 @@ final class ConfigLoaded extends ConfigState { this.playerPageController, required this.panelController, required this.packageInfo, + this.currentPlaylist, + this.currentQueueIndex, }); @override @@ -31,6 +36,8 @@ final class ConfigLoaded extends ConfigState { panelController, playerPageController, packageInfo, + currentPlaylist, + currentQueueIndex, ]; ConfigLoaded copyWith({ @@ -39,14 +46,23 @@ final class ConfigLoaded extends ConfigState { CarouselController? playerPageController, PanelController? panelController, PackageInfo? packageInfo, + MediaPlaylist? currentPlaylist, + int? currentQueueIndex, }) { return ConfigLoaded( - packageInfo: packageInfo ?? this.packageInfo, config: config ?? this.config, - playerPageController: playerPageController ?? this.playerPageController, - panelController: panelController ?? this.panelController, miniPlayerPageController: miniPlayerPageController ?? this.miniPlayerPageController, + playerPageController: playerPageController ?? this.playerPageController, + panelController: panelController ?? this.panelController, + packageInfo: packageInfo ?? this.packageInfo, + currentPlaylist: currentPlaylist ?? this.currentPlaylist, + currentQueueIndex: currentQueueIndex ?? this.currentQueueIndex, ); } + + PlayableMedia? get currentMedia { + if (currentPlaylist == null || currentQueueIndex == null) return null; + return currentPlaylist?.mediaItems?.elementAtOrNull(currentQueueIndex ?? 0); + } } diff --git a/lib/cubits/player/player_cubit.dart b/lib/cubits/player/player_cubit.dart index 7f60c1f..d5d19d0 100644 --- a/lib/cubits/player/player_cubit.dart +++ b/lib/cubits/player/player_cubit.dart @@ -53,11 +53,12 @@ class MediaColorPalette { class MediaPlayerCubit extends AppCubit with DataProviderProtocol, CacheableService { - final ConfigCubit Function() configCubitGetter; late final AudioHandlerImpl audioHandler; late final Box _box; - MediaPlayerCubit(this.configCubitGetter) : super(const MediaPlayerState()); + MediaPlayerCubit() : super(const MediaPlayerState()); + + ConfigCubit get _configCubit => ConfigCubit.read(); Future playFromMediaPlaylist( MediaPlaylist playlist, [ @@ -79,7 +80,7 @@ class MediaPlayerCubit extends AppCubit } else if (autoPlay) { await play(); } - await configCubitGetter().saveCurrentPlaylist(playlist); + await _configCubit.saveCurrentPlaylist(playlist); } Future playFromSong(PlayableMedia media) async { @@ -149,7 +150,7 @@ class MediaPlayerCubit extends AppCubit _box = value; }); audioHandler = await AudioService.init( - builder: () => AudioHandlerImpl(configCubitGetter()), + builder: () => AudioHandlerImpl(_configCubit), config: const AudioServiceConfig( androidNotificationOngoing: true, androidStopForegroundOnPause: true, @@ -159,18 +160,16 @@ class MediaPlayerCubit extends AppCubit final playing = state.playing; if (!playing) return; final position = state.position; - final configCubit = configCubitGetter(); - configCubit.saveCurrentPosition(position); + _configCubit.saveCurrentPosition(position); }); audioHandler.playbackState .map((event) => event.queueIndex) .distinct() .listen((index) { - final configCubit = configCubitGetter(); if (index != null) { - final controller = configCubit.miniPlayerPageController; - final carouselController = configCubit.playerPageController; - configCubit.saveCurrentPlaylistIndex(index); + final controller = _configCubit.miniPlayerPageController; + final carouselController = _configCubit.playerPageController; + _configCubit.saveCurrentPlaylistIndex(index); animateToPage(index, controller); animateToPage(index, carouselController); } @@ -183,7 +182,7 @@ class MediaPlayerCubit extends AppCubit ).distinct().listen((value) async { PaletteGenerator? palette; if (value.$2 != null) { - palette = await configCubitGetter() + palette = await _configCubit .generatePalleteGenerator(value.$2?.artUri.toString() ?? ''); } else { palette = null; diff --git a/lib/utils/safe_animate_to_pageview.dart b/lib/utils/safe_animate_to_pageview.dart index 766170b..6230516 100644 --- a/lib/utils/safe_animate_to_pageview.dart +++ b/lib/utils/safe_animate_to_pageview.dart @@ -1,6 +1,12 @@ import 'package:carousel_slider/carousel_controller.dart'; +import 'package:varanasi_mobile_app/utils/logger.dart'; -void animateToPage(int index, CarouselController? controller) { - if (controller == null || !controller.ready) return; - controller.animateToPage(index); +Future animateToPage(int index, CarouselController? controller) async { + try { + if (controller == null || !controller.ready) return; + await controller.animateToPage(index); + } catch (e, stackTrace) { + Logger.instance + .e("Error while animating to page", error: e, stackTrace: stackTrace); + } } diff --git a/lib/widgets/player/full_screen_player/full_screen_player.dart b/lib/widgets/player/full_screen_player/full_screen_player.dart index 5c945bb..c189d1c 100644 --- a/lib/widgets/player/full_screen_player/full_screen_player.dart +++ b/lib/widgets/player/full_screen_player/full_screen_player.dart @@ -2,12 +2,9 @@ import 'package:cached_network_image/cached_network_image.dart'; import 'package:carousel_slider/carousel_slider.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:hive_flutter/hive_flutter.dart'; import 'package:sliding_up_panel/sliding_up_panel.dart'; import 'package:varanasi_mobile_app/cubits/config/config_cubit.dart'; import 'package:varanasi_mobile_app/cubits/player/player_cubit.dart'; -import 'package:varanasi_mobile_app/models/media_playlist.dart'; -import 'package:varanasi_mobile_app/utils/constants/strings.dart'; import 'package:varanasi_mobile_app/utils/extensions/media_query.dart'; import 'package:varanasi_mobile_app/widgets/play_pause_button.dart'; @@ -29,10 +26,16 @@ class Player extends StatefulWidget { class _PlayerState extends State { @override Widget build(BuildContext context) { - final (controller, configBox) = context.select((ConfigCubit cubit) => - cubit.state is ConfigLoaded - ? (cubit.configLoadedState.playerPageController, cubit.cacheBox) - : (null, cubit.cacheBox)); + final controller = context.select( + (ConfigCubit cubit) => cubit.state is ConfigLoaded + ? cubit.configLoadedState.playerPageController + : null, + ); + final currentMedia = context.select( + (ConfigCubit cubit) => cubit.state is ConfigLoaded + ? cubit.configLoadedState.currentMedia + : null, + ); final (state, audioHandler) = context .select((MediaPlayerCubit cubit) => (cubit.state, cubit.audioHandler)); final queueState = state.queueState; @@ -85,27 +88,7 @@ class _PlayerState extends State { ), ), ), - ValueListenableBuilder( - valueListenable: configBox.listenable(keys: [ - AppStrings.currentPlaylistCacheKey, - AppStrings.currentPlaylistIndexCacheKey - ]), - builder: (context, box, _) { - final MediaPlaylist currentPlaylist = box.get( - AppStrings.currentPlaylistCacheKey, - defaultValue: const MediaPlaylist.empty()); - final int currentPlaylistIndex = box.get( - AppStrings.currentPlaylistIndexCacheKey, - defaultValue: 0, - ); - final mediaItems = currentPlaylist.mediaItems ?? []; - final currentMedia = mediaItems[currentPlaylistIndex]; - return MediaInfo( - mediaItem: mediaItem, - currentMedia: currentMedia, - ); - }, - ), + MediaInfo(mediaItem: mediaItem, currentMedia: currentMedia), AudioSeekbar(audioHandler: audioHandler), const SizedBox(height: 16), Row( From d5cca47f1173faffbbff238cd9a900d10940c669 Mon Sep 17 00:00:00 2001 From: devaryakjha Date: Sun, 22 Oct 2023 17:20:38 +0530 Subject: [PATCH 07/18] =?UTF-8?q?=F0=9F=90=9B=20Fix:=20current=20playlist?= =?UTF-8?q?=20not=20updating?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/cubits/config/config_cubit.dart | 14 ++++---------- lib/cubits/player/player_cubit.dart | 2 +- 2 files changed, 5 insertions(+), 11 deletions(-) diff --git a/lib/cubits/config/config_cubit.dart b/lib/cubits/config/config_cubit.dart index 39dc437..f1c6b29 100644 --- a/lib/cubits/config/config_cubit.dart +++ b/lib/cubits/config/config_cubit.dart @@ -42,16 +42,6 @@ class ConfigCubit extends AppCubit { } else { logger.i('Config box is not empty'); } - _cacheBox.watch(key: AppStrings.currentPlaylistCacheKey).listen((event) { - logger.i('Current playlist is changed to ${event.value}'); - emit(configLoadedState.copyWith(currentPlaylist: event.value)); - }); - _cacheBox - .watch(key: AppStrings.currentPlaylistIndexCacheKey) - .listen((event) { - logger.i('Current playlist index is changed to ${event.value}'); - emit(configLoadedState.copyWith(currentQueueIndex: event.value)); - }); _configBox.watch().listen((event) { final config = event.value; if (config is! AppConfig) return; @@ -127,10 +117,12 @@ class ConfigCubit extends AppCubit { : null; Future saveCurrentPlaylist(MediaPlaylist playlist) { + emit(configLoadedState.copyWith(currentPlaylist: playlist)); return _cacheBox.put(AppStrings.currentPlaylistCacheKey, playlist); } Future clearCurrentPlaylist() { + emit(configLoadedState.copyWith(currentPlaylist: null)); return _cacheBox.delete(AppStrings.currentPlaylistCacheKey); } @@ -139,10 +131,12 @@ class ConfigCubit extends AppCubit { } Future saveCurrentPlaylistIndex(int index) { + emit(configLoadedState.copyWith(currentQueueIndex: index)); return _cacheBox.put(AppStrings.currentPlaylistIndexCacheKey, index); } Future clearCurrentPlaylistIndex() { + emit(configLoadedState.copyWith(currentQueueIndex: null)); return _cacheBox.delete(AppStrings.currentPlaylistIndexCacheKey); } diff --git a/lib/cubits/player/player_cubit.dart b/lib/cubits/player/player_cubit.dart index d5d19d0..31e5f83 100644 --- a/lib/cubits/player/player_cubit.dart +++ b/lib/cubits/player/player_cubit.dart @@ -73,6 +73,7 @@ class MediaPlayerCubit extends AppCubit } return; } + unawaited(_configCubit.saveCurrentPlaylist(playlist)); emit(state.copyWith(currentPlaylist: playlist.id)); await audioHandler.updateQueue(playlist.mediaItemsAsMediaItems); if (startIndex != null) { @@ -80,7 +81,6 @@ class MediaPlayerCubit extends AppCubit } else if (autoPlay) { await play(); } - await _configCubit.saveCurrentPlaylist(playlist); } Future playFromSong(PlayableMedia media) async { From cb9a3ce8f1d59159a70baa4c344d220482567754 Mon Sep 17 00:00:00 2001 From: devaryakjha Date: Sun, 22 Oct 2023 20:22:39 +0530 Subject: [PATCH 08/18] =?UTF-8?q?=F0=9F=92=84=20Update:=20Assets?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- assets/icon/circular_loader.json | 1 + lib/gen/assets.gen.dart | 74 +++++++++++++++++++++++++++++++- lib/widgets/download_button.dart | 23 +++++++--- lib/widgets/favorite_button.dart | 3 ++ pubspec.lock | 8 ++++ pubspec.yaml | 2 + 6 files changed, 104 insertions(+), 7 deletions(-) create mode 100644 assets/icon/circular_loader.json diff --git a/assets/icon/circular_loader.json b/assets/icon/circular_loader.json new file mode 100644 index 0000000..3632444 --- /dev/null +++ b/assets/icon/circular_loader.json @@ -0,0 +1 @@ +{"v":"5.4.2","fr":60,"ip":0,"op":37,"w":600,"h":600,"nm":"loader-1","ddd":0,"assets":[],"layers":[{"ddd":0,"ind":1,"ty":4,"nm":"loader","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[300,300,0],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,-82.843],[82.843,0],[0,82.843],[-82.843,0]],"o":[[0,82.843],[-82.843,0],[0,-82.843],[82.843,0]],"v":[[150,0],[0,150],[-150,0],[0,-150]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[0.086274512112,0.086274512112,0.086274512112,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":30,"ix":5},"lc":2,"lj":1,"ml":10,"ml2":{"a":0,"k":10,"ix":8},"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"tm","s":{"a":0,"k":0,"ix":1},"e":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"n":["0p833_0p833_0p167_0p167"],"t":0,"s":[10],"e":[27]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"n":["0p833_0p833_0p167_0p167"],"t":10,"s":[27],"e":[10]},{"t":40}],"ix":2},"o":{"a":1,"k":[{"i":{"x":[0.5],"y":[0.889]},"o":{"x":[0.5],"y":[0.111]},"n":["0p5_0p889_0p5_0p111"],"t":0,"s":[0],"e":[360]},{"t":40}],"ix":3},"m":1,"ix":2,"nm":"Trim Paths 1","mn":"ADBE Vector Filter - Trim","hd":false}],"ip":0,"op":300,"st":0,"bm":0},{"ddd":0,"ind":2,"ty":4,"nm":"container","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[300,300,0],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[74.439,0],[0,-74.439],[-74.439,0],[0,74.439]],"o":[[-74.439,0],[0,74.439],[74.439,0],[0,-74.439]],"v":[[0,-135],[-135,0],[0,135],[135,0]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ind":1,"ty":"sh","ix":2,"ks":{"a":0,"k":{"i":[[90.981,0],[0,90.981],[-90.981,0],[0,-90.981]],"o":[[-90.981,0],[0,-90.981],[90.981,0],[0,90.981]],"v":[[0,165],[-165,0],[0,-165],[165,0]],"c":true},"ix":2},"nm":"Path 2","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.909620098039,0.909620098039,0.909620098039,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":3,"cix":2,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":300,"st":0,"bm":0}],"markers":[]} \ No newline at end of file diff --git a/lib/gen/assets.gen.dart b/lib/gen/assets.gen.dart index 0d77156..e1ca7d9 100644 --- a/lib/gen/assets.gen.dart +++ b/lib/gen/assets.gen.dart @@ -10,6 +10,7 @@ import 'package:flutter/widgets.dart'; import 'package:flutter_svg/flutter_svg.dart'; import 'package:flutter/services.dart'; +import 'package:lottie/lottie.dart'; class $AssetsIconGen { const $AssetsIconGen(); @@ -21,6 +22,10 @@ class $AssetsIconGen { SvgGenImage get appIconMonotone => const SvgGenImage('assets/icon/app_icon_monotone.svg'); + /// File path: assets/icon/circular_loader.json + LottieGenImage get circularLoader => + const LottieGenImage('assets/icon/circular_loader.json'); + /// File path: assets/icon/download.svg SvgGenImage get download => const SvgGenImage('assets/icon/download.svg'); @@ -35,8 +40,14 @@ class $AssetsIconGen { $AssetsIconNavGen get nav => const $AssetsIconNavGen(); /// List of all assets - List get values => - [appIcon, appIconMonotone, download, downloadFilled, downloading]; + List get values => [ + appIcon, + appIconMonotone, + circularLoader, + download, + downloadFilled, + downloading + ]; } class $AssetsImagesGen { @@ -218,3 +229,62 @@ class SvgGenImage { String get keyName => _assetName; } + +class LottieGenImage { + const LottieGenImage(this._assetName); + + final String _assetName; + + LottieBuilder lottie({ + Animation? controller, + bool? animate, + FrameRate? frameRate, + bool? repeat, + bool? reverse, + LottieDelegates? delegates, + LottieOptions? options, + void Function(LottieComposition)? onLoaded, + LottieImageProviderFactory? imageProviderFactory, + Key? key, + AssetBundle? bundle, + Widget Function(BuildContext, Widget, LottieComposition?)? frameBuilder, + ImageErrorWidgetBuilder? errorBuilder, + double? width, + double? height, + BoxFit? fit, + AlignmentGeometry? alignment, + String? package, + bool? addRepaintBoundary, + FilterQuality? filterQuality, + void Function(String)? onWarning, + }) { + return Lottie.asset( + _assetName, + controller: controller, + animate: animate, + frameRate: frameRate, + repeat: repeat, + reverse: reverse, + delegates: delegates, + options: options, + onLoaded: onLoaded, + imageProviderFactory: imageProviderFactory, + key: key, + bundle: bundle, + frameBuilder: frameBuilder, + errorBuilder: errorBuilder, + width: width, + height: height, + fit: fit, + alignment: alignment, + package: package, + addRepaintBoundary: addRepaintBoundary, + filterQuality: filterQuality, + onWarning: onWarning, + ); + } + + String get path => _assetName; + + String get keyName => _assetName; +} diff --git a/lib/widgets/download_button.dart b/lib/widgets/download_button.dart index f6305e0..6b55071 100644 --- a/lib/widgets/download_button.dart +++ b/lib/widgets/download_button.dart @@ -1,6 +1,8 @@ import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:hive_flutter/hive_flutter.dart'; +import 'package:lottie/lottie.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/gen/assets.gen.dart'; @@ -32,12 +34,22 @@ class DownloadButton extends StatelessWidget { } else { cubit.downloadSong(media); } + HapticFeedback.mediumImpact(); }, - icon: switch (downloaded) { - true => Assets.icon.downloadFilled, - false => Assets.icon.download, - } - .svg(color: Colors.white, width: 24), + icon: SizedBox( + width: 24, + height: 24, + child: Center( + child: downloading + ? Assets.icon.circularLoader + .lottie(width: 36, height: 36, frameRate: FrameRate.max) + : switch (downloaded) { + true => Assets.icon.downloadFilled, + false => Assets.icon.download, + } + .svg(color: Colors.white), + ), + ), color: context.colorScheme.onBackground, ); }); @@ -65,6 +77,7 @@ class DownloadPlaylist extends StatelessWidget { !values.any((element) => !(element?.downloadComplete ?? false)); return IconButton( onPressed: () { + HapticFeedback.mediumImpact(); final cubit = context.read(); if (downloading) { cubit.batchCancel(playlist); diff --git a/lib/widgets/favorite_button.dart b/lib/widgets/favorite_button.dart index d07a9a1..ab2cafa 100644 --- a/lib/widgets/favorite_button.dart +++ b/lib/widgets/favorite_button.dart @@ -1,4 +1,5 @@ import 'package:flutter/material.dart'; +import 'package:flutter/services.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/models/playable_item.dart'; @@ -30,10 +31,12 @@ class FavoriteButton extends StatelessWidget { } else { cubit.favoriteSong(source); } + HapticFeedback.lightImpact(); }, icon: isAdded ? const Icon(Icons.favorite) : const Icon(Icons.favorite_border_rounded), + color: isAdded ? Colors.red : null, ); }, ); diff --git a/pubspec.lock b/pubspec.lock index 05b0a59..d99b34e 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -800,6 +800,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.2.0" + lottie: + dependency: "direct main" + description: + name: lottie + sha256: a93542cc2d60a7057255405f62252533f8e8956e7e06754955669fd32fb4b216 + url: "https://pub.dev" + source: hosted + version: "2.7.0" marquee: dependency: "direct main" description: diff --git a/pubspec.yaml b/pubspec.yaml index 851d76b..96562b6 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -73,6 +73,7 @@ dependencies: google_sign_in: ^6.1.5 flutter_native_splash: ^2.3.3 background_downloader: ^7.10.1 + lottie: ^2.7.0 dev_dependencies: flutter_test: @@ -164,3 +165,4 @@ flutter: flutter_gen: integrations: flutter_svg: true + lottie: true From c06380df1c93ea8ac0e119a90a8c7dbc100e161e Mon Sep 17 00:00:00 2001 From: devaryakjha Date: Sun, 22 Oct 2023 23:35:29 +0530 Subject: [PATCH 09/18] =?UTF-8?q?=F0=9F=9A=A7=20WIP:=20download=20feature?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- assets/icon/download.svg | 12 +-- lib/cubits/download/download_cubit.dart | 7 +- .../data/user_library_repository.dart | 2 +- lib/widgets/download_button.dart | 99 +++++++++++++++---- 4 files changed, 88 insertions(+), 32 deletions(-) diff --git a/assets/icon/download.svg b/assets/icon/download.svg index 04e1c3c..fe7d58c 100644 --- a/assets/icon/download.svg +++ b/assets/icon/download.svg @@ -1,11 +1,5 @@ - - - - - - - + \ No newline at end of file diff --git a/lib/cubits/download/download_cubit.dart b/lib/cubits/download/download_cubit.dart index 0a5456e..7f17ade 100644 --- a/lib/cubits/download/download_cubit.dart +++ b/lib/cubits/download/download_cubit.dart @@ -126,7 +126,7 @@ class DownloadCubit extends AppCubit { Future batchDownload(MediaPlaylist playlist) async { final songs = playlist.mediaItems ?? []; - final filteredsong = songs.whereType(); + final filteredsong = songs.whereType().where(_isNotDownloaded); final tasks = filteredsong.map(_songToTask).toList(); for (final song in filteredsong) { _songMap[song.itemId] = song; @@ -231,4 +231,9 @@ class DownloadCubit extends AppCubit { } await _downloadBox.clear(); } + + bool _isDownloaded(PlayableMedia song) => + _downloadBox.containsKey(song.itemId); + + bool _isNotDownloaded(PlayableMedia song) => !_isDownloaded(song); } diff --git a/lib/features/user-library/data/user_library_repository.dart b/lib/features/user-library/data/user_library_repository.dart index ed15306..5c4fb2a 100644 --- a/lib/features/user-library/data/user_library_repository.dart +++ b/lib/features/user-library/data/user_library_repository.dart @@ -21,7 +21,7 @@ class UserLibraryRepository { List getLibraries() => _box.values.toList(); Future addLibrary(E library) => - _box.add(library); + _box.put(library.id, library); Future updateLibrary(E library) => _box.put(library.id, library); diff --git a/lib/widgets/download_button.dart b/lib/widgets/download_button.dart index 6b55071..510d5c7 100644 --- a/lib/widgets/download_button.dart +++ b/lib/widgets/download_button.dart @@ -2,10 +2,8 @@ import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:hive_flutter/hive_flutter.dart'; -import 'package:lottie/lottie.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/gen/assets.gen.dart'; import 'package:varanasi_mobile_app/models/download.dart'; import 'package:varanasi_mobile_app/models/media_playlist.dart'; import 'package:varanasi_mobile_app/models/playable_item.dart'; @@ -23,6 +21,7 @@ class DownloadButton extends StatelessWidget { final data = snapshot.data; final downloaded = data?.downloadComplete ?? false; final downloading = data?.downloading ?? false; + final progress = data?.progress ?? 0; return IconButton( onPressed: () { @@ -36,19 +35,10 @@ class DownloadButton extends StatelessWidget { } HapticFeedback.mediumImpact(); }, - icon: SizedBox( - width: 24, - height: 24, - child: Center( - child: downloading - ? Assets.icon.circularLoader - .lottie(width: 36, height: 36, frameRate: FrameRate.max) - : switch (downloaded) { - true => Assets.icon.downloadFilled, - false => Assets.icon.download, - } - .svg(color: Colors.white), - ), + icon: DownloadStatus( + downloading: downloading, + progress: progress, + downloaded: downloaded, ), color: context.colorScheme.onBackground, ); @@ -65,6 +55,8 @@ class DownloadPlaylist extends StatelessWidget { @override Widget build(BuildContext context) { + final progress = context.select((DownloadCubit value) => + value.state.playlistProgressMap[playlist.id] ?? 0); final downloadBox = context.read().downloadBox; final keys = playlist.mediaItems?.map((e) => e.itemId).toList(); return ValueListenableBuilder( @@ -90,13 +82,78 @@ class DownloadPlaylist extends StatelessWidget { context.read().addToLibrary(playlist); cubit.batchDownload(playlist); }, - icon: switch (downloaded) { - true => Assets.icon.downloadFilled, - false when !downloading => Assets.icon.download, - false => Assets.icon.downloading, - } - .svg(color: Colors.white, width: 24), + icon: DownloadStatus( + downloading: downloading, + downloaded: downloaded, + progress: progress, + ), + iconSize: 24, ); }); } } + +class DownloadStatus extends StatelessWidget { + const DownloadStatus({ + super.key, + required this.downloading, + required this.progress, + this.dimension = 22, + required this.downloaded, + }); + + final bool downloading; + final bool downloaded; + final double progress; + final double dimension; + + double get iconSize => dimension * 0.6; + + @override + Widget build(BuildContext context) { + return SizedBox.square( + dimension: dimension, + child: Visibility( + visible: !downloaded, + replacement: Container( + decoration: const BoxDecoration( + shape: BoxShape.circle, + color: Colors.white, + ), + child: Icon( + Icons.arrow_downward_rounded, + color: Colors.black, + size: iconSize, + ), + ), + child: Stack( + children: [ + if (downloading) + CircularProgressIndicator( + strokeWidth: 2, + value: downloading ? progress : 1, + valueColor: const AlwaysStoppedAnimation(Colors.white), + backgroundColor: Colors.white.withOpacity(0.3), + ), + if (!downloading) + Container( + decoration: const BoxDecoration( + shape: BoxShape.circle, + border: Border.fromBorderSide( + BorderSide(color: Colors.white, width: 1.5), + ), + ), + ), + Positioned.fill( + child: Icon( + downloading ? Icons.stop_rounded : Icons.arrow_downward_rounded, + color: Colors.white, + size: iconSize, + ), + ), + ], + ), + ), + ); + } +} From 2eff3fbb2887d5c1571f8f20d4c9533784362926 Mon Sep 17 00:00:00 2001 From: devaryakjha Date: Mon, 23 Oct 2023 00:56:37 +0530 Subject: [PATCH 10/18] =?UTF-8?q?=F0=9F=9A=A7=20Fix:=20download=20status?= =?UTF-8?q?=20indicator?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/cubits/download/download_cubit.dart | 32 +++++-- lib/features/settings/ui/settings_page.dart | 10 +-- lib/utils/dialogs/alert_dialog.dart | 33 ------- lib/utils/dialogs/app_dialog.dart | 99 +++++++++++++++++++++ lib/widgets/download_button.dart | 74 ++++++++++----- pubspec.lock | 8 ++ pubspec.yaml | 1 + 7 files changed, 189 insertions(+), 68 deletions(-) delete mode 100644 lib/utils/dialogs/alert_dialog.dart create mode 100644 lib/utils/dialogs/app_dialog.dart diff --git a/lib/cubits/download/download_cubit.dart b/lib/cubits/download/download_cubit.dart index 7f17ade..358326d 100644 --- a/lib/cubits/download/download_cubit.dart +++ b/lib/cubits/download/download_cubit.dart @@ -11,6 +11,7 @@ import 'package:varanasi_mobile_app/models/playable_item.dart'; import 'package:varanasi_mobile_app/models/song.dart'; import 'package:varanasi_mobile_app/utils/app_cubit.dart'; import 'package:varanasi_mobile_app/utils/constants/strings.dart'; +import 'package:varanasi_mobile_app/utils/dialogs/app_dialog.dart'; import 'package:varanasi_mobile_app/utils/logger.dart'; part 'download_state.dart'; @@ -149,8 +150,9 @@ class DownloadCubit extends AppCubit { Future batchCancel(MediaPlaylist media) async { await _downloader.cancelTasksWithIds( - media.mediaItems?.map((e) => e.itemId).toList() ?? []); - await batchDelete(media); + media.mediaItems?.map((e) => e.itemId).toList() ?? [], + ); + await _batchDelete(media); } /// Returns a stream of [DownloadedMedia] for the given [song]. @@ -182,15 +184,33 @@ class DownloadCubit extends AppCubit { DownloadedMedia? getDownloadedMedia(PlayableMedia song) => _downloadBox.get(song.itemId); - Future deleteDownloadedMedia(PlayableMedia song) { - final item = _downloadBox.get(song.itemId); + Future deleteSingle(PlayableMedia media) { + return AppDialog.showAlertDialog( + title: "Remove from Downloads?", + message: "You won't be able to listen to this song offline.", + onConfirm: () async => _deleteSingle(media), + confirmLabel: "Remove", + ); + } + + Future _deleteSingle(PlayableMedia media) { + final item = _downloadBox.get(media.itemId); if (item?.path.isNotEmpty ?? false) { _deleteFile(item!.path); } - return _downloadBox.delete(song.itemId); + return _downloadBox.delete(media.itemId); + } + + Future batchDelete(MediaPlaylist song) async { + await AppDialog.showAlertDialog( + title: "Remove from Downloads?", + message: "You won't be able to listen to these songs offline.", + onConfirm: () => _batchDelete(song), + confirmLabel: "Remove", + ); } - Future batchDelete(MediaPlaylist song) { + Future _batchDelete(MediaPlaylist song) async { final keys = song.mediaItems?.map((e) => e.itemId) ?? []; final values = keys .map((e) => _downloadBox.get(e)) diff --git a/lib/features/settings/ui/settings_page.dart b/lib/features/settings/ui/settings_page.dart index a0a025b..ca08449 100644 --- a/lib/features/settings/ui/settings_page.dart +++ b/lib/features/settings/ui/settings_page.dart @@ -11,7 +11,7 @@ import 'package:varanasi_mobile_app/flavors.dart'; import 'package:varanasi_mobile_app/models/app_config.dart'; import 'package:varanasi_mobile_app/models/download_url.dart'; import 'package:varanasi_mobile_app/utils/clear_cache.dart'; -import 'package:varanasi_mobile_app/utils/dialogs/alert_dialog.dart'; +import 'package:varanasi_mobile_app/utils/dialogs/app_dialog.dart'; import 'package:varanasi_mobile_app/utils/extensions/flex_scheme.dart'; class SettingsPage extends StatelessWidget { @@ -110,10 +110,10 @@ class SettingsPage extends StatelessWidget { message: "Cache is already empty 👍🏻", ).show(context); } else { - showAlertDialog( - context, - "Clear cache", - "Are you sure you want to clear the cache?", + AppDialog.showAlertDialog( + context: context, + title: "Clear cache", + message: "Are you sure you want to clear the cache?", onConfirm: () { clearCache().then((value) { FlushbarHelper.createSuccess( diff --git a/lib/utils/dialogs/alert_dialog.dart b/lib/utils/dialogs/alert_dialog.dart deleted file mode 100644 index 28e14b8..0000000 --- a/lib/utils/dialogs/alert_dialog.dart +++ /dev/null @@ -1,33 +0,0 @@ -import 'package:flutter/cupertino.dart'; -import 'package:flutter/material.dart'; -import 'package:go_router/go_router.dart'; - -Future showAlertDialog( - BuildContext context, - String title, - String message, { - String? cancelLabel, - String? confirmLabel, - VoidCallback? onConfirm, -}) { - return showDialog( - context: context, - builder: (context) => CupertinoAlertDialog( - title: Text(title), - content: Text(message), - actions: [ - CupertinoDialogAction( - onPressed: () => context.pop(), - child: Text(cancelLabel ?? 'Cancel'), - ), - TextButton( - onPressed: () { - context.pop(true); - onConfirm!(); - }, - child: Text(confirmLabel ?? 'Yes'), - ), - ], - ), - ); -} diff --git a/lib/utils/dialogs/app_dialog.dart b/lib/utils/dialogs/app_dialog.dart new file mode 100644 index 0000000..2518ad4 --- /dev/null +++ b/lib/utils/dialogs/app_dialog.dart @@ -0,0 +1,99 @@ +import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; +import 'package:go_router/go_router.dart'; +import 'package:varanasi_mobile_app/utils/helpers/get_app_context.dart'; + +enum AppDialogAction { + cancel, + confirm; + + bool get isCancel => this == AppDialogAction.cancel; + bool get isConfirm => this == AppDialogAction.confirm; +} + +class AppDialog { + AppDialog._(); + + static final AppDialog _instance = AppDialog._(); + + factory AppDialog() => _instance; + + static Widget _adaptiveAction({ + required BuildContext context, + required VoidCallback onPressed, + required Widget child, + bool isDefaultAction = false, + bool isDestructiveAction = false, + TextStyle? textStyle, + }) { + final ThemeData theme = Theme.of(context); + switch (theme.platform) { + case TargetPlatform.android: + case TargetPlatform.fuchsia: + case TargetPlatform.linux: + case TargetPlatform.windows: + return TextButton( + onPressed: onPressed, + style: TextButton.styleFrom(textStyle: textStyle), + child: child, + ); + case TargetPlatform.iOS: + case TargetPlatform.macOS: + return CupertinoDialogAction( + onPressed: onPressed, + isDefaultAction: isDefaultAction, + isDestructiveAction: isDestructiveAction, + textStyle: textStyle, + child: child, + ); + } + } + + static Future showAlertDialog({ + BuildContext? context, + required String title, + required String message, + required VoidCallback onConfirm, + VoidCallback? onCancel, + String cancelLabel = 'Cancel', + String confirmLabel = 'Yes', + AppDialogAction defaultAction = AppDialogAction.cancel, + TextStyle? titleStyle, + TextStyle? messageStyle, + TextStyle? cancelLabelStyle, + TextStyle? confirmLabelStyle, + T Function()? onWillPop, + }) { + return showAdaptiveDialog( + context: context ?? appContext, + builder: (context) => AlertDialog.adaptive( + title: Text(title, style: titleStyle), + titleTextStyle: titleStyle, + content: Text(message, style: messageStyle), + contentTextStyle: messageStyle, + actions: [ + _adaptiveAction( + isDefaultAction: defaultAction.isCancel, + context: context, + onPressed: () { + context.pop(); + onCancel?.call(); + }, + textStyle: cancelLabelStyle, + child: Text(cancelLabel, style: cancelLabelStyle), + ), + _adaptiveAction( + isDefaultAction: defaultAction.isConfirm, + context: context, + onPressed: () { + context.pop(onWillPop?.call()); + onConfirm(); + }, + child: Text(confirmLabel, style: confirmLabelStyle), + textStyle: confirmLabelStyle, + ), + ], + ), + ); + } +} diff --git a/lib/widgets/download_button.dart b/lib/widgets/download_button.dart index 510d5c7..36c7937 100644 --- a/lib/widgets/download_button.dart +++ b/lib/widgets/download_button.dart @@ -1,7 +1,11 @@ +import 'dart:io'; + +import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; 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/user-library/cubit/user_library_cubit.dart'; import 'package:varanasi_mobile_app/models/download.dart'; @@ -27,7 +31,7 @@ class DownloadButton extends StatelessWidget { onPressed: () { final cubit = context.read(); if (downloaded) { - cubit.deleteDownloadedMedia(media); + cubit.deleteSingle(media); } else if (downloading) { cubit.cancelDownload(media); } else { @@ -39,6 +43,7 @@ class DownloadButton extends StatelessWidget { downloading: downloading, progress: progress, downloaded: downloaded, + hideStopIconOnIos: true, ), color: context.colorScheme.onBackground, ); @@ -100,12 +105,14 @@ class DownloadStatus extends StatelessWidget { required this.progress, this.dimension = 22, required this.downloaded, + this.hideStopIconOnIos = false, }); final bool downloading; final bool downloaded; final double progress; final double dimension; + final bool hideStopIconOnIos; double get iconSize => dimension * 0.6; @@ -126,32 +133,51 @@ class DownloadStatus extends StatelessWidget { size: iconSize, ), ), - child: Stack( - children: [ - if (downloading) - CircularProgressIndicator( - strokeWidth: 2, - value: downloading ? progress : 1, - valueColor: const AlwaysStoppedAnimation(Colors.white), - backgroundColor: Colors.white.withOpacity(0.3), + child: Visibility( + visible: downloading, + replacement: DecoratedBox( + decoration: const BoxDecoration( + shape: BoxShape.circle, + border: Border.fromBorderSide( + BorderSide(color: Colors.white, width: 1.25), + ), + ), + child: Icon( + Icons.arrow_downward_rounded, + color: Colors.white, + size: iconSize, + ), + ), + child: Stack( + children: [ + Positioned.fill( + child: hideStopIconOnIos + ? CupertinoActivityIndicator( + radius: dimension / 2, + color: Colors.white, + ) + : CircularPercentIndicator( + radius: dimension / 2, + percent: progress, + lineWidth: 2, + progressColor: Colors.white, + backgroundColor: Colors.white.withOpacity(0.3), + circularStrokeCap: CircularStrokeCap.round, + ), ), - if (!downloading) - Container( - decoration: const BoxDecoration( - shape: BoxShape.circle, - border: Border.fromBorderSide( - BorderSide(color: Colors.white, width: 1.5), + if (hideStopIconOnIos && Platform.isIOS) ...[ + const SizedBox.shrink(), + ] else ...[ + Positioned.fill( + child: Icon( + Icons.stop_rounded, + color: Colors.white, + size: iconSize, ), ), - ), - Positioned.fill( - child: Icon( - downloading ? Icons.stop_rounded : Icons.arrow_downward_rounded, - color: Colors.white, - size: iconSize, - ), - ), - ], + ], + ], + ), ), ), ); diff --git a/pubspec.lock b/pubspec.lock index d99b34e..d65156d 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -960,6 +960,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.2.0" + percent_indicator: + dependency: "direct main" + description: + name: percent_indicator + sha256: c37099ad833a883c9d71782321cb65c3a848c21b6939b6185f0ff6640d05814c + url: "https://pub.dev" + source: hosted + version: "4.2.3" petitparser: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index 96562b6..0182c34 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -74,6 +74,7 @@ dependencies: flutter_native_splash: ^2.3.3 background_downloader: ^7.10.1 lottie: ^2.7.0 + percent_indicator: ^4.2.3 dev_dependencies: flutter_test: From f9d42a25a27dc11bc95bef864f00218e08ccea3e Mon Sep 17 00:00:00 2001 From: devaryakjha Date: Mon, 23 Oct 2023 02:24:14 +0530 Subject: [PATCH 11/18] =?UTF-8?q?=E2=9C=A8=20New:=20Download=20quality=20p?= =?UTF-8?q?icker?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/cubits/download/download_cubit.dart | 17 +++ lib/features/settings/ui/settings_page.dart | 147 +++++++------------- lib/models/app_config.dart | 25 +++- lib/models/app_config.g.dart | 11 +- lib/models/playable_item.dart | 2 +- lib/utils/dialogs/app_dialog.dart | 100 +++++++++++++ 6 files changed, 198 insertions(+), 104 deletions(-) diff --git a/lib/cubits/download/download_cubit.dart b/lib/cubits/download/download_cubit.dart index 358326d..8264dc8 100644 --- a/lib/cubits/download/download_cubit.dart +++ b/lib/cubits/download/download_cubit.dart @@ -5,7 +5,9 @@ import 'package:background_downloader/background_downloader.dart'; import 'package:equatable/equatable.dart'; import 'package:hive_flutter/hive_flutter.dart'; import 'package:rxdart/rxdart.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/media_playlist.dart'; import 'package:varanasi_mobile_app/models/playable_item.dart'; import 'package:varanasi_mobile_app/models/song.dart'; @@ -120,6 +122,8 @@ class DownloadCubit extends AppCubit { Future downloadSong(PlayableMedia song) async { assert(song is Song, 'Only songs can be downloaded'); if (song is! Song) return; + final quality = await getDownloadQuality(); + if (quality == null) return; _songMap[song.itemId] = song; final queued = await _downloader.enqueue(_songToTask(song)); _logger.i('Queued ${song.itemId} status: $queued'); @@ -256,4 +260,17 @@ class DownloadCubit extends AppCubit { _downloadBox.containsKey(song.itemId); bool _isNotDownloaded(PlayableMedia song) => !_isDownloaded(song); + + Future getDownloadQuality() async { + final savedQuality = AppConfig.effectiveDlQuality; + if (savedQuality != null) return savedQuality; + AppConfig.effectiveDlQuality = await AppDialog.showOptionsPicker( + null, + savedQuality ?? DownloadQuality.high, + DownloadQuality.values, + (q) => q.describeQuality, + title: "Select Download Quality", + ); + return AppConfig.effectiveDlQuality; + } } diff --git a/lib/features/settings/ui/settings_page.dart b/lib/features/settings/ui/settings_page.dart index ca08449..ea6b227 100644 --- a/lib/features/settings/ui/settings_page.dart +++ b/lib/features/settings/ui/settings_page.dart @@ -1,10 +1,8 @@ import 'package:another_flushbar/flushbar_helper.dart'; import 'package:flex_color_scheme/flex_color_scheme.dart'; -import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:go_router/go_router.dart'; import 'package:settings_ui/settings_ui.dart'; import 'package:varanasi_mobile_app/cubits/config/config_cubit.dart'; import 'package:varanasi_mobile_app/flavors.dart'; @@ -41,12 +39,51 @@ class SettingsPage extends StatelessWidget { 0, appConfig.copyWith( isDataSaverEnabled: value, - downloadQuality: + streamingQuality: value ? DownloadQuality.low : DownloadQuality.extreme, ), ); }, ), + SettingsTile.navigation( + title: const Text('Download quality'), + leading: const Icon(Icons.download_outlined), + value: Text( + appConfig.downloadingQuality?.describeQuality ?? "", + ), + onPressed: (ctx) { + AppDialog.showOptionsPicker( + ctx, + appConfig.downloadingQuality, + DownloadQuality.values, + (e) => e?.describeQuality ?? "", + title: "Select Download Quality", + ).then((value) { + if (value != null) { + AppConfig.effectiveDlQuality = value; + } + }); + }, + ), + SettingsTile.navigation( + title: const Text('Streaming quality'), + leading: const Icon(Icons.music_note_outlined), + value: Text(appConfig.streamingQuality?.describeQuality ?? ""), + onPressed: (ctx) { + AppDialog.showOptionsPicker( + ctx, + appConfig.streamingQuality, + DownloadQuality.values, + (e) => e?.describeQuality ?? "", + title: "Select Streaming Quality", + ).then((value) { + if (value != null) { + AppConfig.getBox + .put(0, appConfig.copyWith(streamingQuality: value)); + } + }); + }, + ), SettingsTile.switchTile( initialValue: appConfig.isAdvancedModeEnabled, title: const Text("Advanced Settings"), @@ -56,7 +93,7 @@ class SettingsPage extends StatelessWidget { 0, appConfig.copyWith( isAdvancedModeEnabled: value, - downloadQuality: !value ? DownloadQuality.extreme : null, + streamingQuality: !value ? DownloadQuality.extreme : null, colorScheme: !value ? 41 : null, ), ); @@ -70,37 +107,26 @@ class SettingsPage extends StatelessWidget { tiles: [ if (!appConfig.isDataSaverEnabled) SettingsTile.navigation( - title: const Text('Streaming quality'), - leading: const Icon(Icons.music_note_outlined), - value: Text(appConfig.downloadQuality?.describeQuality ?? ""), + leading: const Icon(Icons.format_paint_outlined), + title: const Text('Theme'), + value: Text(appConfig.scheme.describeScheme), onPressed: (ctx) { - _showOptionsPicker( + AppDialog.showOptionsPicker( ctx, - appConfig.downloadQuality, - DownloadQuality.values, - (e) => e?.describeQuality ?? "", + appConfig.scheme, + FlexScheme.values, + (e) => e.describeScheme, + title: "Select Theme", ).then((value) { if (value != null) { - AppConfig.getBox - .put(0, appConfig.copyWith(downloadQuality: value)); + AppConfig.getBox.put( + 0, + appConfig.copyWith(colorScheme: value.index), + ); } }); }, ), - SettingsTile.navigation( - leading: const Icon(Icons.format_paint_outlined), - title: const Text('Theme'), - value: Text(appConfig.scheme.describeScheme), - onPressed: (ctx) { - _showOptionsPicker(ctx, appConfig.scheme, FlexScheme.values, - (e) => e.describeScheme).then((value) { - if (value != null) { - AppConfig.getBox - .put(0, appConfig.copyWith(colorScheme: value.index)); - } - }); - }, - ), SettingsTile( leading: const Icon(Icons.delete_forever_outlined), title: const Text("Clear cache"), @@ -169,73 +195,6 @@ class SettingsPage extends StatelessWidget { } } -Future _showOptionsPicker( - BuildContext context, - T initialValue, - List options, - String Function(T) labelMapper, -) async { - return await showCupertinoModalPopup( - context: context, - builder: (context) { - var selectedScheme = initialValue; - final viewInsets = MediaQuery.viewInsetsOf(context); - return StatefulBuilder(builder: (context, setState) { - return Container( - height: 300, - margin: EdgeInsets.only(bottom: viewInsets.bottom), - color: CupertinoColors.systemBackground.resolveFrom(context), - child: SafeArea( - top: false, - child: Column( - children: [ - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - CupertinoButton( - child: const Text("Close"), - onPressed: () => context.pop(), - ), - CupertinoButton( - child: const Text("Save"), - onPressed: () => context.pop(selectedScheme), - ), - ], - ), - Expanded( - child: CupertinoPicker( - magnification: 1.2, - useMagnifier: true, - itemExtent: 36, - onSelectedItemChanged: (int value) { - setState(() => selectedScheme = options[value]); - }, - squeeze: 1.2, - scrollController: FixedExtentScrollController( - initialItem: options.indexOf(initialValue), - ), - children: options - .map((s) => Center(child: Text(labelMapper(s)))) - .toList(), - ), - ), - ], - ), - ), - ); - }); - }, - ); - // if (value != null) { - // await AppConfig.getBox.put(0, appConfig.copyWith(colorScheme: value)); - // if (context.mounted) { - // FlushbarHelper.createSuccess( - // message: "Theme changed 👍🏻", - // ).show(context); - // } - // } -} - class _VisibileWhenSection extends SettingsSection { const _VisibileWhenSection({ required super.tiles, diff --git a/lib/models/app_config.dart b/lib/models/app_config.dart index 224b719..40c617a 100644 --- a/lib/models/app_config.dart +++ b/lib/models/app_config.dart @@ -20,17 +20,20 @@ class AppConfig extends HiveObject with EquatableMixin { @HiveField(3, defaultValue: false) final bool isDataSaverEnabled; @HiveField(4) - final DownloadQuality? downloadQuality; + final DownloadQuality? streamingQuality; @HiveField(5, defaultValue: false) final bool isAdvancedModeEnabled; + @HiveField(6) + final DownloadQuality? downloadingQuality; AppConfig({ this.sortBy = SortBy.custom, this.repeatMode = 0, this.colorScheme = 41, this.isDataSaverEnabled = false, - this.downloadQuality, + this.streamingQuality, this.isAdvancedModeEnabled = false, + this.downloadingQuality, }); AppConfig copyWith({ @@ -38,17 +41,19 @@ class AppConfig extends HiveObject with EquatableMixin { int? repeatMode, int? colorScheme, bool? isDataSaverEnabled, - DownloadQuality? downloadQuality, + DownloadQuality? streamingQuality, bool? isAdvancedModeEnabled, + DownloadQuality? downloadingQuality, }) { return AppConfig( sortBy: sortBy ?? this.sortBy, repeatMode: repeatMode ?? this.repeatMode, colorScheme: colorScheme ?? this.colorScheme, isDataSaverEnabled: isDataSaverEnabled ?? this.isDataSaverEnabled, - downloadQuality: downloadQuality ?? this.downloadQuality, + streamingQuality: streamingQuality ?? this.streamingQuality, isAdvancedModeEnabled: isAdvancedModeEnabled ?? this.isAdvancedModeEnabled, + downloadingQuality: downloadingQuality ?? this.downloadingQuality, ); } @@ -58,8 +63,9 @@ class AppConfig extends HiveObject with EquatableMixin { repeatMode, colorScheme, isDataSaverEnabled, - downloadQuality, + streamingQuality, isAdvancedModeEnabled, + downloadingQuality, ]; @override @@ -72,4 +78,13 @@ class AppConfig extends HiveObject with EquatableMixin { static Future> openBox() => Hive.openBox(AppStrings.configBoxName); + + static DownloadQuality? get effectiveDlQuality => + getBox.get(0)?.downloadingQuality; + + static set effectiveDlQuality(DownloadQuality? quality) => getBox.put( + 0, + getBox.get(0)?.copyWith(downloadingQuality: quality) ?? + AppConfig(downloadingQuality: quality), + ); } diff --git a/lib/models/app_config.g.dart b/lib/models/app_config.g.dart index 60c6245..ec6f08a 100644 --- a/lib/models/app_config.g.dart +++ b/lib/models/app_config.g.dart @@ -21,15 +21,16 @@ class AppConfigAdapter extends TypeAdapter { repeatMode: fields[1] == null ? 0 : fields[1] as int, colorScheme: fields[2] == null ? 41 : fields[2] as int, isDataSaverEnabled: fields[3] == null ? false : fields[3] as bool, - downloadQuality: fields[4] as DownloadQuality?, + streamingQuality: fields[4] as DownloadQuality?, isAdvancedModeEnabled: fields[5] == null ? false : fields[5] as bool, + downloadingQuality: fields[6] as DownloadQuality?, ); } @override void write(BinaryWriter writer, AppConfig obj) { writer - ..writeByte(6) + ..writeByte(7) ..writeByte(0) ..write(obj.sortBy) ..writeByte(1) @@ -39,9 +40,11 @@ class AppConfigAdapter extends TypeAdapter { ..writeByte(3) ..write(obj.isDataSaverEnabled) ..writeByte(4) - ..write(obj.downloadQuality) + ..write(obj.streamingQuality) ..writeByte(5) - ..write(obj.isAdvancedModeEnabled); + ..write(obj.isAdvancedModeEnabled) + ..writeByte(6) + ..write(obj.downloadingQuality); } @override diff --git a/lib/models/playable_item.dart b/lib/models/playable_item.dart index 70bccf1..9228263 100644 --- a/lib/models/playable_item.dart +++ b/lib/models/playable_item.dart @@ -73,7 +73,7 @@ abstract class PlayableMedia extends Equatable { artist = artists.map((e) => e.name ?? '').join(', '); final isDataSaverEnabled = AppConfig.getBox.get(0)?.isDataSaverEnabled ?? false; - final effectiveQuality = AppConfig.getBox.get(0)?.downloadQuality ?? + final effectiveQuality = AppConfig.getBox.get(0)?.streamingQuality ?? (isDataSaverEnabled ? DownloadQuality.low : DownloadQuality.extreme); id = song.downloadUrl diff --git a/lib/utils/dialogs/app_dialog.dart b/lib/utils/dialogs/app_dialog.dart index 2518ad4..549bf55 100644 --- a/lib/utils/dialogs/app_dialog.dart +++ b/lib/utils/dialogs/app_dialog.dart @@ -1,6 +1,7 @@ import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:go_router/go_router.dart'; +import 'package:varanasi_mobile_app/utils/extensions/extensions.dart'; import 'package:varanasi_mobile_app/utils/helpers/get_app_context.dart'; enum AppDialogAction { @@ -96,4 +97,103 @@ class AppDialog { ), ); } + + static Future showOptionsPicker( + BuildContext? context, + T initialValue, + List options, + String Function(T) labelMapper, { + String? title, + String closeLabel = 'Close', + String selectLabel = 'Save', + }) async { + context ??= appContext; + Widget builder(context) { + final viewInsets = MediaQuery.viewInsetsOf(context); + return StatefulBuilder(builder: (context, setState) { + var selectedValue = initialValue; + return SizedBox( + height: 300, + child: Card( + color: context.colorScheme.surface, + child: Padding( + padding: EdgeInsets.only(bottom: viewInsets.bottom), + child: SafeArea( + top: false, + child: Column( + children: [ + Row( + mainAxisSize: MainAxisSize.min, + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + children: [ + const Expanded( + flex: 4, + child: SizedBox.shrink(), + ), + if (title != null) + Expanded( + flex: 12, + child: Text( + title, + maxLines: 1, + textAlign: TextAlign.center, + style: const TextStyle( + fontSize: 18, + fontWeight: FontWeight.w600, + ), + ), + ), + Expanded( + flex: 4, + child: CupertinoButton( + child: Text(selectLabel), + onPressed: () => context.pop(selectedValue), + ), + ), + ], + ), + Expanded( + child: CupertinoPicker.builder( + childCount: options.length, + itemBuilder: (_, i) { + return Center(child: Text(labelMapper(options[i]))); + }, + magnification: 1.2, + useMagnifier: true, + itemExtent: 36, + onSelectedItemChanged: (int value) { + setState(() => selectedValue = options[value]); + }, + squeeze: 1.2, + scrollController: FixedExtentScrollController( + initialItem: options.indexOf(initialValue), + ), + ), + ), + ], + ), + ), + ), + ), + ); + }); + } + + switch (context.theme.platform) { + case TargetPlatform.android: + case TargetPlatform.fuchsia: + case TargetPlatform.linux: + case TargetPlatform.windows: + return await showModalBottomSheet( + context: context, + builder: builder, + ); + case TargetPlatform.iOS: + case TargetPlatform.macOS: + return await showCupertinoModalPopup( + context: context, + builder: builder, + ); + } + } } From f01af590addd30d05d9b462be571bca21592e7a7 Mon Sep 17 00:00:00 2001 From: devaryakjha Date: Mon, 23 Oct 2023 02:35:21 +0530 Subject: [PATCH 12/18] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20Fix:=20using=20saved?= =?UTF-8?q?=20download=20quality?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/cubits/download/download_cubit.dart | 6 +++- lib/features/settings/ui/settings_page.dart | 31 ++++++++------------- lib/models/app_config.dart | 6 ++++ lib/models/download_url.dart | 7 +++-- lib/utils/dialogs/app_dialog.dart | 2 +- 5 files changed, 29 insertions(+), 23 deletions(-) diff --git a/lib/cubits/download/download_cubit.dart b/lib/cubits/download/download_cubit.dart index 8264dc8..d72b93f 100644 --- a/lib/cubits/download/download_cubit.dart +++ b/lib/cubits/download/download_cubit.dart @@ -111,9 +111,13 @@ class DownloadCubit extends AppCubit { DownloadTask _songToTask(Song song) { final fileName = _fileNameFromSong(song); + final dquality = AppConfig.effectiveDlQuality!; + final dlink = song.downloadUrl?.firstWhere( + (e) => e.quality == dquality.quality, + ); return DownloadTask( taskId: song.itemId, - url: song.itemUrl, + url: dlink?.link ?? song.itemUrl, filename: fileName, updates: Updates.statusAndProgress, ); diff --git a/lib/features/settings/ui/settings_page.dart b/lib/features/settings/ui/settings_page.dart index ea6b227..a767f61 100644 --- a/lib/features/settings/ui/settings_page.dart +++ b/lib/features/settings/ui/settings_page.dart @@ -51,37 +51,30 @@ class SettingsPage extends StatelessWidget { value: Text( appConfig.downloadingQuality?.describeQuality ?? "", ), - onPressed: (ctx) { - AppDialog.showOptionsPicker( + onPressed: (ctx) async { + AppConfig.effectiveDlQuality = + await AppDialog.showOptionsPicker( ctx, - appConfig.downloadingQuality, + appConfig.downloadingQuality ?? DownloadQuality.high, DownloadQuality.values, - (e) => e?.describeQuality ?? "", + (e) => e.describeQuality, title: "Select Download Quality", - ).then((value) { - if (value != null) { - AppConfig.effectiveDlQuality = value; - } - }); + ); }, ), SettingsTile.navigation( title: const Text('Streaming quality'), leading: const Icon(Icons.music_note_outlined), value: Text(appConfig.streamingQuality?.describeQuality ?? ""), - onPressed: (ctx) { - AppDialog.showOptionsPicker( + onPressed: (ctx) async { + AppConfig.effectivestreaQuality = + await AppDialog.showOptionsPicker( ctx, - appConfig.streamingQuality, + appConfig.streamingQuality ?? DownloadQuality.high, DownloadQuality.values, - (e) => e?.describeQuality ?? "", + (e) => e.describeQuality, title: "Select Streaming Quality", - ).then((value) { - if (value != null) { - AppConfig.getBox - .put(0, appConfig.copyWith(streamingQuality: value)); - } - }); + ); }, ), SettingsTile.switchTile( diff --git a/lib/models/app_config.dart b/lib/models/app_config.dart index 40c617a..afdf327 100644 --- a/lib/models/app_config.dart +++ b/lib/models/app_config.dart @@ -87,4 +87,10 @@ class AppConfig extends HiveObject with EquatableMixin { getBox.get(0)?.copyWith(downloadingQuality: quality) ?? AppConfig(downloadingQuality: quality), ); + + static set effectivestreaQuality(DownloadQuality? quality) => getBox.put( + 0, + getBox.get(0)?.copyWith(streamingQuality: quality) ?? + AppConfig(streamingQuality: quality), + ); } diff --git a/lib/models/download_url.dart b/lib/models/download_url.dart index e41e540..c812b56 100644 --- a/lib/models/download_url.dart +++ b/lib/models/download_url.dart @@ -1,6 +1,5 @@ import 'package:equatable/equatable.dart'; import 'package:flex_color_scheme/flex_color_scheme.dart'; -import 'package:flutter/foundation.dart'; import 'package:hive/hive.dart'; import 'package:json_annotation/json_annotation.dart'; @@ -32,7 +31,11 @@ enum DownloadQuality { _ => DownloadQuality.low, }; - String get describeQuality => describeEnum(this).capitalize; + String get describeQuality => switch (this) { + DownloadQuality.high => 'Normal', + DownloadQuality.veryHigh => 'Very High', + _ => name.capitalize, + }; } @JsonSerializable() diff --git a/lib/utils/dialogs/app_dialog.dart b/lib/utils/dialogs/app_dialog.dart index 549bf55..7abf283 100644 --- a/lib/utils/dialogs/app_dialog.dart +++ b/lib/utils/dialogs/app_dialog.dart @@ -110,8 +110,8 @@ class AppDialog { context ??= appContext; Widget builder(context) { final viewInsets = MediaQuery.viewInsetsOf(context); + var selectedValue = initialValue; return StatefulBuilder(builder: (context, setState) { - var selectedValue = initialValue; return SizedBox( height: 300, child: Card( From f1a8a331bf8f53d1bb4050e425961bd68e11c606 Mon Sep 17 00:00:00 2001 From: devaryakjha Date: Mon, 23 Oct 2023 02:40:36 +0530 Subject: [PATCH 13/18] =?UTF-8?q?=F0=9F=90=9B=20Fix:=20batch=20download=20?= =?UTF-8?q?not=20working?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/cubits/download/download_cubit.dart | 2 ++ 1 file changed, 2 insertions(+) diff --git a/lib/cubits/download/download_cubit.dart b/lib/cubits/download/download_cubit.dart index d72b93f..5d4138e 100644 --- a/lib/cubits/download/download_cubit.dart +++ b/lib/cubits/download/download_cubit.dart @@ -134,6 +134,8 @@ class DownloadCubit extends AppCubit { } Future batchDownload(MediaPlaylist playlist) async { + final quality = await getDownloadQuality(); + if (quality == null) return; final songs = playlist.mediaItems ?? []; final filteredsong = songs.whereType().where(_isNotDownloaded); final tasks = filteredsong.map(_songToTask).toList(); From 90f02a10450a9920be2542e7ed33f458acb226a7 Mon Sep 17 00:00:00 2001 From: devaryakjha Date: Mon, 23 Oct 2023 02:44:58 +0530 Subject: [PATCH 14/18] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20Update:=20update=20l?= =?UTF-8?q?ibrary=20when=20already=20exists?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/features/user-library/cubit/user_library_cubit.dart | 9 +++++++-- .../user-library/data/user_library_repository.dart | 2 ++ 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/lib/features/user-library/cubit/user_library_cubit.dart b/lib/features/user-library/cubit/user_library_cubit.dart index d6bd887..dad21fd 100644 --- a/lib/features/user-library/cubit/user_library_cubit.dart +++ b/lib/features/user-library/cubit/user_library_cubit.dart @@ -37,9 +37,14 @@ class UserLibraryCubit extends AppCubit { } Future addToLibrary(MediaPlaylist playlist) async { - _repository.addLibrary(playlist.toUserLibrary()); + final alreadyExists = _repository.libraryExists(playlist.id!); + if (alreadyExists) { + _repository.updateLibrary(playlist.toUserLibrary()); + } else { + _repository.addLibrary(playlist.toUserLibrary()); + AppSnackbar.show("Added to library"); + } final library = _repository.getLibraries(); emit(UserLibraryLoaded(library: library)); - AppSnackbar.show("Added to library"); } } diff --git a/lib/features/user-library/data/user_library_repository.dart b/lib/features/user-library/data/user_library_repository.dart index 5c4fb2a..00528d4 100644 --- a/lib/features/user-library/data/user_library_repository.dart +++ b/lib/features/user-library/data/user_library_repository.dart @@ -20,6 +20,8 @@ class UserLibraryRepository { List getLibraries() => _box.values.toList(); + bool libraryExists(String id) => _box.containsKey(id); + Future addLibrary(E library) => _box.put(library.id, library); From a7596e65ecbb49cf378c700a195de5a8ce08349c Mon Sep 17 00:00:00 2001 From: devaryakjha Date: Mon, 23 Oct 2023 03:08:59 +0530 Subject: [PATCH 15/18] =?UTF-8?q?=E2=9C=A8=20New:=20User=20library?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/features/library/cubit/library_cubit.dart | 19 +++++++++++++++ lib/features/library/ui/library_screen.dart | 4 ++-- .../library/ui/library_widgets/page.dart | 4 ++-- .../user-library/ui/user_library_page.dart | 18 +++++++++++--- lib/utils/router.dart | 24 +++++++++++++------ 5 files changed, 55 insertions(+), 14 deletions(-) diff --git a/lib/features/library/cubit/library_cubit.dart b/lib/features/library/cubit/library_cubit.dart index 20597b0..ae146f0 100644 --- a/lib/features/library/cubit/library_cubit.dart +++ b/lib/features/library/cubit/library_cubit.dart @@ -4,6 +4,7 @@ import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:palette_generator/palette_generator.dart'; import 'package:varanasi_mobile_app/cubits/config/config_cubit.dart'; import 'package:varanasi_mobile_app/features/library/data/library_repository.dart'; +import 'package:varanasi_mobile_app/features/user-library/data/user_library.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/sort_type.dart'; @@ -19,6 +20,24 @@ class LibraryCubit extends Cubit { static LibraryCubit of(BuildContext context) => context.read(); + Future loadUserLibrary(UserLibrary playlist) async { + try { + emit(const LibraryLoading()); + String link = playlist.images.last.link!; + if (!appContext.mounted) return; + final configCubit = appContext.read(); + final colorPalette = await configCubit.generatePalleteGenerator(link); + final image = configCubit.getProvider(link); + emit(LibraryLoaded( + playlist.toMediaPlaylist(), + colorPalette!, + image, + )); + } catch (e, s) { + emit(LibraryError(e, stackTrace: s)); + } + } + Future fetchLibrary(PlayableMedia media) async { try { emit(const LibraryLoading()); diff --git a/lib/features/library/ui/library_screen.dart b/lib/features/library/ui/library_screen.dart index 047b277..bbfbaa7 100644 --- a/lib/features/library/ui/library_screen.dart +++ b/lib/features/library/ui/library_screen.dart @@ -7,9 +7,9 @@ import 'library_widgets/library_loader.dart'; import 'library_widgets/page.dart'; class LibraryPage extends StatelessWidget { - final PlayableMedia source; + final PlayableMedia? source; - const LibraryPage({super.key, required this.source}); + const LibraryPage({super.key, this.source}); @override Widget build(BuildContext context) { diff --git a/lib/features/library/ui/library_widgets/page.dart b/lib/features/library/ui/library_widgets/page.dart index a6f1701..4aec7dd 100644 --- a/lib/features/library/ui/library_widgets/page.dart +++ b/lib/features/library/ui/library_widgets/page.dart @@ -15,7 +15,7 @@ import 'package:varanasi_mobile_app/widgets/typography.dart'; class LibraryContent extends StatefulHookWidget { /// The source of the library content - final PlayableMedia source; + final PlayableMedia? source; const LibraryContent({ super.key, @@ -127,7 +127,7 @@ class _LibraryContentState extends State { ), MediaListView.sliver( sortedMediaItems, - mediaType: widget.source.itemType, + mediaType: widget.source?.itemType, isPlaying: isThisPlaylistPlaying, isItemPlaying: (media) { return media.toMediaItem() == currentMediaItem; diff --git a/lib/features/user-library/ui/user_library_page.dart b/lib/features/user-library/ui/user_library_page.dart index ce56d79..64ad47f 100644 --- a/lib/features/user-library/ui/user_library_page.dart +++ b/lib/features/user-library/ui/user_library_page.dart @@ -1,6 +1,9 @@ +import 'package:cached_network_image/cached_network_image.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/features/user-library/cubit/user_library_cubit.dart'; +import 'package:varanasi_mobile_app/utils/routes.dart'; class UserLibraryPage extends StatelessWidget { const UserLibraryPage({super.key}); @@ -17,13 +20,22 @@ class UserLibraryPage extends StatelessWidget { if (state is! UserLibraryLoaded) { return const Center(child: CircularProgressIndicator()); } - final library = - state.library.map((e) => e.toMediaPlaylist()).toList(); + final library = state.library; return ListView.builder( itemBuilder: (context, index) { final item = library[index]; return ListTile( - leading: Text(item.title ?? ''), + 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, + ), + ), + title: Text(item.title ?? ''), + subtitle: Text(item.description ?? ''), ); }, itemCount: library.length, diff --git a/lib/utils/router.dart b/lib/utils/router.dart index c3eb2b8..94a9be1 100644 --- a/lib/utils/router.dart +++ b/lib/utils/router.dart @@ -9,6 +9,7 @@ import 'package:varanasi_mobile_app/features/search/cubit/search_cubit.dart'; import 'package:varanasi_mobile_app/features/search/ui/search_page.dart'; import 'package:varanasi_mobile_app/features/session/ui/auth_page.dart'; import 'package:varanasi_mobile_app/features/settings/ui/settings_page.dart'; +import 'package:varanasi_mobile_app/features/user-library/data/user_library.dart'; import 'package:varanasi_mobile_app/features/user-library/ui/user_library_page.dart'; import 'package:varanasi_mobile_app/models/media_playlist.dart'; import 'package:varanasi_mobile_app/models/playable_item.dart'; @@ -45,13 +46,22 @@ final routerConfig = GoRouter( name: AppRoutes.library.name, path: AppRoutes.library.path, builder: (context, state) { - final PlayableMedia media = state.extra! as PlayableMedia; - return BlocProvider( - key: state.pageKey, - lazy: false, - create: (context) => LibraryCubit()..fetchLibrary(media), - child: LibraryPage(source: media), - ); + final extra = state.extra!; + return switch (extra) { + _ when extra is PlayableMedia => BlocProvider( + key: state.pageKey, + lazy: false, + create: (context) => LibraryCubit()..fetchLibrary(extra), + child: LibraryPage(source: extra), + ), + _ => BlocProvider( + key: state.pageKey, + lazy: false, + create: (context) => + LibraryCubit()..loadUserLibrary(extra as UserLibrary), + child: const LibraryPage(), + ), + }; }, routes: [ GoRoute( From f9a771f42a52a0951bbb2fe4bb232861802af748 Mon Sep 17 00:00:00 2001 From: devaryakjha Date: Mon, 23 Oct 2023 03:26:41 +0530 Subject: [PATCH 16/18] =?UTF-8?q?=F0=9F=90=9B=20Fix:=20media=20not=20added?= =?UTF-8?q?=20to=20library=20when=20one=20song=20download?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 1 + lib/features/library/cubit/library_cubit.dart | 12 ++---------- lib/widgets/download_button.dart | 6 ++++++ 3 files changed, 9 insertions(+), 10 deletions(-) diff --git a/README.md b/README.md index bc22368..5acc02d 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,5 @@ # Project Varanasi + 🛠️ Currently in development, music streaming application built with Flutter for iOS and Android. The app is designed to have all the basic functionalities of a music app, with a UI heavily inspired by the Spotify mobile app. ### Key Features: diff --git a/lib/features/library/cubit/library_cubit.dart b/lib/features/library/cubit/library_cubit.dart index ae146f0..023053d 100644 --- a/lib/features/library/cubit/library_cubit.dart +++ b/lib/features/library/cubit/library_cubit.dart @@ -28,11 +28,7 @@ class LibraryCubit extends Cubit { final configCubit = appContext.read(); final colorPalette = await configCubit.generatePalleteGenerator(link); final image = configCubit.getProvider(link); - emit(LibraryLoaded( - playlist.toMediaPlaylist(), - colorPalette!, - image, - )); + emit(LibraryLoaded(playlist.toMediaPlaylist(), colorPalette!, image)); } catch (e, s) { emit(LibraryError(e, stackTrace: s)); } @@ -50,11 +46,7 @@ class LibraryCubit extends Cubit { final configCubit = appContext.read(); final colorPalette = await configCubit.generatePalleteGenerator(link); final image = configCubit.getProvider(link); - emit(LibraryLoaded( - playlist, - colorPalette!, - image, - )); + emit(LibraryLoaded(playlist, colorPalette!, image)); } on Exception catch (e, s) { LibraryRepository.instance.deleteCache(media.cacheKey); emit(LibraryError(e, stackTrace: s)); diff --git a/lib/widgets/download_button.dart b/lib/widgets/download_button.dart index 36c7937..c6e5b99 100644 --- a/lib/widgets/download_button.dart +++ b/lib/widgets/download_button.dart @@ -7,6 +7,7 @@ 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'; @@ -37,6 +38,11 @@ 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( From e09733c025bcb9246477a20cf25a5815919a3f52 Mon Sep 17 00:00:00 2001 From: devaryakjha Date: Mon, 23 Oct 2023 03:29:50 +0530 Subject: [PATCH 17/18] =?UTF-8?q?=E2=9C=A8=20New:=20User=20library?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/utils/constants/nav_items.dart | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/lib/utils/constants/nav_items.dart b/lib/utils/constants/nav_items.dart index adb6f9b..2f5c0ef 100644 --- a/lib/utils/constants/nav_items.dart +++ b/lib/utils/constants/nav_items.dart @@ -1,4 +1,3 @@ -import 'package:flutter/foundation.dart'; import 'package:varanasi_mobile_app/gen/assets.gen.dart'; typedef NavItem = ({SvgGenImage icon, SvgGenImage activeIcon, String label}); @@ -16,10 +15,9 @@ NavItems navItems = [ activeIcon: Assets.icon.nav.searchSelected, label: 'Search' ), - if (kDebugMode) - ( - icon: Assets.icon.nav.library, - activeIcon: Assets.icon.nav.librarySelected, - label: 'Library' - ), + ( + icon: Assets.icon.nav.library, + activeIcon: Assets.icon.nav.librarySelected, + label: 'Library' + ), ]; From 06efbf9a191405859f94e1f6d1303847d2a47daa Mon Sep 17 00:00:00 2001 From: devaryakjha Date: Mon, 23 Oct 2023 03:49:17 +0530 Subject: [PATCH 18/18] =?UTF-8?q?=F0=9F=90=9B=20Fix:=20Android=20build?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- android/build.gradle | 2 +- pubspec.yaml | 2 +- shorebird.yaml | 5 ++--- 3 files changed, 4 insertions(+), 5 deletions(-) diff --git a/android/build.gradle b/android/build.gradle index 6b8f20b..7632200 100644 --- a/android/build.gradle +++ b/android/build.gradle @@ -1,5 +1,5 @@ buildscript { - ext.kotlin_version = '1.7.10' + ext.kotlin_version = '1.8.0' repositories { google() mavenCentral() diff --git a/pubspec.yaml b/pubspec.yaml index 0182c34..a6dba7c 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.0.9+11 +version: 0.0.10+12 environment: sdk: ">=3.0.6 <4.0.0" diff --git a/shorebird.yaml b/shorebird.yaml index ec63d82..e9d970f 100644 --- a/shorebird.yaml +++ b/shorebird.yaml @@ -7,9 +7,8 @@ # when requesting patches from Shorebird's servers. app_id: 71fad6a9-60fa-4490-adfd-04a719aefed1 flavors: - dev: 71fad6a9-60fa-4490-adfd-04a719aefed1 - prod: 35783a4a-e8be-4ddb-aef3-b4eb4807d4a8 - + development: 71fad6a9-60fa-4490-adfd-04a719aefed1 + production: 35783a4a-e8be-4ddb-aef3-b4eb4807d4a8 # auto_update controls if Shorebird should automatically update in the background on launch. # If auto_update: false, you will need to use package:shorebird_code_push to trigger updates. # https://pub.dev/packages/shorebird_code_push