home: add tab customization

Finally add tab customization. This implementation is a
bit ugly, but I had to futureproof it for playlists and I'm
planning to clean up a lot of the duplicate code across the app.

This addition notably allows a default tab to be set, which is
something that was widely requested in #12. This UI rework finally
allows it to be added.
This commit is contained in:
OxygenCobalt 2021-10-17 20:27:16 -06:00
parent a253cfccc4
commit 23d1be8ebc
No known key found for this signature in database
GPG key ID: 37DBE3621FE9AD47
39 changed files with 687 additions and 203 deletions

View file

@ -46,14 +46,13 @@ I primarily built Auxio for myself, but you can use it too, I guess.
- Search Functionality
- Audio Focus / Headset Management
- No internet connectivity whatsoever
- Kotlin from the ground-up
- Modular, feature-based architecture
- No rounded corners
- No rounded album corners
## To possibly come in the future:
- Playlists
- Liked songs
- Improved tablet layouts
- More notification actions
- Other things, possibly

View file

@ -97,11 +97,13 @@ dependencies {
implementation "com.google.android.exoplayer:exoplayer-core:2.15.1"
// Image loading
implementation 'io.coil-kt:coil:1.3.2'
implementation 'io.coil-kt:coil:1.4.0'
// Material
implementation "com.google.android.material:material:1.5.0-alpha04"
// Fast scrolling
// TODO: Merge eventually
implementation 'me.zhanghai.android.fastscroll:library:1.1.7'
// --- DEBUG ---

View file

@ -49,7 +49,6 @@ import org.oxycblt.auxio.music.Genre
import org.oxycblt.auxio.music.Song
import org.oxycblt.auxio.ui.DisplayMode
import org.oxycblt.auxio.ui.SortMode
import org.oxycblt.auxio.ui.memberBinding
import org.oxycblt.auxio.util.applyEdge
import org.oxycblt.auxio.util.logD
import org.oxycblt.auxio.util.logE
@ -57,16 +56,12 @@ import org.oxycblt.auxio.util.logE
/**
* The main "Launching Point" fragment of Auxio, allowing navigation to the detail
* views for each respective fragment.
* FIXME: More UI glitches:
* - AppBar will just...expand. For no reason. If you navigate away while it's partially
* collapsed. No, I don't know why. Guess I have to save the state myself.
* - Edge-to-edge is borked still, unsure how to really fix this aside from making some
* FIXME: Edge-to-edge is borked still, unsure how to really fix this aside from making some
* magic layout like Material Files, but even then it might not work since the scrolling
* views are not laid side-by-side to the layout itself.
* @author OxygenCobalt
*/
class HomeFragment : Fragment() {
private val binding: FragmentHomeBinding by memberBinding(FragmentHomeBinding::inflate)
private val detailModel: DetailViewModel by activityViewModels()
private val homeModel: HomeViewModel by activityViewModels()
@ -75,6 +70,7 @@ class HomeFragment : Fragment() {
container: ViewGroup?,
savedInstanceState: Bundle?
): View {
val binding = FragmentHomeBinding.inflate(inflater)
val sortItem: MenuItem
// --- UI SETUP ---
@ -86,6 +82,12 @@ class HomeFragment : Fragment() {
}
binding.homeAppbar.apply {
// I have no idea how to clip the collapsing toolbar while still making the elevation
// overlay bleed into the status bar, so I take the easy way out and just fade the
// toolbar when the offset changes.
// Note: Don't merge this with the other OnOffsetChangedListener, as this one needs
// to be added pre-start to work correctly while the other one needs to be posted to
// work correctly
addOnOffsetChangedListener(
AppBarLayout.OnOffsetChangedListener { _, verticalOffset ->
binding.homeToolbar.alpha = (binding.homeToolbar.height + verticalOffset) /
@ -93,8 +95,13 @@ class HomeFragment : Fragment() {
}
)
// One issue that comes with using our fast scroller is that it allows scrolling
// without the AppBar actually being collapsed in the process. This results in
// the RecyclerView being clipped if you scroll down far enough. To fix this, we
// add another OnOffsetChangeListener that adds padding to the RecyclerView whenever
// the Toolbar is collapsed. This is not really ideal, as it forces a relayout and
// some edge-effect glitches whenever we scroll, but its the best we can do.
post {
// To add our fast scroller, we need to
val vOffset = (
(layoutParams as CoordinatorLayout.LayoutParams)
.behavior as AppBarLayout.Behavior
@ -135,7 +142,7 @@ class HomeFragment : Fragment() {
R.id.submenu_sorting -> { }
// Sorting option was selected, check then and update the mode
// Sorting option was selected, mark it as selected and update the mode
else -> {
item.isChecked = true
@ -179,7 +186,7 @@ class HomeFragment : Fragment() {
// We know that there will only be a fixed amount of tabs, so we manually set this
// limit to that. This also prevents the appbar lift state from being confused during
// page transitions.
offscreenPageLimit = homeModel.tabs.value!!.size
offscreenPageLimit = homeModel.tabs.size
registerOnPageChangeCallback(object : ViewPager2.OnPageChangeCallback() {
override fun onPageSelected(position: Int) = homeModel.updateCurrentTab(position)
@ -187,7 +194,7 @@ class HomeFragment : Fragment() {
}
TabLayoutMediator(binding.homeTabs, binding.homePager) { tab, pos ->
val labelRes = when (homeModel.tabs.value!![pos]) {
val labelRes = when (homeModel.tabs[pos]) {
DisplayMode.SHOW_SONGS -> R.string.lbl_songs
DisplayMode.SHOW_ALBUMS -> R.string.lbl_albums
DisplayMode.SHOW_ARTISTS -> R.string.lbl_artists
@ -199,7 +206,19 @@ class HomeFragment : Fragment() {
// --- VIEWMODEL SETUP ---
homeModel.recreateTabs.observe(viewLifecycleOwner) { recreate ->
// notifyDataSetChanged is not practical for recreating here since it will cache
// the previous fragments. Just instantiate a whole new adapter.
if (recreate) {
binding.homePager.currentItem = 0
binding.homePager.adapter = HomePagerAdapter()
homeModel.finishRecreateTabs()
}
}
homeModel.curTab.observe(viewLifecycleOwner) { tab ->
// Make sure that we update the scrolling view and allowed menu items before whenever
// the tab changes.
binding.homeAppbar.liftOnScrollTargetViewId = when (requireNotNull(tab)) {
DisplayMode.SHOW_SONGS -> {
updateSortMenu(sortItem, tab)
@ -280,10 +299,10 @@ class HomeFragment : Fragment() {
private inner class HomePagerAdapter :
FragmentStateAdapter(childFragmentManager, viewLifecycleOwner.lifecycle) {
override fun getItemCount(): Int = homeModel.tabs.value!!.size
override fun getItemCount(): Int = homeModel.tabs.size
override fun createFragment(position: Int): Fragment {
return when (homeModel.tabs.value!![position]) {
return when (homeModel.tabs[position]) {
DisplayMode.SHOW_SONGS -> SongListFragment()
DisplayMode.SHOW_ALBUMS -> AlbumListFragment()
DisplayMode.SHOW_ARTISTS -> ArtistListFragment()

View file

@ -27,14 +27,18 @@ import org.oxycblt.auxio.music.Genre
import org.oxycblt.auxio.music.MusicStore
import org.oxycblt.auxio.music.Song
import org.oxycblt.auxio.settings.SettingsManager
import org.oxycblt.auxio.settings.tabs.Tab
import org.oxycblt.auxio.ui.DisplayMode
import org.oxycblt.auxio.ui.SortMode
/**
* The ViewModel for managing [HomeFragment]'s data and sorting modes.
* TODO: Custom tabs
* The ViewModel for managing [HomeFragment]'s data, sorting modes, and tab state.
* @author OxygenCobalt
*/
class HomeViewModel : ViewModel() {
class HomeViewModel : ViewModel(), SettingsManager.Callback {
private val musicStore = MusicStore.getInstance()
private val settingsManager = SettingsManager.getInstance()
private val mSongs = MutableLiveData(listOf<Song>())
val songs: LiveData<List<Song>> get() = mSongs
@ -47,32 +51,38 @@ class HomeViewModel : ViewModel() {
private val mGenres = MutableLiveData(listOf<Genre>())
val genres: LiveData<List<Genre>> get() = mGenres
private val mTabs = MutableLiveData(
arrayOf(
DisplayMode.SHOW_SONGS, DisplayMode.SHOW_ALBUMS,
DisplayMode.SHOW_ARTISTS, DisplayMode.SHOW_GENRES
)
)
val tabs: LiveData<Array<DisplayMode>> = mTabs
var tabs: List<DisplayMode> = settingsManager.visibleTabs
private set
private val mCurTab = MutableLiveData(mTabs.value!![0])
private val mCurTab = MutableLiveData(tabs[0])
val curTab: LiveData<DisplayMode> = mCurTab
private val musicStore = MusicStore.getInstance()
private val settingsManager = SettingsManager.getInstance()
/**
* Marker to recreate all library tabs, usually initiated by a settings change.
* When this flag is set, all tabs (and their respective viewpager fragments) will be
* recreated from scratch.
*/
private val mRecreateTabs = MutableLiveData(false)
val recreateTabs: LiveData<Boolean> = mRecreateTabs
init {
mSongs.value = settingsManager.libSongSort.sortSongs(musicStore.songs)
mAlbums.value = settingsManager.libAlbumSort.sortAlbums(musicStore.albums)
mArtists.value = settingsManager.libArtistSort.sortModels(musicStore.artists)
mGenres.value = settingsManager.libGenreSort.sortModels(musicStore.genres)
settingsManager.addCallback(this)
}
/**
* Update the current tab based off of the new ViewPager position.
*/
fun updateCurrentTab(pos: Int) {
mCurTab.value = mTabs.value!![pos]
mCurTab.value = tabs[pos]
}
fun finishRecreateTabs() {
mRecreateTabs.value = false
}
fun getSortForDisplay(displayMode: DisplayMode): SortMode {
@ -108,4 +118,16 @@ class HomeViewModel : ViewModel() {
}
}
}
// --- OVERRIDES ---
override fun onLibTabsUpdate(libTabs: Array<Tab>) {
tabs = settingsManager.visibleTabs
mRecreateTabs.value = true
}
override fun onCleared() {
super.onCleared()
settingsManager.removeCallback(this)
}
}

View file

@ -37,6 +37,10 @@ import org.oxycblt.auxio.ui.memberBinding
import org.oxycblt.auxio.util.applySpans
import org.oxycblt.auxio.util.resolveDrawable
/**
* A Base [Fragment] implementing the base features shared across all detail fragments.
*
*/
abstract class HomeListFragment : Fragment() {
protected val binding: FragmentHomeListBinding by memberBinding(
FragmentHomeListBinding::inflate
@ -83,6 +87,9 @@ abstract class HomeListFragment : Fragment() {
@SuppressLint("NotifyDataSetChanged")
fun updateData(newData: List<T>) {
data = newData
// notifyDataSetChanged here is okay, as we have no idea how the layout changed when
// we re-sort and ListAdapter causes the scroll position to get messed up
notifyDataSetChanged()
}
}

View file

@ -82,7 +82,7 @@ import org.oxycblt.auxio.util.logD
*
* I'm pretty sure nothing is going to happen and MediaStore will continue to be neglected and
* probably deprecated eventually for a "new" API that just coincidentally excludes music indexing.
* Because go **** yourself for wanting to listen to music you own. Be a good consoomer and listen
* Because go screw yourself for wanting to listen to music you own. Be a good consoomer and listen
* to your AlgoPop StreamMix instead.
*
* I wish I was born in the neolithic.

View file

@ -133,8 +133,6 @@ class SearchFragment : Fragment() {
searchModel.searchResults.observe(viewLifecycleOwner) { results ->
searchAdapter.submitList(results) {
// We've just scrolled back to the top, reset the lifted state
// TODO: Maybe find a better way to keep scroll state when the search
// results didn't actually change.
binding.searchRecycler.scrollToPosition(0)
}

View file

@ -35,6 +35,9 @@ import org.oxycblt.auxio.accent.Accent
import org.oxycblt.auxio.accent.AccentDialog
import org.oxycblt.auxio.excluded.ExcludedDialog
import org.oxycblt.auxio.playback.PlaybackViewModel
import org.oxycblt.auxio.settings.pref.IntListPrefDialog
import org.oxycblt.auxio.settings.pref.IntListPreference
import org.oxycblt.auxio.settings.tabs.TabCustomizeDialog
import org.oxycblt.auxio.util.applyEdge
import org.oxycblt.auxio.util.isNight
import org.oxycblt.auxio.util.logD
@ -130,6 +133,13 @@ class SettingsListFragment : PreferenceFragmentCompat() {
summary = Accent.get().getDetailedSummary(context)
}
SettingsManager.KEY_LIB_TABS -> {
onPreferenceClickListener = Preference.OnPreferenceClickListener {
TabCustomizeDialog().show(childFragmentManager, TabCustomizeDialog.TAG)
true
}
}
SettingsManager.KEY_SHOW_COVERS, SettingsManager.KEY_QUALITY_COVERS -> {
onPreferenceChangeListener = Preference.OnPreferenceChangeListener { _, _ ->
Coil.imageLoader(requireContext()).apply {

View file

@ -24,6 +24,7 @@ import androidx.core.content.edit
import androidx.preference.PreferenceManager
import org.oxycblt.auxio.accent.Accent
import org.oxycblt.auxio.playback.state.PlaybackMode
import org.oxycblt.auxio.settings.tabs.Tab
import org.oxycblt.auxio.ui.DisplayMode
import org.oxycblt.auxio.ui.SortMode
@ -70,9 +71,21 @@ class SettingsManager private constructor(context: Context) :
val useAltNotifAction: Boolean
get() = sharedPrefs.getBoolean(KEY_USE_ALT_NOTIFICATION_ACTION, false)
/**
* Whether to even loading embedded covers
*/
/** The current library tabs preferred by the user. */
var libTabs: Array<Tab>
get() = Tab.fromSequence(sharedPrefs.getInt(KEY_LIB_TABS, Tab.SEQUENCE_DEFAULT))
?: Tab.fromSequence(Tab.SEQUENCE_DEFAULT)!!
set(value) {
sharedPrefs.edit {
putInt(KEY_LIB_TABS, Tab.toSequence(value))
apply()
}
}
/** The currently visible library tabs */
val visibleTabs: List<DisplayMode> get() = libTabs.filterIsInstance<Tab.Visible>().map { it.mode }
/** Whether to load embedded covers */
val showCovers: Boolean
get() = sharedPrefs.getBoolean(KEY_SHOW_COVERS, true)
@ -114,6 +127,7 @@ class SettingsManager private constructor(context: Context) :
}
}
/** The song sort mode on HomeFragment **/
var libSongSort: SortMode
get() = SortMode.fromInt(sharedPrefs.getInt(KEY_LIB_SONGS_SORT, Int.MIN_VALUE))
?: SortMode.ASCENDING
@ -124,6 +138,7 @@ class SettingsManager private constructor(context: Context) :
}
}
/** The album sort mode on HomeFragment **/
var libAlbumSort: SortMode
get() = SortMode.fromInt(sharedPrefs.getInt(KEY_LIB_ALBUMS_SORT, Int.MIN_VALUE))
?: SortMode.ASCENDING
@ -134,6 +149,7 @@ class SettingsManager private constructor(context: Context) :
}
}
/** The artist sort mode on HomeFragment **/
var libArtistSort: SortMode
get() = SortMode.fromInt(sharedPrefs.getInt(KEY_LIB_ARTISTS_SORT, Int.MIN_VALUE))
?: SortMode.ASCENDING
@ -144,6 +160,7 @@ class SettingsManager private constructor(context: Context) :
}
}
/** The genre sort mode on HomeFragment **/
var libGenreSort: SortMode
get() = SortMode.fromInt(sharedPrefs.getInt(KEY_LIB_GENRE_SORT, Int.MIN_VALUE))
?: SortMode.ASCENDING
@ -154,6 +171,7 @@ class SettingsManager private constructor(context: Context) :
}
}
/** The detail album sort mode **/
var detailAlbumSort: SortMode
get() = SortMode.fromInt(sharedPrefs.getInt(KEY_DETAIL_ALBUM_SORT, Int.MIN_VALUE))
?: SortMode.ASCENDING
@ -164,6 +182,7 @@ class SettingsManager private constructor(context: Context) :
}
}
/** The detail artist sort mode **/
var detailArtistSort: SortMode
get() = SortMode.fromInt(sharedPrefs.getInt(KEY_DETAIL_ARTIST_SORT, Int.MIN_VALUE))
?: SortMode.YEAR
@ -174,6 +193,7 @@ class SettingsManager private constructor(context: Context) :
}
}
/** The detail genre sort mode **/
var detailGenreSort: SortMode
get() = SortMode.fromInt(sharedPrefs.getInt(KEY_DETAIL_GENRE_SORT, Int.MIN_VALUE))
?: SortMode.ASCENDING
@ -211,6 +231,10 @@ class SettingsManager private constructor(context: Context) :
KEY_QUALITY_COVERS -> callbacks.forEach {
it.onQualityCoverUpdate(useQualityCovers)
}
KEY_LIB_TABS -> callbacks.forEach {
it.onLibTabsUpdate(libTabs)
}
}
}
@ -220,6 +244,7 @@ class SettingsManager private constructor(context: Context) :
* context.
*/
interface Callback {
fun onLibTabsUpdate(libTabs: Array<Tab>) {}
fun onColorizeNotifUpdate(doColorize: Boolean) {}
fun onNotifActionUpdate(useAltAction: Boolean) {}
fun onShowCoverUpdate(showCovers: Boolean) {}
@ -231,6 +256,7 @@ class SettingsManager private constructor(context: Context) :
const val KEY_BLACK_THEME = "KEY_BLACK_THEME"
const val KEY_ACCENT = "KEY_ACCENT2"
const val KEY_LIB_TABS = "KEY_LIB_TABS"
const val KEY_SHOW_COVERS = "KEY_SHOW_COVERS"
const val KEY_QUALITY_COVERS = "KEY_QUALITY_COVERS"
const val KEY_USE_ALT_NOTIFICATION_ACTION = "KEY_ALT_NOTIF_ACTION"

View file

@ -16,7 +16,7 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.oxycblt.auxio.settings
package org.oxycblt.auxio.settings.pref
import android.os.Bundle
import androidx.appcompat.app.AlertDialog
@ -24,6 +24,9 @@ import androidx.preference.PreferenceFragmentCompat
import org.oxycblt.auxio.BuildConfig
import org.oxycblt.auxio.ui.LifecycleDialog
/**
* The dialog shown whenever an [IntListPreference] is shown.
*/
class IntListPrefDialog : LifecycleDialog() {
override fun onConfigDialog(builder: AlertDialog.Builder) {
// Since we have to store the preference key as an argument, we have to find the

View file

@ -16,7 +16,7 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.oxycblt.auxio.settings
package org.oxycblt.auxio.settings.pref
import android.content.Context
import android.content.res.TypedArray

View file

@ -0,0 +1,124 @@
/*
* Copyright (c) 2021 Auxio Project
* Tab.kt is part of Auxio.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.oxycblt.auxio.settings.tabs
import org.oxycblt.auxio.ui.DisplayMode
import org.oxycblt.auxio.util.logE
/**
* A data representation of a library tab.
* A tab can come in two moves, [Visible] or [Invisible]. Invisibility means that the tab
* will still be present in the customization menu, but will not be shown on the home UI.
*
* Like other IO-bound datatypes in Auxio, tabs are stored in a binary format. However, tabs cannot
* be serialized on their own. Instead, they are saved as a sequence of tabs as shown below:
*
* 0bTAB1_TAB2_TAB3_TAB4_TAB5
*
* Where TABN is a chunk representing a tab at position N. TAB5 is reserved for playlists.
* Each chunk in a sequence is represented as:
*
* VTTT
*
* Where V is a bit representing the visibility and T is a 3-bit integer representing the
* [DisplayMode] ordinal for this tab.
*
* To serialize and deserialize a tab sequence, [toSequence] and [fromSequence] can be used
* respectively.
*
* By default, the tab order will be SONGS, ALBUMS, ARTISTS, GENRES, PLAYLISTS
*/
sealed class Tab(open val mode: DisplayMode) {
data class Visible(override val mode: DisplayMode) : Tab(mode)
data class Invisible(override val mode: DisplayMode) : Tab(mode)
companion object {
/** The length a well-formed tab sequence should be **/
const val SEQUENCE_LEN = 4
/** The default tab sequence, represented in integer form **/
const val SEQUENCE_DEFAULT = 0b1000_1001_1010_1011_0100
// Temporary value to make sure we create a 5-tab sequence even though playlists
// aren't implemented yet.
private const val TEMP_BIT_CAP = 20
/**
* Convert an array [tabs] into a sequence of tabs.
*/
fun toSequence(tabs: Array<Tab>): Int {
// Like when deserializing, make sure there are no duplicate tabs for whatever reason.
val distinct = tabs.distinctBy { it.mode }
var sequence = 0b0100
var shift = TEMP_BIT_CAP
distinct.forEach { tab ->
val bin = when (tab) {
is Visible -> 1.shl(3) or tab.mode.ordinal
is Invisible -> tab.mode.ordinal
}
sequence = sequence or bin.shl(shift)
shift -= 4
}
return sequence
}
/**
* Convert a [sequence] into an array of tabs.
*/
fun fromSequence(sequence: Int): Array<Tab>? {
val tabs = mutableListOf<Tab>()
// Try to parse a mode for each chunk in the sequence.
// If we can't parse one, just skip it.
for (shift in (0..TEMP_BIT_CAP).reversed() step 4) {
val chunk = sequence.shr(shift) and 0b1111
val mode = when (chunk and 7) {
0 -> DisplayMode.SHOW_SONGS
1 -> DisplayMode.SHOW_ALBUMS
2 -> DisplayMode.SHOW_ARTISTS
3 -> DisplayMode.SHOW_GENRES
else -> continue
}
// Figure out the visibility
tabs += if (chunk and 1.shl(3) != 0) {
Visible(mode)
} else {
Invisible(mode)
}
}
// Make sure there are no duplicate tabs
val distinct = tabs.distinctBy { it.mode }
// For safety, use the default configuration if something went wrong
// and we have an empty or larger-than-expected tab array.
if (distinct.isEmpty() || distinct.size < SEQUENCE_LEN) {
logE("Sequence size was ${distinct.size}, which is invalid. Using defaults instead")
return null
}
return distinct.toTypedArray()
}
}
}

View file

@ -0,0 +1,89 @@
/*
* Copyright (c) 2021 Auxio Project
* TabAdapter.kt is part of Auxio.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.oxycblt.auxio.settings.tabs
import android.annotation.SuppressLint
import android.view.MotionEvent
import android.view.ViewGroup
import androidx.recyclerview.widget.ItemTouchHelper
import androidx.recyclerview.widget.RecyclerView
import org.oxycblt.auxio.databinding.ItemTabBinding
import org.oxycblt.auxio.util.inflater
class TabAdapter(
private val touchHelper: ItemTouchHelper,
private val getTabs: () -> Array<Tab>,
private val onTabSwitch: (Tab) -> Unit,
) : RecyclerView.Adapter<TabAdapter.TabViewHolder>() {
private val tabs: Array<Tab> get() = getTabs()
override fun getItemCount(): Int = Tab.SEQUENCE_LEN
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): TabViewHolder {
return TabViewHolder(ItemTabBinding.inflate(parent.context.inflater))
}
override fun onBindViewHolder(holder: TabViewHolder, position: Int) {
holder.bind(tabs[position])
}
inner class TabViewHolder(
private val binding: ItemTabBinding
) : RecyclerView.ViewHolder(binding.root) {
init {
binding.root.layoutParams = RecyclerView.LayoutParams(
RecyclerView.LayoutParams.MATCH_PARENT, RecyclerView.LayoutParams.WRAP_CONTENT
)
}
@SuppressLint("ClickableViewAccessibility")
fun bind(tab: Tab) {
binding.root.apply {
setOnClickListener {
// Don't do a typical notifyDataSetChanged call here, because
// A. We don't have a real ViewModel state since this is a dialog
// B. Doing so would cause a relayout and the ripple effect to disappear
// Instead, simply notify a tab change and let TabCustomizeDialog handle it.
binding.tabIcon.isEnabled = !binding.tabIcon.isEnabled
binding.tabName.isEnabled = !binding.tabName.isEnabled
onTabSwitch(tab)
}
}
binding.tabIcon.apply {
setImageResource(tab.mode.icon)
contentDescription = context.getString(tab.mode.string)
isEnabled = tab is Tab.Visible
}
binding.tabName.apply {
setText(tab.mode.string)
isEnabled = tab is Tab.Visible
}
binding.tabDragHandle.setOnTouchListener { _, motionEvent ->
binding.tabDragHandle.performClick()
if (motionEvent.actionMasked == MotionEvent.ACTION_DOWN) {
touchHelper.startDrag(this)
true
} else false
}
}
}
}

View file

@ -0,0 +1,115 @@
/*
* Copyright (c) 2021 Auxio Project
* CustomizeListDialog.kt is part of Auxio.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.oxycblt.auxio.settings.tabs
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.appcompat.app.AlertDialog
import androidx.recyclerview.widget.ItemTouchHelper
import org.oxycblt.auxio.BuildConfig
import org.oxycblt.auxio.R
import org.oxycblt.auxio.databinding.DialogTabsBinding
import org.oxycblt.auxio.settings.SettingsManager
import org.oxycblt.auxio.ui.LifecycleDialog
/**
* The dialog for customizing library tabs. This dialog does not rely on any specific ViewModel
* and serializes it's state instead of
* @author OxygenCobalt
*/
class TabCustomizeDialog : LifecycleDialog() {
private val settingsManager = SettingsManager.getInstance()
private var pendingTabs = settingsManager.libTabs
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View {
val binding = DialogTabsBinding.inflate(inflater)
if (savedInstanceState != null) {
// Restore any pending tab configurations
val tabs = Tab.fromSequence(savedInstanceState.getInt(KEY_TABS))
if (tabs != null) {
pendingTabs = tabs
}
}
// Set up adapter & drag callback
val callback = TabDragCallback { pendingTabs }
val helper = ItemTouchHelper(callback)
val tabAdapter = TabAdapter(
helper,
getTabs = { pendingTabs },
onTabSwitch = { tab ->
// Don't find the specific tab [Which might be outdated due to the nature
// of how viewholders are bound], but instead simply look for the mode in
// the list of pending tabs and update that instead.
val index = pendingTabs.indexOfFirst { it.mode == tab.mode }
if (index != -1) {
val curTab = pendingTabs[index]
pendingTabs[index] = when (curTab) {
is Tab.Visible -> Tab.Invisible(curTab.mode)
is Tab.Invisible -> Tab.Visible(curTab.mode)
}
}
(requireDialog() as AlertDialog).getButton(AlertDialog.BUTTON_POSITIVE).isEnabled =
pendingTabs.filterIsInstance<Tab.Visible>().isNotEmpty()
}
)
callback.addTabAdapter(tabAdapter)
binding.tabRecycler.apply {
adapter = tabAdapter
helper.attachToRecyclerView(this)
}
return binding.root
}
override fun onSaveInstanceState(outState: Bundle) {
super.onSaveInstanceState(outState)
outState.putInt(KEY_TABS, Tab.toSequence(pendingTabs))
}
override fun onConfigDialog(builder: AlertDialog.Builder) {
builder.setTitle(R.string.set_lib_tabs)
builder.setPositiveButton(android.R.string.ok) { _, _ ->
settingsManager.libTabs = pendingTabs
}
// Negative button just dismisses, no need for a listener.
builder.setNegativeButton(android.R.string.cancel, null)
}
companion object {
const val TAG = BuildConfig.APPLICATION_ID + ".tag.TAB_CUSTOMIZE"
const val KEY_TABS = BuildConfig.APPLICATION_ID + ".key.PENDING_TABS"
}
}

View file

@ -0,0 +1,88 @@
/*
* Copyright (c) 2021 Auxio Project
* QueueDragCallback.kt is part of Auxio.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.oxycblt.auxio.settings.tabs
import android.graphics.Canvas
import androidx.recyclerview.widget.ItemTouchHelper
import androidx.recyclerview.widget.RecyclerView
/**
* A simple [ItemTouchHelper.Callback] that handles dragging items in the tab customization menu.
* Unlike QueueAdapter's ItemTouchHelper, this one is bare and simple.
*/
class TabDragCallback(private val getTabs: () -> Array<Tab>) : ItemTouchHelper.Callback() {
private val tabs: Array<Tab> get() = getTabs()
private lateinit var tabAdapter: TabAdapter
override fun getMovementFlags(
recyclerView: RecyclerView,
viewHolder: RecyclerView.ViewHolder
): Int = makeFlag(
ItemTouchHelper.ACTION_STATE_DRAG,
ItemTouchHelper.UP or ItemTouchHelper.DOWN
)
override fun onChildDraw(
c: Canvas,
recyclerView: RecyclerView,
viewHolder: RecyclerView.ViewHolder,
dX: Float,
dY: Float,
actionState: Int,
isCurrentlyActive: Boolean
) {
// No fancy UI magic here. This is a dialog, we don't need to give it as much attention.
// Just make sure the built-in androidx code doesn't get in our way.
viewHolder.itemView.translationX = dX
viewHolder.itemView.translationY = dY
}
override fun clearView(recyclerView: RecyclerView, viewHolder: RecyclerView.ViewHolder) {
viewHolder.itemView.translationX = 0f
viewHolder.itemView.translationY = 0f
}
override fun onMove(
recyclerView: RecyclerView,
viewHolder: RecyclerView.ViewHolder,
target: RecyclerView.ViewHolder
): Boolean {
tabs.swap(viewHolder.bindingAdapterPosition, target.bindingAdapterPosition)
tabAdapter.notifyItemMoved(viewHolder.bindingAdapterPosition, target.bindingAdapterPosition)
return true
}
override fun onSwiped(viewHolder: RecyclerView.ViewHolder, direction: Int) {}
/**
* Add the tab adapter to this callback.
* Done because there's a circular dependency between the two objects
*/
fun addTabAdapter(adapter: TabAdapter) {
tabAdapter = adapter
}
private fun <T : Any> Array<T>.swap(from: Int, to: Int) {
val t = get(to)
val f = get(from)
set(from, t)
set(to, f)
}
}

View file

@ -18,15 +18,21 @@
package org.oxycblt.auxio.ui
import androidx.annotation.DrawableRes
import androidx.annotation.StringRes
import org.oxycblt.auxio.R
/**
* An enum for determining what items to show in a given list.
* Note: **DO NOT RE-ARRANGE THE ENUM**. The ordinals are used to store library tabs, so doing
* changing them would also change the meaning.
* @author OxygenCobalt
*/
enum class DisplayMode {
SHOW_GENRES,
SHOW_ARTISTS,
SHOW_ALBUMS,
SHOW_SONGS;
enum class DisplayMode(@DrawableRes val icon: Int, @StringRes val string: Int) {
SHOW_SONGS(R.drawable.ic_song, R.string.lbl_songs),
SHOW_ALBUMS(R.drawable.ic_album, R.string.lbl_albums),
SHOW_ARTISTS(R.drawable.ic_artist, R.string.lbl_artists),
SHOW_GENRES(R.drawable.ic_genre, R.string.lbl_genres);
companion object {
private const val CONST_NULL = 0xA107

View file

@ -36,7 +36,8 @@ import org.oxycblt.auxio.util.logD
/**
* Auxio's one and only appwidget. This widget follows a more unorthodox approach, effectively
* packing what could be considered 3 or 4 widgets into a single responsive widget. More specifically:
* packing what could be considered multiple widgets into a single responsive widget. More
* specifically:
*
* - For widgets Wx2 or higher, show an expanded view with album art and basic controls
* - For widgets 4x2 or higher, show a complete view with all playback controls
@ -115,7 +116,7 @@ class WidgetProvider : AppWidgetProvider() {
}
}
// / --- INTERNAL METHODS ---
// --- INTERNAL METHODS ---
private fun requestUpdate(context: Context) {
logD("Sending update intent to PlaybackService")

View file

@ -1,5 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:alpha="0.24" android:color="?attr/colorOnSurface" android:state_enabled="false" />
<item android:color="?attr/colorControlNormal" />
</selector>

View file

@ -1,5 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:alpha="0.24" android:color="?attr/colorOnSurface" android:state_enabled="false" />
<item android:color="?attr/colorPrimary" android:state_activated="true" />
<item android:color="?attr/colorControlNormal" />
</selector>

View file

@ -2,7 +2,7 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:tint="@color/overlay_disabled"
android:tint="@color/sel_accented"
android:viewportWidth="24"
android:viewportHeight="24">
<path

View file

@ -11,7 +11,6 @@
android:paddingTop="@dimen/spacing_medium"
android:paddingStart="@dimen/spacing_small"
android:paddingEnd="@dimen/spacing_small"
android:paddingBottom="@dimen/spacing_small"
app:layoutManager="org.oxycblt.auxio.accent.AutoGridLayoutManager"
app:layout_constraintBottom_toTopOf="@+id/accent_cancel"
app:layout_constraintTop_toBottomOf="@+id/accent_header"

View file

@ -0,0 +1,18 @@
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools">
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/tab_recycler"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:overScrollMode="never"
android:paddingTop="@dimen/spacing_medium"
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
app:layout_constraintBottom_toTopOf="@+id/accent_cancel"
app:layout_constraintTop_toBottomOf="@+id/accent_header"
tools:itemCount="5"
tools:listitem="@layout/item_tab" />
</layout>

View file

@ -15,7 +15,7 @@
app:liftOnScroll="true"
app:liftOnScrollTargetViewId="@id/search_recycler">
<androidx.appcompat.widget.Toolbar
<com.google.android.material.appbar.MaterialToolbar
android:id="@+id/search_toolbar"
style="@style/Widget.Auxio.Toolbar.Icon"
app:menu="@menu/menu_search">
@ -43,7 +43,7 @@
</com.google.android.material.textfield.TextInputLayout>
</androidx.appcompat.widget.Toolbar>
</com.google.android.material.appbar.MaterialToolbar>
</org.oxycblt.auxio.ui.LiftAppBarLayout>

View file

@ -91,7 +91,7 @@
android:src="@drawable/ic_handle"
app:layout_constraintBottom_toBottomOf="@+id/album_cover"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="@+id/song_name" />
app:layout_constraintTop_toTopOf="@+id/album_cover" />
</androidx.constraintlayout.widget.ConstraintLayout>
</FrameLayout>

View file

@ -0,0 +1,57 @@
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools">
<androidx.constraintlayout.widget.ConstraintLayout
style="@style/Widget.Auxio.ItemLayout"
android:layout_height="wrap_content"
android:layout_width="match_parent"
android:padding="0dp">
<ImageView
android:id="@+id/tab_icon"
android:scaleType="fitCenter"
android:padding="@dimen/spacing_small"
android:layout_width="@dimen/size_btn_small"
android:layout_height="@dimen/size_btn_small"
android:layout_margin="@dimen/spacing_small"
android:src="@drawable/ic_artist"
app:tint="@color/sel_accented"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:ignore="contentDescription" />
<TextView
android:id="@+id/tab_name"
style="@style/Widget.Auxio.TextView.Item.Primary"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_margin="@dimen/spacing_medium"
android:gravity="center"
android:maxLines="@null"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toEndOf="@+id/tab_icon"
app:layout_constraintTop_toTopOf="parent"
tools:text="Artist" />
<ImageView
android:id="@+id/tab_drag_handle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:clickable="true"
android:contentDescription="@string/desc_tab_handle"
android:focusable="true"
android:minWidth="@dimen/size_btn_small"
android:minHeight="@dimen/size_btn_small"
android:paddingStart="@dimen/spacing_medium"
android:paddingEnd="@dimen/spacing_medium"
android:scaleType="center"
android:src="@drawable/ic_handle"
app:layout_constraintBottom_toBottomOf="@+id/tab_icon"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="@+id/tab_icon" />
</androidx.constraintlayout.widget.ConstraintLayout>
</layout>

View file

@ -8,7 +8,7 @@
android:theme="@style/Theme.Widget"
tools:ignore="Overdraw">
<ImageView
<android.widget.ImageView
android:layout_width="match_parent"
android:layout_height="match_parent"
android:alpha="0.3"
@ -16,7 +16,7 @@
android:scaleType="centerCrop"
android:src="@drawable/ic_song" />
<TextView
<android.widget.TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="center"

View file

@ -7,7 +7,7 @@
android:background="?attr/colorSurface"
android:theme="@style/Theme.Widget">
<ImageView
<android.widget.ImageView
android:id="@+id/widget_cover"
android:layout_width="match_parent"
android:layout_height="0dp"
@ -16,29 +16,29 @@
android:scaleType="centerCrop"
android:src="@drawable/ic_song" />
<LinearLayout style="@style/Widget.Auxio.AppWidget.Panel">
<android.widget.LinearLayout style="@style/Widget.Auxio.AppWidget.Panel">
<TextView
<android.widget.TextView
android:id="@+id/widget_song"
style="@style/Widget.Auxio.TextView.Primary.AppWidget"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/def_widget_song" />
<TextView
<android.widget.TextView
android:id="@+id/widget_artist"
style="@style/Widget.Auxio.TextView.Secondary.AppWidget"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/def_widget_artist" />
<LinearLayout
<android.widget.LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/spacing_medium"
android:orientation="horizontal">
<ImageButton
<android.widget.ImageButton
android:id="@+id/widget_loop"
style="@style/Widget.Auxio.Button.AppWidget"
android:layout_weight="1"
@ -47,7 +47,7 @@
android:contentDescription="@string/desc_change_loop"
android:src="@drawable/ic_loop" />
<ImageButton
<android.widget.ImageButton
android:id="@+id/widget_skip_prev"
android:layout_weight="1"
style="@style/Widget.Auxio.Button.AppWidget"
@ -56,7 +56,7 @@
android:contentDescription="@string/desc_skip_prev"
android:src="@drawable/ic_skip_prev" />
<ImageButton
<android.widget.ImageButton
android:id="@+id/widget_play_pause"
android:layout_weight="1"
style="@style/Widget.Auxio.Button.AppWidget"
@ -65,7 +65,7 @@
android:contentDescription="@string/desc_play_pause"
android:src="@drawable/sel_playing_state" />
<ImageButton
<android.widget.ImageButton
android:id="@+id/widget_skip_next"
android:layout_weight="1"
style="@style/Widget.Auxio.Button.AppWidget"
@ -74,7 +74,7 @@
android:contentDescription="@string/desc_skip_next"
android:src="@drawable/ic_skip_next" />
<ImageButton
<android.widget.ImageButton
android:id="@+id/widget_shuffle"
style="@style/Widget.Auxio.Button.AppWidget"
android:layout_weight="1"
@ -83,8 +83,8 @@
android:contentDescription="@string/desc_shuffle"
android:src="@drawable/ic_shuffle" />
</LinearLayout>
</android.widget.LinearLayout>
</LinearLayout>
</android.widget.LinearLayout>
</LinearLayout>

View file

@ -7,7 +7,7 @@
android:background="?attr/colorSurface"
android:theme="@style/Theme.Widget">
<ImageView
<android.widget.ImageView
android:id="@+id/widget_cover"
android:layout_width="match_parent"
android:layout_height="0dp"
@ -16,28 +16,28 @@
android:scaleType="centerCrop"
android:src="@drawable/ic_song" />
<LinearLayout style="@style/Widget.Auxio.AppWidget.Panel">
<android.widget.LinearLayout style="@style/Widget.Auxio.AppWidget.Panel">
<TextView
<android.widget.TextView
android:id="@+id/widget_song"
style="@style/Widget.Auxio.TextView.Primary.AppWidget"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/def_widget_song" />
<TextView
<android.widget.TextView
android:id="@+id/widget_artist"
style="@style/Widget.Auxio.TextView.Secondary.AppWidget"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/def_widget_artist" />
<LinearLayout
<android.widget.LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/spacing_medium">
<ImageButton
<android.widget.ImageButton
android:id="@+id/widget_skip_prev"
style="@style/Widget.Auxio.Button.AppWidget"
android:layout_weight="1"
@ -46,7 +46,7 @@
android:contentDescription="@string/desc_skip_prev"
android:src="@drawable/ic_skip_prev" />
<ImageButton
<android.widget.ImageButton
android:id="@+id/widget_play_pause"
style="@style/Widget.Auxio.Button.AppWidget"
android:layout_weight="1"
@ -55,7 +55,7 @@
android:contentDescription="@string/desc_play_pause"
android:src="@drawable/ic_play" />
<ImageButton
<android.widget.ImageButton
android:id="@+id/widget_skip_next"
style="@style/Widget.Auxio.Button.AppWidget"
android:layout_weight="1"
@ -64,8 +64,8 @@
android:contentDescription="@string/desc_skip_next"
android:src="@drawable/ic_skip_next" />
</LinearLayout>
</android.widget.LinearLayout>
</LinearLayout>
</android.widget.LinearLayout>
</LinearLayout>

View file

@ -53,8 +53,6 @@
<item name="android:textColorPrimaryInverseDisableOnly">@color/m3_dynamic_primary_text_disable_only</item>
<item name="android:textColorHint">@color/m3_dynamic_dark_hint_foreground</item>
<item name="android:textColorHintInverse">@color/m3_dynamic_hint_foreground</item>
<item name="android:textColorHighlight">@color/m3_dynamic_dark_highlighted_text</item>
<item name="android:textColorHighlightInverse">@color/m3_dynamic_highlighted_text</item>
<item name="android:textColorAlertDialogListItem">@color/m3_dynamic_dark_default_color_primary_text</item>
</style>
</resources>

View file

@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<bool name="enable_theme_settings">false</bool>
</resources>

View file

@ -53,8 +53,6 @@
<item name="android:textColorPrimaryInverseDisableOnly">@color/m3_dynamic_dark_primary_text_disable_only</item>
<item name="android:textColorHint">@color/m3_dynamic_hint_foreground</item>
<item name="android:textColorHintInverse">@color/m3_dynamic_dark_hint_foreground</item>
<item name="android:textColorHighlight">@color/m3_dynamic_highlighted_text</item>
<item name="android:textColorHighlightInverse">@color/m3_dynamic_dark_highlighted_text</item>
<item name="android:textColorAlertDialogListItem">@color/m3_dynamic_default_color_primary_text</item>
</style>

View file

@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<bool name="enable_theme_settings">true</bool>
</resources>

View file

@ -10,6 +10,7 @@
<!-- Size Namespace | Width & Heights for UI elements -->
<dimen name="size_btn_small">48dp</dimen>
<dimen name="size_btn_medium">56dp</dimen>
<dimen name="size_btn_large">64dp</dimen>
<dimen name="size_cover_compact">48dp</dimen>

View file

@ -70,6 +70,8 @@
<string name="setting_black_mode_desc">Use a pure-black dark theme</string>
<string name="set_display">Display</string>
<string name="set_lib_tabs">Library tabs</string>
<string name="set_lib_tabs_desc">Change visibility and order of library tabs</string>
<string name="set_show_covers">Show album covers</string>
<string name="set_show_covers_desc">Turn off to save memory usage</string>
<string name="set_quality_covers">Ignore MediaStore covers</string>
@ -120,7 +122,8 @@
<string name="desc_clear_user_queue">Clear queue</string>
<string name="desc_clear_queue_item">Remove this queue item</string>
<string name="desc_queue_handle">Move queue song</string>
<string name="desc_queue_handle">Move this queue song</string>
<string name="desc_tab_handle">Move this tab</string>
<string name="desc_clear_search">Clear search query</string>
<string name="desc_blacklist_delete">Remove excluded directory</string>
@ -162,6 +165,7 @@
<string name="fmt_next_from">Next From: %s</string>
<string name="fmt_songs_loaded">Songs loaded: %d</string>
<plurals name="fmt_song_count">
<item quantity="one">%d Song</item>
<item quantity="other">%d Songs</item>

View file

@ -31,7 +31,6 @@
<item name="android:layout_height">wrap_content</item>
<item name="android:layout_width">wrap_content</item>
<item name="android:fontFamily">@font/inter_semibold</item>
<item name="android:textColor">?attr/colorPrimary</item>
</style>
<!-- Custom button style that eliminates the weird margin that the neutral button has -->

View file

@ -18,16 +18,16 @@
<item name="colorOnTertiaryContainer">?attr/colorOnPrimaryContainer</item>
<item name="colorSurface">@color/surface</item>
<item name="colorAccent">?attr/colorPrimary</item>
<item name="colorControlNormal">@color/control</item>
<item name="colorControlHighlight">@color/overlay_selection</item>
<item name="colorControlActivated">?attr/colorPrimary</item>
</style>
<!-- Base theme -->
<style name="Theme.Auxio.App" parent="Theme.Auxio.V31">
<!-- Values -->
<item name="colorOutline">@color/overlay_stroke</item>
<item name="colorAccent">?attr/colorSecondary</item>
<item name="colorControlHighlight">@color/overlay_selection</item>
<item name="colorControlActivated">?attr/colorSecondary</item>
<!-- Android component magic -->
<item name="android:textColorHighlight">@color/overlay_text_highlight</item>

View file

@ -5,7 +5,7 @@
<!-- Base toolbar style -->
<style name="Widget.Auxio.Toolbar" parent="ThemeOverlay.Material3.ActionBar">
<item name="android:layout_width">match_parent</item>
<item name="android:layout_height">?android:attr/actionBarSize</item>
<item name="android:layout_height">@dimen/size_btn_medium</item>
<item name="titleTextAppearance">@style/TextAppearance.ToolbarTitle</item>
</style>

View file

@ -1,109 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<PreferenceScreen xmlns:app="http://schemas.android.com/apk/res-auto">
<PreferenceCategory
app:layout="@layout/item_header"
app:title="@string/set_display">
<SwitchPreferenceCompat
app:defaultValue="true"
app:iconSpaceReserved="false"
app:key="KEY_SHOW_COVERS"
app:summary="@string/set_show_covers_desc"
app:title="@string/set_show_covers" />
<SwitchPreferenceCompat
app:defaultValue="false"
app:dependency="KEY_SHOW_COVERS"
app:iconSpaceReserved="false"
app:key="KEY_QUALITY_COVERS"
app:summary="@string/set_quality_covers_desc"
app:title="@string/set_quality_covers" />
<SwitchPreferenceCompat
app:allowDividerBelow="false"
app:defaultValue="false"
app:iconSpaceReserved="false"
app:key="KEY_ALT_NOTIF_ACTION"
app:summaryOff="@string/set_alt_loop"
app:summaryOn="@string/set_alt_shuffle"
app:title="@string/set_alt_action" />
</PreferenceCategory>
<PreferenceCategory
app:layout="@layout/item_header"
app:title="@string/set_audio">
<SwitchPreferenceCompat
app:defaultValue="true"
app:iconSpaceReserved="false"
app:key="KEY_AUDIO_FOCUS"
app:summary="@string/set_focus_desc"
app:title="@string/set_focus" />
<SwitchPreferenceCompat
app:allowDividerBelow="false"
app:defaultValue="true"
app:iconSpaceReserved="false"
app:key="KEY_PLUG_MGT"
app:summary="@string/set_plug_mgt_desc"
app:title="@string/set_plug_mgt" />
</PreferenceCategory>
<PreferenceCategory
app:layout="@layout/item_header"
app:title="@string/set_behavior">
<org.oxycblt.auxio.settings.IntListPreference
app:defaultValue="@integer/play_mode_songs"
app:entries="@array/entries_song_playback_mode"
app:entryValues="@array/values_song_playback_mode"
app:iconSpaceReserved="false"
app:key="KEY_SONG_PLAY_MODE2"
app:title="@string/set_song_mode"
app:useSimpleSummaryProvider="true" />
<SwitchPreferenceCompat
app:defaultValue="true"
app:iconSpaceReserved="false"
app:key="KEY_KEEP_SHUFFLE"
app:summary="@string/set_keep_shuffle_desc"
app:title="@string/set_keep_shuffle" />
<SwitchPreferenceCompat
app:allowDividerBelow="false"
app:defaultValue="true"
app:iconSpaceReserved="false"
app:key="KEY_PREV_REWIND"
app:summary="@string/set_rewind_prev_desc"
app:title="@string/set_rewind_prev" />
<SwitchPreferenceCompat
app:allowDividerBelow="false"
app:defaultValue="false"
app:iconSpaceReserved="false"
app:key="KEY_LOOP_PAUSE"
app:summary="@string/set_loop_pause_desc"
app:title="@string/set_loop_pause" />
</PreferenceCategory>
<PreferenceCategory
app:layout="@layout/item_header"
app:title="@string/set_content">
<Preference
app:iconSpaceReserved="false"
app:key="KEY_SAVE_STATE"
app:summary="@string/set_save_desc"
app:title="@string/set_save" />
<Preference
app:iconSpaceReserved="false"
app:key="KEY_BLACKLIST"
app:summary="@string/set_excluded_desc"
app:title="@string/set_excluded" />
</PreferenceCategory>
</PreferenceScreen>

View file

@ -2,9 +2,10 @@
<PreferenceScreen xmlns:app="http://schemas.android.com/apk/res-auto">
<PreferenceCategory
app:layout="@layout/item_header"
app:isPreferenceVisible="@bool/enable_theme_settings"
app:title="@string/set_ui">
<org.oxycblt.auxio.settings.IntListPreference
<org.oxycblt.auxio.settings.pref.IntListPreference
app:defaultValue="@integer/theme_auto"
app:entries="@array/entires_theme"
app:entryValues="@array/values_theme"
@ -14,7 +15,6 @@
app:title="@string/set_theme" />
<Preference
app:allowDividerBelow="false"
app:icon="@drawable/ic_accent"
app:key="KEY_ACCENT2"
app:summary="@string/clr_blue"
@ -34,6 +34,12 @@
app:layout="@layout/item_header"
app:title="@string/set_display">
<Preference
app:key="KEY_LIB_TABS"
app:title="@string/set_lib_tabs"
app:summary="@string/set_lib_tabs_desc"
app:iconSpaceReserved="false" />
<SwitchPreferenceCompat
app:defaultValue="true"
app:iconSpaceReserved="false"
@ -85,7 +91,7 @@
app:layout="@layout/item_header"
app:title="@string/set_behavior">
<org.oxycblt.auxio.settings.IntListPreference
<org.oxycblt.auxio.settings.pref.IntListPreference
app:defaultValue="@integer/play_mode_songs"
app:entries="@array/entries_song_playback_mode"
app:entryValues="@array/values_song_playback_mode"