all: apply finishing touches
Apply the finishing touches for 2.0.0, mostly documentation but also some odds and ends.
This commit is contained in:
parent
56ded96b10
commit
61624352e4
28 changed files with 235 additions and 188 deletions
|
@ -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)
|
||||
|
|
|
@ -48,6 +48,7 @@ class DetailAppBarLayout @JvmOverloads constructor(
|
|||
|
||||
val toolbar = findViewById<Toolbar>(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<RecyclerView>(R.id.detail_recycler)
|
||||
val newRecycler = (parent as ViewGroup).findViewById<RecyclerView>(liftOnScrollTargetViewId)
|
||||
|
||||
mRecycler = newRecycler
|
||||
return newRecycler
|
||||
|
|
|
@ -70,6 +70,7 @@ class DetailViewModel : ViewModel() {
|
|||
val showMenu: LiveData<MenuConfig?> = mShowMenu
|
||||
|
||||
private val mNavToItem = MutableLiveData<BaseModel?>()
|
||||
|
||||
/** Flag for unified navigation. Observe this to coordinate navigation to an item's UI. */
|
||||
val navToItem: LiveData<BaseModel?> get() = mNavToItem
|
||||
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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<Song>())
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
|
|
@ -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 ---
|
||||
|
|
|
@ -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 -> ""
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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 <T : BaseModel, VH : RecyclerView.ViewHolder> 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
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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<MusicCallback>()
|
||||
|
||||
/**
|
||||
* 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
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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")
|
||||
|
|
|
@ -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) :
|
||||
|
|
|
@ -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<BaseModel>())
|
||||
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<BaseModel>()
|
||||
|
||||
|
@ -133,11 +138,18 @@ class SearchViewModel : ViewModel(), MusicStore.MusicCallback {
|
|||
*/
|
||||
private fun List<Music>.filterByOrNull(value: String): List<BaseModel>? {
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)) {
|
||||
|
|
|
@ -165,7 +165,7 @@ class SettingsListFragment : PreferenceFragmentCompat() {
|
|||
}
|
||||
}
|
||||
|
||||
SettingsManager.KEY_BLACKLIST -> {
|
||||
SettingsManager.KEY_EXCLUDED -> {
|
||||
onPreferenceClickListener = Preference.OnPreferenceClickListener {
|
||||
ExcludedDialog().show(childFragmentManager, ExcludedDialog.TAG)
|
||||
true
|
||||
|
|
|
@ -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"
|
||||
|
||||
|
|
|
@ -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<ActionHeader>(binding) {
|
||||
|
|
|
@ -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].
|
||||
*
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -19,7 +19,7 @@
|
|||
|
||||
<androidx.appcompat.widget.Toolbar
|
||||
android:id="@+id/about_toolbar"
|
||||
style="@style/Widget.Auxio.Toolbar.Icon.Down"
|
||||
style="@style/Widget.Auxio.Toolbar.Icon"
|
||||
app:title="@string/lbl_about" />
|
||||
|
||||
</org.oxycblt.auxio.ui.EdgeAppBarLayout>
|
||||
|
|
|
@ -21,7 +21,7 @@
|
|||
app:liftOnScroll="true"
|
||||
app:liftOnScrollTargetViewId="@id/detail_recycler">
|
||||
|
||||
<androidx.appcompat.widget.Toolbar
|
||||
<com.google.android.material.appbar.MaterialToolbar
|
||||
android:id="@+id/detail_toolbar"
|
||||
style="@style/Widget.Auxio.Toolbar.Icon" />
|
||||
|
||||
|
|
|
@ -19,7 +19,7 @@
|
|||
app:liftOnScroll="true"
|
||||
app:liftOnScrollTargetViewId="@id/queue_recycler">
|
||||
|
||||
<androidx.appcompat.widget.Toolbar
|
||||
<com.google.android.material.appbar.MaterialToolbar
|
||||
android:id="@+id/queue_toolbar"
|
||||
style="@style/Widget.Auxio.Toolbar.Icon.Down"
|
||||
android:elevation="0dp"
|
||||
|
|
|
@ -22,7 +22,7 @@
|
|||
|
||||
<androidx.appcompat.widget.Toolbar
|
||||
android:id="@+id/settings_toolbar"
|
||||
style="@style/Widget.Auxio.Toolbar.Icon.Down"
|
||||
style="@style/Widget.Auxio.Toolbar.Icon"
|
||||
app:title="@string/set_title" />
|
||||
|
||||
</org.oxycblt.auxio.ui.EdgeAppBarLayout>
|
||||
|
|
Loading…
Reference in a new issue