mirror of
https://github.com/KRTirtho/spotube.git
synced 2025-09-12 23:45:18 +00:00
feat: add download tab on library
Addition of download badge in sidebar and navbar library icon Fix SpotubeMarqueeText behavior using auto_size_text
This commit is contained in:
parent
a23ce61446
commit
8d77b6900a
@ -1,5 +1,3 @@
|
|||||||
import 'dart:io';
|
|
||||||
|
|
||||||
import 'package:bitsdojo_window/bitsdojo_window.dart';
|
import 'package:bitsdojo_window/bitsdojo_window.dart';
|
||||||
import 'package:flutter/material.dart' hide Page;
|
import 'package:flutter/material.dart' hide Page;
|
||||||
import 'package:flutter/services.dart';
|
import 'package:flutter/services.dart';
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
import 'package:badges/badges.dart';
|
||||||
import 'package:bitsdojo_window/bitsdojo_window.dart';
|
import 'package:bitsdojo_window/bitsdojo_window.dart';
|
||||||
import 'package:cached_network_image/cached_network_image.dart';
|
import 'package:cached_network_image/cached_network_image.dart';
|
||||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||||
@ -8,6 +9,7 @@ import 'package:spotube/hooks/useBreakpointValue.dart';
|
|||||||
import 'package:spotube/hooks/useBreakpoints.dart';
|
import 'package:spotube/hooks/useBreakpoints.dart';
|
||||||
import 'package:spotube/models/sideBarTiles.dart';
|
import 'package:spotube/models/sideBarTiles.dart';
|
||||||
import 'package:spotube/provider/Auth.dart';
|
import 'package:spotube/provider/Auth.dart';
|
||||||
|
import 'package:spotube/provider/Downloader.dart';
|
||||||
import 'package:spotube/provider/SpotifyRequests.dart';
|
import 'package:spotube/provider/SpotifyRequests.dart';
|
||||||
import 'package:spotube/utils/platform.dart';
|
import 'package:spotube/utils/platform.dart';
|
||||||
import 'package:spotube/utils/type_conversion_utils.dart';
|
import 'package:spotube/utils/type_conversion_utils.dart';
|
||||||
@ -40,6 +42,9 @@ class Sidebar extends HookConsumerWidget {
|
|||||||
final extended = useState(false);
|
final extended = useState(false);
|
||||||
final meSnapshot = ref.watch(currentUserQuery);
|
final meSnapshot = ref.watch(currentUserQuery);
|
||||||
final auth = ref.watch(authProvider);
|
final auth = ref.watch(authProvider);
|
||||||
|
final downloadCount = ref.watch(
|
||||||
|
downloaderProvider.select((s) => s.currentlyRunning),
|
||||||
|
);
|
||||||
|
|
||||||
final int titleBarDragMaxWidth = useBreakpointValue(
|
final int titleBarDragMaxWidth = useBreakpointValue(
|
||||||
md: 80,
|
md: 80,
|
||||||
@ -90,20 +95,34 @@ class Sidebar extends HookConsumerWidget {
|
|||||||
),
|
),
|
||||||
Expanded(
|
Expanded(
|
||||||
child: NavigationRail(
|
child: NavigationRail(
|
||||||
destinations: sidebarTileList
|
destinations: sidebarTileList.map(
|
||||||
.map(
|
(e) {
|
||||||
(e) => NavigationRailDestination(
|
final icon = Icon(e.icon);
|
||||||
icon: Icon(e.icon),
|
return NavigationRailDestination(
|
||||||
label: Text(
|
icon: e.title == "Library" && downloadCount > 0
|
||||||
e.title,
|
? Badge(
|
||||||
style: const TextStyle(
|
badgeColor: Colors.red[100]!,
|
||||||
fontWeight: FontWeight.bold,
|
badgeContent: Text(
|
||||||
fontSize: 16,
|
downloadCount.toString(),
|
||||||
),
|
style: const TextStyle(
|
||||||
|
color: Colors.red,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
animationType: BadgeAnimationType.fade,
|
||||||
|
child: icon,
|
||||||
|
)
|
||||||
|
: icon,
|
||||||
|
label: Text(
|
||||||
|
e.title,
|
||||||
|
style: const TextStyle(
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
fontSize: 16,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
)
|
);
|
||||||
.toList(),
|
},
|
||||||
|
).toList(),
|
||||||
selectedIndex: selectedIndex,
|
selectedIndex: selectedIndex,
|
||||||
onDestinationSelected: onSelectedIndexChanged,
|
onDestinationSelected: onSelectedIndexChanged,
|
||||||
extended: extended.value,
|
extended: extended.value,
|
||||||
|
@ -1,10 +1,13 @@
|
|||||||
|
import 'package:badges/badges.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||||
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:spotube/components/Home/Sidebar.dart';
|
import 'package:spotube/components/Home/Sidebar.dart';
|
||||||
import 'package:spotube/hooks/useBreakpoints.dart';
|
import 'package:spotube/hooks/useBreakpoints.dart';
|
||||||
import 'package:spotube/models/sideBarTiles.dart';
|
import 'package:spotube/models/sideBarTiles.dart';
|
||||||
|
import 'package:spotube/provider/Downloader.dart';
|
||||||
|
|
||||||
class SpotubeNavigationBar extends HookWidget {
|
class SpotubeNavigationBar extends HookConsumerWidget {
|
||||||
final int selectedIndex;
|
final int selectedIndex;
|
||||||
final void Function(int) onSelectedIndexChanged;
|
final void Function(int) onSelectedIndexChanged;
|
||||||
|
|
||||||
@ -15,14 +18,36 @@ class SpotubeNavigationBar extends HookWidget {
|
|||||||
}) : super(key: key);
|
}) : super(key: key);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context, ref) {
|
||||||
|
final downloadCount = ref.watch(
|
||||||
|
downloaderProvider.select((s) => s.currentlyRunning),
|
||||||
|
);
|
||||||
final breakpoint = useBreakpoints();
|
final breakpoint = useBreakpoints();
|
||||||
|
|
||||||
if (breakpoint.isMoreThan(Breakpoints.sm)) return Container();
|
if (breakpoint.isMoreThan(Breakpoints.sm)) return Container();
|
||||||
return NavigationBar(
|
return NavigationBar(
|
||||||
destinations: [
|
destinations: [
|
||||||
...sidebarTileList.map(
|
...sidebarTileList.map(
|
||||||
(e) => NavigationDestination(icon: Icon(e.icon), label: e.title),
|
(e) {
|
||||||
|
final icon = Icon(e.icon);
|
||||||
|
return NavigationDestination(
|
||||||
|
icon: e.title == "Library" && downloadCount > 0
|
||||||
|
? Badge(
|
||||||
|
badgeColor: Colors.red[100]!,
|
||||||
|
badgeContent: Text(
|
||||||
|
downloadCount.toString(),
|
||||||
|
style: const TextStyle(
|
||||||
|
color: Colors.red,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
animationType: BadgeAnimationType.fade,
|
||||||
|
child: icon,
|
||||||
|
)
|
||||||
|
: icon,
|
||||||
|
label: e.title,
|
||||||
|
);
|
||||||
|
},
|
||||||
),
|
),
|
||||||
const NavigationDestination(
|
const NavigationDestination(
|
||||||
icon: Icon(Icons.settings_rounded),
|
icon: Icon(Icons.settings_rounded),
|
||||||
|
83
lib/components/Library/UserDownloads.dart
Normal file
83
lib/components/Library/UserDownloads.dart
Normal file
@ -0,0 +1,83 @@
|
|||||||
|
import 'package:auto_size_text/auto_size_text.dart';
|
||||||
|
import 'package:cached_network_image/cached_network_image.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
|
import 'package:spotify/spotify.dart';
|
||||||
|
import 'package:spotube/provider/Downloader.dart';
|
||||||
|
import 'package:spotube/utils/type_conversion_utils.dart';
|
||||||
|
|
||||||
|
class UserDownloads extends HookConsumerWidget {
|
||||||
|
const UserDownloads({Key? key}) : super(key: key);
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context, ref) {
|
||||||
|
final downloader = ref.watch(downloaderProvider);
|
||||||
|
|
||||||
|
final inQueue = downloader.inQueue.toList();
|
||||||
|
return Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 15),
|
||||||
|
child: Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
|
children: [
|
||||||
|
Expanded(
|
||||||
|
child: AutoSizeText(
|
||||||
|
"Currently downloading (${downloader.currentlyRunning})",
|
||||||
|
maxLines: 1,
|
||||||
|
style: Theme.of(context).textTheme.headline5,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 10),
|
||||||
|
ElevatedButton(
|
||||||
|
style: ElevatedButton.styleFrom(
|
||||||
|
primary: Colors.red[50],
|
||||||
|
onPrimary: Colors.red[400],
|
||||||
|
),
|
||||||
|
child: const Text("Cancel All"),
|
||||||
|
onPressed: downloader.currentlyRunning > 0
|
||||||
|
? downloader.cancelAll
|
||||||
|
: null,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
ListView.builder(
|
||||||
|
itemCount: inQueue.length,
|
||||||
|
shrinkWrap: true,
|
||||||
|
itemBuilder: (context, index) {
|
||||||
|
final track = inQueue[index];
|
||||||
|
return ListTile(
|
||||||
|
title: Text(track.name!),
|
||||||
|
leading: Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 5),
|
||||||
|
child: ClipRRect(
|
||||||
|
borderRadius: BorderRadius.circular(10),
|
||||||
|
child: CachedNetworkImage(
|
||||||
|
height: 40,
|
||||||
|
width: 40,
|
||||||
|
imageUrl: TypeConversionUtils.image_X_UrlString(
|
||||||
|
track.album?.images,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
trailing: const SizedBox(
|
||||||
|
width: 30,
|
||||||
|
height: 30,
|
||||||
|
child: CircularProgressIndicator.adaptive(),
|
||||||
|
),
|
||||||
|
horizontalTitleGap: 5,
|
||||||
|
subtitle: Text(
|
||||||
|
TypeConversionUtils.artists_X_String<Artist>(
|
||||||
|
track.artists ?? [],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -2,6 +2,7 @@ import 'package:flutter/material.dart' hide Image;
|
|||||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
import 'package:spotube/components/Library/UserAlbums.dart';
|
import 'package:spotube/components/Library/UserAlbums.dart';
|
||||||
import 'package:spotube/components/Library/UserArtists.dart';
|
import 'package:spotube/components/Library/UserArtists.dart';
|
||||||
|
import 'package:spotube/components/Library/UserDownloads.dart';
|
||||||
import 'package:spotube/components/Library/UserPlaylists.dart';
|
import 'package:spotube/components/Library/UserPlaylists.dart';
|
||||||
import 'package:spotube/components/Shared/AnonymousFallback.dart';
|
import 'package:spotube/components/Shared/AnonymousFallback.dart';
|
||||||
import 'package:spotube/provider/Auth.dart';
|
import 'package:spotube/provider/Auth.dart';
|
||||||
@ -14,7 +15,7 @@ class UserLibrary extends ConsumerWidget {
|
|||||||
|
|
||||||
return Expanded(
|
return Expanded(
|
||||||
child: DefaultTabController(
|
child: DefaultTabController(
|
||||||
length: 3,
|
length: 4,
|
||||||
child: SafeArea(
|
child: SafeArea(
|
||||||
child: Scaffold(
|
child: Scaffold(
|
||||||
appBar: TabBar(
|
appBar: TabBar(
|
||||||
@ -26,6 +27,7 @@ class UserLibrary extends ConsumerWidget {
|
|||||||
Tab(text: "Playlist"),
|
Tab(text: "Playlist"),
|
||||||
Tab(text: "Artists"),
|
Tab(text: "Artists"),
|
||||||
Tab(text: "Album"),
|
Tab(text: "Album"),
|
||||||
|
Tab(text: "Downloads"),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
body: auth.isLoggedIn
|
body: auth.isLoggedIn
|
||||||
@ -33,6 +35,7 @@ class UserLibrary extends ConsumerWidget {
|
|||||||
const UserPlaylists(),
|
const UserPlaylists(),
|
||||||
UserArtists(),
|
UserArtists(),
|
||||||
const UserAlbums(),
|
const UserAlbums(),
|
||||||
|
const UserDownloads(),
|
||||||
])
|
])
|
||||||
: const AnonymousFallback(),
|
: const AnonymousFallback(),
|
||||||
),
|
),
|
||||||
|
@ -106,7 +106,7 @@ class PlaybuttonCard extends StatelessWidget {
|
|||||||
text: title,
|
text: title,
|
||||||
style:
|
style:
|
||||||
const TextStyle(fontWeight: FontWeight.bold),
|
const TextStyle(fontWeight: FontWeight.bold),
|
||||||
minStartLength: 25,
|
minStartLength: 20,
|
||||||
isHovering: isHovering,
|
isHovering: isHovering,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
|
import 'package:auto_size_text/auto_size_text.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||||
import 'package:marquee/marquee.dart';
|
import 'package:marquee/marquee.dart';
|
||||||
import 'package:spotube/utils/platform.dart';
|
|
||||||
|
|
||||||
class SpotubeMarqueeText extends HookWidget {
|
class SpotubeMarqueeText extends HookWidget {
|
||||||
final int? minStartLength;
|
final int? minStartLength;
|
||||||
@ -18,46 +18,32 @@ class SpotubeMarqueeText extends HookWidget {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final hovering = useState(false);
|
final uKey = useState(UniqueKey());
|
||||||
final isInitial = useState(true);
|
|
||||||
|
|
||||||
useEffect(() {
|
useEffect(() {
|
||||||
if (isHovering != null && isHovering != hovering.value) {
|
uKey.value = UniqueKey();
|
||||||
hovering.value = isHovering!;
|
return;
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}, [isHovering]);
|
}, [isHovering]);
|
||||||
|
|
||||||
if ((!isInitial.value && !hovering.value && kIsDesktop) ||
|
return AutoSizeText(
|
||||||
minStartLength != null && text.length <= minStartLength!) {
|
text,
|
||||||
return Text(
|
minFontSize: 13,
|
||||||
text,
|
|
||||||
style: style,
|
|
||||||
overflow: TextOverflow.ellipsis,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return Marquee(
|
|
||||||
text: text,
|
|
||||||
style: style,
|
style: style,
|
||||||
scrollAxis: Axis.horizontal,
|
overflowReplacement: Marquee(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
key: uKey.value,
|
||||||
blankSpace: 40.0,
|
text: text,
|
||||||
velocity: 30.0,
|
style: style,
|
||||||
accelerationDuration: const Duration(seconds: 1),
|
scrollAxis: Axis.horizontal,
|
||||||
accelerationCurve: Curves.linear,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
decelerationDuration: const Duration(milliseconds: 500),
|
blankSpace: 40.0,
|
||||||
decelerationCurve: Curves.easeOut,
|
velocity: 30.0,
|
||||||
fadingEdgeStartFraction: 0.15,
|
accelerationDuration: const Duration(seconds: 1),
|
||||||
fadingEdgeEndFraction: 0.15,
|
accelerationCurve: Curves.linear,
|
||||||
showFadingOnlyWhenScrolling: true,
|
decelerationDuration: const Duration(milliseconds: 500),
|
||||||
onDone: () {
|
decelerationCurve: Curves.easeOut,
|
||||||
if (isInitial.value) {
|
showFadingOnlyWhenScrolling: true,
|
||||||
isInitial.value = false;
|
numberOfRounds: isHovering == true ? null : 1,
|
||||||
hovering.value = false;
|
),
|
||||||
}
|
|
||||||
},
|
|
||||||
numberOfRounds: hovering.value ? null : 1,
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -193,15 +193,15 @@ class TrackTile extends HookConsumerWidget {
|
|||||||
Checkbox(
|
Checkbox(
|
||||||
value: isChecked,
|
value: isChecked,
|
||||||
onChanged: (s) => onCheckChange?.call(s),
|
onChanged: (s) => onCheckChange?.call(s),
|
||||||
|
)
|
||||||
|
else
|
||||||
|
SizedBox(
|
||||||
|
height: 20,
|
||||||
|
width: 25,
|
||||||
|
child: Center(
|
||||||
|
child: Text((track.key + 1).toString()),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
SizedBox(
|
|
||||||
height: 20,
|
|
||||||
width: 15,
|
|
||||||
child: Text(
|
|
||||||
(track.key + 1).toString(),
|
|
||||||
textAlign: TextAlign.center,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
if (thumbnailUrl != null)
|
if (thumbnailUrl != null)
|
||||||
Padding(
|
Padding(
|
||||||
padding: EdgeInsets.symmetric(
|
padding: EdgeInsets.symmetric(
|
||||||
|
@ -132,23 +132,11 @@ class TracksTableView extends HookConsumerWidget {
|
|||||||
return const DownloadConfirmationDialog();
|
return const DownloadConfirmationDialog();
|
||||||
});
|
});
|
||||||
if (isConfirmed != true) return;
|
if (isConfirmed != true) return;
|
||||||
final queue = Queue(
|
|
||||||
delay: const Duration(seconds: 5),
|
|
||||||
);
|
|
||||||
for (final selectedTrack in selectedTracks) {
|
for (final selectedTrack in selectedTracks) {
|
||||||
queue.add(() async {
|
downloader.addToQueue(selectedTrack);
|
||||||
downloader.addToQueue(
|
|
||||||
await playback.toSpotubeTrack(
|
|
||||||
selectedTrack,
|
|
||||||
noSponsorBlock: true,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
selected.value = [];
|
selected.value = [];
|
||||||
showCheck.value = false;
|
showCheck.value = false;
|
||||||
await queue.onComplete;
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
default:
|
default:
|
||||||
@ -171,7 +159,15 @@ class TracksTableView extends HookConsumerWidget {
|
|||||||
},
|
},
|
||||||
onTap: () {
|
onTap: () {
|
||||||
if (showCheck.value) {
|
if (showCheck.value) {
|
||||||
selected.value = [...selected.value, track.value.id!];
|
final alreadyChecked =
|
||||||
|
selected.value.contains(track.value.id);
|
||||||
|
if (alreadyChecked) {
|
||||||
|
selected.value = selected.value
|
||||||
|
.where((id) => id != track.value.id)
|
||||||
|
.toList();
|
||||||
|
} else {
|
||||||
|
selected.value = [...selected.value, track.value.id!];
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
onTrackPlayButtonPressed?.call(track.value);
|
onTrackPlayButtonPressed?.call(track.value);
|
||||||
}
|
}
|
||||||
|
110
lib/main.dart
110
lib/main.dart
@ -10,11 +10,13 @@ import 'package:go_router/go_router.dart';
|
|||||||
import 'package:hive_flutter/hive_flutter.dart';
|
import 'package:hive_flutter/hive_flutter.dart';
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:shared_preferences/shared_preferences.dart';
|
import 'package:shared_preferences/shared_preferences.dart';
|
||||||
|
import 'package:spotube/components/Shared/DownloadTrackButton.dart';
|
||||||
import 'package:spotube/entities/CacheTrack.dart';
|
import 'package:spotube/entities/CacheTrack.dart';
|
||||||
import 'package:spotube/models/GoRouteDeclarations.dart';
|
import 'package:spotube/models/GoRouteDeclarations.dart';
|
||||||
import 'package:spotube/models/LocalStorageKeys.dart';
|
import 'package:spotube/models/LocalStorageKeys.dart';
|
||||||
import 'package:spotube/models/Logger.dart';
|
import 'package:spotube/models/Logger.dart';
|
||||||
import 'package:spotube/provider/AudioPlayer.dart';
|
import 'package:spotube/provider/AudioPlayer.dart';
|
||||||
|
import 'package:spotube/provider/Downloader.dart';
|
||||||
import 'package:spotube/provider/Playback.dart';
|
import 'package:spotube/provider/Playback.dart';
|
||||||
import 'package:spotube/provider/UserPreferences.dart';
|
import 'package:spotube/provider/UserPreferences.dart';
|
||||||
import 'package:spotube/provider/YouTube.dart';
|
import 'package:spotube/provider/YouTube.dart';
|
||||||
@ -53,41 +55,85 @@ void main() async {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
MobileAudioService? audioServiceHandler;
|
MobileAudioService? audioServiceHandler;
|
||||||
runApp(ProviderScope(
|
runApp(
|
||||||
child: const Spotube(),
|
Builder(
|
||||||
overrides: [
|
builder: (context) {
|
||||||
playbackProvider.overrideWithProvider(ChangeNotifierProvider(
|
return ProviderScope(
|
||||||
(ref) {
|
child: const Spotube(),
|
||||||
final youtube = ref.watch(youtubeProvider);
|
overrides: [
|
||||||
final player = ref.watch(audioPlayerProvider);
|
playbackProvider.overrideWithProvider(
|
||||||
|
ChangeNotifierProvider(
|
||||||
|
(ref) {
|
||||||
|
final youtube = ref.watch(youtubeProvider);
|
||||||
|
final player = ref.watch(audioPlayerProvider);
|
||||||
|
|
||||||
final playback = Playback(
|
final playback = Playback(
|
||||||
player: player,
|
player: player,
|
||||||
youtube: youtube,
|
youtube: youtube,
|
||||||
ref: ref,
|
ref: ref,
|
||||||
);
|
);
|
||||||
|
|
||||||
if (audioServiceHandler == null) {
|
if (audioServiceHandler == null) {
|
||||||
AudioService.init(
|
AudioService.init(
|
||||||
builder: () => MobileAudioService(playback),
|
builder: () => MobileAudioService(playback),
|
||||||
config: const AudioServiceConfig(
|
config: const AudioServiceConfig(
|
||||||
androidNotificationChannelId: 'com.krtirtho.Spotube',
|
androidNotificationChannelId: 'com.krtirtho.Spotube',
|
||||||
androidNotificationChannelName: 'Spotube',
|
androidNotificationChannelName: 'Spotube',
|
||||||
androidNotificationOngoing: true,
|
androidNotificationOngoing: true,
|
||||||
|
),
|
||||||
|
).then(
|
||||||
|
(value) {
|
||||||
|
playback.mobileAudioService = value;
|
||||||
|
audioServiceHandler = value;
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return playback;
|
||||||
|
},
|
||||||
),
|
),
|
||||||
).then(
|
),
|
||||||
(value) {
|
downloaderProvider.overrideWithProvider(
|
||||||
playback.mobileAudioService = value;
|
ChangeNotifierProvider(
|
||||||
audioServiceHandler = value;
|
(ref) {
|
||||||
},
|
return Downloader(
|
||||||
);
|
ref,
|
||||||
}
|
queueInstance,
|
||||||
|
yt: ref.watch(youtubeProvider),
|
||||||
return playback;
|
downloadPath: ref.watch(
|
||||||
},
|
userPreferencesProvider.select(
|
||||||
))
|
(s) => s.downloadLocation,
|
||||||
],
|
),
|
||||||
));
|
),
|
||||||
|
onFileExists: (track) {
|
||||||
|
final logger = getLogger(Downloader);
|
||||||
|
try {
|
||||||
|
logger.v(
|
||||||
|
"[onFileExists] download confirmation for ${track.name}",
|
||||||
|
);
|
||||||
|
return showDialog<bool>(
|
||||||
|
context: context,
|
||||||
|
builder: (_) =>
|
||||||
|
ReplaceDownloadedFileDialog(track: track),
|
||||||
|
).then((s) => s ?? false);
|
||||||
|
} catch (e, stack) {
|
||||||
|
logger.e(
|
||||||
|
"onFileExists",
|
||||||
|
e,
|
||||||
|
stack,
|
||||||
|
);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
)
|
||||||
|
],
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
class Spotube extends StatefulHookConsumerWidget {
|
class Spotube extends StatefulHookConsumerWidget {
|
||||||
|
@ -6,20 +6,26 @@ import 'package:flutter_downloader/flutter_downloader.dart';
|
|||||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
import 'package:queue/queue.dart';
|
import 'package:queue/queue.dart';
|
||||||
import 'package:path/path.dart' as path;
|
import 'package:path/path.dart' as path;
|
||||||
|
import 'package:spotify/spotify.dart';
|
||||||
|
import 'package:spotube/models/Logger.dart';
|
||||||
import 'package:spotube/models/SpotubeTrack.dart';
|
import 'package:spotube/models/SpotubeTrack.dart';
|
||||||
|
import 'package:spotube/provider/Playback.dart';
|
||||||
import 'package:spotube/provider/UserPreferences.dart';
|
import 'package:spotube/provider/UserPreferences.dart';
|
||||||
import 'package:spotube/provider/YouTube.dart';
|
import 'package:spotube/provider/YouTube.dart';
|
||||||
import 'package:spotube/utils/platform.dart';
|
import 'package:spotube/utils/platform.dart';
|
||||||
import 'package:youtube_explode_dart/youtube_explode_dart.dart';
|
import 'package:youtube_explode_dart/youtube_explode_dart.dart';
|
||||||
|
|
||||||
Queue _queueInstance = Queue(delay: const Duration(seconds: 5));
|
Queue queueInstance = Queue(delay: const Duration(seconds: 5));
|
||||||
|
Queue grabberQueue = Queue(delay: const Duration(seconds: 5));
|
||||||
|
|
||||||
class Downloader with ChangeNotifier {
|
class Downloader with ChangeNotifier {
|
||||||
|
Ref ref;
|
||||||
Queue _queue;
|
Queue _queue;
|
||||||
YoutubeExplode yt;
|
YoutubeExplode yt;
|
||||||
String downloadPath;
|
String downloadPath;
|
||||||
FutureOr<bool> Function(SpotubeTrack track)? onFileExists;
|
FutureOr<bool> Function(SpotubeTrack track)? onFileExists;
|
||||||
Downloader(
|
Downloader(
|
||||||
|
this.ref,
|
||||||
this._queue, {
|
this._queue, {
|
||||||
required this.downloadPath,
|
required this.downloadPath,
|
||||||
required this.yt,
|
required this.yt,
|
||||||
@ -27,73 +33,115 @@ class Downloader with ChangeNotifier {
|
|||||||
});
|
});
|
||||||
|
|
||||||
int currentlyRunning = 0;
|
int currentlyRunning = 0;
|
||||||
Set<String> inQueue = {};
|
// ignore: prefer_collection_literals
|
||||||
|
Set<Track> inQueue = Set();
|
||||||
|
|
||||||
void addToQueue(SpotubeTrack track) async {
|
final logger = getLogger(Downloader);
|
||||||
|
|
||||||
|
void addToQueue(Track baseTrack) async {
|
||||||
|
if (inQueue.any((t) => t.id == baseTrack.id!)) return;
|
||||||
|
inQueue.add(baseTrack);
|
||||||
currentlyRunning++;
|
currentlyRunning++;
|
||||||
inQueue.add(track.id!);
|
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
final filename = '${track.ytTrack.title}.mp3';
|
|
||||||
if (kIsMobile) {
|
if (kIsMobile) {
|
||||||
final url =
|
grabberQueue.add(() async {
|
||||||
((await yt.videos.streamsClient.getManifest(track.ytTrack.url)))
|
final track = await ref.read(playbackProvider).toSpotubeTrack(
|
||||||
.audioOnly
|
baseTrack,
|
||||||
.where((audio) => audio.codec.mimeType == "audio/mp4")
|
noSponsorBlock: true,
|
||||||
.withHighestBitrate()
|
);
|
||||||
.url;
|
|
||||||
await FlutterDownloader.enqueue(
|
|
||||||
savedDir: downloadPath,
|
|
||||||
url: url.toString(),
|
|
||||||
fileName: filename,
|
|
||||||
openFileFromNotification: true,
|
|
||||||
showNotification: true,
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
if (inQueue.contains(track.id!)) return;
|
|
||||||
_queue.add(() async {
|
|
||||||
try {
|
|
||||||
final file = File(path.join(downloadPath, filename));
|
|
||||||
if (file.existsSync() && await onFileExists?.call(track) != true) {
|
|
||||||
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();
|
final filename = '${track.ytTrack.title}.mp3';
|
||||||
await audioStream.pipe(outputFileStream);
|
|
||||||
await outputFileStream.flush();
|
final url =
|
||||||
} finally {
|
((await yt.videos.streamsClient.getManifest(track.ytTrack.url)))
|
||||||
currentlyRunning--;
|
.audioOnly
|
||||||
inQueue.remove(track.id);
|
.where((audio) => audio.codec.mimeType == "audio/mp4")
|
||||||
notifyListeners();
|
.withHighestBitrate()
|
||||||
}
|
.url;
|
||||||
|
await FlutterDownloader.enqueue(
|
||||||
|
savedDir: downloadPath,
|
||||||
|
url: url.toString(),
|
||||||
|
fileName: filename,
|
||||||
|
openFileFromNotification: true,
|
||||||
|
showNotification: true,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
grabberQueue.add(() async {
|
||||||
|
final track = await ref.read(playbackProvider).toSpotubeTrack(
|
||||||
|
baseTrack,
|
||||||
|
noSponsorBlock: true,
|
||||||
|
);
|
||||||
|
_queue.add(() async {
|
||||||
|
final filename = '${track.ytTrack.title}.mp3';
|
||||||
|
final file = File(path.join(downloadPath, filename));
|
||||||
|
try {
|
||||||
|
logger.v("[addToQueue] Download starting for ${file.path}");
|
||||||
|
if (file.existsSync() && await onFileExists?.call(track) != true) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
file.createSync(recursive: true);
|
||||||
|
StreamManifest manifest =
|
||||||
|
await yt.videos.streamsClient.getManifest(track.ytTrack.url);
|
||||||
|
logger.v(
|
||||||
|
"[addToQueue] Getting download information for ${file.path}",
|
||||||
|
);
|
||||||
|
final audioStream = yt.videos.streamsClient
|
||||||
|
.get(
|
||||||
|
manifest.audioOnly
|
||||||
|
.where((audio) => audio.codec.mimeType == "audio/mp4")
|
||||||
|
.withHighestBitrate(),
|
||||||
|
)
|
||||||
|
.asBroadcastStream();
|
||||||
|
|
||||||
|
logger.v(
|
||||||
|
"[addToQueue] ${file.path} download started",
|
||||||
|
);
|
||||||
|
|
||||||
|
IOSink outputFileStream = file.openWrite();
|
||||||
|
await audioStream.pipe(outputFileStream);
|
||||||
|
await outputFileStream.flush();
|
||||||
|
logger.v(
|
||||||
|
"[addToQueue] Download of ${file.path} is done successfully",
|
||||||
|
);
|
||||||
|
} catch (e, stack) {
|
||||||
|
logger.e(
|
||||||
|
"[addToQueue] Failed download of ${file.path}",
|
||||||
|
e,
|
||||||
|
stack,
|
||||||
|
);
|
||||||
|
rethrow;
|
||||||
|
} finally {
|
||||||
|
currentlyRunning--;
|
||||||
|
inQueue.removeWhere((t) => t.id == track.id);
|
||||||
|
notifyListeners();
|
||||||
|
}
|
||||||
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
cancelAll() {
|
cancelAll() {
|
||||||
|
grabberQueue.cancel();
|
||||||
|
grabberQueue = Queue();
|
||||||
|
inQueue.clear();
|
||||||
|
currentlyRunning = 0;
|
||||||
if (kIsMobile) {
|
if (kIsMobile) {
|
||||||
FlutterDownloader.cancelAll();
|
FlutterDownloader.cancelAll();
|
||||||
} else {
|
} else {
|
||||||
_queue.cancel();
|
_queue.cancel();
|
||||||
_queueInstance = Queue();
|
queueInstance = Queue();
|
||||||
_queue = _queueInstance;
|
_queue = queueInstance;
|
||||||
}
|
}
|
||||||
|
notifyListeners();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
final downloaderProvider = ChangeNotifierProvider(
|
final downloaderProvider = ChangeNotifierProvider(
|
||||||
(ref) {
|
(ref) {
|
||||||
return Downloader(
|
return Downloader(
|
||||||
_queueInstance,
|
ref,
|
||||||
|
queueInstance,
|
||||||
yt: ref.watch(youtubeProvider),
|
yt: ref.watch(youtubeProvider),
|
||||||
downloadPath: ref.watch(
|
downloadPath: ref.watch(
|
||||||
userPreferencesProvider.select(
|
userPreferencesProvider.select(
|
||||||
|
14
pubspec.lock
14
pubspec.lock
@ -218,6 +218,20 @@ packages:
|
|||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.0.0"
|
version: "1.0.0"
|
||||||
|
auto_size_text:
|
||||||
|
dependency: "direct main"
|
||||||
|
description:
|
||||||
|
name: auto_size_text
|
||||||
|
url: "https://pub.dartlang.org"
|
||||||
|
source: hosted
|
||||||
|
version: "3.0.0"
|
||||||
|
badges:
|
||||||
|
dependency: "direct main"
|
||||||
|
description:
|
||||||
|
name: badges
|
||||||
|
url: "https://pub.dartlang.org"
|
||||||
|
source: hosted
|
||||||
|
version: "2.0.3"
|
||||||
bitsdojo_window:
|
bitsdojo_window:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
|
@ -69,6 +69,8 @@ dependencies:
|
|||||||
popover: ^0.2.6+3
|
popover: ^0.2.6+3
|
||||||
queue: ^3.1.0+1
|
queue: ^3.1.0+1
|
||||||
flutter_downloader: ^1.8.1
|
flutter_downloader: ^1.8.1
|
||||||
|
auto_size_text: ^3.0.0
|
||||||
|
badges: ^2.0.3
|
||||||
|
|
||||||
dev_dependencies:
|
dev_dependencies:
|
||||||
flutter_test:
|
flutter_test:
|
||||||
|
Loading…
Reference in New Issue
Block a user