- implement file extension filters
- implement blacklist for files
This commit is contained in:
Moritz Weber 2023-01-03 14:09:29 +01:00
parent dd446090f3
commit a61d98716a
33 changed files with 781 additions and 60 deletions

View file

@ -5,8 +5,7 @@ const String PERSISTENT_LOOPMODE = 'PERSISTENT_LOOPMODE';
const String PERSISTENT_SHUFFLEMODE = 'PERSISTENT_SHUFFLEMODE';
const String PERSISTENT_PLAYABLE = 'PERSISTENT_PLAYABLE';
const String SETTING_EXCLUDE_SKIPPED_SONGS = 'SETTING_EXCLUDE_SKIPPED_SONGS_ENABLED';
const String SETTING_SKIP_THRESHOLD = 'SETTING_SKIP_THRESHOLD';
const String SETTING_ALLOWED_EXTENSIONS = 'SETTING_ALLOWED_EXTENSIONS';
const String ALBUM_OF_DAY = 'ALBUM_OF_DAY';
const String ARTIST_OF_DAY = 'ARTIST_OF_DAY';
const String ARTIST_OF_DAY = 'ARTIST_OF_DAY';

1
src/lib/defaults.dart Normal file
View file

@ -0,0 +1 @@
const String ALLOWED_FILE_EXTENSIONS = 'mp3,flac,wav,ogg';

View file

@ -14,9 +14,6 @@ class AudioPlayerActor {
.listen(_platformIntegrationRepository.handlePlaybackEvent);
_audioPlayerRepository.positionStream
.listen((duration) => _handlePosition(duration, _currentSong));
// TODO: this doesn't quite fit the design: listening to audioplayer events
_musicDataRepository.songUpdateStream.listen(_handleSongUpdate);
}
final AudioPlayerRepository _audioPlayerRepository;
@ -45,8 +42,4 @@ class AudioPlayerActor {
_musicDataRepository.resetSkipCount(updatedSong);
}
}
void _handleSongUpdate(Map<String, Song> songs) {
_audioPlayerRepository.updateSongs(songs);
}
}

View file

@ -0,0 +1,24 @@
import '../entities/song.dart';
import '../repositories/audio_player_repository.dart';
import '../repositories/music_data_repository.dart';
class MusicDataActor {
MusicDataActor(
this._audioPlayerRepository,
this._musicDataRepository,
) {
_musicDataRepository.songUpdateStream.listen(_handleSongUpdate);
_musicDataRepository.songRemovalStream.listen(_handleSongRemoval);
}
final AudioPlayerRepository _audioPlayerRepository;
final MusicDataRepository _musicDataRepository;
void _handleSongUpdate(Map<String, Song> songs) {
_audioPlayerRepository.updateSongs(songs);
}
void _handleSongRemoval(List<String> paths) {
_audioPlayerRepository.removeBlockedSongs(paths);
}
}

View file

@ -190,10 +190,10 @@ class DynamicQueue implements ManagedQueueInfo {
_queueSubject.add(_queue);
}
void removeQueueIndeces(List<int> indeces, bool permanent) {
_log.d('removeQueueIndeces');
void removeQueueIndices(List<int> indices, bool permanent) {
_log.d('removeQueueIndices');
for (final index in indeces..sort(((a, b) => -a.compareTo(b)))) {
for (final index in indices..sort(((a, b) => -a.compareTo(b)))) {
final queueItem = _queue[index];
if (permanent) {
@ -331,10 +331,13 @@ class DynamicQueue implements ManagedQueueInfo {
/// Update songs contained in queue. Return true if any song was changed.
bool updateSongs(Map<String, Song> songs) {
_log.d('updateSongs');
bool changed = false;
bool queueChanged = false;
bool availableSongsChanged = false;
for (int i = 0; i < _availableSongs.length; i++) {
if (songs.containsKey(_availableSongs[i].song.path)) {
availableSongsChanged = true;
final oldQueueItem = _availableSongs[i] as QueueItemModel;
final newQueueItem = (_availableSongs[i] as QueueItemModel).copyWith(
song: songs[_availableSongs[i].song.path]! as SongModel,
@ -344,19 +347,55 @@ class DynamicQueue implements ManagedQueueInfo {
final index = _queue.indexOf(oldQueueItem);
if (index > -1) {
_queue[index] = newQueueItem;
changed = true;
queueChanged = true;
}
}
}
// TODO: check if db is fine with that (when frequently changing songs)
// FIXME: nope, too many changes when skipping through the queue
if (changed) {
_queueSubject.add(_queue);
_availableSongsSubject.add(_availableSongs);
if (availableSongsChanged) _availableSongsSubject.add(_availableSongs);
if (queueChanged) _queueSubject.add(_queue);
return queueChanged;
}
bool removeSongs(Set<String> paths) {
_log.d('removeSongs');
bool queueChanged = false;
bool availableSongsChanged = false;
final List<int> removedOriginalIndices = [];
for (int i = 0; i < _availableSongs.length; i++) {
if (paths.contains(_availableSongs[i].song.path)) {
availableSongsChanged = true;
final queueItem = _availableSongs.removeAt(i);
removedOriginalIndices.add(queueItem.originalIndex);
final index = _queue.indexOf(queueItem);
if (index > -1) {
_queue.removeAt(index);
queueChanged = true;
}
}
}
removedOriginalIndices.sort();
// this needs to update originalIndex
for (int i = 0; i < _availableSongs.length; i++) {
final originalIndex = _availableSongs[i].originalIndex;
for (int j = 0; j < removedOriginalIndices.length; j++) {
if (originalIndex > removedOriginalIndices[j]) {
_availableSongs[i].originalIndex -= 1;
} else
break;
}
}
return changed;
if (availableSongsChanged) _availableSongsSubject.add(_availableSongs);
if (queueChanged) _queueSubject.add(_queue);
return queueChanged;
}
Future<List<QueueItem>> getQueueItemWithLinks(QueueItem queueItem, List<QueueItem> pool) async {

View file

@ -60,4 +60,5 @@ abstract class AudioPlayerRepository extends AudioPlayerInfoRepository {
/// Current scope: update song information in queue, don't affect playback/queue.
Future<void> updateSongs(Map<String, Song> songs);
Future<void> removeBlockedSongs(List<String> paths);
}

View file

@ -12,6 +12,7 @@ import '../entities/song.dart';
abstract class MusicDataInfoRepository {
Stream<Map<String, Song>> get songUpdateStream;
Stream<List<String>> get songRemovalStream;
Future<Song> getSongByPath(String path);
Stream<Song> getSongStream(String path);
@ -51,6 +52,8 @@ abstract class MusicDataInfoRepository {
Future<List<Song>> searchSongs(String searchText, {int? limit});
Future<List<SmartList>> searchSmartLists(String searchText, {int? limit});
Future<List<Playlist>> searchPlaylists(String searchText, {int? limit});
ValueStream<Set<String>> get blockedFilesStream;
}
abstract class MusicDataRepository extends MusicDataInfoRepository {
@ -91,4 +94,7 @@ abstract class MusicDataRepository extends MusicDataInfoRepository {
});
Future<void> updateSmartList(SmartList smartList);
Future<void> removeSmartList(SmartList smartList);
Future<void> addBlockedFiles(List<String> paths);
Future<void> removeBlockedFiles(List<String> paths);
}

View file

@ -2,11 +2,13 @@ import 'package:rxdart/rxdart.dart';
abstract class SettingsInfoRepository {
Stream<List<String>> get libraryFoldersStream;
ValueStream<String> get fileExtensionsStream;
ValueStream<bool> get manageExternalStorageGranted;
}
abstract class SettingsRepository extends SettingsInfoRepository {
Future<void> addLibraryFolder(String? path);
Future<void> removeLibraryFolder(String? path);
Future<void> setFileExtension(String extensions);
Future<void> setManageExternalStorageGranted(bool granted);
}

View file

@ -4,6 +4,7 @@ import 'package:just_audio/just_audio.dart';
import 'package:on_audio_query/on_audio_query.dart';
import 'domain/actors/audio_player_actor.dart';
import 'domain/actors/music_data_actor.dart';
import 'domain/actors/persistence_actor.dart';
import 'domain/actors/platform_integration_actor.dart';
import 'domain/entities/album.dart';
@ -118,6 +119,7 @@ Future<void> setupGetIt() async {
getIt.registerLazySingleton<SettingsStore>(
() => SettingsStore(
settingsRepository: getIt(),
musicDataRepository: getIt(),
),
);
getIt.registerLazySingleton<QueuePageStore>(
@ -361,6 +363,13 @@ Future<void> setupGetIt() async {
),
);
getIt.registerSingleton<MusicDataActor>(
MusicDataActor(
getIt(),
getIt(),
),
);
getIt.registerSingleton<PersistenceActor>(
PersistenceActor(
getIt(),

View file

@ -7,6 +7,7 @@ import '../../domain/entities/song.dart';
import '../state/album_page_store.dart';
import '../state/audio_store.dart';
import '../state/music_data_store.dart';
import '../state/settings_store.dart';
import '../theming.dart';
import '../widgets/album_sliver_appbar.dart';
import '../widgets/bottom_sheet/add_to_playlist.dart';
@ -67,7 +68,7 @@ class _AlbumDetailsPageState extends State<AlbumDetailsPage> {
builder: (context) {
final bool isMultiSelectEnabled = store.selection.isMultiSelectEnabled;
final List<bool> isSelected = store.selection.isSelected.toList();
return SliverList(
delegate: SliverChildListDelegate(
[
@ -75,7 +76,8 @@ class _AlbumDetailsPageState extends State<AlbumDetailsPage> {
if (songsByDisc.length > 1)
ListTile(
title: Text('Disc ${d + 1}', style: TEXT_HEADER),
leading: const SizedBox(width: 40, child: Icon(Icons.album_rounded)),
leading:
const SizedBox(width: 40, child: Icon(Icons.album_rounded)),
contentPadding: const EdgeInsets.only(left: HORIZONTAL_PADDING),
),
if (songsByDisc.length > 1)
@ -151,6 +153,7 @@ class _AlbumDetailsPageState extends State<AlbumDetailsPage> {
Future<void> _openMultiselectMenu(BuildContext context) async {
final audioStore = GetIt.I<AudioStore>();
final musicDataStore = GetIt.I<MusicDataStore>();
final settingsStore = GetIt.I<SettingsStore>();
showModalBottomSheet(
context: context,
@ -201,6 +204,14 @@ class _AlbumDetailsPageState extends State<AlbumDetailsPage> {
},
),
AddToPlaylistTile(songs: songs, musicDataStore: musicDataStore),
ListTile(
title: const Text('Block from library'),
leading: const Icon(Icons.block),
onTap: () {
settingsStore.addBlockedFiles(songs.map((e) => e.path).toList());
Navigator.of(context, rootNavigator: true).pop();
},
),
],
);
}),

View file

@ -0,0 +1,60 @@
import 'package:flutter/material.dart';
import 'package:flutter_mobx/flutter_mobx.dart';
import 'package:get_it/get_it.dart';
import '../state/navigation_store.dart';
import '../state/settings_store.dart';
import '../theming.dart';
class BlockedFilesPage extends StatelessWidget {
const BlockedFilesPage({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
final SettingsStore settingsStore = GetIt.I<SettingsStore>();
final NavigationStore navStore = GetIt.I<NavigationStore>();
return SafeArea(
child: Scaffold(
appBar: AppBar(
title: const Text(
'Blocked Files',
style: TEXT_HEADER,
),
centerTitle: true,
leading: IconButton(
icon: const Icon(Icons.chevron_left_rounded),
onPressed: () => navStore.pop(context),
),
titleSpacing: 0.0,
),
body: Observer(
builder: (context) {
final paths = settingsStore.blockedFilesStream.value?.toList() ?? [];
return ListView.separated(
itemCount: paths.length,
itemBuilder: (_, int index) {
final String path = paths[index];
final split = path.split('/');
return ListTile(
title: Text(split.last),
subtitle: Text(
split.sublist(0, split.length - 1).join('/'),
style: TEXT_SMALL_SUBTITLE,
),
trailing: IconButton(
icon: const Icon(Icons.delete_rounded),
onPressed: () => settingsStore.removeBlockedFiles([path]),
),
);
},
separatorBuilder: (BuildContext context, int index) => const SizedBox(
height: 4.0,
),
);
},
),
),
);
}
}

View file

@ -58,6 +58,7 @@ class HomeSettingsPage extends StatelessWidget {
'Home Customization',
style: TEXT_HEADER,
),
centerTitle: true,
leading: IconButton(
icon: const Icon(Icons.chevron_left_rounded),
onPressed: () => navStore.pop(context),

View file

@ -23,6 +23,7 @@ class LibraryFoldersPage extends StatelessWidget {
'Library Folders',
style: TEXT_HEADER,
),
centerTitle: true,
leading: IconButton(
icon: const Icon(Icons.chevron_left_rounded),
onPressed: () => navStore.pop(context),

View file

@ -2,13 +2,14 @@ import 'package:flutter/material.dart';
import 'package:flutter_mobx/flutter_mobx.dart';
import 'package:get_it/get_it.dart';
import '../../defaults.dart';
import '../state/music_data_store.dart';
import '../state/navigation_store.dart';
import '../state/settings_store.dart';
import '../theming.dart';
import 'blocked_files_page.dart';
import 'library_folders_page.dart';
class SettingsPage extends StatelessWidget {
const SettingsPage({Key? key}) : super(key: key);
@ -18,6 +19,8 @@ class SettingsPage extends StatelessWidget {
final SettingsStore settingsStore = GetIt.I<SettingsStore>();
final NavigationStore navStore = GetIt.I<NavigationStore>();
final TextEditingController _textController = TextEditingController();
return SafeArea(
child: Scaffold(
appBar: AppBar(
@ -29,6 +32,7 @@ class SettingsPage extends StatelessWidget {
icon: const Icon(Icons.chevron_left_rounded),
onPressed: () => navStore.pop(context),
),
centerTitle: true,
),
body: ListView(
children: [
@ -68,13 +72,88 @@ class SettingsPage extends StatelessWidget {
const Divider(
height: 4.0,
),
const ListTile(
title: Text('Allowed file extensions'),
subtitle: Text('Comma-separated list. Lower- or uppercase does not matter.'),
),
Padding(
padding: const EdgeInsets.only(
left: HORIZONTAL_PADDING,
right: HORIZONTAL_PADDING,
bottom: 16.0,
),
child: Observer(
builder: (context) {
final text = settingsStore.fileExtensionsStream.value;
if (text == null) return Container();
if (_textController.text == '') _textController.text = text;
return Row(
children: [
Expanded(
child: TextFormField(
controller: _textController,
onChanged: (value) => settingsStore.setFileExtensions(value),
textAlign: TextAlign.start,
decoration: const InputDecoration(
filled: true,
fillColor: DARK35,
border: OutlineInputBorder(
borderSide: BorderSide.none,
borderRadius: BorderRadius.all(Radius.circular(4.0)),
),
errorStyle: TextStyle(height: 0, fontSize: 0),
contentPadding: EdgeInsets.only(
top: 0.0,
bottom: 0.0,
left: 6.0,
right: 6.0,
),
),
),
),
const SizedBox(width: 8.0),
IconButton(
icon: const Icon(Icons.settings_backup_restore_rounded),
onPressed: () {
_textController.text = ALLOWED_FILE_EXTENSIONS;
settingsStore.setFileExtensions(ALLOWED_FILE_EXTENSIONS);
},
),
],
);
},
),
),
const Divider(
height: 4.0,
),
Observer(
builder: (_) {
final blockedFiles = settingsStore.numBlockedFiles;
return ListTile(
title: const Text('Manage blocked files'),
subtitle: Text('Number of currently blocked files: $blockedFiles'),
trailing: const Icon(Icons.chevron_right_rounded),
onTap: () => navStore.push(
context,
MaterialPageRoute(
builder: (BuildContext context) => const BlockedFilesPage(),
),
),
);},
),
const Divider(
height: 4.0,
),
Observer(
builder: (context) => SwitchListTile(
value: settingsStore.manageExternalStorageGranted.value ?? false,
onChanged: settingsStore.setManageExternalStorageGranted,
title: const Text('Grant permission to manage all files'),
subtitle: const Text(
'This permission can improve library updates in some cases. Revoking the permission will result in a restart of mucke.',
'This permission can improve library updates in some cases. Revoking the permission will result in a restart of the app.',
style: TEXT_SMALL_SUBTITLE,
),
isThreeLine: true,
@ -98,13 +177,16 @@ class SettingsSection extends StatelessWidget {
padding: const EdgeInsets.only(
left: 16.0,
right: 16.0,
top: 16.0,
top: 24.0,
bottom: 4.0,
),
child: Text(
text,
style: const TextStyle(
fontSize: 18.0,
style: TEXT_HEADER.underlined(
textColor: Colors.white,
underlineColor: LIGHT1,
thickness: 4,
distance: 8,
),
),
);

View file

@ -1,5 +1,6 @@
import 'package:mobx/mobx.dart';
import '../../domain/repositories/music_data_repository.dart';
import '../../domain/repositories/settings_repository.dart';
part 'settings_store.g.dart';
@ -7,15 +8,18 @@ part 'settings_store.g.dart';
class SettingsStore extends _SettingsStore with _$SettingsStore {
SettingsStore({
required SettingsRepository settingsRepository,
}) : super(settingsRepository);
required MusicDataRepository musicDataRepository,
}) : super(settingsRepository, musicDataRepository);
}
abstract class _SettingsStore with Store {
_SettingsStore(
this._settingsRepository,
this._musicDataRepository,
);
final SettingsRepository _settingsRepository;
final MusicDataRepository _musicDataRepository;
@observable
late ObservableStream<List<String>> libraryFoldersStream =
@ -27,6 +31,17 @@ abstract class _SettingsStore with Store {
initialValue: _settingsRepository.manageExternalStorageGranted.valueOrNull ?? false,
);
@observable
late ObservableStream<String> fileExtensionsStream =
_settingsRepository.fileExtensionsStream.asObservable(initialValue: '');
@observable
late ObservableStream<Set<String>> blockedFilesStream =
_musicDataRepository.blockedFilesStream.asObservable(initialValue: {});
@computed
int get numBlockedFiles => blockedFilesStream.value!.length;
Future<void> addLibraryFolder(String? path) async {
await _settingsRepository.addLibraryFolder(path);
}
@ -39,5 +54,17 @@ abstract class _SettingsStore with Store {
await _settingsRepository.setManageExternalStorageGranted(granted);
}
Future<void> setFileExtensions(String? extensions) async {
if (extensions != null) await _settingsRepository.setFileExtension(extensions);
}
Future<void> addBlockedFiles(List<String> paths) async {
await _musicDataRepository.addBlockedFiles(paths);
}
Future<void> removeBlockedFiles(List<String> paths) async {
await _musicDataRepository.removeBlockedFiles(paths);
}
void dispose() {}
}

View file

@ -43,11 +43,46 @@ mixin _$SettingsStore on _SettingsStore, Store {
});
}
late final _$fileExtensionsStreamAtom =
Atom(name: '_SettingsStore.fileExtensionsStream', context: context);
@override
ObservableStream<String> get fileExtensionsStream {
_$fileExtensionsStreamAtom.reportRead();
return super.fileExtensionsStream;
}
@override
set fileExtensionsStream(ObservableStream<String> value) {
_$fileExtensionsStreamAtom.reportWrite(value, super.fileExtensionsStream,
() {
super.fileExtensionsStream = value;
});
}
late final _$blockedFilesStreamAtom =
Atom(name: '_SettingsStore.blockedFilesStream', context: context);
@override
ObservableStream<Set<String>> get blockedFilesStream {
_$blockedFilesStreamAtom.reportRead();
return super.blockedFilesStream;
}
@override
set blockedFilesStream(ObservableStream<Set<String>> value) {
_$blockedFilesStreamAtom.reportWrite(value, super.blockedFilesStream, () {
super.blockedFilesStream = value;
});
}
@override
String toString() {
return '''
libraryFoldersStream: ${libraryFoldersStream},
manageExternalStorageGranted: ${manageExternalStorageGranted}
manageExternalStorageGranted: ${manageExternalStorageGranted},
fileExtensionsStream: ${fileExtensionsStream},
blockedFilesStream: ${blockedFilesStream}
''';
}
}

View file

@ -10,6 +10,7 @@ import '../pages/artist_details_page.dart';
import '../state/audio_store.dart';
import '../state/music_data_store.dart';
import '../state/navigation_store.dart';
import '../state/settings_store.dart';
import '../state/song_store.dart';
import '../theming.dart';
import '../utils.dart' as utils;
@ -61,6 +62,7 @@ class _SongBottomSheetState extends State<SongBottomSheet> {
final AudioStore audioStore = GetIt.I<AudioStore>();
final MusicDataStore musicDataStore = GetIt.I<MusicDataStore>();
final NavigationStore navStore = GetIt.I<NavigationStore>();
final SettingsStore settingsStore = GetIt.I<SettingsStore>();
int optionIndex = 0;
@ -276,6 +278,14 @@ class _SongBottomSheetState extends State<SongBottomSheet> {
),
],
AddToPlaylistTile(songs: [song], musicDataStore: musicDataStore),
ListTile(
title: const Text('Block from library'),
leading: const Icon(Icons.block),
onTap: () {
settingsStore.addBlockedFiles([song.path]);
Navigator.of(context, rootNavigator: true).pop();
},
),
];
return MyBottomSheet(widgets: widgets);

View file

@ -30,6 +30,10 @@ class LocalMusicFetcherImpl implements LocalMusicFetcher {
final musicDirectories = await _settingsDataSource.libraryFoldersStream.first;
final libDirs = musicDirectories.map((e) => Directory(e));
final extString = await _settingsDataSource.fileExtensionsStream.first;
final allowedExtensions = getExtensionSet(extString);
final blockedPaths = await _musicDataSource.blockedFilesStream.first;
final List<aq.SongModel> aqSongs = [];
for (final libDir in libDirs) {
@ -56,7 +60,9 @@ class LocalMusicFetcherImpl implements LocalMusicFetcher {
final Directory dir = await getApplicationSupportDirectory();
for (final aqSong in aqSongs.toSet()) {
if (aqSong.data.toLowerCase().endsWith('.wma')) continue;
if (!allowedExtensions.contains(aqSong.fileExtension.toLowerCase())) continue;
if (blockedPaths.contains(aqSong.data)) continue;
final data = aqSong.getMap;
// changed includes the creation time
// => also update, when the file was created later (and wasn't really changed)
@ -162,4 +168,11 @@ class LocalMusicFetcherImpl implements LocalMusicFetcher {
'ARTISTS': artistSet.toList(),
};
}
Set<String> getExtensionSet(String extString) {
List<String> extensions = extString.toLowerCase().split(',');
extensions = extensions.map((e) => e.trim()).toList();
extensions = extensions.whereNot((element) => element.isEmpty).toList();
return Set<String>.from(extensions);
}
}

View file

@ -4,6 +4,7 @@ import 'package:collection/collection.dart';
import 'package:drift/drift.dart';
import '../../../constants.dart';
import '../../../domain/entities/playable.dart';
import '../../models/album_model.dart';
import '../../models/artist_model.dart';
import '../../models/song_model.dart';
@ -12,8 +13,17 @@ import '../music_data_source_contract.dart';
part 'music_data_dao.g.dart';
@DriftAccessor(
tables: [Albums, Artists, Songs, Playlists, PlaylistEntries, KeyValueEntries])
@DriftAccessor(tables: [
Albums,
Artists,
Songs,
Playlists,
PlaylistEntries,
KeyValueEntries,
BlockedFiles,
HistoryEntries,
SmartListArtists
])
class MusicDataDao extends DatabaseAccessor<MoorDatabase>
with _$MusicDataDaoMixin
implements MusicDataSource {
@ -98,6 +108,8 @@ class MusicDataDao extends DatabaseAccessor<MoorDatabase>
@override
Future<void> insertSongs(List<SongModel> songModels) async {
final List<SongModel> deletedSongs = [];
transaction(() async {
await update(songs).write(const SongsCompanion(present: Value(false)));
@ -107,9 +119,25 @@ class MusicDataDao extends DatabaseAccessor<MoorDatabase>
songModels.map((e) => e.toMoorInsert()).toList(),
);
});
await (delete(songs)..where((tbl) => tbl.present.equals(false))).go();
});
deletedSongs.addAll(await (select(songs)..where((tbl) => tbl.present.equals(false))).get().then(
(moorSongList) => moorSongList.map((moorSong) => SongModel.fromMoor(moorSong)).toList()));
await (delete(songs)..where((tbl) => tbl.present.equals(false))).go();
final Set<String> artistSet = {};
final Set<int> albumSet = {};
for (final song in deletedSongs) {
artistSet.add(song.artist);
albumSet.add(song.albumId);
}
// delete empty albums
albumSet.forEach(_deleteAlbumIfEmpty);
// delete artists without albums
artistSet.forEach(_deleteArtistIfEmpty);
}
@override
@ -319,4 +347,75 @@ class MusicDataDao extends DatabaseAccessor<MoorDatabase>
.getSingleOrNull()
.then((v) => v?.id);
}
@override
Future<void> addBlockedFiles(List<String> paths) async {
final Set<String> artistSet = {};
final Set<int> albumSet = {};
for (final path in paths) {
final song = await getSongByPath(path);
artistSet.add(song!.artist);
albumSet.add(song.albumId);
}
await batch((batch) {
batch.insertAllOnConflictUpdate(
blockedFiles,
paths.map((e) => BlockedFilesCompanion(path: Value(e))),
);
// delete songs
batch.deleteWhere<$SongsTable, dynamic>(songs, (tbl) => tbl.path.isIn(paths));
});
// delete empty albums
albumSet.forEach(_deleteAlbumIfEmpty);
// delete artists without albums
artistSet.forEach(_deleteArtistIfEmpty);
}
// Delete empty albums and all their database appearances.
Future<void> _deleteAlbumIfEmpty(int albumId) async {
final aSongs = await (select(songs)..where((tbl) => tbl.albumId.equals(albumId))).get();
if (aSongs.isEmpty) {
await (delete(albums)..where((tbl) => tbl.id.equals(albumId))).go();
// delete history entries with this album
await (delete(historyEntries)
..where((tbl) =>
tbl.type.equals(PlayableType.album.toString()) &
tbl.identifier.equals(albumId.toString())))
.go();
}
}
// Delete empty artists and all their database appearances.
Future<void> _deleteArtistIfEmpty(String name) async {
final aAlbums = await (select(albums)..where((tbl) => tbl.artist.equals(name))).get();
if (aAlbums.isEmpty) {
final emptyArtists = await (select(artists)..where((tbl) => tbl.name.equals(name))).get();
await (delete(artists)..where((tbl) => tbl.name.equals(name))).go();
await (delete(smartListArtists)..where((tbl) => tbl.artistName.equals(name))).go();
for (final emptyArtist in emptyArtists) {
(delete(historyEntries)
..where((tbl) =>
tbl.type.equals(PlayableType.artist.toString()) &
tbl.identifier.equals(emptyArtist.id.toString())))
.go();
}
}
}
@override
Stream<Set<String>> get blockedFilesStream =>
select(blockedFiles).watch().map((value) => value.map((e) => e.path).toSet());
@override
Future<void> removeBlockedFiles(List<String> paths) async {
await batch((batch) {
batch.deleteWhere<$BlockedFilesTable, dynamic>(blockedFiles, (tbl) => tbl.path.isIn(paths));
});
}
}

View file

@ -13,4 +13,8 @@ mixin _$MusicDataDaoMixin on DatabaseAccessor<MoorDatabase> {
$PlaylistsTable get playlists => attachedDatabase.playlists;
$PlaylistEntriesTable get playlistEntries => attachedDatabase.playlistEntries;
$KeyValueEntriesTable get keyValueEntries => attachedDatabase.keyValueEntries;
$BlockedFilesTable get blockedFiles => attachedDatabase.blockedFiles;
$HistoryEntriesTable get historyEntries => attachedDatabase.historyEntries;
$SmartListArtistsTable get smartListArtists =>
attachedDatabase.smartListArtists;
}

View file

@ -166,7 +166,8 @@ class PersistentStateDao extends DatabaseAccessor<MoorDatabase>
result = AllSongs();
break;
case PlayableType.album:
result = await (select(albums)..where((tbl) => tbl.id.equals(int.parse(data['id'] as String))))
result = await (select(albums)
..where((tbl) => tbl.id.equals(int.parse(data['id'] as String))))
.getSingleOrNull()
.then((value) => value == null ? null : AlbumModel.fromMoor(value));
break;
@ -178,19 +179,25 @@ class PersistentStateDao extends DatabaseAccessor<MoorDatabase>
case PlayableType.playlist:
final plId = int.parse(data['id'] as String);
// TODO: need proper getter for this
final moorPl = await (select(playlists)..where((tbl) => tbl.id.equals(plId))).getSingle();
result = PlaylistModel.fromMoor(moorPl);
final moorPl =
await (select(playlists)..where((tbl) => tbl.id.equals(plId))).getSingleOrNull();
result = moorPl == null ? null : PlaylistModel.fromMoor(moorPl);
break;
case PlayableType.smartlist:
final slId = int.parse(data['id'] as String);
final sl = await (select(smartLists)..where((tbl) => tbl.id.equals(slId))).getSingle();
final sl =
await (select(smartLists)..where((tbl) => tbl.id.equals(slId))).getSingleOrNull();
final slArtists =
await ((select(smartListArtists)..where((tbl) => tbl.smartListId.equals(slId))).join(
[innerJoin(artists, artists.name.equalsExp(smartListArtists.artistName))],
)).map((p0) => p0.readTable(artists)).get();
if (sl == null)
result = null;
else {
final slArtists =
await ((select(smartListArtists)..where((tbl) => tbl.smartListId.equals(slId))).join(
[innerJoin(artists, artists.name.equalsExp(smartListArtists.artistName))],
)).map((p0) => p0.readTable(artists)).get();
result = SmartListModel.fromMoor(sl, slArtists);
result = SmartListModel.fromMoor(sl, slArtists);
}
break;
case PlayableType.search:
result = SearchQuery(data['id'] as String);

View file

@ -1,6 +1,7 @@
import 'package:drift/drift.dart';
import 'package:rxdart/rxdart.dart';
import '../../../domain/entities/playable.dart';
import '../../../domain/entities/shuffle_mode.dart';
import '../../../domain/entities/smart_list.dart' as sl;
import '../../models/playlist_model.dart';
@ -11,8 +12,16 @@ import '../playlist_data_source.dart';
part 'playlist_dao.g.dart';
@DriftAccessor(
tables: [Albums, Artists, Songs, Playlists, PlaylistEntries, SmartLists, SmartListArtists])
@DriftAccessor(tables: [
Albums,
Artists,
Songs,
Playlists,
PlaylistEntries,
SmartLists,
SmartListArtists,
HistoryEntries
])
class PlaylistDao extends DatabaseAccessor<MoorDatabase>
with _$PlaylistDaoMixin
implements PlaylistDataSource {
@ -38,7 +47,7 @@ class PlaylistDao extends DatabaseAccessor<MoorDatabase>
}
await (update(playlists)..where((tbl) => tbl.id.equals(playlist.id)))
.write(PlaylistsCompanion(timeChanged: Value(DateTime.now())));
.write(PlaylistsCompanion(timeChanged: Value(DateTime.now())));
await batch((batch) {
batch.insertAll(playlistEntries, entries);
@ -93,6 +102,11 @@ class PlaylistDao extends DatabaseAccessor<MoorDatabase>
Future<void> removePlaylist(PlaylistModel playlist) async {
await (delete(playlists)..where((tbl) => tbl.id.equals(playlist.id))).go();
await (delete(playlistEntries)..where((tbl) => tbl.playlistId.equals(playlist.id))).go();
await (delete(historyEntries)
..where((tbl) =>
tbl.type.equals(PlayableType.playlist.toString()) &
tbl.identifier.equals(playlist.id.toString())))
.go();
}
@override
@ -126,7 +140,7 @@ class PlaylistDao extends DatabaseAccessor<MoorDatabase>
.write(PlaylistEntriesCompanion(position: Value(newIndex)));
await (update(playlists)..where((tbl) => tbl.id.equals(playlistId)))
.write(PlaylistsCompanion(timeChanged: Value(DateTime.now())));
.write(PlaylistsCompanion(timeChanged: Value(DateTime.now())));
});
}
}
@ -198,6 +212,11 @@ class PlaylistDao extends DatabaseAccessor<MoorDatabase>
await (delete(smartLists)..where((tbl) => tbl.id.equals(smartListModel.id))).go();
await (delete(smartListArtists)..where((tbl) => tbl.smartListId.equals(smartListModel.id)))
.go();
await (delete(historyEntries)
..where((tbl) =>
tbl.type.equals(PlayableType.playlist.toString()) &
tbl.identifier.equals(smartListModel.id.toString())))
.go();
}
@override
@ -353,6 +372,56 @@ class PlaylistDao extends DatabaseAccessor<MoorDatabase>
}
return result;
}
@override
Future<void> removeBlockedSongs(List<String> paths) async {
final Map<int, List<int>> playlistIndexMap = {};
// gather the playlist entries to remove
for (final path in paths) {
final entries =
await (select(playlistEntries)..where((tbl) => tbl.songPath.equals(path))).get();
for (final entry in entries) {
if (playlistIndexMap.containsKey(entry.playlistId)) {
playlistIndexMap[entry.playlistId]!.add(entry.position);
} else {
playlistIndexMap[entry.playlistId] = [entry.position];
}
}
}
for (final playlistId in playlistIndexMap.keys) {
final indices = List<int>.from(playlistIndexMap[playlistId]!);
indices.sort();
final entries =
await (select(playlistEntries)..where((tbl) => tbl.playlistId.equals(playlistId))).get();
int removedCount = 0;
transaction(() async {
for (final index
in List.generate(entries.length - indices.first, (i) => i + indices.first)) {
if (indices.isNotEmpty && index == indices.first) {
// remove the entry
await (delete(playlistEntries)
..where((tbl) => tbl.position.equals(index) & tbl.playlistId.equals(playlistId)))
.go();
removedCount += 1;
indices.removeAt(0);
} else {
// adapt entry position
await (update(playlistEntries)
..where((tbl) => tbl.position.equals(index) & tbl.playlistId.equals(playlistId)))
.write(PlaylistEntriesCompanion(position: Value(index - removedCount)));
}
await (update(playlists)..where((tbl) => tbl.id.equals(playlistId)))
.write(PlaylistsCompanion(timeChanged: Value(DateTime.now())));
}
});
}
}
}
List<OrderingTerm Function($SongsTable)> _generateOrderingTerms(sl.OrderBy orderBy) {

View file

@ -15,4 +15,5 @@ mixin _$PlaylistDaoMixin on DatabaseAccessor<MoorDatabase> {
$SmartListsTable get smartLists => attachedDatabase.smartLists;
$SmartListArtistsTable get smartListArtists =>
attachedDatabase.smartListArtists;
$HistoryEntriesTable get historyEntries => attachedDatabase.historyEntries;
}

View file

@ -1,11 +1,12 @@
import 'package:drift/drift.dart';
import '../../../constants.dart';
import '../moor_database.dart';
import '../settings_data_source.dart';
part 'settings_dao.g.dart';
@DriftAccessor(tables: [LibraryFolders, KeyValueEntries])
@DriftAccessor(tables: [LibraryFolders, KeyValueEntries, BlockedFiles])
class SettingsDao extends DatabaseAccessor<MoorDatabase>
with _$SettingsDaoMixin
implements SettingsDataSource {
@ -24,4 +25,17 @@ class SettingsDao extends DatabaseAccessor<MoorDatabase>
Future<void> addLibraryFolder(String path) async {
await into(libraryFolders).insert(LibraryFoldersCompanion(path: Value(path)));
}
@override
Stream<String> get fileExtensionsStream =>
(select(keyValueEntries)..where((tbl) => tbl.key.equals(SETTING_ALLOWED_EXTENSIONS)))
.watchSingle()
.map((event) => event.value);
@override
Future<void> setFileExtension(String extensions) async {
print(extensions);
await (update(keyValueEntries)..where((tbl) => tbl.key.equals(SETTING_ALLOWED_EXTENSIONS)))
.write(KeyValueEntriesCompanion(value: Value(extensions)));
}
}

View file

@ -9,4 +9,5 @@ part of 'settings_dao.dart';
mixin _$SettingsDaoMixin on DatabaseAccessor<MoorDatabase> {
$LibraryFoldersTable get libraryFolders => attachedDatabase.libraryFolders;
$KeyValueEntriesTable get keyValueEntries => attachedDatabase.keyValueEntries;
$BlockedFilesTable get blockedFiles => attachedDatabase.blockedFiles;
}

View file

@ -9,6 +9,7 @@ import 'package:path/path.dart' as p;
import 'package:path_provider/path_provider.dart';
import '../../constants.dart';
import '../../defaults.dart';
import '../../domain/entities/playable.dart';
import 'moor/history_dao.dart';
import 'moor/home_widget_dao.dart';
@ -179,6 +180,13 @@ class HistoryEntries extends Table {
TextColumn get identifier => text()();
}
class BlockedFiles extends Table {
TextColumn get path => text()();
@override
Set<Column> get primaryKey => {path};
}
@DriftDatabase(
tables: [
Albums,
@ -194,6 +202,7 @@ class HistoryEntries extends Table {
KeyValueEntries,
HomeWidgets,
HistoryEntries,
BlockedFiles,
],
daos: [
PersistentStateDao,
@ -215,7 +224,7 @@ class MoorDatabase extends _$MoorDatabase {
MoorDatabase.connect(DatabaseConnection connection) : super.connect(connection);
@override
int get schemaVersion => 10;
int get schemaVersion => 11;
@override
MigrationStrategy get migration => MigrationStrategy(
@ -230,6 +239,9 @@ class MoorDatabase extends _$MoorDatabase {
await into(keyValueEntries).insert(
const KeyValueEntriesCompanion(key: Value(PERSISTENT_SHUFFLEMODE), value: Value('0')),
);
await into(keyValueEntries).insert(
const KeyValueEntriesCompanion(key: Value(SETTING_ALLOWED_EXTENSIONS), value: Value(ALLOWED_FILE_EXTENSIONS)),
);
final Map initialPlayable = {
'id': '',
'type': PlayableType.all.toString(),
@ -376,6 +388,12 @@ class MoorDatabase extends _$MoorDatabase {
if (from < 10) {
await m.createTable(historyEntries);
}
if (from < 11) {
await m.createTable(blockedFiles);
await into(keyValueEntries).insert(
const KeyValueEntriesCompanion(key: Value(SETTING_ALLOWED_EXTENSIONS), value: Value(ALLOWED_FILE_EXTENSIONS)),
);
}
},
);
}

View file

@ -4353,6 +4353,148 @@ class $HistoryEntriesTable extends HistoryEntries
}
}
class BlockedFile extends DataClass implements Insertable<BlockedFile> {
final String path;
BlockedFile({required this.path});
factory BlockedFile.fromData(Map<String, dynamic> data, {String? prefix}) {
final effectivePrefix = prefix ?? '';
return BlockedFile(
path: const StringType()
.mapFromDatabaseResponse(data['${effectivePrefix}path'])!,
);
}
@override
Map<String, Expression> toColumns(bool nullToAbsent) {
final map = <String, Expression>{};
map['path'] = Variable<String>(path);
return map;
}
BlockedFilesCompanion toCompanion(bool nullToAbsent) {
return BlockedFilesCompanion(
path: Value(path),
);
}
factory BlockedFile.fromJson(Map<String, dynamic> json,
{ValueSerializer? serializer}) {
serializer ??= driftRuntimeOptions.defaultSerializer;
return BlockedFile(
path: serializer.fromJson<String>(json['path']),
);
}
@override
Map<String, dynamic> toJson({ValueSerializer? serializer}) {
serializer ??= driftRuntimeOptions.defaultSerializer;
return <String, dynamic>{
'path': serializer.toJson<String>(path),
};
}
BlockedFile copyWith({String? path}) => BlockedFile(
path: path ?? this.path,
);
@override
String toString() {
return (StringBuffer('BlockedFile(')
..write('path: $path')
..write(')'))
.toString();
}
@override
int get hashCode => path.hashCode;
@override
bool operator ==(Object other) =>
identical(this, other) ||
(other is BlockedFile && other.path == this.path);
}
class BlockedFilesCompanion extends UpdateCompanion<BlockedFile> {
final Value<String> path;
const BlockedFilesCompanion({
this.path = const Value.absent(),
});
BlockedFilesCompanion.insert({
required String path,
}) : path = Value(path);
static Insertable<BlockedFile> custom({
Expression<String>? path,
}) {
return RawValuesInsertable({
if (path != null) 'path': path,
});
}
BlockedFilesCompanion copyWith({Value<String>? path}) {
return BlockedFilesCompanion(
path: path ?? this.path,
);
}
@override
Map<String, Expression> toColumns(bool nullToAbsent) {
final map = <String, Expression>{};
if (path.present) {
map['path'] = Variable<String>(path.value);
}
return map;
}
@override
String toString() {
return (StringBuffer('BlockedFilesCompanion(')
..write('path: $path')
..write(')'))
.toString();
}
}
class $BlockedFilesTable extends BlockedFiles
with TableInfo<$BlockedFilesTable, BlockedFile> {
@override
final GeneratedDatabase attachedDatabase;
final String? _alias;
$BlockedFilesTable(this.attachedDatabase, [this._alias]);
final VerificationMeta _pathMeta = const VerificationMeta('path');
@override
late final GeneratedColumn<String?> path = GeneratedColumn<String?>(
'path', aliasedName, false,
type: const StringType(), requiredDuringInsert: true);
@override
List<GeneratedColumn> get $columns => [path];
@override
String get aliasedName => _alias ?? 'blocked_files';
@override
String get actualTableName => 'blocked_files';
@override
VerificationContext validateIntegrity(Insertable<BlockedFile> instance,
{bool isInserting = false}) {
final context = VerificationContext();
final data = instance.toColumns(true);
if (data.containsKey('path')) {
context.handle(
_pathMeta, path.isAcceptableOrUnknown(data['path']!, _pathMeta));
} else if (isInserting) {
context.missing(_pathMeta);
}
return context;
}
@override
Set<GeneratedColumn> get $primaryKey => {path};
@override
BlockedFile map(Map<String, dynamic> data, {String? tablePrefix}) {
return BlockedFile.fromData(data,
prefix: tablePrefix != null ? '$tablePrefix.' : null);
}
@override
$BlockedFilesTable createAlias(String alias) {
return $BlockedFilesTable(attachedDatabase, alias);
}
}
abstract class _$MoorDatabase extends GeneratedDatabase {
_$MoorDatabase(QueryExecutor e) : super(SqlTypeSystem.defaultInstance, e);
_$MoorDatabase.connect(DatabaseConnection c) : super.connect(c);
@ -4373,6 +4515,7 @@ abstract class _$MoorDatabase extends GeneratedDatabase {
$KeyValueEntriesTable(this);
late final $HomeWidgetsTable homeWidgets = $HomeWidgetsTable(this);
late final $HistoryEntriesTable historyEntries = $HistoryEntriesTable(this);
late final $BlockedFilesTable blockedFiles = $BlockedFilesTable(this);
late final PersistentStateDao persistentStateDao =
PersistentStateDao(this as MoorDatabase);
late final SettingsDao settingsDao = SettingsDao(this as MoorDatabase);
@ -4396,6 +4539,7 @@ abstract class _$MoorDatabase extends GeneratedDatabase {
playlistEntries,
keyValueEntries,
homeWidgets,
historyEntries
historyEntries,
blockedFiles
];
}

View file

@ -35,4 +35,8 @@ abstract class MusicDataSource {
Future<List<SongModel>> searchSongs(String searchText, {int? limit});
Future<int?> getAlbumId(String? title, String? artist, int? year);
Stream<Set<String>> get blockedFilesStream;
Future<void> addBlockedFiles(List<String> paths);
Future<void> removeBlockedFiles(List<String> paths);
}

View file

@ -20,6 +20,7 @@ abstract class PlaylistDataSource {
Future<void> moveEntry(int playlistId, int oldIndex, int newIndex);
Stream<List<SongModel>> getPlaylistSongStream(PlaylistModel playlist);
Future<List<PlaylistModel>> searchPlaylists(String searchText, {int? limit});
Future<void> removeBlockedSongs(List<String> paths);
Stream<List<SmartListModel>> get smartListsStream;
Stream<SmartListModel> getSmartListStream(int smartListId);

View file

@ -2,4 +2,7 @@ abstract class SettingsDataSource {
Stream<List<String>> get libraryFoldersStream;
Future<void> addLibraryFolder(String path);
Future<void> removeLibraryFolder(String path);
Stream<String> get fileExtensionsStream;
Future<void> setFileExtension(String extensions);
}

View file

@ -198,7 +198,7 @@ class AudioPlayerRepositoryImpl implements AudioPlayerRepository {
}
Future<void> _removeQueueIndices(List<int> indices, bool permanent) async {
_dynamicQueue.removeQueueIndeces(indices, permanent);
_dynamicQueue.removeQueueIndices(indices, permanent);
final newQueue = _dynamicQueue.queue;
final newCurrentIndex = newQueue.isNotEmpty
@ -213,9 +213,9 @@ class AudioPlayerRepositoryImpl implements AudioPlayerRepository {
} else {
final newSongs = await _dynamicQueue.updateCurrentIndex(newCurrentIndex);
if (newSongs.isNotEmpty) {
await _audioPlayerDataSource.addToQueue(newSongs.map((e) => e as SongModel).toList());
_queueSubject.add(_dynamicQueue.queue);
}
await _audioPlayerDataSource.addToQueue(newSongs.map((e) => e as SongModel).toList());
_queueSubject.add(_dynamicQueue.queue);
}
}
}
@ -298,6 +298,21 @@ class AudioPlayerRepositoryImpl implements AudioPlayerRepository {
}
}
@override
Future<void> removeBlockedSongs(List<String> paths) async {
final pathSet = Set<String>.from(paths);
final oldQueue = List<Song>.from(_dynamicQueue.queue);
if (_dynamicQueue.removeSongs(pathSet)) {
final indicesToRemove = <int>[];
for (int i = 0; i < oldQueue.length; i++) {
if (pathSet.contains(oldQueue[i].path)) indicesToRemove.add(i);
}
if (indicesToRemove.isNotEmpty) _audioPlayerDataSource.removeQueueIndices(indicesToRemove);
_queueSubject.add(_dynamicQueue.queue);
}
}
void _updateCurrentSong(List<Song>? queue, int? index) {
if (queue != null && index != null && index < queue.length) {
_log.d('Current song: ${queue[index]}');

View file

@ -34,6 +34,7 @@ class MusicDataRepositoryImpl implements MusicDataRepository {
_getAlbumOfDay().then((value) => _albumOfDaySubject.add(value));
_getArtistOfDay().then((value) => _artistOfDaySubject.add(value));
_minuteStream.listen((_) => _updateHighlightStreams());
_musicDataSource.blockedFilesStream.listen(_blockedFilesSubject.add);
}
final LocalMusicFetcher _localMusicFetcher;
@ -41,10 +42,12 @@ class MusicDataRepositoryImpl implements MusicDataRepository {
final PlaylistDataSource _playlistDataSource;
final BehaviorSubject<Map<String, Song>> _songUpdateSubject = BehaviorSubject();
final BehaviorSubject<List<String>> _songRemovalSubject = BehaviorSubject();
final BehaviorSubject<List<Song>> _songSubject = BehaviorSubject();
final Stream _minuteStream = Stream.periodic(const Duration(minutes: 1));
final BehaviorSubject<Album?> _albumOfDaySubject = BehaviorSubject();
final BehaviorSubject<Artist?> _artistOfDaySubject = BehaviorSubject();
final BehaviorSubject<Set<String>> _blockedFilesSubject = BehaviorSubject();
static final _log = FimberLog('MusicDataRepositoryImpl');
@ -73,6 +76,12 @@ class MusicDataRepositoryImpl implements MusicDataRepository {
@override
Stream<List<Artist>> get artistStream => _musicDataSource.artistStream;
@override
ValueStream<Set<String>> get blockedFilesStream => _blockedFilesSubject.stream;
@override
Stream<List<String>> get songRemovalStream => _songRemovalSubject.stream;
@override
Stream<List<Song>> getAlbumSongStream(Album album) =>
_musicDataSource.getAlbumSongStream(album as AlbumModel);
@ -117,7 +126,7 @@ class MusicDataRepositoryImpl implements MusicDataRepository {
await _updateArtists(artists);
await _updateAlbums(albums);
await _updateSongs(songs);
await _musicDataSource.insertSongs(songs);
_log.d('updateDatabase finished');
@ -134,10 +143,6 @@ class MusicDataRepositoryImpl implements MusicDataRepository {
await _musicDataSource.insertAlbums(albums);
}
Future<void> _updateSongs(List<SongModel> songs) async {
await _musicDataSource.insertSongs(songs);
}
@override
Future<void> setSongsBlockLevel(List<Song> songs, int blockLevel) async {
final changedSongs = songs.where((e) => e.blockLevel != blockLevel);
@ -548,4 +553,17 @@ class MusicDataRepositoryImpl implements MusicDataRepository {
return combined;
});
}
@override
Future<void> addBlockedFiles(List<String> paths) async {
_songRemovalSubject.add(paths);
await _playlistDataSource.removeBlockedSongs(paths);
await _musicDataSource.addBlockedFiles(paths);
_updateHighlightStreams();
}
@override
Future<void> removeBlockedFiles(List<String> paths) async {
await _musicDataSource.removeBlockedFiles(paths);
}
}

View file

@ -6,13 +6,14 @@ import '../datasources/settings_data_source.dart';
class SettingsRepositoryImpl implements SettingsRepository {
SettingsRepositoryImpl(this._settingsDataSource) {
Permission.manageExternalStorage.isGranted
.then(_manageExternalStorageGrantedSubject.add);
Permission.manageExternalStorage.isGranted.then(_manageExternalStorageGrantedSubject.add);
_settingsDataSource.fileExtensionsStream.listen(_fileExtensionsSubject.add);
}
final SettingsDataSource _settingsDataSource;
final BehaviorSubject<bool> _manageExternalStorageGrantedSubject = BehaviorSubject();
final BehaviorSubject<String> _fileExtensionsSubject = BehaviorSubject();
@override
Stream<List<String>> get libraryFoldersStream => _settingsDataSource.libraryFoldersStream;
@ -46,4 +47,12 @@ class SettingsRepositoryImpl implements SettingsRepository {
}
}
}
@override
ValueStream<String> get fileExtensionsStream => _fileExtensionsSubject.stream;
@override
Future<void> setFileExtension(String extensions) async {
await _settingsDataSource.setFileExtension(extensions);
}
}