Implement theme customization
Implement the ability to change the theme to auto/light/dark.
This commit is contained in:
parent
76c1fe1d75
commit
2dc7ba3420
16 changed files with 297 additions and 79 deletions
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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!!)
|
||||
}
|
||||
|
|
|
@ -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"
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
118
app/src/main/java/org/oxycblt/auxio/settings/SettingsManager.kt
Normal file
118
app/src/main/java/org/oxycblt/auxio/settings/SettingsManager.kt
Normal 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) {}
|
||||
}
|
||||
}
|
11
app/src/main/res/drawable/ic_day.xml
Normal file
11
app/src/main/res/drawable/ic_day.xml
Normal 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>
|
29
app/src/main/res/layout/fragment_settings.xml
Normal file
29
app/src/main/res/layout/fragment_settings.xml
Normal 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>
|
17
app/src/main/res/layout/item_prefs_header.xml
Normal file
17
app/src/main/res/layout/item_prefs_header.xml
Normal 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" />
|
|
@ -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>
|
|
@ -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>
|
13
app/src/main/res/values/arrays.xml
Normal file
13
app/src/main/res/values/arrays.xml
Normal 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>
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
15
app/src/main/res/xml/prefs_main.xml
Normal file
15
app/src/main/res/xml/prefs_main.xml
Normal 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>
|
Loading…
Reference in a new issue