Implement theme customization

Implement the ability to change the theme to auto/light/dark.
This commit is contained in:
OxygenCobalt 2020-11-28 16:17:54 -07:00
parent 76c1fe1d75
commit 2dc7ba3420
16 changed files with 297 additions and 79 deletions

View file

@ -75,6 +75,9 @@ dependencies {
// Media
implementation 'androidx.media:media:1.2.0'
// Preferences
implementation 'androidx.preference:preference-ktx:1.1.1'
// --- THIRD PARTY ---
// ExoPlayer

View file

@ -5,18 +5,22 @@ import android.content.Intent
import android.util.AttributeSet
import android.view.View
import androidx.appcompat.app.AppCompatActivity
import androidx.appcompat.app.AppCompatDelegate
import org.oxycblt.auxio.playback.PlaybackService
import org.oxycblt.auxio.prefs.PrefsManager
import org.oxycblt.auxio.settings.SettingsManager
import org.oxycblt.auxio.ui.accent
// FIXME: Fix bug where fast navigation will break the animations and
// lead to nothing being displayed [Possibly Un-fixable]
// TODO: Landscape UI layouts
// FIXME: Compat issue with Versions 5 that leads to progress bar looking off
class MainActivity : AppCompatActivity(R.layout.activity_main) {
class MainActivity : AppCompatActivity(R.layout.activity_main), SettingsManager.Callback {
override fun onCreateView(name: String, context: Context, attrs: AttributeSet): View? {
val prefsManager = PrefsManager.init(this)
val settingsManager = SettingsManager.init(applicationContext)
AppCompatDelegate.setDefaultNightMode(
settingsManager.getTheme()
)
// Apply the theme
setTheme(accent.second)
@ -31,4 +35,22 @@ class MainActivity : AppCompatActivity(R.layout.activity_main) {
startService(it)
}
}
override fun onResume() {
super.onResume()
// Perform callback additions/removals in onPause/onResume so that they are always
// ran when the activity is recreated.
SettingsManager.getInstance().addCallback(this)
}
override fun onPause() {
super.onPause()
SettingsManager.getInstance().removeCallback(this)
}
override fun onThemeUpdate(value: Int) {
AppCompatDelegate.setDefaultNightMode(value)
}
}

View file

@ -13,9 +13,9 @@ import org.oxycblt.auxio.R
import org.oxycblt.auxio.music.BaseModel
import org.oxycblt.auxio.music.Header
import org.oxycblt.auxio.music.MusicStore
import org.oxycblt.auxio.prefs.PrefsManager
import org.oxycblt.auxio.recycler.ShowMode
import org.oxycblt.auxio.recycler.SortMode
import org.oxycblt.auxio.settings.SettingsManager
/**
* A [ViewModel] that manages what [LibraryFragment] is currently showing, and also the search
@ -40,7 +40,7 @@ class LibraryViewModel : ViewModel() {
val searchHasFocus: Boolean get() = mSearchHasFocus
init {
val prefsManager = PrefsManager.getInstance()
val prefsManager = SettingsManager.getInstance()
viewModelScope.launch {
mSortMode.value = withContext(Dispatchers.IO) {
@ -135,7 +135,7 @@ class LibraryViewModel : ViewModel() {
viewModelScope.launch {
withContext(Dispatchers.IO) {
val prefsManager = PrefsManager.getInstance()
val prefsManager = SettingsManager.getInstance()
prefsManager.setLibrarySortMode(mSortMode.value!!)
}

View file

@ -1,71 +0,0 @@
package org.oxycblt.auxio.prefs
import android.content.Context
import android.content.SharedPreferences
import org.oxycblt.auxio.recycler.SortMode
/**
* Wrapper around the [SharedPreferences] class that writes & reads values.
* Please run any getter/setter in a coroutine. Its not required, but it prevents slowdowns
* on older devices.
* @author OxygenCobalt
*/
class PrefsManager private constructor(context: Context) {
private val sharedPrefs = context.getSharedPreferences(
"auxio_prefs", Context.MODE_PRIVATE
)
private lateinit var mLibrarySortMode: SortMode
fun setLibrarySortMode(sortMode: SortMode) {
mLibrarySortMode = sortMode
sharedPrefs.edit()
.putInt(Keys.KEY_LIBRARY_SORT_MODE, sortMode.toConstant())
.apply()
}
fun getLibrarySortMode(): SortMode {
if (!::mLibrarySortMode.isInitialized) {
mLibrarySortMode = SortMode.fromConstant(
sharedPrefs.getInt(
Keys.KEY_LIBRARY_SORT_MODE,
SortMode.CONSTANT_ALPHA_DOWN
)
) ?: SortMode.ALPHA_DOWN
}
return mLibrarySortMode
}
companion object {
@Volatile
private lateinit var INSTANCE: PrefsManager
/**
* Init the single instance of [PrefsManager]. Done so that every object
* can have access to it regardless of if it has a context.
*/
fun init(context: Context): PrefsManager {
synchronized(this) {
INSTANCE = PrefsManager(context)
return getInstance()
}
}
/**
* Get the single instance of [PrefsManager].
*/
fun getInstance(): PrefsManager {
check(::INSTANCE.isInitialized) {
"PrefsManager must be initialized with init() before getting its instance."
}
return INSTANCE
}
}
object Keys {
const val KEY_LIBRARY_SORT_MODE = "KEY_LIBRARY_SORT_MODE"
}
}

View file

@ -0,0 +1,11 @@
package org.oxycblt.auxio.settings
import android.os.Bundle
import androidx.preference.PreferenceFragmentCompat
import org.oxycblt.auxio.R
class SettingListFragment : PreferenceFragmentCompat() {
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
setPreferencesFromResource(R.xml.prefs_main, rootKey)
}
}

View file

@ -0,0 +1,20 @@
package org.oxycblt.auxio.settings
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.fragment.app.Fragment
import org.oxycblt.auxio.databinding.FragmentSettingsBinding
class SettingsFragment : Fragment() {
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View {
val binding = FragmentSettingsBinding.inflate(inflater)
return binding.root
}
}

View file

@ -0,0 +1,118 @@
package org.oxycblt.auxio.settings
import android.content.Context
import android.content.SharedPreferences
import androidx.appcompat.app.AppCompatDelegate
import androidx.preference.PreferenceManager
import org.oxycblt.auxio.recycler.SortMode
/**
* Wrapper around the [SharedPreferences] class that writes & reads values without a context.
*
* **Note:** Run any getter in a IO coroutine if possible, as SharedPrefs will read from disk
* the first time it occurs.
* @author OxygenCobalt
*/
class SettingsManager private constructor(context: Context) : SharedPreferences.OnSharedPreferenceChangeListener {
private val sharedPrefs = PreferenceManager.getDefaultSharedPreferences(context)
init {
sharedPrefs.registerOnSharedPreferenceChangeListener(this)
}
private val callbacks = mutableListOf<Callback>()
fun addCallback(callback: Callback) {
callbacks.add(callback)
}
fun removeCallback(callback: Callback) {
callbacks.remove(callback)
}
fun setLibrarySortMode(sortMode: SortMode) {
sharedPrefs.edit()
.putInt(Keys.KEY_LIBRARY_SORT_MODE, sortMode.toConstant())
.apply()
}
fun getLibrarySortMode(): SortMode {
return SortMode.fromConstant(
sharedPrefs.getInt(
Keys.KEY_LIBRARY_SORT_MODE,
SortMode.CONSTANT_ALPHA_DOWN
)
) ?: SortMode.ALPHA_DOWN
}
fun getTheme(): Int {
// Turn the string from SharedPreferences into an actual theme value that can
// be used, as apparently the preference system provided by androidx doesn't like integers
// for some...reason.
return when (sharedPrefs.getString(Keys.KEY_THEME, Theme.THEME_AUTO)) {
Theme.THEME_AUTO -> AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM
Theme.THEME_LIGHT -> AppCompatDelegate.MODE_NIGHT_NO
Theme.THEME_DARK -> AppCompatDelegate.MODE_NIGHT_YES
else -> AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM
}
}
override fun onSharedPreferenceChanged(sharedPreferences: SharedPreferences?, key: String?) {
when (key) {
Keys.KEY_THEME -> {
callbacks.forEach {
it.onThemeUpdate(getTheme())
}
}
}
}
companion object {
@Volatile
private lateinit var INSTANCE: SettingsManager
/**
* Init the single instance of [SettingsManager]. Done so that every object
* can have access to it regardless of if it has a context.
*/
fun init(context: Context): SettingsManager {
if (!::INSTANCE.isInitialized) {
synchronized(this) {
INSTANCE = SettingsManager(context)
}
}
return getInstance()
}
/**
* Get the single instance of [SettingsManager].
*/
fun getInstance(): SettingsManager {
check(::INSTANCE.isInitialized) {
"PrefsManager must be initialized with init() before getting its instance."
}
return INSTANCE
}
}
object Keys {
const val KEY_LIBRARY_SORT_MODE = "KEY_LIBRARY_SORT_MODE"
const val KEY_THEME = "KEY_THEME"
}
private object Theme {
const val THEME_AUTO = "AUTO"
const val THEME_LIGHT = "LIGHT"
const val THEME_DARK = "DARK"
}
/**
* A safe interface for receiving preference updates, use this instead of
* [SharedPreferences.OnSharedPreferenceChangeListener].
*/
interface Callback {
fun onThemeUpdate(value: Int) {}
}
}

View file

@ -0,0 +1,11 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24"
android:tint="?attr/colorPrimary">
<path
android:fillColor="@android:color/white"
android:pathData="M20,8.69L20,4h-4.69L12,0.69 8.69,4L4,4v4.69L0.69,12 4,15.31L4,20h4.69L12,23.31 15.31,20L20,20v-4.69L23.31,12 20,8.69zM12,18c-3.31,0 -6,-2.69 -6,-6s2.69,-6 6,-6 6,2.69 6,6 -2.69,6 -6,6zM12,8c-2.21,0 -4,1.79 -4,4s1.79,4 4,4 4,-1.79 4,-4 -1.79,-4 -4,-4z"/>
</vector>

View file

@ -0,0 +1,29 @@
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:tools="http://schemas.android.com/tools"
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<androidx.appcompat.widget.Toolbar
android:id="@+id/song_toolbar"
android:layout_width="match_parent"
android:layout_height="?android:attr/actionBarSize"
android:background="?android:attr/windowBackground"
android:elevation="@dimen/elevation_normal"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:title="@string/label_settings"
app:titleTextAppearance="@style/TextAppearance.Toolbar.Header" />
<androidx.fragment.app.FragmentContainerView
android:id="@+id/settings_list_fragment"
android:name="org.oxycblt.auxio.settings.SettingListFragment"
android:layout_width="match_parent"
android:layout_height="match_parent" />
</LinearLayout>
</layout>

View file

@ -0,0 +1,17 @@
<?xml version="1.0" encoding="utf-8"?>
<TextView
android:id="@android:id/title"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textColor="?android:attr/textColorPrimary"
android:fontFamily="@font/inter_semibold"
android:paddingStart="@dimen/padding_medium"
android:paddingTop="@dimen/padding_small"
android:paddingEnd="@dimen/padding_small"
android:paddingBottom="@dimen/padding_small"
android:textSize="19sp"
android:background="@drawable/ui_header_dividers"
android:text="@{header.name}"
tools:text="UI"
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools" />

View file

@ -8,4 +8,8 @@
android:id="@+id/songs_fragment"
android:title="@string/label_songs"
android:icon="@drawable/ic_song" />
<item
android:id="@+id/settings_fragment"
android:title="@string/label_settings"
android:icon="@drawable/ic_settings" />
</menu>

View file

@ -88,4 +88,9 @@
android:name="org.oxycblt.auxio.songs.SongsFragment"
android:label="fragment_songs"
tools:layout="@layout/fragment_songs" />
<fragment
android:id="@+id/settings_fragment"
android:name="org.oxycblt.auxio.settings.SettingsFragment"
android:label="SettingsFragment"
tools:layout="@layout/fragment_settings" />
</navigation>

View file

@ -0,0 +1,13 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<array name="theme_entries">
<item>@string/label_settings_theme_auto</item>
<item>@string/label_settings_theme_light</item>
<item>@string/label_settings_theme_dark</item>
</array>
<array name="theme_values">
<item>AUTO</item>
<item>LIGHT</item>
<item>DARK</item>
</array>
</resources>

View file

@ -28,6 +28,12 @@
<string name="label_channel">Music Playback</string>
<string name="label_service_playback">The music playback service for Auxio.</string>
<string name="label_settings">Settings</string>
<string name="label_settings_ui">Appearance</string>
<string name="label_settings_theme">Theme</string>
<string name="label_settings_theme_auto">Auto</string>
<string name="label_settings_theme_light">Light</string>
<string name="label_settings_theme_dark">Dark</string>
<string name="label_settings_theme_choose">Choose theme</string>
<!-- Debug Namespace | Debug labels -->
<string name="debug_state_saved">State saved</string>

View file

@ -5,12 +5,14 @@
<item name="android:windowBackground">@color/background</item>
<item name="android:statusBarColor">@android:color/black</item>
<item name="android:fontFamily">@font/inter</item>
<item name="indicatorFastScrollerStyle">@style/FastScrollTheme</item>
<item name="android:textCursorDrawable">@drawable/ui_cursor</item>
<item name="android:fitsSystemWindows">true</item>
<item name="android:scrollbars">none</item>
<item name="popupMenuStyle">@style/Widget.CustomPopup</item>
<item name="colorControlNormal">@color/control_color</item>
<item name="alertDialogTheme">@style/Theme.CustomDialog</item>
<item name="indicatorFastScrollerStyle">@style/FastScrollTheme</item>
</style>
<!-- Hack to fix the weird icon/underline with LibraryFragment's SearchView -->
@ -48,6 +50,19 @@
<item name="android:popupBackground">@color/background</item>
</style>
<!-- Custom Dialog Theme -->
<style name="Theme.CustomDialog" parent="Theme.MaterialComponents.DayNight.Dialog">
<item name="colorBackgroundFloating">@color/background</item>
<item name="android:windowTitleStyle">@style/TextAppearance.Dialog.Title</item>
<item name="colorPrimary">@color/control_color</item>
<item name="colorSecondary">@color/control_color</item>
</style>
<style name="TextAppearance.Dialog.Title" parent="@android:style/TextAppearance.Material.Title">
<item name="android:fontFamily">@font/inter_black</item>
</style>
<!-- Fast scroll theme -->
<style name="FastScrollTheme" parent="Widget.IndicatorFastScroll.FastScroller">
<item name="android:textColor">@color/ui_state_color</item>
<item name="android:textAppearance">@style/TextAppearance.FastScroll</item>

View file

@ -0,0 +1,15 @@
<?xml version="1.0" encoding="utf-8"?>
<PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<PreferenceCategory
android:title="@string/label_settings_ui"
android:layout="@layout/item_prefs_header">
<ListPreference
app:key="KEY_THEME"
android:title="@string/label_settings_theme"
android:icon="@drawable/ic_day"
android:entries="@array/theme_entries"
android:entryValues="@array/theme_values"
app:useSimpleSummaryProvider="true" />
</PreferenceCategory>
</PreferenceScreen>