Skip to content

Commit

Permalink
Merge pull request #44 from devaryakjha/43-show-downloads-inside-user…
Browse files Browse the repository at this point in the history
…-library

✨ Downloads inside user library
  • Loading branch information
devaryakjha committed Oct 23, 2023
2 parents f61c94f + b66d9e8 commit a87d5bf
Show file tree
Hide file tree
Showing 21 changed files with 328 additions and 115 deletions.
51 changes: 47 additions & 4 deletions lib/cubits/download/download_cubit.dart
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,14 @@ import 'dart:io';
import 'package:background_downloader/background_downloader.dart';
import 'package:equatable/equatable.dart';
import 'package:hive_flutter/hive_flutter.dart';
import 'package:path/path.dart' as path;
import 'package:path_provider/path_provider.dart';
import 'package:rxdart/rxdart.dart';
import 'package:varanasi_mobile_app/features/user-library/data/user_library.dart';
import 'package:varanasi_mobile_app/models/app_config.dart';
import 'package:varanasi_mobile_app/models/download.dart';
import 'package:varanasi_mobile_app/models/download_url.dart';
import 'package:varanasi_mobile_app/models/image.dart';
import 'package:varanasi_mobile_app/models/media_playlist.dart';
import 'package:varanasi_mobile_app/models/playable_item.dart';
import 'package:varanasi_mobile_app/models/song.dart';
Expand All @@ -19,7 +23,7 @@ import 'package:varanasi_mobile_app/utils/logger.dart';
part 'download_state.dart';

class DownloadCubit extends AppCubit<DownloadState> {
DownloadCubit() : super(const DownloadState());
DownloadCubit() : super(DownloadInitial());

late final Box<DownloadedMedia> _downloadBox;
late final FileDownloader _downloader;
Expand All @@ -30,11 +34,19 @@ class DownloadCubit extends AppCubit<DownloadState> {

Box<DownloadedMedia> get downloadBox => _downloadBox;

FileDownloader get downloader => _downloader;

DownloadLoadedState get loadedState => state as DownloadLoadedState;

@override
FutureOr<void> init() async {
final baseDir = await getApplicationDocumentsDirectory();
emit(DownloadLoadedState(downloadDirectory: baseDir));
_songMap = {};
_downloadBox = Hive.box<DownloadedMedia>(AppStrings.downloadBoxName);
_downloader = FileDownloader();
_downloader = FileDownloader()
..trackTasks()
..resumeFromBackground();
_downloader.updates.listen((update) {
if (update is TaskStatusUpdate) {
_handleTaskStatusUpdate(update);
Expand Down Expand Up @@ -142,12 +154,13 @@ class DownloadCubit extends AppCubit<DownloadState> {
for (final song in filteredsong) {
_songMap[song.itemId] = song;
}
emit(state.updateProgress(MapEntry(playlist.id!, 0)));
emit(loadedState.updateProgress(MapEntry(playlist.id!, 0)));
await _downloader.downloadBatch(
tasks,
batchProgressCallback: (succeeded, failed) {
final percentComplete = (succeeded + failed) / tasks.length;
emit(state.updateProgress(MapEntry(playlist.id!, percentComplete)));
emit(loadedState
.updateProgress(MapEntry(playlist.id!, percentComplete)));
_logger.i('Batch progress: $succeeded, $failed');
},
taskStatusCallback: _handleTaskStatusUpdate,
Expand Down Expand Up @@ -279,4 +292,34 @@ class DownloadCubit extends AppCubit<DownloadState> {
);
return AppConfig.effectiveDlQuality;
}

Stream<UserLibrary> get downloadLibraryStream {
return _downloadBox.watch().map((event) => toUserLibrary());
}

UserLibrary toUserLibrary() {
final List<DownloadedMedia> values = _downloadBox.values.toList();
final library = UserLibrary(
id: "downloads",
title: "Downloads",
description: "Your downloaded songs",
mediaItems: values.map((e) => e.media).toList(),
images: const [Image.likedSongs],
type: UserLibraryType.download,
);
return library;
}

String getDownloadPath(String id) {
final item = _downloadBox.get(id);
final filename = _fileNameFromSong(item!.media);
return path.join(loadedState.downloadDirectory.path, '', filename);
}

File? getCacheFile(String itemId, String itemUrl) {
final ext = itemUrl.split('.').last;
final fileName = '$itemId.$ext';
return File(
path.join(loadedState.downloadDirectory.path, 'cache', fileName));
}
}
25 changes: 19 additions & 6 deletions lib/cubits/download/download_state.dart
Original file line number Diff line number Diff line change
@@ -1,25 +1,38 @@
// ignore_for_file: public_member_api_docs, sort_constructors_first
part of 'download_cubit.dart';

class DownloadState extends Equatable {
sealed class DownloadState extends Equatable {
const DownloadState();

@override
List<Object> get props => [];
}

class DownloadInitial extends DownloadState {}

class DownloadLoadedState extends DownloadState {
final Map<String, double> playlistProgressMap;
final Directory downloadDirectory;

const DownloadState({
const DownloadLoadedState({
this.playlistProgressMap = const {},
required this.downloadDirectory,
});

@override
List<Object> get props => [playlistProgressMap];
List<Object> get props => [playlistProgressMap, downloadDirectory];

DownloadState copyWith({
DownloadLoadedState copyWith({
Map<String, double>? playlistProgressMap,
Directory? downloadDirectory,
}) {
return DownloadState(
return DownloadLoadedState(
playlistProgressMap: playlistProgressMap ?? this.playlistProgressMap,
downloadDirectory: downloadDirectory ?? this.downloadDirectory,
);
}

DownloadState updateProgress(MapEntry<String, double> entry) {
DownloadLoadedState updateProgress(MapEntry<String, double> entry) {
final Map<String, double> oldProgress = Map.from(playlistProgressMap)
..addEntries([entry]);
return copyWith(
Expand Down
12 changes: 11 additions & 1 deletion lib/cubits/player/player_cubit.dart
Original file line number Diff line number Diff line change
Expand Up @@ -62,10 +62,13 @@ class MediaPlayerCubit extends AppCubit<MediaPlayerState>

Future<void> playFromMediaPlaylist<T extends PlayableMedia>(
MediaPlaylist<T> playlist, [
int? startIndex,
PlayableMedia? initialMedia,
bool autoPlay = true,
]) async {
if (playlist.id == state.currentPlaylist && !audioHandler.player.playing) {
final startIndex = initialMedia == null
? null
: state.queueState.queue.indexOf(initialMedia.toMediaItem());
if (startIndex != null) {
await skipToIndex(startIndex, autoPlay);
} else if (autoPlay) {
Expand All @@ -76,6 +79,13 @@ class MediaPlayerCubit extends AppCubit<MediaPlayerState>
unawaited(_configCubit.saveCurrentPlaylist(playlist));
emit(state.copyWith(currentPlaylist: playlist.id));
await audioHandler.updateQueue(playlist.mediaItemsAsMediaItems);
final shuffleModeEnabled = audioHandler.player.shuffleModeEnabled;
if (shuffleModeEnabled) {
await audioHandler.setShuffleMode(AudioServiceShuffleMode.all);
}
final startIndex = initialMedia == null
? null
: state.queueState.queue.indexOf(initialMedia.toMediaItem());
if (startIndex != null) {
await skipToIndex(startIndex, autoPlay);
} else if (autoPlay) {
Expand Down
14 changes: 12 additions & 2 deletions lib/features/library/cubit/library_cubit.dart
Original file line number Diff line number Diff line change
Expand Up @@ -26,9 +26,19 @@ class LibraryCubit extends Cubit<LibraryState> {
String link = playlist.images.last.link!;
if (!appContext.mounted) return;
final configCubit = appContext.read<ConfigCubit>();
final colorPalette = await configCubit.generatePalleteGenerator(link);
PaletteGenerator.fromColors([]);
if (!appContext.mounted) return;
final colorPalette = playlist.isDownload && appContext.mounted
? PaletteGenerator.fromColors(
[PaletteColor(appContext.colorScheme.secondaryContainer, 1)])
: await configCubit.generatePalleteGenerator(link);
final image = configCubit.getProvider(link);
emit(LibraryLoaded(playlist.toMediaPlaylist(), colorPalette!, image));
emit(LibraryLoaded(
playlist.toMediaPlaylist(),
colorPalette!,
image,
sourceLibrary: playlist,
));
} catch (e, s) {
emit(LibraryError(e, stackTrace: s));
}
Expand Down
7 changes: 6 additions & 1 deletion lib/features/library/cubit/library_state.dart
Original file line number Diff line number Diff line change
Expand Up @@ -27,16 +27,19 @@ class LibraryLoaded<T extends PlayableMedia> extends LibraryState {
final PaletteGenerator colorPalette;
final ImageProvider image;
final bool showTitleInAppBar;
final UserLibrary? sourceLibrary;

const LibraryLoaded(
this.playlist,
this.colorPalette,
this.image, {
this.showTitleInAppBar = false,
this.sourceLibrary,
});

@override
List<Object> get props => [playlist, colorPalette, image, showTitleInAppBar];
List<Object?> get props =>
[playlist, colorPalette, image, showTitleInAppBar, sourceLibrary];

PaletteColor? get baseColor =>
colorPalette.dominantColor ?? colorPalette.vibrantColor;
Expand Down Expand Up @@ -75,12 +78,14 @@ class LibraryLoaded<T extends PlayableMedia> extends LibraryState {
PaletteGenerator? colorPalette,
ImageProvider? image,
bool? showTitleInAppBar,
UserLibrary? sourceLibrary,
}) {
return LibraryLoaded<T>(
playlist ?? this.playlist,
colorPalette ?? this.colorPalette,
image ?? this.image,
showTitleInAppBar: showTitleInAppBar ?? this.showTitleInAppBar,
sourceLibrary: sourceLibrary ?? this.sourceLibrary,
);
}

Expand Down
27 changes: 17 additions & 10 deletions lib/features/library/ui/library_widgets/library_app_bar.dart
Original file line number Diff line number Diff line change
Expand Up @@ -17,14 +17,19 @@ class LibraryAppBar extends StatelessWidget {
final LibraryLoaded<PlayableMedia> state;
final EdgeInsets padding;

bool get isFromUserLibrary =>
state.sourceLibrary != null &&
(state.sourceLibrary!.isDownload == true ||
state.sourceLibrary!.isFavorite == true);

@override
Widget build(BuildContext context) {
return SliverAppBar(
centerTitle: true,
elevation: 0,
scrolledUnderElevation: 10,
stretch: true,
expandedHeight: kSliverExpandedHeight,
expandedHeight: isFromUserLibrary ? 130 : kSliverExpandedHeight,
pinned: kSliverAppBarPinned,
collapsedHeight: kToolbarHeight,
flexibleSpace: LayoutBuilder(builder: (context, constraints) {
Expand Down Expand Up @@ -64,7 +69,7 @@ class LibraryAppBar extends StatelessWidget {
children: [
Container(
height: 64,
width: context.width * 0.85,
width: context.width * 0.8,
alignment: Alignment.topCenter,
child: SizedBox(
height: 32,
Expand All @@ -79,17 +84,19 @@ class LibraryAppBar extends StatelessWidget {
),
),
),
AnimatedContainer(
duration: kThemeAnimationDuration,
height: imageHeight,
width: imageHeight,
decoration: BoxDecoration(boxShadow: state.boxShadow),
child: Image(
if (!isFromUserLibrary)
AnimatedContainer(
duration: kThemeAnimationDuration,
height: imageHeight,
width: imageHeight,
image: state.image,
decoration: BoxDecoration(boxShadow: state.boxShadow),
child: Image(
height: imageHeight,
width: imageHeight,
image: state.image,
fit: BoxFit.cover,
),
),
),
],
),
),
Expand Down
17 changes: 14 additions & 3 deletions lib/features/library/ui/library_widgets/page.dart
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart' hide Typography;
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
Expand All @@ -10,6 +11,7 @@ import 'package:varanasi_mobile_app/models/playable_item.dart';
import 'package:varanasi_mobile_app/utils/extensions/extensions.dart';
import 'package:varanasi_mobile_app/utils/extensions/media_query.dart';
import 'package:varanasi_mobile_app/widgets/add_to_library.dart';
import 'package:varanasi_mobile_app/widgets/disable_child.dart';
import 'package:varanasi_mobile_app/widgets/download_button.dart';
import 'package:varanasi_mobile_app/widgets/media_list.dart';
import 'package:varanasi_mobile_app/widgets/play_pause_button.dart';
Expand Down Expand Up @@ -128,8 +130,17 @@ class _LibraryContentState extends State<LibraryContent> {
Row(
key: titleKey,
children: [
AddToLibrary(state.playlist),
DownloadPlaylist(playlist: state.playlist),
AddToLibrary(
state.playlist,
sourceLibrary: state.sourceLibrary,
),
DisableChild(
disabled: !kDebugMode &&
state.sourceLibrary?.isDownload == true,
child: DownloadPlaylist(
playlist: state.playlist,
),
),
const Spacer(),
const ShuffleModeToggle(iconSize: 24),
],
Expand All @@ -151,7 +162,7 @@ class _LibraryContentState extends State<LibraryContent> {
} else {
context.readMediaPlayerCubit.playFromMediaPlaylist(
state.playlist.copyWith(mediaItems: sortedMediaItems),
index,
mediaItem,
);
}
},
Expand Down
32 changes: 6 additions & 26 deletions lib/features/library/ui/library_widgets/sort_by_toggle.dart
Original file line number Diff line number Diff line change
@@ -1,11 +1,9 @@
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:go_router/go_router.dart';
import 'package:varanasi_mobile_app/cubits/config/config_cubit.dart';
import 'package:varanasi_mobile_app/models/sort_type.dart';
import 'package:varanasi_mobile_app/utils/dialogs/app_dialog.dart';
import 'package:varanasi_mobile_app/utils/extensions/extensions.dart';
import 'package:varanasi_mobile_app/utils/helpers/show_bottom_sheet.dart';

class SortByToggle extends StatelessWidget {
const SortByToggle({super.key});
Expand All @@ -31,30 +29,12 @@ class SortByToggle extends StatelessWidget {
),
child: const Text('Sort'),
onPressed: () async {
final padding = MediaQuery.paddingOf(context);
// show dialog
final value = await showAppBottomSheet<SortBy>(
final value = await AppDialog.showOptionsPicker(
context,
builder: (context) => ListView(
padding: padding.copyWith(left: 8, right: 8, top: 16),
children: [
ListTile(
title: const Text('Sort by'),
titleTextStyle: context.textTheme.bodyLarge
?.copyWith(fontWeight: FontWeight.bold),
),
...SortBy.values.map(
(e) => RadioListTile(
controlAffinity: ListTileControlAffinity.trailing,
groupValue: sortBy,
value: e,
onChanged: (value) => context.pop(value),
title: Text(describeEnum(e).capitalize),
selected: sortBy == e,
),
),
],
),
sortBy,
SortBy.values,
(e) => e.name.capitalize,
title: "Sort by",
);
if (context.mounted && value != null) {
context.read<ConfigCubit>().setSortType(value);
Expand Down
Loading

0 comments on commit a87d5bf

Please sign in to comment.