feat: add download queue for desktop & initial playlist download support

This commit is contained in:
Kingkor Roy Tirtho 2022-08-09 09:10:51 +06:00
parent 92bc611c5e
commit 08f913e976
5 changed files with 176 additions and 11 deletions

View File

@ -30,6 +30,10 @@ class TrackTile extends HookConsumerWidget {
final bool isActive; final bool isActive;
final bool isChecked;
final bool showCheck;
final void Function(bool?)? onCheckChange;
TrackTile( TrackTile(
this.playback, { this.playback, {
required this.track, required this.track,
@ -40,6 +44,9 @@ class TrackTile extends HookConsumerWidget {
this.thumbnailUrl, this.thumbnailUrl,
this.onTrackPlayButtonPressed, this.onTrackPlayButtonPressed,
this.showAlbum = true, this.showAlbum = true,
this.isChecked = false,
this.showCheck = false,
this.onCheckChange,
Key? key, Key? key,
}) : super(key: key); }) : super(key: key);
@ -182,6 +189,11 @@ class TrackTile extends HookConsumerWidget {
type: MaterialType.transparency, type: MaterialType.transparency,
child: Row( child: Row(
children: [ children: [
if (showCheck)
Checkbox(
value: isChecked,
onChanged: (s) => onCheckChange?.call(s),
),
SizedBox( SizedBox(
height: 20, height: 20,
width: 25, width: 25,

View File

@ -1,8 +1,10 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:spotify/spotify.dart'; import 'package:spotify/spotify.dart';
import 'package:spotube/components/Shared/TrackTile.dart'; import 'package:spotube/components/Shared/TrackTile.dart';
import 'package:spotube/hooks/useBreakpoints.dart'; import 'package:spotube/hooks/useBreakpoints.dart';
import 'package:spotube/provider/Downloader.dart';
import 'package:spotube/provider/Playback.dart'; import 'package:spotube/provider/Playback.dart';
import 'package:spotube/utils/primitive_utils.dart'; import 'package:spotube/utils/primitive_utils.dart';
import 'package:spotube/utils/type_conversion_utils.dart'; import 'package:spotube/utils/type_conversion_utils.dart';
@ -26,16 +28,31 @@ class TracksTableView extends HookConsumerWidget {
@override @override
Widget build(context, ref) { Widget build(context, ref) {
Playback playback = ref.watch(playbackProvider); Playback playback = ref.watch(playbackProvider);
final downloader = ref.watch(downloaderProvider);
TextStyle tableHeadStyle = TextStyle tableHeadStyle =
const TextStyle(fontWeight: FontWeight.bold, fontSize: 16); const TextStyle(fontWeight: FontWeight.bold, fontSize: 16);
final breakpoint = useBreakpoints(); final breakpoint = useBreakpoints();
final selected = useState<List<String>>([]);
final showCheck = useState<bool>(false);
return SliverList( return SliverList(
delegate: SliverChildListDelegate([ delegate: SliverChildListDelegate([
if (heading != null) heading!, if (heading != null) heading!,
Row( Row(
children: [ children: [
Checkbox(
value: selected.value.length == tracks.length,
onChanged: (checked) {
if (!showCheck.value) showCheck.value = true;
if (checked == true) {
selected.value = tracks.map((s) => s.id!).toList();
} else {
selected.value = [];
}
},
),
Padding( Padding(
padding: const EdgeInsets.all(8.0), padding: const EdgeInsets.all(8.0),
child: Text( child: Text(
@ -75,8 +92,36 @@ class TracksTableView extends HookConsumerWidget {
Text("Time", style: tableHeadStyle), Text("Time", style: tableHeadStyle),
const SizedBox(width: 10), const SizedBox(width: 10),
], ],
SizedBox( PopupMenuButton(
width: breakpoint.isLessThan(Breakpoints.lg) ? 40 : 110, itemBuilder: (context) {
return [
PopupMenuItem(
child: Row(
children: const [
Icon(Icons.file_download_outlined),
Text("Download"),
],
),
onTap: () async {
final spotubeTracks = await Future.wait(
tracks
.where(
(track) => selected.value.contains(track.id),
)
.map((track) {
return Future.delayed(const Duration(seconds: 2),
() => playback.toSpotubeTrack(track));
}),
);
for (var spotubeTrack in spotubeTracks) {
downloader.addToQueue(spotubeTrack);
}
},
value: "download",
),
];
},
), ),
], ],
), ),
@ -87,7 +132,12 @@ class TracksTableView extends HookConsumerWidget {
); );
String duration = String duration =
"${track.value.duration?.inMinutes.remainder(60)}:${PrimitiveUtils.zeroPadNumStr(track.value.duration?.inSeconds.remainder(60) ?? 0)}"; "${track.value.duration?.inMinutes.remainder(60)}:${PrimitiveUtils.zeroPadNumStr(track.value.duration?.inSeconds.remainder(60) ?? 0)}";
return TrackTile( return GestureDetector(
onDoubleTap: () {
showCheck.value = true;
selected.value = [...selected.value, track.value.id!];
},
child: TrackTile(
playback, playback,
playlistId: playlistId, playlistId: playlistId,
track: track, track: track,
@ -96,6 +146,18 @@ class TracksTableView extends HookConsumerWidget {
userPlaylist: userPlaylist, userPlaylist: userPlaylist,
isActive: playback.track?.id == track.value.id, isActive: playback.track?.id == track.value.id,
onTrackPlayButtonPressed: onTrackPlayButtonPressed, onTrackPlayButtonPressed: onTrackPlayButtonPressed,
isChecked: selected.value.contains(track.value.id),
showCheck: showCheck.value,
onCheckChange: (checked) {
if (checked == true) {
selected.value = [...selected.value, track.value.id!];
} else {
selected.value = selected.value
.where((id) => id != track.value.id)
.toList();
}
},
),
); );
}).toList() }).toList()
]), ]),

View File

@ -0,0 +1,75 @@
import 'dart:io';
import 'package:flutter/widgets.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:queue/queue.dart';
import 'package:path/path.dart' as path;
import 'package:spotube/models/SpotubeTrack.dart';
import 'package:spotube/provider/UserPreferences.dart';
import 'package:spotube/provider/YouTube.dart';
import 'package:youtube_explode_dart/youtube_explode_dart.dart';
Queue _queueInstance = Queue(delay: const Duration(seconds: 1));
class Downloader with ChangeNotifier {
Queue _queue;
YoutubeExplode yt;
String downloadPath;
Downloader(
this._queue, {
required this.downloadPath,
required this.yt,
});
int currentlyRunning = 0;
void addToQueue(SpotubeTrack track) {
currentlyRunning++;
notifyListeners();
_queue.add(() async {
try {
final file =
File(path.join(downloadPath, '${track.ytTrack.title}.mp3'));
// TODO find a way to let the UI know there's already provided file is available
if (file.existsSync()) return;
file.createSync(recursive: true);
StreamManifest manifest =
await yt.videos.streamsClient.getManifest(track.ytTrack.url);
final audioStream = yt.videos.streamsClient
.get(
manifest.audioOnly
.where((audio) => audio.codec.mimeType == "audio/mp4")
.withHighestBitrate(),
)
.asBroadcastStream();
IOSink outputFileStream = file.openWrite();
await audioStream.pipe(outputFileStream);
await outputFileStream.flush();
} finally {
currentlyRunning--;
notifyListeners();
}
});
}
cancel() {
_queue.cancel();
_queueInstance = Queue();
_queue = _queueInstance;
}
}
final downloaderProvider = ChangeNotifierProvider(
(ref) {
return Downloader(
_queueInstance,
yt: ref.watch(youtubeProvider),
downloadPath: ref.watch(
userPreferencesProvider.select(
(s) => s.downloadLocation,
),
),
);
},
);

View File

@ -538,6 +538,13 @@ packages:
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "0.0.9" version: "0.0.9"
flutter_downloader:
dependency: "direct main"
description:
name: flutter_downloader
url: "https://pub.dartlang.org"
source: hosted
version: "1.8.1"
flutter_hooks: flutter_hooks:
dependency: "direct main" dependency: "direct main"
description: description:
@ -1017,6 +1024,13 @@ packages:
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "1.2.0" version: "1.2.0"
queue:
dependency: "direct main"
description:
name: queue
url: "https://pub.dartlang.org"
source: hosted
version: "3.1.0+1"
riverpod: riverpod:
dependency: transitive dependency: transitive
description: description:

View File

@ -67,6 +67,8 @@ dependencies:
audio_session: ^0.1.9 audio_session: ^0.1.9
file_picker: ^4.6.1 file_picker: ^4.6.1
popover: ^0.2.6+3 popover: ^0.2.6+3
queue: ^3.1.0+1
flutter_downloader: ^1.8.1
dev_dependencies: dev_dependencies:
flutter_test: flutter_test: