diff --git a/app/src/main/kotlin/org/equeim/tremotesf/ui/Settings.kt b/app/src/main/kotlin/org/equeim/tremotesf/ui/Settings.kt index 824f5236..d3124a4f 100644 --- a/app/src/main/kotlin/org/equeim/tremotesf/ui/Settings.kt +++ b/app/src/main/kotlin/org/equeim/tremotesf/ui/Settings.kt @@ -28,7 +28,6 @@ import kotlinx.coroutines.withContext import org.equeim.tremotesf.R import org.equeim.tremotesf.TremotesfApplication import org.equeim.tremotesf.torrentfile.rpc.requests.torrentproperties.TorrentLimits -import org.equeim.tremotesf.ui.Settings.Property import org.equeim.tremotesf.ui.torrentslistfragment.TorrentsListFragmentViewModel import timber.log.Timber import kotlin.reflect.KClass @@ -124,41 +123,6 @@ object Settings { } } - private class EnumPrefsMapper>( - private val enumClass: Class, - @StringRes private val keyResId: Int, - @StringRes private val defaultValueResId: Int, - val enumToPrefsValue: (T) -> String, - ) { - private val enumValues = requireNotNull(enumClass.enumConstants) - - fun prefsValueToEnum(prefsValue: String): T { - enumValues.find { enumToPrefsValue(it) == prefsValue }?.let { return it } - val key = context.getString(keyResId) - Timber.e("Unknown prefs value $prefsValue for key $key and enum $enumClass") - val defaultPrefsValue = context.getString(defaultValueResId) - return enumValues.find { enumToPrefsValue(it) == defaultPrefsValue } - ?: throw IllegalStateException("Did not find value of enum $enumClass for default prefs value $defaultPrefsValue and key $key") - } - } - - private inline fun > EnumPrefsMapper( - @StringRes keyResId: Int, - @StringRes defaultValueResId: Int, - noinline enumToPrefsValue: (T) -> String, - ): EnumPrefsMapper = - EnumPrefsMapper(T::class.java, keyResId, defaultValueResId, enumToPrefsValue) - - private interface MappedPrefsEnum { - val prefsValue: String - } - - private inline fun EnumPrefsMapper( - @StringRes keyResId: Int, - @StringRes defaultValueResId: Int, - ): EnumPrefsMapper where T : MappedPrefsEnum, T : Enum = - EnumPrefsMapper(T::class.java, keyResId, defaultValueResId, MappedPrefsEnum::prefsValue) - enum class ColorTheme( @StringRes prefsValueResId: Int, @StyleRes val activityThemeResId: Int = 0, @@ -172,12 +136,12 @@ object Settings { private val colorThemeMapper = EnumPrefsMapper(R.string.prefs_color_theme_key, R.string.prefs_color_theme_default_value) - val colorTheme: MutableProperty = mutableProperty( + val colorTheme: Property = PrefsProperty( R.string.prefs_color_theme_key, R.string.prefs_color_theme_default_value ).map( - transformGetter = colorThemeMapper::prefsValueToEnum, - transformSetter = { it.prefsValue } + prefsToMapped = colorThemeMapper::prefsValueToEnum, + mappedToPrefs = ColorTheme::prefsValue ) enum class DarkThemeMode(@StringRes prefsValueResId: Int, val nightMode: Int) : @@ -201,74 +165,77 @@ object Settings { R.string.prefs_dark_theme_mode_default_value ) val darkThemeMode: Property = - property( + PrefsProperty( R.string.prefs_dark_theme_mode_key, R.string.prefs_dark_theme_mode_default_value - ).map(darkThemeModeMapper::prefsValueToEnum) + ).map(prefsToMapped = darkThemeModeMapper::prefsValueToEnum, mappedToPrefs = DarkThemeMode::prefsValue) - val torrentCompactView: Property = property( + val torrentCompactView: Property = PrefsProperty( R.string.prefs_torrent_compact_view_key, R.bool.prefs_torrent_compact_view_default_value ) - val torrentNameMultiline: Property = property( + val torrentNameMultiline: Property = PrefsProperty( R.string.prefs_torrent_name_multiline_key, R.bool.prefs_torrent_name_multiline_default_value ) val quickReturn: Property = - property(R.string.prefs_quick_return, R.bool.prefs_quick_return_default_value) + PrefsProperty(R.string.prefs_quick_return, R.bool.prefs_quick_return_default_value) - val showPersistentNotification: Property = property( + val showPersistentNotification: Property = PrefsProperty( R.string.prefs_persistent_notification_key, R.bool.prefs_persistent_notification_default_value ) - val notifyOnFinished: Property = property( + val notifyOnFinished: Property = PrefsProperty( R.string.prefs_notify_on_finished_key, R.bool.prefs_notify_on_finished_default_value ) val notifyOnAdded: Property = - property(R.string.prefs_notify_on_added_key, R.bool.prefs_notify_on_added_default_value) + PrefsProperty(R.string.prefs_notify_on_added_key, R.bool.prefs_notify_on_added_default_value) - val backgroundUpdateInterval: Property = property( + val backgroundUpdateInterval: Property = PrefsProperty( R.string.prefs_background_update_interval_key, R.string.prefs_background_update_interval_default_value - ).map { - try { - it.toLong() - } catch (ignore: NumberFormatException) { - 0 - } - } + ).map( + prefsToMapped = { + try { + it.toLong() + } catch (ignore: NumberFormatException) { + 0 + } + }, + mappedToPrefs = Long::toString + ) val notifyOnFinishedSinceLastConnection: Property = - property( + PrefsProperty( R.string.prefs_notify_on_finished_since_last_key, R.bool.prefs_notify_on_finished_since_last_default_value ) val notifyOnAddedSinceLastConnection: Property = - property( + PrefsProperty( R.string.prefs_notify_on_added_since_last_key, R.bool.prefs_notify_on_added_since_last_default_value ) - val userDismissedNotificationPermissionRequest: MutableProperty = - mutableProperty( + val userDismissedNotificationPermissionRequest: Property = + PrefsProperty( R.string.prefs_user_dismissed_notification_permission_request_key, R.bool.prefs_user_dismissed_notification_permission_request_default_value ) val deleteFiles: Property = - property(R.string.prefs_delete_files_key, R.bool.prefs_delete_files_default_value) + PrefsProperty(R.string.prefs_delete_files_key, R.bool.prefs_delete_files_default_value) val fillTorrentLinkFromKeyboard: Property = - property(R.string.prefs_link_from_clipboard_key, R.bool.prefs_link_from_clipboard_default_value) + PrefsProperty(R.string.prefs_link_from_clipboard_key, R.bool.prefs_link_from_clipboard_default_value) val rememberAddTorrentParameters: Property = - property( + PrefsProperty( R.string.prefs_remember_add_torrent_parameters_key, R.bool.prefs_remember_add_torrent_parameters_default_value ) @@ -283,12 +250,12 @@ object Settings { R.string.prefs_last_add_torrent_start_after_adding_key, R.string.prefs_last_add_torrent_start_after_adding_default_value ) - val lastAddTorrentStartAfterAdding: MutableProperty = mutableProperty( + val lastAddTorrentStartAfterAdding: Property = PrefsProperty( R.string.prefs_last_add_torrent_start_after_adding_key, R.string.prefs_last_add_torrent_start_after_adding_default_value ).map( - transformGetter = startTorrentAfterAddingMapper::prefsValueToEnum, - transformSetter = startTorrentAfterAddingMapper.enumToPrefsValue + prefsToMapped = startTorrentAfterAddingMapper::prefsValueToEnum, + mappedToPrefs = startTorrentAfterAddingMapper.enumToPrefsValue ) private val bandwidthPriorityMapper = EnumPrefsMapper( @@ -301,180 +268,183 @@ object Settings { TorrentLimits.BandwidthPriority.High -> "high" } } - val lastAddTorrentPriority: MutableProperty = mutableProperty( + val lastAddTorrentPriority: Property = PrefsProperty( R.string.prefs_last_add_torrent_priority_key, R.string.prefs_last_add_torrent_priority_default_value ).map( - transformGetter = bandwidthPriorityMapper::prefsValueToEnum, - transformSetter = bandwidthPriorityMapper.enumToPrefsValue + prefsToMapped = bandwidthPriorityMapper::prefsValueToEnum, + mappedToPrefs = bandwidthPriorityMapper.enumToPrefsValue ) - val torrentsSortMode: MutableProperty = - mutableProperty( + val torrentsSortMode: Property = + PrefsProperty( R.string.torrents_sort_mode_key, R.integer.torrents_sort_mode_default_value ).map( - transformGetter = { TorrentsListFragmentViewModel.SortMode.entries.getOrElse(it) { TorrentsListFragmentViewModel.SortMode.DEFAULT } }, - transformSetter = { it.ordinal } + prefsToMapped = { TorrentsListFragmentViewModel.SortMode.entries.getOrElse(it) { TorrentsListFragmentViewModel.SortMode.DEFAULT } }, + mappedToPrefs = { it.ordinal } ) - val torrentsSortOrder: MutableProperty = - mutableProperty( + val torrentsSortOrder: Property = + PrefsProperty( R.string.torrents_sort_order_key, R.integer.torrents_sort_order_default_value ).map( - transformGetter = { TorrentsListFragmentViewModel.SortOrder.entries.getOrElse(it) { TorrentsListFragmentViewModel.SortOrder.DEFAULT } }, - transformSetter = { it.ordinal } + prefsToMapped = { TorrentsListFragmentViewModel.SortOrder.entries.getOrElse(it) { TorrentsListFragmentViewModel.SortOrder.DEFAULT } }, + mappedToPrefs = { it.ordinal } ) - val torrentsStatusFilter: MutableProperty = - mutableProperty( + val torrentsStatusFilter: Property = + PrefsProperty( R.string.torrents_status_filter_key, R.integer.torrents_status_filter_default_value ).map( - transformGetter = { TorrentsListFragmentViewModel.StatusFilterMode.entries.getOrElse(it) { TorrentsListFragmentViewModel.StatusFilterMode.DEFAULT } }, - transformSetter = { it.ordinal } + prefsToMapped = { TorrentsListFragmentViewModel.StatusFilterMode.entries.getOrElse(it) { TorrentsListFragmentViewModel.StatusFilterMode.DEFAULT } }, + mappedToPrefs = { it.ordinal } ) - val torrentsTrackerFilter: MutableProperty = mutableProperty( + val torrentsTrackerFilter: Property = PrefsProperty( R.string.torrents_tracker_filter_key, R.string.torrents_tracker_filter_default_value ) - val torrentsDirectoryFilter: MutableProperty = mutableProperty( + val torrentsDirectoryFilter: Property = PrefsProperty( R.string.torrents_directory_filter_key, R.string.torrents_directory_filter_default_value ) - private fun property( - kClass: KClass, - @StringRes keyResId: Int, - @AnyRes defaultValueResId: Int, - key: String = context.getString(keyResId), - ): Property { - @Suppress("IMPLICIT_CAST_TO_ANY", "UNCHECKED_CAST") - val defaultValue = when (kClass) { - Boolean::class -> context.resources.getBoolean(defaultValueResId) - Float::class -> context.resources.getDimension(defaultValueResId) - Int::class -> context.resources.getInteger(defaultValueResId) - Long::class -> context.resources.getInteger(defaultValueResId).toLong() - String::class -> context.getString(defaultValueResId) - Set::class -> context.resources.getStringArray(defaultValueResId).toSet() - else -> throw IllegalArgumentException("Unsupported property type $kClass") - } as T - - val getter = getSharedPreferencesGetter(kClass) - - return object : Property { - override val key = key - - override suspend fun get(): T { - migrate() - return preferences.getter(key, defaultValue) - } - - override fun flow() = callbackFlow { - send(get()) - val listener = SharedPreferences.OnSharedPreferenceChangeListener { _, changedKey -> - if (changedKey == key) { - launch { send(get()) } - } - } - preferences.registerOnSharedPreferenceChangeListener(listener) - awaitClose { preferences.unregisterOnSharedPreferenceChangeListener(listener) } - }.flowOn(Dispatchers.IO) - } + interface Property { + val key: String + suspend fun get(): T + fun flow(): Flow + suspend fun set(value: T) } - private inline fun property( + private inline fun PrefsProperty( @StringRes keyResId: Int, @AnyRes defaultValueResId: Int, - ): Property = property(T::class, keyResId, defaultValueResId) + ) = PrefsProperty(T::class, keyResId, defaultValueResId) - private fun mutableProperty( + private class PrefsProperty( kClass: KClass, @StringRes keyResId: Int, @AnyRes defaultValueResId: Int, - ): MutableProperty { - val key = context.getString(keyResId) - val property = property(kClass, keyResId, defaultValueResId, key = key) - val setter = getSharedPreferencesSetter(kClass) - return object : MutableProperty, Property by property { - override suspend fun set(value: T) = withContext(Dispatchers.IO) { - migrate() - preferences.setter(key, value) + ) : Property { + private val defaultValue = getDefaultValue(kClass, defaultValueResId) + private val getter = getSharedPreferencesGetter(kClass) + private val setter = getSharedPreferencesSetter(kClass) + + override val key: String = context.getString(keyResId) + + override suspend fun get(): T = withContext(Dispatchers.IO) { + migrate() + preferences.getter(key, defaultValue) + } + + override fun flow(): Flow = callbackFlow { + send(get()) + val listener = SharedPreferences.OnSharedPreferenceChangeListener { _, changedKey -> + if (changedKey == key) { + launch { send(get()) } + } } + preferences.registerOnSharedPreferenceChangeListener(listener) + awaitClose { preferences.unregisterOnSharedPreferenceChangeListener(listener) } + }.flowOn(Dispatchers.IO) + + override suspend fun set(value: T) = withContext(Dispatchers.IO) { + migrate() + preferences.edit { setter(key, value) } } - } - private inline fun mutableProperty( - @StringRes keyResId: Int, - @AnyRes defaultValueResId: Int, - ): MutableProperty = mutableProperty(T::class, keyResId, defaultValueResId) + private companion object { + private fun getDefaultValue(kClass: KClass, @AnyRes defaultValueResId: Int): T { + @Suppress("IMPLICIT_CAST_TO_ANY", "UNCHECKED_CAST") + return when (kClass) { + Boolean::class -> context.resources.getBoolean(defaultValueResId) + Float::class -> context.resources.getDimension(defaultValueResId) + Int::class -> context.resources.getInteger(defaultValueResId) + Long::class -> context.resources.getInteger(defaultValueResId).toLong() + String::class -> context.getString(defaultValueResId) + Set::class -> context.resources.getStringArray(defaultValueResId).toSet() + else -> throw IllegalArgumentException("Unsupported property type $kClass") + } as T + } - interface Property { - val key: String - suspend fun get(): T - fun flow(): Flow - } + private fun getSharedPreferencesGetter(kClass: KClass): SharedPreferences.(String, T) -> T { + @Suppress("UNCHECKED_CAST") + return when (kClass) { + Boolean::class -> SharedPreferences::getBoolean + Float::class -> SharedPreferences::getFloat + Int::class -> SharedPreferences::getInt + Long::class -> SharedPreferences::getLong + String::class -> SharedPreferences::getString + Set::class -> SharedPreferences::getStringSet + else -> throw IllegalArgumentException("Unsupported property type $kClass") + } as SharedPreferences.(String, T) -> T + } - interface MutableProperty : Property { - suspend fun set(value: T) + private fun getSharedPreferencesSetter(kClass: KClass): SharedPreferences.Editor.(String, T) -> Unit { + @Suppress("UNCHECKED_CAST") + return when (kClass) { + Boolean::class -> SharedPreferences.Editor::putBoolean + Float::class -> SharedPreferences.Editor::putFloat + Int::class -> SharedPreferences.Editor::putInt + Long::class -> SharedPreferences.Editor::putLong + String::class -> SharedPreferences.Editor::putString + Set::class -> SharedPreferences.Editor::putStringSet + else -> throw IllegalArgumentException("Unsupported property type $kClass") + } as SharedPreferences.Editor.(String, T) -> Unit + } + } } -} -private fun getSharedPreferencesGetter(kClass: KClass): suspend SharedPreferences.(String, T) -> T { - @Suppress("UNCHECKED_CAST") - val baseGetter = when (kClass) { - Boolean::class -> SharedPreferences::getBoolean - Float::class -> SharedPreferences::getFloat - Int::class -> SharedPreferences::getInt - Long::class -> SharedPreferences::getLong - String::class -> SharedPreferences::getString - Set::class -> SharedPreferences::getStringSet - else -> throw IllegalArgumentException("Unsupported property type $kClass") - } as SharedPreferences.(String, T) -> T - return { key, defaultValue -> - withContext(Dispatchers.IO) { - baseGetter(key, defaultValue) - } + private fun Property.map(prefsToMapped: (T) -> R, mappedToPrefs: (R) -> T) = + MappedProperty(this, prefsToMapped, mappedToPrefs) + + private class MappedProperty( + private val prefsProperty: Property, + private val prefsToMapped: (T) -> R, + private val mappedToPrefs: (R) -> T, + ) : Property { + override val key: String get() = prefsProperty.key + override suspend fun get(): R = prefsToMapped(prefsProperty.get()) + override fun flow(): Flow = prefsProperty.flow().map(prefsToMapped) + override suspend fun set(value: R) = prefsProperty.set(mappedToPrefs(value)) } -} -private fun getSharedPreferencesSetter(kClass: KClass): suspend SharedPreferences.(String, T) -> Unit { - @Suppress("UNCHECKED_CAST") - val baseSetter = when (kClass) { - Boolean::class -> SharedPreferences.Editor::putBoolean - Float::class -> SharedPreferences.Editor::putFloat - Int::class -> SharedPreferences.Editor::putInt - Long::class -> SharedPreferences.Editor::putLong - String::class -> SharedPreferences.Editor::putString - Set::class -> SharedPreferences.Editor::putStringSet - else -> throw IllegalArgumentException("Unsupported property type $kClass") - } as SharedPreferences.Editor.(String, T) -> Unit - return { key, value -> - withContext(Dispatchers.IO) { - edit { - baseSetter(key, value) - } + private class EnumPrefsMapper>( + private val enumClass: Class, + @StringRes private val keyResId: Int, + @StringRes private val defaultValueResId: Int, + val enumToPrefsValue: (T) -> String, + ) { + private val enumValues = requireNotNull(enumClass.enumConstants) + + fun prefsValueToEnum(prefsValue: String): T { + enumValues.find { enumToPrefsValue(it) == prefsValue }?.let { return it } + val key = context.getString(keyResId) + Timber.e("Unknown prefs value $prefsValue for key $key and enum $enumClass") + val defaultPrefsValue = context.getString(defaultValueResId) + return enumValues.find { enumToPrefsValue(it) == defaultPrefsValue } + ?: throw IllegalStateException("Did not find value of enum $enumClass for default prefs value $defaultPrefsValue and key $key") } } -} -private fun Property.map(transform: suspend (T) -> R) = - object : Property { - override val key = this@map.key - override suspend fun get() = transform(this@map.get()) - override fun flow() = this@map.flow().map(transform) - } + private inline fun > EnumPrefsMapper( + @StringRes keyResId: Int, + @StringRes defaultValueResId: Int, + noinline enumToPrefsValue: (T) -> String, + ): EnumPrefsMapper = + EnumPrefsMapper(T::class.java, keyResId, defaultValueResId, enumToPrefsValue) -private fun Settings.MutableProperty.map( - transformGetter: suspend (T) -> R, - transformSetter: suspend (R) -> T, -) = - object : Settings.MutableProperty { - override val key = this@map.key - override suspend fun get() = transformGetter(this@map.get()) - override fun flow(): Flow = this@map.flow().map(transformGetter) - override suspend fun set(value: R) = this@map.set(transformSetter(value)) + private interface MappedPrefsEnum { + val prefsValue: String } + + private inline fun EnumPrefsMapper( + @StringRes keyResId: Int, + @StringRes defaultValueResId: Int, + ): EnumPrefsMapper where T : MappedPrefsEnum, T : Enum = + EnumPrefsMapper(T::class.java, keyResId, defaultValueResId, MappedPrefsEnum::prefsValue) +} diff --git a/app/src/main/kotlin/org/equeim/tremotesf/ui/torrentslistfragment/TorrentsListFragmentViewModel.kt b/app/src/main/kotlin/org/equeim/tremotesf/ui/torrentslistfragment/TorrentsListFragmentViewModel.kt index 6613ff9f..4cd3eeb9 100644 --- a/app/src/main/kotlin/org/equeim/tremotesf/ui/torrentslistfragment/TorrentsListFragmentViewModel.kt +++ b/app/src/main/kotlin/org/equeim/tremotesf/ui/torrentslistfragment/TorrentsListFragmentViewModel.kt @@ -139,7 +139,7 @@ class TorrentsListFragmentViewModel(application: Application, savedStateHandle: val directoryFilter: Flow = Settings.torrentsDirectoryFilter.flow() fun setDirectoryFilter(filter: String) = Settings.torrentsDirectoryFilter.setAsync(filter) - private fun Settings.MutableProperty.setAsync(value: T) { + private fun Settings.Property.setAsync(value: T) { viewModelScope.launch { set(value) } }