Skip to content

Commit

Permalink
feat(download): track table view multi select improvement, tap to pla…
Browse files Browse the repository at this point in the history
…y track support, existing track replace confirmation dialog and bulk download confirmation dialog
  • Loading branch information
KRTirtho committed Aug 9, 2022
1 parent ff369c7 commit e217553
Show file tree
Hide file tree
Showing 6 changed files with 204 additions and 42 deletions.
19 changes: 19 additions & 0 deletions lib/components/Home/Home.dart
Original file line number Diff line number Diff line change
Expand Up @@ -14,13 +14,15 @@ import 'package:spotube/components/Home/SpotubeNavigationBar.dart';
import 'package:spotube/components/LoaderShimmers/ShimmerCategories.dart';
import 'package:spotube/components/Lyrics/SyncedLyrics.dart';
import 'package:spotube/components/Search/Search.dart';
import 'package:spotube/components/Shared/DownloadTrackButton.dart';
import 'package:spotube/components/Shared/PageWindowTitleBar.dart';
import 'package:spotube/components/Player/Player.dart';
import 'package:spotube/components/Library/UserLibrary.dart';
import 'package:spotube/hooks/useBreakpointValue.dart';
import 'package:spotube/hooks/usePaginatedFutureProvider.dart';
import 'package:spotube/hooks/useUpdateChecker.dart';
import 'package:spotube/models/Logger.dart';
import 'package:spotube/provider/Downloader.dart';
import 'package:spotube/provider/SpotifyRequests.dart';
import 'package:spotube/utils/platform.dart';

Expand Down Expand Up @@ -53,6 +55,23 @@ class Home extends HookConsumerWidget {
final _selectedIndex = useState(0);
_onSelectedIndexChanged(int index) => _selectedIndex.value = index;

final downloader = ref.watch(downloaderProvider);
final isMounted = useIsMounted();

useEffect(() {
downloader.onFileExists = (track) async {
if (!isMounted()) return false;
return await showDialog<bool>(
context: context,
builder: (context) => ReplaceDownloadedFileDialog(
track: track,
),
) ??
false;
};
return null;
}, [downloader]);

// checks for latest version of the application
useUpdateChecker(ref);

Expand Down
92 changes: 92 additions & 0 deletions lib/components/Shared/DownloadConfirmationDialog.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
import 'package:cached_network_image/cached_network_image.dart';
import 'package:flutter/material.dart';

class DownloadConfirmationDialog extends StatelessWidget {
const DownloadConfirmationDialog({Key? key}) : super(key: key);

@override
Widget build(BuildContext context) {
return AlertDialog(
contentPadding: const EdgeInsets.all(15),
title: Row(
children: [
const Text("Are you sure?"),
const SizedBox(width: 10),
CachedNetworkImage(
imageUrl:
"https://c.tenor.com/kHcmsxlKHEAAAAAM/rock-one-eyebrow-raised-rock-staring.gif",
height: 40,
width: 40,
)
],
),
content: ConstrainedBox(
constraints: const BoxConstraints(maxWidth: 400),
child: SingleChildScrollView(
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: const [
Text(
"If you download all Tracks at bulk you're clearly pirating Music & causing damage to the creative society of Music. I hope you are aware of this. Always, try respecting & supporting Artist's hard work",
textAlign: TextAlign.justify,
),
SizedBox(height: 10),
Text(
"BTW, your IP can get blocked on YouTube due excessive download requests than usual. IP block means you can't use YouTube (even if you're logged in) for at least 2-3 months from that IP device. And Spotube doesn't hold any responsibility if this ever happens",
style: TextStyle(
color: Colors.red,
fontWeight: FontWeight.bold,
),
textAlign: TextAlign.justify,
),
SizedBox(height: 10),
Text(
"By clicking 'accept' you agree to following terms:",
),
SizedBox(height: 10),
BulletPoint("I know I'm pirating Music. I'm bad"),
SizedBox(height: 10),
BulletPoint(
"I'll support the Artist wherever I can and I'm only doing this because I don't have money to buy their art"),
SizedBox(height: 10),
BulletPoint(
"I'm completely aware that my IP can get blocked on YouTube & I don't hold Spotube or his owners/contributors responsible for any accidents caused by my current action"),
],
),
),
),
actions: [
ElevatedButton(
child: const Text("Decline"),
onPressed: () => Navigator.of(context).pop(false),
),
ElevatedButton(
child: const Text("Accept"),
onPressed: () => Navigator.of(context).pop(true),
style: ElevatedButton.styleFrom(
primary: Colors.red,
onPrimary: Colors.white,
),
)
],
);
}
}

class BulletPoint extends StatelessWidget {
final String text;
const BulletPoint(this.text, {Key? key}) : super(key: key);

@override
Widget build(BuildContext context) {
return Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text("●"),
const SizedBox(width: 5),
Flexible(child: Text(text)),
],
);
}
}
49 changes: 30 additions & 19 deletions lib/components/Shared/DownloadTrackButton.dart
Original file line number Diff line number Diff line change
Expand Up @@ -67,25 +67,7 @@ class DownloadTrackButton extends HookConsumerWidget {
final shouldReplace = await showDialog<bool>(
context: context,
builder: (context) {
return AlertDialog(
title: const Text("Track Already Exists"),
content: const Text(
"Do you want to replace the already downloaded track?"),
actions: [
TextButton(
child: const Text("No"),
onPressed: () {
Navigator.pop(context, false);
},
),
TextButton(
child: const Text("Yes"),
onPressed: () {
Navigator.pop(context, true);
},
)
],
);
return ReplaceDownloadedFileDialog(track: track!);
},
);
if (shouldReplace != true) return;
Expand Down Expand Up @@ -209,3 +191,32 @@ class DownloadTrackButton extends HookConsumerWidget {
);
}
}

class ReplaceDownloadedFileDialog extends StatelessWidget {
final Track track;
const ReplaceDownloadedFileDialog({required this.track, Key? key})
: super(key: key);

@override
Widget build(BuildContext context) {
return AlertDialog(
title: Text("Track ${track.name} Already Exists"),
content:
const Text("Do you want to replace the already downloaded track?"),
actions: [
TextButton(
child: const Text("No"),
onPressed: () {
Navigator.pop(context, false);
},
),
TextButton(
child: const Text("Yes"),
onPressed: () {
Navigator.pop(context, true);
},
)
],
);
}
}
63 changes: 45 additions & 18 deletions lib/components/Shared/TracksTableView.dart
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:queue/queue.dart';
import 'package:spotify/spotify.dart';
import 'package:spotube/components/Shared/DownloadConfirmationDialog.dart';
import 'package:spotube/components/Shared/TrackTile.dart';
import 'package:spotube/hooks/useBreakpoints.dart';
import 'package:spotube/provider/Downloader.dart';
Expand Down Expand Up @@ -50,6 +52,7 @@ class TracksTableView extends HookConsumerWidget {
selected.value = tracks.map((s) => s.id!).toList();
} else {
selected.value = [];
showCheck.value = false;
}
},
),
Expand Down Expand Up @@ -96,32 +99,49 @@ class TracksTableView extends HookConsumerWidget {
itemBuilder: (context) {
return [
PopupMenuItem(
enabled: selected.value.isNotEmpty,
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",
),
];
},
onSelected: (action) async {
switch (action) {
case "download":
{
final isConfirmed = await showDialog(
context: context,
builder: (context) {
return const DownloadConfirmationDialog();
});
if (isConfirmed != true) return;
final queue = Queue(
delay: const Duration(seconds: 5),
);
final selectedTracks = tracks.where(
(track) => selected.value.contains(track.id),
);
for (final selectedTrack in selectedTracks) {
queue.add(() async {
downloader.addToQueue(
await playback.toSpotubeTrack(
selectedTrack,
noSponsorBlock: true,
),
);
});
}
await queue.onComplete;
break;
}
default:
}
},
),
],
),
Expand All @@ -132,11 +152,18 @@ class TracksTableView extends HookConsumerWidget {
);
String duration =
"${track.value.duration?.inMinutes.remainder(60)}:${PrimitiveUtils.zeroPadNumStr(track.value.duration?.inSeconds.remainder(60) ?? 0)}";
return GestureDetector(
onDoubleTap: () {
return InkWell(
onLongPress: () {
showCheck.value = true;
selected.value = [...selected.value, track.value.id!];
},
onTap: () {
if (showCheck.value) {
selected.value = [...selected.value, track.value.id!];
} else {
onTrackPlayButtonPressed?.call(track.value);
}
},
child: TrackTile(
playback,
playlistId: playlistId,
Expand Down
14 changes: 11 additions & 3 deletions lib/provider/Downloader.dart
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import 'dart:async';
import 'dart:io';

import 'package:flutter/widgets.dart';
Expand All @@ -9,29 +10,35 @@ 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));
Queue _queueInstance = Queue(delay: const Duration(seconds: 5));

class Downloader with ChangeNotifier {
Queue _queue;
YoutubeExplode yt;
String downloadPath;
FutureOr<bool> Function(SpotubeTrack track)? onFileExists;
Downloader(
this._queue, {
required this.downloadPath,
required this.yt,
this.onFileExists,
});

int currentlyRunning = 0;
Set<String> inQueue = {};

void addToQueue(SpotubeTrack track) {
if (inQueue.contains(track.id!)) return;
currentlyRunning++;
inQueue.add(track.id!);
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;
if (file.existsSync() && await onFileExists?.call(track) != true) {
return;
}
file.createSync(recursive: true);
StreamManifest manifest =
await yt.videos.streamsClient.getManifest(track.ytTrack.url);
Expand All @@ -48,6 +55,7 @@ class Downloader with ChangeNotifier {
await outputFileStream.flush();
} finally {
currentlyRunning--;
inQueue.remove(track.id);
notifyListeners();
}
});
Expand Down
9 changes: 7 additions & 2 deletions lib/provider/Playback.dart
Original file line number Diff line number Diff line change
Expand Up @@ -333,7 +333,10 @@ class Playback extends PersistedChangeNotifier {
}

// playlist & track list methods
Future<SpotubeTrack> toSpotubeTrack(Track track) async {
Future<SpotubeTrack> toSpotubeTrack(
Track track, {
bool noSponsorBlock = false,
}) async {
try {
final format = preferences.ytSearchFormat;
final matchAlgorithm = preferences.trackMatchAlgorithm;
Expand Down Expand Up @@ -452,7 +455,9 @@ class Playback extends PersistedChangeNotifier {
(segment) => segment.toJson(),
)
.toList()
: await getSkipSegments(ytVideo.id.value);
: noSponsorBlock
? List.castFrom<dynamic, Map<String, int>>([])
: await getSkipSegments(ytVideo.id.value);

// only save when the track isn't available in the cache with same
// matchAlgorithm
Expand Down

0 comments on commit e217553

Please sign in to comment.