Merge branch 'master' into build

This commit is contained in:
Kingkor Roy Tirtho 2022-06-02 19:00:49 +06:00
commit 640d227ea5
22 changed files with 351 additions and 147 deletions

View File

@ -145,7 +145,6 @@ Bu why? You can learn about it [here](https://dev.to/krtirtho/choosing-open-sour
- [hooks_riverpod](https://riverpod.dev/) - Riverpod with hooks - [hooks_riverpod](https://riverpod.dev/) - Riverpod with hooks
- [go_router](https://github.com/flutter/packages/tree/main/packages/go_router) - A declarative router for Flutter based on Navigation 2 supporting deep linking, data-driven routes and more - [go_router](https://github.com/flutter/packages/tree/main/packages/go_router) - A declarative router for Flutter based on Navigation 2 supporting deep linking, data-driven routes and more
- [palette_generator](https://github.com/flutter/packages/tree/main/packages/palette_generator) - Flutter package for generating palette colors from a source image. - [palette_generator](https://github.com/flutter/packages/tree/main/packages/palette_generator) - Flutter package for generating palette colors from a source image.
- [audio_session](https://github.com/ryanheise/audio_session) - Sets the iOS audio session category and Android audio attributes for your app, and manages your app's audio focus, mixing and ducking behaviour.
- [logger](https://github.com/leisim/logger) - Small, easy to use and extensible logger which prints beautiful logs - [logger](https://github.com/leisim/logger) - Small, easy to use and extensible logger which prints beautiful logs
- [flutter_launcher_icons](https://github.com/fluttercommunity/flutter_launcher_icons) - A package which simplifies the task of updating your Flutter app's launcher icon. - [flutter_launcher_icons](https://github.com/fluttercommunity/flutter_launcher_icons) - A package which simplifies the task of updating your Flutter app's launcher icon.
- [permission_handler](https://github.com/baseflow/flutter-permission-handler) - Permission plugin for Flutter. This plugin provides a cross-platform (iOS, Android) API to request and check permissions. - [permission_handler](https://github.com/baseflow/flutter-permission-handler) - Permission plugin for Flutter. This plugin provides a cross-platform (iOS, Android) API to request and check permissions.

View File

@ -7,6 +7,7 @@ import 'package:spotube/helpers/artist-to-string.dart';
import 'package:spotube/helpers/image-to-url-string.dart'; import 'package:spotube/helpers/image-to-url-string.dart';
import 'package:spotube/helpers/simple-track-to-track.dart'; import 'package:spotube/helpers/simple-track-to-track.dart';
import 'package:spotube/hooks/useBreakpointValue.dart'; import 'package:spotube/hooks/useBreakpointValue.dart';
import 'package:spotube/models/CurrentPlaylist.dart';
import 'package:spotube/provider/Playback.dart'; import 'package:spotube/provider/Playback.dart';
import 'package:spotube/provider/SpotifyDI.dart'; import 'package:spotube/provider/SpotifyDI.dart';

View File

@ -7,6 +7,7 @@ import 'package:spotube/components/Shared/TracksTableView.dart';
import 'package:spotube/helpers/image-to-url-string.dart'; import 'package:spotube/helpers/image-to-url-string.dart';
import 'package:spotube/helpers/simple-track-to-track.dart'; import 'package:spotube/helpers/simple-track-to-track.dart';
import 'package:spotube/hooks/useForceUpdate.dart'; import 'package:spotube/hooks/useForceUpdate.dart';
import 'package:spotube/models/CurrentPlaylist.dart';
import 'package:spotube/provider/Auth.dart'; import 'package:spotube/provider/Auth.dart';
import 'package:spotube/provider/Playback.dart'; import 'package:spotube/provider/Playback.dart';
import 'package:spotube/provider/SpotifyDI.dart'; import 'package:spotube/provider/SpotifyDI.dart';

View File

@ -15,6 +15,7 @@ import 'package:spotube/helpers/zero-pad-num-str.dart';
import 'package:spotube/hooks/useBreakpointValue.dart'; import 'package:spotube/hooks/useBreakpointValue.dart';
import 'package:spotube/hooks/useBreakpoints.dart'; import 'package:spotube/hooks/useBreakpoints.dart';
import 'package:spotube/hooks/useForceUpdate.dart'; import 'package:spotube/hooks/useForceUpdate.dart';
import 'package:spotube/models/CurrentPlaylist.dart';
import 'package:spotube/models/Logger.dart'; import 'package:spotube/models/Logger.dart';
import 'package:spotube/provider/Playback.dart'; import 'package:spotube/provider/Playback.dart';
import 'package:spotube/provider/SpotifyDI.dart'; import 'package:spotube/provider/SpotifyDI.dart';

View File

@ -19,12 +19,14 @@ import 'package:spotube/components/Library/UserLibrary.dart';
import 'package:spotube/hooks/useBreakpointValue.dart'; import 'package:spotube/hooks/useBreakpointValue.dart';
import 'package:spotube/hooks/useHotKeys.dart'; import 'package:spotube/hooks/useHotKeys.dart';
import 'package:spotube/hooks/usePaginatedFutureProvider.dart'; import 'package:spotube/hooks/usePaginatedFutureProvider.dart';
import 'package:spotube/hooks/useUpdateChecker.dart';
import 'package:spotube/models/Logger.dart'; import 'package:spotube/models/Logger.dart';
import 'package:spotube/provider/SpotifyRequests.dart'; import 'package:spotube/provider/SpotifyRequests.dart';
List<String> spotifyScopes = [ List<String> spotifyScopes = [
"playlist-modify-public", "playlist-modify-public",
"playlist-modify-private", "playlist-modify-private",
"playlist-read-private",
"user-library-read", "user-library-read",
"user-library-modify", "user-library-modify",
"user-read-private", "user-read-private",
@ -52,6 +54,8 @@ class Home extends HookConsumerWidget {
// initializing global hot keys // initializing global hot keys
useHotKeys(ref); useHotKeys(ref);
// checks for latest version of the application
useUpdateChecker(ref);
final titleBarContents = Container( final titleBarContents = Container(
color: Theme.of(context).scaffoldBackgroundColor, color: Theme.of(context).scaffoldBackgroundColor,

View File

@ -1,9 +1,7 @@
import 'dart:async'; import 'dart:async';
import 'dart:io';
import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:just_audio/just_audio.dart';
import 'package:shared_preferences/shared_preferences.dart'; import 'package:shared_preferences/shared_preferences.dart';
import 'package:spotube/components/Player/PlayerActions.dart'; import 'package:spotube/components/Player/PlayerActions.dart';
import 'package:spotube/components/Player/PlayerOverlay.dart'; import 'package:spotube/components/Player/PlayerOverlay.dart';
@ -15,6 +13,7 @@ import 'package:spotube/models/LocalStorageKeys.dart';
import 'package:spotube/models/Logger.dart'; import 'package:spotube/models/Logger.dart';
import 'package:spotube/provider/Playback.dart'; import 'package:spotube/provider/Playback.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:spotube/utils/AudioPlayerHandler.dart';
class Player extends HookConsumerWidget { class Player extends HookConsumerWidget {
Player({Key? key}) : super(key: key); Player({Key? key}) : super(key: key);
@ -28,7 +27,7 @@ class Player extends HookConsumerWidget {
final breakpoint = useBreakpoints(); final breakpoint = useBreakpoints();
final AudioPlayer player = playback.player; final AudioPlayerHandler player = playback.player;
final Future<SharedPreferences> future = final Future<SharedPreferences> future =
useMemoized(SharedPreferences.getInstance); useMemoized(SharedPreferences.getInstance);
@ -36,33 +35,19 @@ class Player extends HookConsumerWidget {
useFuture(future, initialData: null); useFuture(future, initialData: null);
useEffect(() { useEffect(() {
// registering all the stream subscription listeners of player
playback.register();
/// warm up the audio player before playing actual audio /// warm up the audio player before playing actual audio
/// It's for resolving unresolved issue related to just_audio's /// It's for resolving unresolved issue related to just_audio's
/// [disposeAllPlayers] method which is throwing /// [disposeAllPlayers] method which is throwing
/// [UnimplementedException] in the [PlatformInterface] /// [UnimplementedException] in the [PlatformInterface]
/// implementation /// implementation
if (Platform.isAndroid || Platform.isIOS) { player.core.setAsset("assets/warmer.mp3");
playback.audioSession return null;
?.setActive(true)
.then((_) => player.setAsset("assets/warmer.mp3"))
.catchError((e) {
logger.e("useEffect", e, StackTrace.current);
});
} else {
player.setAsset("assets/warmer.mp3");
}
return () {
playback.dispose();
};
}, []); }, []);
useEffect(() { useEffect(() {
if (localStorage.hasData) { if (localStorage.hasData) {
_volume.value = localStorage.data?.getDouble(LocalStorageKeys.volume) ?? _volume.value = localStorage.data?.getDouble(LocalStorageKeys.volume) ??
player.volume; player.core.volume;
} }
return null; return null;
}, [localStorage.data]); }, [localStorage.data]);
@ -154,7 +139,7 @@ class Player extends HookConsumerWidget {
value: _volume.value, value: _volume.value,
onChanged: (value) async { onChanged: (value) async {
try { try {
await player.setVolume(value).then((_) { await player.core.setVolume(value).then((_) {
_volume.value = value; _volume.value = value;
localStorage.data?.setDouble( localStorage.data?.setDouble(
LocalStorageKeys.volume, LocalStorageKeys.volume,

View File

@ -1,10 +1,10 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:just_audio/just_audio.dart';
import 'package:spotube/helpers/zero-pad-num-str.dart'; import 'package:spotube/helpers/zero-pad-num-str.dart';
import 'package:spotube/hooks/playback.dart'; import 'package:spotube/hooks/playback.dart';
import 'package:spotube/models/Logger.dart'; import 'package:spotube/models/Logger.dart';
import 'package:spotube/provider/Playback.dart'; import 'package:spotube/provider/Playback.dart';
import 'package:spotube/utils/AudioPlayerHandler.dart';
class PlayerControls extends HookConsumerWidget { class PlayerControls extends HookConsumerWidget {
final Color? iconColor; final Color? iconColor;
@ -18,7 +18,7 @@ class PlayerControls extends HookConsumerWidget {
@override @override
Widget build(BuildContext context, ref) { Widget build(BuildContext context, ref) {
final Playback playback = ref.watch(playbackProvider); final Playback playback = ref.watch(playbackProvider);
final AudioPlayer player = playback.player; final AudioPlayerHandler player = playback.player;
final onNext = useNextTrack(playback); final onNext = useNextTrack(playback);
@ -33,9 +33,7 @@ class PlayerControls extends HookConsumerWidget {
child: Column( child: Column(
children: [ children: [
StreamBuilder<Duration>( StreamBuilder<Duration>(
stream: player.positionStream.isBroadcast stream: player.core.positionStream,
? player.positionStream
: player.positionStream.asBroadcastStream(),
builder: (context, snapshot) { builder: (context, snapshot) {
final totalMinutes = final totalMinutes =
zeroPadNumStr(duration.inMinutes.remainder(60)); zeroPadNumStr(duration.inMinutes.remainder(60));

View File

@ -5,6 +5,7 @@ import 'package:spotify/spotify.dart';
import 'package:spotube/components/Shared/PlaybuttonCard.dart'; import 'package:spotube/components/Shared/PlaybuttonCard.dart';
import 'package:spotube/helpers/image-to-url-string.dart'; import 'package:spotube/helpers/image-to-url-string.dart';
import 'package:spotube/hooks/useBreakpointValue.dart'; import 'package:spotube/hooks/useBreakpointValue.dart';
import 'package:spotube/models/CurrentPlaylist.dart';
import 'package:spotube/provider/Playback.dart'; import 'package:spotube/provider/Playback.dart';
import 'package:spotube/provider/SpotifyDI.dart'; import 'package:spotube/provider/SpotifyDI.dart';

View File

@ -2,6 +2,7 @@ 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:hooks_riverpod/hooks_riverpod.dart';
import 'package:spotube/provider/SpotifyDI.dart'; import 'package:spotube/provider/SpotifyDI.dart';
import 'package:spotube/provider/SpotifyRequests.dart';
class PlaylistCreateDialog extends HookConsumerWidget { class PlaylistCreateDialog extends HookConsumerWidget {
const PlaylistCreateDialog({Key? key}) : super(key: key); const PlaylistCreateDialog({Key? key}) : super(key: key);
@ -43,13 +44,16 @@ class PlaylistCreateDialog extends HookConsumerWidget {
final me = await spotify.me.get(); final me = await spotify.me.get();
await spotify.playlists await spotify.playlists
.createPlaylist( .createPlaylist(
me.id!, me.id!,
playlistName.text, playlistName.text,
collaborative: collaborative.value, collaborative: collaborative.value,
public: public.value, public: public.value,
description: description.text, description: description.text,
) )
.then((_) => Navigator.pop(context)); .then((_) {
ref.refresh(currentUserPlaylistsQuery);
Navigator.pop(context);
});
}, },
) )
], ],

View File

@ -5,6 +5,7 @@ import 'package:spotube/components/Shared/PageWindowTitleBar.dart';
import 'package:spotube/components/Shared/TracksTableView.dart'; import 'package:spotube/components/Shared/TracksTableView.dart';
import 'package:spotube/helpers/image-to-url-string.dart'; import 'package:spotube/helpers/image-to-url-string.dart';
import 'package:spotube/hooks/useForceUpdate.dart'; import 'package:spotube/hooks/useForceUpdate.dart';
import 'package:spotube/models/CurrentPlaylist.dart';
import 'package:spotube/models/Logger.dart'; import 'package:spotube/models/Logger.dart';
import 'package:spotube/provider/Auth.dart'; import 'package:spotube/provider/Auth.dart';
import 'package:spotube/provider/Playback.dart'; import 'package:spotube/provider/Playback.dart';

View File

@ -11,6 +11,7 @@ import 'package:spotube/helpers/image-to-url-string.dart';
import 'package:spotube/helpers/simple-album-to-album.dart'; import 'package:spotube/helpers/simple-album-to-album.dart';
import 'package:spotube/helpers/zero-pad-num-str.dart'; import 'package:spotube/helpers/zero-pad-num-str.dart';
import 'package:spotube/hooks/useBreakpoints.dart'; import 'package:spotube/hooks/useBreakpoints.dart';
import 'package:spotube/models/CurrentPlaylist.dart';
import 'package:spotube/provider/Auth.dart'; import 'package:spotube/provider/Auth.dart';
import 'package:spotube/provider/Playback.dart'; import 'package:spotube/provider/Playback.dart';
import 'package:spotube/provider/SpotifyDI.dart'; import 'package:spotube/provider/SpotifyDI.dart';

View File

@ -217,6 +217,7 @@ class Settings extends HookConsumerWidget {
children: [ children: [
const Text("Download lyrics along with the Track"), const Text("Download lyrics along with the Track"),
Switch.adaptive( Switch.adaptive(
activeColor: Theme.of(context).primaryColor,
value: preferences.saveTrackLyrics, value: preferences.saveTrackLyrics,
onChanged: (state) { onChanged: (state) {
preferences.setSaveTrackLyrics(state); preferences.setSaveTrackLyrics(state);
@ -265,6 +266,22 @@ class Settings extends HookConsumerWidget {
) )
], ],
), ),
const SizedBox(height: 10),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
const Expanded(
flex: 2,
child: Text("Check for Update)"),
),
Switch.adaptive(
activeColor: Theme.of(context).primaryColor,
value: preferences.checkUpdate,
onChanged: (checked) =>
preferences.setCheckUpdate(checked),
)
],
),
if (auth.isLoggedIn) if (auth.isLoggedIn)
Builder(builder: (context) { Builder(builder: (context) {
Auth auth = ref.watch(authProvider); Auth auth = ref.watch(authProvider);

View File

@ -1,12 +1,12 @@
import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:spotube/provider/AudioPlayer.dart'; import 'package:spotube/provider/Playback.dart';
useSyncedLyrics(WidgetRef ref, Map<int, String> lyricsMap) { useSyncedLyrics(WidgetRef ref, Map<int, String> lyricsMap) {
final player = ref.watch(audioPlayerProvider); final player = ref.watch(playbackProvider.select(
final stream = player.positionStream.isBroadcast (value) => (value.player),
? player.positionStream ));
: player.positionStream.asBroadcastStream(); final stream = player.core.positionStream;
final currentTime = useState(0); final currentTime = useState(0);

View File

@ -0,0 +1,90 @@
import 'dart:async';
import 'dart:convert';
import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:http/http.dart' as http;
import 'package:spotube/components/Shared/AnchorButton.dart';
import 'package:spotube/hooks/usePackageInfo.dart';
import 'package:spotube/provider/UserPreferences.dart';
import 'package:url_launcher/url_launcher_string.dart';
import 'package:version/version.dart';
void useUpdateChecker(WidgetRef ref) {
final isCheckUpdateEnabled =
ref.watch(userPreferencesProvider.select((s) => s.checkUpdate));
final packageInfo = usePackageInfo(
appName: 'Spotube',
packageName: 'spotube',
);
final Future<List<Version?>> Function() checkUpdate = useCallback(
() async {
final value = await http.get(
Uri.parse(
"https://api.github.com/repos/KRTirtho/spotube/releases/latest"),
);
final tagName =
(jsonDecode(value.body)["tag_name"] as String).replaceAll("v", "");
final currentVersion = packageInfo.version == "Unknown"
? null
: Version.parse(
packageInfo.version,
);
final latestVersion = Version.parse(tagName);
return [currentVersion, latestVersion];
},
[packageInfo.version],
);
final context = useContext();
download(String url) => launchUrlString(
url,
mode: LaunchMode.externalApplication,
);
useEffect(() {
if (!isCheckUpdateEnabled) return null;
checkUpdate().then((value) {
if (value.first == null) return;
if (value.first! <= value.last) return;
showDialog(
context: context,
builder: (context) {
final url =
"https://github.com/KRTirtho/spotube/releases/tag/v${value.last}";
return AlertDialog(
title: const Text("Spotube has an update"),
actions: [
ElevatedButton(
child: const Text("Download Now"),
onPressed: () => download(url),
),
],
content: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
Text("Spotube v${value.last} has been released"),
Row(
children: [
const Text("Read the latest "),
AnchorButton(
"release notes",
style: const TextStyle(color: Colors.blue),
onTap: () => launchUrlString(
url,
mode: LaunchMode.externalApplication,
),
),
],
),
],
),
);
});
});
return null;
}, [packageInfo, isCheckUpdateEnabled]);
}

View File

@ -1,5 +1,6 @@
import 'dart:io'; import 'dart:io';
import 'package:audio_service/audio_service.dart';
import 'package:bitsdojo_window/bitsdojo_window.dart'; import 'package:bitsdojo_window/bitsdojo_window.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';
@ -9,20 +10,28 @@ import 'package:hotkey_manager/hotkey_manager.dart';
import 'package:spotube/models/GoRouteDeclarations.dart'; import 'package:spotube/models/GoRouteDeclarations.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/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:just_audio_background/just_audio_background.dart';
import 'package:spotube/themes/dark-theme.dart'; import 'package:spotube/themes/dark-theme.dart';
import 'package:spotube/themes/light-theme.dart'; import 'package:spotube/themes/light-theme.dart';
import 'package:spotube/utils/AudioPlayerHandler.dart';
void main() async { void main() async {
if (Platform.isAndroid || Platform.isIOS) { // await JustAudioBackground.init(
await JustAudioBackground.init( // androidNotificationChannelId: 'oss.krtirtho.Spotube',
androidNotificationChannelId: 'oss.krtirtho.Spotube', // androidNotificationChannelName: 'Spotube',
// androidNotificationOngoing: true,
// );
AudioPlayerHandler audioPlayerHandler = await AudioService.init(
builder: () => AudioPlayerHandler(),
config: const AudioServiceConfig(
androidNotificationChannelId: 'com.krtirtho.Spotube',
androidNotificationChannelName: 'Spotube', androidNotificationChannelName: 'Spotube',
androidNotificationOngoing: true, androidNotificationOngoing: true,
); ),
} else { );
if (!Platform.isAndroid && !Platform.isIOS) {
WidgetsFlutterBinding.ensureInitialized(); WidgetsFlutterBinding.ensureInitialized();
await hotKeyManager.unregisterAll(); await hotKeyManager.unregisterAll();
doWhenWindowReady(() { doWhenWindowReady(() {
@ -35,14 +44,29 @@ void main() async {
appWindow.show(); appWindow.show();
}); });
} }
runApp(ProviderScope(child: MyApp())); runApp(ProviderScope(
child: Spotube(),
overrides: [
playbackProvider.overrideWithProvider(ChangeNotifierProvider(
(ref) {
final youtube = ref.watch(youtubeProvider);
final preferences = ref.watch(userPreferencesProvider);
return Playback(
player: audioPlayerHandler,
youtube: youtube,
preferences: preferences,
);
},
))
],
));
} }
class MyApp extends HookConsumerWidget { class Spotube extends HookConsumerWidget {
final GoRouter _router = createGoRouter(); final GoRouter _router = createGoRouter();
final logger = getLogger(MyApp); final logger = getLogger(Spotube);
MyApp({Key? key}) : super(key: key); Spotube({Key? key}) : super(key: key);
@override @override
Widget build(BuildContext context, ref) { Widget build(BuildContext context, ref) {
final themeMode = final themeMode =

View File

@ -0,0 +1,38 @@
import 'package:spotify/spotify.dart';
class CurrentPlaylist {
List<Track>? _tempTrack;
List<Track> tracks;
String id;
String name;
String thumbnail;
CurrentPlaylist({
required this.tracks,
required this.id,
required this.name,
required this.thumbnail,
});
List<String> get trackIds => tracks.map((e) => e.id!).toList();
bool shuffle() {
// won't shuffle if already shuffled
if (_tempTrack == null) {
_tempTrack = [...tracks];
tracks.shuffle();
return true;
}
return false;
}
bool unshuffle() {
// without _tempTracks unshuffling can't be done
if (_tempTrack != null) {
tracks = [..._tempTrack!];
_tempTrack = null;
return true;
}
return false;
}
}

View File

@ -1,59 +1,21 @@
import 'dart:async'; import 'dart:async';
import 'package:audio_session/audio_session.dart'; import 'package:audio_service/audio_service.dart';
import 'package:flutter/cupertino.dart'; import 'package:flutter/cupertino.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:just_audio/just_audio.dart'; import 'package:just_audio/just_audio.dart';
import 'package:just_audio_background/just_audio_background.dart';
import 'package:spotify/spotify.dart'; import 'package:spotify/spotify.dart';
import 'package:spotube/helpers/artist-to-string.dart'; import 'package:spotube/helpers/artist-to-string.dart';
import 'package:spotube/helpers/image-to-url-string.dart'; import 'package:spotube/helpers/image-to-url-string.dart';
import 'package:spotube/helpers/search-youtube.dart'; import 'package:spotube/helpers/search-youtube.dart';
import 'package:spotube/models/CurrentPlaylist.dart';
import 'package:spotube/models/Logger.dart'; import 'package:spotube/models/Logger.dart';
import 'package:spotube/provider/AudioPlayer.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/AudioPlayerHandler.dart';
import 'package:youtube_explode_dart/youtube_explode_dart.dart'; import 'package:youtube_explode_dart/youtube_explode_dart.dart';
class CurrentPlaylist {
List<Track>? _tempTrack;
List<Track> tracks;
String id;
String name;
String thumbnail;
CurrentPlaylist({
required this.tracks,
required this.id,
required this.name,
required this.thumbnail,
});
List<String> get trackIds => tracks.map((e) => e.id!).toList();
bool shuffle() {
// won't shuffle if already shuffled
if (_tempTrack == null) {
_tempTrack = [...tracks];
tracks.shuffle();
return true;
}
return false;
}
bool unshuffle() {
// without _tempTracks unshuffling can't be done
if (_tempTrack != null) {
tracks = [..._tempTrack!];
_tempTrack = null;
return true;
}
return false;
}
}
class Playback extends ChangeNotifier { class Playback extends ChangeNotifier {
ChangeNotifierProviderRef<Playback> ref;
AudioSource? _currentAudioSource; AudioSource? _currentAudioSource;
final _logger = getLogger(Playback); final _logger = getLogger(Playback);
CurrentPlaylist? _currentPlaylist; CurrentPlaylist? _currentPlaylist;
@ -63,36 +25,38 @@ class Playback extends ChangeNotifier {
bool _isPlaying = false; bool _isPlaying = false;
Duration? duration; Duration? duration;
// listeners
StreamSubscription<bool>? _playingStreamListener;
StreamSubscription<Duration?>? _durationStreamListener;
StreamSubscription<AudioInterruptionEvent>? _audioInterruptionEventListener;
StreamSubscription<Duration>? _positionStreamListener;
Duration _prevPosition = Duration.zero; Duration _prevPosition = Duration.zero;
bool _shuffled = false; bool _shuffled = false;
AudioPlayer player; AudioPlayerHandler player;
YoutubeExplode youtube; YoutubeExplode youtube;
AudioSession? _audioSession; UserPreferences preferences;
Playback({ Playback({
required this.player, required this.player,
required this.youtube, required this.youtube,
required this.ref, required this.preferences,
CurrentPlaylist? currentPlaylist, CurrentPlaylist? currentPlaylist,
Track? currentTrack, Track? currentTrack,
}) : _currentPlaylist = currentPlaylist, }) : _currentPlaylist = currentPlaylist,
_currentTrack = currentTrack; _currentTrack = currentTrack {
player.onNextRequest = () {
movePlaylistPositionBy(1);
};
player.onPreviousRequest = () {
movePlaylistPositionBy(-1);
};
_init();
}
void register() { void _init() {
_playingStreamListener = player.playingStream.listen( player.core.playingStream.listen(
(playing) { (playing) {
_isPlaying = playing; _isPlaying = playing;
notifyListeners(); notifyListeners();
}, },
); );
_durationStreamListener = player.durationStream.listen((event) async { player.core.durationStream.listen((event) async {
if (event != null) { if (event != null) {
// Actually things doesn't work all the time as they were // Actually things doesn't work all the time as they were
// described. So instead of listening to a `_ready` // described. So instead of listening to a `_ready`
@ -101,7 +65,7 @@ class Playback extends ChangeNotifier {
// been loaded thus indicating buffering started // been loaded thus indicating buffering started
if (event != Duration.zero && event != duration) { if (event != Duration.zero && event != duration) {
// this line is for prev/next or already playing playlist // this line is for prev/next or already playing playlist
if (player.playing) await player.pause(); if (player.core.playing) await player.pause();
await player.play(); await player.play();
} }
duration = event; duration = event;
@ -109,8 +73,7 @@ class Playback extends ChangeNotifier {
} }
}); });
_positionStreamListener = player.core.createPositionStream().listen((position) async {
player.createPositionStream().listen((position) async {
// detecting multiple same call // detecting multiple same call
if (_prevPosition.inSeconds == position.inSeconds) return; if (_prevPosition.inSeconds == position.inSeconds) return;
_prevPosition = position; _prevPosition = position;
@ -126,28 +89,18 @@ class Playback extends ChangeNotifier {
await player.pause(); await player.pause();
movePlaylistPositionBy(1); movePlaylistPositionBy(1);
} else { } else {
await audioSession?.setActive(false);
_isPlaying = false; _isPlaying = false;
duration = null; duration = null;
notifyListeners(); notifyListeners();
} }
} }
}); });
AudioSession.instance.then((session) async {
_audioSession = session;
await session.configure(const AudioSessionConfiguration.music());
_audioInterruptionEventListener = session.interruptionEventStream.listen(
(AudioInterruptionEvent event) {},
);
});
} }
bool get shuffled => _shuffled; bool get shuffled => _shuffled;
CurrentPlaylist? get currentPlaylist => _currentPlaylist; CurrentPlaylist? get currentPlaylist => _currentPlaylist;
Track? get currentTrack => _currentTrack; Track? get currentTrack => _currentTrack;
bool get isPlaying => _isPlaying; bool get isPlaying => _isPlaying;
AudioSession? get audioSession => _audioSession;
set setCurrentTrack(Track track) { set setCurrentTrack(Track track) {
_logger.v("[Setting Current Track] ${track.name} - ${track.id}"); _logger.v("[Setting Current Track] ${track.name} - ${track.id}");
@ -168,7 +121,6 @@ class Playback extends ChangeNotifier {
duration = null; duration = null;
_currentPlaylist = null; _currentPlaylist = null;
_currentTrack = null; _currentTrack = null;
_audioSession?.setActive(false);
notifyListeners(); notifyListeners();
} }
@ -188,16 +140,6 @@ class Playback extends ChangeNotifier {
} }
} }
@override
dispose() {
_durationStreamListener?.cancel();
_playingStreamListener?.cancel();
_audioInterruptionEventListener?.cancel();
_positionStreamListener?.cancel();
_audioSession?.setActive(false);
super.dispose();
}
void movePlaylistPositionBy(int pos) { void movePlaylistPositionBy(int pos) {
_logger.v("[Playlist Position Move] $pos"); _logger.v("[Playlist Position Move] $pos");
if (_currentTrack != null && _currentPlaylist != null) { if (_currentTrack != null && _currentPlaylist != null) {
@ -227,7 +169,7 @@ class Playback extends ChangeNotifier {
// the track is already playing so no need to change that // the track is already playing so no need to change that
if (track != null && track.id == _currentTrack?.id) return; if (track != null && track.id == _currentTrack?.id) return;
track ??= _currentTrack; track ??= _currentTrack;
if (track != null && await _audioSession?.setActive(true) == true) { if (track != null) {
Uri? parsedUri = Uri.tryParse(track.uri ?? ""); Uri? parsedUri = Uri.tryParse(track.uri ?? "");
final tag = MediaItem( final tag = MediaItem(
id: track.id!, id: track.id!,
@ -236,9 +178,10 @@ class Playback extends ChangeNotifier {
artist: artistsToString(track.artists ?? <ArtistSimple>[]), artist: artistsToString(track.artists ?? <ArtistSimple>[]),
artUri: Uri.parse(imageToUrlString(track.album?.images)), artUri: Uri.parse(imageToUrlString(track.album?.images)),
); );
player.addItem(tag);
if (parsedUri != null && parsedUri.hasAbsolutePath) { if (parsedUri != null && parsedUri.hasAbsolutePath) {
_currentAudioSource = AudioSource.uri(parsedUri, tag: tag); _currentAudioSource = AudioSource.uri(parsedUri);
await player await player.core
.setAudioSource( .setAudioSource(
_currentAudioSource!, _currentAudioSource!,
preload: true, preload: true,
@ -247,17 +190,17 @@ class Playback extends ChangeNotifier {
_currentTrack = track; _currentTrack = track;
notifyListeners(); notifyListeners();
}); });
// await player.play();
return;
} }
final preferences = ref.read(userPreferencesProvider);
final spotubeTrack = await toSpotubeTrack( final spotubeTrack = await toSpotubeTrack(
youtube, youtube,
track, track,
preferences.ytSearchFormat, preferences.ytSearchFormat,
); );
if (setTrackUriById(track.id!, spotubeTrack.ytUri)) { if (setTrackUriById(track.id!, spotubeTrack.ytUri)) {
_currentAudioSource = _currentAudioSource = AudioSource.uri(Uri.parse(spotubeTrack.ytUri));
AudioSource.uri(Uri.parse(spotubeTrack.ytUri), tag: tag); await player.core
await player
.setAudioSource( .setAudioSource(
_currentAudioSource!, _currentAudioSource!,
preload: true, preload: true,
@ -266,6 +209,7 @@ class Playback extends ChangeNotifier {
_currentTrack = spotubeTrack; _currentTrack = spotubeTrack;
notifyListeners(); notifyListeners();
}); });
// await player.play();
} }
} }
} catch (e, stack) { } catch (e, stack) {
@ -289,11 +233,12 @@ class Playback extends ChangeNotifier {
} }
final playbackProvider = ChangeNotifierProvider<Playback>((ref) { final playbackProvider = ChangeNotifierProvider<Playback>((ref) {
final player = ref.watch(audioPlayerProvider); final player = AudioPlayerHandler();
final youtube = ref.watch(youtubeProvider); final youtube = ref.watch(youtubeProvider);
final preferences = ref.watch(userPreferencesProvider);
return Playback( return Playback(
player: player, player: player,
youtube: youtube, youtube: youtube,
ref: ref, preferences: preferences,
); );
}); });

View File

@ -20,6 +20,8 @@ class UserPreferences extends PersistedChangeNotifier {
HotKey? prevTrackHotKey; HotKey? prevTrackHotKey;
HotKey? playPauseHotKey; HotKey? playPauseHotKey;
bool checkUpdate;
MaterialColor accentColorScheme; MaterialColor accentColorScheme;
MaterialColor backgroundColorScheme; MaterialColor backgroundColorScheme;
UserPreferences({ UserPreferences({
@ -33,6 +35,7 @@ class UserPreferences extends PersistedChangeNotifier {
this.nextTrackHotKey, this.nextTrackHotKey,
this.prevTrackHotKey, this.prevTrackHotKey,
this.playPauseHotKey, this.playPauseHotKey,
this.checkUpdate = true,
}) : super(); }) : super();
void setThemeMode(ThemeMode mode) { void setThemeMode(ThemeMode mode) {
@ -95,10 +98,17 @@ class UserPreferences extends PersistedChangeNotifier {
updatePersistence(); updatePersistence();
} }
void setCheckUpdate(bool check) {
checkUpdate = check;
notifyListeners();
updatePersistence();
}
@override @override
FutureOr<void> loadFromLocal(Map<String, dynamic> map) { FutureOr<void> loadFromLocal(Map<String, dynamic> map) {
saveTrackLyrics = map["saveTrackLyrics"] ?? false; saveTrackLyrics = map["saveTrackLyrics"] ?? false;
recommendationMarket = map["recommendationMarket"] ?? recommendationMarket; recommendationMarket = map["recommendationMarket"] ?? recommendationMarket;
checkUpdate = map["checkUpdate"] ?? checkUpdate;
geniusAccessToken = geniusAccessToken =
map["geniusAccessToken"] ?? getRandomElement(lyricsSecrets); map["geniusAccessToken"] ?? getRandomElement(lyricsSecrets);
nextTrackHotKey = map["nextTrackHotKey"] != null nextTrackHotKey = map["nextTrackHotKey"] != null
@ -133,6 +143,7 @@ class UserPreferences extends PersistedChangeNotifier {
"themeMode": themeMode.index, "themeMode": themeMode.index,
"backgroundColorScheme": backgroundColorScheme.value, "backgroundColorScheme": backgroundColorScheme.value,
"accentColorScheme": accentColorScheme.value, "accentColorScheme": accentColorScheme.value,
"checkUpdate": checkUpdate,
}; };
} }
} }

View File

@ -0,0 +1,83 @@
import 'dart:async';
import 'package:audio_service/audio_service.dart';
import 'package:just_audio/just_audio.dart';
/// An [AudioHandler] for playing a single item.
class AudioPlayerHandler extends BaseAudioHandler {
final _player = AudioPlayer();
FutureOr<void> Function()? onNextRequest;
FutureOr<void> Function()? onPreviousRequest;
/// Initialise our audio handler.
AudioPlayerHandler() {
// So that our clients (the Flutter UI and the system notification) know
// what state to display, here we set up our audio handler to broadcast all
// playback state changes as they happen via playbackState...
_player.playbackEventStream.map(_transformEvent).pipe(playbackState);
}
AudioPlayer get core => _player;
void addItem(MediaItem item) {
mediaItem.add(item);
}
// In this simple example, we handle only 4 actions: play, pause, seek and
// stop. Any button press from the Flutter UI, notification, lock screen or
// headset will be routed through to these 4 methods so that you can handle
// your audio playback logic in one place.
@override
Future<void> play() => _player.play();
@override
Future<void> pause() => _player.pause();
@override
Future<void> seek(Duration position) => _player.seek(position);
@override
Future<void> stop() => _player.stop();
@override
Future<void> skipToNext() async {
await onNextRequest?.call();
await super.skipToNext();
}
@override
Future<void> skipToPrevious() async {
await onPreviousRequest?.call();
await super.skipToPrevious();
}
/// Transform a just_audio event into an audio_service state.
///
/// This method is used from the constructor. Every event received from the
/// just_audio player will be transformed into an audio_service state so that
/// it can be broadcast to audio_service clients.
PlaybackState _transformEvent(PlaybackEvent event) {
return PlaybackState(
controls: [
MediaControl.skipToPrevious,
if (_player.playing) MediaControl.pause else MediaControl.play,
MediaControl.skipToNext,
],
androidCompactActionIndices: const [0, 1, 2],
processingState: const {
ProcessingState.idle: AudioProcessingState.idle,
ProcessingState.loading: AudioProcessingState.loading,
ProcessingState.buffering: AudioProcessingState.buffering,
ProcessingState.ready: AudioProcessingState.ready,
ProcessingState.completed: AudioProcessingState.completed,
}[_player.processingState]!,
playing: _player.playing,
updatePosition: _player.position,
bufferedPosition: _player.bufferedPosition,
speed: _player.speed,
queueIndex: event.currentIndex,
);
}
}

View File

@ -30,7 +30,7 @@ packages:
source: hosted source: hosted
version: "2.8.2" version: "2.8.2"
audio_service: audio_service:
dependency: transitive dependency: "direct main"
description: description:
name: audio_service name: audio_service
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
@ -338,13 +338,6 @@ packages:
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "0.9.21" version: "0.9.21"
just_audio_background:
dependency: "direct main"
description:
name: just_audio_background
url: "https://pub.dartlang.org"
source: hosted
version: "0.0.1-beta.5"
just_audio_libwinmedia: just_audio_libwinmedia:
dependency: "direct main" dependency: "direct main"
description: description:
@ -870,6 +863,13 @@ packages:
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "2.1.2" version: "2.1.2"
version:
dependency: "direct main"
description:
name: version
url: "https://pub.dartlang.org"
source: hosted
version: "2.0.0"
win32: win32:
dependency: transitive dependency: transitive
description: description:

View File

@ -54,13 +54,13 @@ dependencies:
hooks_riverpod: ^1.0.3 hooks_riverpod: ^1.0.3
go_router: ^3.0.4 go_router: ^3.0.4
palette_generator: ^0.3.3 palette_generator: ^0.3.3
audio_session: ^0.1.6+1
just_audio_background: ^0.0.1-beta.5
logger: ^1.1.0 logger: ^1.1.0
permission_handler: ^9.2.0 permission_handler: ^9.2.0
marquee: ^2.2.1 marquee: ^2.2.1
scroll_to_index: ^2.1.1 scroll_to_index: ^2.1.1
package_info_plus: ^1.4.2 package_info_plus: ^1.4.2
version: ^2.0.0
audio_service: ^0.18.4
dev_dependencies: dev_dependencies:
flutter_test: flutter_test:

View File

@ -13,7 +13,7 @@ import 'package:spotube/main.dart';
void main() { void main() {
testWidgets('Counter increments smoke test', (WidgetTester tester) async { testWidgets('Counter increments smoke test', (WidgetTester tester) async {
// Build our app and trigger a frame. // Build our app and trigger a frame.
await tester.pumpWidget(MyApp()); await tester.pumpWidget(Spotube());
// Verify that our counter starts at 0. // Verify that our counter starts at 0.
expect(find.text('0'), findsOneWidget); expect(find.text('0'), findsOneWidget);