From 23d1be8ebcb430774e522e236e2f28423752b631 Mon Sep 17 00:00:00 2001 From: OxygenCobalt Date: Sun, 17 Oct 2021 20:27:16 -0600 Subject: [PATCH] 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. --- README.md | 5 +- app/build.gradle | 4 +- .../org/oxycblt/auxio/home/HomeFragment.kt | 47 +++++-- .../org/oxycblt/auxio/home/HomeViewModel.kt | 50 +++++-- .../auxio/home/list/HomeListFragment.kt | 7 + .../org/oxycblt/auxio/music/MusicLoader.kt | 2 +- .../oxycblt/auxio/search/SearchFragment.kt | 2 - .../auxio/settings/SettingsListFragment.kt | 10 ++ .../oxycblt/auxio/settings/SettingsManager.kt | 32 ++++- .../settings/{ => pref}/IntListPrefDialog.kt | 5 +- .../settings/{ => pref}/IntListPreference.kt | 2 +- .../org/oxycblt/auxio/settings/tabs/Tab.kt | 124 ++++++++++++++++++ .../oxycblt/auxio/settings/tabs/TabAdapter.kt | 89 +++++++++++++ .../auxio/settings/tabs/TabCustomizeDialog.kt | 115 ++++++++++++++++ .../auxio/settings/tabs/TabDragCallback.kt | 88 +++++++++++++ .../java/org/oxycblt/auxio/ui/DisplayMode.kt | 16 ++- .../oxycblt/auxio/widgets/WidgetProvider.kt | 5 +- app/src/main/res/color/overlay_disabled.xml | 5 - app/src/main/res/color/sel_accented.xml | 1 + app/src/main/res/drawable/ic_queue.xml | 2 +- app/src/main/res/layout/dialog_accent.xml | 1 - app/src/main/res/layout/dialog_tabs.xml | 18 +++ app/src/main/res/layout/fragment_search.xml | 4 +- app/src/main/res/layout/item_queue_song.xml | 2 +- app/src/main/res/layout/item_tab.xml | 57 ++++++++ app/src/main/res/layout/widget_default.xml | 4 +- app/src/main/res/layout/widget_full.xml | 24 ++-- app/src/main/res/layout/widget_small.xml | 20 +-- .../main/res/values-night-v31/styles_core.xml | 2 - app/src/main/res/values-v31/bools.xml | 4 + app/src/main/res/values-v31/styles_core.xml | 2 - app/src/main/res/values/bools.xml | 4 + app/src/main/res/values/dimens.xml | 1 + app/src/main/res/values/strings.xml | 6 +- app/src/main/res/values/styles_android.xml | 1 - app/src/main/res/values/styles_core.xml | 6 +- app/src/main/res/values/styles_ui.xml | 2 +- app/src/main/res/xml-v31/prefs_main.xml | 109 --------------- app/src/main/res/xml/prefs_main.xml | 12 +- 39 files changed, 687 insertions(+), 203 deletions(-) rename app/src/main/java/org/oxycblt/auxio/settings/{ => pref}/IntListPrefDialog.kt (94%) rename app/src/main/java/org/oxycblt/auxio/settings/{ => pref}/IntListPreference.kt (98%) create mode 100644 app/src/main/java/org/oxycblt/auxio/settings/tabs/Tab.kt create mode 100644 app/src/main/java/org/oxycblt/auxio/settings/tabs/TabAdapter.kt create mode 100644 app/src/main/java/org/oxycblt/auxio/settings/tabs/TabCustomizeDialog.kt create mode 100644 app/src/main/java/org/oxycblt/auxio/settings/tabs/TabDragCallback.kt delete mode 100644 app/src/main/res/color/overlay_disabled.xml create mode 100644 app/src/main/res/layout/dialog_tabs.xml create mode 100644 app/src/main/res/layout/item_tab.xml create mode 100644 app/src/main/res/values-v31/bools.xml create mode 100644 app/src/main/res/values/bools.xml delete mode 100644 app/src/main/res/xml-v31/prefs_main.xml diff --git a/README.md b/README.md index 0432e3bc3..c41a9d029 100644 --- a/README.md +++ b/README.md @@ -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 diff --git a/app/build.gradle b/app/build.gradle index 74413472f..15583a9fd 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -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 --- diff --git a/app/src/main/java/org/oxycblt/auxio/home/HomeFragment.kt b/app/src/main/java/org/oxycblt/auxio/home/HomeFragment.kt index 5a6f20dc2..da1d29355 100644 --- a/app/src/main/java/org/oxycblt/auxio/home/HomeFragment.kt +++ b/app/src/main/java/org/oxycblt/auxio/home/HomeFragment.kt @@ -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 - * 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. + * 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() diff --git a/app/src/main/java/org/oxycblt/auxio/home/HomeViewModel.kt b/app/src/main/java/org/oxycblt/auxio/home/HomeViewModel.kt index 061083982..5a21a4b69 100644 --- a/app/src/main/java/org/oxycblt/auxio/home/HomeViewModel.kt +++ b/app/src/main/java/org/oxycblt/auxio/home/HomeViewModel.kt @@ -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()) val songs: LiveData> get() = mSongs @@ -47,32 +51,38 @@ class HomeViewModel : ViewModel() { private val mGenres = MutableLiveData(listOf()) val genres: LiveData> get() = mGenres - private val mTabs = MutableLiveData( - arrayOf( - DisplayMode.SHOW_SONGS, DisplayMode.SHOW_ALBUMS, - DisplayMode.SHOW_ARTISTS, DisplayMode.SHOW_GENRES - ) - ) - val tabs: LiveData> = mTabs + var tabs: List = settingsManager.visibleTabs + private set - private val mCurTab = MutableLiveData(mTabs.value!![0]) + private val mCurTab = MutableLiveData(tabs[0]) val curTab: LiveData = 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 = 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) { + tabs = settingsManager.visibleTabs + mRecreateTabs.value = true + } + + override fun onCleared() { + super.onCleared() + settingsManager.removeCallback(this) + } } diff --git a/app/src/main/java/org/oxycblt/auxio/home/list/HomeListFragment.kt b/app/src/main/java/org/oxycblt/auxio/home/list/HomeListFragment.kt index c06cb1a14..be1066b86 100644 --- a/app/src/main/java/org/oxycblt/auxio/home/list/HomeListFragment.kt +++ b/app/src/main/java/org/oxycblt/auxio/home/list/HomeListFragment.kt @@ -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) { 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() } } diff --git a/app/src/main/java/org/oxycblt/auxio/music/MusicLoader.kt b/app/src/main/java/org/oxycblt/auxio/music/MusicLoader.kt index 6642537f5..561bc4c07 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/MusicLoader.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/MusicLoader.kt @@ -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. diff --git a/app/src/main/java/org/oxycblt/auxio/search/SearchFragment.kt b/app/src/main/java/org/oxycblt/auxio/search/SearchFragment.kt index 73359a9fd..0aa51c56c 100644 --- a/app/src/main/java/org/oxycblt/auxio/search/SearchFragment.kt +++ b/app/src/main/java/org/oxycblt/auxio/search/SearchFragment.kt @@ -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) } diff --git a/app/src/main/java/org/oxycblt/auxio/settings/SettingsListFragment.kt b/app/src/main/java/org/oxycblt/auxio/settings/SettingsListFragment.kt index 52877126f..715b3e352 100644 --- a/app/src/main/java/org/oxycblt/auxio/settings/SettingsListFragment.kt +++ b/app/src/main/java/org/oxycblt/auxio/settings/SettingsListFragment.kt @@ -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 { diff --git a/app/src/main/java/org/oxycblt/auxio/settings/SettingsManager.kt b/app/src/main/java/org/oxycblt/auxio/settings/SettingsManager.kt index f35cff086..025eb047e 100644 --- a/app/src/main/java/org/oxycblt/auxio/settings/SettingsManager.kt +++ b/app/src/main/java/org/oxycblt/auxio/settings/SettingsManager.kt @@ -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 + 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 get() = libTabs.filterIsInstance().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) {} 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" diff --git a/app/src/main/java/org/oxycblt/auxio/settings/IntListPrefDialog.kt b/app/src/main/java/org/oxycblt/auxio/settings/pref/IntListPrefDialog.kt similarity index 94% rename from app/src/main/java/org/oxycblt/auxio/settings/IntListPrefDialog.kt rename to app/src/main/java/org/oxycblt/auxio/settings/pref/IntListPrefDialog.kt index 8cf8fbe94..93bbd141e 100644 --- a/app/src/main/java/org/oxycblt/auxio/settings/IntListPrefDialog.kt +++ b/app/src/main/java/org/oxycblt/auxio/settings/pref/IntListPrefDialog.kt @@ -16,7 +16,7 @@ * along with this program. If not, see . */ -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 diff --git a/app/src/main/java/org/oxycblt/auxio/settings/IntListPreference.kt b/app/src/main/java/org/oxycblt/auxio/settings/pref/IntListPreference.kt similarity index 98% rename from app/src/main/java/org/oxycblt/auxio/settings/IntListPreference.kt rename to app/src/main/java/org/oxycblt/auxio/settings/pref/IntListPreference.kt index 565b457f5..8f277d86b 100644 --- a/app/src/main/java/org/oxycblt/auxio/settings/IntListPreference.kt +++ b/app/src/main/java/org/oxycblt/auxio/settings/pref/IntListPreference.kt @@ -16,7 +16,7 @@ * along with this program. If not, see . */ -package org.oxycblt.auxio.settings +package org.oxycblt.auxio.settings.pref import android.content.Context import android.content.res.TypedArray diff --git a/app/src/main/java/org/oxycblt/auxio/settings/tabs/Tab.kt b/app/src/main/java/org/oxycblt/auxio/settings/tabs/Tab.kt new file mode 100644 index 000000000..a814b27f2 --- /dev/null +++ b/app/src/main/java/org/oxycblt/auxio/settings/tabs/Tab.kt @@ -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 . + */ + +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): 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? { + val tabs = mutableListOf() + + // 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() + } + } +} diff --git a/app/src/main/java/org/oxycblt/auxio/settings/tabs/TabAdapter.kt b/app/src/main/java/org/oxycblt/auxio/settings/tabs/TabAdapter.kt new file mode 100644 index 000000000..8ac9095b2 --- /dev/null +++ b/app/src/main/java/org/oxycblt/auxio/settings/tabs/TabAdapter.kt @@ -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 . + */ + +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, + private val onTabSwitch: (Tab) -> Unit, +) : RecyclerView.Adapter() { + private val tabs: Array 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 + } + } + } +} diff --git a/app/src/main/java/org/oxycblt/auxio/settings/tabs/TabCustomizeDialog.kt b/app/src/main/java/org/oxycblt/auxio/settings/tabs/TabCustomizeDialog.kt new file mode 100644 index 000000000..0a1b462b2 --- /dev/null +++ b/app/src/main/java/org/oxycblt/auxio/settings/tabs/TabCustomizeDialog.kt @@ -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 . + */ + +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().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" + } +} diff --git a/app/src/main/java/org/oxycblt/auxio/settings/tabs/TabDragCallback.kt b/app/src/main/java/org/oxycblt/auxio/settings/tabs/TabDragCallback.kt new file mode 100644 index 000000000..fce3627f3 --- /dev/null +++ b/app/src/main/java/org/oxycblt/auxio/settings/tabs/TabDragCallback.kt @@ -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 . + */ + +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) : ItemTouchHelper.Callback() { + private val tabs: Array 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 Array.swap(from: Int, to: Int) { + val t = get(to) + val f = get(from) + + set(from, t) + set(to, f) + } +} diff --git a/app/src/main/java/org/oxycblt/auxio/ui/DisplayMode.kt b/app/src/main/java/org/oxycblt/auxio/ui/DisplayMode.kt index b10a0f039..8238b968c 100644 --- a/app/src/main/java/org/oxycblt/auxio/ui/DisplayMode.kt +++ b/app/src/main/java/org/oxycblt/auxio/ui/DisplayMode.kt @@ -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 diff --git a/app/src/main/java/org/oxycblt/auxio/widgets/WidgetProvider.kt b/app/src/main/java/org/oxycblt/auxio/widgets/WidgetProvider.kt index 47a459623..afe49cd5e 100644 --- a/app/src/main/java/org/oxycblt/auxio/widgets/WidgetProvider.kt +++ b/app/src/main/java/org/oxycblt/auxio/widgets/WidgetProvider.kt @@ -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") diff --git a/app/src/main/res/color/overlay_disabled.xml b/app/src/main/res/color/overlay_disabled.xml deleted file mode 100644 index 5e67a4dd7..000000000 --- a/app/src/main/res/color/overlay_disabled.xml +++ /dev/null @@ -1,5 +0,0 @@ - - - - - \ No newline at end of file diff --git a/app/src/main/res/color/sel_accented.xml b/app/src/main/res/color/sel_accented.xml index 8f06bce61..9ade5ce76 100644 --- a/app/src/main/res/color/sel_accented.xml +++ b/app/src/main/res/color/sel_accented.xml @@ -1,5 +1,6 @@ + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_queue.xml b/app/src/main/res/drawable/ic_queue.xml index bba0ecc39..fe3d42c2b 100644 --- a/app/src/main/res/drawable/ic_queue.xml +++ b/app/src/main/res/drawable/ic_queue.xml @@ -2,7 +2,7 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_search.xml b/app/src/main/res/layout/fragment_search.xml index 8c98df0a3..77407d8b5 100644 --- a/app/src/main/res/layout/fragment_search.xml +++ b/app/src/main/res/layout/fragment_search.xml @@ -15,7 +15,7 @@ app:liftOnScroll="true" app:liftOnScrollTargetViewId="@id/search_recycler"> - @@ -43,7 +43,7 @@ - + diff --git a/app/src/main/res/layout/item_queue_song.xml b/app/src/main/res/layout/item_queue_song.xml index 8ecfda84c..b84eca75e 100644 --- a/app/src/main/res/layout/item_queue_song.xml +++ b/app/src/main/res/layout/item_queue_song.xml @@ -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" /> diff --git a/app/src/main/res/layout/item_tab.xml b/app/src/main/res/layout/item_tab.xml new file mode 100644 index 000000000..600b003bd --- /dev/null +++ b/app/src/main/res/layout/item_tab.xml @@ -0,0 +1,57 @@ + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/widget_default.xml b/app/src/main/res/layout/widget_default.xml index 7a94fbc9d..998d8f61b 100644 --- a/app/src/main/res/layout/widget_default.xml +++ b/app/src/main/res/layout/widget_default.xml @@ -8,7 +8,7 @@ android:theme="@style/Theme.Widget" tools:ignore="Overdraw"> - - - - + - - - - - - - - - + - + diff --git a/app/src/main/res/layout/widget_small.xml b/app/src/main/res/layout/widget_small.xml index 89f244e01..1a33476eb 100644 --- a/app/src/main/res/layout/widget_small.xml +++ b/app/src/main/res/layout/widget_small.xml @@ -7,7 +7,7 @@ android:background="?attr/colorSurface" android:theme="@style/Theme.Widget"> - - + - - - - - - - + - + diff --git a/app/src/main/res/values-night-v31/styles_core.xml b/app/src/main/res/values-night-v31/styles_core.xml index a38b2b700..32e82b793 100644 --- a/app/src/main/res/values-night-v31/styles_core.xml +++ b/app/src/main/res/values-night-v31/styles_core.xml @@ -53,8 +53,6 @@ @color/m3_dynamic_primary_text_disable_only @color/m3_dynamic_dark_hint_foreground @color/m3_dynamic_hint_foreground - @color/m3_dynamic_dark_highlighted_text - @color/m3_dynamic_highlighted_text @color/m3_dynamic_dark_default_color_primary_text \ No newline at end of file diff --git a/app/src/main/res/values-v31/bools.xml b/app/src/main/res/values-v31/bools.xml new file mode 100644 index 000000000..4bd8a8884 --- /dev/null +++ b/app/src/main/res/values-v31/bools.xml @@ -0,0 +1,4 @@ + + + false + \ No newline at end of file diff --git a/app/src/main/res/values-v31/styles_core.xml b/app/src/main/res/values-v31/styles_core.xml index 3b0c87750..cc365c34b 100644 --- a/app/src/main/res/values-v31/styles_core.xml +++ b/app/src/main/res/values-v31/styles_core.xml @@ -53,8 +53,6 @@ @color/m3_dynamic_dark_primary_text_disable_only @color/m3_dynamic_hint_foreground @color/m3_dynamic_dark_hint_foreground - @color/m3_dynamic_highlighted_text - @color/m3_dynamic_dark_highlighted_text @color/m3_dynamic_default_color_primary_text diff --git a/app/src/main/res/values/bools.xml b/app/src/main/res/values/bools.xml new file mode 100644 index 000000000..3198b801d --- /dev/null +++ b/app/src/main/res/values/bools.xml @@ -0,0 +1,4 @@ + + + true + \ No newline at end of file diff --git a/app/src/main/res/values/dimens.xml b/app/src/main/res/values/dimens.xml index f5a6960e5..1cebb1202 100644 --- a/app/src/main/res/values/dimens.xml +++ b/app/src/main/res/values/dimens.xml @@ -10,6 +10,7 @@ 48dp + 56dp 64dp 48dp diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index ea94169e0..3359c3586 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -70,6 +70,8 @@ Use a pure-black dark theme Display + Library tabs + Change visibility and order of library tabs Show album covers Turn off to save memory usage Ignore MediaStore covers @@ -120,7 +122,8 @@ Clear queue Remove this queue item - Move queue song + Move this queue song + Move this tab Clear search query Remove excluded directory @@ -162,6 +165,7 @@ Next From: %s Songs loaded: %d + %d Song %d Songs diff --git a/app/src/main/res/values/styles_android.xml b/app/src/main/res/values/styles_android.xml index fc036cfb8..a9d414753 100644 --- a/app/src/main/res/values/styles_android.xml +++ b/app/src/main/res/values/styles_android.xml @@ -31,7 +31,6 @@ wrap_content wrap_content @font/inter_semibold - ?attr/colorPrimary diff --git a/app/src/main/res/values/styles_core.xml b/app/src/main/res/values/styles_core.xml index 7fa19105c..c462b1ae0 100644 --- a/app/src/main/res/values/styles_core.xml +++ b/app/src/main/res/values/styles_core.xml @@ -18,16 +18,16 @@ ?attr/colorOnPrimaryContainer @color/surface - ?attr/colorPrimary @color/control - @color/overlay_selection - ?attr/colorPrimary diff --git a/app/src/main/res/xml-v31/prefs_main.xml b/app/src/main/res/xml-v31/prefs_main.xml deleted file mode 100644 index a06a0a624..000000000 --- a/app/src/main/res/xml-v31/prefs_main.xml +++ /dev/null @@ -1,109 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/xml/prefs_main.xml b/app/src/main/res/xml/prefs_main.xml index b1949aeee..381ca0ac6 100644 --- a/app/src/main/res/xml/prefs_main.xml +++ b/app/src/main/res/xml/prefs_main.xml @@ -2,9 +2,10 @@ - + + -