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/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/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/assets/icon/download.svg b/assets/icon/download.svg
new file mode 100644
index 0000000..fe7d58c
--- /dev/null
+++ b/assets/icon/download.svg
@@ -0,0 +1,5 @@
+
\ 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
diff --git a/lib/app.dart b/lib/app.dart
index af5abb0..c8f213e 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,14 +22,17 @@ 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(),
),
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 669f5eb..f1c6b29 100644
--- a/lib/cubits/config/config_cubit.dart
+++ b/lib/cubits/config/config_cubit.dart
@@ -2,28 +2,36 @@ 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: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';
+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
void init() async {
final packageInfo = await PackageInfo.fromPlatform();
@@ -56,6 +64,8 @@ class ConfigCubit extends AppCubit {
playerPageController: CarouselController(),
miniPlayerPageController: CarouselController(),
packageInfo: packageInfo,
+ currentQueueIndex: savedPlaylistIndex,
+ currentPlaylist: savedPlaylist,
));
}
@@ -107,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);
}
@@ -119,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);
}
@@ -165,4 +179,7 @@ class ConfigCubit extends AppCubit {
ConfigLoaded? get configOrNull =>
state is ConfigLoaded ? state as ConfigLoaded : null;
+
+ PlayableMedia? get currentMedia =>
+ savedPlaylist?.mediaItems?[savedPlaylistIndex ?? 0];
}
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/download/download_cubit.dart b/lib/cubits/download/download_cubit.dart
index a6badf6..5d4138e 100644
--- a/lib/cubits/download/download_cubit.dart
+++ b/lib/cubits/download/download_cubit.dart
@@ -1,15 +1,19 @@
import 'dart:async';
+import 'dart:io';
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';
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';
@@ -24,11 +28,12 @@ class DownloadCubit extends AppCubit {
Logger get _logger => Logger.instance;
+ Box get downloadBox => _downloadBox;
+
@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) {
@@ -89,6 +94,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}');
@@ -103,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,
);
@@ -114,14 +126,18 @@ 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');
}
Future batchDownload(MediaPlaylist playlist) async {
+ final quality = await getDownloadQuality();
+ if (quality == null) return;
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;
@@ -142,6 +158,13 @@ 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
@@ -171,6 +194,89 @@ class DownloadCubit extends AppCubit {
DownloadedMedia? getDownloadedMedia(PlayableMedia song) =>
_downloadBox.get(song.itemId);
- Future deleteDownloadedMedia(PlayableMedia song) =>
- _downloadBox.delete(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(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) async {
+ 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);
+ }
+ }
+
+ 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();
+ }
+
+ bool _isDownloaded(PlayableMedia song) =>
+ _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/cubits/player/player_cubit.dart b/lib/cubits/player/player_cubit.dart
index 7f60c1f..31e5f83 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, [
@@ -72,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) {
@@ -79,7 +81,6 @@ class MediaPlayerCubit extends AppCubit
} else if (autoPlay) {
await play();
}
- await configCubitGetter().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/features/library/cubit/library_cubit.dart b/lib/features/library/cubit/library_cubit.dart
index 20597b0..023053d 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,20 @@ 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());
@@ -31,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/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/settings/ui/settings_page.dart b/lib/features/settings/ui/settings_page.dart
index a0a025b..a767f61 100644
--- a/lib/features/settings/ui/settings_page.dart
+++ b/lib/features/settings/ui/settings_page.dart
@@ -1,17 +1,15 @@
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';
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 {
@@ -41,12 +39,44 @@ 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) async {
+ AppConfig.effectiveDlQuality =
+ await AppDialog.showOptionsPicker(
+ ctx,
+ appConfig.downloadingQuality ?? DownloadQuality.high,
+ DownloadQuality.values,
+ (e) => e.describeQuality,
+ title: "Select Download Quality",
+ );
+ },
+ ),
+ SettingsTile.navigation(
+ title: const Text('Streaming quality'),
+ leading: const Icon(Icons.music_note_outlined),
+ value: Text(appConfig.streamingQuality?.describeQuality ?? ""),
+ onPressed: (ctx) async {
+ AppConfig.effectivestreaQuality =
+ await AppDialog.showOptionsPicker(
+ ctx,
+ appConfig.streamingQuality ?? DownloadQuality.high,
+ DownloadQuality.values,
+ (e) => e.describeQuality,
+ title: "Select Streaming Quality",
+ );
+ },
+ ),
SettingsTile.switchTile(
initialValue: appConfig.isAdvancedModeEnabled,
title: const Text("Advanced Settings"),
@@ -56,7 +86,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 +100,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"),
@@ -110,10 +129,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(
@@ -169,73 +188,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/features/user-library/cubit/user_library_cubit.dart b/lib/features/user-library/cubit/user_library_cubit.dart
index 19812e1..dad21fd 100644
--- a/lib/features/user-library/cubit/user_library_cubit.dart
+++ b/lib/features/user-library/cubit/user_library_cubit.dart
@@ -1,8 +1,50 @@
-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 {
+ 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));
+ }
}
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