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 - Search Functionality
- Audio Focus / Headset Management - Audio Focus / Headset Management
- No internet connectivity whatsoever - No internet connectivity whatsoever
- Kotlin from the ground-up - No rounded album corners
- Modular, feature-based architecture
- No rounded corners
## To possibly come in the future: ## To possibly come in the future:
- Playlists - Playlists
- Liked songs - Liked songs
- Improved tablet layouts
- More notification actions - More notification actions
- Other things, possibly - Other things, possibly

View file

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

View file

@ -49,7 +49,6 @@ import org.oxycblt.auxio.music.Genre
import org.oxycblt.auxio.music.Song import org.oxycblt.auxio.music.Song
import org.oxycblt.auxio.ui.DisplayMode import org.oxycblt.auxio.ui.DisplayMode
import org.oxycblt.auxio.ui.SortMode import org.oxycblt.auxio.ui.SortMode
import org.oxycblt.auxio.ui.memberBinding
import org.oxycblt.auxio.util.applyEdge import org.oxycblt.auxio.util.applyEdge
import org.oxycblt.auxio.util.logD import org.oxycblt.auxio.util.logD
import org.oxycblt.auxio.util.logE 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 * The main "Launching Point" fragment of Auxio, allowing navigation to the detail
* views for each respective fragment. * views for each respective fragment.
* FIXME: More UI glitches: * FIXME: Edge-to-edge is borked still, unsure how to really fix this aside from making some
* - 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
* magic layout like Material Files, but even then it might not work since the scrolling * 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. * views are not laid side-by-side to the layout itself.
* @author OxygenCobalt * @author OxygenCobalt
*/ */
class HomeFragment : Fragment() { class HomeFragment : Fragment() {
private val binding: FragmentHomeBinding by memberBinding(FragmentHomeBinding::inflate)
private val detailModel: DetailViewModel by activityViewModels() private val detailModel: DetailViewModel by activityViewModels()
private val homeModel: HomeViewModel by activityViewModels() private val homeModel: HomeViewModel by activityViewModels()
@ -75,6 +70,7 @@ class HomeFragment : Fragment() {
container: ViewGroup?, container: ViewGroup?,
savedInstanceState: Bundle? savedInstanceState: Bundle?
): View { ): View {
val binding = FragmentHomeBinding.inflate(inflater)
val sortItem: MenuItem val sortItem: MenuItem
// --- UI SETUP --- // --- UI SETUP ---
@ -86,6 +82,12 @@ class HomeFragment : Fragment() {
} }
binding.homeAppbar.apply { 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( addOnOffsetChangedListener(
AppBarLayout.OnOffsetChangedListener { _, verticalOffset -> AppBarLayout.OnOffsetChangedListener { _, verticalOffset ->
binding.homeToolbar.alpha = (binding.homeToolbar.height + 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 { post {
// To add our fast scroller, we need to
val vOffset = ( val vOffset = (
(layoutParams as CoordinatorLayout.LayoutParams) (layoutParams as CoordinatorLayout.LayoutParams)
.behavior as AppBarLayout.Behavior .behavior as AppBarLayout.Behavior
@ -135,7 +142,7 @@ class HomeFragment : Fragment() {
R.id.submenu_sorting -> { } 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 -> { else -> {
item.isChecked = true 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 // 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 // limit to that. This also prevents the appbar lift state from being confused during
// page transitions. // page transitions.
offscreenPageLimit = homeModel.tabs.value!!.size offscreenPageLimit = homeModel.tabs.size
registerOnPageChangeCallback(object : ViewPager2.OnPageChangeCallback() { registerOnPageChangeCallback(object : ViewPager2.OnPageChangeCallback() {
override fun onPageSelected(position: Int) = homeModel.updateCurrentTab(position) override fun onPageSelected(position: Int) = homeModel.updateCurrentTab(position)
@ -187,7 +194,7 @@ class HomeFragment : Fragment() {
} }
TabLayoutMediator(binding.homeTabs, binding.homePager) { tab, pos -> 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_SONGS -> R.string.lbl_songs
DisplayMode.SHOW_ALBUMS -> R.string.lbl_albums DisplayMode.SHOW_ALBUMS -> R.string.lbl_albums
DisplayMode.SHOW_ARTISTS -> R.string.lbl_artists DisplayMode.SHOW_ARTISTS -> R.string.lbl_artists
@ -199,7 +206,19 @@ class HomeFragment : Fragment() {
// --- VIEWMODEL SETUP --- // --- 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 -> 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)) { binding.homeAppbar.liftOnScrollTargetViewId = when (requireNotNull(tab)) {
DisplayMode.SHOW_SONGS -> { DisplayMode.SHOW_SONGS -> {
updateSortMenu(sortItem, tab) updateSortMenu(sortItem, tab)
@ -280,10 +299,10 @@ class HomeFragment : Fragment() {
private inner class HomePagerAdapter : private inner class HomePagerAdapter :
FragmentStateAdapter(childFragmentManager, viewLifecycleOwner.lifecycle) { 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 { override fun createFragment(position: Int): Fragment {
return when (homeModel.tabs.value!![position]) { return when (homeModel.tabs[position]) {
DisplayMode.SHOW_SONGS -> SongListFragment() DisplayMode.SHOW_SONGS -> SongListFragment()
DisplayMode.SHOW_ALBUMS -> AlbumListFragment() DisplayMode.SHOW_ALBUMS -> AlbumListFragment()
DisplayMode.SHOW_ARTISTS -> ArtistListFragment() 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.MusicStore
import org.oxycblt.auxio.music.Song import org.oxycblt.auxio.music.Song
import org.oxycblt.auxio.settings.SettingsManager import org.oxycblt.auxio.settings.SettingsManager
import org.oxycblt.auxio.settings.tabs.Tab
import org.oxycblt.auxio.ui.DisplayMode import org.oxycblt.auxio.ui.DisplayMode
import org.oxycblt.auxio.ui.SortMode import org.oxycblt.auxio.ui.SortMode
/** /**
* The ViewModel for managing [HomeFragment]'s data and sorting modes. * The ViewModel for managing [HomeFragment]'s data, sorting modes, and tab state.
* TODO: Custom tabs * @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>()) private val mSongs = MutableLiveData(listOf<Song>())
val songs: LiveData<List<Song>> get() = mSongs val songs: LiveData<List<Song>> get() = mSongs
@ -47,32 +51,38 @@ class HomeViewModel : ViewModel() {
private val mGenres = MutableLiveData(listOf<Genre>()) private val mGenres = MutableLiveData(listOf<Genre>())
val genres: LiveData<List<Genre>> get() = mGenres val genres: LiveData<List<Genre>> get() = mGenres
private val mTabs = MutableLiveData( var tabs: List<DisplayMode> = settingsManager.visibleTabs
arrayOf( private set
DisplayMode.SHOW_SONGS, DisplayMode.SHOW_ALBUMS,
DisplayMode.SHOW_ARTISTS, DisplayMode.SHOW_GENRES
)
)
val tabs: LiveData<Array<DisplayMode>> = mTabs
private val mCurTab = MutableLiveData(mTabs.value!![0]) private val mCurTab = MutableLiveData(tabs[0])
val curTab: LiveData<DisplayMode> = mCurTab 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 { init {
mSongs.value = settingsManager.libSongSort.sortSongs(musicStore.songs) mSongs.value = settingsManager.libSongSort.sortSongs(musicStore.songs)
mAlbums.value = settingsManager.libAlbumSort.sortAlbums(musicStore.albums) mAlbums.value = settingsManager.libAlbumSort.sortAlbums(musicStore.albums)
mArtists.value = settingsManager.libArtistSort.sortModels(musicStore.artists) mArtists.value = settingsManager.libArtistSort.sortModels(musicStore.artists)
mGenres.value = settingsManager.libGenreSort.sortModels(musicStore.genres) mGenres.value = settingsManager.libGenreSort.sortModels(musicStore.genres)
settingsManager.addCallback(this)
} }
/** /**
* Update the current tab based off of the new ViewPager position. * Update the current tab based off of the new ViewPager position.
*/ */
fun updateCurrentTab(pos: Int) { fun updateCurrentTab(pos: Int) {
mCurTab.value = mTabs.value!![pos] mCurTab.value = tabs[pos]
}
fun finishRecreateTabs() {
mRecreateTabs.value = false
} }
fun getSortForDisplay(displayMode: DisplayMode): SortMode { 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.applySpans
import org.oxycblt.auxio.util.resolveDrawable import org.oxycblt.auxio.util.resolveDrawable
/**
* A Base [Fragment] implementing the base features shared across all detail fragments.
*
*/
abstract class HomeListFragment : Fragment() { abstract class HomeListFragment : Fragment() {
protected val binding: FragmentHomeListBinding by memberBinding( protected val binding: FragmentHomeListBinding by memberBinding(
FragmentHomeListBinding::inflate FragmentHomeListBinding::inflate
@ -83,6 +87,9 @@ abstract class HomeListFragment : Fragment() {
@SuppressLint("NotifyDataSetChanged") @SuppressLint("NotifyDataSetChanged")
fun updateData(newData: List<T>) { fun updateData(newData: List<T>) {
data = newData 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() 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 * 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. * 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. * to your AlgoPop StreamMix instead.
* *
* I wish I was born in the neolithic. * I wish I was born in the neolithic.

View file

@ -133,8 +133,6 @@ class SearchFragment : Fragment() {
searchModel.searchResults.observe(viewLifecycleOwner) { results -> searchModel.searchResults.observe(viewLifecycleOwner) { results ->
searchAdapter.submitList(results) { searchAdapter.submitList(results) {
// We've just scrolled back to the top, reset the lifted state // 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) 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.accent.AccentDialog
import org.oxycblt.auxio.excluded.ExcludedDialog import org.oxycblt.auxio.excluded.ExcludedDialog
import org.oxycblt.auxio.playback.PlaybackViewModel 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.applyEdge
import org.oxycblt.auxio.util.isNight import org.oxycblt.auxio.util.isNight
import org.oxycblt.auxio.util.logD import org.oxycblt.auxio.util.logD
@ -130,6 +133,13 @@ class SettingsListFragment : PreferenceFragmentCompat() {
summary = Accent.get().getDetailedSummary(context) 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 -> { SettingsManager.KEY_SHOW_COVERS, SettingsManager.KEY_QUALITY_COVERS -> {
onPreferenceChangeListener = Preference.OnPreferenceChangeListener { _, _ -> onPreferenceChangeListener = Preference.OnPreferenceChangeListener { _, _ ->
Coil.imageLoader(requireContext()).apply { Coil.imageLoader(requireContext()).apply {

View file

@ -24,6 +24,7 @@ import androidx.core.content.edit
import androidx.preference.PreferenceManager import androidx.preference.PreferenceManager
import org.oxycblt.auxio.accent.Accent import org.oxycblt.auxio.accent.Accent
import org.oxycblt.auxio.playback.state.PlaybackMode 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.DisplayMode
import org.oxycblt.auxio.ui.SortMode import org.oxycblt.auxio.ui.SortMode
@ -70,9 +71,21 @@ class SettingsManager private constructor(context: Context) :
val useAltNotifAction: Boolean val useAltNotifAction: Boolean
get() = sharedPrefs.getBoolean(KEY_USE_ALT_NOTIFICATION_ACTION, false) get() = sharedPrefs.getBoolean(KEY_USE_ALT_NOTIFICATION_ACTION, false)
/** /** The current library tabs preferred by the user. */
* Whether to even loading embedded covers 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 val showCovers: Boolean
get() = sharedPrefs.getBoolean(KEY_SHOW_COVERS, true) 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 var libSongSort: SortMode
get() = SortMode.fromInt(sharedPrefs.getInt(KEY_LIB_SONGS_SORT, Int.MIN_VALUE)) get() = SortMode.fromInt(sharedPrefs.getInt(KEY_LIB_SONGS_SORT, Int.MIN_VALUE))
?: SortMode.ASCENDING ?: SortMode.ASCENDING
@ -124,6 +138,7 @@ class SettingsManager private constructor(context: Context) :
} }
} }
/** The album sort mode on HomeFragment **/
var libAlbumSort: SortMode var libAlbumSort: SortMode
get() = SortMode.fromInt(sharedPrefs.getInt(KEY_LIB_ALBUMS_SORT, Int.MIN_VALUE)) get() = SortMode.fromInt(sharedPrefs.getInt(KEY_LIB_ALBUMS_SORT, Int.MIN_VALUE))
?: SortMode.ASCENDING ?: SortMode.ASCENDING
@ -134,6 +149,7 @@ class SettingsManager private constructor(context: Context) :
} }
} }
/** The artist sort mode on HomeFragment **/
var libArtistSort: SortMode var libArtistSort: SortMode
get() = SortMode.fromInt(sharedPrefs.getInt(KEY_LIB_ARTISTS_SORT, Int.MIN_VALUE)) get() = SortMode.fromInt(sharedPrefs.getInt(KEY_LIB_ARTISTS_SORT, Int.MIN_VALUE))
?: SortMode.ASCENDING ?: SortMode.ASCENDING
@ -144,6 +160,7 @@ class SettingsManager private constructor(context: Context) :
} }
} }
/** The genre sort mode on HomeFragment **/
var libGenreSort: SortMode var libGenreSort: SortMode
get() = SortMode.fromInt(sharedPrefs.getInt(KEY_LIB_GENRE_SORT, Int.MIN_VALUE)) get() = SortMode.fromInt(sharedPrefs.getInt(KEY_LIB_GENRE_SORT, Int.MIN_VALUE))
?: SortMode.ASCENDING ?: SortMode.ASCENDING
@ -154,6 +171,7 @@ class SettingsManager private constructor(context: Context) :
} }
} }
/** The detail album sort mode **/
var detailAlbumSort: SortMode var detailAlbumSort: SortMode
get() = SortMode.fromInt(sharedPrefs.getInt(KEY_DETAIL_ALBUM_SORT, Int.MIN_VALUE)) get() = SortMode.fromInt(sharedPrefs.getInt(KEY_DETAIL_ALBUM_SORT, Int.MIN_VALUE))
?: SortMode.ASCENDING ?: SortMode.ASCENDING
@ -164,6 +182,7 @@ class SettingsManager private constructor(context: Context) :
} }
} }
/** The detail artist sort mode **/
var detailArtistSort: SortMode var detailArtistSort: SortMode
get() = SortMode.fromInt(sharedPrefs.getInt(KEY_DETAIL_ARTIST_SORT, Int.MIN_VALUE)) get() = SortMode.fromInt(sharedPrefs.getInt(KEY_DETAIL_ARTIST_SORT, Int.MIN_VALUE))
?: SortMode.YEAR ?: SortMode.YEAR
@ -174,6 +193,7 @@ class SettingsManager private constructor(context: Context) :
} }
} }
/** The detail genre sort mode **/
var detailGenreSort: SortMode var detailGenreSort: SortMode
get() = SortMode.fromInt(sharedPrefs.getInt(KEY_DETAIL_GENRE_SORT, Int.MIN_VALUE)) get() = SortMode.fromInt(sharedPrefs.getInt(KEY_DETAIL_GENRE_SORT, Int.MIN_VALUE))
?: SortMode.ASCENDING ?: SortMode.ASCENDING
@ -211,6 +231,10 @@ class SettingsManager private constructor(context: Context) :
KEY_QUALITY_COVERS -> callbacks.forEach { KEY_QUALITY_COVERS -> callbacks.forEach {
it.onQualityCoverUpdate(useQualityCovers) it.onQualityCoverUpdate(useQualityCovers)
} }
KEY_LIB_TABS -> callbacks.forEach {
it.onLibTabsUpdate(libTabs)
}
} }
} }
@ -220,6 +244,7 @@ class SettingsManager private constructor(context: Context) :
* context. * context.
*/ */
interface Callback { interface Callback {
fun onLibTabsUpdate(libTabs: Array<Tab>) {}
fun onColorizeNotifUpdate(doColorize: Boolean) {} fun onColorizeNotifUpdate(doColorize: Boolean) {}
fun onNotifActionUpdate(useAltAction: Boolean) {} fun onNotifActionUpdate(useAltAction: Boolean) {}
fun onShowCoverUpdate(showCovers: 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_BLACK_THEME = "KEY_BLACK_THEME"
const val KEY_ACCENT = "KEY_ACCENT2" 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_SHOW_COVERS = "KEY_SHOW_COVERS"
const val KEY_QUALITY_COVERS = "KEY_QUALITY_COVERS" const val KEY_QUALITY_COVERS = "KEY_QUALITY_COVERS"
const val KEY_USE_ALT_NOTIFICATION_ACTION = "KEY_ALT_NOTIF_ACTION" 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/>. * 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 android.os.Bundle
import androidx.appcompat.app.AlertDialog import androidx.appcompat.app.AlertDialog
@ -24,6 +24,9 @@ import androidx.preference.PreferenceFragmentCompat
import org.oxycblt.auxio.BuildConfig import org.oxycblt.auxio.BuildConfig
import org.oxycblt.auxio.ui.LifecycleDialog import org.oxycblt.auxio.ui.LifecycleDialog
/**
* The dialog shown whenever an [IntListPreference] is shown.
*/
class IntListPrefDialog : LifecycleDialog() { class IntListPrefDialog : LifecycleDialog() {
override fun onConfigDialog(builder: AlertDialog.Builder) { override fun onConfigDialog(builder: AlertDialog.Builder) {
// Since we have to store the preference key as an argument, we have to find the // 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/>. * 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.Context
import android.content.res.TypedArray 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 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. * 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 * @author OxygenCobalt
*/ */
enum class DisplayMode { enum class DisplayMode(@DrawableRes val icon: Int, @StringRes val string: Int) {
SHOW_GENRES, SHOW_SONGS(R.drawable.ic_song, R.string.lbl_songs),
SHOW_ARTISTS, SHOW_ALBUMS(R.drawable.ic_album, R.string.lbl_albums),
SHOW_ALBUMS, SHOW_ARTISTS(R.drawable.ic_artist, R.string.lbl_artists),
SHOW_SONGS; SHOW_GENRES(R.drawable.ic_genre, R.string.lbl_genres);
companion object { companion object {
private const val CONST_NULL = 0xA107 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 * 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 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 * - 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) { private fun requestUpdate(context: Context) {
logD("Sending update intent to PlaybackService") 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"?> <?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android"> <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/colorPrimary" android:state_activated="true" />
<item android:color="?attr/colorControlNormal" /> <item android:color="?attr/colorControlNormal" />
</selector> </selector>

View file

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

View file

@ -11,7 +11,6 @@
android:paddingTop="@dimen/spacing_medium" android:paddingTop="@dimen/spacing_medium"
android:paddingStart="@dimen/spacing_small" android:paddingStart="@dimen/spacing_small"
android:paddingEnd="@dimen/spacing_small" android:paddingEnd="@dimen/spacing_small"
android:paddingBottom="@dimen/spacing_small"
app:layoutManager="org.oxycblt.auxio.accent.AutoGridLayoutManager" app:layoutManager="org.oxycblt.auxio.accent.AutoGridLayoutManager"
app:layout_constraintBottom_toTopOf="@+id/accent_cancel" app:layout_constraintBottom_toTopOf="@+id/accent_cancel"
app:layout_constraintTop_toBottomOf="@+id/accent_header" 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:liftOnScroll="true"
app:liftOnScrollTargetViewId="@id/search_recycler"> app:liftOnScrollTargetViewId="@id/search_recycler">
<androidx.appcompat.widget.Toolbar <com.google.android.material.appbar.MaterialToolbar
android:id="@+id/search_toolbar" android:id="@+id/search_toolbar"
style="@style/Widget.Auxio.Toolbar.Icon" style="@style/Widget.Auxio.Toolbar.Icon"
app:menu="@menu/menu_search"> app:menu="@menu/menu_search">
@ -43,7 +43,7 @@
</com.google.android.material.textfield.TextInputLayout> </com.google.android.material.textfield.TextInputLayout>
</androidx.appcompat.widget.Toolbar> </com.google.android.material.appbar.MaterialToolbar>
</org.oxycblt.auxio.ui.LiftAppBarLayout> </org.oxycblt.auxio.ui.LiftAppBarLayout>

View file

@ -91,7 +91,7 @@
android:src="@drawable/ic_handle" android:src="@drawable/ic_handle"
app:layout_constraintBottom_toBottomOf="@+id/album_cover" app:layout_constraintBottom_toBottomOf="@+id/album_cover"
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="@+id/song_name" /> app:layout_constraintTop_toTopOf="@+id/album_cover" />
</androidx.constraintlayout.widget.ConstraintLayout> </androidx.constraintlayout.widget.ConstraintLayout>
</FrameLayout> </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" android:theme="@style/Theme.Widget"
tools:ignore="Overdraw"> tools:ignore="Overdraw">
<ImageView <android.widget.ImageView
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"
android:alpha="0.3" android:alpha="0.3"
@ -16,7 +16,7 @@
android:scaleType="centerCrop" android:scaleType="centerCrop"
android:src="@drawable/ic_song" /> android:src="@drawable/ic_song" />
<TextView <android.widget.TextView
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_gravity="center" android:layout_gravity="center"

View file

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

View file

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

View file

@ -53,8 +53,6 @@
<item name="android:textColorPrimaryInverseDisableOnly">@color/m3_dynamic_primary_text_disable_only</item> <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:textColorHint">@color/m3_dynamic_dark_hint_foreground</item>
<item name="android:textColorHintInverse">@color/m3_dynamic_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> <item name="android:textColorAlertDialogListItem">@color/m3_dynamic_dark_default_color_primary_text</item>
</style> </style>
</resources> </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:textColorPrimaryInverseDisableOnly">@color/m3_dynamic_dark_primary_text_disable_only</item>
<item name="android:textColorHint">@color/m3_dynamic_hint_foreground</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: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> <item name="android:textColorAlertDialogListItem">@color/m3_dynamic_default_color_primary_text</item>
</style> </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 --> <!-- Size Namespace | Width & Heights for UI elements -->
<dimen name="size_btn_small">48dp</dimen> <dimen name="size_btn_small">48dp</dimen>
<dimen name="size_btn_medium">56dp</dimen>
<dimen name="size_btn_large">64dp</dimen> <dimen name="size_btn_large">64dp</dimen>
<dimen name="size_cover_compact">48dp</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="setting_black_mode_desc">Use a pure-black dark theme</string>
<string name="set_display">Display</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">Show album covers</string>
<string name="set_show_covers_desc">Turn off to save memory usage</string> <string name="set_show_covers_desc">Turn off to save memory usage</string>
<string name="set_quality_covers">Ignore MediaStore covers</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_user_queue">Clear queue</string>
<string name="desc_clear_queue_item">Remove this queue item</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_clear_search">Clear search query</string>
<string name="desc_blacklist_delete">Remove excluded directory</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_next_from">Next From: %s</string>
<string name="fmt_songs_loaded">Songs loaded: %d</string> <string name="fmt_songs_loaded">Songs loaded: %d</string>
<plurals name="fmt_song_count"> <plurals name="fmt_song_count">
<item quantity="one">%d Song</item> <item quantity="one">%d Song</item>
<item quantity="other">%d Songs</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_height">wrap_content</item>
<item name="android:layout_width">wrap_content</item> <item name="android:layout_width">wrap_content</item>
<item name="android:fontFamily">@font/inter_semibold</item> <item name="android:fontFamily">@font/inter_semibold</item>
<item name="android:textColor">?attr/colorPrimary</item>
</style> </style>
<!-- Custom button style that eliminates the weird margin that the neutral button has --> <!-- 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="colorOnTertiaryContainer">?attr/colorOnPrimaryContainer</item>
<item name="colorSurface">@color/surface</item> <item name="colorSurface">@color/surface</item>
<item name="colorAccent">?attr/colorPrimary</item>
<item name="colorControlNormal">@color/control</item> <item name="colorControlNormal">@color/control</item>
<item name="colorControlHighlight">@color/overlay_selection</item>
<item name="colorControlActivated">?attr/colorPrimary</item>
</style> </style>
<!-- Base theme --> <!-- Base theme -->
<style name="Theme.Auxio.App" parent="Theme.Auxio.V31"> <style name="Theme.Auxio.App" parent="Theme.Auxio.V31">
<!-- Values --> <!-- Values -->
<item name="colorOutline">@color/overlay_stroke</item> <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 --> <!-- Android component magic -->
<item name="android:textColorHighlight">@color/overlay_text_highlight</item> <item name="android:textColorHighlight">@color/overlay_text_highlight</item>

View file

@ -5,7 +5,7 @@
<!-- Base toolbar style --> <!-- Base toolbar style -->
<style name="Widget.Auxio.Toolbar" parent="ThemeOverlay.Material3.ActionBar"> <style name="Widget.Auxio.Toolbar" parent="ThemeOverlay.Material3.ActionBar">
<item name="android:layout_width">match_parent</item> <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> <item name="titleTextAppearance">@style/TextAppearance.ToolbarTitle</item>
</style> </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"> <PreferenceScreen xmlns:app="http://schemas.android.com/apk/res-auto">
<PreferenceCategory <PreferenceCategory
app:layout="@layout/item_header" app:layout="@layout/item_header"
app:isPreferenceVisible="@bool/enable_theme_settings"
app:title="@string/set_ui"> app:title="@string/set_ui">
<org.oxycblt.auxio.settings.IntListPreference <org.oxycblt.auxio.settings.pref.IntListPreference
app:defaultValue="@integer/theme_auto" app:defaultValue="@integer/theme_auto"
app:entries="@array/entires_theme" app:entries="@array/entires_theme"
app:entryValues="@array/values_theme" app:entryValues="@array/values_theme"
@ -14,7 +15,6 @@
app:title="@string/set_theme" /> app:title="@string/set_theme" />
<Preference <Preference
app:allowDividerBelow="false"
app:icon="@drawable/ic_accent" app:icon="@drawable/ic_accent"
app:key="KEY_ACCENT2" app:key="KEY_ACCENT2"
app:summary="@string/clr_blue" app:summary="@string/clr_blue"
@ -34,6 +34,12 @@
app:layout="@layout/item_header" app:layout="@layout/item_header"
app:title="@string/set_display"> 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 <SwitchPreferenceCompat
app:defaultValue="true" app:defaultValue="true"
app:iconSpaceReserved="false" app:iconSpaceReserved="false"
@ -85,7 +91,7 @@
app:layout="@layout/item_header" app:layout="@layout/item_header"
app:title="@string/set_behavior"> app:title="@string/set_behavior">
<org.oxycblt.auxio.settings.IntListPreference <org.oxycblt.auxio.settings.pref.IntListPreference
app:defaultValue="@integer/play_mode_songs" app:defaultValue="@integer/play_mode_songs"
app:entries="@array/entries_song_playback_mode" app:entries="@array/entries_song_playback_mode"
app:entryValues="@array/values_song_playback_mode" app:entryValues="@array/values_song_playback_mode"