mirror of
https://github.com/KRTirtho/spotube.git
synced 2025-09-13 07:55:18 +00:00
refactor: create audio player wrapper and remove just_audio (again)
This commit is contained in:
parent
7df2a0daba
commit
12915f3e5a
@ -27,8 +27,12 @@ class PlayPauseAction extends Action<PlayPauseIntent> {
|
|||||||
final playlistNotifier = intent.ref.read(PlaylistQueueNotifier.notifier);
|
final playlistNotifier = intent.ref.read(PlaylistQueueNotifier.notifier);
|
||||||
if (playlist == null) {
|
if (playlist == null) {
|
||||||
return null;
|
return null;
|
||||||
} else if (!PlaylistQueueNotifier.isPlaying) {
|
} else if (!audioPlayer.isPlaying) {
|
||||||
await playlistNotifier.play();
|
if (audioPlayer.hasSource && !audioPlayer.isCompleted) {
|
||||||
|
await playlistNotifier.resume();
|
||||||
|
} else {
|
||||||
|
await playlistNotifier.play();
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
await playlistNotifier.pause();
|
await playlistNotifier.pause();
|
||||||
}
|
}
|
||||||
@ -103,8 +107,7 @@ class SeekAction extends Action<SeekIntent> {
|
|||||||
);
|
);
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
final position =
|
final position = (await audioPlayer.position ?? Duration.zero).inSeconds;
|
||||||
(await audioPlayer.getCurrentPosition() ?? Duration.zero).inSeconds;
|
|
||||||
await playlistNotifier.seek(
|
await playlistNotifier.seek(
|
||||||
Duration(
|
Duration(
|
||||||
seconds: intent.forward ? position + 5 : position - 5,
|
seconds: intent.forward ? position + 5 : position - 5,
|
||||||
|
@ -7,6 +7,7 @@ import 'package:spotube/components/shared/playbutton_card.dart';
|
|||||||
import 'package:spotube/hooks/use_breakpoint_value.dart';
|
import 'package:spotube/hooks/use_breakpoint_value.dart';
|
||||||
import 'package:spotube/provider/playlist_queue_provider.dart';
|
import 'package:spotube/provider/playlist_queue_provider.dart';
|
||||||
import 'package:spotube/provider/spotify_provider.dart';
|
import 'package:spotube/provider/spotify_provider.dart';
|
||||||
|
import 'package:spotube/services/audio_player.dart';
|
||||||
import 'package:spotube/utils/service_utils.dart';
|
import 'package:spotube/utils/service_utils.dart';
|
||||||
import 'package:spotube/utils/type_conversion_utils.dart';
|
import 'package:spotube/utils/type_conversion_utils.dart';
|
||||||
|
|
||||||
@ -41,8 +42,7 @@ class AlbumCard extends HookConsumerWidget {
|
|||||||
@override
|
@override
|
||||||
Widget build(BuildContext context, ref) {
|
Widget build(BuildContext context, ref) {
|
||||||
final playlist = ref.watch(PlaylistQueueNotifier.provider);
|
final playlist = ref.watch(PlaylistQueueNotifier.provider);
|
||||||
final playing = useStream(PlaylistQueueNotifier.playing).data ??
|
final playing = useStream(audioPlayer.playingStream).data ?? false;
|
||||||
PlaylistQueueNotifier.isPlaying;
|
|
||||||
final playlistNotifier = ref.watch(PlaylistQueueNotifier.notifier);
|
final playlistNotifier = ref.watch(PlaylistQueueNotifier.notifier);
|
||||||
final queryClient = useQueryClient();
|
final queryClient = useQueryClient();
|
||||||
final query = queryClient
|
final query = queryClient
|
||||||
|
@ -10,6 +10,7 @@ import 'package:spotube/extensions/context.dart';
|
|||||||
import 'package:spotube/hooks/use_progress.dart';
|
import 'package:spotube/hooks/use_progress.dart';
|
||||||
import 'package:spotube/models/logger.dart';
|
import 'package:spotube/models/logger.dart';
|
||||||
import 'package:spotube/provider/playlist_queue_provider.dart';
|
import 'package:spotube/provider/playlist_queue_provider.dart';
|
||||||
|
import 'package:spotube/services/audio_player.dart';
|
||||||
import 'package:spotube/utils/primitive_utils.dart';
|
import 'package:spotube/utils/primitive_utils.dart';
|
||||||
|
|
||||||
class PlayerControls extends HookConsumerWidget {
|
class PlayerControls extends HookConsumerWidget {
|
||||||
@ -43,9 +44,8 @@ class PlayerControls extends HookConsumerWidget {
|
|||||||
[]);
|
[]);
|
||||||
final playlist = ref.watch(PlaylistQueueNotifier.provider);
|
final playlist = ref.watch(PlaylistQueueNotifier.provider);
|
||||||
final playlistNotifier = ref.watch(PlaylistQueueNotifier.notifier);
|
final playlistNotifier = ref.watch(PlaylistQueueNotifier.notifier);
|
||||||
final playing = useStream(PlaylistQueueNotifier.playing).data ??
|
final playing = useStream(audioPlayer.playingStream).data ?? false;
|
||||||
PlaylistQueueNotifier.isPlaying;
|
final buffering = useStream(audioPlayer.bufferingStream).data ?? true;
|
||||||
final buffering = useStream(playlistNotifier.buffering).data ?? true;
|
|
||||||
final theme = Theme.of(context);
|
final theme = Theme.of(context);
|
||||||
|
|
||||||
final isDominantColorDark = ThemeData.estimateBrightnessForColor(
|
final isDominantColorDark = ThemeData.estimateBrightnessForColor(
|
||||||
@ -141,6 +141,7 @@ class PlayerControls extends HookConsumerWidget {
|
|||||||
// there's an edge case for value being bigger
|
// there's an edge case for value being bigger
|
||||||
// than total duration. Keeping it resolved
|
// than total duration. Keeping it resolved
|
||||||
value: progress.value.toDouble(),
|
value: progress.value.toDouble(),
|
||||||
|
secondaryTrackValue: progressObj.item4,
|
||||||
onChanged: playlist?.isLoading == true || buffering
|
onChanged: playlist?.isLoading == true || buffering
|
||||||
? null
|
? null
|
||||||
: (v) {
|
: (v) {
|
||||||
@ -154,6 +155,7 @@ class PlayerControls extends HookConsumerWidget {
|
|||||||
);
|
);
|
||||||
},
|
},
|
||||||
activeColor: sliderColor,
|
activeColor: sliderColor,
|
||||||
|
secondaryActiveColor: sliderColor.withOpacity(0.2),
|
||||||
inactiveColor: sliderColor.withOpacity(0.15),
|
inactiveColor: sliderColor.withOpacity(0.15),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
@ -10,6 +10,7 @@ import 'package:spotube/components/player/player_track_details.dart';
|
|||||||
import 'package:spotube/collections/intents.dart';
|
import 'package:spotube/collections/intents.dart';
|
||||||
import 'package:spotube/hooks/use_progress.dart';
|
import 'package:spotube/hooks/use_progress.dart';
|
||||||
import 'package:spotube/provider/playlist_queue_provider.dart';
|
import 'package:spotube/provider/playlist_queue_provider.dart';
|
||||||
|
import 'package:spotube/services/audio_player.dart';
|
||||||
import 'package:spotube/utils/service_utils.dart';
|
import 'package:spotube/utils/service_utils.dart';
|
||||||
|
|
||||||
class PlayerOverlay extends HookConsumerWidget {
|
class PlayerOverlay extends HookConsumerWidget {
|
||||||
@ -27,8 +28,7 @@ class PlayerOverlay extends HookConsumerWidget {
|
|||||||
);
|
);
|
||||||
final playlistNotifier = ref.watch(PlaylistQueueNotifier.notifier);
|
final playlistNotifier = ref.watch(PlaylistQueueNotifier.notifier);
|
||||||
final playlist = ref.watch(PlaylistQueueNotifier.provider);
|
final playlist = ref.watch(PlaylistQueueNotifier.provider);
|
||||||
final playing = useStream(PlaylistQueueNotifier.playing).data ??
|
final playing = useStream(audioPlayer.playingStream).data ?? false;
|
||||||
PlaylistQueueNotifier.isPlaying;
|
|
||||||
|
|
||||||
final theme = Theme.of(context);
|
final theme = Theme.of(context);
|
||||||
final textColor = theme.colorScheme.primary;
|
final textColor = theme.colorScheme.primary;
|
||||||
|
@ -6,6 +6,7 @@ import 'package:spotify/spotify.dart';
|
|||||||
import 'package:spotube/components/shared/playbutton_card.dart';
|
import 'package:spotube/components/shared/playbutton_card.dart';
|
||||||
import 'package:spotube/provider/playlist_queue_provider.dart';
|
import 'package:spotube/provider/playlist_queue_provider.dart';
|
||||||
import 'package:spotube/provider/spotify_provider.dart';
|
import 'package:spotube/provider/spotify_provider.dart';
|
||||||
|
import 'package:spotube/services/audio_player.dart';
|
||||||
import 'package:spotube/services/queries/queries.dart';
|
import 'package:spotube/services/queries/queries.dart';
|
||||||
import 'package:spotube/utils/service_utils.dart';
|
import 'package:spotube/utils/service_utils.dart';
|
||||||
import 'package:spotube/utils/type_conversion_utils.dart';
|
import 'package:spotube/utils/type_conversion_utils.dart';
|
||||||
@ -20,8 +21,7 @@ class PlaylistCard extends HookConsumerWidget {
|
|||||||
Widget build(BuildContext context, ref) {
|
Widget build(BuildContext context, ref) {
|
||||||
final playlistQueue = ref.watch(PlaylistQueueNotifier.provider);
|
final playlistQueue = ref.watch(PlaylistQueueNotifier.provider);
|
||||||
final playlistNotifier = ref.watch(PlaylistQueueNotifier.notifier);
|
final playlistNotifier = ref.watch(PlaylistQueueNotifier.notifier);
|
||||||
final playing = useStream(PlaylistQueueNotifier.playing).data ??
|
final playing = useStream(audioPlayer.playingStream).data ?? false;
|
||||||
PlaylistQueueNotifier.isPlaying;
|
|
||||||
final queryBowl = QueryClient.of(context);
|
final queryBowl = QueryClient.of(context);
|
||||||
final query = queryBowl.getQuery<List<Track>, dynamic>(
|
final query = queryBowl.getQuery<List<Track>, dynamic>(
|
||||||
"playlist-tracks/${playlist.id}",
|
"playlist-tracks/${playlist.id}",
|
||||||
|
@ -2,16 +2,18 @@ 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/playlist_queue_provider.dart';
|
import 'package:spotube/provider/playlist_queue_provider.dart';
|
||||||
|
import 'package:spotube/services/audio_player.dart';
|
||||||
import 'package:tuple/tuple.dart';
|
import 'package:tuple/tuple.dart';
|
||||||
|
|
||||||
Tuple3<double, Duration, Duration> useProgress(WidgetRef ref) {
|
Tuple4<double, Duration, Duration, double> useProgress(WidgetRef ref) {
|
||||||
ref.watch(PlaylistQueueNotifier.provider);
|
ref.watch(PlaylistQueueNotifier.provider);
|
||||||
|
|
||||||
|
final bufferProgress =
|
||||||
|
useStream(audioPlayer.bufferedPositionStream).data?.inSeconds ?? 0;
|
||||||
final playlistNotifier = ref.watch(PlaylistQueueNotifier.notifier);
|
final playlistNotifier = ref.watch(PlaylistQueueNotifier.notifier);
|
||||||
|
|
||||||
final duration =
|
final duration = useStream(audioPlayer.durationStream).data ?? Duration.zero;
|
||||||
useStream(PlaylistQueueNotifier.duration).data ?? Duration.zero;
|
final positionSnapshot = useStream(audioPlayer.positionStream);
|
||||||
final positionSnapshot = useStream(PlaylistQueueNotifier.position);
|
|
||||||
|
|
||||||
final position = positionSnapshot.data ?? Duration.zero;
|
final position = positionSnapshot.data ?? Duration.zero;
|
||||||
|
|
||||||
@ -31,9 +33,12 @@ Tuple3<double, Duration, Duration> useProgress(WidgetRef ref) {
|
|||||||
return null;
|
return null;
|
||||||
}, [positionSnapshot.hasData, duration]);
|
}, [positionSnapshot.hasData, duration]);
|
||||||
|
|
||||||
return Tuple3(
|
return Tuple4(
|
||||||
sliderMax == 0 || sliderValue > sliderMax ? 0 : sliderValue / sliderMax,
|
sliderMax == 0 || sliderValue > sliderMax ? 0 : sliderValue / sliderMax,
|
||||||
position,
|
position,
|
||||||
duration,
|
duration,
|
||||||
|
sliderMax == 0 || bufferProgress > sliderMax
|
||||||
|
? 0
|
||||||
|
: bufferProgress / sliderMax,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -1,13 +1,13 @@
|
|||||||
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/playlist_queue_provider.dart';
|
import 'package:spotube/services/audio_player.dart';
|
||||||
|
|
||||||
int useSyncedLyrics(
|
int useSyncedLyrics(
|
||||||
WidgetRef ref,
|
WidgetRef ref,
|
||||||
Map<int, String> lyricsMap,
|
Map<int, String> lyricsMap,
|
||||||
int delay,
|
int delay,
|
||||||
) {
|
) {
|
||||||
final stream = PlaylistQueueNotifier.position;
|
final stream = audioPlayer.positionStream;
|
||||||
|
|
||||||
final currentTime = useState(0);
|
final currentTime = useState(0);
|
||||||
|
|
||||||
|
@ -71,6 +71,7 @@ Future<void> main(List<String> rawArgs) async {
|
|||||||
}
|
}
|
||||||
|
|
||||||
WidgetsFlutterBinding.ensureInitialized();
|
WidgetsFlutterBinding.ensureInitialized();
|
||||||
|
|
||||||
await DesktopTools.ensureInitialized(
|
await DesktopTools.ensureInitialized(
|
||||||
DesktopWindowOptions(
|
DesktopWindowOptions(
|
||||||
hideTitleBar: true,
|
hideTitleBar: true,
|
||||||
|
@ -1,6 +1,5 @@
|
|||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
|
|
||||||
import 'package:audioplayers/audioplayers.dart';
|
|
||||||
import 'package:flutter_cache_manager/flutter_cache_manager.dart';
|
import 'package:flutter_cache_manager/flutter_cache_manager.dart';
|
||||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
import 'package:palette_generator/palette_generator.dart';
|
import 'package:palette_generator/palette_generator.dart';
|
||||||
@ -140,8 +139,6 @@ class PlaylistQueueNotifier extends PersistedStateNotifier<PlaylistQueue?> {
|
|||||||
|
|
||||||
late AudioServices audioServices;
|
late AudioServices audioServices;
|
||||||
|
|
||||||
final StreamController<bool> _bufferingController;
|
|
||||||
|
|
||||||
static final provider =
|
static final provider =
|
||||||
StateNotifierProvider<PlaylistQueueNotifier, PlaylistQueue?>(
|
StateNotifierProvider<PlaylistQueueNotifier, PlaylistQueue?>(
|
||||||
(ref) => PlaylistQueueNotifier._(ref),
|
(ref) => PlaylistQueueNotifier._(ref),
|
||||||
@ -149,16 +146,14 @@ class PlaylistQueueNotifier extends PersistedStateNotifier<PlaylistQueue?> {
|
|||||||
|
|
||||||
static final notifier = provider.notifier;
|
static final notifier = provider.notifier;
|
||||||
|
|
||||||
PlaylistQueueNotifier._(this.ref)
|
PlaylistQueueNotifier._(this.ref) : super(null, "playlist") {
|
||||||
: _bufferingController = StreamController.broadcast(),
|
|
||||||
super(null, "playlist") {
|
|
||||||
configure();
|
configure();
|
||||||
}
|
}
|
||||||
|
|
||||||
void configure() async {
|
void configure() async {
|
||||||
audioServices = await AudioServices.create(ref, this);
|
audioServices = await AudioServices.create(ref, this);
|
||||||
|
|
||||||
audioPlayer.onPlayerComplete.listen((event) async {
|
audioPlayer.completedStream.listen((event) async {
|
||||||
if (!isLoaded) return;
|
if (!isLoaded) return;
|
||||||
if (state!.isLooping) {
|
if (state!.isLooping) {
|
||||||
await audioPlayer.seek(Duration.zero);
|
await audioPlayer.seek(Duration.zero);
|
||||||
@ -170,10 +165,9 @@ class PlaylistQueueNotifier extends PersistedStateNotifier<PlaylistQueue?> {
|
|||||||
|
|
||||||
bool isPreSearching = false;
|
bool isPreSearching = false;
|
||||||
|
|
||||||
audioPlayer.onPositionChanged.listen((pos) async {
|
audioPlayer.positionStream.listen((pos) async {
|
||||||
if (!isLoaded) return;
|
if (!isLoaded) return;
|
||||||
_bufferingController.add(false);
|
final currentDuration = await audioPlayer.duration ?? Duration.zero;
|
||||||
final currentDuration = await audioPlayer.getDuration() ?? Duration.zero;
|
|
||||||
|
|
||||||
// skip all the activeTrack.skipSegments
|
// skip all the activeTrack.skipSegments
|
||||||
if (state?.isLoading != true &&
|
if (state?.isLoading != true &&
|
||||||
@ -199,18 +193,16 @@ class PlaylistQueueNotifier extends PersistedStateNotifier<PlaylistQueue?> {
|
|||||||
!isPreSearching) {
|
!isPreSearching) {
|
||||||
isPreSearching = true;
|
isPreSearching = true;
|
||||||
final tracks = state!.tracks.toList();
|
final tracks = state!.tracks.toList();
|
||||||
tracks[state!.active + 1] = await SpotubeTrack.fetchFromTrack(
|
final newTrack = await SpotubeTrack.fetchFromTrack(
|
||||||
state!.tracks.elementAt(state!.active + 1),
|
state!.tracks.elementAt(state!.active + 1),
|
||||||
preferences,
|
preferences,
|
||||||
);
|
);
|
||||||
|
tracks[state!.active + 1] = newTrack;
|
||||||
|
await audioPlayer.preload(newTrack.ytUri);
|
||||||
state = state!.copyWith(tracks: Set.from(tracks));
|
state = state!.copyWith(tracks: Set.from(tracks));
|
||||||
isPreSearching = false;
|
isPreSearching = false;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
audioPlayer.onSeekComplete.listen((event) {
|
|
||||||
_bufferingController.add(false);
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// properties
|
// properties
|
||||||
@ -222,31 +214,6 @@ class PlaylistQueueNotifier extends PersistedStateNotifier<PlaylistQueue?> {
|
|||||||
|
|
||||||
bool get isLoaded => state != null;
|
bool get isLoaded => state != null;
|
||||||
|
|
||||||
Stream<bool> get buffering =>
|
|
||||||
_bufferingController.stream.asyncMap((bufferEvent) async {
|
|
||||||
final duration = await audioPlayer.getDuration();
|
|
||||||
final position = await audioPlayer.getCurrentPosition();
|
|
||||||
final isBuffering = state?.activeTrack is! SpotubeTrack &&
|
|
||||||
audioPlayer.state == PlayerState.playing &&
|
|
||||||
(bufferEvent || (duration == null && position == null));
|
|
||||||
return isBuffering;
|
|
||||||
});
|
|
||||||
|
|
||||||
Future<bool> get isBuffering => buffering.first;
|
|
||||||
|
|
||||||
// redirectors
|
|
||||||
static bool get isPlaying => audioPlayer.state == PlayerState.playing;
|
|
||||||
static bool get isPaused => audioPlayer.state == PlayerState.paused;
|
|
||||||
static bool get isStopped => audioPlayer.state == PlayerState.stopped;
|
|
||||||
|
|
||||||
static Stream<Duration> get duration =>
|
|
||||||
audioPlayer.onDurationChanged.asBroadcastStream();
|
|
||||||
static Stream<Duration> get position =>
|
|
||||||
audioPlayer.onPositionChanged.asBroadcastStream();
|
|
||||||
static Stream<bool> get playing => audioPlayer.onPlayerStateChanged
|
|
||||||
.map((event) => event == PlayerState.playing)
|
|
||||||
.asBroadcastStream();
|
|
||||||
|
|
||||||
List<Video> get siblings => state?.isLoading == false
|
List<Video> get siblings => state?.isLoading == false
|
||||||
? (state!.activeTrack as SpotubeTrack).siblings
|
? (state!.activeTrack as SpotubeTrack).siblings
|
||||||
: [];
|
: [];
|
||||||
@ -360,14 +327,10 @@ class PlaylistQueueNotifier extends PersistedStateNotifier<PlaylistQueue?> {
|
|||||||
|
|
||||||
Future<void> play() async {
|
Future<void> play() async {
|
||||||
if (!isLoaded) return;
|
if (!isLoaded) return;
|
||||||
_bufferingController.add(true);
|
|
||||||
await pause();
|
await pause();
|
||||||
await audioServices.addTrack(state!.activeTrack);
|
await audioServices.addTrack(state!.activeTrack);
|
||||||
if (state!.activeTrack is LocalTrack) {
|
if (state!.activeTrack is LocalTrack) {
|
||||||
await audioPlayer.play(
|
await audioPlayer.play((state!.activeTrack as LocalTrack).path);
|
||||||
DeviceFileSource((state!.activeTrack as LocalTrack).path),
|
|
||||||
mode: PlayerMode.mediaPlayer,
|
|
||||||
);
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (state!.activeTrack is! SpotubeTrack) {
|
if (state!.activeTrack is! SpotubeTrack) {
|
||||||
@ -392,15 +355,9 @@ class PlaylistQueueNotifier extends PersistedStateNotifier<PlaylistQueue?> {
|
|||||||
final cached =
|
final cached =
|
||||||
await DefaultCacheManager().getFileFromCache(state!.activeTrack.id!);
|
await DefaultCacheManager().getFileFromCache(state!.activeTrack.id!);
|
||||||
if (preferences.predownload && cached != null) {
|
if (preferences.predownload && cached != null) {
|
||||||
await audioPlayer.play(
|
await audioPlayer.play(cached.file.path);
|
||||||
DeviceFileSource(cached.file.path),
|
|
||||||
mode: PlayerMode.mediaPlayer,
|
|
||||||
);
|
|
||||||
} else {
|
} else {
|
||||||
await audioPlayer.play(
|
await audioPlayer.play((state!.activeTrack as SpotubeTrack).ytUri);
|
||||||
UrlSource((state!.activeTrack as SpotubeTrack).ytUri),
|
|
||||||
mode: PlayerMode.mediaPlayer,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -475,7 +432,6 @@ class PlaylistQueueNotifier extends PersistedStateNotifier<PlaylistQueue?> {
|
|||||||
|
|
||||||
Future<void> seek(Duration position) async {
|
Future<void> seek(Duration position) async {
|
||||||
if (!isLoaded) return;
|
if (!isLoaded) return;
|
||||||
_bufferingController.add(true);
|
|
||||||
await audioPlayer.seek(position);
|
await audioPlayer.seek(position);
|
||||||
await resume();
|
await resume();
|
||||||
}
|
}
|
||||||
|
@ -212,7 +212,7 @@ class UserPreferences extends PersistedChangeNotifier {
|
|||||||
|
|
||||||
showSystemTrayIcon = map["showSystemTrayIcon"] ?? showSystemTrayIcon;
|
showSystemTrayIcon = map["showSystemTrayIcon"] ?? showSystemTrayIcon;
|
||||||
|
|
||||||
final localeMap = jsonDecode(map["locale"]);
|
final localeMap = map["locale"] != null ? jsonDecode(map["locale"]) : null;
|
||||||
locale =
|
locale =
|
||||||
localeMap != null ? Locale(localeMap?["lc"], localeMap?["cc"]) : locale;
|
localeMap != null ? Locale(localeMap?["lc"], localeMap?["cc"]) : locale;
|
||||||
}
|
}
|
||||||
|
@ -1,25 +1,244 @@
|
|||||||
import 'package:audioplayers/audioplayers.dart';
|
import 'dart:async';
|
||||||
|
|
||||||
final audioPlayer = (() {
|
import 'package:audioplayers/audioplayers.dart' as ap;
|
||||||
AudioPlayer.global.setAudioContext(
|
|
||||||
const AudioContext(
|
final audioPlayer = SpotubeAudioPlayer();
|
||||||
android: AudioContextAndroid(
|
|
||||||
audioFocus: AndroidAudioFocus.gain,
|
enum PlayerState {
|
||||||
audioMode: AndroidAudioMode.inCall,
|
playing,
|
||||||
contentType: AndroidContentType.music,
|
paused,
|
||||||
stayAwake: true,
|
completed,
|
||||||
usageType: AndroidUsageType.media,
|
buffering,
|
||||||
),
|
stopped;
|
||||||
iOS: AudioContextIOS(
|
|
||||||
category: AVAudioSessionCategory.playback,
|
static PlayerState fromApPlayerState(ap.PlayerState state) {
|
||||||
options: [
|
switch (state) {
|
||||||
AVAudioSessionOptions.allowBluetooth,
|
case ap.PlayerState.playing:
|
||||||
AVAudioSessionOptions.allowBluetoothA2DP,
|
return PlayerState.playing;
|
||||||
AVAudioSessionOptions.defaultToSpeaker,
|
case ap.PlayerState.paused:
|
||||||
AVAudioSessionOptions.mixWithOthers,
|
return PlayerState.paused;
|
||||||
],
|
case ap.PlayerState.stopped:
|
||||||
),
|
return PlayerState.stopped;
|
||||||
),
|
case ap.PlayerState.completed:
|
||||||
);
|
return PlayerState.completed;
|
||||||
return AudioPlayer();
|
}
|
||||||
})();
|
}
|
||||||
|
|
||||||
|
ap.PlayerState get asAudioPlayerPlayerState {
|
||||||
|
switch (this) {
|
||||||
|
case PlayerState.playing:
|
||||||
|
return ap.PlayerState.playing;
|
||||||
|
case PlayerState.paused:
|
||||||
|
return ap.PlayerState.paused;
|
||||||
|
case PlayerState.stopped:
|
||||||
|
return ap.PlayerState.stopped;
|
||||||
|
case PlayerState.completed:
|
||||||
|
return ap.PlayerState.completed;
|
||||||
|
case PlayerState.buffering:
|
||||||
|
return ap.PlayerState.paused;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class SpotubeAudioPlayer {
|
||||||
|
final ap.AudioPlayer? _audioPlayer;
|
||||||
|
|
||||||
|
SpotubeAudioPlayer()
|
||||||
|
: _audioPlayer = apSupportedPlatform ? ap.AudioPlayer() : null;
|
||||||
|
|
||||||
|
/// Whether the current platform supports the audioplayers plugin
|
||||||
|
static const bool apSupportedPlatform = true;
|
||||||
|
|
||||||
|
// stream getters
|
||||||
|
Stream<Duration> get durationStream {
|
||||||
|
if (apSupportedPlatform) {
|
||||||
|
return _audioPlayer!.onDurationChanged;
|
||||||
|
} else {
|
||||||
|
throw UnimplementedError();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Stream<Duration> get positionStream {
|
||||||
|
if (apSupportedPlatform) {
|
||||||
|
return _audioPlayer!.onPositionChanged;
|
||||||
|
} else {
|
||||||
|
throw UnimplementedError();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Stream<Duration> get bufferedPositionStream {
|
||||||
|
if (apSupportedPlatform) {
|
||||||
|
// audioplayers doesn't have the capability to get buffered position
|
||||||
|
return const Stream.empty();
|
||||||
|
} else {
|
||||||
|
throw UnimplementedError();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Stream<void> get completedStream {
|
||||||
|
if (apSupportedPlatform) {
|
||||||
|
return _audioPlayer!.onPlayerComplete;
|
||||||
|
} else {
|
||||||
|
throw UnimplementedError();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Stream<bool> get playingStream {
|
||||||
|
if (apSupportedPlatform) {
|
||||||
|
return _audioPlayer!.onPlayerStateChanged.map((state) {
|
||||||
|
return state == ap.PlayerState.playing;
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
throw UnimplementedError();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Stream<bool> get bufferingStream {
|
||||||
|
if (apSupportedPlatform) {
|
||||||
|
return const Stream.empty();
|
||||||
|
} else {
|
||||||
|
throw UnimplementedError();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Stream<PlayerState> get playerStateStream =>
|
||||||
|
_audioPlayer!.onPlayerStateChanged
|
||||||
|
.map((state) => PlayerState.fromApPlayerState(state));
|
||||||
|
|
||||||
|
// regular info getter
|
||||||
|
|
||||||
|
Future<Duration?> get duration async {
|
||||||
|
if (apSupportedPlatform) {
|
||||||
|
return await _audioPlayer!.getDuration();
|
||||||
|
} else {
|
||||||
|
throw UnimplementedError();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<Duration?> get position async {
|
||||||
|
if (apSupportedPlatform) {
|
||||||
|
return await _audioPlayer!.getCurrentPosition();
|
||||||
|
} else {
|
||||||
|
throw UnimplementedError();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<Duration?> get bufferedPosition async {
|
||||||
|
if (apSupportedPlatform) {
|
||||||
|
// audioplayers doesn't have the capability to get buffered position
|
||||||
|
return null;
|
||||||
|
} else {
|
||||||
|
throw UnimplementedError();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bool get hasSource {
|
||||||
|
if (apSupportedPlatform) {
|
||||||
|
return _audioPlayer!.source != null;
|
||||||
|
} else {
|
||||||
|
throw UnimplementedError();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// states
|
||||||
|
bool get isPlaying {
|
||||||
|
if (apSupportedPlatform) {
|
||||||
|
return _audioPlayer!.state == ap.PlayerState.playing;
|
||||||
|
} else {
|
||||||
|
throw UnimplementedError();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bool get isPaused {
|
||||||
|
if (apSupportedPlatform) {
|
||||||
|
return _audioPlayer!.state == ap.PlayerState.paused;
|
||||||
|
} else {
|
||||||
|
throw UnimplementedError();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bool get isStopped {
|
||||||
|
if (apSupportedPlatform) {
|
||||||
|
return _audioPlayer!.state == ap.PlayerState.stopped;
|
||||||
|
} else {
|
||||||
|
throw UnimplementedError();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bool get isCompleted {
|
||||||
|
if (apSupportedPlatform) {
|
||||||
|
return _audioPlayer!.state == ap.PlayerState.completed;
|
||||||
|
} else {
|
||||||
|
throw UnimplementedError();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bool get isBuffering {
|
||||||
|
if (apSupportedPlatform) {
|
||||||
|
// audioplayers doesn't have the capability to get buffering state
|
||||||
|
return false;
|
||||||
|
} else {
|
||||||
|
throw UnimplementedError();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Object _resolveUrlType(String url) {
|
||||||
|
if (apSupportedPlatform) {
|
||||||
|
if (url.startsWith("https")) {
|
||||||
|
return ap.UrlSource(url);
|
||||||
|
} else {
|
||||||
|
return ap.DeviceFileSource(url);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
throw UnimplementedError();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> preload(String url) async {
|
||||||
|
final urlType = _resolveUrlType(url);
|
||||||
|
if (apSupportedPlatform && urlType is ap.Source) {
|
||||||
|
// audioplayers doesn't have the capability to preload
|
||||||
|
return;
|
||||||
|
} else {
|
||||||
|
throw UnimplementedError();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> play(String url) async {
|
||||||
|
final urlType = _resolveUrlType(url);
|
||||||
|
if (apSupportedPlatform && urlType is ap.Source) {
|
||||||
|
await _audioPlayer?.play(urlType);
|
||||||
|
} else {
|
||||||
|
throw UnimplementedError();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> pause() async {
|
||||||
|
await _audioPlayer?.pause();
|
||||||
|
throw UnimplementedError();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> resume() async {
|
||||||
|
await _audioPlayer?.resume();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> stop() async {
|
||||||
|
await _audioPlayer?.stop();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> seek(Duration position) async {
|
||||||
|
await _audioPlayer?.seek(position);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> setVolume(double volume) async {
|
||||||
|
await _audioPlayer?.setVolume(volume);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> setSpeed(double speed) async {
|
||||||
|
await _audioPlayer?.setPlaybackRate(speed);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> dispose() async {
|
||||||
|
await _audioPlayer?.dispose();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -1,7 +1,6 @@
|
|||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
import 'dart:io';
|
import 'dart:io';
|
||||||
|
|
||||||
import 'package:audioplayers/audioplayers.dart';
|
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:mpris_service/mpris_service.dart';
|
import 'package:mpris_service/mpris_service.dart';
|
||||||
import 'package:spotify/spotify.dart';
|
import 'package:spotify/spotify.dart';
|
||||||
@ -39,7 +38,7 @@ class LinuxAudioService {
|
|||||||
pause: playlistNotifier.pause,
|
pause: playlistNotifier.pause,
|
||||||
play: playlistNotifier.resume,
|
play: playlistNotifier.resume,
|
||||||
playPause: () async {
|
playPause: () async {
|
||||||
if (PlaylistQueueNotifier.isPlaying) {
|
if (audioPlayer.isPlaying) {
|
||||||
await playlistNotifier.pause();
|
await playlistNotifier.pause();
|
||||||
} else {
|
} else {
|
||||||
await playlistNotifier.resume();
|
await playlistNotifier.resume();
|
||||||
@ -61,8 +60,9 @@ class LinuxAudioService {
|
|||||||
));
|
));
|
||||||
|
|
||||||
final playerStateStream =
|
final playerStateStream =
|
||||||
audioPlayer.onPlayerStateChanged.listen((state) async {
|
audioPlayer.playerStateStream.listen((state) async {
|
||||||
switch (state) {
|
switch (state) {
|
||||||
|
case PlayerState.buffering:
|
||||||
case PlayerState.playing:
|
case PlayerState.playing:
|
||||||
mpris.playbackStatus = MPRISPlaybackStatus.playing;
|
mpris.playbackStatus = MPRISPlaybackStatus.playing;
|
||||||
break;
|
break;
|
||||||
@ -78,12 +78,12 @@ class LinuxAudioService {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
final positionStream = audioPlayer.onPositionChanged.listen((pos) async {
|
final positionStream = audioPlayer.positionStream.listen((pos) async {
|
||||||
mpris.position = pos;
|
mpris.position = pos;
|
||||||
});
|
});
|
||||||
|
|
||||||
final durationStream =
|
final durationStream =
|
||||||
audioPlayer.onDurationChanged.listen((duration) async {
|
audioPlayer.durationStream.listen((duration) async {
|
||||||
mpris.metadata = mpris.metadata.copyWith(length: duration);
|
mpris.metadata = mpris.metadata.copyWith(length: duration);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -2,7 +2,6 @@ import 'dart:async';
|
|||||||
|
|
||||||
import 'package:audio_service/audio_service.dart';
|
import 'package:audio_service/audio_service.dart';
|
||||||
import 'package:audio_session/audio_session.dart';
|
import 'package:audio_session/audio_session.dart';
|
||||||
import 'package:audioplayers/audioplayers.dart';
|
|
||||||
import 'package:spotube/provider/playlist_queue_provider.dart';
|
import 'package:spotube/provider/playlist_queue_provider.dart';
|
||||||
import 'package:spotube/services/audio_player.dart';
|
import 'package:spotube/services/audio_player.dart';
|
||||||
|
|
||||||
@ -29,11 +28,14 @@ class MobileAudioService extends BaseAudioHandler {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
audioPlayer.onPlayerStateChanged.listen((state) async {
|
audioPlayer.playerStateStream.listen((state) async {
|
||||||
playbackState.add(await _transformEvent());
|
playbackState.add(await _transformEvent());
|
||||||
});
|
});
|
||||||
|
|
||||||
audioPlayer.onPositionChanged.listen((pos) async {
|
audioPlayer.positionStream.listen((pos) async {
|
||||||
|
playbackState.add(await _transformEvent());
|
||||||
|
});
|
||||||
|
audioPlayer.bufferedPositionStream.listen((pos) async {
|
||||||
playbackState.add(await _transformEvent());
|
playbackState.add(await _transformEvent());
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -93,18 +95,15 @@ class MobileAudioService extends BaseAudioHandler {
|
|||||||
@override
|
@override
|
||||||
Future<void> onTaskRemoved() async {
|
Future<void> onTaskRemoved() async {
|
||||||
await playlistNotifier.stop();
|
await playlistNotifier.stop();
|
||||||
await audioPlayer.release();
|
|
||||||
return super.onTaskRemoved();
|
return super.onTaskRemoved();
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<PlaybackState> _transformEvent() async {
|
Future<PlaybackState> _transformEvent() async {
|
||||||
final position = (await audioPlayer.getCurrentPosition()) ?? Duration.zero;
|
final position = (await audioPlayer.position) ?? Duration.zero;
|
||||||
return PlaybackState(
|
return PlaybackState(
|
||||||
controls: [
|
controls: [
|
||||||
MediaControl.skipToPrevious,
|
MediaControl.skipToPrevious,
|
||||||
audioPlayer.state == PlayerState.playing
|
audioPlayer.isPlaying ? MediaControl.pause : MediaControl.play,
|
||||||
? MediaControl.pause
|
|
||||||
: MediaControl.play,
|
|
||||||
MediaControl.skipToNext,
|
MediaControl.skipToNext,
|
||||||
MediaControl.stop,
|
MediaControl.stop,
|
||||||
],
|
],
|
||||||
@ -112,9 +111,9 @@ class MobileAudioService extends BaseAudioHandler {
|
|||||||
MediaAction.seek,
|
MediaAction.seek,
|
||||||
},
|
},
|
||||||
androidCompactActionIndices: const [0, 1, 2],
|
androidCompactActionIndices: const [0, 1, 2],
|
||||||
playing: audioPlayer.state == PlayerState.playing,
|
playing: audioPlayer.isPlaying,
|
||||||
updatePosition: position,
|
updatePosition: position,
|
||||||
bufferedPosition: position,
|
bufferedPosition: await audioPlayer.bufferedPosition ?? Duration.zero,
|
||||||
shuffleMode: playlist?.isShuffled == true
|
shuffleMode: playlist?.isShuffled == true
|
||||||
? AudioServiceShuffleMode.all
|
? AudioServiceShuffleMode.all
|
||||||
: AudioServiceShuffleMode.none,
|
: AudioServiceShuffleMode.none,
|
||||||
|
@ -1,6 +1,5 @@
|
|||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
|
|
||||||
import 'package:audioplayers/audioplayers.dart';
|
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:smtc_windows/smtc_windows.dart';
|
import 'package:smtc_windows/smtc_windows.dart';
|
||||||
import 'package:spotify/spotify.dart';
|
import 'package:spotify/spotify.dart';
|
||||||
@ -41,7 +40,7 @@ class WindowsAudioService {
|
|||||||
});
|
});
|
||||||
|
|
||||||
final playerStateStream =
|
final playerStateStream =
|
||||||
audioPlayer.onPlayerStateChanged.listen((state) async {
|
audioPlayer.playerStateStream.listen((state) async {
|
||||||
switch (state) {
|
switch (state) {
|
||||||
case PlayerState.playing:
|
case PlayerState.playing:
|
||||||
await smtc.setPlaybackStatus(PlaybackStatus.Playing);
|
await smtc.setPlaybackStatus(PlaybackStatus.Playing);
|
||||||
@ -62,12 +61,11 @@ class WindowsAudioService {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
final positionStream = audioPlayer.onPositionChanged.listen((pos) async {
|
final positionStream = audioPlayer.positionStream.listen((pos) async {
|
||||||
await smtc.setPosition(pos);
|
await smtc.setPosition(pos);
|
||||||
});
|
});
|
||||||
|
|
||||||
final durationStream =
|
final durationStream = audioPlayer.durationStream.listen((duration) async {
|
||||||
audioPlayer.onDurationChanged.listen((duration) async {
|
|
||||||
await smtc.setEndTime(duration);
|
await smtc.setEndTime(duration);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -11,7 +11,6 @@ import audioplayers_darwin
|
|||||||
import catcher
|
import catcher
|
||||||
import device_info_plus
|
import device_info_plus
|
||||||
import flutter_secure_storage_macos
|
import flutter_secure_storage_macos
|
||||||
import just_audio
|
|
||||||
import local_notifier
|
import local_notifier
|
||||||
import package_info_plus
|
import package_info_plus
|
||||||
import path_provider_foundation
|
import path_provider_foundation
|
||||||
@ -31,7 +30,6 @@ func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
|
|||||||
CatcherPlugin.register(with: registry.registrar(forPlugin: "CatcherPlugin"))
|
CatcherPlugin.register(with: registry.registrar(forPlugin: "CatcherPlugin"))
|
||||||
DeviceInfoPlusMacosPlugin.register(with: registry.registrar(forPlugin: "DeviceInfoPlusMacosPlugin"))
|
DeviceInfoPlusMacosPlugin.register(with: registry.registrar(forPlugin: "DeviceInfoPlusMacosPlugin"))
|
||||||
FlutterSecureStoragePlugin.register(with: registry.registrar(forPlugin: "FlutterSecureStoragePlugin"))
|
FlutterSecureStoragePlugin.register(with: registry.registrar(forPlugin: "FlutterSecureStoragePlugin"))
|
||||||
JustAudioPlugin.register(with: registry.registrar(forPlugin: "JustAudioPlugin"))
|
|
||||||
LocalNotifierPlugin.register(with: registry.registrar(forPlugin: "LocalNotifierPlugin"))
|
LocalNotifierPlugin.register(with: registry.registrar(forPlugin: "LocalNotifierPlugin"))
|
||||||
FLTPackageInfoPlusPlugin.register(with: registry.registrar(forPlugin: "FLTPackageInfoPlusPlugin"))
|
FLTPackageInfoPlusPlugin.register(with: registry.registrar(forPlugin: "FLTPackageInfoPlusPlugin"))
|
||||||
PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin"))
|
PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin"))
|
||||||
|
28
pubspec.lock
28
pubspec.lock
@ -998,30 +998,6 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "6.6.1"
|
version: "6.6.1"
|
||||||
just_audio:
|
|
||||||
dependency: "direct main"
|
|
||||||
description:
|
|
||||||
name: just_audio
|
|
||||||
sha256: "7e6d31508dacd01a066e3889caf6282e5f1eb60707c230203b21a83af5c55586"
|
|
||||||
url: "https://pub.dev"
|
|
||||||
source: hosted
|
|
||||||
version: "0.9.32"
|
|
||||||
just_audio_platform_interface:
|
|
||||||
dependency: transitive
|
|
||||||
description:
|
|
||||||
name: just_audio_platform_interface
|
|
||||||
sha256: eff112d5138bea3ba544b6338b1e0537a32b5e1425e4d0dc38f732771cda7c84
|
|
||||||
url: "https://pub.dev"
|
|
||||||
source: hosted
|
|
||||||
version: "4.2.0"
|
|
||||||
just_audio_web:
|
|
||||||
dependency: transitive
|
|
||||||
description:
|
|
||||||
name: just_audio_web
|
|
||||||
sha256: "89d8db6f19f3821bb6bf908c4bfb846079afb2ab575b783d781a6bf119e3abaf"
|
|
||||||
url: "https://pub.dev"
|
|
||||||
source: hosted
|
|
||||||
version: "0.4.7"
|
|
||||||
lints:
|
lints:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@ -1657,10 +1633,10 @@ packages:
|
|||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: synchronized
|
name: synchronized
|
||||||
sha256: "33b31b6beb98100bf9add464a36a8dd03eb10c7a8cf15aeec535e9b054aaf04b"
|
sha256: "5fcbd27688af6082f5abd611af56ee575342c30e87541d0245f7ff99faa02c60"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "3.0.1"
|
version: "3.1.0"
|
||||||
system_theme:
|
system_theme:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
|
@ -55,7 +55,6 @@ dependencies:
|
|||||||
introduction_screen: ^3.0.2
|
introduction_screen: ^3.0.2
|
||||||
json_annotation: ^4.8.0
|
json_annotation: ^4.8.0
|
||||||
json_serializable: ^6.6.0
|
json_serializable: ^6.6.0
|
||||||
just_audio: ^0.9.32
|
|
||||||
logger: ^1.1.0
|
logger: ^1.1.0
|
||||||
metadata_god: ^0.4.1
|
metadata_god: ^0.4.1
|
||||||
mime: ^1.0.2
|
mime: ^1.0.2
|
||||||
|
Loading…
Reference in New Issue
Block a user