queue links implemented; cleanup
This commit is contained in:
parent
cbb8559954
commit
a031fedd39
13 changed files with 306 additions and 105 deletions
|
@ -10,8 +10,8 @@ import 'domain/repositories/music_data_repository.dart';
|
|||
import 'presentation/state/audio_store.dart';
|
||||
import 'presentation/state/music_data_store.dart';
|
||||
import 'presentation/state/navigation_store.dart';
|
||||
import 'system/datasources/audio_manager.dart';
|
||||
import 'system/datasources/audio_manager_contract.dart';
|
||||
import 'system/audio/audio_manager.dart';
|
||||
import 'system/audio/audio_manager_contract.dart';
|
||||
import 'system/datasources/local_music_fetcher.dart';
|
||||
import 'system/datasources/local_music_fetcher_contract.dart';
|
||||
import 'system/datasources/moor_music_data_source.dart';
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import 'package:audio_service/audio_service.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import '../../system/datasources/audio_player_task.dart';
|
||||
import '../../system/audio/audio_player_task.dart';
|
||||
|
||||
class AudioServiceWidget extends StatefulWidget {
|
||||
const AudioServiceWidget({@required this.child});
|
||||
|
|
|
@ -4,13 +4,15 @@ import 'dart:ui';
|
|||
|
||||
import 'package:audio_service/audio_service.dart';
|
||||
import 'package:just_audio/just_audio.dart';
|
||||
import 'package:logging/logging.dart';
|
||||
import 'package:moor/isolate.dart';
|
||||
import 'package:moor/moor.dart';
|
||||
|
||||
import '../../domain/entities/shuffle_mode.dart';
|
||||
import '../datasources/moor_music_data_source.dart';
|
||||
import '../models/queue_item.dart';
|
||||
import '../models/song_model.dart';
|
||||
import 'moor_music_data_source.dart';
|
||||
import 'queue_manager.dart';
|
||||
import 'queue_generator.dart';
|
||||
|
||||
const String INIT = 'INIT';
|
||||
const String PLAY_WITH_CONTEXT = 'PLAY_WITH_CONTEXT';
|
||||
|
@ -22,13 +24,15 @@ const String KEY_INDEX = 'INDEX';
|
|||
class AudioPlayerTask extends BackgroundAudioTask {
|
||||
final audioPlayer = AudioPlayer();
|
||||
MoorMusicDataSource moorMusicDataSource;
|
||||
QueueManager qm;
|
||||
QueueGenerator queueGenerator;
|
||||
|
||||
// TODO: confusing naming
|
||||
List<MediaItem> originalPlaybackContext = <MediaItem>[];
|
||||
List<MediaItem> playbackContext = <MediaItem>[];
|
||||
List<QueueItem> playbackContext = <QueueItem>[];
|
||||
|
||||
// TODO: this is not trivial: queue is loaded by audioplayer
|
||||
// this reference enables direct manipulation of the loaded queue
|
||||
ConcatenatingAudioSource queue;
|
||||
List<int> permutation;
|
||||
|
||||
ShuffleMode _shuffleMode = ShuffleMode.none;
|
||||
ShuffleMode get shuffleMode => _shuffleMode;
|
||||
|
@ -40,10 +44,9 @@ class AudioPlayerTask extends BackgroundAudioTask {
|
|||
int _playbackIndex = -1;
|
||||
int get playbackIndex => _playbackIndex;
|
||||
set playbackIndex(int i) {
|
||||
print(i);
|
||||
if (i != null) {
|
||||
_playbackIndex = i;
|
||||
AudioServiceBackground.setMediaItem(playbackContext[i]);
|
||||
AudioServiceBackground.setMediaItem(playbackContext[i].mediaItem);
|
||||
AudioServiceBackground.sendCustomEvent({KEY_INDEX: i});
|
||||
|
||||
AudioServiceBackground.setState(
|
||||
|
@ -61,6 +64,12 @@ class AudioPlayerTask extends BackgroundAudioTask {
|
|||
}
|
||||
}
|
||||
|
||||
static final _log = Logger('AudioPlayerTask')
|
||||
..onRecord.listen((record) {
|
||||
print(
|
||||
'${record.time} [${record.level.name}] ${record.loggerName}: ${record.message}');
|
||||
});
|
||||
|
||||
@override
|
||||
Future<void> onStop() async {
|
||||
await audioPlayer.stop();
|
||||
|
@ -112,19 +121,20 @@ class AudioPlayerTask extends BackgroundAudioTask {
|
|||
Future<void> init() async {
|
||||
print('AudioPlayerTask.init');
|
||||
audioPlayer.playerStateStream.listen((event) => handlePlayerState(event));
|
||||
audioPlayer.currentIndexStream.listen((event) => playbackIndex = event);
|
||||
audioPlayer.sequenceStateStream
|
||||
.listen((event) => playbackIndex = event?.currentIndex);
|
||||
.listen((event) => handleSequenceState(event));
|
||||
|
||||
final connectPort = IsolateNameServer.lookupPortByName(MOOR_ISOLATE);
|
||||
final MoorIsolate moorIsolate = MoorIsolate.fromConnectPort(connectPort);
|
||||
final DatabaseConnection databaseConnection = await moorIsolate.connect();
|
||||
moorMusicDataSource = MoorMusicDataSource.connect(databaseConnection);
|
||||
|
||||
qm = QueueManager(moorMusicDataSource);
|
||||
queueGenerator = QueueGenerator(moorMusicDataSource);
|
||||
}
|
||||
|
||||
Future<void> playWithContext(List<String> context, int index) async {
|
||||
final mediaItems = await qm.getMediaItemsFromPaths(context);
|
||||
final mediaItems = await queueGenerator.getMediaItemsFromPaths(context);
|
||||
playPlaylist(mediaItems, index);
|
||||
}
|
||||
|
||||
|
@ -137,22 +147,44 @@ class AudioPlayerTask extends BackgroundAudioTask {
|
|||
Future<void> setShuffleMode(ShuffleMode mode) async {
|
||||
shuffleMode = mode;
|
||||
|
||||
final index = permutation[playbackIndex];
|
||||
permutation =
|
||||
qm.generatePermutation(shuffleMode, originalPlaybackContext, index);
|
||||
final QueueItem currentQueueItem = playbackContext[playbackIndex];
|
||||
final int index = currentQueueItem.originalIndex;
|
||||
playbackContext =
|
||||
qm.getPermutatedSongs(originalPlaybackContext, permutation);
|
||||
await queueGenerator.generateQueue(shuffleMode, originalPlaybackContext, index);
|
||||
final queuedMediaItems = playbackContext.map((e) => e.mediaItem).toList();
|
||||
|
||||
AudioServiceBackground.setQueue(playbackContext);
|
||||
// FIXME: this does not react correctly when inserted track is currently played
|
||||
AudioServiceBackground.setQueue(queuedMediaItems);
|
||||
|
||||
final newQueue = queueGenerator.mediaItemsToAudioSource(queuedMediaItems);
|
||||
_updateQueue(newQueue, currentQueueItem);
|
||||
}
|
||||
|
||||
void _updateQueue(ConcatenatingAudioSource newQueue, QueueItem currentQueueItem) {
|
||||
final int index = currentQueueItem.originalIndex;
|
||||
|
||||
final newQueue = qm.mediaItemsToAudioSource(playbackContext);
|
||||
queue.removeRange(0, playbackIndex);
|
||||
queue.removeRange(1, queue.length);
|
||||
|
||||
if (shuffleMode == ShuffleMode.none) {
|
||||
queue.insertAll(0, newQueue.children.sublist(0, index));
|
||||
queue.addAll(newQueue.children.sublist(index + 1));
|
||||
playbackIndex = index;
|
||||
switch (currentQueueItem.type) {
|
||||
case QueueItemType.standard:
|
||||
queue.insertAll(0, newQueue.children.sublist(0, index));
|
||||
queue.addAll(newQueue.children.sublist(index + 1));
|
||||
playbackIndex = index;
|
||||
break;
|
||||
case QueueItemType.predecessor:
|
||||
queue.insertAll(0, newQueue.children.sublist(0, index));
|
||||
queue.addAll(newQueue.children.sublist(index));
|
||||
playbackIndex = index;
|
||||
break;
|
||||
case QueueItemType.successor:
|
||||
queue.insertAll(0, newQueue.children.sublist(0, index + 1));
|
||||
queue.addAll(newQueue.children.sublist(index + 1));
|
||||
playbackIndex = index;
|
||||
break;
|
||||
}
|
||||
|
||||
} else {
|
||||
queue.addAll(newQueue.children.sublist(1));
|
||||
}
|
||||
|
@ -171,18 +203,20 @@ class AudioPlayerTask extends BackgroundAudioTask {
|
|||
}
|
||||
|
||||
Future<void> playPlaylist(List<MediaItem> mediaItems, int index) async {
|
||||
permutation = qm.generatePermutation(shuffleMode, mediaItems, index);
|
||||
playbackContext = qm.getPermutatedSongs(mediaItems, permutation);
|
||||
originalPlaybackContext = mediaItems;
|
||||
|
||||
AudioServiceBackground.setQueue(playbackContext);
|
||||
queue = qm.mediaItemsToAudioSource(playbackContext);
|
||||
playbackContext = await queueGenerator.generateQueue(shuffleMode, mediaItems, index);
|
||||
final queuedMediaItems = playbackContext.map((e) => e.mediaItem).toList();
|
||||
|
||||
AudioServiceBackground.setQueue(queuedMediaItems);
|
||||
queue = queueGenerator.mediaItemsToAudioSource(queuedMediaItems);
|
||||
audioPlayer.play();
|
||||
final int startIndex = shuffleMode == ShuffleMode.none ? index : 0;
|
||||
await audioPlayer.load(queue, initialIndex: startIndex);
|
||||
}
|
||||
|
||||
void handlePlayerState(PlayerState ps) {
|
||||
_log.info('handlePlayerState called');
|
||||
if (ps.processingState == ProcessingState.ready && ps.playing) {
|
||||
AudioServiceBackground.setState(
|
||||
controls: [
|
||||
|
@ -211,4 +245,14 @@ class AudioPlayerTask extends BackgroundAudioTask {
|
|||
);
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: this can only be a temporary solution! gets called too often.
|
||||
void handleSequenceState(SequenceState st) {
|
||||
_log.info('handleSequenceState called');
|
||||
if (0 <= playbackIndex && playbackIndex < playbackContext.length) {
|
||||
_log.info('handleSequenceState: setting MediaItem');
|
||||
AudioServiceBackground.setMediaItem(
|
||||
playbackContext[playbackIndex].mediaItem);
|
||||
}
|
||||
}
|
||||
}
|
176
lib/system/audio/queue_generator.dart
Normal file
176
lib/system/audio/queue_generator.dart
Normal file
|
@ -0,0 +1,176 @@
|
|||
import 'package:audio_service/audio_service.dart';
|
||||
import 'package:just_audio/just_audio.dart';
|
||||
|
||||
import '../../domain/entities/shuffle_mode.dart';
|
||||
import '../datasources/music_data_source_contract.dart';
|
||||
import '../models/queue_item.dart';
|
||||
import '../models/song_model.dart';
|
||||
|
||||
class QueueGenerator {
|
||||
QueueGenerator(this._musicDataSource);
|
||||
|
||||
final MusicDataSource _musicDataSource;
|
||||
|
||||
// TODO: test
|
||||
// TODO: optimize -> too slow for whole library
|
||||
// fetching all songs together and preparing playback takes ~500ms compared to ~10.000ms individually
|
||||
Future<List<MediaItem>> getMediaItemsFromPaths(List<String> paths) async {
|
||||
final mediaItems = <MediaItem>[];
|
||||
for (final path in paths) {
|
||||
final song = await _musicDataSource.getSongByPath(path);
|
||||
mediaItems.add(song.toMediaItem());
|
||||
}
|
||||
|
||||
return mediaItems;
|
||||
}
|
||||
|
||||
Future<List<QueueItem>> generateQueue(
|
||||
ShuffleMode shuffleMode,
|
||||
List<MediaItem> mediaItems,
|
||||
int startIndex,
|
||||
) async {
|
||||
List<QueueItem> queue;
|
||||
|
||||
switch (shuffleMode) {
|
||||
case ShuffleMode.none:
|
||||
queue = _generateNormalQueue(mediaItems);
|
||||
break;
|
||||
case ShuffleMode.standard:
|
||||
queue = _generateShuffleQueue(mediaItems, startIndex);
|
||||
break;
|
||||
case ShuffleMode.plus:
|
||||
queue = await _generateShufflePlusQueue(mediaItems, startIndex);
|
||||
}
|
||||
|
||||
return queue;
|
||||
}
|
||||
|
||||
ConcatenatingAudioSource mediaItemsToAudioSource(List<MediaItem> mediaItems) {
|
||||
return ConcatenatingAudioSource(
|
||||
children: mediaItems
|
||||
.map((MediaItem m) => AudioSource.uri(Uri.file(m.id)))
|
||||
.toList());
|
||||
}
|
||||
|
||||
List<QueueItem> _generateNormalQueue(List<MediaItem> mediaItems) {
|
||||
return List<QueueItem>.generate(
|
||||
mediaItems.length,
|
||||
(i) => QueueItem(
|
||||
mediaItems[i],
|
||||
originalIndex: i,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
List<QueueItem> _generateShuffleQueue(
|
||||
List<MediaItem> mediaItems,
|
||||
int startIndex,
|
||||
) {
|
||||
final List<QueueItem> queue = List<QueueItem>.generate(
|
||||
mediaItems.length,
|
||||
(i) => QueueItem(
|
||||
mediaItems[i],
|
||||
originalIndex: i,
|
||||
),
|
||||
);
|
||||
queue.removeAt(startIndex);
|
||||
queue.shuffle();
|
||||
final first = QueueItem(
|
||||
mediaItems[startIndex],
|
||||
originalIndex: startIndex,
|
||||
);
|
||||
return [first] + queue;
|
||||
}
|
||||
|
||||
Future<List<QueueItem>> _generateShufflePlusQueue(
|
||||
List<MediaItem> mediaItems,
|
||||
int startIndex,
|
||||
) async {
|
||||
final List<QueueItem> queue = await _getQueueItemWithLinks(
|
||||
mediaItems[startIndex],
|
||||
startIndex,
|
||||
);
|
||||
final List<int> indices = [];
|
||||
|
||||
// filter mediaitem list
|
||||
// TODO: multiply higher rated songs
|
||||
for (var i = 0; i < mediaItems.length; i++) {
|
||||
if (i != startIndex && !(mediaItems[i].extras['blocked'] as bool)) {
|
||||
indices.add(i);
|
||||
}
|
||||
}
|
||||
|
||||
indices.shuffle();
|
||||
|
||||
for (var i = 0; i < indices.length; i++) {
|
||||
final int index = indices[i];
|
||||
final MediaItem mediaItem = mediaItems[index];
|
||||
|
||||
queue.addAll(await _getQueueItemWithLinks(mediaItem, index));
|
||||
}
|
||||
|
||||
return queue;
|
||||
}
|
||||
|
||||
// TODO: naming things is hard
|
||||
Future<List<QueueItem>> _getQueueItemWithLinks(
|
||||
MediaItem mediaItem,
|
||||
int index,
|
||||
) async {
|
||||
final List<QueueItem> queueItems = [];
|
||||
|
||||
final predecessors = await _getPredecessors(mediaItem);
|
||||
final successors = await _getSuccessors(mediaItem);
|
||||
|
||||
for (final p in predecessors) {
|
||||
queueItems.add(QueueItem(
|
||||
p,
|
||||
originalIndex: index,
|
||||
type: QueueItemType.predecessor,
|
||||
));
|
||||
}
|
||||
|
||||
queueItems.add(QueueItem(
|
||||
mediaItem,
|
||||
originalIndex: index,
|
||||
));
|
||||
|
||||
for (final p in successors) {
|
||||
queueItems.add(QueueItem(
|
||||
p,
|
||||
originalIndex: index,
|
||||
type: QueueItemType.successor,
|
||||
));
|
||||
}
|
||||
|
||||
return queueItems;
|
||||
}
|
||||
|
||||
Future<List<MediaItem>> _getPredecessors(MediaItem mediaItem) async {
|
||||
final List<MediaItem> mediaItems = [];
|
||||
MediaItem currentMediaItem = mediaItem;
|
||||
|
||||
while (currentMediaItem.previous != null) {
|
||||
currentMediaItem =
|
||||
(await _musicDataSource.getSongByPath(currentMediaItem.previous))
|
||||
.toMediaItem();
|
||||
mediaItems.add(currentMediaItem);
|
||||
}
|
||||
|
||||
return mediaItems.reversed.toList();
|
||||
}
|
||||
|
||||
Future<List<MediaItem>> _getSuccessors(MediaItem mediaItem) async {
|
||||
final List<MediaItem> mediaItems = [];
|
||||
MediaItem currentMediaItem = mediaItem;
|
||||
|
||||
while (currentMediaItem.next != null) {
|
||||
currentMediaItem =
|
||||
(await _musicDataSource.getSongByPath(currentMediaItem.next))
|
||||
.toMediaItem();
|
||||
mediaItems.add(currentMediaItem);
|
||||
}
|
||||
|
||||
return mediaItems.toList();
|
||||
}
|
||||
}
|
|
@ -1,73 +0,0 @@
|
|||
import 'package:audio_service/audio_service.dart';
|
||||
import 'package:just_audio/just_audio.dart';
|
||||
|
||||
import '../../domain/entities/shuffle_mode.dart';
|
||||
import 'music_data_source_contract.dart';
|
||||
|
||||
class QueueManager {
|
||||
QueueManager(this._musicDataSource);
|
||||
|
||||
final MusicDataSource _musicDataSource;
|
||||
|
||||
// TODO: test
|
||||
// TODO: optimize -> too slow for whole library
|
||||
// fetching all songs together and preparing playback takes ~500ms compared to ~10.000ms individually
|
||||
Future<List<MediaItem>> getMediaItemsFromPaths(List<String> paths) async {
|
||||
final mediaItems = <MediaItem>[];
|
||||
for (final path in paths) {
|
||||
final song = await _musicDataSource.getSongByPath(path);
|
||||
mediaItems.add(song.toMediaItem());
|
||||
}
|
||||
|
||||
return mediaItems;
|
||||
}
|
||||
|
||||
// TODO: test
|
||||
List<int> generatePermutation(
|
||||
ShuffleMode shuffleMode, List<MediaItem> mediaItems, int startIndex) {
|
||||
// permutation[i] = j; => song j is on the i-th position in the permutated list
|
||||
List<int> permutation;
|
||||
final int length = mediaItems.length;
|
||||
|
||||
switch (shuffleMode) {
|
||||
case ShuffleMode.none:
|
||||
permutation = List<int>.generate(length, (i) => i);
|
||||
break;
|
||||
case ShuffleMode.standard:
|
||||
final tmp = List<int>.generate(length, (i) => i)
|
||||
..removeAt(startIndex)
|
||||
..shuffle();
|
||||
permutation = [startIndex] + tmp;
|
||||
break;
|
||||
case ShuffleMode.plus:
|
||||
permutation = generatePlusPermutation(mediaItems, startIndex);
|
||||
}
|
||||
|
||||
return permutation;
|
||||
}
|
||||
|
||||
List<MediaItem> getPermutatedSongs(
|
||||
List<MediaItem> songs, List<int> permutation) {
|
||||
return List.generate(
|
||||
permutation.length, (index) => songs[permutation[index]]);
|
||||
}
|
||||
|
||||
ConcatenatingAudioSource mediaItemsToAudioSource(List<MediaItem> mediaItems) {
|
||||
return ConcatenatingAudioSource(
|
||||
children: mediaItems
|
||||
.map((MediaItem m) => AudioSource.uri(Uri.file(m.id)))
|
||||
.toList());
|
||||
}
|
||||
|
||||
List<int> generatePlusPermutation(
|
||||
List<MediaItem> mediaItems, int startIndex) {
|
||||
final List<int> indices = [];
|
||||
for (var i = 0; i < mediaItems.length; i++) {
|
||||
if (i != startIndex && mediaItems[i].extras['blocked'] == 'false') {
|
||||
indices.add(i);
|
||||
}
|
||||
}
|
||||
indices.shuffle();
|
||||
return [startIndex] + indices;
|
||||
}
|
||||
}
|
11
lib/system/models/queue_item.dart
Normal file
11
lib/system/models/queue_item.dart
Normal file
|
@ -0,0 +1,11 @@
|
|||
import 'package:audio_service/audio_service.dart';
|
||||
|
||||
class QueueItem {
|
||||
QueueItem(this.mediaItem, {this.originalIndex, this.type = QueueItemType.standard});
|
||||
|
||||
final MediaItem mediaItem;
|
||||
final int originalIndex;
|
||||
final QueueItemType type;
|
||||
}
|
||||
|
||||
enum QueueItemType { standard, predecessor, successor }
|
|
@ -209,3 +209,9 @@ class SongModel extends Song {
|
|||
return [discNumber, trackNumber];
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: maybe move to another file
|
||||
extension SongModelExtension on MediaItem {
|
||||
String get previous => extras['previous'] as String;
|
||||
String get next => extras['next'] as String;
|
||||
}
|
||||
|
|
|
@ -5,7 +5,7 @@ import '../../domain/entities/playback_state.dart';
|
|||
import '../../domain/entities/shuffle_mode.dart';
|
||||
import '../../domain/entities/song.dart';
|
||||
import '../../domain/repositories/audio_repository.dart';
|
||||
import '../datasources/audio_manager_contract.dart';
|
||||
import '../audio/audio_manager_contract.dart';
|
||||
import '../models/song_model.dart';
|
||||
|
||||
class AudioRepositoryImpl implements AudioRepository {
|
||||
|
|
|
@ -3,13 +3,13 @@
|
|||
import 'package:mucke/injection_container.dart';
|
||||
import 'package:mucke/system/repositories/audio_repository_impl.dart';
|
||||
import 'package:mucke/system/repositories/music_data_repository_impl.dart';
|
||||
import 'package:mucke/system/datasources/audio_manager_contract.dart';
|
||||
import 'package:mucke/system/audio/audio_manager_contract.dart';
|
||||
import 'package:mucke/system/datasources/local_music_fetcher.dart';
|
||||
import 'package:mucke/system/datasources/audio_player_task.dart';
|
||||
import 'package:mucke/system/audio/audio_player_task.dart';
|
||||
import 'package:mucke/system/datasources/local_music_fetcher_contract.dart';
|
||||
import 'package:mucke/system/datasources/audio_manager.dart';
|
||||
import 'package:mucke/system/audio/audio_manager.dart';
|
||||
import 'package:mucke/system/datasources/moor_music_data_source.dart';
|
||||
import 'package:mucke/system/datasources/queue_manager.dart';
|
||||
import 'package:mucke/system/audio/queue_generator.dart';
|
||||
import 'package:mucke/system/datasources/music_data_source_contract.dart';
|
||||
import 'package:mucke/system/models/artist_model.dart';
|
||||
import 'package:mucke/system/models/album_model.dart';
|
||||
|
|
37
test/system/datasources/queue_manager_test.dart
Normal file
37
test/system/datasources/queue_manager_test.dart
Normal file
|
@ -0,0 +1,37 @@
|
|||
import 'package:audio_service/audio_service.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:mockito/mockito.dart';
|
||||
import 'package:mucke/system/datasources/music_data_source_contract.dart';
|
||||
import 'package:mucke/system/audio/queue_generator.dart';
|
||||
import 'package:mucke/system/models/album_model.dart';
|
||||
import 'package:mucke/system/models/song_model.dart';
|
||||
|
||||
import '../../test_constants.dart';
|
||||
|
||||
class MockMusicDataSource extends Mock implements MusicDataSource {}
|
||||
|
||||
void main() {
|
||||
QueueGenerator queueManager;
|
||||
MusicDataSource musicDataSource;
|
||||
List<MediaItem> mediaItems;
|
||||
|
||||
group('generatePlusPermutation', () {
|
||||
|
||||
setUp(() {
|
||||
musicDataSource = MockMusicDataSource();
|
||||
queueManager = QueueGenerator(musicDataSource);
|
||||
});
|
||||
|
||||
test(
|
||||
'should exclude blocked songs',
|
||||
() async {
|
||||
// arrange
|
||||
|
||||
// act
|
||||
|
||||
// assert
|
||||
|
||||
},
|
||||
);
|
||||
});
|
||||
}
|
|
@ -1,6 +1,6 @@
|
|||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:mockito/mockito.dart';
|
||||
import 'package:mucke/system/datasources/audio_manager_contract.dart';
|
||||
import 'package:mucke/system/audio/audio_manager_contract.dart';
|
||||
import 'package:mucke/system/models/song_model.dart';
|
||||
import 'package:mucke/system/repositories/audio_repository_impl.dart';
|
||||
|
||||
|
|
Loading…
Add table
Reference in a new issue