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 ---
|
// --- 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)
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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)
|
||||||
|
|
||||||
|
|
|
@ -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,12 +112,10 @@ 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)
|
||||||
private val scrollerPadding = Rect(0, 0, 0, 0)
|
private val scrollerPadding = Rect(0, 0, 0, 0)
|
||||||
|
@ -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 ---
|
||||||
|
|
|
@ -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 -> ""
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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()
|
||||||
|
|
|
@ -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()
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -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(
|
||||||
|
|
|
@ -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")
|
||||||
|
|
|
@ -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) :
|
||||||
|
|
|
@ -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)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -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)) {
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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"
|
||||||
|
|
||||||
|
|
|
@ -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) {
|
||||||
|
|
|
@ -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].
|
||||||
*
|
*
|
||||||
|
|
|
@ -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)
|
|
||||||
}
|
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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" />
|
||||||
|
|
||||||
|
|
|
@ -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"
|
||||||
|
|
|
@ -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>
|
||||||
|
|
Loading…
Reference in a new issue