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:
OxygenCobalt 2021-11-25 12:02:10 -07:00
parent 56ded96b10
commit 61624352e4
No known key found for this signature in database
GPG key ID: 37DBE3621FE9AD47
28 changed files with 235 additions and 188 deletions

View file

@ -78,6 +78,8 @@ class MainFragment : Fragment(), PlaybackLayout.ActionCallback {
// --- VIEWMODEL SETUP --- // --- 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.setActionCallback(this)
binding.playbackLayout.setSong(playbackModel.song.value) binding.playbackLayout.setSong(playbackModel.song.value)

View file

@ -48,6 +48,7 @@ class DetailAppBarLayout @JvmOverloads constructor(
val toolbar = findViewById<Toolbar>(R.id.detail_toolbar) 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 { val newTitleView = Toolbar::class.java.getDeclaredField("mTitleTextView").run {
isAccessible = true isAccessible = true
get(toolbar) as AppCompatTextView get(toolbar) as AppCompatTextView
@ -66,7 +67,7 @@ class DetailAppBarLayout @JvmOverloads constructor(
return recycler return recycler
} }
val newRecycler = (parent as ViewGroup).findViewById<RecyclerView>(R.id.detail_recycler) val newRecycler = (parent as ViewGroup).findViewById<RecyclerView>(liftOnScrollTargetViewId)
mRecycler = newRecycler mRecycler = newRecycler
return newRecycler return newRecycler

View file

@ -70,6 +70,7 @@ class DetailViewModel : ViewModel() {
val showMenu: LiveData<MenuConfig?> = mShowMenu val showMenu: LiveData<MenuConfig?> = mShowMenu
private val mNavToItem = MutableLiveData<BaseModel?>() private val mNavToItem = MutableLiveData<BaseModel?>()
/** Flag for unified navigation. Observe this to coordinate navigation to an item's UI. */ /** Flag for unified navigation. Observe this to coordinate navigation to an item's UI. */
val navToItem: LiveData<BaseModel?> get() = mNavToItem val navToItem: LiveData<BaseModel?> get() = mNavToItem

View file

@ -25,6 +25,10 @@ import android.widget.FrameLayout
import androidx.core.view.updatePadding import androidx.core.view.updatePadding
import org.oxycblt.auxio.util.systemBarsCompat import org.oxycblt.auxio.util.systemBarsCompat
/**
* A container for a FloatingActionButton that enables edge-to-edge support.
* @author OxygenCobalt
*/
class FloatingActionButtonContainer @JvmOverloads constructor( class FloatingActionButtonContainer @JvmOverloads constructor(
context: Context, context: Context,
attrs: AttributeSet? = null, attrs: AttributeSet? = null,

View file

@ -18,11 +18,15 @@
package org.oxycblt.auxio.home package org.oxycblt.auxio.home
import android.graphics.LinearGradient
import android.graphics.Shader
import android.os.Bundle import android.os.Bundle
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.MenuItem import android.view.MenuItem
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import androidx.appcompat.widget.AppCompatTextView
import androidx.appcompat.widget.Toolbar
import androidx.core.view.iterator import androidx.core.view.iterator
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
import androidx.fragment.app.activityViewModels 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.ui.DisplayMode
import org.oxycblt.auxio.util.logD import org.oxycblt.auxio.util.logD
import org.oxycblt.auxio.util.logE import org.oxycblt.auxio.util.logE
import org.oxycblt.auxio.util.resolveAttr
/** /**
* The main "Launching Point" fragment of Auxio, allowing navigation to the detail * The main "Launching Point" fragment of Auxio, allowing navigation to the detail
* views for each respective fragment. * views for each respective item.
* @author OxygenCobalt * @author OxygenCobalt
*/ */
class HomeFragment : Fragment() { class HomeFragment : Fragment() {
@ -96,6 +101,7 @@ class HomeFragment : Fragment() {
R.id.option_sort_asc -> { R.id.option_sort_asc -> {
item.isChecked = !item.isChecked item.isChecked = !item.isChecked
val new = homeModel.getSortForDisplay(homeModel.curTab.value!!) val new = homeModel.getSortForDisplay(homeModel.curTab.value!!)
.ascending(item.isChecked) .ascending(item.isChecked)
@ -117,6 +123,21 @@ class HomeFragment : Fragment() {
} }
sortItem = menu.findItem(R.id.submenu_sorting) 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 { binding.homePager.apply {

View file

@ -21,6 +21,8 @@ package org.oxycblt.auxio.home
import androidx.lifecycle.LiveData import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import kotlinx.coroutines.launch
import org.oxycblt.auxio.music.Album import org.oxycblt.auxio.music.Album
import org.oxycblt.auxio.music.Artist import org.oxycblt.auxio.music.Artist
import org.oxycblt.auxio.music.Genre 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. * The ViewModel for managing [HomeFragment]'s data, sorting modes, and tab state.
* @author OxygenCobalt * @author OxygenCobalt
*/ */
class HomeViewModel : ViewModel(), SettingsManager.Callback, MusicStore.MusicCallback { class HomeViewModel : ViewModel(), SettingsManager.Callback {
private val settingsManager = SettingsManager.getInstance() private val settingsManager = SettingsManager.getInstance()
private val mSongs = MutableLiveData(listOf<Song>()) private val mSongs = MutableLiveData(listOf<Song>())
@ -73,7 +75,15 @@ class HomeViewModel : ViewModel(), SettingsManager.Callback, MusicStore.MusicCal
init { init {
settingsManager.addCallback(this) 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 settingsManager.libAlbumSort = sort
mAlbums.value = sort.sortAlbums(mAlbums.value!!) mAlbums.value = sort.sortAlbums(mAlbums.value!!)
} }
DisplayMode.SHOW_ARTISTS -> { DisplayMode.SHOW_ARTISTS -> {
settingsManager.libArtistSort = sort settingsManager.libArtistSort = sort
mArtists.value = sort.sortParents(mArtists.value!!) mArtists.value = sort.sortParents(mArtists.value!!)
} }
DisplayMode.SHOW_GENRES -> { DisplayMode.SHOW_GENRES -> {
settingsManager.libGenreSort = sort settingsManager.libGenreSort = sort
mGenres.value = sort.sortParents(mGenres.value!!) mGenres.value = sort.sortParents(mGenres.value!!)
} }
else -> {} else -> {}
} }
} }
@ -137,16 +150,8 @@ class HomeViewModel : ViewModel(), SettingsManager.Callback, MusicStore.MusicCal
mRecreateTabs.value = true 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() { override fun onCleared() {
super.onCleared() super.onCleared()
settingsManager.removeCallback(this) settingsManager.removeCallback(this)
MusicStore.cancelAwaitInstance(this)
} }
} }

View file

@ -48,6 +48,8 @@ import kotlin.math.sqrt
* - Variable names are no longer prefixed with m * - Variable names are no longer prefixed with m
* - Made path management compat-friendly * - Made path management compat-friendly
* - Converted to kotlin * - Converted to kotlin
*
* @author Hai Zhang, OxygenCobalt
*/ */
class FastScrollPopupDrawable(context: Context) : Drawable() { class FastScrollPopupDrawable(context: Context) : Drawable() {
private val paint: Paint = Paint().apply { private val paint: Paint = Paint().apply {
@ -116,7 +118,7 @@ class FastScrollPopupDrawable(context: Context) : Drawable() {
val r = height / 2 val r = height / 2
val sqrt2 = sqrt(2.0).toFloat() val sqrt2 = sqrt(2.0).toFloat()
// Ensure we are convex. // Ensure we are convex
width = (r + sqrt2 * r).coerceAtLeast(width) width = (r + sqrt2 * r).coerceAtLeast(width)
pathArcTo(path, r, r, r, 90f, 180f) pathArcTo(path, r, r, r, 90f, 180f)

View file

@ -34,6 +34,7 @@ import android.widget.FrameLayout
import android.widget.TextView import android.widget.TextView
import androidx.appcompat.widget.AppCompatTextView import androidx.appcompat.widget.AppCompatTextView
import androidx.core.math.MathUtils import androidx.core.math.MathUtils
import androidx.core.view.isInvisible
import androidx.core.widget.TextViewCompat import androidx.core.widget.TextViewCompat
import androidx.recyclerview.widget.GridLayoutManager import androidx.recyclerview.widget.GridLayoutManager
import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.LinearLayoutManager
@ -66,33 +67,44 @@ import kotlin.math.abs
* - Redundant functions have been merged * - Redundant functions have been merged
* - Variable names are no longer prefixed with m * - Variable names are no longer prefixed with m
* - Added drag listener * - Added drag listener
* - TODO: Added documentation * - Added documentation
*
* @author Hai Zhang, OxygenCobalt
*/ */
class FastScrollRecyclerView @JvmOverloads constructor( class FastScrollRecyclerView @JvmOverloads constructor(
context: Context, context: Context,
attrs: AttributeSet? = null, attrs: AttributeSet? = null,
defStyleAttr: Int = -1 defStyleAttr: Int = -1
) : RecyclerView(context, attrs, defStyleAttr) { ) : 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 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 var onDragListener: ((Boolean) -> Unit)? = null
private val minTouchTargetSize: Int = resources.getDimensionPixelSize(R.dimen.size_btn_small) private val minTouchTargetSize: Int = resources.getDimensionPixelSize(R.dimen.size_btn_small)
private val touchSlop: Int = ViewConfiguration.get(context).scaledTouchSlop 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 trackView: View
private val thumbView: View private val thumbView: View
private val popupView: TextView private val popupView: TextView
// Touch values
private val thumbWidth: Int private val thumbWidth: Int
private val thumbHeight: Int private val thumbHeight: Int
private var thumbOffset = 0 private var thumbOffset = 0
private var downX = 0f private var downX = 0f
private var downY = 0f private var downY = 0f
private var lastY = 0f private var lastY = 0f
private var dragStartY = 0f private var dragStartY = 0f
private var dragStartThumbOffset = 0 private var dragStartThumbOffset = 0
// State
private var dragging = false private var dragging = false
private var showingScrollbar = false private var showingScrollbar = false
private var showingPopup = false private var showingPopup = false
@ -100,11 +112,9 @@ class FastScrollRecyclerView @JvmOverloads constructor(
private val childRect = Rect() private val childRect = Rect()
private val hideScrollbarRunnable = Runnable { private val hideScrollbarRunnable = Runnable {
if (dragging) { if (!dragging) {
return@Runnable hideScrollbar()
} }
hideScrollbar()
} }
private val initialPadding = Rect(paddingLeft, paddingTop, paddingRight, paddingBottom) 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 // We use a listener instead of overriding onTouchEvent so that we don't conflict with
// RecyclerView touch events. // RecyclerView touch events.
addOnItemTouchListener(object : SimpleOnItemTouchListener() { addOnItemTouchListener(object : SimpleOnItemTouchListener() {
override fun onInterceptTouchEvent(
recyclerView: RecyclerView,
event: MotionEvent
): Boolean {
return onItemTouch(event)
}
override fun onTouchEvent( override fun onTouchEvent(
recyclerView: RecyclerView, recyclerView: RecyclerView,
event: MotionEvent event: MotionEvent
) { ) {
onItemTouch(event) 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.isInvisible = popupText.isEmpty()
popupView.visibility = if (hasPopup) View.VISIBLE else View.INVISIBLE
if (hasPopup) { if (popupText.isNotEmpty()) {
val popupLayoutParams = popupView.layoutParams as FrameLayout.LayoutParams val popupLayoutParams = popupView.layoutParams as FrameLayout.LayoutParams
if (popupView.text != popupText) { if (popupView.text != popupText) {
@ -343,9 +352,8 @@ class FastScrollRecyclerView @JvmOverloads constructor(
downY = eventY downY = eventY
val scrollX = trackView.scrollX val scrollX = trackView.scrollX
val isInScrollbar = ( val isInScrollbar =
eventX >= thumbView.left - scrollX && eventX < thumbView.right - scrollX eventX >= thumbView.left - scrollX && eventX < thumbView.right - scrollX
)
if (trackView.alpha > 0 && isInScrollbar) { if (trackView.alpha > 0 && isInScrollbar) {
dragStartY = eventY dragStartY = eventY
@ -384,6 +392,7 @@ class FastScrollRecyclerView @JvmOverloads constructor(
MotionEvent.ACTION_UP, MotionEvent.ACTION_CANCEL -> setDragging(false) MotionEvent.ACTION_UP, MotionEvent.ACTION_CANCEL -> setDragging(false)
} }
lastY = eventY lastY = eventY
return dragging return dragging
} }
@ -462,21 +471,21 @@ class FastScrollRecyclerView @JvmOverloads constructor(
} }
} }
private fun setDragging(dragging: Boolean) { private fun setDragging(isDragging: Boolean) {
if (this.dragging == dragging) { if (dragging == isDragging) {
return return
} }
this.dragging = dragging dragging = isDragging
if (this.dragging) { if (dragging) {
parent.requestDisallowInterceptTouchEvent(true) parent.requestDisallowInterceptTouchEvent(true)
} }
trackView.isPressed = this.dragging trackView.isPressed = dragging
thumbView.isPressed = this.dragging thumbView.isPressed = dragging
if (this.dragging) { if (dragging) {
removeCallbacks(hideScrollbarRunnable) removeCallbacks(hideScrollbarRunnable)
showScrollbar() showScrollbar()
showPopup() showPopup()
@ -485,7 +494,7 @@ class FastScrollRecyclerView @JvmOverloads constructor(
hidePopup() hidePopup()
} }
onDragListener?.invoke(dragging) onDragListener?.invoke(isDragging)
} }
// --- SCROLLBAR APPEARANCE --- // --- SCROLLBAR APPEARANCE ---

View file

@ -32,6 +32,10 @@ import org.oxycblt.auxio.ui.Sort
import org.oxycblt.auxio.ui.newMenu import org.oxycblt.auxio.ui.newMenu
import org.oxycblt.auxio.ui.sliceArticle import org.oxycblt.auxio.ui.sliceArticle
/**
* A [HomeListFragment] for showing a list of [Album]s.
* @author
*/
class AlbumListFragment : HomeListFragment() { class AlbumListFragment : HomeListFragment() {
override fun onCreateView( override fun onCreateView(
inflater: LayoutInflater, inflater: LayoutInflater,
@ -54,19 +58,24 @@ class AlbumListFragment : HomeListFragment() {
return binding.root return binding.root
} }
override val popupProvider: (Int) -> String override val listPopupProvider: (Int) -> String
get() = { idx -> get() = { idx ->
val album = homeModel.albums.value!![idx] val album = homeModel.albums.value!![idx]
// Change how we display the popup depending on the mode.
when (homeModel.getSortForDisplay(DisplayMode.SHOW_ALBUMS)) { when (homeModel.getSortForDisplay(DisplayMode.SHOW_ALBUMS)) {
// By Name -> Use Name
is Sort.ByName -> album.name.sliceArticle() is Sort.ByName -> album.name.sliceArticle()
.first().uppercase() .first().uppercase()
// By Artist -> Use Artist Name
is Sort.ByArtist -> album.artist.resolvedName.sliceArticle() is Sort.ByArtist -> album.artist.resolvedName.sliceArticle()
.first().uppercase() .first().uppercase()
// Year -> Use Full Year
is Sort.ByYear -> album.year.toString() is Sort.ByYear -> album.year.toString()
// Unsupported sort, error gracefully
else -> "" else -> ""
} }
} }

View file

@ -30,6 +30,10 @@ import org.oxycblt.auxio.ui.ArtistViewHolder
import org.oxycblt.auxio.ui.newMenu import org.oxycblt.auxio.ui.newMenu
import org.oxycblt.auxio.ui.sliceArticle import org.oxycblt.auxio.ui.sliceArticle
/**
* A [HomeListFragment] for showing a list of [Artist]s.
* @author
*/
class ArtistListFragment : HomeListFragment() { class ArtistListFragment : HomeListFragment() {
override fun onCreateView( override fun onCreateView(
inflater: LayoutInflater, inflater: LayoutInflater,
@ -52,7 +56,7 @@ class ArtistListFragment : HomeListFragment() {
return binding.root return binding.root
} }
override val popupProvider: (Int) -> String override val listPopupProvider: (Int) -> String
get() = { idx -> get() = { idx ->
homeModel.artists.value!![idx].resolvedName homeModel.artists.value!![idx].resolvedName
.sliceArticle().first().uppercase() .sliceArticle().first().uppercase()

View file

@ -30,6 +30,10 @@ import org.oxycblt.auxio.ui.GenreViewHolder
import org.oxycblt.auxio.ui.newMenu import org.oxycblt.auxio.ui.newMenu
import org.oxycblt.auxio.ui.sliceArticle import org.oxycblt.auxio.ui.sliceArticle
/**
* A [HomeListFragment] for showing a list of [Genre]s.
* @author
*/
class GenreListFragment : HomeListFragment() { class GenreListFragment : HomeListFragment() {
override fun onCreateView( override fun onCreateView(
inflater: LayoutInflater, inflater: LayoutInflater,
@ -52,7 +56,7 @@ class GenreListFragment : HomeListFragment() {
return binding.root return binding.root
} }
override val popupProvider: (Int) -> String override val listPopupProvider: (Int) -> String
get() = { idx -> get() = { idx ->
homeModel.genres.value!![idx].resolvedName homeModel.genres.value!![idx].resolvedName
.sliceArticle().first().uppercase() .sliceArticle().first().uppercase()

View file

@ -19,8 +19,6 @@
package org.oxycblt.auxio.home.list package org.oxycblt.auxio.home.list
import android.annotation.SuppressLint import android.annotation.SuppressLint
import android.os.Bundle
import android.view.View
import androidx.annotation.IdRes import androidx.annotation.IdRes
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
import androidx.fragment.app.activityViewModels import androidx.fragment.app.activityViewModels
@ -34,8 +32,8 @@ import org.oxycblt.auxio.ui.memberBinding
import org.oxycblt.auxio.util.applySpans 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() { abstract class HomeListFragment : Fragment() {
protected val binding: FragmentHomeListBinding by memberBinding( protected val binding: FragmentHomeListBinding by memberBinding(
@ -45,16 +43,10 @@ abstract class HomeListFragment : Fragment() {
protected val homeModel: HomeViewModel by activityViewModels() protected val homeModel: HomeViewModel by activityViewModels()
protected val playbackModel: PlaybackViewModel by activityViewModels() protected val playbackModel: PlaybackViewModel by activityViewModels()
abstract val popupProvider: (Int) -> String /**
* The popup provider to use for the fast scroller view.
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { */
super.onViewCreated(view, savedInstanceState) abstract val listPopupProvider: (Int) -> String
binding.homeRecycler.popupProvider = popupProvider
binding.homeRecycler.onDragListener = { dragging ->
homeModel.updateFastScrolling(dragging)
}
}
protected fun <T : BaseModel, VH : RecyclerView.ViewHolder> setupRecycler( protected fun <T : BaseModel, VH : RecyclerView.ViewHolder> setupRecycler(
@IdRes uniqueId: Int, @IdRes uniqueId: Int,
@ -66,6 +58,11 @@ abstract class HomeListFragment : Fragment() {
adapter = homeAdapter adapter = homeAdapter
setHasFixedSize(true) setHasFixedSize(true)
applySpans() applySpans()
popupProvider = listPopupProvider
onDragListener = { dragging ->
homeModel.updateFastScrolling(dragging)
}
} }
// Make sure that this RecyclerView has data before startup // Make sure that this RecyclerView has data before startup

View file

@ -30,6 +30,10 @@ import org.oxycblt.auxio.ui.Sort
import org.oxycblt.auxio.ui.newMenu import org.oxycblt.auxio.ui.newMenu
import org.oxycblt.auxio.ui.sliceArticle import org.oxycblt.auxio.ui.sliceArticle
/**
* A [HomeListFragment] for showing a list of [Song]s.
* @author
*/
class SongListFragment : HomeListFragment() { class SongListFragment : HomeListFragment() {
override fun onCreateView( override fun onCreateView(
inflater: LayoutInflater, inflater: LayoutInflater,
@ -50,21 +54,26 @@ class SongListFragment : HomeListFragment() {
return binding.root return binding.root
} }
override val popupProvider: (Int) -> String override val listPopupProvider: (Int) -> String
get() = { idx -> get() = { idx ->
val song = homeModel.songs.value!![idx] val song = homeModel.songs.value!![idx]
// Change how we display the popup depending on the mode.
when (homeModel.getSortForDisplay(DisplayMode.SHOW_SONGS)) { when (homeModel.getSortForDisplay(DisplayMode.SHOW_SONGS)) {
// Name -> Use name
is Sort.ByName -> song.name.sliceArticle() is Sort.ByName -> song.name.sliceArticle()
.first().uppercase() .first().uppercase()
// Artist -> Use Artist Name
is Sort.ByArtist -> is Sort.ByArtist ->
song.album.artist.resolvedName song.album.artist.resolvedName
.sliceArticle().first().uppercase() .sliceArticle().first().uppercase()
// Album -> Use Album Name
is Sort.ByAlbum -> song.album.name.sliceArticle() is Sort.ByAlbum -> song.album.name.sliceArticle()
.first().uppercase() .first().uppercase()
// Year -> Use Full Year
is Sort.ByYear -> song.album.year.toString() is Sort.ByYear -> song.album.year.toString()
} }
} }

View file

@ -131,14 +131,9 @@ class MusicStore private constructor() {
NO_PERMS, NO_MUSIC, FAILED NO_PERMS, NO_MUSIC, FAILED
} }
interface MusicCallback {
fun onLoaded(musicStore: MusicStore)
}
companion object { companion object {
@Volatile @Volatile
private var RESPONSE: Response? = null private var RESPONSE: Response? = null
private val AWAITING = mutableListOf<MusicCallback>()
/** /**
* Initialize the loading process for this instance. This must be ran on a background * Initialize the loading process for this instance. This must be ran on a background
@ -162,37 +157,28 @@ class MusicStore private constructor() {
response response
} }
if (response is Response.Ok) {
AWAITING.forEach { it.onLoaded(response.musicStore) }
AWAITING.clear()
}
return response return response
} }
/** /**
* Await the successful creation of a [MusicStore] instance. The [callback] * Await the successful creation of a [MusicStore] instance. The co-routine calling
* will be called if the instance is already loaded. It's recommended to call * this will block until the successful creation of a [MusicStore], in which it will
* [cancelAwaitInstance] if the object is about to be destroyed to prevent any * then be returned.
* memory leaks.
*/ */
fun awaitInstance(callback: MusicCallback) { suspend fun awaitInstance() = withContext(Dispatchers.Default) {
// FIXME: There has to be some coroutiney way to do this instead of just making // We have to do a withContext call so we don't block the JVM thread
// a leak-prone callback system val musicStore: MusicStore
val currentInstance = maybeGetInstance()
if (currentInstance != null) { while (true) {
callback.onLoaded(currentInstance) val response = RESPONSE
if (response is Response.Ok) {
musicStore = response.musicStore
break
}
} }
AWAITING.add(callback) musicStore
}
/**
* Remove a callback from the queue.
*/
fun cancelAwaitInstance(callback: MusicCallback) {
AWAITING.remove(callback)
} }
/** /**

View file

@ -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 * 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. * or extendable. You have been warned.
* *
* TODO: Add the queue view into this layout.
*
* @author OxygenCobalt (With help from Umano and Hai Zhang) * @author OxygenCobalt (With help from Umano and Hai Zhang)
*/ */
class PlaybackLayout @JvmOverloads constructor( class PlaybackLayout @JvmOverloads constructor(

View file

@ -19,6 +19,8 @@
package org.oxycblt.auxio.playback.queue package org.oxycblt.auxio.playback.queue
import android.annotation.SuppressLint import android.annotation.SuppressLint
import android.content.res.ColorStateList
import android.graphics.drawable.ColorDrawable
import android.view.MotionEvent import android.view.MotionEvent
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
@ -26,6 +28,7 @@ import androidx.core.view.isInvisible
import androidx.recyclerview.widget.AsyncListDiffer import androidx.recyclerview.widget.AsyncListDiffer
import androidx.recyclerview.widget.ItemTouchHelper import androidx.recyclerview.widget.ItemTouchHelper
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import com.google.android.material.shape.MaterialShapeDrawable
import org.oxycblt.auxio.databinding.ItemQueueSongBinding import org.oxycblt.auxio.databinding.ItemQueueSongBinding
import org.oxycblt.auxio.music.ActionHeader import org.oxycblt.auxio.music.ActionHeader
import org.oxycblt.auxio.music.BaseModel 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.BaseViewHolder
import org.oxycblt.auxio.ui.DiffCallback import org.oxycblt.auxio.ui.DiffCallback
import org.oxycblt.auxio.ui.HeaderViewHolder import org.oxycblt.auxio.ui.HeaderViewHolder
import org.oxycblt.auxio.util.applyMaterialDrawable
import org.oxycblt.auxio.util.inflater import org.oxycblt.auxio.util.inflater
import org.oxycblt.auxio.util.logD import org.oxycblt.auxio.util.logD
import org.oxycblt.auxio.util.logE import org.oxycblt.auxio.util.logE
@ -151,7 +153,11 @@ class QueueAdapter(
val backgroundView: View get() = binding.background val backgroundView: View get() = binding.background
init { init {
binding.body.applyMaterialDrawable() binding.body.background = MaterialShapeDrawable.createWithElevationOverlay(
binding.root.context
).apply {
fillColor = ColorStateList.valueOf((binding.body.background as ColorDrawable).color)
}
} }
@SuppressLint("ClickableViewAccessibility") @SuppressLint("ClickableViewAccessibility")

View file

@ -34,7 +34,6 @@ import org.oxycblt.auxio.util.queryAll
/** /**
* A SQLite database for managing the persistent playback state and queue. * 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. * 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 * @author OxygenCobalt
*/ */
class PlaybackStateDatabase(context: Context) : class PlaybackStateDatabase(context: Context) :

View file

@ -29,6 +29,7 @@ import org.oxycblt.auxio.music.BaseModel
import org.oxycblt.auxio.music.Header import org.oxycblt.auxio.music.Header
import org.oxycblt.auxio.music.HeaderString import org.oxycblt.auxio.music.HeaderString
import org.oxycblt.auxio.music.Music import org.oxycblt.auxio.music.Music
import org.oxycblt.auxio.music.MusicParent
import org.oxycblt.auxio.music.MusicStore import org.oxycblt.auxio.music.MusicStore
import org.oxycblt.auxio.settings.SettingsManager import org.oxycblt.auxio.settings.SettingsManager
import org.oxycblt.auxio.ui.DisplayMode import org.oxycblt.auxio.ui.DisplayMode
@ -38,7 +39,7 @@ import java.text.Normalizer
* The [ViewModel] for the search functionality * The [ViewModel] for the search functionality
* @author OxygenCobalt * @author OxygenCobalt
*/ */
class SearchViewModel : ViewModel(), MusicStore.MusicCallback { class SearchViewModel : ViewModel() {
private val mSearchResults = MutableLiveData(listOf<BaseModel>()) private val mSearchResults = MutableLiveData(listOf<BaseModel>())
private var mIsNavigating = false private var mIsNavigating = false
private var mFilterMode: DisplayMode? = null private var mFilterMode: DisplayMode? = null
@ -54,7 +55,10 @@ class SearchViewModel : ViewModel(), MusicStore.MusicCallback {
init { init {
mFilterMode = settingsManager.searchFilterMode mFilterMode = settingsManager.searchFilterMode
MusicStore.awaitInstance(this) viewModelScope.launch {
MusicStore.awaitInstance()
search(mLastQuery)
}
} }
/** /**
@ -71,6 +75,7 @@ class SearchViewModel : ViewModel(), MusicStore.MusicCallback {
return return
} }
// Searching can be quite expensive, so hop on a co-routine
viewModelScope.launch { viewModelScope.launch {
val results = mutableListOf<BaseModel>() val results = mutableListOf<BaseModel>()
@ -133,11 +138,18 @@ class SearchViewModel : ViewModel(), MusicStore.MusicCallback {
*/ */
private fun List<Music>.filterByOrNull(value: String): List<BaseModel>? { private fun List<Music>.filterByOrNull(value: String): List<BaseModel>? {
val filtered = filter { 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" // 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 // [e.g all accented/unicode chars become latin chars] instead. Hopefully this
// shouldn't break other language's search functionality. // shouldn't break other language's search functionality.
it.name.contains(value, ignoreCase = true) || name.contains(value, ignoreCase = true) ||
it.name.normalized().contains(value, ignoreCase = true) name.normalized().contains(value, ignoreCase = true)
} }
return if (filtered.isNotEmpty()) filtered else null return if (filtered.isNotEmpty()) filtered else null
@ -179,15 +191,4 @@ class SearchViewModel : ViewModel(), MusicStore.MusicCallback {
fun setNavigating(isNavigating: Boolean) { fun setNavigating(isNavigating: Boolean) {
mIsNavigating = isNavigating mIsNavigating = isNavigating
} }
// --- OVERRIDES ---
override fun onLoaded(musicStore: MusicStore) {
search(mLastQuery)
}
override fun onCleared() {
super.onCleared()
MusicStore.cancelAwaitInstance(this)
}
} }

View file

@ -28,8 +28,8 @@ import org.oxycblt.auxio.playback.state.PlaybackMode
// A couple of utils for migrating from old settings values to the new // A couple of utils for migrating from old settings values to the new
// formats used in 1.3.2 & 1.4.0 // 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 // TODO: Slate these for removal eventually. There probably isn't that many left who have the
// everyone has migrated. // old values but 2.0.0 will probably convince most of those to update too.
fun handleThemeCompat(prefs: SharedPreferences): Int { fun handleThemeCompat(prefs: SharedPreferences): Int {
if (prefs.contains(OldKeys.KEY_THEME)) { if (prefs.contains(OldKeys.KEY_THEME)) {

View file

@ -165,7 +165,7 @@ class SettingsListFragment : PreferenceFragmentCompat() {
} }
} }
SettingsManager.KEY_BLACKLIST -> { SettingsManager.KEY_EXCLUDED -> {
onPreferenceClickListener = Preference.OnPreferenceClickListener { onPreferenceClickListener = Preference.OnPreferenceClickListener {
ExcludedDialog().show(childFragmentManager, ExcludedDialog.TAG) ExcludedDialog().show(childFragmentManager, ExcludedDialog.TAG)
true true

View file

@ -249,9 +249,8 @@ class SettingsManager private constructor(context: Context) :
} }
companion object { companion object {
// Preference keys // Note: The old way of naming keys was to prefix them with KEY_. Now it's to prefix them
// The old way of naming keys was to prefix them with KEY_. Now it's to prefix them with // with auxio_.
// auxio_.
const val KEY_THEME = "KEY_THEME2" const val KEY_THEME = "KEY_THEME2"
const val KEY_BLACK_THEME = "KEY_BLACK_THEME" const val KEY_BLACK_THEME = "KEY_BLACK_THEME"
const val KEY_ACCENT = "auxio_accent" 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_LOOP_PAUSE = "KEY_LOOP_PAUSE"
const val KEY_SAVE_STATE = "auxio_save_state" 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" const val KEY_SEARCH_FILTER_MODE = "KEY_SEARCH_FILTER"

View file

@ -249,6 +249,9 @@ class HeaderViewHolder private constructor(
} }
} }
/**
* The Shared ViewHolder for an [ActionHeader]. Instantiation should be done with [from]
*/
class ActionHeaderViewHolder private constructor( class ActionHeaderViewHolder private constructor(
private val binding: ItemActionHeaderBinding private val binding: ItemActionHeaderBinding
) : BaseViewHolder<ActionHeader>(binding) { ) : BaseViewHolder<ActionHeader>(binding) {

View file

@ -22,10 +22,8 @@ import android.content.Context
import android.content.res.ColorStateList import android.content.res.ColorStateList
import android.content.res.Resources import android.content.res.Resources
import android.graphics.Rect import android.graphics.Rect
import android.graphics.drawable.ColorDrawable
import android.os.Build import android.os.Build
import android.util.TypedValue import android.util.TypedValue
import android.view.View
import android.view.WindowInsets import android.view.WindowInsets
import androidx.annotation.AttrRes import androidx.annotation.AttrRes
import androidx.annotation.ColorInt import androidx.annotation.ColorInt
@ -34,23 +32,8 @@ import androidx.annotation.DrawableRes
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import androidx.recyclerview.widget.GridLayoutManager import androidx.recyclerview.widget.GridLayoutManager
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import com.google.android.material.shape.MaterialShapeDrawable
import org.oxycblt.auxio.R 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]. * Apply the recommended spans for a [RecyclerView].
* *

View file

@ -27,6 +27,63 @@ import org.oxycblt.auxio.playback.system.PlaybackService
import org.oxycblt.auxio.util.newBroadcastIntent import org.oxycblt.auxio.util.newBroadcastIntent
import org.oxycblt.auxio.util.newMainIntent 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( private fun createViews(
context: Context, context: Context,
@LayoutRes layout: Int @LayoutRes layout: Int
@ -141,60 +198,3 @@ private fun RemoteViews.applyFullControls(context: Context, state: WidgetState):
return this 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)
}

View file

@ -19,7 +19,7 @@
<androidx.appcompat.widget.Toolbar <androidx.appcompat.widget.Toolbar
android:id="@+id/about_toolbar" android:id="@+id/about_toolbar"
style="@style/Widget.Auxio.Toolbar.Icon.Down" style="@style/Widget.Auxio.Toolbar.Icon"
app:title="@string/lbl_about" /> app:title="@string/lbl_about" />
</org.oxycblt.auxio.ui.EdgeAppBarLayout> </org.oxycblt.auxio.ui.EdgeAppBarLayout>

View file

@ -21,7 +21,7 @@
app:liftOnScroll="true" app:liftOnScroll="true"
app:liftOnScrollTargetViewId="@id/detail_recycler"> app:liftOnScrollTargetViewId="@id/detail_recycler">
<androidx.appcompat.widget.Toolbar <com.google.android.material.appbar.MaterialToolbar
android:id="@+id/detail_toolbar" android:id="@+id/detail_toolbar"
style="@style/Widget.Auxio.Toolbar.Icon" /> style="@style/Widget.Auxio.Toolbar.Icon" />

View file

@ -19,7 +19,7 @@
app:liftOnScroll="true" app:liftOnScroll="true"
app:liftOnScrollTargetViewId="@id/queue_recycler"> app:liftOnScrollTargetViewId="@id/queue_recycler">
<androidx.appcompat.widget.Toolbar <com.google.android.material.appbar.MaterialToolbar
android:id="@+id/queue_toolbar" android:id="@+id/queue_toolbar"
style="@style/Widget.Auxio.Toolbar.Icon.Down" style="@style/Widget.Auxio.Toolbar.Icon.Down"
android:elevation="0dp" android:elevation="0dp"

View file

@ -22,7 +22,7 @@
<androidx.appcompat.widget.Toolbar <androidx.appcompat.widget.Toolbar
android:id="@+id/settings_toolbar" android:id="@+id/settings_toolbar"
style="@style/Widget.Auxio.Toolbar.Icon.Down" style="@style/Widget.Auxio.Toolbar.Icon"
app:title="@string/set_title" /> app:title="@string/set_title" />
</org.oxycblt.auxio.ui.EdgeAppBarLayout> </org.oxycblt.auxio.ui.EdgeAppBarLayout>