Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

✨ Downloads inside user library #44

Merged
merged 10 commits into from
Oct 23, 2023
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