close #36
- implement file extension filters - implement blacklist for files
This commit is contained in:
parent
dd446090f3
commit
a61d98716a
33 changed files with 781 additions and 60 deletions
|
@ -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
1
src/lib/defaults.dart
Normal file
|
@ -0,0 +1 @@
|
|||
const String ALLOWED_FILE_EXTENSIONS = 'mp3,flac,wav,ogg';
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
24
src/lib/domain/actors/music_data_actor.dart
Normal file
24
src/lib/domain/actors/music_data_actor.dart
Normal 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);
|
||||
}
|
||||
}
|
|
@ -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 {
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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(),
|
||||
|
|
|
@ -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();
|
||||
},
|
||||
),
|
||||
],
|
||||
);
|
||||
}),
|
||||
|
|
60
src/lib/presentation/pages/blocked_files_page.dart
Normal file
60
src/lib/presentation/pages/blocked_files_page.dart
Normal 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,
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
|
@ -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),
|
||||
|
|
|
@ -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),
|
||||
|
|
|
@ -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,
|
||||
),
|
||||
),
|
||||
);
|
||||
|
|
|
@ -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() {}
|
||||
}
|
||||
|
|
|
@ -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}
|
||||
''';
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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));
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -15,4 +15,5 @@ mixin _$PlaylistDaoMixin on DatabaseAccessor<MoorDatabase> {
|
|||
$SmartListsTable get smartLists => attachedDatabase.smartLists;
|
||||
$SmartListArtistsTable get smartListArtists =>
|
||||
attachedDatabase.smartListArtists;
|
||||
$HistoryEntriesTable get historyEntries => attachedDatabase.historyEntries;
|
||||
}
|
||||
|
|
|
@ -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)));
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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)),
|
||||
);
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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
|
||||
];
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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]}');
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Add table
Reference in a new issue