all: rework formatting
Do some miscellanious formatting reworks. 1. Remove all instances of m in favor of _. _ is only used when names collide or if something should be internal. 2. Make fragments apply their own click listeners. 3. Remove instances of inc/dec and replace them with the more straightfoward + 1 or - 1.
This commit is contained in:
parent
1a9e55e73b
commit
d296a3aed9
37 changed files with 388 additions and 379 deletions
|
@ -6,9 +6,6 @@ plugins {
|
||||||
}
|
}
|
||||||
|
|
||||||
android {
|
android {
|
||||||
compileSdkVersion 32
|
|
||||||
buildToolsVersion "32.0.0"
|
|
||||||
|
|
||||||
defaultConfig {
|
defaultConfig {
|
||||||
applicationId "org.oxycblt.auxio"
|
applicationId "org.oxycblt.auxio"
|
||||||
versionName "2.2.2"
|
versionName "2.2.2"
|
||||||
|
@ -22,6 +19,20 @@ android {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
compileSdkVersion 32
|
||||||
|
buildToolsVersion "32.0.0"
|
||||||
|
|
||||||
|
// ExoPlayer needs Java 8 to compile.
|
||||||
|
|
||||||
|
kotlinOptions {
|
||||||
|
jvmTarget = "1.8"
|
||||||
|
freeCompilerArgs += "-Xjvm-default=all"
|
||||||
|
}
|
||||||
|
|
||||||
|
compileOptions {
|
||||||
|
targetCompatibility JavaVersion.VERSION_1_8
|
||||||
|
}
|
||||||
|
|
||||||
buildTypes {
|
buildTypes {
|
||||||
debug {
|
debug {
|
||||||
debuggable true
|
debuggable true
|
||||||
|
@ -35,17 +46,6 @@ android {
|
||||||
proguardFiles getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro"
|
proguardFiles getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ExoPlayer needs Java 8 to compile.
|
|
||||||
|
|
||||||
kotlinOptions {
|
|
||||||
jvmTarget = "1.8"
|
|
||||||
freeCompilerArgs += "-Xjvm-default=all"
|
|
||||||
}
|
|
||||||
|
|
||||||
compileOptions {
|
|
||||||
targetCompatibility JavaVersion.VERSION_1_8
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
afterEvaluate {
|
afterEvaluate {
|
||||||
|
@ -102,7 +102,7 @@ dependencies {
|
||||||
implementation "io.coil-kt:coil:2.0.0-rc03"
|
implementation "io.coil-kt:coil:2.0.0-rc03"
|
||||||
|
|
||||||
// Material
|
// Material
|
||||||
implementation "com.google.android.material:material:1.6.0-rc01"
|
implementation "com.google.android.material:material:1.6.0"
|
||||||
|
|
||||||
// LeakCanary
|
// LeakCanary
|
||||||
debugImplementation "com.squareup.leakcanary:leakcanary-android:2.9.1"
|
debugImplementation "com.squareup.leakcanary:leakcanary-android:2.9.1"
|
||||||
|
|
|
@ -46,8 +46,6 @@ import org.oxycblt.auxio.util.systemBarInsetsCompat
|
||||||
*
|
*
|
||||||
* TODO: Rework some fragments to use listeners *even more*
|
* TODO: Rework some fragments to use listeners *even more*
|
||||||
*
|
*
|
||||||
* TODO: Phase out m for _
|
|
||||||
*
|
|
||||||
* TODO: Fix how selection works in the RecyclerViews (doing it poorly right now)
|
* TODO: Fix how selection works in the RecyclerViews (doing it poorly right now)
|
||||||
*
|
*
|
||||||
* TODO: Rework padding ethos
|
* TODO: Rework padding ethos
|
||||||
|
|
|
@ -275,6 +275,6 @@ abstract class BaseFetcher : Fetcher {
|
||||||
|
|
||||||
private fun Dimension.mosaicSize(): Int {
|
private fun Dimension.mosaicSize(): Int {
|
||||||
val size = pxOrElse { 512 }
|
val size = pxOrElse { 512 }
|
||||||
return if (size.mod(2) != 0) size.inc() else size
|
return if (size.mod(2) > 0) size + 1 else size
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -112,7 +112,7 @@ private constructor(
|
||||||
private val genre: Genre,
|
private val genre: Genre,
|
||||||
) : BaseFetcher() {
|
) : BaseFetcher() {
|
||||||
override suspend fun fetch(): FetchResult? {
|
override suspend fun fetch(): FetchResult? {
|
||||||
// Don't sort here to preserve compatibility with previous variations of this image.
|
// Don't sort here to preserve compatibility with previous versions of this image.
|
||||||
val albums = genre.songs.groupBy { it.album }.keys
|
val albums = genre.songs.groupBy { it.album }.keys
|
||||||
val results = albums.mapAtMost(4) { album -> fetchArt(context, album) }
|
val results = albums.mapAtMost(4) { album -> fetchArt(context, album) }
|
||||||
|
|
||||||
|
|
|
@ -22,7 +22,6 @@ import coil.size.Size
|
||||||
import coil.size.pxOrElse
|
import coil.size.pxOrElse
|
||||||
import coil.transform.Transformation
|
import coil.transform.Transformation
|
||||||
import kotlin.math.min
|
import kotlin.math.min
|
||||||
import org.oxycblt.auxio.util.logE
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A transformation that performs a center crop-style transformation on an image, however unlike the
|
* A transformation that performs a center crop-style transformation on an image, however unlike the
|
||||||
|
@ -46,12 +45,7 @@ class SquareFrameTransform : Transformation {
|
||||||
val dst = Bitmap.createBitmap(input, x, y, dstSize, dstSize)
|
val dst = Bitmap.createBitmap(input, x, y, dstSize, dstSize)
|
||||||
|
|
||||||
if (dstSize != desiredWidth || dstSize != desiredHeight) {
|
if (dstSize != desiredWidth || dstSize != desiredHeight) {
|
||||||
try {
|
return Bitmap.createScaledBitmap(dst, desiredWidth, desiredHeight, true)
|
||||||
// Desired size differs from the cropped size, resize the bitmap.
|
|
||||||
return Bitmap.createScaledBitmap(dst, desiredWidth, desiredHeight, true)
|
|
||||||
} catch (e: Exception) {
|
|
||||||
logE(e.stackTraceToString())
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return dst
|
return dst
|
||||||
|
|
|
@ -19,6 +19,7 @@ package org.oxycblt.auxio.detail
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
|
import android.view.MenuItem
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import androidx.core.view.children
|
import androidx.core.view.children
|
||||||
import androidx.navigation.fragment.findNavController
|
import androidx.navigation.fragment.findNavController
|
||||||
|
@ -54,23 +55,7 @@ class AlbumDetailFragment : DetailFragment(), AlbumDetailAdapter.Listener {
|
||||||
override fun onBindingCreated(binding: FragmentDetailBinding, savedInstanceState: Bundle?) {
|
override fun onBindingCreated(binding: FragmentDetailBinding, savedInstanceState: Bundle?) {
|
||||||
detailModel.setAlbumId(args.albumId)
|
detailModel.setAlbumId(args.albumId)
|
||||||
|
|
||||||
setupToolbar(unlikelyToBeNull(detailModel.currentAlbum.value), R.menu.menu_album_detail) {
|
setupToolbar(unlikelyToBeNull(detailModel.currentAlbum.value), R.menu.menu_album_detail)
|
||||||
itemId ->
|
|
||||||
when (itemId) {
|
|
||||||
R.id.action_play_next -> {
|
|
||||||
playbackModel.playNext(unlikelyToBeNull(detailModel.currentAlbum.value))
|
|
||||||
requireContext().showToast(R.string.lbl_queue_added)
|
|
||||||
true
|
|
||||||
}
|
|
||||||
R.id.action_queue_add -> {
|
|
||||||
playbackModel.addToQueue(unlikelyToBeNull(detailModel.currentAlbum.value))
|
|
||||||
requireContext().showToast(R.string.lbl_queue_added)
|
|
||||||
true
|
|
||||||
}
|
|
||||||
else -> false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
requireBinding().detailRecycler.apply {
|
requireBinding().detailRecycler.apply {
|
||||||
adapter = detailAdapter
|
adapter = detailAdapter
|
||||||
applySpans { pos ->
|
applySpans { pos ->
|
||||||
|
@ -86,6 +71,22 @@ class AlbumDetailFragment : DetailFragment(), AlbumDetailAdapter.Listener {
|
||||||
playbackModel.song.observe(viewLifecycleOwner, ::updateSong)
|
playbackModel.song.observe(viewLifecycleOwner, ::updateSong)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun onMenuItemClick(item: MenuItem): Boolean {
|
||||||
|
return when (item.itemId) {
|
||||||
|
R.id.action_play_next -> {
|
||||||
|
playbackModel.playNext(unlikelyToBeNull(detailModel.currentAlbum.value))
|
||||||
|
requireContext().showToast(R.string.lbl_queue_added)
|
||||||
|
true
|
||||||
|
}
|
||||||
|
R.id.action_queue_add -> {
|
||||||
|
playbackModel.addToQueue(unlikelyToBeNull(detailModel.currentAlbum.value))
|
||||||
|
requireContext().showToast(R.string.lbl_queue_added)
|
||||||
|
true
|
||||||
|
}
|
||||||
|
else -> false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
override fun onItemClick(item: Item) {
|
override fun onItemClick(item: Item) {
|
||||||
if (item is Song) {
|
if (item is Song) {
|
||||||
playbackModel.playSong(item, PlaybackMode.IN_ALBUM)
|
playbackModel.playSong(item, PlaybackMode.IN_ALBUM)
|
||||||
|
|
|
@ -18,6 +18,7 @@
|
||||||
package org.oxycblt.auxio.detail
|
package org.oxycblt.auxio.detail
|
||||||
|
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
|
import android.view.MenuItem
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import androidx.navigation.fragment.findNavController
|
import androidx.navigation.fragment.findNavController
|
||||||
import androidx.navigation.fragment.navArgs
|
import androidx.navigation.fragment.navArgs
|
||||||
|
@ -69,6 +70,8 @@ class ArtistDetailFragment : DetailFragment(), DetailAdapter.Listener {
|
||||||
playbackModel.parent.observe(viewLifecycleOwner, ::updateParent)
|
playbackModel.parent.observe(viewLifecycleOwner, ::updateParent)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun onMenuItemClick(item: MenuItem): Boolean = false
|
||||||
|
|
||||||
override fun onItemClick(item: Item) {
|
override fun onItemClick(item: Item) {
|
||||||
when (item) {
|
when (item) {
|
||||||
is Song -> playbackModel.playSong(item, PlaybackMode.IN_ARTIST)
|
is Song -> playbackModel.playSong(item, PlaybackMode.IN_ARTIST)
|
||||||
|
|
|
@ -46,11 +46,11 @@ class DetailAppBarLayout
|
||||||
@JvmOverloads
|
@JvmOverloads
|
||||||
constructor(context: Context, attrs: AttributeSet? = null, @AttrRes defStyleAttr: Int = 0) :
|
constructor(context: Context, attrs: AttributeSet? = null, @AttrRes defStyleAttr: Int = 0) :
|
||||||
EdgeAppBarLayout(context, attrs, defStyleAttr) {
|
EdgeAppBarLayout(context, attrs, defStyleAttr) {
|
||||||
private var mTitleView: AppCompatTextView? = null
|
private var titleView: AppCompatTextView? = null
|
||||||
private var mRecycler: RecyclerView? = null
|
private var recycler: RecyclerView? = null
|
||||||
|
|
||||||
private var titleShown: Boolean? = null
|
private var titleShown: Boolean? = null
|
||||||
private var mTitleAnimator: ValueAnimator? = null
|
private var titleAnimator: ValueAnimator? = null
|
||||||
|
|
||||||
override fun onAttachedToWindow() {
|
override fun onAttachedToWindow() {
|
||||||
super.onAttachedToWindow()
|
super.onAttachedToWindow()
|
||||||
|
@ -58,7 +58,7 @@ constructor(context: Context, attrs: AttributeSet? = null, @AttrRes defStyleAttr
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun findTitleView(): AppCompatTextView? {
|
private fun findTitleView(): AppCompatTextView? {
|
||||||
val titleView = mTitleView
|
val titleView = titleView
|
||||||
if (titleView != null) {
|
if (titleView != null) {
|
||||||
return titleView
|
return titleView
|
||||||
}
|
}
|
||||||
|
@ -79,12 +79,12 @@ constructor(context: Context, attrs: AttributeSet? = null, @AttrRes defStyleAttr
|
||||||
}
|
}
|
||||||
|
|
||||||
newTitleView.alpha = 0f
|
newTitleView.alpha = 0f
|
||||||
mTitleView = newTitleView
|
this.titleView = newTitleView
|
||||||
return newTitleView
|
return newTitleView
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun findRecyclerView(): RecyclerView {
|
private fun findRecyclerView(): RecyclerView {
|
||||||
val recycler = mRecycler
|
val recycler = recycler
|
||||||
|
|
||||||
if (recycler != null) {
|
if (recycler != null) {
|
||||||
return recycler
|
return recycler
|
||||||
|
@ -92,7 +92,7 @@ constructor(context: Context, attrs: AttributeSet? = null, @AttrRes defStyleAttr
|
||||||
|
|
||||||
val newRecycler = (parent as ViewGroup).findViewById<RecyclerView>(liftOnScrollTargetViewId)
|
val newRecycler = (parent as ViewGroup).findViewById<RecyclerView>(liftOnScrollTargetViewId)
|
||||||
|
|
||||||
mRecycler = newRecycler
|
this.recycler = newRecycler
|
||||||
return newRecycler
|
return newRecycler
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -101,10 +101,10 @@ constructor(context: Context, attrs: AttributeSet? = null, @AttrRes defStyleAttr
|
||||||
|
|
||||||
titleShown = visible
|
titleShown = visible
|
||||||
|
|
||||||
val titleAnimator = mTitleAnimator
|
val titleAnimator = titleAnimator
|
||||||
if (titleAnimator != null) {
|
if (titleAnimator != null) {
|
||||||
titleAnimator.cancel()
|
titleAnimator.cancel()
|
||||||
mTitleAnimator = null
|
this.titleAnimator = null
|
||||||
}
|
}
|
||||||
|
|
||||||
val titleView = findTitleView()
|
val titleView = findTitleView()
|
||||||
|
@ -121,7 +121,7 @@ constructor(context: Context, attrs: AttributeSet? = null, @AttrRes defStyleAttr
|
||||||
|
|
||||||
if (titleView?.alpha == to) return
|
if (titleView?.alpha == to) return
|
||||||
|
|
||||||
mTitleAnimator =
|
this.titleAnimator =
|
||||||
ValueAnimator.ofFloat(from, to).apply {
|
ValueAnimator.ofFloat(from, to).apply {
|
||||||
addUpdateListener { titleView?.alpha = it.animatedValue as Float }
|
addUpdateListener { titleView?.alpha = it.animatedValue as Float }
|
||||||
|
|
||||||
|
|
|
@ -21,6 +21,7 @@ import android.view.LayoutInflater
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import androidx.annotation.MenuRes
|
import androidx.annotation.MenuRes
|
||||||
import androidx.appcompat.widget.PopupMenu
|
import androidx.appcompat.widget.PopupMenu
|
||||||
|
import androidx.appcompat.widget.Toolbar
|
||||||
import androidx.core.view.children
|
import androidx.core.view.children
|
||||||
import androidx.fragment.app.Fragment
|
import androidx.fragment.app.Fragment
|
||||||
import androidx.fragment.app.activityViewModels
|
import androidx.fragment.app.activityViewModels
|
||||||
|
@ -39,7 +40,8 @@ import org.oxycblt.auxio.util.unlikelyToBeNull
|
||||||
* A Base [Fragment] implementing the base features shared across all detail fragments.
|
* A Base [Fragment] implementing the base features shared across all detail fragments.
|
||||||
* @author OxygenCobalt
|
* @author OxygenCobalt
|
||||||
*/
|
*/
|
||||||
abstract class DetailFragment : ViewBindingFragment<FragmentDetailBinding>() {
|
abstract class DetailFragment :
|
||||||
|
ViewBindingFragment<FragmentDetailBinding>(), Toolbar.OnMenuItemClickListener {
|
||||||
protected val detailModel: DetailViewModel by activityViewModels()
|
protected val detailModel: DetailViewModel by activityViewModels()
|
||||||
protected val navModel: NavigationViewModel by activityViewModels()
|
protected val navModel: NavigationViewModel by activityViewModels()
|
||||||
protected val playbackModel: PlaybackViewModel by activityViewModels()
|
protected val playbackModel: PlaybackViewModel by activityViewModels()
|
||||||
|
@ -49,6 +51,7 @@ abstract class DetailFragment : ViewBindingFragment<FragmentDetailBinding>() {
|
||||||
|
|
||||||
override fun onDestroyBinding(binding: FragmentDetailBinding) {
|
override fun onDestroyBinding(binding: FragmentDetailBinding) {
|
||||||
super.onDestroyBinding(binding)
|
super.onDestroyBinding(binding)
|
||||||
|
binding.detailToolbar.setOnMenuItemClickListener(null)
|
||||||
binding.detailRecycler.adapter = null
|
binding.detailRecycler.adapter = null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -56,13 +59,8 @@ abstract class DetailFragment : ViewBindingFragment<FragmentDetailBinding>() {
|
||||||
* Shortcut method for doing setup of the detail toolbar.
|
* Shortcut method for doing setup of the detail toolbar.
|
||||||
* @param data Parent data to use as the toolbar title
|
* @param data Parent data to use as the toolbar title
|
||||||
* @param menuId Menu resource to use
|
* @param menuId Menu resource to use
|
||||||
* @param onMenuClick (Optional) a click listener for that menu
|
|
||||||
*/
|
*/
|
||||||
protected fun setupToolbar(
|
protected fun setupToolbar(data: MusicParent, @MenuRes menuId: Int = -1) {
|
||||||
data: MusicParent,
|
|
||||||
@MenuRes menuId: Int = -1,
|
|
||||||
onMenuClick: ((itemId: Int) -> Boolean)? = null
|
|
||||||
) {
|
|
||||||
requireBinding().detailToolbar.apply {
|
requireBinding().detailToolbar.apply {
|
||||||
title = data.resolveName(context)
|
title = data.resolveName(context)
|
||||||
|
|
||||||
|
@ -71,10 +69,7 @@ abstract class DetailFragment : ViewBindingFragment<FragmentDetailBinding>() {
|
||||||
}
|
}
|
||||||
|
|
||||||
setNavigationOnClickListener { findNavController().navigateUp() }
|
setNavigationOnClickListener { findNavController().navigateUp() }
|
||||||
|
setOnMenuItemClickListener(this@DetailFragment)
|
||||||
onMenuClick?.let { onClick ->
|
|
||||||
setOnMenuItemClickListener { item -> onClick(item.itemId) }
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -44,13 +44,13 @@ class DetailViewModel : ViewModel() {
|
||||||
private val musicStore = MusicStore.getInstance()
|
private val musicStore = MusicStore.getInstance()
|
||||||
private val settingsManager = SettingsManager.getInstance()
|
private val settingsManager = SettingsManager.getInstance()
|
||||||
|
|
||||||
private val mCurrentAlbum = MutableLiveData<Album?>()
|
private val _currentAlbum = MutableLiveData<Album?>()
|
||||||
val currentAlbum: LiveData<Album?>
|
val currentAlbum: LiveData<Album?>
|
||||||
get() = mCurrentAlbum
|
get() = _currentAlbum
|
||||||
|
|
||||||
private val mAlbumData = MutableLiveData(listOf<Item>())
|
private val _albumData = MutableLiveData(listOf<Item>())
|
||||||
val albumData: LiveData<List<Item>>
|
val albumData: LiveData<List<Item>>
|
||||||
get() = mAlbumData
|
get() = _albumData
|
||||||
|
|
||||||
var albumSort: Sort
|
var albumSort: Sort
|
||||||
get() = settingsManager.detailAlbumSort
|
get() = settingsManager.detailAlbumSort
|
||||||
|
@ -59,12 +59,12 @@ class DetailViewModel : ViewModel() {
|
||||||
currentAlbum.value?.let(::refreshAlbumData)
|
currentAlbum.value?.let(::refreshAlbumData)
|
||||||
}
|
}
|
||||||
|
|
||||||
private val mCurrentArtist = MutableLiveData<Artist?>()
|
private val _currentArtist = MutableLiveData<Artist?>()
|
||||||
val currentArtist: LiveData<Artist?>
|
val currentArtist: LiveData<Artist?>
|
||||||
get() = mCurrentArtist
|
get() = _currentArtist
|
||||||
|
|
||||||
private val mArtistData = MutableLiveData(listOf<Item>())
|
private val _artistData = MutableLiveData(listOf<Item>())
|
||||||
val artistData: LiveData<List<Item>> = mArtistData
|
val artistData: LiveData<List<Item>> = _artistData
|
||||||
|
|
||||||
var artistSort: Sort
|
var artistSort: Sort
|
||||||
get() = settingsManager.detailArtistSort
|
get() = settingsManager.detailArtistSort
|
||||||
|
@ -73,12 +73,12 @@ class DetailViewModel : ViewModel() {
|
||||||
currentArtist.value?.let(::refreshArtistData)
|
currentArtist.value?.let(::refreshArtistData)
|
||||||
}
|
}
|
||||||
|
|
||||||
private val mCurrentGenre = MutableLiveData<Genre?>()
|
private val _currentGenre = MutableLiveData<Genre?>()
|
||||||
val currentGenre: LiveData<Genre?>
|
val currentGenre: LiveData<Genre?>
|
||||||
get() = mCurrentGenre
|
get() = _currentGenre
|
||||||
|
|
||||||
private val mGenreData = MutableLiveData(listOf<Item>())
|
private val _genreData = MutableLiveData(listOf<Item>())
|
||||||
val genreData: LiveData<List<Item>> = mGenreData
|
val genreData: LiveData<List<Item>> = _genreData
|
||||||
|
|
||||||
var genreSort: Sort
|
var genreSort: Sort
|
||||||
get() = settingsManager.detailGenreSort
|
get() = settingsManager.detailGenreSort
|
||||||
|
@ -88,30 +88,30 @@ class DetailViewModel : ViewModel() {
|
||||||
}
|
}
|
||||||
|
|
||||||
fun setAlbumId(id: Long) {
|
fun setAlbumId(id: Long) {
|
||||||
if (mCurrentAlbum.value?.id == id) return
|
if (_currentAlbum.value?.id == id) return
|
||||||
val library = unlikelyToBeNull(musicStore.library)
|
val library = unlikelyToBeNull(musicStore.library)
|
||||||
val album =
|
val album =
|
||||||
requireNotNull(library.albums.find { it.id == id }) { "Invalid album id provided " }
|
requireNotNull(library.albums.find { it.id == id }) { "Invalid album id provided " }
|
||||||
|
|
||||||
mCurrentAlbum.value = album
|
_currentAlbum.value = album
|
||||||
refreshAlbumData(album)
|
refreshAlbumData(album)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun setArtistId(id: Long) {
|
fun setArtistId(id: Long) {
|
||||||
if (mCurrentArtist.value?.id == id) return
|
if (_currentArtist.value?.id == id) return
|
||||||
val library = unlikelyToBeNull(musicStore.library)
|
val library = unlikelyToBeNull(musicStore.library)
|
||||||
val artist =
|
val artist =
|
||||||
requireNotNull(library.artists.find { it.id == id }) { "Invalid artist id provided" }
|
requireNotNull(library.artists.find { it.id == id }) { "Invalid artist id provided" }
|
||||||
mCurrentArtist.value = artist
|
_currentArtist.value = artist
|
||||||
refreshArtistData(artist)
|
refreshArtistData(artist)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun setGenreId(id: Long) {
|
fun setGenreId(id: Long) {
|
||||||
if (mCurrentGenre.value?.id == id) return
|
if (_currentGenre.value?.id == id) return
|
||||||
val library = unlikelyToBeNull(musicStore.library)
|
val library = unlikelyToBeNull(musicStore.library)
|
||||||
val genre =
|
val genre =
|
||||||
requireNotNull(library.genres.find { it.id == id }) { "Invalid genre id provided" }
|
requireNotNull(library.genres.find { it.id == id }) { "Invalid genre id provided" }
|
||||||
mCurrentGenre.value = genre
|
_currentGenre.value = genre
|
||||||
refreshGenreData(genre)
|
refreshGenreData(genre)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -120,7 +120,7 @@ class DetailViewModel : ViewModel() {
|
||||||
val data = mutableListOf<Item>(genre)
|
val data = mutableListOf<Item>(genre)
|
||||||
data.add(SortHeader(-2, R.string.lbl_songs))
|
data.add(SortHeader(-2, R.string.lbl_songs))
|
||||||
data.addAll(genreSort.genre(genre))
|
data.addAll(genreSort.genre(genre))
|
||||||
mGenreData.value = data
|
_genreData.value = data
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun refreshArtistData(artist: Artist) {
|
private fun refreshArtistData(artist: Artist) {
|
||||||
|
@ -130,7 +130,7 @@ class DetailViewModel : ViewModel() {
|
||||||
data.addAll(Sort.ByYear(false).albums(artist.albums))
|
data.addAll(Sort.ByYear(false).albums(artist.albums))
|
||||||
data.add(SortHeader(-3, R.string.lbl_songs))
|
data.add(SortHeader(-3, R.string.lbl_songs))
|
||||||
data.addAll(artistSort.artist(artist))
|
data.addAll(artistSort.artist(artist))
|
||||||
mArtistData.value = data.toList()
|
_artistData.value = data.toList()
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun refreshAlbumData(album: Album) {
|
private fun refreshAlbumData(album: Album) {
|
||||||
|
@ -138,6 +138,6 @@ class DetailViewModel : ViewModel() {
|
||||||
val data = mutableListOf<Item>(album)
|
val data = mutableListOf<Item>(album)
|
||||||
data.add(SortHeader(id = -2, R.string.lbl_songs))
|
data.add(SortHeader(id = -2, R.string.lbl_songs))
|
||||||
data.addAll(albumSort.album(album))
|
data.addAll(albumSort.album(album))
|
||||||
mAlbumData.value = data
|
_albumData.value = data
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -18,6 +18,7 @@
|
||||||
package org.oxycblt.auxio.detail
|
package org.oxycblt.auxio.detail
|
||||||
|
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
|
import android.view.MenuItem
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import androidx.navigation.fragment.findNavController
|
import androidx.navigation.fragment.findNavController
|
||||||
import androidx.navigation.fragment.navArgs
|
import androidx.navigation.fragment.navArgs
|
||||||
|
@ -65,6 +66,8 @@ class GenreDetailFragment : DetailFragment(), DetailAdapter.Listener {
|
||||||
playbackModel.song.observe(viewLifecycleOwner, ::updateSong)
|
playbackModel.song.observe(viewLifecycleOwner, ::updateSong)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun onMenuItemClick(item: MenuItem): Boolean = false
|
||||||
|
|
||||||
override fun onItemClick(item: Item) {
|
override fun onItemClick(item: Item) {
|
||||||
when (item) {
|
when (item) {
|
||||||
is Song -> playbackModel.playSong(item, PlaybackMode.IN_GENRE)
|
is Song -> playbackModel.playSong(item, PlaybackMode.IN_GENRE)
|
||||||
|
|
|
@ -27,12 +27,12 @@ import org.oxycblt.auxio.databinding.ItemAlbumSongBinding
|
||||||
import org.oxycblt.auxio.databinding.ItemDetailBinding
|
import org.oxycblt.auxio.databinding.ItemDetailBinding
|
||||||
import org.oxycblt.auxio.music.Album
|
import org.oxycblt.auxio.music.Album
|
||||||
import org.oxycblt.auxio.music.Song
|
import org.oxycblt.auxio.music.Song
|
||||||
import org.oxycblt.auxio.music.toDuration
|
|
||||||
import org.oxycblt.auxio.ui.BindingViewHolder
|
import org.oxycblt.auxio.ui.BindingViewHolder
|
||||||
import org.oxycblt.auxio.ui.Item
|
import org.oxycblt.auxio.ui.Item
|
||||||
import org.oxycblt.auxio.ui.MenuItemListener
|
import org.oxycblt.auxio.ui.MenuItemListener
|
||||||
import org.oxycblt.auxio.ui.SimpleItemCallback
|
import org.oxycblt.auxio.ui.SimpleItemCallback
|
||||||
import org.oxycblt.auxio.util.context
|
import org.oxycblt.auxio.util.context
|
||||||
|
import org.oxycblt.auxio.util.formatDuration
|
||||||
import org.oxycblt.auxio.util.getPluralSafe
|
import org.oxycblt.auxio.util.getPluralSafe
|
||||||
import org.oxycblt.auxio.util.inflater
|
import org.oxycblt.auxio.util.inflater
|
||||||
import org.oxycblt.auxio.util.textSafe
|
import org.oxycblt.auxio.util.textSafe
|
||||||
|
@ -185,7 +185,7 @@ private class AlbumSongViewHolder private constructor(private val binding: ItemA
|
||||||
}
|
}
|
||||||
|
|
||||||
binding.songName.textSafe = item.resolveName(binding.context)
|
binding.songName.textSafe = item.resolveName(binding.context)
|
||||||
binding.songDuration.textSafe = item.seconds.toDuration(false)
|
binding.songDuration.textSafe = item.seconds.formatDuration(false)
|
||||||
|
|
||||||
binding.root.apply {
|
binding.root.apply {
|
||||||
setOnClickListener { listener.onItemClick(item) }
|
setOnClickListener { listener.onItemClick(item) }
|
||||||
|
|
|
@ -20,6 +20,7 @@ package org.oxycblt.auxio.home
|
||||||
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 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
|
||||||
|
@ -60,7 +61,7 @@ import org.oxycblt.auxio.util.unlikelyToBeNull
|
||||||
*
|
*
|
||||||
* TODO: Add duration and song count sorts
|
* TODO: Add duration and song count sorts
|
||||||
*/
|
*/
|
||||||
class HomeFragment : ViewBindingFragment<FragmentHomeBinding>() {
|
class HomeFragment : ViewBindingFragment<FragmentHomeBinding>(), Toolbar.OnMenuItemClickListener {
|
||||||
private val playbackModel: PlaybackViewModel by activityViewModels()
|
private val playbackModel: PlaybackViewModel by activityViewModels()
|
||||||
private val navModel: NavigationViewModel by activityViewModels()
|
private val navModel: NavigationViewModel by activityViewModels()
|
||||||
private val homeModel: HomeViewModel by activityViewModels()
|
private val homeModel: HomeViewModel by activityViewModels()
|
||||||
|
@ -73,11 +74,7 @@ class HomeFragment : ViewBindingFragment<FragmentHomeBinding>() {
|
||||||
|
|
||||||
binding.homeToolbar.apply {
|
binding.homeToolbar.apply {
|
||||||
sortItem = menu.findItem(R.id.submenu_sorting)
|
sortItem = menu.findItem(R.id.submenu_sorting)
|
||||||
|
setOnMenuItemClickListener(this@HomeFragment)
|
||||||
setOnMenuItemClickListener { item ->
|
|
||||||
onMenuClick(item)
|
|
||||||
true
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
binding.homePager.apply {
|
binding.homePager.apply {
|
||||||
|
@ -103,7 +100,7 @@ class HomeFragment : ViewBindingFragment<FragmentHomeBinding>() {
|
||||||
|
|
||||||
// --- VIEWMODEL SETUP ---
|
// --- VIEWMODEL SETUP ---
|
||||||
|
|
||||||
homeModel.fastScrolling.observe(viewLifecycleOwner, ::updateFastScrolling)
|
homeModel.isFastScrolling.observe(viewLifecycleOwner, ::updateFastScrolling)
|
||||||
homeModel.currentTab.observe(viewLifecycleOwner) { tab -> updateCurrentTab(sortItem, tab) }
|
homeModel.currentTab.observe(viewLifecycleOwner) { tab -> updateCurrentTab(sortItem, tab) }
|
||||||
homeModel.recreateTabs.observe(viewLifecycleOwner, ::handleRecreateTabs)
|
homeModel.recreateTabs.observe(viewLifecycleOwner, ::handleRecreateTabs)
|
||||||
|
|
||||||
|
@ -111,7 +108,12 @@ class HomeFragment : ViewBindingFragment<FragmentHomeBinding>() {
|
||||||
navModel.exploreNavigationItem.observe(viewLifecycleOwner, ::handleNavigation)
|
navModel.exploreNavigationItem.observe(viewLifecycleOwner, ::handleNavigation)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun onMenuClick(item: MenuItem) {
|
override fun onDestroyBinding(binding: FragmentHomeBinding) {
|
||||||
|
super.onDestroyBinding(binding)
|
||||||
|
binding.homeToolbar.setOnMenuItemClickListener(null)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onMenuItemClick(item: MenuItem): Boolean {
|
||||||
when (item.itemId) {
|
when (item.itemId) {
|
||||||
R.id.action_search -> {
|
R.id.action_search -> {
|
||||||
logD("Navigating to search")
|
logD("Navigating to search")
|
||||||
|
@ -147,6 +149,8 @@ class HomeFragment : ViewBindingFragment<FragmentHomeBinding>() {
|
||||||
.assignId(item.itemId)))
|
.assignId(item.itemId)))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun updateFastScrolling(isFastScrolling: Boolean) {
|
private fun updateFastScrolling(isFastScrolling: Boolean) {
|
||||||
|
|
|
@ -40,21 +40,21 @@ class HomeViewModel : ViewModel(), SettingsManager.Callback, MusicStore.Callback
|
||||||
private val musicStore = MusicStore.getInstance()
|
private val musicStore = MusicStore.getInstance()
|
||||||
private val settingsManager = SettingsManager.getInstance()
|
private val settingsManager = SettingsManager.getInstance()
|
||||||
|
|
||||||
private val mSongs = MutableLiveData(listOf<Song>())
|
private val _songs = MutableLiveData(listOf<Song>())
|
||||||
val songs: LiveData<List<Song>>
|
val songs: LiveData<List<Song>>
|
||||||
get() = mSongs
|
get() = _songs
|
||||||
|
|
||||||
private val mAlbums = MutableLiveData(listOf<Album>())
|
private val _albums = MutableLiveData(listOf<Album>())
|
||||||
val albums: LiveData<List<Album>>
|
val albums: LiveData<List<Album>>
|
||||||
get() = mAlbums
|
get() = _albums
|
||||||
|
|
||||||
private val mArtists = MutableLiveData(listOf<Artist>())
|
private val _artists = MutableLiveData(listOf<Artist>())
|
||||||
val artists: LiveData<List<Artist>>
|
val artists: LiveData<List<Artist>>
|
||||||
get() = mArtists
|
get() = _artists
|
||||||
|
|
||||||
private val mGenres = MutableLiveData(listOf<Genre>())
|
private val _genres = MutableLiveData(listOf<Genre>())
|
||||||
val genres: LiveData<List<Genre>>
|
val genres: LiveData<List<Genre>>
|
||||||
get() = mGenres
|
get() = _genres
|
||||||
|
|
||||||
var tabs: List<DisplayMode> = visibleTabs
|
var tabs: List<DisplayMode> = visibleTabs
|
||||||
private set
|
private set
|
||||||
|
@ -63,18 +63,18 @@ class HomeViewModel : ViewModel(), SettingsManager.Callback, MusicStore.Callback
|
||||||
private val visibleTabs: List<DisplayMode>
|
private val visibleTabs: List<DisplayMode>
|
||||||
get() = settingsManager.libTabs.filterIsInstance<Tab.Visible>().map { it.mode }
|
get() = settingsManager.libTabs.filterIsInstance<Tab.Visible>().map { it.mode }
|
||||||
|
|
||||||
private val mCurrentTab = MutableLiveData(tabs[0])
|
private val _currentTab = MutableLiveData(tabs[0])
|
||||||
val currentTab: LiveData<DisplayMode> = mCurrentTab
|
val currentTab: LiveData<DisplayMode> = _currentTab
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Marker to recreate all library tabs, usually initiated by a settings change. When this flag
|
* Marker to recreate all library tabs, usually initiated by a settings change. When this flag
|
||||||
* is set, all tabs (and their respective viewpager fragments) will be recreated from scratch.
|
* is set, all tabs (and their respective viewpager fragments) will be recreated from scratch.
|
||||||
*/
|
*/
|
||||||
private val mRecreateTabs = MutableLiveData(false)
|
private val _shouldRecreateTabs = MutableLiveData(false)
|
||||||
val recreateTabs: LiveData<Boolean> = mRecreateTabs
|
val recreateTabs: LiveData<Boolean> = _shouldRecreateTabs
|
||||||
|
|
||||||
private val mFastScrolling = MutableLiveData(false)
|
private val _isFastScrolling = MutableLiveData(false)
|
||||||
val fastScrolling: LiveData<Boolean> = mFastScrolling
|
val isFastScrolling: LiveData<Boolean> = _isFastScrolling
|
||||||
|
|
||||||
init {
|
init {
|
||||||
musicStore.addCallback(this)
|
musicStore.addCallback(this)
|
||||||
|
@ -84,11 +84,11 @@ class HomeViewModel : ViewModel(), SettingsManager.Callback, MusicStore.Callback
|
||||||
/** Update the current tab based off of the new ViewPager position. */
|
/** Update the current tab based off of the new ViewPager position. */
|
||||||
fun updateCurrentTab(pos: Int) {
|
fun updateCurrentTab(pos: Int) {
|
||||||
logD("Updating current tab to ${tabs[pos]}")
|
logD("Updating current tab to ${tabs[pos]}")
|
||||||
mCurrentTab.value = tabs[pos]
|
_currentTab.value = tabs[pos]
|
||||||
}
|
}
|
||||||
|
|
||||||
fun finishRecreateTabs() {
|
fun finishRecreateTabs() {
|
||||||
mRecreateTabs.value = false
|
_shouldRecreateTabs.value = false
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getSortForDisplay(displayMode: DisplayMode): Sort {
|
fun getSortForDisplay(displayMode: DisplayMode): Sort {
|
||||||
|
@ -102,23 +102,23 @@ class HomeViewModel : ViewModel(), SettingsManager.Callback, MusicStore.Callback
|
||||||
|
|
||||||
/** Update the currently displayed item's [Sort]. */
|
/** Update the currently displayed item's [Sort]. */
|
||||||
fun updateCurrentSort(sort: Sort) {
|
fun updateCurrentSort(sort: Sort) {
|
||||||
logD("Updating ${mCurrentTab.value} sort to $sort")
|
logD("Updating ${_currentTab.value} sort to $sort")
|
||||||
when (mCurrentTab.value) {
|
when (_currentTab.value) {
|
||||||
DisplayMode.SHOW_SONGS -> {
|
DisplayMode.SHOW_SONGS -> {
|
||||||
settingsManager.libSongSort = sort
|
settingsManager.libSongSort = sort
|
||||||
mSongs.value = sort.songs(unlikelyToBeNull(mSongs.value))
|
_songs.value = sort.songs(unlikelyToBeNull(_songs.value))
|
||||||
}
|
}
|
||||||
DisplayMode.SHOW_ALBUMS -> {
|
DisplayMode.SHOW_ALBUMS -> {
|
||||||
settingsManager.libAlbumSort = sort
|
settingsManager.libAlbumSort = sort
|
||||||
mAlbums.value = sort.albums(unlikelyToBeNull(mAlbums.value))
|
_albums.value = sort.albums(unlikelyToBeNull(_albums.value))
|
||||||
}
|
}
|
||||||
DisplayMode.SHOW_ARTISTS -> {
|
DisplayMode.SHOW_ARTISTS -> {
|
||||||
settingsManager.libArtistSort = sort
|
settingsManager.libArtistSort = sort
|
||||||
mArtists.value = sort.artists(unlikelyToBeNull(mArtists.value))
|
_artists.value = sort.artists(unlikelyToBeNull(_artists.value))
|
||||||
}
|
}
|
||||||
DisplayMode.SHOW_GENRES -> {
|
DisplayMode.SHOW_GENRES -> {
|
||||||
settingsManager.libGenreSort = sort
|
settingsManager.libGenreSort = sort
|
||||||
mGenres.value = sort.genres(unlikelyToBeNull(mGenres.value))
|
_genres.value = sort.genres(unlikelyToBeNull(_genres.value))
|
||||||
}
|
}
|
||||||
else -> {}
|
else -> {}
|
||||||
}
|
}
|
||||||
|
@ -129,7 +129,7 @@ class HomeViewModel : ViewModel(), SettingsManager.Callback, MusicStore.Callback
|
||||||
* begins to fast scroll.
|
* begins to fast scroll.
|
||||||
*/
|
*/
|
||||||
fun updateFastScrolling(scrolling: Boolean) {
|
fun updateFastScrolling(scrolling: Boolean) {
|
||||||
mFastScrolling.value = scrolling
|
_isFastScrolling.value = scrolling
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- OVERRIDES ---
|
// --- OVERRIDES ---
|
||||||
|
@ -137,16 +137,16 @@ class HomeViewModel : ViewModel(), SettingsManager.Callback, MusicStore.Callback
|
||||||
override fun onMusicUpdate(response: MusicStore.Response) {
|
override fun onMusicUpdate(response: MusicStore.Response) {
|
||||||
if (response is MusicStore.Response.Ok) {
|
if (response is MusicStore.Response.Ok) {
|
||||||
val library = response.library
|
val library = response.library
|
||||||
mSongs.value = settingsManager.libSongSort.songs(library.songs)
|
_songs.value = settingsManager.libSongSort.songs(library.songs)
|
||||||
mAlbums.value = settingsManager.libAlbumSort.albums(library.albums)
|
_albums.value = settingsManager.libAlbumSort.albums(library.albums)
|
||||||
mArtists.value = settingsManager.libArtistSort.artists(library.artists)
|
_artists.value = settingsManager.libArtistSort.artists(library.artists)
|
||||||
mGenres.value = settingsManager.libGenreSort.genres(library.genres)
|
_genres.value = settingsManager.libGenreSort.genres(library.genres)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onLibTabsUpdate(libTabs: Array<Tab>) {
|
override fun onLibTabsUpdate(libTabs: Array<Tab>) {
|
||||||
tabs = visibleTabs
|
tabs = visibleTabs
|
||||||
mRecreateTabs.value = true
|
_shouldRecreateTabs.value = true
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onCleared() {
|
override fun onCleared() {
|
||||||
|
|
|
@ -106,7 +106,7 @@ object Indexer {
|
||||||
private val Context.contentResolverSafe: ContentResolver
|
private val Context.contentResolverSafe: ContentResolver
|
||||||
get() = applicationContext.contentResolver
|
get() = applicationContext.contentResolver
|
||||||
|
|
||||||
fun run(context: Context): MusicStore.Library? {
|
fun index(context: Context): MusicStore.Library? {
|
||||||
val songs = loadSongs(context)
|
val songs = loadSongs(context)
|
||||||
if (songs.isEmpty()) return null
|
if (songs.isEmpty()) return null
|
||||||
|
|
||||||
|
@ -116,14 +116,12 @@ object Indexer {
|
||||||
|
|
||||||
// Sanity check: Ensure that all songs are linked up to albums/artists/genres.
|
// Sanity check: Ensure that all songs are linked up to albums/artists/genres.
|
||||||
for (song in songs) {
|
for (song in songs) {
|
||||||
if (song.internalIsMissingAlbum ||
|
if (song._isMissingAlbum || song._isMissingArtist || song._isMissingGenre) {
|
||||||
song.internalIsMissingArtist ||
|
|
||||||
song.internalIsMissingGenre) {
|
|
||||||
throw IllegalStateException(
|
throw IllegalStateException(
|
||||||
"Found malformed song: ${song.rawName} [" +
|
"Found malformed song: ${song.rawName} [" +
|
||||||
"album: ${!song.internalIsMissingAlbum} " +
|
"album: ${!song._isMissingAlbum} " +
|
||||||
"artist: ${!song.internalIsMissingArtist} " +
|
"artist: ${!song._isMissingArtist} " +
|
||||||
"genre: ${!song.internalIsMissingGenre}]")
|
"genre: ${!song._isMissingGenre}]")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -242,9 +240,9 @@ object Indexer {
|
||||||
songs
|
songs
|
||||||
.distinctBy {
|
.distinctBy {
|
||||||
it.rawName to
|
it.rawName to
|
||||||
it.internalMediaStoreAlbumName to
|
it._mediaStoreAlbumName to
|
||||||
it.internalMediaStoreArtistName to
|
it._mediaStoreArtistName to
|
||||||
it.internalMediaStoreAlbumArtistName to
|
it._mediaStoreAlbumArtistName to
|
||||||
it.track to
|
it.track to
|
||||||
it.duration
|
it.duration
|
||||||
}
|
}
|
||||||
|
@ -270,7 +268,7 @@ object Indexer {
|
||||||
*/
|
*/
|
||||||
private fun buildAlbums(songs: List<Song>): List<Album> {
|
private fun buildAlbums(songs: List<Song>): List<Album> {
|
||||||
val albums = mutableListOf<Album>()
|
val albums = mutableListOf<Album>()
|
||||||
val songsByAlbum = songs.groupBy { it.internalAlbumGroupingId }
|
val songsByAlbum = songs.groupBy { it._albumGroupingId }
|
||||||
|
|
||||||
for (entry in songsByAlbum) {
|
for (entry in songsByAlbum) {
|
||||||
val albumSongs = entry.value
|
val albumSongs = entry.value
|
||||||
|
@ -289,13 +287,13 @@ object Indexer {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
val albumName = templateSong.internalMediaStoreAlbumName
|
val albumName = templateSong._mediaStoreAlbumName
|
||||||
val albumYear = templateSong.internalMediaStoreYear
|
val albumYear = templateSong._mediaStoreYear
|
||||||
val albumCoverUri =
|
val albumCoverUri =
|
||||||
ContentUris.withAppendedId(
|
ContentUris.withAppendedId(
|
||||||
Uri.parse("content://media/external/audio/albumart"),
|
Uri.parse("content://media/external/audio/albumart"),
|
||||||
templateSong.internalMediaStoreAlbumId)
|
templateSong._mediaStoreAlbumId)
|
||||||
val artistName = templateSong.internalGroupingArtistName
|
val artistName = templateSong._artistGroupingName
|
||||||
|
|
||||||
albums.add(
|
albums.add(
|
||||||
Album(
|
Album(
|
||||||
|
@ -318,14 +316,14 @@ object Indexer {
|
||||||
*/
|
*/
|
||||||
private fun buildArtists(albums: List<Album>): List<Artist> {
|
private fun buildArtists(albums: List<Album>): List<Artist> {
|
||||||
val artists = mutableListOf<Artist>()
|
val artists = mutableListOf<Artist>()
|
||||||
val albumsByArtist = albums.groupBy { it.internalArtistGroupingId }
|
val albumsByArtist = albums.groupBy { it._artistGroupingId }
|
||||||
|
|
||||||
for (entry in albumsByArtist) {
|
for (entry in albumsByArtist) {
|
||||||
val templateAlbum = entry.value[0]
|
val templateAlbum = entry.value[0]
|
||||||
val artistName =
|
val artistName =
|
||||||
when (templateAlbum.internalGroupingArtistName) {
|
when (templateAlbum._artistGroupingName) {
|
||||||
MediaStore.UNKNOWN_STRING -> null
|
MediaStore.UNKNOWN_STRING -> null
|
||||||
else -> templateAlbum.internalGroupingArtistName
|
else -> templateAlbum._artistGroupingName
|
||||||
}
|
}
|
||||||
val artistAlbums = entry.value
|
val artistAlbums = entry.value
|
||||||
|
|
||||||
|
@ -366,7 +364,7 @@ object Indexer {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
val songsWithoutGenres = songs.filter { it.internalIsMissingGenre }
|
val songsWithoutGenres = songs.filter { it._isMissingGenre }
|
||||||
if (songsWithoutGenres.isNotEmpty()) {
|
if (songsWithoutGenres.isNotEmpty()) {
|
||||||
// Songs that don't have a genre will be thrown into an unknown genre.
|
// Songs that don't have a genre will be thrown into an unknown genre.
|
||||||
val unknownGenre = Genre(null, songsWithoutGenres)
|
val unknownGenre = Genre(null, songsWithoutGenres)
|
||||||
|
@ -398,9 +396,7 @@ object Indexer {
|
||||||
|
|
||||||
while (cursor.moveToNext()) {
|
while (cursor.moveToNext()) {
|
||||||
val id = cursor.getLong(idIndex)
|
val id = cursor.getLong(idIndex)
|
||||||
songs.find { it.internalMediaStoreId == id }?.let { song ->
|
songs.find { it._mediaStoreId == id }?.let { song -> genreSongs.add(song) }
|
||||||
genreSongs.add(song)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -24,6 +24,7 @@ import android.provider.MediaStore
|
||||||
import androidx.core.text.isDigitsOnly
|
import androidx.core.text.isDigitsOnly
|
||||||
import org.oxycblt.auxio.R
|
import org.oxycblt.auxio.R
|
||||||
import org.oxycblt.auxio.ui.Item
|
import org.oxycblt.auxio.ui.Item
|
||||||
|
import org.oxycblt.auxio.util.formatDuration
|
||||||
import org.oxycblt.auxio.util.unlikelyToBeNull
|
import org.oxycblt.auxio.util.unlikelyToBeNull
|
||||||
|
|
||||||
// --- MUSIC MODELS ---
|
// --- MUSIC MODELS ---
|
||||||
|
@ -59,17 +60,17 @@ data class Song(
|
||||||
/** The track number of this song, null if there isn't any. */
|
/** The track number of this song, null if there isn't any. */
|
||||||
val track: Int?,
|
val track: Int?,
|
||||||
/** Internal field. Do not use. */
|
/** Internal field. Do not use. */
|
||||||
val internalMediaStoreId: Long,
|
val _mediaStoreId: Long,
|
||||||
/** Internal field. Do not use. */
|
/** Internal field. Do not use. */
|
||||||
val internalMediaStoreYear: Int?,
|
val _mediaStoreYear: Int?,
|
||||||
/** Internal field. Do not use. */
|
/** Internal field. Do not use. */
|
||||||
val internalMediaStoreAlbumName: String,
|
val _mediaStoreAlbumName: String,
|
||||||
/** Internal field. Do not use. */
|
/** Internal field. Do not use. */
|
||||||
val internalMediaStoreAlbumId: Long,
|
val _mediaStoreAlbumId: Long,
|
||||||
/** Internal field. Do not use. */
|
/** Internal field. Do not use. */
|
||||||
val internalMediaStoreArtistName: String?,
|
val _mediaStoreArtistName: String?,
|
||||||
/** Internal field. Do not use. */
|
/** Internal field. Do not use. */
|
||||||
val internalMediaStoreAlbumArtistName: String?,
|
val _mediaStoreAlbumArtistName: String?,
|
||||||
) : Music() {
|
) : Music() {
|
||||||
override val id: Long
|
override val id: Long
|
||||||
get() {
|
get() {
|
||||||
|
@ -89,68 +90,65 @@ data class Song(
|
||||||
/** The URI for this song. */
|
/** The URI for this song. */
|
||||||
val uri: Uri
|
val uri: Uri
|
||||||
get() =
|
get() =
|
||||||
ContentUris.withAppendedId(
|
ContentUris.withAppendedId(MediaStore.Audio.Media.EXTERNAL_CONTENT_URI, _mediaStoreId)
|
||||||
MediaStore.Audio.Media.EXTERNAL_CONTENT_URI, internalMediaStoreId)
|
|
||||||
/** The duration of this song, in seconds (rounded down) */
|
/** The duration of this song, in seconds (rounded down) */
|
||||||
val seconds: Long
|
val seconds: Long
|
||||||
get() = duration / 1000
|
get() = duration / 1000
|
||||||
|
|
||||||
private var mAlbum: Album? = null
|
private var _album: Album? = null
|
||||||
/** The album of this song. */
|
/** The album of this song. */
|
||||||
val album: Album
|
val album: Album
|
||||||
get() = unlikelyToBeNull(mAlbum)
|
get() = unlikelyToBeNull(_album)
|
||||||
|
|
||||||
private var mGenre: Genre? = null
|
private var _genre: Genre? = null
|
||||||
/** The genre of this song. Will be an "unknown genre" if the song does not have any. */
|
/** The genre of this song. Will be an "unknown genre" if the song does not have any. */
|
||||||
val genre: Genre
|
val genre: Genre
|
||||||
get() = unlikelyToBeNull(mGenre)
|
get() = unlikelyToBeNull(_genre)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The raw artist name for this song in particular. First uses the artist tag, and then falls
|
* The raw artist name for this song in particular. First uses the artist tag, and then falls
|
||||||
* back to the album artist tag (i.e parent artist name). Null if name is unknown.
|
* back to the album artist tag (i.e parent artist name). Null if name is unknown.
|
||||||
*/
|
*/
|
||||||
val individualRawArtistName: String?
|
val individualRawArtistName: String?
|
||||||
get() = internalMediaStoreArtistName ?: album.artist.rawName
|
get() = _mediaStoreArtistName ?: album.artist.rawName
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Resolve the artist name for this song in particular. First uses the artist tag, and then
|
* Resolve the artist name for this song in particular. First uses the artist tag, and then
|
||||||
* falls back to the album artist tag (i.e parent artist name)
|
* falls back to the album artist tag (i.e parent artist name)
|
||||||
*/
|
*/
|
||||||
fun resolveIndividualArtistName(context: Context) =
|
fun resolveIndividualArtistName(context: Context) =
|
||||||
internalMediaStoreArtistName ?: album.artist.resolveName(context)
|
_mediaStoreArtistName ?: album.artist.resolveName(context)
|
||||||
|
|
||||||
/** Internal field. Do not use. */
|
/** Internal field. Do not use. */
|
||||||
val internalAlbumGroupingId: Long
|
val _albumGroupingId: Long
|
||||||
get() {
|
get() {
|
||||||
var result = internalGroupingArtistName.lowercase().hashCode().toLong()
|
var result = _artistGroupingName.lowercase().hashCode().toLong()
|
||||||
result = 31 * result + internalMediaStoreAlbumName.lowercase().hashCode()
|
result = 31 * result + _mediaStoreAlbumName.lowercase().hashCode()
|
||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Internal field. Do not use. */
|
/** Internal field. Do not use. */
|
||||||
val internalGroupingArtistName: String
|
val _artistGroupingName: String
|
||||||
get() =
|
get() = _mediaStoreAlbumArtistName ?: _mediaStoreArtistName ?: MediaStore.UNKNOWN_STRING
|
||||||
internalMediaStoreAlbumArtistName
|
|
||||||
?: internalMediaStoreArtistName ?: MediaStore.UNKNOWN_STRING
|
|
||||||
|
|
||||||
/** Internal field. Do not use. */
|
/** Internal field. Do not use. */
|
||||||
val internalIsMissingAlbum: Boolean
|
val _isMissingAlbum: Boolean
|
||||||
get() = mAlbum == null
|
get() = _album == null
|
||||||
/** Internal field. Do not use. */
|
/** Internal field. Do not use. */
|
||||||
val internalIsMissingArtist: Boolean
|
val _isMissingArtist: Boolean
|
||||||
get() = mAlbum?.internalIsMissingArtist ?: true
|
get() = _album?._isMissingArtist ?: true
|
||||||
/** Internal field. Do not use. */
|
/** Internal field. Do not use. */
|
||||||
val internalIsMissingGenre: Boolean
|
val _isMissingGenre: Boolean
|
||||||
get() = mGenre == null
|
get() = _genre == null
|
||||||
|
|
||||||
/** Internal method. Do not use. */
|
/** Internal method. Do not use. */
|
||||||
fun internalLinkAlbum(album: Album) {
|
fun _linkAlbum(album: Album) {
|
||||||
mAlbum = album
|
_album = album
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Internal method. Do not use. */
|
/** Internal method. Do not use. */
|
||||||
fun internalLinkGenre(genre: Genre) {
|
fun _linkGenre(genre: Genre) {
|
||||||
mGenre = genre
|
_genre = genre
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -164,11 +162,11 @@ data class Album(
|
||||||
/** The songs of this album. */
|
/** The songs of this album. */
|
||||||
val songs: List<Song>,
|
val songs: List<Song>,
|
||||||
/** Internal field. Do not use. */
|
/** Internal field. Do not use. */
|
||||||
val internalGroupingArtistName: String,
|
val _artistGroupingName: String,
|
||||||
) : MusicParent() {
|
) : MusicParent() {
|
||||||
init {
|
init {
|
||||||
for (song in songs) {
|
for (song in songs) {
|
||||||
song.internalLinkAlbum(this)
|
song._linkAlbum(this)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -187,24 +185,24 @@ data class Album(
|
||||||
|
|
||||||
/** The formatted total duration of this album */
|
/** The formatted total duration of this album */
|
||||||
val totalDuration: String
|
val totalDuration: String
|
||||||
get() = songs.sumOf { it.seconds }.toDuration(false)
|
get() = songs.sumOf { it.seconds }.formatDuration(false)
|
||||||
|
|
||||||
private var mArtist: Artist? = null
|
private var _artist: Artist? = null
|
||||||
/** The parent artist of this album. */
|
/** The parent artist of this album. */
|
||||||
val artist: Artist
|
val artist: Artist
|
||||||
get() = unlikelyToBeNull(mArtist)
|
get() = unlikelyToBeNull(_artist)
|
||||||
|
|
||||||
/** Internal field. Do not use. */
|
/** Internal field. Do not use. */
|
||||||
val internalArtistGroupingId: Long
|
val _artistGroupingId: Long
|
||||||
get() = internalGroupingArtistName.lowercase().hashCode().toLong()
|
get() = _artistGroupingName.lowercase().hashCode().toLong()
|
||||||
|
|
||||||
/** Internal field. Do not use. */
|
/** Internal field. Do not use. */
|
||||||
val internalIsMissingArtist: Boolean
|
val _isMissingArtist: Boolean
|
||||||
get() = mArtist == null
|
get() = _artist == null
|
||||||
|
|
||||||
/** Internal method. Do not use. */
|
/** Internal method. Do not use. */
|
||||||
fun internalLinkArtist(artist: Artist) {
|
fun _linkArtist(artist: Artist) {
|
||||||
mArtist = artist
|
_artist = artist
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -219,7 +217,7 @@ data class Artist(
|
||||||
) : MusicParent() {
|
) : MusicParent() {
|
||||||
init {
|
init {
|
||||||
for (album in albums) {
|
for (album in albums) {
|
||||||
album.internalLinkArtist(this)
|
album._linkArtist(this)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -239,7 +237,7 @@ data class Artist(
|
||||||
data class Genre(override val rawName: String?, val songs: List<Song>) : MusicParent() {
|
data class Genre(override val rawName: String?, val songs: List<Song>) : MusicParent() {
|
||||||
init {
|
init {
|
||||||
for (song in songs) {
|
for (song in songs) {
|
||||||
song.internalLinkGenre(this)
|
song._linkGenre(this)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -254,7 +252,7 @@ data class Genre(override val rawName: String?, val songs: List<Song>) : MusicPa
|
||||||
|
|
||||||
/** The formatted total duration of this genre */
|
/** The formatted total duration of this genre */
|
||||||
val totalDuration: String
|
val totalDuration: String
|
||||||
get() = songs.sumOf { it.seconds }.toDuration(false)
|
get() = songs.sumOf { it.seconds }.formatDuration(false)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -60,16 +60,16 @@ class MusicStore private constructor() {
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Load/Sort the entire music library. Should always be ran on a coroutine. */
|
/** Load/Sort the entire music library. Should always be ran on a coroutine. */
|
||||||
suspend fun index(context: Context): Response {
|
suspend fun load(context: Context): Response {
|
||||||
logD("Starting initial music load")
|
logD("Starting initial music load")
|
||||||
val newResponse = withContext(Dispatchers.IO) { indexImpl(context) }.also { response = it }
|
val newResponse = withContext(Dispatchers.IO) { loadImpl(context) }.also { response = it }
|
||||||
for (callback in callbacks) {
|
for (callback in callbacks) {
|
||||||
callback.onMusicUpdate(newResponse)
|
callback.onMusicUpdate(newResponse)
|
||||||
}
|
}
|
||||||
return newResponse
|
return newResponse
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun indexImpl(context: Context): Response {
|
private fun loadImpl(context: Context): Response {
|
||||||
val notGranted =
|
val notGranted =
|
||||||
ContextCompat.checkSelfPermission(context, Manifest.permission.READ_EXTERNAL_STORAGE) ==
|
ContextCompat.checkSelfPermission(context, Manifest.permission.READ_EXTERNAL_STORAGE) ==
|
||||||
PackageManager.PERMISSION_DENIED
|
PackageManager.PERMISSION_DENIED
|
||||||
|
@ -81,7 +81,7 @@ class MusicStore private constructor() {
|
||||||
val response =
|
val response =
|
||||||
try {
|
try {
|
||||||
val start = System.currentTimeMillis()
|
val start = System.currentTimeMillis()
|
||||||
val library = Indexer.run(context)
|
val library = Indexer.index(context)
|
||||||
if (library != null) {
|
if (library != null) {
|
||||||
logD(
|
logD(
|
||||||
"Music load completed successfully in ${System.currentTimeMillis() - start}ms")
|
"Music load completed successfully in ${System.currentTimeMillis() - start}ms")
|
||||||
|
@ -132,8 +132,6 @@ class MusicStore private constructor() {
|
||||||
/**
|
/**
|
||||||
* A response that [MusicStore] returns when loading music. And before you ask, yes, I do like
|
* A response that [MusicStore] returns when loading music. And before you ask, yes, I do like
|
||||||
* rust.
|
* rust.
|
||||||
*
|
|
||||||
* TODO: Add the exception to the "FAILED" ErrorKind
|
|
||||||
*/
|
*/
|
||||||
sealed class Response {
|
sealed class Response {
|
||||||
class Ok(val library: Library) : Response()
|
class Ok(val library: Library) : Response()
|
||||||
|
|
|
@ -17,28 +17,9 @@
|
||||||
|
|
||||||
package org.oxycblt.auxio.music
|
package org.oxycblt.auxio.music
|
||||||
|
|
||||||
import android.text.format.DateUtils
|
|
||||||
import org.oxycblt.auxio.util.logD
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
// --- EXTENSION FUNCTIONS ---
|
// --- EXTENSION FUNCTIONS ---
|
||||||
|
|
||||||
/**
|
|
||||||
* Convert a [Long] of seconds into a string duration.
|
|
||||||
* @param isElapsed Whether this duration is represents elapsed time. If this is false, then --:--
|
|
||||||
* will be returned if the second value is 0.
|
|
||||||
*/
|
|
||||||
fun Long.toDuration(isElapsed: Boolean): String {
|
|
||||||
if (!isElapsed && this == 0L) {
|
|
||||||
logD("Non-elapsed duration is zero, using --:--")
|
|
||||||
return "--:--"
|
|
||||||
}
|
|
||||||
|
|
||||||
var durationString = DateUtils.formatElapsedTime(this)
|
|
||||||
|
|
||||||
// If the duration begins with a excess zero [e.g 01:42], then cut it off.
|
|
||||||
if (durationString[0] == '0') {
|
|
||||||
durationString = durationString.slice(1 until durationString.length)
|
|
||||||
}
|
|
||||||
|
|
||||||
return durationString
|
|
||||||
}
|
|
||||||
|
|
|
@ -28,8 +28,8 @@ import org.oxycblt.auxio.util.logD
|
||||||
class MusicViewModel : ViewModel(), MusicStore.Callback {
|
class MusicViewModel : ViewModel(), MusicStore.Callback {
|
||||||
private val musicStore = MusicStore.getInstance()
|
private val musicStore = MusicStore.getInstance()
|
||||||
|
|
||||||
private val mLoaderResponse = MutableLiveData<MusicStore.Response?>(null)
|
private val _loaderResponse = MutableLiveData<MusicStore.Response?>(null)
|
||||||
val loaderResponse: LiveData<MusicStore.Response?> = mLoaderResponse
|
val loaderResponse: LiveData<MusicStore.Response?> = _loaderResponse
|
||||||
|
|
||||||
private var isBusy = false
|
private var isBusy = false
|
||||||
|
|
||||||
|
@ -42,29 +42,29 @@ class MusicViewModel : ViewModel(), MusicStore.Callback {
|
||||||
* navigated to and because SnackBars will have the best UX here.
|
* navigated to and because SnackBars will have the best UX here.
|
||||||
*/
|
*/
|
||||||
fun loadMusic(context: Context) {
|
fun loadMusic(context: Context) {
|
||||||
if (mLoaderResponse.value != null || isBusy) {
|
if (_loaderResponse.value != null || isBusy) {
|
||||||
logD("Loader is busy/already completed, not reloading")
|
logD("Loader is busy/already completed, not reloading")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
isBusy = true
|
isBusy = true
|
||||||
mLoaderResponse.value = null
|
_loaderResponse.value = null
|
||||||
|
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
val result = musicStore.index(context)
|
val result = musicStore.load(context)
|
||||||
mLoaderResponse.value = result
|
_loaderResponse.value = result
|
||||||
isBusy = false
|
isBusy = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun reloadMusic(context: Context) {
|
fun reloadMusic(context: Context) {
|
||||||
logD("Reloading music library")
|
logD("Reloading music library")
|
||||||
mLoaderResponse.value = null
|
_loaderResponse.value = null
|
||||||
loadMusic(context)
|
loadMusic(context)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onMusicUpdate(response: MusicStore.Response) {
|
override fun onMusicUpdate(response: MusicStore.Response) {
|
||||||
mLoaderResponse.value = response
|
_loaderResponse.value = response
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onCleared() {
|
override fun onCleared() {
|
||||||
|
|
|
@ -29,7 +29,7 @@ import org.oxycblt.auxio.util.requireBackgroundThread
|
||||||
/**
|
/**
|
||||||
* Database for storing excluded directories. Note that the paths stored here will not work with
|
* Database for storing excluded directories. Note that the paths stored here will not work with
|
||||||
* MediaStore unless you append a "%" at the end. Yes. I know Room exists. But that would needlessly
|
* MediaStore unless you append a "%" at the end. Yes. I know Room exists. But that would needlessly
|
||||||
* bloat my app and has crippling bugs.
|
* bloat my app and has crippling bugs. TODO: Migrate this to SharedPreferences?
|
||||||
* @author OxygenCobalt
|
* @author OxygenCobalt
|
||||||
*/
|
*/
|
||||||
class ExcludedDatabase(context: Context) : SQLiteOpenHelper(context, DB_NAME, null, DB_VERSION) {
|
class ExcludedDatabase(context: Context) : SQLiteOpenHelper(context, DB_NAME, null, DB_VERSION) {
|
||||||
|
|
|
@ -37,9 +37,9 @@ import org.oxycblt.auxio.util.unlikelyToBeNull
|
||||||
* TODO: Unify with MusicViewModel
|
* TODO: Unify with MusicViewModel
|
||||||
*/
|
*/
|
||||||
class ExcludedViewModel(private val excludedDatabase: ExcludedDatabase) : ViewModel() {
|
class ExcludedViewModel(private val excludedDatabase: ExcludedDatabase) : ViewModel() {
|
||||||
private val mPaths = MutableLiveData(mutableListOf<String>())
|
private val _paths = MutableLiveData(mutableListOf<String>())
|
||||||
val paths: LiveData<MutableList<String>>
|
val paths: LiveData<MutableList<String>>
|
||||||
get() = mPaths
|
get() = _paths
|
||||||
|
|
||||||
var isModified: Boolean = false
|
var isModified: Boolean = false
|
||||||
private set
|
private set
|
||||||
|
@ -53,10 +53,10 @@ class ExcludedViewModel(private val excludedDatabase: ExcludedDatabase) : ViewMo
|
||||||
* called.
|
* called.
|
||||||
*/
|
*/
|
||||||
fun addPath(path: String) {
|
fun addPath(path: String) {
|
||||||
val paths = unlikelyToBeNull(mPaths.value)
|
val paths = unlikelyToBeNull(_paths.value)
|
||||||
if (!paths.contains(path)) {
|
if (!paths.contains(path)) {
|
||||||
paths.add(path)
|
paths.add(path)
|
||||||
mPaths.value = mPaths.value
|
_paths.value = _paths.value
|
||||||
isModified = true
|
isModified = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -66,8 +66,8 @@ class ExcludedViewModel(private val excludedDatabase: ExcludedDatabase) : ViewMo
|
||||||
* [save] is called.
|
* [save] is called.
|
||||||
*/
|
*/
|
||||||
fun removePath(path: String) {
|
fun removePath(path: String) {
|
||||||
unlikelyToBeNull(mPaths.value).remove(path)
|
unlikelyToBeNull(_paths.value).remove(path)
|
||||||
mPaths.value = mPaths.value
|
_paths.value = _paths.value
|
||||||
isModified = true
|
isModified = true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -75,7 +75,7 @@ class ExcludedViewModel(private val excludedDatabase: ExcludedDatabase) : ViewMo
|
||||||
fun save(onDone: () -> Unit) {
|
fun save(onDone: () -> Unit) {
|
||||||
viewModelScope.launch(Dispatchers.IO) {
|
viewModelScope.launch(Dispatchers.IO) {
|
||||||
val start = System.currentTimeMillis()
|
val start = System.currentTimeMillis()
|
||||||
excludedDatabase.writePaths(unlikelyToBeNull(mPaths.value))
|
excludedDatabase.writePaths(unlikelyToBeNull(_paths.value))
|
||||||
isModified = false
|
isModified = false
|
||||||
onDone()
|
onDone()
|
||||||
this@ExcludedViewModel.logD(
|
this@ExcludedViewModel.logD(
|
||||||
|
@ -90,7 +90,7 @@ class ExcludedViewModel(private val excludedDatabase: ExcludedDatabase) : ViewMo
|
||||||
isModified = false
|
isModified = false
|
||||||
|
|
||||||
val dbPaths = excludedDatabase.readPaths()
|
val dbPaths = excludedDatabase.readPaths()
|
||||||
withContext(Dispatchers.Main) { mPaths.value = dbPaths.toMutableList() }
|
withContext(Dispatchers.Main) { _paths.value = dbPaths.toMutableList() }
|
||||||
|
|
||||||
this@ExcludedViewModel.logD(
|
this@ExcludedViewModel.logD(
|
||||||
"Path load completed successfully in ${System.currentTimeMillis() - start}ms")
|
"Path load completed successfully in ${System.currentTimeMillis() - start}ms")
|
||||||
|
|
|
@ -20,6 +20,7 @@ package org.oxycblt.auxio.playback
|
||||||
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 androidx.appcompat.widget.Toolbar
|
||||||
import androidx.core.view.updatePadding
|
import androidx.core.view.updatePadding
|
||||||
import androidx.fragment.app.Fragment
|
import androidx.fragment.app.Fragment
|
||||||
import androidx.fragment.app.activityViewModels
|
import androidx.fragment.app.activityViewModels
|
||||||
|
@ -31,12 +32,12 @@ import org.oxycblt.auxio.coil.bindAlbumCover
|
||||||
import org.oxycblt.auxio.databinding.FragmentPlaybackPanelBinding
|
import org.oxycblt.auxio.databinding.FragmentPlaybackPanelBinding
|
||||||
import org.oxycblt.auxio.music.MusicParent
|
import org.oxycblt.auxio.music.MusicParent
|
||||||
import org.oxycblt.auxio.music.Song
|
import org.oxycblt.auxio.music.Song
|
||||||
import org.oxycblt.auxio.music.toDuration
|
|
||||||
import org.oxycblt.auxio.playback.state.RepeatMode
|
import org.oxycblt.auxio.playback.state.RepeatMode
|
||||||
import org.oxycblt.auxio.ui.MainNavigationAction
|
import org.oxycblt.auxio.ui.MainNavigationAction
|
||||||
import org.oxycblt.auxio.ui.NavigationViewModel
|
import org.oxycblt.auxio.ui.NavigationViewModel
|
||||||
import org.oxycblt.auxio.ui.ViewBindingFragment
|
import org.oxycblt.auxio.ui.ViewBindingFragment
|
||||||
import org.oxycblt.auxio.util.clamp
|
import org.oxycblt.auxio.util.clamp
|
||||||
|
import org.oxycblt.auxio.util.formatDuration
|
||||||
import org.oxycblt.auxio.util.getAttrColorSafe
|
import org.oxycblt.auxio.util.getAttrColorSafe
|
||||||
import org.oxycblt.auxio.util.logD
|
import org.oxycblt.auxio.util.logD
|
||||||
import org.oxycblt.auxio.util.stateList
|
import org.oxycblt.auxio.util.stateList
|
||||||
|
@ -55,7 +56,8 @@ import org.oxycblt.auxio.util.textSafe
|
||||||
class PlaybackPanelFragment :
|
class PlaybackPanelFragment :
|
||||||
ViewBindingFragment<FragmentPlaybackPanelBinding>(),
|
ViewBindingFragment<FragmentPlaybackPanelBinding>(),
|
||||||
Slider.OnChangeListener,
|
Slider.OnChangeListener,
|
||||||
Slider.OnSliderTouchListener {
|
Slider.OnSliderTouchListener,
|
||||||
|
Toolbar.OnMenuItemClickListener {
|
||||||
private val playbackModel: PlaybackViewModel by activityViewModels()
|
private val playbackModel: PlaybackViewModel by activityViewModels()
|
||||||
private val navModel: NavigationViewModel by activityViewModels()
|
private val navModel: NavigationViewModel by activityViewModels()
|
||||||
|
|
||||||
|
@ -77,16 +79,7 @@ class PlaybackPanelFragment :
|
||||||
|
|
||||||
binding.playbackToolbar.apply {
|
binding.playbackToolbar.apply {
|
||||||
setNavigationOnClickListener { navModel.mainNavigateTo(MainNavigationAction.COLLAPSE) }
|
setNavigationOnClickListener { navModel.mainNavigateTo(MainNavigationAction.COLLAPSE) }
|
||||||
|
setOnMenuItemClickListener(this@PlaybackPanelFragment)
|
||||||
setOnMenuItemClickListener { item ->
|
|
||||||
if (item.itemId == R.id.action_queue) {
|
|
||||||
navModel.mainNavigateTo(MainNavigationAction.QUEUE)
|
|
||||||
true
|
|
||||||
} else {
|
|
||||||
false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
queueItem = menu.findItem(R.id.action_queue)
|
queueItem = menu.findItem(R.id.action_queue)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -127,6 +120,8 @@ class PlaybackPanelFragment :
|
||||||
binding.playbackSkipNext.setOnClickListener { playbackModel.skipNext() }
|
binding.playbackSkipNext.setOnClickListener { playbackModel.skipNext() }
|
||||||
binding.playbackShuffle.setOnClickListener { playbackModel.invertShuffled() }
|
binding.playbackShuffle.setOnClickListener { playbackModel.invertShuffled() }
|
||||||
|
|
||||||
|
binding.playbackSeekBar.apply {}
|
||||||
|
|
||||||
// --- VIEWMODEL SETUP --
|
// --- VIEWMODEL SETUP --
|
||||||
|
|
||||||
playbackModel.song.observe(viewLifecycleOwner, ::updateSong)
|
playbackModel.song.observe(viewLifecycleOwner, ::updateSong)
|
||||||
|
@ -146,11 +141,22 @@ class PlaybackPanelFragment :
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onDestroyBinding(binding: FragmentPlaybackPanelBinding) {
|
override fun onDestroyBinding(binding: FragmentPlaybackPanelBinding) {
|
||||||
|
binding.playbackToolbar.setOnMenuItemClickListener(null)
|
||||||
binding.playbackSong.isSelected = false
|
binding.playbackSong.isSelected = false
|
||||||
binding.playbackSeekBar.removeOnChangeListener(this)
|
binding.playbackSeekBar.removeOnChangeListener(this)
|
||||||
binding.playbackSeekBar.removeOnChangeListener(this)
|
binding.playbackSeekBar.removeOnChangeListener(this)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun onMenuItemClick(item: MenuItem): Boolean {
|
||||||
|
return when (item.itemId) {
|
||||||
|
R.id.action_queue -> {
|
||||||
|
navModel.mainNavigateTo(MainNavigationAction.QUEUE)
|
||||||
|
true
|
||||||
|
}
|
||||||
|
else -> false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
override fun onStartTrackingTouch(slider: Slider) {
|
override fun onStartTrackingTouch(slider: Slider) {
|
||||||
requireBinding().playbackPosition.isActivated = true
|
requireBinding().playbackPosition.isActivated = true
|
||||||
}
|
}
|
||||||
|
@ -162,7 +168,7 @@ class PlaybackPanelFragment :
|
||||||
|
|
||||||
override fun onValueChange(slider: Slider, value: Float, fromUser: Boolean) {
|
override fun onValueChange(slider: Slider, value: Float, fromUser: Boolean) {
|
||||||
if (fromUser) {
|
if (fromUser) {
|
||||||
requireBinding().playbackPosition.textSafe = value.toLong().toDuration(true)
|
requireBinding().playbackPosition.textSafe = value.toLong().formatDuration(true)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -178,7 +184,7 @@ class PlaybackPanelFragment :
|
||||||
|
|
||||||
// Normally if a song had a duration
|
// Normally if a song had a duration
|
||||||
val seconds = song.seconds
|
val seconds = song.seconds
|
||||||
binding.playbackDuration.textSafe = seconds.toDuration(false)
|
binding.playbackDuration.textSafe = seconds.formatDuration(false)
|
||||||
binding.playbackSeekBar.apply {
|
binding.playbackSeekBar.apply {
|
||||||
isEnabled = seconds > 0L
|
isEnabled = seconds > 0L
|
||||||
valueTo = max(seconds, 1L).toFloat()
|
valueTo = max(seconds, 1L).toFloat()
|
||||||
|
@ -197,7 +203,7 @@ class PlaybackPanelFragment :
|
||||||
val binding = requireBinding()
|
val binding = requireBinding()
|
||||||
if (!binding.playbackPosition.isActivated) {
|
if (!binding.playbackPosition.isActivated) {
|
||||||
binding.playbackSeekBar.value = position.toFloat()
|
binding.playbackSeekBar.value = position.toFloat()
|
||||||
binding.playbackPosition.textSafe = position.toDuration(true)
|
binding.playbackPosition.textSafe = position.formatDuration(true)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -55,43 +55,35 @@ class PlaybackViewModel : ViewModel(), PlaybackStateManager.Callback {
|
||||||
private val settingsManager = SettingsManager.getInstance()
|
private val settingsManager = SettingsManager.getInstance()
|
||||||
private val playbackManager = PlaybackStateManager.getInstance()
|
private val playbackManager = PlaybackStateManager.getInstance()
|
||||||
|
|
||||||
// Playback
|
private var intentUri: Uri? = null
|
||||||
private val mSong = MutableLiveData<Song?>()
|
|
||||||
private val mParent = MutableLiveData<MusicParent?>()
|
|
||||||
|
|
||||||
// States
|
|
||||||
private val mIsPlaying = MutableLiveData(false)
|
|
||||||
private val mPositionSecs = MutableLiveData(0L)
|
|
||||||
private val mRepeatMode = MutableLiveData(RepeatMode.NONE)
|
|
||||||
private val mIsShuffled = MutableLiveData(false)
|
|
||||||
|
|
||||||
// Queue
|
|
||||||
private val mNextUp = MutableLiveData(listOf<Song>())
|
|
||||||
|
|
||||||
// Other
|
|
||||||
private var mIntentUri: Uri? = null
|
|
||||||
|
|
||||||
|
private val _song = MutableLiveData<Song?>()
|
||||||
/** The current song. */
|
/** The current song. */
|
||||||
val song: LiveData<Song?>
|
val song: LiveData<Song?>
|
||||||
get() = mSong
|
get() = _song
|
||||||
|
private val _parent = MutableLiveData<MusicParent?>()
|
||||||
/** The current model that is being played from, such as an [Album] or [Artist] */
|
/** The current model that is being played from, such as an [Album] or [Artist] */
|
||||||
val parent: LiveData<MusicParent?>
|
val parent: LiveData<MusicParent?>
|
||||||
get() = mParent
|
get() = _parent
|
||||||
|
private val _isPlaying = MutableLiveData(false)
|
||||||
val isPlaying: LiveData<Boolean>
|
val isPlaying: LiveData<Boolean>
|
||||||
get() = mIsPlaying
|
get() = _isPlaying
|
||||||
|
private val _positionSecs = MutableLiveData(0L)
|
||||||
/** The current playback position, in seconds */
|
/** The current playback position, in seconds */
|
||||||
val positionSecs: LiveData<Long>
|
val positionSecs: LiveData<Long>
|
||||||
get() = mPositionSecs
|
get() = _positionSecs
|
||||||
|
private val _repeatMode = MutableLiveData(RepeatMode.NONE)
|
||||||
/** The current repeat mode, see [RepeatMode] for more information */
|
/** The current repeat mode, see [RepeatMode] for more information */
|
||||||
val repeatMode: LiveData<RepeatMode>
|
val repeatMode: LiveData<RepeatMode>
|
||||||
get() = mRepeatMode
|
get() = _repeatMode
|
||||||
|
private val _isShuffled = MutableLiveData(false)
|
||||||
val isShuffled: LiveData<Boolean>
|
val isShuffled: LiveData<Boolean>
|
||||||
get() = mIsShuffled
|
get() = _isShuffled
|
||||||
|
|
||||||
|
private val _nextUp = MutableLiveData(listOf<Song>())
|
||||||
/** The queue, without the previous items. */
|
/** The queue, without the previous items. */
|
||||||
val nextUp: LiveData<List<Song>>
|
val nextUp: LiveData<List<Song>>
|
||||||
get() = mNextUp
|
get() = _nextUp
|
||||||
|
|
||||||
init {
|
init {
|
||||||
playbackManager.addCallback(this)
|
playbackManager.addCallback(this)
|
||||||
|
@ -167,7 +159,7 @@ class PlaybackViewModel : ViewModel(), PlaybackStateManager.Callback {
|
||||||
} else {
|
} else {
|
||||||
logD("Cant play this URI right now, waiting")
|
logD("Cant play this URI right now, waiting")
|
||||||
|
|
||||||
mIntentUri = uri
|
intentUri = uri
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -208,7 +200,7 @@ class PlaybackViewModel : ViewModel(), PlaybackStateManager.Callback {
|
||||||
*/
|
*/
|
||||||
fun removeQueueDataItem(adapterIndex: Int, apply: () -> Unit) {
|
fun removeQueueDataItem(adapterIndex: Int, apply: () -> Unit) {
|
||||||
val index =
|
val index =
|
||||||
adapterIndex + (playbackManager.queue.size - unlikelyToBeNull(mNextUp.value).size)
|
adapterIndex + (playbackManager.queue.size - unlikelyToBeNull(_nextUp.value).size)
|
||||||
if (index in playbackManager.queue.indices) {
|
if (index in playbackManager.queue.indices) {
|
||||||
apply()
|
apply()
|
||||||
playbackManager.removeQueueItem(index)
|
playbackManager.removeQueueItem(index)
|
||||||
|
@ -219,7 +211,7 @@ class PlaybackViewModel : ViewModel(), PlaybackStateManager.Callback {
|
||||||
* is called just before the change is committed so that the adapter can be updated.
|
* is called just before the change is committed so that the adapter can be updated.
|
||||||
*/
|
*/
|
||||||
fun moveQueueDataItems(adapterFrom: Int, adapterTo: Int, apply: () -> Unit): Boolean {
|
fun moveQueueDataItems(adapterFrom: Int, adapterTo: Int, apply: () -> Unit): Boolean {
|
||||||
val delta = (playbackManager.queue.size - unlikelyToBeNull(mNextUp.value).size)
|
val delta = (playbackManager.queue.size - unlikelyToBeNull(_nextUp.value).size)
|
||||||
val from = adapterFrom + delta
|
val from = adapterFrom + delta
|
||||||
val to = adapterTo + delta
|
val to = adapterTo + delta
|
||||||
if (from in playbackManager.queue.indices && to in playbackManager.queue.indices) {
|
if (from in playbackManager.queue.indices && to in playbackManager.queue.indices) {
|
||||||
|
@ -287,12 +279,12 @@ class PlaybackViewModel : ViewModel(), PlaybackStateManager.Callback {
|
||||||
* - Restore the last playback state if there is no active file intent.
|
* - Restore the last playback state if there is no active file intent.
|
||||||
*/
|
*/
|
||||||
fun setupPlayback(context: Context) {
|
fun setupPlayback(context: Context) {
|
||||||
val intentUri = mIntentUri
|
val intentUri = intentUri
|
||||||
|
|
||||||
if (intentUri != null) {
|
if (intentUri != null) {
|
||||||
playWithUriInternal(intentUri, context)
|
playWithUriInternal(intentUri, context)
|
||||||
// Remove the uri after finishing the calls so that this does not fire again.
|
// Remove the uri after finishing the calls so that this does not fire again.
|
||||||
mIntentUri = null
|
this.intentUri = null
|
||||||
} else if (!playbackManager.isInitialized) {
|
} else if (!playbackManager.isInitialized) {
|
||||||
// Otherwise just restore
|
// Otherwise just restore
|
||||||
viewModelScope.launch { playbackManager.restoreState(context) }
|
viewModelScope.launch { playbackManager.restoreState(context) }
|
||||||
|
@ -319,33 +311,33 @@ class PlaybackViewModel : ViewModel(), PlaybackStateManager.Callback {
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onIndexMoved(index: Int) {
|
override fun onIndexMoved(index: Int) {
|
||||||
mSong.value = playbackManager.song
|
_song.value = playbackManager.song
|
||||||
mNextUp.value = playbackManager.queue.slice(index.inc() until playbackManager.queue.size)
|
_nextUp.value = playbackManager.queue.slice(index + 1 until playbackManager.queue.size)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onQueueChanged(index: Int, queue: List<Song>) {
|
override fun onQueueChanged(index: Int, queue: List<Song>) {
|
||||||
mNextUp.value = queue.slice(index.inc() until queue.size)
|
_nextUp.value = queue.slice(index + 1 until queue.size)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onNewPlayback(index: Int, queue: List<Song>, parent: MusicParent?) {
|
override fun onNewPlayback(index: Int, queue: List<Song>, parent: MusicParent?) {
|
||||||
mParent.value = playbackManager.parent
|
_parent.value = playbackManager.parent
|
||||||
mSong.value = playbackManager.song
|
_song.value = playbackManager.song
|
||||||
mNextUp.value = queue.slice(index.inc() until queue.size)
|
_nextUp.value = queue.slice(index + 1 until queue.size)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onPositionChanged(positionMs: Long) {
|
override fun onPositionChanged(positionMs: Long) {
|
||||||
mPositionSecs.value = positionMs / 1000
|
_positionSecs.value = positionMs / 1000
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onPlayingChanged(isPlaying: Boolean) {
|
override fun onPlayingChanged(isPlaying: Boolean) {
|
||||||
mIsPlaying.value = isPlaying
|
_isPlaying.value = isPlaying
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onShuffledChanged(isShuffled: Boolean) {
|
override fun onShuffledChanged(isShuffled: Boolean) {
|
||||||
mIsShuffled.value = isShuffled
|
_isShuffled.value = isShuffled
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onRepeatChanged(repeatMode: RepeatMode) {
|
override fun onRepeatChanged(repeatMode: RepeatMode) {
|
||||||
mRepeatMode.value = repeatMode
|
_repeatMode.value = repeatMode
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -118,31 +118,31 @@ class HybridBackingData<T>(
|
||||||
private val adapter: RecyclerView.Adapter<*>,
|
private val adapter: RecyclerView.Adapter<*>,
|
||||||
diffCallback: DiffUtil.ItemCallback<T>
|
diffCallback: DiffUtil.ItemCallback<T>
|
||||||
) : BackingData<T>() {
|
) : BackingData<T>() {
|
||||||
private var mCurrentList = mutableListOf<T>()
|
private var _currentList = mutableListOf<T>()
|
||||||
val currentList: List<T>
|
val currentList: List<T>
|
||||||
get() = mCurrentList
|
get() = _currentList
|
||||||
|
|
||||||
private val differ = AsyncListDiffer(adapter, diffCallback)
|
private val differ = AsyncListDiffer(adapter, diffCallback)
|
||||||
|
|
||||||
override fun getItem(position: Int): T = mCurrentList[position]
|
override fun getItem(position: Int): T = _currentList[position]
|
||||||
override fun getItemCount(): Int = mCurrentList.size
|
override fun getItemCount(): Int = _currentList.size
|
||||||
|
|
||||||
fun submitList(newData: List<T>, onDone: () -> Unit = {}) {
|
fun submitList(newData: List<T>, onDone: () -> Unit = {}) {
|
||||||
if (newData != mCurrentList) {
|
if (newData != _currentList) {
|
||||||
mCurrentList = newData.toMutableList()
|
_currentList = newData.toMutableList()
|
||||||
differ.submitList(newData, onDone)
|
differ.submitList(newData, onDone)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun moveItems(from: Int, to: Int) {
|
fun moveItems(from: Int, to: Int) {
|
||||||
mCurrentList.add(to, mCurrentList.removeAt(from))
|
_currentList.add(to, _currentList.removeAt(from))
|
||||||
differ.rewriteListUnsafe(mCurrentList)
|
differ.rewriteListUnsafe(_currentList)
|
||||||
adapter.notifyItemMoved(from, to)
|
adapter.notifyItemMoved(from, to)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun removeItem(at: Int) {
|
fun removeItem(at: Int) {
|
||||||
mCurrentList.removeAt(at)
|
_currentList.removeAt(at)
|
||||||
differ.rewriteListUnsafe(mCurrentList)
|
differ.rewriteListUnsafe(_currentList)
|
||||||
adapter.notifyItemRemoved(at)
|
adapter.notifyItemRemoved(at)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -152,7 +152,7 @@ class HybridBackingData<T>(
|
||||||
* can do to marry the adapter primitives with DiffUtil.
|
* can do to marry the adapter primitives with DiffUtil.
|
||||||
*/
|
*/
|
||||||
private fun <T> AsyncListDiffer<T>.rewriteListUnsafe(newList: List<T>) {
|
private fun <T> AsyncListDiffer<T>.rewriteListUnsafe(newList: List<T>) {
|
||||||
differMaxGenerationsField.set(this, (differMaxGenerationsField.get(this) as Int).inc())
|
differMaxGenerationsField.set(this, (differMaxGenerationsField.get(this) as Int) + 1)
|
||||||
differListField.set(this, newList.toMutableList())
|
differListField.set(this, newList.toMutableList())
|
||||||
differImmutableListField.set(this, newList)
|
differImmutableListField.set(this, newList)
|
||||||
}
|
}
|
||||||
|
|
|
@ -41,23 +41,24 @@ import org.oxycblt.auxio.util.logD
|
||||||
*
|
*
|
||||||
* All access should be done with [PlaybackStateManager.getInstance].
|
* All access should be done with [PlaybackStateManager.getInstance].
|
||||||
* @author OxygenCobalt
|
* @author OxygenCobalt
|
||||||
|
*
|
||||||
|
* TODO: Add a controller role and move song loading/seeking to that TODO: Make PlaybackViewModel
|
||||||
|
* pass "delayed actions" to this and then await the service to start it???
|
||||||
*/
|
*/
|
||||||
class PlaybackStateManager private constructor() {
|
class PlaybackStateManager private constructor() {
|
||||||
private val musicStore = MusicStore.getInstance()
|
private val musicStore = MusicStore.getInstance()
|
||||||
private val settingsManager = SettingsManager.getInstance()
|
private val settingsManager = SettingsManager.getInstance()
|
||||||
|
|
||||||
// Playback
|
|
||||||
private var mutableQueue = mutableListOf<Song>()
|
|
||||||
|
|
||||||
/** The currently playing song. Null if there isn't one */
|
/** The currently playing song. Null if there isn't one */
|
||||||
val song
|
val song
|
||||||
get() = queue.getOrNull(index)
|
get() = queue.getOrNull(index)
|
||||||
/** The parent the queue is based on, null if all songs */
|
/** The parent the queue is based on, null if all songs */
|
||||||
var parent: MusicParent? = null
|
var parent: MusicParent? = null
|
||||||
private set
|
private set
|
||||||
|
private var _queue = mutableListOf<Song>()
|
||||||
/** The current queue determined by [parent] */
|
/** The current queue determined by [parent] */
|
||||||
val queue
|
val queue
|
||||||
get() = mutableQueue
|
get() = _queue
|
||||||
/** The current position in the queue */
|
/** The current position in the queue */
|
||||||
var index = -1
|
var index = -1
|
||||||
private set
|
private set
|
||||||
|
@ -160,8 +161,8 @@ class PlaybackStateManager private constructor() {
|
||||||
fun next() {
|
fun next() {
|
||||||
// Increment the index, if it cannot be incremented any further, then
|
// Increment the index, if it cannot be incremented any further, then
|
||||||
// repeat and pause/resume playback depending on the setting
|
// repeat and pause/resume playback depending on the setting
|
||||||
if (index < mutableQueue.lastIndex) {
|
if (index < _queue.lastIndex) {
|
||||||
goto(index.inc(), true)
|
goto(index + 1, true)
|
||||||
} else {
|
} else {
|
||||||
goto(0, repeatMode == RepeatMode.ALL)
|
goto(0, repeatMode == RepeatMode.ALL)
|
||||||
}
|
}
|
||||||
|
@ -174,7 +175,7 @@ class PlaybackStateManager private constructor() {
|
||||||
rewind()
|
rewind()
|
||||||
isPlaying = true
|
isPlaying = true
|
||||||
} else {
|
} else {
|
||||||
goto(max(index.dec(), 0), true)
|
goto(max(index - 1, 0), true)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -187,39 +188,39 @@ class PlaybackStateManager private constructor() {
|
||||||
|
|
||||||
/** Add a [song] to the top of the queue. */
|
/** Add a [song] to the top of the queue. */
|
||||||
fun playNext(song: Song) {
|
fun playNext(song: Song) {
|
||||||
mutableQueue.add(index.inc(), song)
|
_queue.add(index + 1, song)
|
||||||
notifyQueueChanged()
|
notifyQueueChanged()
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Add a list of [songs] to the top of the queue. */
|
/** Add a list of [songs] to the top of the queue. */
|
||||||
fun playNext(songs: List<Song>) {
|
fun playNext(songs: List<Song>) {
|
||||||
mutableQueue.addAll(index.inc(), songs)
|
_queue.addAll(index + 1, songs)
|
||||||
notifyQueueChanged()
|
notifyQueueChanged()
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Add a [song] to the end of the queue. */
|
/** Add a [song] to the end of the queue. */
|
||||||
fun addToQueue(song: Song) {
|
fun addToQueue(song: Song) {
|
||||||
mutableQueue.add(song)
|
_queue.add(song)
|
||||||
notifyQueueChanged()
|
notifyQueueChanged()
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Add a list of [songs] to the end of the queue. */
|
/** Add a list of [songs] to the end of the queue. */
|
||||||
fun addToQueue(songs: List<Song>) {
|
fun addToQueue(songs: List<Song>) {
|
||||||
mutableQueue.addAll(songs)
|
_queue.addAll(songs)
|
||||||
notifyQueueChanged()
|
notifyQueueChanged()
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Move a queue item at [from] to a position at [to]. Will ignore invalid indexes. */
|
/** Move a queue item at [from] to a position at [to]. Will ignore invalid indexes. */
|
||||||
fun moveQueueItem(from: Int, to: Int) {
|
fun moveQueueItem(from: Int, to: Int) {
|
||||||
logD("Moving item $from to position $to")
|
logD("Moving item $from to position $to")
|
||||||
mutableQueue.add(to, mutableQueue.removeAt(from))
|
_queue.add(to, _queue.removeAt(from))
|
||||||
notifyQueueChanged()
|
notifyQueueChanged()
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Remove a queue item at [index]. Will ignore invalid indexes. */
|
/** Remove a queue item at [index]. Will ignore invalid indexes. */
|
||||||
fun removeQueueItem(index: Int) {
|
fun removeQueueItem(index: Int) {
|
||||||
logD("Removing item ${mutableQueue[index].rawName}")
|
logD("Removing item ${_queue[index].rawName}")
|
||||||
mutableQueue.removeAt(index)
|
_queue.removeAt(index)
|
||||||
notifyQueueChanged()
|
notifyQueueChanged()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -240,7 +241,7 @@ class PlaybackStateManager private constructor() {
|
||||||
) {
|
) {
|
||||||
if (shuffled) {
|
if (shuffled) {
|
||||||
if (regenShuffledQueue) {
|
if (regenShuffledQueue) {
|
||||||
mutableQueue =
|
_queue =
|
||||||
parent
|
parent
|
||||||
.let { parent ->
|
.let { parent ->
|
||||||
when (parent) {
|
when (parent) {
|
||||||
|
@ -253,15 +254,15 @@ class PlaybackStateManager private constructor() {
|
||||||
.toMutableList()
|
.toMutableList()
|
||||||
}
|
}
|
||||||
|
|
||||||
mutableQueue.shuffle()
|
_queue.shuffle()
|
||||||
|
|
||||||
if (keep != null) {
|
if (keep != null) {
|
||||||
mutableQueue.add(0, mutableQueue.removeAt(mutableQueue.indexOf(keep)))
|
_queue.add(0, _queue.removeAt(_queue.indexOf(keep)))
|
||||||
}
|
}
|
||||||
|
|
||||||
index = 0
|
index = 0
|
||||||
} else {
|
} else {
|
||||||
mutableQueue =
|
_queue =
|
||||||
parent
|
parent
|
||||||
.let { parent ->
|
.let { parent ->
|
||||||
when (parent) {
|
when (parent) {
|
||||||
|
@ -343,7 +344,7 @@ class PlaybackStateManager private constructor() {
|
||||||
if (state != null) {
|
if (state != null) {
|
||||||
index = state.index
|
index = state.index
|
||||||
parent = state.parent
|
parent = state.parent
|
||||||
mutableQueue = state.queue.toMutableList()
|
_queue = state.queue.toMutableList()
|
||||||
repeatMode = state.repeatMode
|
repeatMode = state.repeatMode
|
||||||
isShuffled = state.isShuffled
|
isShuffled = state.isShuffled
|
||||||
|
|
||||||
|
@ -372,7 +373,7 @@ class PlaybackStateManager private constructor() {
|
||||||
PlaybackStateDatabase.SavedState(
|
PlaybackStateDatabase.SavedState(
|
||||||
index = index,
|
index = index,
|
||||||
parent = parent,
|
parent = parent,
|
||||||
queue = mutableQueue,
|
queue = _queue,
|
||||||
positionMs = positionMs,
|
positionMs = positionMs,
|
||||||
isShuffled = isShuffled,
|
isShuffled = isShuffled,
|
||||||
repeatMode = repeatMode))
|
repeatMode = repeatMode))
|
||||||
|
|
|
@ -19,8 +19,10 @@ package org.oxycblt.auxio.search
|
||||||
|
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.view.LayoutInflater
|
import android.view.LayoutInflater
|
||||||
|
import android.view.MenuItem
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.view.inputmethod.InputMethodManager
|
import android.view.inputmethod.InputMethodManager
|
||||||
|
import androidx.appcompat.widget.Toolbar
|
||||||
import androidx.core.view.isInvisible
|
import androidx.core.view.isInvisible
|
||||||
import androidx.core.view.postDelayed
|
import androidx.core.view.postDelayed
|
||||||
import androidx.core.widget.addTextChangedListener
|
import androidx.core.widget.addTextChangedListener
|
||||||
|
@ -53,7 +55,10 @@ import org.oxycblt.auxio.util.requireAttached
|
||||||
* A [Fragment] that allows for the searching of the entire music library.
|
* A [Fragment] that allows for the searching of the entire music library.
|
||||||
* @author OxygenCobalt
|
* @author OxygenCobalt
|
||||||
*/
|
*/
|
||||||
class SearchFragment : ViewBindingFragment<FragmentSearchBinding>(), MenuItemListener {
|
class SearchFragment :
|
||||||
|
ViewBindingFragment<FragmentSearchBinding>(),
|
||||||
|
MenuItemListener,
|
||||||
|
Toolbar.OnMenuItemClickListener {
|
||||||
// SearchViewModel is only scoped to this Fragment
|
// SearchViewModel is only scoped to this Fragment
|
||||||
private val searchModel: SearchViewModel by viewModels()
|
private val searchModel: SearchViewModel by viewModels()
|
||||||
private val playbackModel: PlaybackViewModel by activityViewModels()
|
private val playbackModel: PlaybackViewModel by activityViewModels()
|
||||||
|
@ -75,15 +80,7 @@ class SearchFragment : ViewBindingFragment<FragmentSearchBinding>(), MenuItemLis
|
||||||
findNavController().navigateUp()
|
findNavController().navigateUp()
|
||||||
}
|
}
|
||||||
|
|
||||||
setOnMenuItemClickListener { item ->
|
setOnMenuItemClickListener(this@SearchFragment)
|
||||||
if (item.itemId != R.id.submenu_filtering) {
|
|
||||||
searchModel.updateFilterModeWithId(context, item.itemId)
|
|
||||||
item.isChecked = true
|
|
||||||
true
|
|
||||||
} else {
|
|
||||||
false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
binding.searchEditText.apply {
|
binding.searchEditText.apply {
|
||||||
|
@ -116,11 +113,25 @@ class SearchFragment : ViewBindingFragment<FragmentSearchBinding>(), MenuItemLis
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onDestroyBinding(binding: FragmentSearchBinding) {
|
override fun onDestroyBinding(binding: FragmentSearchBinding) {
|
||||||
super.onDestroyBinding(binding)
|
binding.searchToolbar.setOnMenuItemClickListener(null)
|
||||||
binding.searchRecycler.adapter = null
|
binding.searchRecycler.adapter = null
|
||||||
imm = null
|
imm = null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun onMenuItemClick(item: MenuItem): Boolean {
|
||||||
|
when (item.itemId) {
|
||||||
|
R.id.submenu_filtering -> {}
|
||||||
|
else -> {
|
||||||
|
if (item.itemId != R.id.submenu_filtering) {
|
||||||
|
searchModel.updateFilterModeWithId(requireContext(), item.itemId)
|
||||||
|
item.isChecked = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
override fun onItemClick(item: Item) {
|
override fun onItemClick(item: Item) {
|
||||||
when (item) {
|
when (item) {
|
||||||
is Song -> playbackModel.playSong(item)
|
is Song -> playbackModel.playSong(item)
|
||||||
|
|
|
@ -43,30 +43,30 @@ class SearchViewModel : ViewModel() {
|
||||||
private val musicStore = MusicStore.getInstance()
|
private val musicStore = MusicStore.getInstance()
|
||||||
private val settingsManager = SettingsManager.getInstance()
|
private val settingsManager = SettingsManager.getInstance()
|
||||||
|
|
||||||
private val mSearchResults = MutableLiveData(listOf<Item>())
|
private val _searchResults = MutableLiveData(listOf<Item>())
|
||||||
private var mFilterMode: DisplayMode? = null
|
private var _filterMode: DisplayMode? = null
|
||||||
private var mLastQuery: String? = null
|
private var lastQuery: String? = null
|
||||||
|
|
||||||
/** Current search results from the last [search] call. */
|
/** Current search results from the last [search] call. */
|
||||||
val searchResults: LiveData<List<Item>>
|
val searchResults: LiveData<List<Item>>
|
||||||
get() = mSearchResults
|
get() = _searchResults
|
||||||
val filterMode: DisplayMode?
|
val filterMode: DisplayMode?
|
||||||
get() = mFilterMode
|
get() = _filterMode
|
||||||
|
|
||||||
init {
|
init {
|
||||||
mFilterMode = settingsManager.searchFilterMode
|
_filterMode = settingsManager.searchFilterMode
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Use [query] to perform a search of the music library. Will push results to [searchResults].
|
* Use [query] to perform a search of the music library. Will push results to [searchResults].
|
||||||
*/
|
*/
|
||||||
fun search(context: Context, query: String?) {
|
fun search(context: Context, query: String?) {
|
||||||
mLastQuery = query
|
lastQuery = query
|
||||||
|
|
||||||
val library = musicStore.library
|
val library = musicStore.library
|
||||||
if (query.isNullOrEmpty() || library == null) {
|
if (query.isNullOrEmpty() || library == null) {
|
||||||
logD("No music/query, ignoring search")
|
logD("No music/query, ignoring search")
|
||||||
mSearchResults.value = listOf()
|
_searchResults.value = listOf()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -79,48 +79,48 @@ class SearchViewModel : ViewModel() {
|
||||||
|
|
||||||
// Note: a filter mode of null means to not filter at all.
|
// Note: a filter mode of null means to not filter at all.
|
||||||
|
|
||||||
if (mFilterMode == null || mFilterMode == DisplayMode.SHOW_ARTISTS) {
|
if (_filterMode == null || _filterMode == DisplayMode.SHOW_ARTISTS) {
|
||||||
library.artists.filterByOrNull(context, query)?.let { artists ->
|
library.artists.filterByOrNull(context, query)?.let { artists ->
|
||||||
results.add(Header(-1, R.string.lbl_artists))
|
results.add(Header(-1, R.string.lbl_artists))
|
||||||
results.addAll(sort.artists(artists))
|
results.addAll(sort.artists(artists))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (mFilterMode == null || mFilterMode == DisplayMode.SHOW_ALBUMS) {
|
if (_filterMode == null || _filterMode == DisplayMode.SHOW_ALBUMS) {
|
||||||
library.albums.filterByOrNull(context, query)?.let { albums ->
|
library.albums.filterByOrNull(context, query)?.let { albums ->
|
||||||
results.add(Header(-2, R.string.lbl_albums))
|
results.add(Header(-2, R.string.lbl_albums))
|
||||||
results.addAll(sort.albums(albums))
|
results.addAll(sort.albums(albums))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (mFilterMode == null || mFilterMode == DisplayMode.SHOW_GENRES) {
|
if (_filterMode == null || _filterMode == DisplayMode.SHOW_GENRES) {
|
||||||
library.genres.filterByOrNull(context, query)?.let { genres ->
|
library.genres.filterByOrNull(context, query)?.let { genres ->
|
||||||
results.add(Header(-3, R.string.lbl_genres))
|
results.add(Header(-3, R.string.lbl_genres))
|
||||||
results.addAll(sort.genres(genres))
|
results.addAll(sort.genres(genres))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (mFilterMode == null || mFilterMode == DisplayMode.SHOW_SONGS) {
|
if (_filterMode == null || _filterMode == DisplayMode.SHOW_SONGS) {
|
||||||
library.songs.filterByOrNull(context, query)?.let { songs ->
|
library.songs.filterByOrNull(context, query)?.let { songs ->
|
||||||
results.add(Header(-4, R.string.lbl_songs))
|
results.add(Header(-4, R.string.lbl_songs))
|
||||||
results.addAll(sort.songs(songs))
|
results.addAll(sort.songs(songs))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
mSearchResults.value = results
|
_searchResults.value = results
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Re-search the library using the last query. Will push results to [searchResults]. */
|
/** Re-search the library using the last query. Will push results to [searchResults]. */
|
||||||
fun refresh(context: Context) {
|
fun refresh(context: Context) {
|
||||||
search(context, mLastQuery)
|
search(context, lastQuery)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Update the current filter mode with a menu [id]. New value will be pushed to [filterMode].
|
* Update the current filter mode with a menu [id]. New value will be pushed to [filterMode].
|
||||||
*/
|
*/
|
||||||
fun updateFilterModeWithId(context: Context, @IdRes id: Int) {
|
fun updateFilterModeWithId(context: Context, @IdRes id: Int) {
|
||||||
mFilterMode =
|
_filterMode =
|
||||||
when (id) {
|
when (id) {
|
||||||
R.id.option_filter_songs -> DisplayMode.SHOW_SONGS
|
R.id.option_filter_songs -> DisplayMode.SHOW_SONGS
|
||||||
R.id.option_filter_albums -> DisplayMode.SHOW_ALBUMS
|
R.id.option_filter_albums -> DisplayMode.SHOW_ALBUMS
|
||||||
|
@ -129,9 +129,9 @@ class SearchViewModel : ViewModel() {
|
||||||
else -> null
|
else -> null
|
||||||
}
|
}
|
||||||
|
|
||||||
logD("Updating filter mode to $mFilterMode")
|
logD("Updating filter mode to $_filterMode")
|
||||||
|
|
||||||
settingsManager.searchFilterMode = mFilterMode
|
settingsManager.searchFilterMode = _filterMode
|
||||||
|
|
||||||
refresh(context)
|
refresh(context)
|
||||||
}
|
}
|
||||||
|
|
|
@ -32,8 +32,8 @@ import org.oxycblt.auxio.BuildConfig
|
||||||
import org.oxycblt.auxio.R
|
import org.oxycblt.auxio.R
|
||||||
import org.oxycblt.auxio.databinding.FragmentAboutBinding
|
import org.oxycblt.auxio.databinding.FragmentAboutBinding
|
||||||
import org.oxycblt.auxio.home.HomeViewModel
|
import org.oxycblt.auxio.home.HomeViewModel
|
||||||
import org.oxycblt.auxio.music.toDuration
|
|
||||||
import org.oxycblt.auxio.ui.ViewBindingFragment
|
import org.oxycblt.auxio.ui.ViewBindingFragment
|
||||||
|
import org.oxycblt.auxio.util.formatDuration
|
||||||
import org.oxycblt.auxio.util.logD
|
import org.oxycblt.auxio.util.logD
|
||||||
import org.oxycblt.auxio.util.showToast
|
import org.oxycblt.auxio.util.showToast
|
||||||
import org.oxycblt.auxio.util.systemBarInsetsCompat
|
import org.oxycblt.auxio.util.systemBarInsetsCompat
|
||||||
|
@ -64,7 +64,8 @@ class AboutFragment : ViewBindingFragment<FragmentAboutBinding>() {
|
||||||
homeModel.songs.observe(viewLifecycleOwner) { songs ->
|
homeModel.songs.observe(viewLifecycleOwner) { songs ->
|
||||||
binding.aboutSongCount.textSafe = getString(R.string.fmt_songs_loaded, songs.size)
|
binding.aboutSongCount.textSafe = getString(R.string.fmt_songs_loaded, songs.size)
|
||||||
binding.aboutTotalDuration.textSafe =
|
binding.aboutTotalDuration.textSafe =
|
||||||
getString(R.string.fmt_total_duration, songs.sumOf { it.seconds }.toDuration(false))
|
getString(
|
||||||
|
R.string.fmt_total_duration, songs.sumOf { it.seconds }.formatDuration(false))
|
||||||
}
|
}
|
||||||
|
|
||||||
homeModel.albums.observe(viewLifecycleOwner) { albums ->
|
homeModel.albums.observe(viewLifecycleOwner) { albums ->
|
||||||
|
|
|
@ -24,36 +24,39 @@ import org.oxycblt.auxio.music.Music
|
||||||
|
|
||||||
/** A ViewModel that handles complicated navigation situations. */
|
/** A ViewModel that handles complicated navigation situations. */
|
||||||
class NavigationViewModel : ViewModel() {
|
class NavigationViewModel : ViewModel() {
|
||||||
private val mMainNavigationAction = MutableLiveData<MainNavigationAction?>()
|
private val _mainNavigationAction = MutableLiveData<MainNavigationAction?>()
|
||||||
/** Flag for main fragment navigation. Intended for MainFragment use only. */
|
/** Flag for main fragment navigation. Intended for MainFragment use only. */
|
||||||
val mainNavigationAction: LiveData<MainNavigationAction?>
|
val mainNavigationAction: LiveData<MainNavigationAction?>
|
||||||
get() = mMainNavigationAction
|
get() = _mainNavigationAction
|
||||||
|
|
||||||
private val mExploreNavigationItem = MutableLiveData<Music?>()
|
private val _exploreNavigationItem = MutableLiveData<Music?>()
|
||||||
/** Flag for unified navigation. Observe this to coordinate navigation to an item's UI. */
|
/**
|
||||||
|
* Flag for navigation within the explore fragments. Observe this to coordinate navigation to an
|
||||||
|
* item's UI.
|
||||||
|
*/
|
||||||
val exploreNavigationItem: LiveData<Music?>
|
val exploreNavigationItem: LiveData<Music?>
|
||||||
get() = mExploreNavigationItem
|
get() = _exploreNavigationItem
|
||||||
|
|
||||||
/** Notify MainFragment to navigate to the location outlined in [MainNavigationAction]. */
|
/** Notify MainFragment to navigate to the location outlined in [MainNavigationAction]. */
|
||||||
fun mainNavigateTo(action: MainNavigationAction) {
|
fun mainNavigateTo(action: MainNavigationAction) {
|
||||||
if (mMainNavigationAction.value != null) return
|
if (_mainNavigationAction.value != null) return
|
||||||
mMainNavigationAction.value = action
|
_mainNavigationAction.value = action
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Mark that the main navigation process is done. */
|
/** Mark that the main navigation process is done. */
|
||||||
fun finishMainNavigation() {
|
fun finishMainNavigation() {
|
||||||
mMainNavigationAction.value = null
|
_mainNavigationAction.value = null
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Navigate to an item's detail menu, whether a song/album/artist */
|
/** Navigate to an item's detail menu, whether a song/album/artist */
|
||||||
fun exploreNavigateTo(item: Music) {
|
fun exploreNavigateTo(item: Music) {
|
||||||
if (mExploreNavigationItem.value != null) return
|
if (_exploreNavigationItem.value != null) return
|
||||||
mExploreNavigationItem.value = item
|
_exploreNavigationItem.value = item
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Mark that the item navigation process is done. */
|
/** Mark that the item navigation process is done. */
|
||||||
fun finishExploreNavigation() {
|
fun finishExploreNavigation() {
|
||||||
mExploreNavigationItem.value = null
|
_exploreNavigationItem.value = null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -161,13 +161,13 @@ abstract class BackingData<T> {
|
||||||
* [AsyncBackingData] is not preferable due to bugs involving diffing.
|
* [AsyncBackingData] is not preferable due to bugs involving diffing.
|
||||||
*/
|
*/
|
||||||
class PrimitiveBackingData<T>(private val adapter: RecyclerView.Adapter<*>) : BackingData<T>() {
|
class PrimitiveBackingData<T>(private val adapter: RecyclerView.Adapter<*>) : BackingData<T>() {
|
||||||
private var mCurrentList = mutableListOf<T>()
|
private var _currentList = mutableListOf<T>()
|
||||||
/** The current list backing this adapter. */
|
/** The current list backing this adapter. */
|
||||||
val currentList: List<T>
|
val currentList: List<T>
|
||||||
get() = mCurrentList
|
get() = _currentList
|
||||||
|
|
||||||
override fun getItem(position: Int): T = mCurrentList[position]
|
override fun getItem(position: Int): T = _currentList[position]
|
||||||
override fun getItemCount(): Int = mCurrentList.size
|
override fun getItemCount(): Int = _currentList.size
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Update the list with a [newList]. This calls [RecyclerView.Adapter.notifyDataSetChanged]
|
* Update the list with a [newList]. This calls [RecyclerView.Adapter.notifyDataSetChanged]
|
||||||
|
@ -175,7 +175,7 @@ class PrimitiveBackingData<T>(private val adapter: RecyclerView.Adapter<*>) : Ba
|
||||||
*/
|
*/
|
||||||
@Suppress("NotifyDatasetChanged")
|
@Suppress("NotifyDatasetChanged")
|
||||||
fun submitList(newList: List<T>) {
|
fun submitList(newList: List<T>) {
|
||||||
mCurrentList = newList.toMutableList()
|
_currentList = newList.toMutableList()
|
||||||
adapter.notifyDataSetChanged()
|
adapter.notifyDataSetChanged()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -276,10 +276,10 @@ sealed class Sort(open val isAscending: Boolean) {
|
||||||
* a non-equal result being propagated upwards.
|
* a non-equal result being propagated upwards.
|
||||||
*/
|
*/
|
||||||
class MultiComparator<T>(vararg comparators: Comparator<T>) : Comparator<T> {
|
class MultiComparator<T>(vararg comparators: Comparator<T>) : Comparator<T> {
|
||||||
private val mComparators = comparators
|
private val _comparators = comparators
|
||||||
|
|
||||||
override fun compare(a: T?, b: T?): Int {
|
override fun compare(a: T?, b: T?): Int {
|
||||||
for (comparator in mComparators) {
|
for (comparator in _comparators) {
|
||||||
val result = comparator.compare(a, b)
|
val result = comparator.compare(a, b)
|
||||||
if (result != 0) {
|
if (result != 0) {
|
||||||
return result
|
return result
|
||||||
|
|
|
@ -29,7 +29,7 @@ import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||||
import org.oxycblt.auxio.util.logD
|
import org.oxycblt.auxio.util.logD
|
||||||
|
|
||||||
abstract class ViewBindingDialogFragment<T : ViewBinding> : DialogFragment() {
|
abstract class ViewBindingDialogFragment<T : ViewBinding> : DialogFragment() {
|
||||||
private var mBinding: T? = null
|
private var _binding: T? = null
|
||||||
|
|
||||||
protected abstract fun onCreateBinding(inflater: LayoutInflater): T
|
protected abstract fun onCreateBinding(inflater: LayoutInflater): T
|
||||||
protected open fun onBindingCreated(binding: T, savedInstanceState: Bundle?) {}
|
protected open fun onBindingCreated(binding: T, savedInstanceState: Bundle?) {}
|
||||||
|
@ -37,10 +37,10 @@ abstract class ViewBindingDialogFragment<T : ViewBinding> : DialogFragment() {
|
||||||
protected open fun onConfigDialog(builder: AlertDialog.Builder) {}
|
protected open fun onConfigDialog(builder: AlertDialog.Builder) {}
|
||||||
|
|
||||||
protected val binding: T?
|
protected val binding: T?
|
||||||
get() = mBinding
|
get() = _binding
|
||||||
|
|
||||||
protected fun requireBinding(): T {
|
protected fun requireBinding(): T {
|
||||||
return requireNotNull(mBinding) {
|
return requireNotNull(_binding) {
|
||||||
"ViewBinding was not available, as the fragment was not in a valid state"
|
"ViewBinding was not available, as the fragment was not in a valid state"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -49,7 +49,7 @@ abstract class ViewBindingDialogFragment<T : ViewBinding> : DialogFragment() {
|
||||||
inflater: LayoutInflater,
|
inflater: LayoutInflater,
|
||||||
container: ViewGroup?,
|
container: ViewGroup?,
|
||||||
savedInstanceState: Bundle?
|
savedInstanceState: Bundle?
|
||||||
): View = onCreateBinding(inflater).also { mBinding = it }.root
|
): View = onCreateBinding(inflater).also { _binding = it }.root
|
||||||
|
|
||||||
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
|
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
|
||||||
return MaterialAlertDialogBuilder(requireActivity(), theme).run {
|
return MaterialAlertDialogBuilder(requireActivity(), theme).run {
|
||||||
|
@ -68,6 +68,6 @@ abstract class ViewBindingDialogFragment<T : ViewBinding> : DialogFragment() {
|
||||||
override fun onDestroyView() {
|
override fun onDestroyView() {
|
||||||
super.onDestroyView()
|
super.onDestroyView()
|
||||||
onDestroyBinding(requireBinding())
|
onDestroyBinding(requireBinding())
|
||||||
mBinding = null
|
_binding = null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -27,17 +27,17 @@ import org.oxycblt.auxio.util.logD
|
||||||
|
|
||||||
/** A fragment enabling ViewBinding inflation and usage across the fragment lifecycle. */
|
/** A fragment enabling ViewBinding inflation and usage across the fragment lifecycle. */
|
||||||
abstract class ViewBindingFragment<T : ViewBinding> : Fragment() {
|
abstract class ViewBindingFragment<T : ViewBinding> : Fragment() {
|
||||||
private var mBinding: T? = null
|
private var _binding: T? = null
|
||||||
|
|
||||||
protected abstract fun onCreateBinding(inflater: LayoutInflater): T
|
protected abstract fun onCreateBinding(inflater: LayoutInflater): T
|
||||||
protected open fun onBindingCreated(binding: T, savedInstanceState: Bundle?) {}
|
protected open fun onBindingCreated(binding: T, savedInstanceState: Bundle?) {}
|
||||||
protected open fun onDestroyBinding(binding: T) {}
|
protected open fun onDestroyBinding(binding: T) {}
|
||||||
|
|
||||||
protected val binding: T?
|
protected val binding: T?
|
||||||
get() = mBinding
|
get() = _binding
|
||||||
|
|
||||||
protected fun requireBinding(): T {
|
protected fun requireBinding(): T {
|
||||||
return requireNotNull(mBinding) {
|
return requireNotNull(_binding) {
|
||||||
"ViewBinding was not available, as the fragment was not in a valid state"
|
"ViewBinding was not available, as the fragment was not in a valid state"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -46,7 +46,7 @@ abstract class ViewBindingFragment<T : ViewBinding> : Fragment() {
|
||||||
inflater: LayoutInflater,
|
inflater: LayoutInflater,
|
||||||
container: ViewGroup?,
|
container: ViewGroup?,
|
||||||
savedInstanceState: Bundle?
|
savedInstanceState: Bundle?
|
||||||
): View = onCreateBinding(inflater).also { mBinding = it }.root
|
): View = onCreateBinding(inflater).also { _binding = it }.root
|
||||||
|
|
||||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||||
super.onViewCreated(view, savedInstanceState)
|
super.onViewCreated(view, savedInstanceState)
|
||||||
|
@ -57,6 +57,6 @@ abstract class ViewBindingFragment<T : ViewBinding> : Fragment() {
|
||||||
override fun onDestroyView() {
|
override fun onDestroyView() {
|
||||||
super.onDestroyView()
|
super.onDestroyView()
|
||||||
onDestroyBinding(requireBinding())
|
onDestroyBinding(requireBinding())
|
||||||
mBinding = null
|
_binding = null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -18,6 +18,7 @@
|
||||||
package org.oxycblt.auxio.util
|
package org.oxycblt.auxio.util
|
||||||
|
|
||||||
import android.os.Looper
|
import android.os.Looper
|
||||||
|
import android.text.format.DateUtils
|
||||||
import androidx.core.math.MathUtils
|
import androidx.core.math.MathUtils
|
||||||
import org.oxycblt.auxio.BuildConfig
|
import org.oxycblt.auxio.BuildConfig
|
||||||
|
|
||||||
|
@ -45,3 +46,24 @@ fun Int.clamp(min: Int, max: Int): Int = MathUtils.clamp(this, min, max)
|
||||||
|
|
||||||
/** Shortcut to clamp an integer between [min] and [max] */
|
/** Shortcut to clamp an integer between [min] and [max] */
|
||||||
fun Long.clamp(min: Long, max: Long): Long = MathUtils.clamp(this, min, max)
|
fun Long.clamp(min: Long, max: Long): Long = MathUtils.clamp(this, min, max)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convert a [Long] of seconds into a string duration.
|
||||||
|
* @param isElapsed Whether this duration is represents elapsed time. If this is false, then --:--
|
||||||
|
* will be returned if the second value is 0.
|
||||||
|
*/
|
||||||
|
fun Long.formatDuration(isElapsed: Boolean): String {
|
||||||
|
if (!isElapsed && this == 0L) {
|
||||||
|
logD("Non-elapsed duration is zero, using --:--")
|
||||||
|
return "--:--"
|
||||||
|
}
|
||||||
|
|
||||||
|
var durationString = DateUtils.formatElapsedTime(this)
|
||||||
|
|
||||||
|
// If the duration begins with a excess zero [e.g 01:42], then cut it off.
|
||||||
|
if (durationString[0] == '0') {
|
||||||
|
durationString = durationString.slice(1 until durationString.length)
|
||||||
|
}
|
||||||
|
|
||||||
|
return durationString
|
||||||
|
}
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<resources>
|
<resources>
|
||||||
|
<!-- Note: Not actually transparent, making it transparent would actually make it translucent -->
|
||||||
<color name="chrome_transparent">#01151515</color>
|
<color name="chrome_transparent">#01151515</color>
|
||||||
|
|
||||||
<color name="red_primary">#FFB4A8</color>
|
<color name="red_primary">#FFB4A8</color>
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<resources>
|
<resources>
|
||||||
|
<!-- Note: Not actually transparent, making it transparent would actually make it translucent -->
|
||||||
<color name="chrome_transparent">#01fafafa</color>
|
<color name="chrome_transparent">#01fafafa</color>
|
||||||
<color name="chrome_translucent">#80000000</color>
|
<color name="chrome_translucent">#80000000</color>
|
||||||
|
|
||||||
|
|
|
@ -9,7 +9,7 @@ buildscript {
|
||||||
}
|
}
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
classpath 'com.android.tools.build:gradle:7.1.3'
|
classpath 'com.android.tools.build:gradle:7.2.0'
|
||||||
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
|
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
|
||||||
classpath "androidx.navigation:navigation-safe-args-gradle-plugin:$navigation_version"
|
classpath "androidx.navigation:navigation-safe-args-gradle-plugin:$navigation_version"
|
||||||
classpath "com.diffplug.spotless:spotless-plugin-gradle:6.3.0"
|
classpath "com.diffplug.spotless:spotless-plugin-gradle:6.3.0"
|
||||||
|
|
Loading…
Reference in a new issue