diff --git a/app/src/main/java/org/oxycblt/auxio/MainFragment.kt b/app/src/main/java/org/oxycblt/auxio/MainFragment.kt index b70ffb4ec..e4f5397f8 100644 --- a/app/src/main/java/org/oxycblt/auxio/MainFragment.kt +++ b/app/src/main/java/org/oxycblt/auxio/MainFragment.kt @@ -78,6 +78,8 @@ class MainFragment : Fragment(), PlaybackLayout.ActionCallback { // --- VIEWMODEL SETUP --- + // We have to control the bar view from here since using a Fragment in PlaybackLayout + // would result in annoying UI issues. binding.playbackLayout.setActionCallback(this) binding.playbackLayout.setSong(playbackModel.song.value) diff --git a/app/src/main/java/org/oxycblt/auxio/detail/DetailAppBarLayout.kt b/app/src/main/java/org/oxycblt/auxio/detail/DetailAppBarLayout.kt index 6d9d779ff..516eff0d6 100644 --- a/app/src/main/java/org/oxycblt/auxio/detail/DetailAppBarLayout.kt +++ b/app/src/main/java/org/oxycblt/auxio/detail/DetailAppBarLayout.kt @@ -48,6 +48,7 @@ class DetailAppBarLayout @JvmOverloads constructor( val toolbar = findViewById(R.id.detail_toolbar) + // Reflect to get the actual title view to do transformations on val newTitleView = Toolbar::class.java.getDeclaredField("mTitleTextView").run { isAccessible = true get(toolbar) as AppCompatTextView @@ -66,7 +67,7 @@ class DetailAppBarLayout @JvmOverloads constructor( return recycler } - val newRecycler = (parent as ViewGroup).findViewById(R.id.detail_recycler) + val newRecycler = (parent as ViewGroup).findViewById(liftOnScrollTargetViewId) mRecycler = newRecycler return newRecycler diff --git a/app/src/main/java/org/oxycblt/auxio/detail/DetailViewModel.kt b/app/src/main/java/org/oxycblt/auxio/detail/DetailViewModel.kt index 8f8a9b05b..11d4aef22 100644 --- a/app/src/main/java/org/oxycblt/auxio/detail/DetailViewModel.kt +++ b/app/src/main/java/org/oxycblt/auxio/detail/DetailViewModel.kt @@ -70,6 +70,7 @@ class DetailViewModel : ViewModel() { val showMenu: LiveData = mShowMenu private val mNavToItem = MutableLiveData() + /** Flag for unified navigation. Observe this to coordinate navigation to an item's UI. */ val navToItem: LiveData get() = mNavToItem diff --git a/app/src/main/java/org/oxycblt/auxio/home/FloatingActionButtonContainer.kt b/app/src/main/java/org/oxycblt/auxio/home/FloatingActionButtonContainer.kt index 490e69b16..b73a2b371 100644 --- a/app/src/main/java/org/oxycblt/auxio/home/FloatingActionButtonContainer.kt +++ b/app/src/main/java/org/oxycblt/auxio/home/FloatingActionButtonContainer.kt @@ -25,6 +25,10 @@ import android.widget.FrameLayout import androidx.core.view.updatePadding import org.oxycblt.auxio.util.systemBarsCompat +/** + * A container for a FloatingActionButton that enables edge-to-edge support. + * @author OxygenCobalt + */ class FloatingActionButtonContainer @JvmOverloads constructor( context: Context, attrs: AttributeSet? = null, 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 8363f16a2..af893c993 100644 --- a/app/src/main/java/org/oxycblt/auxio/home/HomeFragment.kt +++ b/app/src/main/java/org/oxycblt/auxio/home/HomeFragment.kt @@ -18,11 +18,15 @@ package org.oxycblt.auxio.home +import android.graphics.LinearGradient +import android.graphics.Shader import android.os.Bundle import android.view.LayoutInflater import android.view.MenuItem import android.view.View import android.view.ViewGroup +import androidx.appcompat.widget.AppCompatTextView +import androidx.appcompat.widget.Toolbar import androidx.core.view.iterator import androidx.fragment.app.Fragment import androidx.fragment.app.activityViewModels @@ -49,10 +53,11 @@ import org.oxycblt.auxio.playback.PlaybackViewModel import org.oxycblt.auxio.ui.DisplayMode import org.oxycblt.auxio.util.logD import org.oxycblt.auxio.util.logE +import org.oxycblt.auxio.util.resolveAttr /** * The main "Launching Point" fragment of Auxio, allowing navigation to the detail - * views for each respective fragment. + * views for each respective item. * @author OxygenCobalt */ class HomeFragment : Fragment() { @@ -96,6 +101,7 @@ class HomeFragment : Fragment() { R.id.option_sort_asc -> { item.isChecked = !item.isChecked + val new = homeModel.getSortForDisplay(homeModel.curTab.value!!) .ascending(item.isChecked) @@ -117,6 +123,21 @@ class HomeFragment : Fragment() { } sortItem = menu.findItem(R.id.submenu_sorting) + + // Apply a nice gradient to the toolbar title view. + val titleView = Toolbar::class.java.getDeclaredField("mTitleTextView").run { + isAccessible = true + get(this@apply) as AppCompatTextView + } + + titleView.paint.shader = LinearGradient( + 0f, 0f, titleView.paint.measureText(titleView.text.toString()), titleView.textSize, + intArrayOf( + R.attr.colorPrimary.resolveAttr(context), + R.attr.colorSecondary.resolveAttr(context) + ), + null, Shader.TileMode.CLAMP + ) } binding.homePager.apply { 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 bbaa9b0c1..d97eb1330 100644 --- a/app/src/main/java/org/oxycblt/auxio/home/HomeViewModel.kt +++ b/app/src/main/java/org/oxycblt/auxio/home/HomeViewModel.kt @@ -21,6 +21,8 @@ package org.oxycblt.auxio.home import androidx.lifecycle.LiveData import androidx.lifecycle.MutableLiveData import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import kotlinx.coroutines.launch import org.oxycblt.auxio.music.Album import org.oxycblt.auxio.music.Artist import org.oxycblt.auxio.music.Genre @@ -35,7 +37,7 @@ import org.oxycblt.auxio.ui.Sort * The ViewModel for managing [HomeFragment]'s data, sorting modes, and tab state. * @author OxygenCobalt */ -class HomeViewModel : ViewModel(), SettingsManager.Callback, MusicStore.MusicCallback { +class HomeViewModel : ViewModel(), SettingsManager.Callback { private val settingsManager = SettingsManager.getInstance() private val mSongs = MutableLiveData(listOf()) @@ -73,7 +75,15 @@ class HomeViewModel : ViewModel(), SettingsManager.Callback, MusicStore.MusicCal init { settingsManager.addCallback(this) - MusicStore.awaitInstance(this) + + viewModelScope.launch { + val musicStore = MusicStore.awaitInstance() + + mSongs.value = settingsManager.libSongSort.sortSongs(musicStore.songs) + mAlbums.value = settingsManager.libAlbumSort.sortAlbums(musicStore.albums) + mArtists.value = settingsManager.libArtistSort.sortParents(musicStore.artists) + mGenres.value = settingsManager.libGenreSort.sortParents(musicStore.genres) + } } /** @@ -110,14 +120,17 @@ class HomeViewModel : ViewModel(), SettingsManager.Callback, MusicStore.MusicCal settingsManager.libAlbumSort = sort mAlbums.value = sort.sortAlbums(mAlbums.value!!) } + DisplayMode.SHOW_ARTISTS -> { settingsManager.libArtistSort = sort mArtists.value = sort.sortParents(mArtists.value!!) } + DisplayMode.SHOW_GENRES -> { settingsManager.libGenreSort = sort mGenres.value = sort.sortParents(mGenres.value!!) } + else -> {} } } @@ -137,16 +150,8 @@ class HomeViewModel : ViewModel(), SettingsManager.Callback, MusicStore.MusicCal mRecreateTabs.value = true } - override fun onLoaded(musicStore: MusicStore) { - mSongs.value = settingsManager.libSongSort.sortSongs(musicStore.songs) - mAlbums.value = settingsManager.libAlbumSort.sortAlbums(musicStore.albums) - mArtists.value = settingsManager.libArtistSort.sortParents(musicStore.artists) - mGenres.value = settingsManager.libGenreSort.sortParents(musicStore.genres) - } - override fun onCleared() { super.onCleared() settingsManager.removeCallback(this) - MusicStore.cancelAwaitInstance(this) } } diff --git a/app/src/main/java/org/oxycblt/auxio/home/fastscroll/FastScrollPopupDrawable.kt b/app/src/main/java/org/oxycblt/auxio/home/fastscroll/FastScrollPopupDrawable.kt index e784e677e..a15b6cc65 100644 --- a/app/src/main/java/org/oxycblt/auxio/home/fastscroll/FastScrollPopupDrawable.kt +++ b/app/src/main/java/org/oxycblt/auxio/home/fastscroll/FastScrollPopupDrawable.kt @@ -48,6 +48,8 @@ import kotlin.math.sqrt * - Variable names are no longer prefixed with m * - Made path management compat-friendly * - Converted to kotlin + * + * @author Hai Zhang, OxygenCobalt */ class FastScrollPopupDrawable(context: Context) : Drawable() { private val paint: Paint = Paint().apply { @@ -116,7 +118,7 @@ class FastScrollPopupDrawable(context: Context) : Drawable() { val r = height / 2 val sqrt2 = sqrt(2.0).toFloat() - // Ensure we are convex. + // Ensure we are convex width = (r + sqrt2 * r).coerceAtLeast(width) pathArcTo(path, r, r, r, 90f, 180f) diff --git a/app/src/main/java/org/oxycblt/auxio/home/fastscroll/FastScrollRecyclerView.kt b/app/src/main/java/org/oxycblt/auxio/home/fastscroll/FastScrollRecyclerView.kt index 8041f71bd..3c9eaa6d6 100644 --- a/app/src/main/java/org/oxycblt/auxio/home/fastscroll/FastScrollRecyclerView.kt +++ b/app/src/main/java/org/oxycblt/auxio/home/fastscroll/FastScrollRecyclerView.kt @@ -34,6 +34,7 @@ import android.widget.FrameLayout import android.widget.TextView import androidx.appcompat.widget.AppCompatTextView import androidx.core.math.MathUtils +import androidx.core.view.isInvisible import androidx.core.widget.TextViewCompat import androidx.recyclerview.widget.GridLayoutManager import androidx.recyclerview.widget.LinearLayoutManager @@ -66,33 +67,44 @@ import kotlin.math.abs * - Redundant functions have been merged * - Variable names are no longer prefixed with m * - Added drag listener - * - TODO: Added documentation + * - Added documentation + * + * @author Hai Zhang, OxygenCobalt */ class FastScrollRecyclerView @JvmOverloads constructor( context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = -1 ) : RecyclerView(context, attrs, defStyleAttr) { + /** Callback to provide a string to be shown on the popup when an item is passed */ var popupProvider: ((Int) -> String)? = null + + /** + * A listener for when a drag event occurs. The value will be true if a drag has begun, + * and false if a drag ended. + */ var onDragListener: ((Boolean) -> Unit)? = null private val minTouchTargetSize: Int = resources.getDimensionPixelSize(R.dimen.size_btn_small) private val touchSlop: Int = ViewConfiguration.get(context).scaledTouchSlop + // Views for the track, thumb, and popup. Note that the track view is mostly vestigal + // and is only for bounds checking. private val trackView: View private val thumbView: View private val popupView: TextView + // Touch values private val thumbWidth: Int private val thumbHeight: Int private var thumbOffset = 0 - private var downX = 0f private var downY = 0f private var lastY = 0f private var dragStartY = 0f private var dragStartThumbOffset = 0 + // State private var dragging = false private var showingScrollbar = false private var showingPopup = false @@ -100,11 +112,9 @@ class FastScrollRecyclerView @JvmOverloads constructor( private val childRect = Rect() private val hideScrollbarRunnable = Runnable { - if (dragging) { - return@Runnable + if (!dragging) { + hideScrollbar() } - - hideScrollbar() } private val initialPadding = Rect(paddingLeft, paddingTop, paddingRight, paddingBottom) @@ -174,19 +184,19 @@ class FastScrollRecyclerView @JvmOverloads constructor( // We use a listener instead of overriding onTouchEvent so that we don't conflict with // RecyclerView touch events. addOnItemTouchListener(object : SimpleOnItemTouchListener() { - override fun onInterceptTouchEvent( - recyclerView: RecyclerView, - event: MotionEvent - ): Boolean { - return onItemTouch(event) - } - override fun onTouchEvent( recyclerView: RecyclerView, event: MotionEvent ) { onItemTouch(event) } + + override fun onInterceptTouchEvent( + recyclerView: RecyclerView, + event: MotionEvent + ): Boolean { + return onItemTouch(event) + } }) } @@ -227,10 +237,9 @@ class FastScrollRecyclerView @JvmOverloads constructor( "" } - val hasPopup = !TextUtils.isEmpty(popupText) - popupView.visibility = if (hasPopup) View.VISIBLE else View.INVISIBLE + popupView.isInvisible = popupText.isEmpty() - if (hasPopup) { + if (popupText.isNotEmpty()) { val popupLayoutParams = popupView.layoutParams as FrameLayout.LayoutParams if (popupView.text != popupText) { @@ -343,9 +352,8 @@ class FastScrollRecyclerView @JvmOverloads constructor( downY = eventY val scrollX = trackView.scrollX - val isInScrollbar = ( + val isInScrollbar = eventX >= thumbView.left - scrollX && eventX < thumbView.right - scrollX - ) if (trackView.alpha > 0 && isInScrollbar) { dragStartY = eventY @@ -384,6 +392,7 @@ class FastScrollRecyclerView @JvmOverloads constructor( MotionEvent.ACTION_UP, MotionEvent.ACTION_CANCEL -> setDragging(false) } + lastY = eventY return dragging } @@ -462,21 +471,21 @@ class FastScrollRecyclerView @JvmOverloads constructor( } } - private fun setDragging(dragging: Boolean) { - if (this.dragging == dragging) { + private fun setDragging(isDragging: Boolean) { + if (dragging == isDragging) { return } - this.dragging = dragging + dragging = isDragging - if (this.dragging) { + if (dragging) { parent.requestDisallowInterceptTouchEvent(true) } - trackView.isPressed = this.dragging - thumbView.isPressed = this.dragging + trackView.isPressed = dragging + thumbView.isPressed = dragging - if (this.dragging) { + if (dragging) { removeCallbacks(hideScrollbarRunnable) showScrollbar() showPopup() @@ -485,7 +494,7 @@ class FastScrollRecyclerView @JvmOverloads constructor( hidePopup() } - onDragListener?.invoke(dragging) + onDragListener?.invoke(isDragging) } // --- SCROLLBAR APPEARANCE --- diff --git a/app/src/main/java/org/oxycblt/auxio/home/list/AlbumListFragment.kt b/app/src/main/java/org/oxycblt/auxio/home/list/AlbumListFragment.kt index 894878096..9e7a31d32 100644 --- a/app/src/main/java/org/oxycblt/auxio/home/list/AlbumListFragment.kt +++ b/app/src/main/java/org/oxycblt/auxio/home/list/AlbumListFragment.kt @@ -32,6 +32,10 @@ import org.oxycblt.auxio.ui.Sort import org.oxycblt.auxio.ui.newMenu import org.oxycblt.auxio.ui.sliceArticle +/** + * A [HomeListFragment] for showing a list of [Album]s. + * @author + */ class AlbumListFragment : HomeListFragment() { override fun onCreateView( inflater: LayoutInflater, @@ -54,19 +58,24 @@ class AlbumListFragment : HomeListFragment() { return binding.root } - override val popupProvider: (Int) -> String + override val listPopupProvider: (Int) -> String get() = { idx -> val album = homeModel.albums.value!![idx] + // Change how we display the popup depending on the mode. when (homeModel.getSortForDisplay(DisplayMode.SHOW_ALBUMS)) { + // By Name -> Use Name is Sort.ByName -> album.name.sliceArticle() .first().uppercase() + // By Artist -> Use Artist Name is Sort.ByArtist -> album.artist.resolvedName.sliceArticle() .first().uppercase() + // Year -> Use Full Year is Sort.ByYear -> album.year.toString() + // Unsupported sort, error gracefully else -> "" } } diff --git a/app/src/main/java/org/oxycblt/auxio/home/list/ArtistListFragment.kt b/app/src/main/java/org/oxycblt/auxio/home/list/ArtistListFragment.kt index 7b8622a54..b9a26711e 100644 --- a/app/src/main/java/org/oxycblt/auxio/home/list/ArtistListFragment.kt +++ b/app/src/main/java/org/oxycblt/auxio/home/list/ArtistListFragment.kt @@ -30,6 +30,10 @@ import org.oxycblt.auxio.ui.ArtistViewHolder import org.oxycblt.auxio.ui.newMenu import org.oxycblt.auxio.ui.sliceArticle +/** + * A [HomeListFragment] for showing a list of [Artist]s. + * @author + */ class ArtistListFragment : HomeListFragment() { override fun onCreateView( inflater: LayoutInflater, @@ -52,7 +56,7 @@ class ArtistListFragment : HomeListFragment() { return binding.root } - override val popupProvider: (Int) -> String + override val listPopupProvider: (Int) -> String get() = { idx -> homeModel.artists.value!![idx].resolvedName .sliceArticle().first().uppercase() diff --git a/app/src/main/java/org/oxycblt/auxio/home/list/GenreListFragment.kt b/app/src/main/java/org/oxycblt/auxio/home/list/GenreListFragment.kt index 647f91502..bfd44685d 100644 --- a/app/src/main/java/org/oxycblt/auxio/home/list/GenreListFragment.kt +++ b/app/src/main/java/org/oxycblt/auxio/home/list/GenreListFragment.kt @@ -30,6 +30,10 @@ import org.oxycblt.auxio.ui.GenreViewHolder import org.oxycblt.auxio.ui.newMenu import org.oxycblt.auxio.ui.sliceArticle +/** + * A [HomeListFragment] for showing a list of [Genre]s. + * @author + */ class GenreListFragment : HomeListFragment() { override fun onCreateView( inflater: LayoutInflater, @@ -52,7 +56,7 @@ class GenreListFragment : HomeListFragment() { return binding.root } - override val popupProvider: (Int) -> String + override val listPopupProvider: (Int) -> String get() = { idx -> homeModel.genres.value!![idx].resolvedName .sliceArticle().first().uppercase() 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 0bd5de2ad..8b5f8ecc3 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 @@ -19,8 +19,6 @@ package org.oxycblt.auxio.home.list import android.annotation.SuppressLint -import android.os.Bundle -import android.view.View import androidx.annotation.IdRes import androidx.fragment.app.Fragment import androidx.fragment.app.activityViewModels @@ -34,8 +32,8 @@ import org.oxycblt.auxio.ui.memberBinding import org.oxycblt.auxio.util.applySpans /** - * A Base [Fragment] implementing the base features shared across all detail fragments. - * + * A Base [Fragment] implementing the base features shared across all list fragments in the home UI. + * @author OxygenCobalt */ abstract class HomeListFragment : Fragment() { protected val binding: FragmentHomeListBinding by memberBinding( @@ -45,16 +43,10 @@ abstract class HomeListFragment : Fragment() { protected val homeModel: HomeViewModel by activityViewModels() protected val playbackModel: PlaybackViewModel by activityViewModels() - abstract val popupProvider: (Int) -> String - - override fun onViewCreated(view: View, savedInstanceState: Bundle?) { - super.onViewCreated(view, savedInstanceState) - - binding.homeRecycler.popupProvider = popupProvider - binding.homeRecycler.onDragListener = { dragging -> - homeModel.updateFastScrolling(dragging) - } - } + /** + * The popup provider to use for the fast scroller view. + */ + abstract val listPopupProvider: (Int) -> String protected fun setupRecycler( @IdRes uniqueId: Int, @@ -66,6 +58,11 @@ abstract class HomeListFragment : Fragment() { adapter = homeAdapter setHasFixedSize(true) applySpans() + + popupProvider = listPopupProvider + onDragListener = { dragging -> + homeModel.updateFastScrolling(dragging) + } } // Make sure that this RecyclerView has data before startup diff --git a/app/src/main/java/org/oxycblt/auxio/home/list/SongListFragment.kt b/app/src/main/java/org/oxycblt/auxio/home/list/SongListFragment.kt index 8043292b8..c8726cda4 100644 --- a/app/src/main/java/org/oxycblt/auxio/home/list/SongListFragment.kt +++ b/app/src/main/java/org/oxycblt/auxio/home/list/SongListFragment.kt @@ -30,6 +30,10 @@ import org.oxycblt.auxio.ui.Sort import org.oxycblt.auxio.ui.newMenu import org.oxycblt.auxio.ui.sliceArticle +/** + * A [HomeListFragment] for showing a list of [Song]s. + * @author + */ class SongListFragment : HomeListFragment() { override fun onCreateView( inflater: LayoutInflater, @@ -50,21 +54,26 @@ class SongListFragment : HomeListFragment() { return binding.root } - override val popupProvider: (Int) -> String + override val listPopupProvider: (Int) -> String get() = { idx -> val song = homeModel.songs.value!![idx] + // Change how we display the popup depending on the mode. when (homeModel.getSortForDisplay(DisplayMode.SHOW_SONGS)) { + // Name -> Use name is Sort.ByName -> song.name.sliceArticle() .first().uppercase() + // Artist -> Use Artist Name is Sort.ByArtist -> song.album.artist.resolvedName .sliceArticle().first().uppercase() + // Album -> Use Album Name is Sort.ByAlbum -> song.album.name.sliceArticle() .first().uppercase() + // Year -> Use Full Year is Sort.ByYear -> song.album.year.toString() } } diff --git a/app/src/main/java/org/oxycblt/auxio/music/MusicStore.kt b/app/src/main/java/org/oxycblt/auxio/music/MusicStore.kt index 8f417035f..e7cfd3536 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/MusicStore.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/MusicStore.kt @@ -131,14 +131,9 @@ class MusicStore private constructor() { NO_PERMS, NO_MUSIC, FAILED } - interface MusicCallback { - fun onLoaded(musicStore: MusicStore) - } - companion object { @Volatile private var RESPONSE: Response? = null - private val AWAITING = mutableListOf() /** * Initialize the loading process for this instance. This must be ran on a background @@ -162,37 +157,28 @@ class MusicStore private constructor() { response } - if (response is Response.Ok) { - AWAITING.forEach { it.onLoaded(response.musicStore) } - AWAITING.clear() - } - return response } /** - * Await the successful creation of a [MusicStore] instance. The [callback] - * will be called if the instance is already loaded. It's recommended to call - * [cancelAwaitInstance] if the object is about to be destroyed to prevent any - * memory leaks. + * Await the successful creation of a [MusicStore] instance. The co-routine calling + * this will block until the successful creation of a [MusicStore], in which it will + * then be returned. */ - fun awaitInstance(callback: MusicCallback) { - // FIXME: There has to be some coroutiney way to do this instead of just making - // a leak-prone callback system - val currentInstance = maybeGetInstance() + suspend fun awaitInstance() = withContext(Dispatchers.Default) { + // We have to do a withContext call so we don't block the JVM thread + val musicStore: MusicStore - if (currentInstance != null) { - callback.onLoaded(currentInstance) + while (true) { + val response = RESPONSE + + if (response is Response.Ok) { + musicStore = response.musicStore + break + } } - AWAITING.add(callback) - } - - /** - * Remove a callback from the queue. - */ - fun cancelAwaitInstance(callback: MusicCallback) { - AWAITING.remove(callback) + musicStore } /** diff --git a/app/src/main/java/org/oxycblt/auxio/playback/PlaybackLayout.kt b/app/src/main/java/org/oxycblt/auxio/playback/PlaybackLayout.kt index 7b4bc929d..efbb40868 100644 --- a/app/src/main/java/org/oxycblt/auxio/playback/PlaybackLayout.kt +++ b/app/src/main/java/org/oxycblt/auxio/playback/PlaybackLayout.kt @@ -43,6 +43,8 @@ import kotlin.math.min * of state and view magic. I tried my best to document it, but it's probably not the most friendly * or extendable. You have been warned. * + * TODO: Add the queue view into this layout. + * * @author OxygenCobalt (With help from Umano and Hai Zhang) */ class PlaybackLayout @JvmOverloads constructor( diff --git a/app/src/main/java/org/oxycblt/auxio/playback/queue/QueueAdapter.kt b/app/src/main/java/org/oxycblt/auxio/playback/queue/QueueAdapter.kt index 87c415986..4f65d4cc6 100644 --- a/app/src/main/java/org/oxycblt/auxio/playback/queue/QueueAdapter.kt +++ b/app/src/main/java/org/oxycblt/auxio/playback/queue/QueueAdapter.kt @@ -19,6 +19,8 @@ package org.oxycblt.auxio.playback.queue import android.annotation.SuppressLint +import android.content.res.ColorStateList +import android.graphics.drawable.ColorDrawable import android.view.MotionEvent import android.view.View import android.view.ViewGroup @@ -26,6 +28,7 @@ import androidx.core.view.isInvisible import androidx.recyclerview.widget.AsyncListDiffer import androidx.recyclerview.widget.ItemTouchHelper import androidx.recyclerview.widget.RecyclerView +import com.google.android.material.shape.MaterialShapeDrawable import org.oxycblt.auxio.databinding.ItemQueueSongBinding import org.oxycblt.auxio.music.ActionHeader import org.oxycblt.auxio.music.BaseModel @@ -35,7 +38,6 @@ import org.oxycblt.auxio.ui.ActionHeaderViewHolder import org.oxycblt.auxio.ui.BaseViewHolder import org.oxycblt.auxio.ui.DiffCallback import org.oxycblt.auxio.ui.HeaderViewHolder -import org.oxycblt.auxio.util.applyMaterialDrawable import org.oxycblt.auxio.util.inflater import org.oxycblt.auxio.util.logD import org.oxycblt.auxio.util.logE @@ -151,7 +153,11 @@ class QueueAdapter( val backgroundView: View get() = binding.background init { - binding.body.applyMaterialDrawable() + binding.body.background = MaterialShapeDrawable.createWithElevationOverlay( + binding.root.context + ).apply { + fillColor = ColorStateList.valueOf((binding.body.background as ColorDrawable).color) + } } @SuppressLint("ClickableViewAccessibility") diff --git a/app/src/main/java/org/oxycblt/auxio/playback/state/PlaybackStateDatabase.kt b/app/src/main/java/org/oxycblt/auxio/playback/state/PlaybackStateDatabase.kt index a1f7ca0b3..9446fd9b9 100644 --- a/app/src/main/java/org/oxycblt/auxio/playback/state/PlaybackStateDatabase.kt +++ b/app/src/main/java/org/oxycblt/auxio/playback/state/PlaybackStateDatabase.kt @@ -34,7 +34,6 @@ import org.oxycblt.auxio.util.queryAll /** * A SQLite database for managing the persistent playback state and queue. * Yes. I know Room exists. But that would needlessly bloat my app and has crippling bugs. - * LEFT-OFF: Improve hashing by making everything a long * @author OxygenCobalt */ class PlaybackStateDatabase(context: Context) : diff --git a/app/src/main/java/org/oxycblt/auxio/search/SearchViewModel.kt b/app/src/main/java/org/oxycblt/auxio/search/SearchViewModel.kt index d138497a5..ba55d8b9a 100644 --- a/app/src/main/java/org/oxycblt/auxio/search/SearchViewModel.kt +++ b/app/src/main/java/org/oxycblt/auxio/search/SearchViewModel.kt @@ -29,6 +29,7 @@ import org.oxycblt.auxio.music.BaseModel import org.oxycblt.auxio.music.Header import org.oxycblt.auxio.music.HeaderString import org.oxycblt.auxio.music.Music +import org.oxycblt.auxio.music.MusicParent import org.oxycblt.auxio.music.MusicStore import org.oxycblt.auxio.settings.SettingsManager import org.oxycblt.auxio.ui.DisplayMode @@ -38,7 +39,7 @@ import java.text.Normalizer * The [ViewModel] for the search functionality * @author OxygenCobalt */ -class SearchViewModel : ViewModel(), MusicStore.MusicCallback { +class SearchViewModel : ViewModel() { private val mSearchResults = MutableLiveData(listOf()) private var mIsNavigating = false private var mFilterMode: DisplayMode? = null @@ -54,7 +55,10 @@ class SearchViewModel : ViewModel(), MusicStore.MusicCallback { init { mFilterMode = settingsManager.searchFilterMode - MusicStore.awaitInstance(this) + viewModelScope.launch { + MusicStore.awaitInstance() + search(mLastQuery) + } } /** @@ -71,6 +75,7 @@ class SearchViewModel : ViewModel(), MusicStore.MusicCallback { return } + // Searching can be quite expensive, so hop on a co-routine viewModelScope.launch { val results = mutableListOf() @@ -133,11 +138,18 @@ class SearchViewModel : ViewModel(), MusicStore.MusicCallback { */ private fun List.filterByOrNull(value: String): List? { val filtered = filter { + // Ensure the name we match with is correct. + val name = if (it is MusicParent) { + it.resolvedName + } else { + it.name + } + // First see if the normal item name will work. If that fails, try the "normalized" // [e.g all accented/unicode chars become latin chars] instead. Hopefully this // shouldn't break other language's search functionality. - it.name.contains(value, ignoreCase = true) || - it.name.normalized().contains(value, ignoreCase = true) + name.contains(value, ignoreCase = true) || + name.normalized().contains(value, ignoreCase = true) } return if (filtered.isNotEmpty()) filtered else null @@ -179,15 +191,4 @@ class SearchViewModel : ViewModel(), MusicStore.MusicCallback { fun setNavigating(isNavigating: Boolean) { mIsNavigating = isNavigating } - - // --- OVERRIDES --- - - override fun onLoaded(musicStore: MusicStore) { - search(mLastQuery) - } - - override fun onCleared() { - super.onCleared() - MusicStore.cancelAwaitInstance(this) - } } diff --git a/app/src/main/java/org/oxycblt/auxio/settings/SettingsCompat.kt b/app/src/main/java/org/oxycblt/auxio/settings/SettingsCompat.kt index 74aae2ca7..bccd305ca 100644 --- a/app/src/main/java/org/oxycblt/auxio/settings/SettingsCompat.kt +++ b/app/src/main/java/org/oxycblt/auxio/settings/SettingsCompat.kt @@ -28,8 +28,8 @@ import org.oxycblt.auxio.playback.state.PlaybackMode // A couple of utils for migrating from old settings values to the new // formats used in 1.3.2 & 1.4.0 -// TODO: Slate these for removal in 2.0.0. 1.4.0 was Pre-FDroid so it's extremely likely that -// everyone has migrated. +// TODO: Slate these for removal eventually. There probably isn't that many left who have the +// old values but 2.0.0 will probably convince most of those to update too. fun handleThemeCompat(prefs: SharedPreferences): Int { if (prefs.contains(OldKeys.KEY_THEME)) { 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 60403ca58..ff249f7dd 100644 --- a/app/src/main/java/org/oxycblt/auxio/settings/SettingsListFragment.kt +++ b/app/src/main/java/org/oxycblt/auxio/settings/SettingsListFragment.kt @@ -165,7 +165,7 @@ class SettingsListFragment : PreferenceFragmentCompat() { } } - SettingsManager.KEY_BLACKLIST -> { + SettingsManager.KEY_EXCLUDED -> { onPreferenceClickListener = Preference.OnPreferenceClickListener { ExcludedDialog().show(childFragmentManager, ExcludedDialog.TAG) true 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 575273d73..039cdcb0b 100644 --- a/app/src/main/java/org/oxycblt/auxio/settings/SettingsManager.kt +++ b/app/src/main/java/org/oxycblt/auxio/settings/SettingsManager.kt @@ -249,9 +249,8 @@ class SettingsManager private constructor(context: Context) : } companion object { - // Preference keys - // The old way of naming keys was to prefix them with KEY_. Now it's to prefix them with - // auxio_. + // Note: The old way of naming keys was to prefix them with KEY_. Now it's to prefix them + // with auxio_. const val KEY_THEME = "KEY_THEME2" const val KEY_BLACK_THEME = "KEY_BLACK_THEME" const val KEY_ACCENT = "auxio_accent" @@ -270,7 +269,7 @@ class SettingsManager private constructor(context: Context) : const val KEY_LOOP_PAUSE = "KEY_LOOP_PAUSE" const val KEY_SAVE_STATE = "auxio_save_state" - const val KEY_BLACKLIST = "KEY_BLACKLIST" + const val KEY_EXCLUDED = "auxio_excluded_dirs" const val KEY_SEARCH_FILTER_MODE = "KEY_SEARCH_FILTER" diff --git a/app/src/main/java/org/oxycblt/auxio/ui/ViewHolders.kt b/app/src/main/java/org/oxycblt/auxio/ui/ViewHolders.kt index 9d40c2f00..2b700a774 100644 --- a/app/src/main/java/org/oxycblt/auxio/ui/ViewHolders.kt +++ b/app/src/main/java/org/oxycblt/auxio/ui/ViewHolders.kt @@ -249,6 +249,9 @@ class HeaderViewHolder private constructor( } } +/** + * The Shared ViewHolder for an [ActionHeader]. Instantiation should be done with [from] + */ class ActionHeaderViewHolder private constructor( private val binding: ItemActionHeaderBinding ) : BaseViewHolder(binding) { diff --git a/app/src/main/java/org/oxycblt/auxio/util/ViewUtil.kt b/app/src/main/java/org/oxycblt/auxio/util/ViewUtil.kt index deffc647b..f85bc1225 100644 --- a/app/src/main/java/org/oxycblt/auxio/util/ViewUtil.kt +++ b/app/src/main/java/org/oxycblt/auxio/util/ViewUtil.kt @@ -22,10 +22,8 @@ import android.content.Context import android.content.res.ColorStateList import android.content.res.Resources import android.graphics.Rect -import android.graphics.drawable.ColorDrawable import android.os.Build import android.util.TypedValue -import android.view.View import android.view.WindowInsets import androidx.annotation.AttrRes import androidx.annotation.ColorInt @@ -34,23 +32,8 @@ import androidx.annotation.DrawableRes import androidx.core.content.ContextCompat import androidx.recyclerview.widget.GridLayoutManager import androidx.recyclerview.widget.RecyclerView -import com.google.android.material.shape.MaterialShapeDrawable import org.oxycblt.auxio.R -/** - * Apply a [MaterialShapeDrawable] to this view, automatically initializing the elevation overlay - * and setting the fill color. The [View]'s current elevation will be applied to the drawable. - * This functions assumes that the background is a [ColorDrawable] and will crash if not. - */ -fun View.applyMaterialDrawable() { - check(background is ColorDrawable) { "Background was not defined as a solid color" } - - background = MaterialShapeDrawable.createWithElevationOverlay(context).apply { - elevation = this@applyMaterialDrawable.elevation - fillColor = ColorStateList.valueOf((background as ColorDrawable).color) - } -} - /** * Apply the recommended spans for a [RecyclerView]. * diff --git a/app/src/main/java/org/oxycblt/auxio/widgets/Forms.kt b/app/src/main/java/org/oxycblt/auxio/widgets/Forms.kt index 35c0213d2..82a1b7798 100644 --- a/app/src/main/java/org/oxycblt/auxio/widgets/Forms.kt +++ b/app/src/main/java/org/oxycblt/auxio/widgets/Forms.kt @@ -27,6 +27,63 @@ import org.oxycblt.auxio.playback.system.PlaybackService import org.oxycblt.auxio.util.newBroadcastIntent import org.oxycblt.auxio.util.newMainIntent +/** + * The default widget is displayed whenever there is no music playing. It just shows the + * message "No music playing". + */ +fun createDefaultWidget(context: Context): RemoteViews { + return createViews(context, R.layout.widget_default) +} + +/** + * The tiny widget is for an edge-case situation where a 2xN widget happens to be smaller than + * 100dp. It just shows the cover, titles, and a button. + */ +fun createTinyWidget(context: Context, state: WidgetState): RemoteViews { + return createViews(context, R.layout.widget_tiny) + .applyMeta(context, state) + .applyPlayControls(context, state) +} + +/** + * The small widget is for 2x2 widgets and just shows the cover art and playback controls. + * This is generally because a Medium widget is too large for this widget size and a text-only + * widget is too small for this widget size. + */ +fun createSmallWidget(context: Context, state: WidgetState): RemoteViews { + return createViews(context, R.layout.widget_small) + .applyCover(context, state) + .applyControls(context, state) +} + +/** + * The medium widget is for 2x3 widgets and shows the cover art, title/artist, and three + * controls. This is the default widget configuration. + */ +fun createMediumWidget(context: Context, state: WidgetState): RemoteViews { + return createViews(context, R.layout.widget_medium) + .applyMeta(context, state) + .applyControls(context, state) +} + +/** + * The wide widget is for Nx2 widgets and is like the small widget but with more controls. + */ +fun createWideWidget(context: Context, state: WidgetState): RemoteViews { + return createViews(context, R.layout.widget_wide) + .applyCover(context, state) + .applyFullControls(context, state) +} + +/** + * The large widget is for 3x4 widgets and shows all metadata and controls. + */ +fun createLargeWidget(context: Context, state: WidgetState): RemoteViews { + return createViews(context, R.layout.widget_large) + .applyMeta(context, state) + .applyFullControls(context, state) +} + private fun createViews( context: Context, @LayoutRes layout: Int @@ -141,60 +198,3 @@ private fun RemoteViews.applyFullControls(context: Context, state: WidgetState): return this } - -/** - * The default widget is displayed whenever there is no music playing. It just shows the - * message "No music playing". - */ -fun createDefaultWidget(context: Context): RemoteViews { - return createViews(context, R.layout.widget_default) -} - -/** - * The tiny widget is for an edge-case situation where a 2xN widget happens to be smaller than - * 100dp. It just shows the cover, titles, and a button. - */ -fun createTinyWidget(context: Context, state: WidgetState): RemoteViews { - return createViews(context, R.layout.widget_tiny) - .applyMeta(context, state) - .applyPlayControls(context, state) -} - -/** - * The small widget is for 2x2 widgets and just shows the cover art and playback controls. - * This is generally because a Medium widget is too large for this widget size and a text-only - * widget is too small for this widget size. - */ -fun createSmallWidget(context: Context, state: WidgetState): RemoteViews { - return createViews(context, R.layout.widget_small) - .applyCover(context, state) - .applyControls(context, state) -} - -/** - * The medium widget is for 2x3 widgets and shows the cover art, title/artist, and three - * controls. This is the default widget configuration. - */ -fun createMediumWidget(context: Context, state: WidgetState): RemoteViews { - return createViews(context, R.layout.widget_medium) - .applyMeta(context, state) - .applyControls(context, state) -} - -/** - * The wide widget is for Nx2 widgets and is like the small widget but with more controls. - */ -fun createWideWidget(context: Context, state: WidgetState): RemoteViews { - return createViews(context, R.layout.widget_wide) - .applyCover(context, state) - .applyFullControls(context, state) -} - -/** - * The large widget is for 3x4 widgets and shows all metadata and controls. - */ -fun createLargeWidget(context: Context, state: WidgetState): RemoteViews { - return createViews(context, R.layout.widget_large) - .applyMeta(context, state) - .applyFullControls(context, state) -} diff --git a/app/src/main/res/layout/fragment_about.xml b/app/src/main/res/layout/fragment_about.xml index 9c796fc67..af30e6156 100644 --- a/app/src/main/res/layout/fragment_about.xml +++ b/app/src/main/res/layout/fragment_about.xml @@ -19,7 +19,7 @@ diff --git a/app/src/main/res/layout/fragment_detail.xml b/app/src/main/res/layout/fragment_detail.xml index db841e50b..68b1cb612 100644 --- a/app/src/main/res/layout/fragment_detail.xml +++ b/app/src/main/res/layout/fragment_detail.xml @@ -21,7 +21,7 @@ app:liftOnScroll="true" app:liftOnScrollTargetViewId="@id/detail_recycler"> - diff --git a/app/src/main/res/layout/fragment_queue.xml b/app/src/main/res/layout/fragment_queue.xml index 2d5b5c97c..2c80466f9 100644 --- a/app/src/main/res/layout/fragment_queue.xml +++ b/app/src/main/res/layout/fragment_queue.xml @@ -19,7 +19,7 @@ app:liftOnScroll="true" app:liftOnScrollTargetViewId="@id/queue_recycler"> -