diff --git a/lib/domain/entities/smart_list.dart b/lib/domain/entities/smart_list.dart index 9066bbf..e39b6ad 100644 --- a/lib/domain/entities/smart_list.dart +++ b/lib/domain/entities/smart_list.dart @@ -30,6 +30,8 @@ class Filter extends Equatable { this.maxPlayCount, required this.minLikeCount, required this.maxLikeCount, + this.minSkipCount, + this.maxSkipCount, this.minYear, this.maxYear, required this.excludeBlocked, @@ -45,6 +47,9 @@ class Filter extends Equatable { final int minLikeCount; final int maxLikeCount; + final int? minSkipCount; + final int? maxSkipCount; + final int? minYear; final int? maxYear; @@ -83,6 +88,7 @@ enum OrderCriterion { artistName, likeCount, playCount, + skipCount, songTitle, timeAdded, year, @@ -97,6 +103,8 @@ extension OrderCriterionExtension on String { return OrderCriterion.likeCount; case 'OrderCriterion.playCount': return OrderCriterion.playCount; + case 'OrderCriterion.skipCount': + return OrderCriterion.skipCount; case 'OrderCriterion.songTitle': return OrderCriterion.songTitle; case 'OrderCriterion.timeAdded': diff --git a/lib/domain/repositories/settings_repository.dart b/lib/domain/repositories/settings_repository.dart index 91f4a33..2ebc36a 100644 --- a/lib/domain/repositories/settings_repository.dart +++ b/lib/domain/repositories/settings_repository.dart @@ -2,4 +2,9 @@ abstract class SettingsRepository { Stream> get libraryFoldersStream; Future addLibraryFolder(String? path); Future removeLibraryFolder(String? path); + + Future setBlockSkippedSongs(bool enabled); + Stream get isBlockSkippedSongsEnabled; + Future setBlockSkippedSongsThreshold(int threshold); + Stream get blockSkippedSongsThreshold; } diff --git a/lib/injection_container.dart b/lib/injection_container.dart index 09cea66..92a16b6 100644 --- a/lib/injection_container.dart +++ b/lib/injection_container.dart @@ -28,6 +28,7 @@ import 'presentation/state/audio_store.dart'; import 'presentation/state/music_data_store.dart'; import 'presentation/state/navigation_store.dart'; import 'presentation/state/search_page_store.dart'; +import 'presentation/state/settings_page_store.dart'; import 'presentation/state/settings_store.dart'; import 'presentation/state/smart_list_form_store.dart'; import 'presentation/state/smart_list_page_store.dart'; @@ -79,16 +80,21 @@ Future setupGetIt() async { musicDataInfoRepository: getIt(), ), ); + getIt.registerLazySingleton( + () => SettingsStore( + settingsRepository: getIt(), + musicDataRepository: getIt(), + ), + ); getIt.registerFactoryParam( (Artist? artist, _) => ArtistPageStore(artist: artist!, musicDataInfoRepository: getIt()), ); getIt.registerFactoryParam( (Album? album, _) => AlbumPageStore(album: album!, musicDataInfoRepository: getIt()), ); - getIt.registerFactory( - () => SettingsStore( + getIt.registerFactory( + () => SettingsPageStore( settingsRepository: getIt(), - musicDataRepository: getIt(), ), ); getIt.registerFactoryParam( diff --git a/lib/presentation/pages/playlists_page.dart b/lib/presentation/pages/playlists_page.dart index af8d464..47f8ab3 100644 --- a/lib/presentation/pages/playlists_page.dart +++ b/lib/presentation/pages/playlists_page.dart @@ -12,8 +12,8 @@ import '../state/music_data_store.dart'; import '../state/settings_store.dart'; import '../theming.dart'; import 'playlist_page.dart'; +import 'smart_list_form_page.dart'; import 'smart_list_page.dart'; -import 'smart_lists_form_page.dart'; class PlaylistsPage extends StatefulWidget { const PlaylistsPage({Key? key}) : super(key: key); diff --git a/lib/presentation/pages/settings_page.dart b/lib/presentation/pages/settings_page.dart index 780e2f3..73e4ac6 100644 --- a/lib/presentation/pages/settings_page.dart +++ b/lib/presentation/pages/settings_page.dart @@ -1,15 +1,54 @@ import 'package:flutter/material.dart'; import 'package:flutter_mobx/flutter_mobx.dart'; import 'package:get_it/get_it.dart'; +import 'package:mobx/mobx.dart'; import '../state/music_data_store.dart'; +import '../state/settings_page_store.dart'; import '../theming.dart'; import 'library_folders_page.dart'; -import 'smart_lists_settings_page.dart'; -class SettingsPage extends StatelessWidget { +class SettingsPage extends StatefulWidget { const SettingsPage({Key? key}) : super(key: key); + @override + State createState() => _SettingsPageState(); +} + +class _SettingsPageState extends State { + late SettingsPageStore settingsPageStore; + late TextEditingController _controller; + late ReactionDisposer _dispose; + + @override + void initState() { + super.initState(); + settingsPageStore = GetIt.I(); + settingsPageStore.init(); + settingsPageStore.setupValidations(); + + _controller = TextEditingController(); + _controller.addListener(() { + if (_controller.text != settingsPageStore.blockSkippedSongsThreshold) { + print('ctrl listener: ${_controller.text}'); + settingsPageStore.blockSkippedSongsThreshold = _controller.text; + } + }); + _dispose = autorun((_) { + if (_controller.text != settingsPageStore.blockSkippedSongsThreshold) { + print('autorun: ${settingsPageStore.blockSkippedSongsThreshold}'); + _controller.text = settingsPageStore.blockSkippedSongsThreshold; + } + }); + } + + @override + void dispose() { + super.dispose(); + settingsPageStore.dispose(); + _dispose(); + } + @override Widget build(BuildContext context) { final MusicDataStore musicDataStore = GetIt.I(); @@ -65,18 +104,66 @@ class SettingsPage extends StatelessWidget { height: 4.0, ), const SettingsSection(text: 'Home page'), - ListTile( - title: const Text('Customize smart lists'), - trailing: const Icon(Icons.chevron_right), - onTap: () => Navigator.of(context).push( - MaterialPageRoute( - builder: (BuildContext context) => const SmartListsSettingsPage(), - ), - ), + const ListTile( + title: Text('Soon (tm)'), ), const Divider( height: 4.0, ), + const SettingsSection(text: 'Customize playback'), + Observer( + builder: (_) { + final bool enabled = settingsPageStore.isBlockSkippedSongsEnabled; + + return Column( + children: [ + Padding( + padding: + const EdgeInsets.symmetric(horizontal: HORIZONTAL_PADDING, vertical: 4.0), + child: Row( + children: [ + const Text('Mark skipped songs as blocked'), + const Spacer(), + Switch( + value: settingsPageStore.isBlockSkippedSongsEnabled, + onChanged: (bool value) { + print('set: $value'); + settingsPageStore.isBlockSkippedSongsEnabled = value; + }, + ), + ], + ), + ), + Padding( + padding: + const EdgeInsets.symmetric(horizontal: HORIZONTAL_PADDING, vertical: 4.0), + child: Row( + children: [ + const Text('Minimum skip count to block songs'), + const Spacer(), + SizedBox( + width: 56.0, + child: TextFormField( + controller: _controller, + enabled: enabled, + keyboardType: TextInputType.number, + textAlign: TextAlign.center, + onChanged: (value) { + print(value); + }, + decoration: InputDecoration( + errorText: settingsPageStore.error.skipCountThreshold, + errorStyle: const TextStyle(height: 0, fontSize: 0), + ), + ), + ), + ], + ), + ), + ], + ); + }, + ), ], ), ), diff --git a/lib/presentation/pages/smart_lists_form_page.dart b/lib/presentation/pages/smart_list_form_page.dart similarity index 83% rename from lib/presentation/pages/smart_lists_form_page.dart rename to lib/presentation/pages/smart_list_form_page.dart index 1327b5d..3aec1cd 100644 --- a/lib/presentation/pages/smart_lists_form_page.dart +++ b/lib/presentation/pages/smart_list_form_page.dart @@ -176,7 +176,7 @@ class _SmartListFormPageState extends State { child: TextFormField( enabled: store.maxPlayCountEnabled, keyboardType: TextInputType.number, - initialValue: store.maxPlayCount.toString(), + initialValue: store.maxPlayCount, textAlign: TextAlign.center, onChanged: (value) { store.maxPlayCount = value; @@ -193,6 +193,74 @@ class _SmartListFormPageState extends State { ), ), const SizedBox(height: 24.0), + Padding( + padding: const EdgeInsets.symmetric(horizontal: HORIZONTAL_PADDING), + child: Observer( + builder: (_) { + return Row( + children: [ + Switch( + value: store.minSkipCountEnabled, + onChanged: (bool value) => store.minSkipCountEnabled = value, + ), + const Text('Minimum skip count'), + const Spacer(), + SizedBox( + width: 36.0, + child: TextFormField( + enabled: store.minSkipCountEnabled, + keyboardType: TextInputType.number, + initialValue: store.minSkipCount, + onChanged: (value) { + store.minSkipCount = value; + }, + textAlign: TextAlign.center, + decoration: InputDecoration( + errorText: store.error.minSkipCount, + errorStyle: const TextStyle(height: 0, fontSize: 0), + ), + ), + ), + ], + ); + }, + ), + ), + const SizedBox(height: 24.0), + Padding( + padding: const EdgeInsets.symmetric(horizontal: HORIZONTAL_PADDING), + child: Observer( + builder: (_) { + return Row( + children: [ + Switch( + value: store.maxSkipCountEnabled, + onChanged: (bool value) => store.maxSkipCountEnabled = value, + ), + const Text('Maximum skip count'), + const Spacer(), + SizedBox( + width: 36.0, + child: TextFormField( + enabled: store.maxSkipCountEnabled, + keyboardType: TextInputType.number, + initialValue: store.maxSkipCount, + textAlign: TextAlign.center, + onChanged: (value) { + store.maxSkipCount = value; + }, + decoration: InputDecoration( + errorText: store.error.maxSkipCount, + errorStyle: const TextStyle(height: 0, fontSize: 0), + ), + ), + ), + ], + ); + }, + ), + ), + const SizedBox(height: 24.0), Padding( padding: const EdgeInsets.symmetric(horizontal: HORIZONTAL_PADDING), child: Observer( @@ -244,7 +312,7 @@ class _SmartListFormPageState extends State { child: TextFormField( enabled: store.maxYearEnabled, keyboardType: TextInputType.number, - initialValue: store.maxYear.toString(), + initialValue: store.maxYear, textAlign: TextAlign.center, onChanged: (value) { store.maxYear = value; @@ -278,7 +346,7 @@ class _SmartListFormPageState extends State { child: TextFormField( enabled: store.limitEnabled, keyboardType: TextInputType.number, - initialValue: store.limit.toString(), + initialValue: store.limit, textAlign: TextAlign.center, onChanged: (value) { store.limit = value; diff --git a/lib/presentation/pages/smart_list_page.dart b/lib/presentation/pages/smart_list_page.dart index 5d921a2..df012a3 100644 --- a/lib/presentation/pages/smart_list_page.dart +++ b/lib/presentation/pages/smart_list_page.dart @@ -9,7 +9,7 @@ import '../state/smart_list_page_store.dart'; import '../theming.dart'; import '../widgets/song_bottom_sheet.dart'; import '../widgets/song_list_tile.dart'; -import 'smart_lists_form_page.dart'; +import 'smart_list_form_page.dart'; class SmartListPage extends StatefulWidget { const SmartListPage({Key? key, required this.smartList}) : super(key: key); diff --git a/lib/presentation/pages/smart_lists_settings_page.dart b/lib/presentation/pages/smart_lists_settings_page.dart index 786cb74..b7e09ff 100644 --- a/lib/presentation/pages/smart_lists_settings_page.dart +++ b/lib/presentation/pages/smart_lists_settings_page.dart @@ -5,8 +5,9 @@ import 'package:get_it/get_it.dart'; import '../../domain/entities/smart_list.dart'; import '../state/settings_store.dart'; import '../theming.dart'; -import 'smart_lists_form_page.dart'; +import 'smart_list_form_page.dart'; +// TODO: currently unused class SmartListsSettingsPage extends StatelessWidget { const SmartListsSettingsPage({Key? key}) : super(key: key); diff --git a/lib/presentation/pages/songs_page.dart b/lib/presentation/pages/songs_page.dart index 088312a..635a493 100644 --- a/lib/presentation/pages/songs_page.dart +++ b/lib/presentation/pages/songs_page.dart @@ -6,6 +6,7 @@ import 'package:mobx/mobx.dart'; import '../../domain/entities/song.dart'; import '../state/audio_store.dart'; import '../state/music_data_store.dart'; +import '../state/settings_store.dart'; import '../widgets/song_bottom_sheet.dart'; import '../widgets/song_list_tile.dart'; @@ -22,12 +23,15 @@ class _SongsPageState extends State with AutomaticKeepAliveClientMixi print('SongsPage.build'); final MusicDataStore musicDataStore = GetIt.I(); final AudioStore audioStore = GetIt.I(); + final SettingsStore settingsStore = GetIt.I(); super.build(context); return Observer(builder: (_) { print('SongsPage.build -> Observer.builder'); final songStream = musicDataStore.songStream; + final isBlockSkippedSongsEnabled = settingsStore.isBlockSkippedSongsEnabled.first; + final blockSkippedSongsThreshold = settingsStore.blockSkippedSongsThreshold.first; switch (songStream.status) { case StreamStatus.active: @@ -43,6 +47,8 @@ class _SongsPageState extends State with AutomaticKeepAliveClientMixi subtitle: Subtitle.artistAlbum, onTap: () => audioStore.playSong(index, songs), onTapMore: () => SongBottomSheet()(song, context), + isBlockSkippedSongsEnabled: isBlockSkippedSongsEnabled.value, + blockSkippedSongsThreshold: blockSkippedSongsThreshold.value, ); }, separatorBuilder: (BuildContext context, int index) => const SizedBox( diff --git a/lib/presentation/state/settings_page_store.dart b/lib/presentation/state/settings_page_store.dart new file mode 100644 index 0000000..6d2664a --- /dev/null +++ b/lib/presentation/state/settings_page_store.dart @@ -0,0 +1,84 @@ +import 'package:mobx/mobx.dart'; + +import '../../domain/repositories/settings_repository.dart'; +import '../utils.dart'; + +part 'settings_page_store.g.dart'; + +class SettingsPageStore extends _SettingsPageStore with _$SettingsPageStore { + SettingsPageStore({ + required SettingsRepository settingsRepository, + }) : super(settingsRepository); +} + +abstract class _SettingsPageStore with Store { + _SettingsPageStore( + this._settingsRepository, + ); + + final SettingsRepository _settingsRepository; + + final FormErrorState error = FormErrorState(); + + @observable + bool isBlockSkippedSongsEnabled = false; + + @observable + String blockSkippedSongsThreshold = '-1'; + + late List _disposers; + + Future addLibraryFolder(String? path) async { + await _settingsRepository.addLibraryFolder(path); + } + + Future removeLibraryFolder(String? path) async { + await _settingsRepository.removeLibraryFolder(path); + } + + @action + Future init() async { + isBlockSkippedSongsEnabled = await _settingsRepository.isBlockSkippedSongsEnabled.first; + blockSkippedSongsThreshold = (await _settingsRepository.blockSkippedSongsThreshold.first).toString(); + } + + void setupValidations() { + _disposers = [ + reaction((_) => blockSkippedSongsThreshold, + (String n) => _validateSkipCountThreshold(isBlockSkippedSongsEnabled, n)), + reaction((_) => isBlockSkippedSongsEnabled, _validateBlockSkipCountEnabled), + ]; + } + + void dispose() { + for (final d in _disposers) { + d(); + } + } + + void validateAll() { + _validateSkipCountThreshold(isBlockSkippedSongsEnabled, blockSkippedSongsThreshold); + } + + void _validateSkipCountThreshold(bool enabled, String number) { + error.skipCountThreshold = validateNumber(enabled, number); + if (!error.hasErrors) { + final val = int.parse(blockSkippedSongsThreshold); + _settingsRepository.setBlockSkippedSongsThreshold(val); + } + } + + void _validateBlockSkipCountEnabled(bool enabled) { + _settingsRepository.setBlockSkippedSongs(enabled); + } +} + +class FormErrorState = _FormErrorState with _$FormErrorState; + +abstract class _FormErrorState with Store { + @observable + String? skipCountThreshold; + + @computed + bool get hasErrors => skipCountThreshold != null; +} diff --git a/lib/presentation/state/settings_page_store.g.dart b/lib/presentation/state/settings_page_store.g.dart new file mode 100644 index 0000000..5779160 --- /dev/null +++ b/lib/presentation/state/settings_page_store.g.dart @@ -0,0 +1,94 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'settings_page_store.dart'; + +// ************************************************************************** +// StoreGenerator +// ************************************************************************** + +// ignore_for_file: non_constant_identifier_names, unnecessary_brace_in_string_interps, unnecessary_lambdas, prefer_expression_function_bodies, lines_longer_than_80_chars, avoid_as, avoid_annotating_with_dynamic + +mixin _$SettingsPageStore on _SettingsPageStore, Store { + final _$isBlockSkippedSongsEnabledAtom = + Atom(name: '_SettingsPageStore.isBlockSkippedSongsEnabled'); + + @override + bool get isBlockSkippedSongsEnabled { + _$isBlockSkippedSongsEnabledAtom.reportRead(); + return super.isBlockSkippedSongsEnabled; + } + + @override + set isBlockSkippedSongsEnabled(bool value) { + _$isBlockSkippedSongsEnabledAtom + .reportWrite(value, super.isBlockSkippedSongsEnabled, () { + super.isBlockSkippedSongsEnabled = value; + }); + } + + final _$blockSkippedSongsThresholdAtom = + Atom(name: '_SettingsPageStore.blockSkippedSongsThreshold'); + + @override + String get blockSkippedSongsThreshold { + _$blockSkippedSongsThresholdAtom.reportRead(); + return super.blockSkippedSongsThreshold; + } + + @override + set blockSkippedSongsThreshold(String value) { + _$blockSkippedSongsThresholdAtom + .reportWrite(value, super.blockSkippedSongsThreshold, () { + super.blockSkippedSongsThreshold = value; + }); + } + + final _$initAsyncAction = AsyncAction('_SettingsPageStore.init'); + + @override + Future init() { + return _$initAsyncAction.run(() => super.init()); + } + + @override + String toString() { + return ''' +isBlockSkippedSongsEnabled: ${isBlockSkippedSongsEnabled}, +blockSkippedSongsThreshold: ${blockSkippedSongsThreshold} + '''; + } +} + +mixin _$FormErrorState on _FormErrorState, Store { + Computed? _$hasErrorsComputed; + + @override + bool get hasErrors => + (_$hasErrorsComputed ??= Computed(() => super.hasErrors, + name: '_FormErrorState.hasErrors')) + .value; + + final _$skipCountThresholdAtom = + Atom(name: '_FormErrorState.skipCountThreshold'); + + @override + String? get skipCountThreshold { + _$skipCountThresholdAtom.reportRead(); + return super.skipCountThreshold; + } + + @override + set skipCountThreshold(String? value) { + _$skipCountThresholdAtom.reportWrite(value, super.skipCountThreshold, () { + super.skipCountThreshold = value; + }); + } + + @override + String toString() { + return ''' +skipCountThreshold: ${skipCountThreshold}, +hasErrors: ${hasErrors} + '''; + } +} diff --git a/lib/presentation/state/settings_store.dart b/lib/presentation/state/settings_store.dart index c933fab..8b62310 100644 --- a/lib/presentation/state/settings_store.dart +++ b/lib/presentation/state/settings_store.dart @@ -30,6 +30,12 @@ abstract class _SettingsStore with Store { late ObservableStream> libraryFoldersStream = _settingsRepository.libraryFoldersStream.asObservable(initialValue: []); + @observable + late ObservableStream isBlockSkippedSongsEnabled = _settingsRepository.isBlockSkippedSongsEnabled.asObservable(); + + @observable + late ObservableStream blockSkippedSongsThreshold = _settingsRepository.blockSkippedSongsThreshold.asObservable(); + Future addLibraryFolder(String? path) async { await _settingsRepository.addLibraryFolder(path); } diff --git a/lib/presentation/state/settings_store.g.dart b/lib/presentation/state/settings_store.g.dart index b0e8556..53795b0 100644 --- a/lib/presentation/state/settings_store.g.dart +++ b/lib/presentation/state/settings_store.g.dart @@ -41,11 +41,47 @@ mixin _$SettingsStore on _SettingsStore, Store { }); } + final _$isBlockSkippedSongsEnabledAtom = + Atom(name: '_SettingsStore.isBlockSkippedSongsEnabled'); + + @override + ObservableStream get isBlockSkippedSongsEnabled { + _$isBlockSkippedSongsEnabledAtom.reportRead(); + return super.isBlockSkippedSongsEnabled; + } + + @override + set isBlockSkippedSongsEnabled(ObservableStream value) { + _$isBlockSkippedSongsEnabledAtom + .reportWrite(value, super.isBlockSkippedSongsEnabled, () { + super.isBlockSkippedSongsEnabled = value; + }); + } + + final _$blockSkippedSongsThresholdAtom = + Atom(name: '_SettingsStore.blockSkippedSongsThreshold'); + + @override + ObservableStream get blockSkippedSongsThreshold { + _$blockSkippedSongsThresholdAtom.reportRead(); + return super.blockSkippedSongsThreshold; + } + + @override + set blockSkippedSongsThreshold(ObservableStream value) { + _$blockSkippedSongsThresholdAtom + .reportWrite(value, super.blockSkippedSongsThreshold, () { + super.blockSkippedSongsThreshold = value; + }); + } + @override String toString() { return ''' smartListsStream: ${smartListsStream}, -libraryFoldersStream: ${libraryFoldersStream} +libraryFoldersStream: ${libraryFoldersStream}, +isBlockSkippedSongsEnabled: ${isBlockSkippedSongsEnabled}, +blockSkippedSongsThreshold: ${blockSkippedSongsThreshold} '''; } } diff --git a/lib/presentation/state/smart_list_form_store.dart b/lib/presentation/state/smart_list_form_store.dart index 42eea6b..745dde4 100644 --- a/lib/presentation/state/smart_list_form_store.dart +++ b/lib/presentation/state/smart_list_form_store.dart @@ -3,6 +3,7 @@ import 'package:mobx/mobx.dart'; import '../../domain/entities/artist.dart'; import '../../domain/entities/smart_list.dart'; import '../../domain/repositories/music_data_repository.dart'; +import '../utils.dart'; part 'smart_list_form_store.g.dart'; @@ -42,6 +43,16 @@ abstract class _SmartListStore with Store { @observable late String maxPlayCount = _intToString(_smartList?.filter.maxPlayCount); + @observable + late bool minSkipCountEnabled = _smartList?.filter.minSkipCount != null; + @observable + late String minSkipCount = _intToString(_smartList?.filter.minSkipCount); + + @observable + late bool maxSkipCountEnabled = _smartList?.filter.maxSkipCount != null; + @observable + late String maxSkipCount = _intToString(_smartList?.filter.maxSkipCount); + @observable late bool minYearEnabled = _smartList?.filter.minYear != null; @observable @@ -118,6 +129,8 @@ abstract class _SmartListStore with Store { reaction((_) => name, _validateName), reaction((_) => minPlayCount, (String n) => _validateMinPlayCount(minPlayCountEnabled, n)), reaction((_) => maxPlayCount, (String n) => _validateMaxPlayCount(maxPlayCountEnabled, n)), + reaction((_) => minSkipCount, (String n) => _validateMinSkipCount(minSkipCountEnabled, n)), + reaction((_) => maxSkipCount, (String n) => _validateMaxSkipCount(maxSkipCountEnabled, n)), reaction((_) => minYear, (String n) => _validateMinYear(minYearEnabled, n)), reaction((_) => maxYear, (String n) => _validateMaxYear(maxYearEnabled, n)), reaction((_) => limit, (String n) => _validateLimit(limitEnabled, n)), @@ -135,6 +148,8 @@ abstract class _SmartListStore with Store { _validateName(name); _validateMinPlayCount(minPlayCountEnabled, minPlayCount); _validateMaxPlayCount(maxPlayCountEnabled, maxPlayCount); + _validateMinSkipCount(minSkipCountEnabled, minSkipCount); + _validateMaxSkipCount(maxSkipCountEnabled, maxSkipCount); _validateMinYear(minYearEnabled, minYear); _validateMaxYear(maxYearEnabled, maxYear); _validateLimit(limitEnabled, limit); @@ -145,29 +160,34 @@ abstract class _SmartListStore with Store { } void _validateMinPlayCount(bool enabled, String number) { - error.minPlayCount = _validateNumber(enabled, number); + error.minPlayCount = validateNumber(enabled, number); } void _validateMaxPlayCount(bool enabled, String number) { - error.maxPlayCount = _validateNumber(enabled, number); + error.maxPlayCount = validateNumber(enabled, number); + } + + void _validateMinSkipCount(bool enabled, String number) { + error.minSkipCount = validateNumber(enabled, number); + } + + void _validateMaxSkipCount(bool enabled, String number) { + error.maxSkipCount = validateNumber(enabled, number); } void _validateMinYear(bool enabled, String number) { - error.minYear = _validateNumber(enabled, number); + error.minYear = validateNumber(enabled, number); } void _validateMaxYear(bool enabled, String number) { - error.maxYear = _validateNumber(enabled, number); + error.maxYear = validateNumber(enabled, number); } void _validateLimit(bool enabled, String number) { - error.limit = _validateNumber(enabled, number); + error.limit = validateNumber(enabled, number); } - String? _validateNumber(bool enabled, String number) { - if (!enabled) return null; - return int.tryParse(number) == null ? 'Error' : null; - } + Future _createSmartList() async { await _musicDataRepository.insertSmartList( @@ -177,6 +197,8 @@ abstract class _SmartListStore with Store { excludeArtists: excludeArtists, minPlayCount: minPlayCountEnabled ? int.tryParse(minPlayCount) : null, maxPlayCount: maxPlayCountEnabled ? int.tryParse(maxPlayCount) : null, + minSkipCount: minSkipCountEnabled ? int.tryParse(minSkipCount) : null, + maxSkipCount: maxSkipCountEnabled ? int.tryParse(maxSkipCount) : null, minYear: minYearEnabled ? int.tryParse(minYear) : null, maxYear: maxYearEnabled ? int.tryParse(maxYear) : null, minLikeCount: minLikeCount, @@ -201,6 +223,8 @@ abstract class _SmartListStore with Store { excludeArtists: excludeArtists, minPlayCount: minPlayCountEnabled ? int.tryParse(minPlayCount) : null, maxPlayCount: maxPlayCountEnabled ? int.tryParse(maxPlayCount) : null, + minSkipCount: minSkipCountEnabled ? int.tryParse(minSkipCount) : null, + maxSkipCount: maxSkipCountEnabled ? int.tryParse(maxSkipCount) : null, minYear: minYearEnabled ? int.tryParse(minYear) : null, maxYear: maxYearEnabled ? int.tryParse(maxYear) : null, minLikeCount: minLikeCount, @@ -229,6 +253,12 @@ abstract class _FormErrorState with Store { @observable String? maxPlayCount; + @observable + String? minSkipCount; + + @observable + String? maxSkipCount; + @observable String? minYear; @@ -267,6 +297,7 @@ List _createOrderState(OrderBy? orderBy) { OrderCriterion.songTitle: 'Song title', OrderCriterion.likeCount: 'Like count', OrderCriterion.playCount: 'Play count', + OrderCriterion.skipCount: 'Skip count', OrderCriterion.artistName: 'Artist name', OrderCriterion.year: 'Year', OrderCriterion.timeAdded: 'Time added', @@ -279,6 +310,8 @@ List _createOrderState(OrderBy? orderBy) { descriptions[OrderCriterion.likeCount]!), OrderEntry(false, OrderCriterion.playCount, OrderDirection.ascending, descriptions[OrderCriterion.playCount]!), + OrderEntry(false, OrderCriterion.skipCount, OrderDirection.ascending, + descriptions[OrderCriterion.skipCount]!), OrderEntry(false, OrderCriterion.artistName, OrderDirection.ascending, descriptions[OrderCriterion.artistName]!), OrderEntry( diff --git a/lib/presentation/state/smart_list_form_store.g.dart b/lib/presentation/state/smart_list_form_store.g.dart index 7130d72..7fb40c2 100644 --- a/lib/presentation/state/smart_list_form_store.g.dart +++ b/lib/presentation/state/smart_list_form_store.g.dart @@ -116,6 +116,68 @@ mixin _$SmartListFormStore on _SmartListStore, Store { }); } + final _$minSkipCountEnabledAtom = + Atom(name: '_SmartListStore.minSkipCountEnabled'); + + @override + bool get minSkipCountEnabled { + _$minSkipCountEnabledAtom.reportRead(); + return super.minSkipCountEnabled; + } + + @override + set minSkipCountEnabled(bool value) { + _$minSkipCountEnabledAtom.reportWrite(value, super.minSkipCountEnabled, () { + super.minSkipCountEnabled = value; + }); + } + + final _$minSkipCountAtom = Atom(name: '_SmartListStore.minSkipCount'); + + @override + String get minSkipCount { + _$minSkipCountAtom.reportRead(); + return super.minSkipCount; + } + + @override + set minSkipCount(String value) { + _$minSkipCountAtom.reportWrite(value, super.minSkipCount, () { + super.minSkipCount = value; + }); + } + + final _$maxSkipCountEnabledAtom = + Atom(name: '_SmartListStore.maxSkipCountEnabled'); + + @override + bool get maxSkipCountEnabled { + _$maxSkipCountEnabledAtom.reportRead(); + return super.maxSkipCountEnabled; + } + + @override + set maxSkipCountEnabled(bool value) { + _$maxSkipCountEnabledAtom.reportWrite(value, super.maxSkipCountEnabled, () { + super.maxSkipCountEnabled = value; + }); + } + + final _$maxSkipCountAtom = Atom(name: '_SmartListStore.maxSkipCount'); + + @override + String get maxSkipCount { + _$maxSkipCountAtom.reportRead(); + return super.maxSkipCount; + } + + @override + set maxSkipCount(String value) { + _$maxSkipCountAtom.reportWrite(value, super.maxSkipCount, () { + super.maxSkipCount = value; + }); + } + final _$minYearEnabledAtom = Atom(name: '_SmartListStore.minYearEnabled'); @override @@ -334,6 +396,10 @@ minPlayCountEnabled: ${minPlayCountEnabled}, minPlayCount: ${minPlayCount}, maxPlayCountEnabled: ${maxPlayCountEnabled}, maxPlayCount: ${maxPlayCount}, +minSkipCountEnabled: ${minSkipCountEnabled}, +minSkipCount: ${minSkipCount}, +maxSkipCountEnabled: ${maxSkipCountEnabled}, +maxSkipCount: ${maxSkipCount}, minYearEnabled: ${minYearEnabled}, minYear: ${minYear}, maxYearEnabled: ${maxYearEnabled}, @@ -402,6 +468,36 @@ mixin _$FormErrorState on _FormErrorState, Store { }); } + final _$minSkipCountAtom = Atom(name: '_FormErrorState.minSkipCount'); + + @override + String? get minSkipCount { + _$minSkipCountAtom.reportRead(); + return super.minSkipCount; + } + + @override + set minSkipCount(String? value) { + _$minSkipCountAtom.reportWrite(value, super.minSkipCount, () { + super.minSkipCount = value; + }); + } + + final _$maxSkipCountAtom = Atom(name: '_FormErrorState.maxSkipCount'); + + @override + String? get maxSkipCount { + _$maxSkipCountAtom.reportRead(); + return super.maxSkipCount; + } + + @override + set maxSkipCount(String? value) { + _$maxSkipCountAtom.reportWrite(value, super.maxSkipCount, () { + super.maxSkipCount = value; + }); + } + final _$minYearAtom = Atom(name: '_FormErrorState.minYear'); @override @@ -453,6 +549,8 @@ mixin _$FormErrorState on _FormErrorState, Store { name: ${name}, minPlayCount: ${minPlayCount}, maxPlayCount: ${maxPlayCount}, +minSkipCount: ${minSkipCount}, +maxSkipCount: ${maxSkipCount}, minYear: ${minYear}, maxYear: ${maxYear}, limit: ${limit}, diff --git a/lib/presentation/theming.dart b/lib/presentation/theming.dart index 931bec5..7120901 100644 --- a/lib/presentation/theming.dart +++ b/lib/presentation/theming.dart @@ -35,6 +35,8 @@ ThemeData theme() => ThemeData( primary: LIGHT1, ), ), + progressIndicatorTheme: const ProgressIndicatorThemeData(color: LIGHT2), + sliderTheme: const SliderThemeData(activeTrackColor: LIGHT2, thumbColor: LIGHT2, inactiveTrackColor: Colors.white24), // https://api.flutter.dev/flutter/material/TextTheme-class.html textTheme: const TextTheme( headline1: TextStyle( diff --git a/lib/presentation/utils.dart b/lib/presentation/utils.dart index ed9f7d7..62a3cc9 100644 --- a/lib/presentation/utils.dart +++ b/lib/presentation/utils.dart @@ -21,7 +21,7 @@ String msToTimeString(Duration duration) { final int hours = duration.inHours; final int minutes = duration.inMinutes.remainder(60); - + final String twoDigitMinutes = twoDigits(minutes); final String twoDigitSeconds = twoDigits(duration.inSeconds.remainder(60)); @@ -30,3 +30,8 @@ String msToTimeString(Duration duration) { } return '$minutes:$twoDigitSeconds'; } + +String? validateNumber(bool enabled, String number) { + if (!enabled) return null; + return int.tryParse(number) == null ? 'Error' : null; +} diff --git a/lib/presentation/widgets/song_list_tile.dart b/lib/presentation/widgets/song_list_tile.dart index 9396063..05b8c7b 100644 --- a/lib/presentation/widgets/song_list_tile.dart +++ b/lib/presentation/widgets/song_list_tile.dart @@ -15,6 +15,8 @@ class SongListTile extends StatelessWidget { this.highlight = false, this.showAlbum = true, this.subtitle = Subtitle.artist, + this.isBlockSkippedSongsEnabled, + this.blockSkippedSongsThreshold, }) : super(key: key); final Song song; @@ -23,9 +25,14 @@ class SongListTile extends StatelessWidget { final bool highlight; final bool showAlbum; final Subtitle subtitle; + final bool? isBlockSkippedSongsEnabled; + final int? blockSkippedSongsThreshold; @override Widget build(BuildContext context) { + final isBlockEnabled = isBlockSkippedSongsEnabled ?? false; + final blockThreshold = blockSkippedSongsThreshold ?? 1000; + final Widget leading = showAlbum ? Image( image: utils.getAlbumImage(song.albumArtPath), @@ -97,10 +104,16 @@ class SongListTile extends StatelessWidget { mainAxisSize: MainAxisSize.min, children: [ if (song.blocked) - Icon( + const Icon( Icons.remove_circle_outline, size: 14.0, - color: Colors.white.withOpacity(0.4), + color: Colors.white38, + ), + if (!song.blocked && isBlockEnabled && blockThreshold <= song.skipCount) + Icon( + Icons.skip_next_rounded, + size: 18.0, + color: Colors.red.shade900, ), IconButton( icon: const Icon(Icons.more_vert), diff --git a/lib/system/datasources/moor/playlist_dao.dart b/lib/system/datasources/moor/playlist_dao.dart index c17ed37..2c10d80 100644 --- a/lib/system/datasources/moor/playlist_dao.dart +++ b/lib/system/datasources/moor/playlist_dao.dart @@ -156,6 +156,8 @@ class PlaylistDao extends DatabaseAccessor excludeArtists: Value(filter.excludeArtists), minPlayCount: Value(filter.minPlayCount), maxPlayCount: Value(filter.maxPlayCount), + minSkipCount: Value(filter.minSkipCount), + maxSkipCount: Value(filter.maxSkipCount), minLikeCount: Value(filter.minLikeCount), maxLikeCount: Value(filter.maxLikeCount), minYear: Value(filter.minYear), @@ -256,6 +258,11 @@ class PlaylistDao extends DatabaseAccessor if (filter.maxPlayCount != null) query = query..where((tbl) => tbl.playCount.isSmallerOrEqualValue(filter.maxPlayCount)); + if (filter.minSkipCount != null) + query = query..where((tbl) => tbl.skipCount.isBiggerOrEqualValue(filter.minSkipCount)); + if (filter.maxSkipCount != null) + query = query..where((tbl) => tbl.skipCount.isSmallerOrEqualValue(filter.maxSkipCount)); + if (filter.minYear != null) query = query..where((tbl) => tbl.year.isBiggerOrEqualValue(filter.minYear)); if (filter.maxYear != null) @@ -312,6 +319,14 @@ List _generateOrderingTerms(sl.OrderBy order ), ); break; + case sl.OrderCriterion.skipCount: + orderingTerms.add( + ($SongsTable t) => OrderingTerm( + expression: t.skipCount, + mode: mode, + ), + ); + break; case sl.OrderCriterion.songTitle: orderingTerms.add( ($SongsTable t) => OrderingTerm( diff --git a/lib/system/datasources/moor/settings_dao.dart b/lib/system/datasources/moor/settings_dao.dart index b8fa297..f579732 100644 --- a/lib/system/datasources/moor/settings_dao.dart +++ b/lib/system/datasources/moor/settings_dao.dart @@ -5,7 +5,7 @@ import '../settings_data_source.dart'; part 'settings_dao.g.dart'; -@UseDao(tables: [LibraryFolders, SmartLists, SmartListArtists, Artists]) +@UseDao(tables: [LibraryFolders, BlockSkippedSongs]) class SettingsDao extends DatabaseAccessor with _$SettingsDaoMixin implements SettingsDataSource { @@ -24,4 +24,22 @@ class SettingsDao extends DatabaseAccessor Future addLibraryFolder(String path) async { await into(libraryFolders).insert(LibraryFoldersCompanion(path: Value(path))); } + + @override + Stream get isBlockSkippedSongsEnabled => + (select(blockSkippedSongs)..limit(1)).watchSingle().map((tbl) => tbl.enabled); + + @override + Stream get blockSkippedSongsThreshold => + (select(blockSkippedSongs)..limit(1)).watchSingle().map((tbl) => tbl.threshold); + + @override + Future setBlockSkippedSongsThreshold(int threshold) async { + await update(blockSkippedSongs).write(BlockSkippedSongsCompanion(threshold: Value(threshold))); + } + + @override + Future setBlockSkippedSongs(bool enabled) async { + await update(blockSkippedSongs).write(BlockSkippedSongsCompanion(enabled: Value(enabled))); + } } diff --git a/lib/system/datasources/moor/settings_dao.g.dart b/lib/system/datasources/moor/settings_dao.g.dart index 0050e12..3de8c4d 100644 --- a/lib/system/datasources/moor/settings_dao.g.dart +++ b/lib/system/datasources/moor/settings_dao.g.dart @@ -8,8 +8,6 @@ part of 'settings_dao.dart'; mixin _$SettingsDaoMixin on DatabaseAccessor { $LibraryFoldersTable get libraryFolders => attachedDatabase.libraryFolders; - $SmartListsTable get smartLists => attachedDatabase.smartLists; - $SmartListArtistsTable get smartListArtists => - attachedDatabase.smartListArtists; - $ArtistsTable get artists => attachedDatabase.artists; + $BlockSkippedSongsTable get blockSkippedSongs => + attachedDatabase.blockSkippedSongs; } diff --git a/lib/system/datasources/moor_database.dart b/lib/system/datasources/moor_database.dart index 44081f0..eeb7c3f 100644 --- a/lib/system/datasources/moor_database.dart +++ b/lib/system/datasources/moor_database.dart @@ -107,6 +107,11 @@ class LibraryFolders extends Table { TextColumn get path => text()(); } +class BlockSkippedSongs extends Table { + BoolColumn get enabled => boolean().withDefault(const Constant(false))(); + IntColumn get threshold => integer().withDefault(const Constant(3))(); +} + class MoorAlbumOfDay extends Table { IntColumn get albumId => integer()(); IntColumn get milliSecSinceEpoch => integer()(); @@ -128,6 +133,8 @@ class SmartLists extends Table { IntColumn get maxLikeCount => integer().withDefault(const Constant(5))(); IntColumn get minPlayCount => integer().nullable()(); IntColumn get maxPlayCount => integer().nullable()(); + IntColumn get minSkipCount => integer().nullable()(); + IntColumn get maxSkipCount => integer().nullable()(); IntColumn get minYear => integer().nullable()(); IntColumn get maxYear => integer().nullable()(); IntColumn get limit => integer().nullable()(); @@ -173,6 +180,7 @@ class PlaylistEntries extends Table { SmartListArtists, Playlists, PlaylistEntries, + BlockSkippedSongs, ], daos: [ PersistentStateDao, @@ -192,7 +200,7 @@ class MoorDatabase extends _$MoorDatabase { MoorDatabase.connect(DatabaseConnection connection) : super.connect(connection); @override - int get schemaVersion => 4; + int get schemaVersion => 5; @override MigrationStrategy get migration => MigrationStrategy(beforeOpen: (details) async { @@ -235,6 +243,12 @@ class MoorDatabase extends _$MoorDatabase { await m.createTable(playlists); await m.createTable(playlistEntries); } + if (from < 5) { + await m.addColumn(smartLists, smartLists.minSkipCount); + await m.addColumn(smartLists, smartLists.maxSkipCount); + await m.createTable(blockSkippedSongs); + await into(blockSkippedSongs).insert(const BlockSkippedSongsCompanion()); + } }); } diff --git a/lib/system/datasources/moor_database.g.dart b/lib/system/datasources/moor_database.g.dart index 7da978d..67ff96c 100644 --- a/lib/system/datasources/moor_database.g.dart +++ b/lib/system/datasources/moor_database.g.dart @@ -2814,6 +2814,8 @@ class MoorSmartList extends DataClass implements Insertable { final int maxLikeCount; final int? minPlayCount; final int? maxPlayCount; + final int? minSkipCount; + final int? maxSkipCount; final int? minYear; final int? maxYear; final int? limit; @@ -2829,6 +2831,8 @@ class MoorSmartList extends DataClass implements Insertable { required this.maxLikeCount, this.minPlayCount, this.maxPlayCount, + this.minSkipCount, + this.maxSkipCount, this.minYear, this.maxYear, this.limit, @@ -2857,6 +2861,10 @@ class MoorSmartList extends DataClass implements Insertable { .mapFromDatabaseResponse(data['${effectivePrefix}min_play_count']), maxPlayCount: const IntType() .mapFromDatabaseResponse(data['${effectivePrefix}max_play_count']), + minSkipCount: const IntType() + .mapFromDatabaseResponse(data['${effectivePrefix}min_skip_count']), + maxSkipCount: const IntType() + .mapFromDatabaseResponse(data['${effectivePrefix}max_skip_count']), minYear: const IntType() .mapFromDatabaseResponse(data['${effectivePrefix}min_year']), maxYear: const IntType() @@ -2887,6 +2895,12 @@ class MoorSmartList extends DataClass implements Insertable { if (!nullToAbsent || maxPlayCount != null) { map['max_play_count'] = Variable(maxPlayCount); } + if (!nullToAbsent || minSkipCount != null) { + map['min_skip_count'] = Variable(minSkipCount); + } + if (!nullToAbsent || maxSkipCount != null) { + map['max_skip_count'] = Variable(maxSkipCount); + } if (!nullToAbsent || minYear != null) { map['min_year'] = Variable(minYear); } @@ -2918,6 +2932,12 @@ class MoorSmartList extends DataClass implements Insertable { maxPlayCount: maxPlayCount == null && nullToAbsent ? const Value.absent() : Value(maxPlayCount), + minSkipCount: minSkipCount == null && nullToAbsent + ? const Value.absent() + : Value(minSkipCount), + maxSkipCount: maxSkipCount == null && nullToAbsent + ? const Value.absent() + : Value(maxSkipCount), minYear: minYear == null && nullToAbsent ? const Value.absent() : Value(minYear), @@ -2944,6 +2964,8 @@ class MoorSmartList extends DataClass implements Insertable { maxLikeCount: serializer.fromJson(json['maxLikeCount']), minPlayCount: serializer.fromJson(json['minPlayCount']), maxPlayCount: serializer.fromJson(json['maxPlayCount']), + minSkipCount: serializer.fromJson(json['minSkipCount']), + maxSkipCount: serializer.fromJson(json['maxSkipCount']), minYear: serializer.fromJson(json['minYear']), maxYear: serializer.fromJson(json['maxYear']), limit: serializer.fromJson(json['limit']), @@ -2964,6 +2986,8 @@ class MoorSmartList extends DataClass implements Insertable { 'maxLikeCount': serializer.toJson(maxLikeCount), 'minPlayCount': serializer.toJson(minPlayCount), 'maxPlayCount': serializer.toJson(maxPlayCount), + 'minSkipCount': serializer.toJson(minSkipCount), + 'maxSkipCount': serializer.toJson(maxSkipCount), 'minYear': serializer.toJson(minYear), 'maxYear': serializer.toJson(maxYear), 'limit': serializer.toJson(limit), @@ -2982,6 +3006,8 @@ class MoorSmartList extends DataClass implements Insertable { int? maxLikeCount, int? minPlayCount, int? maxPlayCount, + int? minSkipCount, + int? maxSkipCount, int? minYear, int? maxYear, int? limit, @@ -2997,6 +3023,8 @@ class MoorSmartList extends DataClass implements Insertable { maxLikeCount: maxLikeCount ?? this.maxLikeCount, minPlayCount: minPlayCount ?? this.minPlayCount, maxPlayCount: maxPlayCount ?? this.maxPlayCount, + minSkipCount: minSkipCount ?? this.minSkipCount, + maxSkipCount: maxSkipCount ?? this.maxSkipCount, minYear: minYear ?? this.minYear, maxYear: maxYear ?? this.maxYear, limit: limit ?? this.limit, @@ -3015,6 +3043,8 @@ class MoorSmartList extends DataClass implements Insertable { ..write('maxLikeCount: $maxLikeCount, ') ..write('minPlayCount: $minPlayCount, ') ..write('maxPlayCount: $maxPlayCount, ') + ..write('minSkipCount: $minSkipCount, ') + ..write('maxSkipCount: $maxSkipCount, ') ..write('minYear: $minYear, ') ..write('maxYear: $maxYear, ') ..write('limit: $limit, ') @@ -3044,15 +3074,20 @@ class MoorSmartList extends DataClass implements Insertable { $mrjc( maxPlayCount.hashCode, $mrjc( - minYear.hashCode, + minSkipCount.hashCode, $mrjc( - maxYear.hashCode, + maxSkipCount.hashCode, $mrjc( - limit.hashCode, + minYear.hashCode, $mrjc( - orderCriteria.hashCode, - orderDirections - .hashCode)))))))))))))); + maxYear.hashCode, + $mrjc( + limit.hashCode, + $mrjc( + orderCriteria + .hashCode, + orderDirections + .hashCode)))))))))))))))); @override bool operator ==(Object other) => identical(this, other) || @@ -3066,6 +3101,8 @@ class MoorSmartList extends DataClass implements Insertable { other.maxLikeCount == this.maxLikeCount && other.minPlayCount == this.minPlayCount && other.maxPlayCount == this.maxPlayCount && + other.minSkipCount == this.minSkipCount && + other.maxSkipCount == this.maxSkipCount && other.minYear == this.minYear && other.maxYear == this.maxYear && other.limit == this.limit && @@ -3083,6 +3120,8 @@ class SmartListsCompanion extends UpdateCompanion { final Value maxLikeCount; final Value minPlayCount; final Value maxPlayCount; + final Value minSkipCount; + final Value maxSkipCount; final Value minYear; final Value maxYear; final Value limit; @@ -3098,6 +3137,8 @@ class SmartListsCompanion extends UpdateCompanion { this.maxLikeCount = const Value.absent(), this.minPlayCount = const Value.absent(), this.maxPlayCount = const Value.absent(), + this.minSkipCount = const Value.absent(), + this.maxSkipCount = const Value.absent(), this.minYear = const Value.absent(), this.maxYear = const Value.absent(), this.limit = const Value.absent(), @@ -3114,6 +3155,8 @@ class SmartListsCompanion extends UpdateCompanion { this.maxLikeCount = const Value.absent(), this.minPlayCount = const Value.absent(), this.maxPlayCount = const Value.absent(), + this.minSkipCount = const Value.absent(), + this.maxSkipCount = const Value.absent(), this.minYear = const Value.absent(), this.maxYear = const Value.absent(), this.limit = const Value.absent(), @@ -3132,6 +3175,8 @@ class SmartListsCompanion extends UpdateCompanion { Expression? maxLikeCount, Expression? minPlayCount, Expression? maxPlayCount, + Expression? minSkipCount, + Expression? maxSkipCount, Expression? minYear, Expression? maxYear, Expression? limit, @@ -3148,6 +3193,8 @@ class SmartListsCompanion extends UpdateCompanion { if (maxLikeCount != null) 'max_like_count': maxLikeCount, if (minPlayCount != null) 'min_play_count': minPlayCount, if (maxPlayCount != null) 'max_play_count': maxPlayCount, + if (minSkipCount != null) 'min_skip_count': minSkipCount, + if (maxSkipCount != null) 'max_skip_count': maxSkipCount, if (minYear != null) 'min_year': minYear, if (maxYear != null) 'max_year': maxYear, if (limit != null) 'limit': limit, @@ -3166,6 +3213,8 @@ class SmartListsCompanion extends UpdateCompanion { Value? maxLikeCount, Value? minPlayCount, Value? maxPlayCount, + Value? minSkipCount, + Value? maxSkipCount, Value? minYear, Value? maxYear, Value? limit, @@ -3181,6 +3230,8 @@ class SmartListsCompanion extends UpdateCompanion { maxLikeCount: maxLikeCount ?? this.maxLikeCount, minPlayCount: minPlayCount ?? this.minPlayCount, maxPlayCount: maxPlayCount ?? this.maxPlayCount, + minSkipCount: minSkipCount ?? this.minSkipCount, + maxSkipCount: maxSkipCount ?? this.maxSkipCount, minYear: minYear ?? this.minYear, maxYear: maxYear ?? this.maxYear, limit: limit ?? this.limit, @@ -3219,6 +3270,12 @@ class SmartListsCompanion extends UpdateCompanion { if (maxPlayCount.present) { map['max_play_count'] = Variable(maxPlayCount.value); } + if (minSkipCount.present) { + map['min_skip_count'] = Variable(minSkipCount.value); + } + if (maxSkipCount.present) { + map['max_skip_count'] = Variable(maxSkipCount.value); + } if (minYear.present) { map['min_year'] = Variable(minYear.value); } @@ -3249,6 +3306,8 @@ class SmartListsCompanion extends UpdateCompanion { ..write('maxLikeCount: $maxLikeCount, ') ..write('minPlayCount: $minPlayCount, ') ..write('maxPlayCount: $maxPlayCount, ') + ..write('minSkipCount: $minSkipCount, ') + ..write('maxSkipCount: $maxSkipCount, ') ..write('minYear: $minYear, ') ..write('maxYear: $maxYear, ') ..write('limit: $limit, ') @@ -3355,6 +3414,30 @@ class $SmartListsTable extends SmartLists ); } + final VerificationMeta _minSkipCountMeta = + const VerificationMeta('minSkipCount'); + @override + late final GeneratedIntColumn minSkipCount = _constructMinSkipCount(); + GeneratedIntColumn _constructMinSkipCount() { + return GeneratedIntColumn( + 'min_skip_count', + $tableName, + true, + ); + } + + final VerificationMeta _maxSkipCountMeta = + const VerificationMeta('maxSkipCount'); + @override + late final GeneratedIntColumn maxSkipCount = _constructMaxSkipCount(); + GeneratedIntColumn _constructMaxSkipCount() { + return GeneratedIntColumn( + 'max_skip_count', + $tableName, + true, + ); + } + final VerificationMeta _minYearMeta = const VerificationMeta('minYear'); @override late final GeneratedIntColumn minYear = _constructMinYear(); @@ -3423,6 +3506,8 @@ class $SmartListsTable extends SmartLists maxLikeCount, minPlayCount, maxPlayCount, + minSkipCount, + maxSkipCount, minYear, maxYear, limit, @@ -3491,6 +3576,18 @@ class $SmartListsTable extends SmartLists maxPlayCount.isAcceptableOrUnknown( data['max_play_count']!, _maxPlayCountMeta)); } + if (data.containsKey('min_skip_count')) { + context.handle( + _minSkipCountMeta, + minSkipCount.isAcceptableOrUnknown( + data['min_skip_count']!, _minSkipCountMeta)); + } + if (data.containsKey('max_skip_count')) { + context.handle( + _maxSkipCountMeta, + maxSkipCount.isAcceptableOrUnknown( + data['max_skip_count']!, _maxSkipCountMeta)); + } if (data.containsKey('min_year')) { context.handle(_minYearMeta, minYear.isAcceptableOrUnknown(data['min_year']!, _minYearMeta)); @@ -4153,6 +4250,188 @@ class $PlaylistEntriesTable extends PlaylistEntries } } +class BlockSkippedSong extends DataClass + implements Insertable { + final bool enabled; + final int threshold; + BlockSkippedSong({required this.enabled, required this.threshold}); + factory BlockSkippedSong.fromData( + Map data, GeneratedDatabase db, + {String? prefix}) { + final effectivePrefix = prefix ?? ''; + return BlockSkippedSong( + enabled: const BoolType() + .mapFromDatabaseResponse(data['${effectivePrefix}enabled'])!, + threshold: const IntType() + .mapFromDatabaseResponse(data['${effectivePrefix}threshold'])!, + ); + } + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['enabled'] = Variable(enabled); + map['threshold'] = Variable(threshold); + return map; + } + + BlockSkippedSongsCompanion toCompanion(bool nullToAbsent) { + return BlockSkippedSongsCompanion( + enabled: Value(enabled), + threshold: Value(threshold), + ); + } + + factory BlockSkippedSong.fromJson(Map json, + {ValueSerializer? serializer}) { + serializer ??= moorRuntimeOptions.defaultSerializer; + return BlockSkippedSong( + enabled: serializer.fromJson(json['enabled']), + threshold: serializer.fromJson(json['threshold']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= moorRuntimeOptions.defaultSerializer; + return { + 'enabled': serializer.toJson(enabled), + 'threshold': serializer.toJson(threshold), + }; + } + + BlockSkippedSong copyWith({bool? enabled, int? threshold}) => + BlockSkippedSong( + enabled: enabled ?? this.enabled, + threshold: threshold ?? this.threshold, + ); + @override + String toString() { + return (StringBuffer('BlockSkippedSong(') + ..write('enabled: $enabled, ') + ..write('threshold: $threshold') + ..write(')')) + .toString(); + } + + @override + int get hashCode => $mrjf($mrjc(enabled.hashCode, threshold.hashCode)); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is BlockSkippedSong && + other.enabled == this.enabled && + other.threshold == this.threshold); +} + +class BlockSkippedSongsCompanion extends UpdateCompanion { + final Value enabled; + final Value threshold; + const BlockSkippedSongsCompanion({ + this.enabled = const Value.absent(), + this.threshold = const Value.absent(), + }); + BlockSkippedSongsCompanion.insert({ + this.enabled = const Value.absent(), + this.threshold = const Value.absent(), + }); + static Insertable custom({ + Expression? enabled, + Expression? threshold, + }) { + return RawValuesInsertable({ + if (enabled != null) 'enabled': enabled, + if (threshold != null) 'threshold': threshold, + }); + } + + BlockSkippedSongsCompanion copyWith( + {Value? enabled, Value? threshold}) { + return BlockSkippedSongsCompanion( + enabled: enabled ?? this.enabled, + threshold: threshold ?? this.threshold, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (enabled.present) { + map['enabled'] = Variable(enabled.value); + } + if (threshold.present) { + map['threshold'] = Variable(threshold.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('BlockSkippedSongsCompanion(') + ..write('enabled: $enabled, ') + ..write('threshold: $threshold') + ..write(')')) + .toString(); + } +} + +class $BlockSkippedSongsTable extends BlockSkippedSongs + with TableInfo<$BlockSkippedSongsTable, BlockSkippedSong> { + final GeneratedDatabase _db; + final String? _alias; + $BlockSkippedSongsTable(this._db, [this._alias]); + final VerificationMeta _enabledMeta = const VerificationMeta('enabled'); + @override + late final GeneratedBoolColumn enabled = _constructEnabled(); + GeneratedBoolColumn _constructEnabled() { + return GeneratedBoolColumn('enabled', $tableName, false, + defaultValue: const Constant(false)); + } + + final VerificationMeta _thresholdMeta = const VerificationMeta('threshold'); + @override + late final GeneratedIntColumn threshold = _constructThreshold(); + GeneratedIntColumn _constructThreshold() { + return GeneratedIntColumn('threshold', $tableName, false, + defaultValue: const Constant(3)); + } + + @override + List get $columns => [enabled, threshold]; + @override + $BlockSkippedSongsTable get asDslTable => this; + @override + String get $tableName => _alias ?? 'block_skipped_songs'; + @override + final String actualTableName = 'block_skipped_songs'; + @override + VerificationContext validateIntegrity(Insertable instance, + {bool isInserting = false}) { + final context = VerificationContext(); + final data = instance.toColumns(true); + if (data.containsKey('enabled')) { + context.handle(_enabledMeta, + enabled.isAcceptableOrUnknown(data['enabled']!, _enabledMeta)); + } + if (data.containsKey('threshold')) { + context.handle(_thresholdMeta, + threshold.isAcceptableOrUnknown(data['threshold']!, _thresholdMeta)); + } + return context; + } + + @override + Set get $primaryKey => {}; + @override + BlockSkippedSong map(Map data, {String? tablePrefix}) { + return BlockSkippedSong.fromData(data, _db, + prefix: tablePrefix != null ? '$tablePrefix.' : null); + } + + @override + $BlockSkippedSongsTable createAlias(String alias) { + return $BlockSkippedSongsTable(_db, alias); + } +} + abstract class _$MoorDatabase extends GeneratedDatabase { _$MoorDatabase(QueryExecutor e) : super(SqlTypeSystem.defaultInstance, e); _$MoorDatabase.connect(DatabaseConnection c) : super.connect(c); @@ -4178,6 +4457,8 @@ abstract class _$MoorDatabase extends GeneratedDatabase { late final $PlaylistsTable playlists = $PlaylistsTable(this); late final $PlaylistEntriesTable playlistEntries = $PlaylistEntriesTable(this); + late final $BlockSkippedSongsTable blockSkippedSongs = + $BlockSkippedSongsTable(this); late final PersistentStateDao persistentStateDao = PersistentStateDao(this as MoorDatabase); late final SettingsDao settingsDao = SettingsDao(this as MoorDatabase); @@ -4201,6 +4482,7 @@ abstract class _$MoorDatabase extends GeneratedDatabase { smartLists, smartListArtists, playlists, - playlistEntries + playlistEntries, + blockSkippedSongs ]; } diff --git a/lib/system/datasources/settings_data_source.dart b/lib/system/datasources/settings_data_source.dart index 4bb50bd..4465bc4 100644 --- a/lib/system/datasources/settings_data_source.dart +++ b/lib/system/datasources/settings_data_source.dart @@ -2,4 +2,9 @@ abstract class SettingsDataSource { Stream> get libraryFoldersStream; Future addLibraryFolder(String path); Future removeLibraryFolder(String path); + + Future setBlockSkippedSongs(bool enabled); + Stream get isBlockSkippedSongsEnabled; + Future setBlockSkippedSongsThreshold(int threshold); + Stream get blockSkippedSongsThreshold; } diff --git a/lib/system/models/smart_list_model.dart b/lib/system/models/smart_list_model.dart index 19ac87e..13afa49 100644 --- a/lib/system/models/smart_list_model.dart +++ b/lib/system/models/smart_list_model.dart @@ -28,6 +28,8 @@ class SmartListModel extends SmartList { maxLikeCount: moorSmartList.maxLikeCount, minPlayCount: moorSmartList.minPlayCount, maxPlayCount: moorSmartList.maxPlayCount, + minSkipCount: moorSmartList.minSkipCount, + maxSkipCount: moorSmartList.maxSkipCount, minYear: moorSmartList.minYear, maxYear: moorSmartList.maxYear, excludeBlocked: moorSmartList.excludeBlocked, @@ -57,6 +59,8 @@ class SmartListModel extends SmartList { maxPlayCount: m.Value(filter.maxPlayCount), minLikeCount: m.Value(filter.minLikeCount), maxLikeCount: m.Value(filter.maxLikeCount), + minSkipCount: m.Value(filter.minSkipCount), + maxSkipCount: m.Value(filter.maxSkipCount), minYear: m.Value(filter.minYear), maxYear: m.Value(filter.maxYear), excludeBlocked: m.Value(filter.excludeBlocked), diff --git a/lib/system/repositories/settings_repository_impl.dart b/lib/system/repositories/settings_repository_impl.dart index 7484042..2677e47 100644 --- a/lib/system/repositories/settings_repository_impl.dart +++ b/lib/system/repositories/settings_repository_impl.dart @@ -20,4 +20,20 @@ class SettingsRepositoryImpl implements SettingsRepository { if (path == null) return; await _settingsDataSource.removeLibraryFolder(path); } + + @override + Stream get blockSkippedSongsThreshold => _settingsDataSource.blockSkippedSongsThreshold; + + @override + Stream get isBlockSkippedSongsEnabled => _settingsDataSource.isBlockSkippedSongsEnabled; + + @override + Future setBlockSkippedSongsThreshold(int threshold) async { + _settingsDataSource.setBlockSkippedSongsThreshold(threshold); + } + + @override + Future setBlockSkippedSongs(bool enabled) async { + _settingsDataSource.setBlockSkippedSongs(enabled); + } }