From a07f2f20fcdd0113513eb68f96edd3fdc8723a8d Mon Sep 17 00:00:00 2001 From: charlag Date: Sat, 21 May 2022 22:33:13 +0200 Subject: [PATCH] WIP Prefs: ListPreference, autoupdate --- .../preference/PreferencesActivity.kt | 66 ++++++++---- .../preference/PreferencesFragment.kt | 64 +++++++++-- .../tusky/settings/SettingsDSL.kt | 101 +++++++++++++++--- 3 files changed, 188 insertions(+), 43 deletions(-) diff --git a/app/src/main/java/com/keylesspalace/tusky/components/preference/PreferencesActivity.kt b/app/src/main/java/com/keylesspalace/tusky/components/preference/PreferencesActivity.kt index ee79a624b..54afbd85a 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/preference/PreferencesActivity.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/preference/PreferencesActivity.kt @@ -22,6 +22,7 @@ import android.os.Bundle import android.util.Log import androidx.fragment.app.Fragment import androidx.fragment.app.commit +import androidx.lifecycle.lifecycleScope import androidx.preference.PreferenceManager import com.keylesspalace.tusky.BaseActivity import com.keylesspalace.tusky.MainActivity @@ -30,10 +31,13 @@ import com.keylesspalace.tusky.appstore.EventHub import com.keylesspalace.tusky.appstore.PreferenceChangedEvent import com.keylesspalace.tusky.databinding.ActivityPreferencesBinding import com.keylesspalace.tusky.settings.PrefKeys +import com.keylesspalace.tusky.settings.PrefStore +import com.keylesspalace.tusky.settings.get import com.keylesspalace.tusky.util.ThemeUtils import com.keylesspalace.tusky.util.getNonNullString import dagger.android.DispatchingAndroidInjector import dagger.android.HasAndroidInjector +import kotlinx.coroutines.launch import javax.inject.Inject class PreferencesActivity : @@ -47,6 +51,10 @@ class PreferencesActivity : @Inject lateinit var androidInjector: DispatchingAndroidInjector + @Inject + lateinit var prefStore: PrefStore + + private var restartActivitiesOnExit: Boolean = false override fun onCreate(savedInstanceState: Bundle?) { @@ -93,16 +101,49 @@ class PreferencesActivity : } restartActivitiesOnExit = intent.getBooleanExtra("restart", false) + + lifecycleScope.launch { + // When certain preferences are changed we want to apply the changes + // TODO: Maybe this is not the best place to do it and we should trigger + // it from fragment? + var lastValue = prefStore.get() + prefStore.data.collect { newValue -> + if (lastValue.appTheme != newValue.appTheme) { + Log.d("activeTheme", newValue.appTheme) + ThemeUtils.setAppNightMode(newValue.appTheme) + restartActivitiesOnExit = true + restartCurrentActivity() + } else if (lastValue.statusTextSize != newValue.statusTextSize || + lastValue.useAbsoluteTime != newValue.useAbsoluteTime || + lastValue.showBotOverlay != newValue.showBotOverlay || + lastValue.animateAvatars != newValue.animateAvatars || + lastValue.useBlurhash != newValue.useBlurhash || + lastValue.showCardsInTimelines != newValue.showCardsInTimelines || + lastValue.confirmReblogs != newValue.confirmReblogs || + lastValue.enableSwipeForTabs != newValue.enableSwipeForTabs || + lastValue.mainNavPosition != newValue.mainNavPosition || + lastValue.hideTopToolbar != newValue.hideTopToolbar + ) { + restartActivitiesOnExit = true + } else if (lastValue.language != newValue.language) { + restartActivitiesOnExit = true + restartCurrentActivity() + } + lastValue = newValue + } + } } override fun onResume() { super.onResume() - PreferenceManager.getDefaultSharedPreferences(this).registerOnSharedPreferenceChangeListener(this) + PreferenceManager.getDefaultSharedPreferences(this) + .registerOnSharedPreferenceChangeListener(this) } override fun onPause() { super.onPause() - PreferenceManager.getDefaultSharedPreferences(this).unregisterOnSharedPreferenceChangeListener(this) + PreferenceManager.getDefaultSharedPreferences(this) + .unregisterOnSharedPreferenceChangeListener(this) } private fun saveInstanceState(outState: Bundle) { @@ -115,26 +156,7 @@ class PreferencesActivity : } override fun onSharedPreferenceChanged(sharedPreferences: SharedPreferences, key: String) { - when (key) { - "appTheme" -> { - val theme = sharedPreferences.getNonNullString("appTheme", ThemeUtils.APP_THEME_DEFAULT) - Log.d("activeTheme", theme) - ThemeUtils.setAppNightMode(theme) - - restartActivitiesOnExit = true - this.restartCurrentActivity() - } - "statusTextSize", "absoluteTimeView", "showBotOverlay", "animateGifAvatars", "useBlurhash", - "showCardsInTimelines", "confirmReblogs", "confirmFavourites", - "enableSwipeForTabs", "mainNavPosition", PrefKeys.HIDE_TOP_TOOLBAR -> { - restartActivitiesOnExit = true - } - "language" -> { - restartActivitiesOnExit = true - this.restartCurrentActivity() - } - } - + // FIXME eventHub.dispatch(PreferenceChangedEvent(key)) } diff --git a/app/src/main/java/com/keylesspalace/tusky/components/preference/PreferencesFragment.kt b/app/src/main/java/com/keylesspalace/tusky/components/preference/PreferencesFragment.kt index b7043c737..19e173dd8 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/preference/PreferencesFragment.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/preference/PreferencesFragment.kt @@ -25,10 +25,13 @@ import androidx.lifecycle.lifecycleScope import com.keylesspalace.tusky.R import com.keylesspalace.tusky.db.AccountManager import com.keylesspalace.tusky.di.Injectable +import com.keylesspalace.tusky.settings.AppTheme import com.keylesspalace.tusky.settings.PrefData import com.keylesspalace.tusky.settings.PrefStore import com.keylesspalace.tusky.settings.getBlocking +import com.keylesspalace.tusky.settings.listPreference import com.keylesspalace.tusky.settings.makePreferenceScreen +import com.keylesspalace.tusky.settings.named import com.keylesspalace.tusky.settings.preferenceCategory import com.keylesspalace.tusky.settings.switchPreference import com.keylesspalace.tusky.util.ThemeUtils @@ -36,7 +39,9 @@ import com.mikepenz.iconics.IconicsDrawable import com.mikepenz.iconics.typeface.library.googlematerial.GoogleMaterial import com.mikepenz.iconics.utils.colorInt import com.mikepenz.iconics.utils.sizePx +import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext import okhttp3.OkHttpClient import javax.inject.Inject @@ -53,6 +58,7 @@ class PreferencesFragment : Fragment(), Injectable { private val iconSize by lazy { resources.getDimensionPixelSize(R.dimen.preference_icon_size) } + private var updateTrigger: (() -> Unit)? = null private fun updatePrefs(updater: (PrefData) -> PrefData) { lifecycleScope.launch { prefStore.updateData(updater) @@ -70,33 +76,73 @@ class PreferencesFragment : Fragment(), Injectable { prefStore.data.collect { prefs = it // trigger update? + withContext(Dispatchers.Main) { + updateTrigger?.invoke() + } } } - makePreferenceScreen(view) { + this.updateTrigger = makePreferenceScreen(view) { preferenceCategory(R.string.pref_title_appearance_settings) { - switchPreference(getString(R.string.pref_title_hide_top_toolbar), prefs::hideTopToolbar) { + val themeOptions = listOf( + AppTheme.NIGHT.value named R.string.app_them_dark, + AppTheme.DAY.value named R.string.app_theme_light, + AppTheme.BLACK.value named R.string.app_theme_black, + AppTheme.AUTO.value named R.string.app_theme_auto, + AppTheme.AUTO_SYSTEM.value named R.string.app_theme_system, + ) + listPreference( + getString(R.string.pref_title_app_theme), + themeOptions, + { prefs.appTheme }) { + updatePrefs { data -> data.copy(appTheme = it) } + } + switchPreference( + getString(R.string.pref_title_hide_top_toolbar), + { prefs.hideTopToolbar } + ) { updatePrefs { data -> data.copy(hideTopToolbar = it) } } - switchPreference(getString(R.string.pref_title_hide_follow_button), prefs::hideFab) { + switchPreference( + getString(R.string.pref_title_hide_follow_button), + { prefs.hideFab } + ) { updatePrefs { data -> data.copy(hideFab = it) } } - switchPreference(getString(R.string.pref_title_absolute_time), prefs::useAbsoluteTime) { + switchPreference( + getString(R.string.pref_title_absolute_time), + { prefs.useAbsoluteTime } + ) { updatePrefs { data -> data.copy(useAbsoluteTime = it) } } - switchPreference(getString(R.string.pref_title_bot_overlay), prefs::showBotOverlay) { + switchPreference( + getString(R.string.pref_title_bot_overlay), + { prefs.showBotOverlay } + ) { updatePrefs { data -> data.copy(showBotOverlay = it) } } - switchPreference(getString(R.string.pref_title_animate_gif_avatars), prefs::animateAvatars) { + switchPreference( + getString(R.string.pref_title_animate_gif_avatars), + { prefs.animateAvatars } + ) { updatePrefs { data -> data.copy(animateAvatars = it) } } - switchPreference(getString(R.string.pref_title_animate_custom_emojis), prefs::animateEmojis) { + switchPreference( + getString(R.string.pref_title_animate_custom_emojis), + { prefs.animateEmojis } + ) { updatePrefs { data -> data.copy(animateEmojis = it) } } - switchPreference(getString(R.string.pref_title_gradient_for_media), prefs::useBlurhash) { + switchPreference( + getString(R.string.pref_title_gradient_for_media), + { prefs.useBlurhash } + ) { updatePrefs { data -> data.copy(useBlurhash = it) } } - switchPreference(getString(R.string.pref_title_show_cards_in_timelines), prefs::showCardsInTimelines) { + switchPreference( + getString(R.string.pref_title_show_cards_in_timelines), + { prefs.showCardsInTimelines } + ) { updatePrefs { data -> data.copy(showCardsInTimelines = it) } } } diff --git a/app/src/main/java/com/keylesspalace/tusky/settings/SettingsDSL.kt b/app/src/main/java/com/keylesspalace/tusky/settings/SettingsDSL.kt index acdbd2ac3..cba13650c 100644 --- a/app/src/main/java/com/keylesspalace/tusky/settings/SettingsDSL.kt +++ b/app/src/main/java/com/keylesspalace/tusky/settings/SettingsDSL.kt @@ -11,16 +11,21 @@ import android.widget.ImageView import android.widget.LinearLayout import android.widget.TextView import androidx.annotation.StringRes +import androidx.appcompat.app.AlertDialog import androidx.fragment.app.Fragment import at.connyduck.sparkbutton.helpers.Utils import com.google.android.material.switchmaterial.SwitchMaterial import com.keylesspalace.tusky.R import com.keylesspalace.tusky.util.ThemeUtils +typealias Update = () -> Unit + class PreferenceParent( val context: Context, - val addPref: (pref: View) -> Unit -) + val registerUpdate: (update: Update) -> Unit, + val addPref: (pref: View) -> Unit, +) { +} //inline fun PreferenceParent.preference(builder: Preference.() -> Unit): Preference { // val pref = Preference(context) @@ -103,6 +108,16 @@ fun PreferenceParent.checkBoxPreference( addPref(layout) } +private fun TextView.setTextAppearanceRef(ref: Int) { + val refs = TypedValue() + context.theme.resolveAttribute(ref, refs, true) + setTextAppearance(context, refs.resourceId) +} + +private fun TextView.setTextColorRef(ref: Int) { + setTextColor(ThemeUtils.getColor(context, ref)) +} + fun PreferenceParent.switchPreference( title: String, isChecked: () -> Boolean, @@ -111,12 +126,8 @@ fun PreferenceParent.switchPreference( val layout = itemLayout(context) val textView = TextView(context).apply { text = title - val refs = TypedValue() - context.theme.resolveAttribute(android.R.attr.textAppearanceListItem, refs, true) - setTextAppearance(context, refs.resourceId) - setTextColor(ThemeUtils.getColor(context, android.R.attr.textColorPrimary)) - // this is in resource but not at runtime? -// setSingleLine() + setTextAppearanceRef(android.R.attr.textAppearanceListItem) + setTextColorRef(android.R.attr.textColorPrimary) ellipsize = TextUtils.TruncateAt.MARQUEE } textView.layoutParams = LinearLayout.LayoutParams( @@ -140,13 +151,70 @@ fun PreferenceParent.switchPreference( layout.addView(switchLayout) val switch = SwitchMaterial(context) - switch.isChecked = isChecked() + registerUpdate { + switch.isChecked = isChecked() + } switch.setOnCheckedChangeListener { _, isChecked -> onSelection(isChecked) } switchLayout.addView(switch) addPref(layout) } +data class PreferenceOption(@StringRes val name: Int, val value: T) +infix fun T.named(@StringRes name: Int) = PreferenceOption(name, this) + +fun PreferenceParent.listPreference( + title: String, + options: List>, + selected: () -> T, + onSelection: (T) -> Unit, +) { + val layout = itemLayout(context).apply { + isClickable = true + val outValue = TypedValue() + context.theme.resolveAttribute(android.R.attr.selectableItemBackground, outValue, true) + setBackgroundResource(outValue.resourceId) + setPadding(dpToPx(16), dpToPx(16), dpToPx(16), dpToPx(16)) + } + val linearLayout = LinearLayout(context).apply { + orientation = LinearLayout.VERTICAL + } + + + val titleView = TextView(context).apply { + text = title + setTextAppearanceRef(android.R.attr.textAppearanceListItem) + setTextColorRef(android.R.attr.textColorPrimary) + } + linearLayout.addView(titleView) + + val optionView = TextView(context) + linearLayout.addView(optionView) + + layout.addView(linearLayout) + + addPref(layout) + + registerUpdate { + val selectedOptionIndex = options.indexOfFirst { it.value == selected() } + + optionView.setText(options[selectedOptionIndex].name) + + layout.setOnClickListener { + AlertDialog.Builder(context) + .setSingleChoiceItems( + options.map { context.getString(it.name) }.toTypedArray(), + selectedOptionIndex, + ) { dialog, wh -> + onSelection(options[wh].value) + dialog.dismiss() + } + .setCancelable(true) + .show() + } + } +} + fun PreferenceParent.preferenceCategory( @StringRes title: Int, @@ -175,19 +243,28 @@ fun PreferenceParent.preferenceCategory( } titleLayout.addView(titleView) - val newParent = PreferenceParent(context) { categoryLayout.addView(it) } + val newParent = PreferenceParent(context, registerUpdate) { categoryLayout.addView(it) } builder(newParent) } inline fun Fragment.makePreferenceScreen( viewGroup: ViewGroup, builder: PreferenceParent.() -> Unit -) { +): (() -> Unit) { val context = requireContext() - val parent = PreferenceParent(context) { viewGroup.addView(it) } + val updates = mutableListOf() + val updateTrigger = { + for (update in updates) { + update() + } + } + val parent = PreferenceParent(context, updates::add) { viewGroup.addView(it) } // For some functions (like dependencies) it's much easier for us if we attach screen first // and change it later builder(parent) + // Run once to update all views + updateTrigger() + return updateTrigger } fun View.dpToPx(dp: Int) = Utils.dpToPx(this.context, dp)