mirror of
https://github.com/KRTirtho/spotube.git
synced 2025-09-12 23:45:18 +00:00
feat: add download queue for desktop & initial playlist download support
This commit is contained in:
parent
92bc611c5e
commit
08f913e976
@ -30,6 +30,10 @@ class TrackTile extends HookConsumerWidget {
|
||||
|
||||
final bool isActive;
|
||||
|
||||
final bool isChecked;
|
||||
final bool showCheck;
|
||||
final void Function(bool?)? onCheckChange;
|
||||
|
||||
TrackTile(
|
||||
this.playback, {
|
||||
required this.track,
|
||||
@ -40,6 +44,9 @@ class TrackTile extends HookConsumerWidget {
|
||||
this.thumbnailUrl,
|
||||
this.onTrackPlayButtonPressed,
|
||||
this.showAlbum = true,
|
||||
this.isChecked = false,
|
||||
this.showCheck = false,
|
||||
this.onCheckChange,
|
||||
Key? key,
|
||||
}) : super(key: key);
|
||||
|
||||
@ -182,6 +189,11 @@ class TrackTile extends HookConsumerWidget {
|
||||
type: MaterialType.transparency,
|
||||
child: Row(
|
||||
children: [
|
||||
if (showCheck)
|
||||
Checkbox(
|
||||
value: isChecked,
|
||||
onChanged: (s) => onCheckChange?.call(s),
|
||||
),
|
||||
SizedBox(
|
||||
height: 20,
|
||||
width: 25,
|
||||
|
@ -1,8 +1,10 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:spotify/spotify.dart';
|
||||
import 'package:spotube/components/Shared/TrackTile.dart';
|
||||
import 'package:spotube/hooks/useBreakpoints.dart';
|
||||
import 'package:spotube/provider/Downloader.dart';
|
||||
import 'package:spotube/provider/Playback.dart';
|
||||
import 'package:spotube/utils/primitive_utils.dart';
|
||||
import 'package:spotube/utils/type_conversion_utils.dart';
|
||||
@ -26,16 +28,31 @@ class TracksTableView extends HookConsumerWidget {
|
||||
@override
|
||||
Widget build(context, ref) {
|
||||
Playback playback = ref.watch(playbackProvider);
|
||||
final downloader = ref.watch(downloaderProvider);
|
||||
TextStyle tableHeadStyle =
|
||||
const TextStyle(fontWeight: FontWeight.bold, fontSize: 16);
|
||||
|
||||
final breakpoint = useBreakpoints();
|
||||
|
||||
final selected = useState<List<String>>([]);
|
||||
final showCheck = useState<bool>(false);
|
||||
|
||||
return SliverList(
|
||||
delegate: SliverChildListDelegate([
|
||||
if (heading != null) heading!,
|
||||
Row(
|
||||
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: const EdgeInsets.all(8.0),
|
||||
child: Text(
|
||||
@ -75,8 +92,36 @@ class TracksTableView extends HookConsumerWidget {
|
||||
Text("Time", style: tableHeadStyle),
|
||||
const SizedBox(width: 10),
|
||||
],
|
||||
SizedBox(
|
||||
width: breakpoint.isLessThan(Breakpoints.lg) ? 40 : 110,
|
||||
PopupMenuButton(
|
||||
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 =
|
||||
"${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,
|
||||
playlistId: playlistId,
|
||||
track: track,
|
||||
@ -96,6 +146,18 @@ class TracksTableView extends HookConsumerWidget {
|
||||
userPlaylist: userPlaylist,
|
||||
isActive: playback.track?.id == track.value.id,
|
||||
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()
|
||||
]),
|
||||
|
75
lib/provider/Downloader.dart
Normal file
75
lib/provider/Downloader.dart
Normal 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,
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
14
pubspec.lock
14
pubspec.lock
@ -538,6 +538,13 @@ packages:
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
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:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
@ -1017,6 +1024,13 @@ packages:
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "1.2.0"
|
||||
queue:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: queue
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "3.1.0+1"
|
||||
riverpod:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
@ -67,6 +67,8 @@ dependencies:
|
||||
audio_session: ^0.1.9
|
||||
file_picker: ^4.6.1
|
||||
popover: ^0.2.6+3
|
||||
queue: ^3.1.0+1
|
||||
flutter_downloader: ^1.8.1
|
||||
|
||||
dev_dependencies:
|
||||
flutter_test:
|
||||
|
Loading…
Reference in New Issue
Block a user