mirror of
https://github.com/KRTirtho/spotube.git
synced 2025-09-13 07:55:18 +00:00
Merge branch 'master' into build
This commit is contained in:
commit
640d227ea5
@ -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.
|
||||||
|
@ -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';
|
||||||
|
|
||||||
|
@ -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';
|
||||||
|
@ -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';
|
||||||
|
@ -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,
|
||||||
|
@ -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,
|
||||||
|
@ -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));
|
||||||
|
@ -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';
|
||||||
|
|
||||||
|
@ -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);
|
||||||
|
});
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
],
|
],
|
||||||
|
@ -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';
|
||||||
|
@ -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';
|
||||||
|
@ -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);
|
||||||
|
@ -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);
|
||||||
|
|
||||||
|
90
lib/hooks/useUpdateChecker.dart
Normal file
90
lib/hooks/useUpdateChecker.dart
Normal 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]);
|
||||||
|
}
|
@ -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 =
|
||||||
|
38
lib/models/CurrentPlaylist.dart
Normal file
38
lib/models/CurrentPlaylist.dart
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
@ -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,
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
@ -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,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
83
lib/utils/AudioPlayerHandler.dart
Normal file
83
lib/utils/AudioPlayerHandler.dart
Normal 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,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
16
pubspec.lock
16
pubspec.lock
@ -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:
|
||||||
|
@ -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:
|
||||||
|
@ -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);
|
||||||
|
Loading…
Reference in New Issue
Block a user