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 {
|
||||
compileSdkVersion 32
|
||||
buildToolsVersion "32.0.0"
|
||||
|
||||
defaultConfig {
|
||||
applicationId "org.oxycblt.auxio"
|
||||
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 {
|
||||
debug {
|
||||
debuggable true
|
||||
|
@ -35,17 +46,6 @@ android {
|
|||
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 {
|
||||
|
@ -102,7 +102,7 @@ dependencies {
|
|||
implementation "io.coil-kt:coil:2.0.0-rc03"
|
||||
|
||||
// Material
|
||||
implementation "com.google.android.material:material:1.6.0-rc01"
|
||||
implementation "com.google.android.material:material:1.6.0"
|
||||
|
||||
// LeakCanary
|
||||
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: Phase out m for _
|
||||
*
|
||||
* TODO: Fix how selection works in the RecyclerViews (doing it poorly right now)
|
||||
*
|
||||
* TODO: Rework padding ethos
|
||||
|
|
|
@ -275,6 +275,6 @@ abstract class BaseFetcher : Fetcher {
|
|||
|
||||
private fun Dimension.mosaicSize(): Int {
|
||||
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,
|
||||
) : BaseFetcher() {
|
||||
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 results = albums.mapAtMost(4) { album -> fetchArt(context, album) }
|
||||
|
||||
|
|
|
@ -22,7 +22,6 @@ import coil.size.Size
|
|||
import coil.size.pxOrElse
|
||||
import coil.transform.Transformation
|
||||
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
|
||||
|
@ -46,12 +45,7 @@ class SquareFrameTransform : Transformation {
|
|||
val dst = Bitmap.createBitmap(input, x, y, dstSize, dstSize)
|
||||
|
||||
if (dstSize != desiredWidth || dstSize != desiredHeight) {
|
||||
try {
|
||||
// Desired size differs from the cropped size, resize the bitmap.
|
||||
return Bitmap.createScaledBitmap(dst, desiredWidth, desiredHeight, true)
|
||||
} catch (e: Exception) {
|
||||
logE(e.stackTraceToString())
|
||||
}
|
||||
return Bitmap.createScaledBitmap(dst, desiredWidth, desiredHeight, true)
|
||||
}
|
||||
|
||||
return dst
|
||||
|
|
|
@ -19,6 +19,7 @@ package org.oxycblt.auxio.detail
|
|||
|
||||
import android.content.Context
|
||||
import android.os.Bundle
|
||||
import android.view.MenuItem
|
||||
import android.view.View
|
||||
import androidx.core.view.children
|
||||
import androidx.navigation.fragment.findNavController
|
||||
|
@ -54,23 +55,7 @@ class AlbumDetailFragment : DetailFragment(), AlbumDetailAdapter.Listener {
|
|||
override fun onBindingCreated(binding: FragmentDetailBinding, savedInstanceState: Bundle?) {
|
||||
detailModel.setAlbumId(args.albumId)
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
setupToolbar(unlikelyToBeNull(detailModel.currentAlbum.value), R.menu.menu_album_detail)
|
||||
requireBinding().detailRecycler.apply {
|
||||
adapter = detailAdapter
|
||||
applySpans { pos ->
|
||||
|
@ -86,6 +71,22 @@ class AlbumDetailFragment : DetailFragment(), AlbumDetailAdapter.Listener {
|
|||
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) {
|
||||
if (item is Song) {
|
||||
playbackModel.playSong(item, PlaybackMode.IN_ALBUM)
|
||||
|
|
|
@ -18,6 +18,7 @@
|
|||
package org.oxycblt.auxio.detail
|
||||
|
||||
import android.os.Bundle
|
||||
import android.view.MenuItem
|
||||
import android.view.View
|
||||
import androidx.navigation.fragment.findNavController
|
||||
import androidx.navigation.fragment.navArgs
|
||||
|
@ -69,6 +70,8 @@ class ArtistDetailFragment : DetailFragment(), DetailAdapter.Listener {
|
|||
playbackModel.parent.observe(viewLifecycleOwner, ::updateParent)
|
||||
}
|
||||
|
||||
override fun onMenuItemClick(item: MenuItem): Boolean = false
|
||||
|
||||
override fun onItemClick(item: Item) {
|
||||
when (item) {
|
||||
is Song -> playbackModel.playSong(item, PlaybackMode.IN_ARTIST)
|
||||
|
|
|
@ -46,11 +46,11 @@ class DetailAppBarLayout
|
|||
@JvmOverloads
|
||||
constructor(context: Context, attrs: AttributeSet? = null, @AttrRes defStyleAttr: Int = 0) :
|
||||
EdgeAppBarLayout(context, attrs, defStyleAttr) {
|
||||
private var mTitleView: AppCompatTextView? = null
|
||||
private var mRecycler: RecyclerView? = null
|
||||
private var titleView: AppCompatTextView? = null
|
||||
private var recycler: RecyclerView? = null
|
||||
|
||||
private var titleShown: Boolean? = null
|
||||
private var mTitleAnimator: ValueAnimator? = null
|
||||
private var titleAnimator: ValueAnimator? = null
|
||||
|
||||
override fun onAttachedToWindow() {
|
||||
super.onAttachedToWindow()
|
||||
|
@ -58,7 +58,7 @@ constructor(context: Context, attrs: AttributeSet? = null, @AttrRes defStyleAttr
|
|||
}
|
||||
|
||||
private fun findTitleView(): AppCompatTextView? {
|
||||
val titleView = mTitleView
|
||||
val titleView = titleView
|
||||
if (titleView != null) {
|
||||
return titleView
|
||||
}
|
||||
|
@ -79,12 +79,12 @@ constructor(context: Context, attrs: AttributeSet? = null, @AttrRes defStyleAttr
|
|||
}
|
||||
|
||||
newTitleView.alpha = 0f
|
||||
mTitleView = newTitleView
|
||||
this.titleView = newTitleView
|
||||
return newTitleView
|
||||
}
|
||||
|
||||
private fun findRecyclerView(): RecyclerView {
|
||||
val recycler = mRecycler
|
||||
val recycler = recycler
|
||||
|
||||
if (recycler != null) {
|
||||
return recycler
|
||||
|
@ -92,7 +92,7 @@ constructor(context: Context, attrs: AttributeSet? = null, @AttrRes defStyleAttr
|
|||
|
||||
val newRecycler = (parent as ViewGroup).findViewById<RecyclerView>(liftOnScrollTargetViewId)
|
||||
|
||||
mRecycler = newRecycler
|
||||
this.recycler = newRecycler
|
||||
return newRecycler
|
||||
}
|
||||
|
||||
|
@ -101,10 +101,10 @@ constructor(context: Context, attrs: AttributeSet? = null, @AttrRes defStyleAttr
|
|||
|
||||
titleShown = visible
|
||||
|
||||
val titleAnimator = mTitleAnimator
|
||||
val titleAnimator = titleAnimator
|
||||
if (titleAnimator != null) {
|
||||
titleAnimator.cancel()
|
||||
mTitleAnimator = null
|
||||
this.titleAnimator = null
|
||||
}
|
||||
|
||||
val titleView = findTitleView()
|
||||
|
@ -121,7 +121,7 @@ constructor(context: Context, attrs: AttributeSet? = null, @AttrRes defStyleAttr
|
|||
|
||||
if (titleView?.alpha == to) return
|
||||
|
||||
mTitleAnimator =
|
||||
this.titleAnimator =
|
||||
ValueAnimator.ofFloat(from, to).apply {
|
||||
addUpdateListener { titleView?.alpha = it.animatedValue as Float }
|
||||
|
||||
|
|
|
@ -21,6 +21,7 @@ import android.view.LayoutInflater
|
|||
import android.view.View
|
||||
import androidx.annotation.MenuRes
|
||||
import androidx.appcompat.widget.PopupMenu
|
||||
import androidx.appcompat.widget.Toolbar
|
||||
import androidx.core.view.children
|
||||
import androidx.fragment.app.Fragment
|
||||
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.
|
||||
* @author OxygenCobalt
|
||||
*/
|
||||
abstract class DetailFragment : ViewBindingFragment<FragmentDetailBinding>() {
|
||||
abstract class DetailFragment :
|
||||
ViewBindingFragment<FragmentDetailBinding>(), Toolbar.OnMenuItemClickListener {
|
||||
protected val detailModel: DetailViewModel by activityViewModels()
|
||||
protected val navModel: NavigationViewModel by activityViewModels()
|
||||
protected val playbackModel: PlaybackViewModel by activityViewModels()
|
||||
|
@ -49,6 +51,7 @@ abstract class DetailFragment : ViewBindingFragment<FragmentDetailBinding>() {
|
|||
|
||||
override fun onDestroyBinding(binding: FragmentDetailBinding) {
|
||||
super.onDestroyBinding(binding)
|
||||
binding.detailToolbar.setOnMenuItemClickListener(null)
|
||||
binding.detailRecycler.adapter = null
|
||||
}
|
||||
|
||||
|
@ -56,13 +59,8 @@ abstract class DetailFragment : ViewBindingFragment<FragmentDetailBinding>() {
|
|||
* Shortcut method for doing setup of the detail toolbar.
|
||||
* @param data Parent data to use as the toolbar title
|
||||
* @param menuId Menu resource to use
|
||||
* @param onMenuClick (Optional) a click listener for that menu
|
||||
*/
|
||||
protected fun setupToolbar(
|
||||
data: MusicParent,
|
||||
@MenuRes menuId: Int = -1,
|
||||
onMenuClick: ((itemId: Int) -> Boolean)? = null
|
||||
) {
|
||||
protected fun setupToolbar(data: MusicParent, @MenuRes menuId: Int = -1) {
|
||||
requireBinding().detailToolbar.apply {
|
||||
title = data.resolveName(context)
|
||||
|
||||
|
@ -71,10 +69,7 @@ abstract class DetailFragment : ViewBindingFragment<FragmentDetailBinding>() {
|
|||
}
|
||||
|
||||
setNavigationOnClickListener { findNavController().navigateUp() }
|
||||
|
||||
onMenuClick?.let { onClick ->
|
||||
setOnMenuItemClickListener { item -> onClick(item.itemId) }
|
||||
}
|
||||
setOnMenuItemClickListener(this@DetailFragment)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -44,13 +44,13 @@ class DetailViewModel : ViewModel() {
|
|||
private val musicStore = MusicStore.getInstance()
|
||||
private val settingsManager = SettingsManager.getInstance()
|
||||
|
||||
private val mCurrentAlbum = MutableLiveData<Album?>()
|
||||
private val _currentAlbum = MutableLiveData<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>>
|
||||
get() = mAlbumData
|
||||
get() = _albumData
|
||||
|
||||
var albumSort: Sort
|
||||
get() = settingsManager.detailAlbumSort
|
||||
|
@ -59,12 +59,12 @@ class DetailViewModel : ViewModel() {
|
|||
currentAlbum.value?.let(::refreshAlbumData)
|
||||
}
|
||||
|
||||
private val mCurrentArtist = MutableLiveData<Artist?>()
|
||||
private val _currentArtist = MutableLiveData<Artist?>()
|
||||
val currentArtist: LiveData<Artist?>
|
||||
get() = mCurrentArtist
|
||||
get() = _currentArtist
|
||||
|
||||
private val mArtistData = MutableLiveData(listOf<Item>())
|
||||
val artistData: LiveData<List<Item>> = mArtistData
|
||||
private val _artistData = MutableLiveData(listOf<Item>())
|
||||
val artistData: LiveData<List<Item>> = _artistData
|
||||
|
||||
var artistSort: Sort
|
||||
get() = settingsManager.detailArtistSort
|
||||
|
@ -73,12 +73,12 @@ class DetailViewModel : ViewModel() {
|
|||
currentArtist.value?.let(::refreshArtistData)
|
||||
}
|
||||
|
||||
private val mCurrentGenre = MutableLiveData<Genre?>()
|
||||
private val _currentGenre = MutableLiveData<Genre?>()
|
||||
val currentGenre: LiveData<Genre?>
|
||||
get() = mCurrentGenre
|
||||
get() = _currentGenre
|
||||
|
||||
private val mGenreData = MutableLiveData(listOf<Item>())
|
||||
val genreData: LiveData<List<Item>> = mGenreData
|
||||
private val _genreData = MutableLiveData(listOf<Item>())
|
||||
val genreData: LiveData<List<Item>> = _genreData
|
||||
|
||||
var genreSort: Sort
|
||||
get() = settingsManager.detailGenreSort
|
||||
|
@ -88,30 +88,30 @@ class DetailViewModel : ViewModel() {
|
|||
}
|
||||
|
||||
fun setAlbumId(id: Long) {
|
||||
if (mCurrentAlbum.value?.id == id) return
|
||||
if (_currentAlbum.value?.id == id) return
|
||||
val library = unlikelyToBeNull(musicStore.library)
|
||||
val album =
|
||||
requireNotNull(library.albums.find { it.id == id }) { "Invalid album id provided " }
|
||||
|
||||
mCurrentAlbum.value = album
|
||||
_currentAlbum.value = album
|
||||
refreshAlbumData(album)
|
||||
}
|
||||
|
||||
fun setArtistId(id: Long) {
|
||||
if (mCurrentArtist.value?.id == id) return
|
||||
if (_currentArtist.value?.id == id) return
|
||||
val library = unlikelyToBeNull(musicStore.library)
|
||||
val artist =
|
||||
requireNotNull(library.artists.find { it.id == id }) { "Invalid artist id provided" }
|
||||
mCurrentArtist.value = artist
|
||||
_currentArtist.value = artist
|
||||
refreshArtistData(artist)
|
||||
}
|
||||
|
||||
fun setGenreId(id: Long) {
|
||||
if (mCurrentGenre.value?.id == id) return
|
||||
if (_currentGenre.value?.id == id) return
|
||||
val library = unlikelyToBeNull(musicStore.library)
|
||||
val genre =
|
||||
requireNotNull(library.genres.find { it.id == id }) { "Invalid genre id provided" }
|
||||
mCurrentGenre.value = genre
|
||||
_currentGenre.value = genre
|
||||
refreshGenreData(genre)
|
||||
}
|
||||
|
||||
|
@ -120,7 +120,7 @@ class DetailViewModel : ViewModel() {
|
|||
val data = mutableListOf<Item>(genre)
|
||||
data.add(SortHeader(-2, R.string.lbl_songs))
|
||||
data.addAll(genreSort.genre(genre))
|
||||
mGenreData.value = data
|
||||
_genreData.value = data
|
||||
}
|
||||
|
||||
private fun refreshArtistData(artist: Artist) {
|
||||
|
@ -130,7 +130,7 @@ class DetailViewModel : ViewModel() {
|
|||
data.addAll(Sort.ByYear(false).albums(artist.albums))
|
||||
data.add(SortHeader(-3, R.string.lbl_songs))
|
||||
data.addAll(artistSort.artist(artist))
|
||||
mArtistData.value = data.toList()
|
||||
_artistData.value = data.toList()
|
||||
}
|
||||
|
||||
private fun refreshAlbumData(album: Album) {
|
||||
|
@ -138,6 +138,6 @@ class DetailViewModel : ViewModel() {
|
|||
val data = mutableListOf<Item>(album)
|
||||
data.add(SortHeader(id = -2, R.string.lbl_songs))
|
||||
data.addAll(albumSort.album(album))
|
||||
mAlbumData.value = data
|
||||
_albumData.value = data
|
||||
}
|
||||
}
|
||||
|
|
|
@ -18,6 +18,7 @@
|
|||
package org.oxycblt.auxio.detail
|
||||
|
||||
import android.os.Bundle
|
||||
import android.view.MenuItem
|
||||
import android.view.View
|
||||
import androidx.navigation.fragment.findNavController
|
||||
import androidx.navigation.fragment.navArgs
|
||||
|
@ -65,6 +66,8 @@ class GenreDetailFragment : DetailFragment(), DetailAdapter.Listener {
|
|||
playbackModel.song.observe(viewLifecycleOwner, ::updateSong)
|
||||
}
|
||||
|
||||
override fun onMenuItemClick(item: MenuItem): Boolean = false
|
||||
|
||||
override fun onItemClick(item: Item) {
|
||||
when (item) {
|
||||
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.music.Album
|
||||
import org.oxycblt.auxio.music.Song
|
||||
import org.oxycblt.auxio.music.toDuration
|
||||
import org.oxycblt.auxio.ui.BindingViewHolder
|
||||
import org.oxycblt.auxio.ui.Item
|
||||
import org.oxycblt.auxio.ui.MenuItemListener
|
||||
import org.oxycblt.auxio.ui.SimpleItemCallback
|
||||
import org.oxycblt.auxio.util.context
|
||||
import org.oxycblt.auxio.util.formatDuration
|
||||
import org.oxycblt.auxio.util.getPluralSafe
|
||||
import org.oxycblt.auxio.util.inflater
|
||||
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.songDuration.textSafe = item.seconds.toDuration(false)
|
||||
binding.songDuration.textSafe = item.seconds.formatDuration(false)
|
||||
|
||||
binding.root.apply {
|
||||
setOnClickListener { listener.onItemClick(item) }
|
||||
|
|
|
@ -20,6 +20,7 @@ package org.oxycblt.auxio.home
|
|||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import android.view.MenuItem
|
||||
import androidx.appcompat.widget.Toolbar
|
||||
import androidx.core.view.iterator
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.fragment.app.activityViewModels
|
||||
|
@ -60,7 +61,7 @@ import org.oxycblt.auxio.util.unlikelyToBeNull
|
|||
*
|
||||
* 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 navModel: NavigationViewModel by activityViewModels()
|
||||
private val homeModel: HomeViewModel by activityViewModels()
|
||||
|
@ -73,11 +74,7 @@ class HomeFragment : ViewBindingFragment<FragmentHomeBinding>() {
|
|||
|
||||
binding.homeToolbar.apply {
|
||||
sortItem = menu.findItem(R.id.submenu_sorting)
|
||||
|
||||
setOnMenuItemClickListener { item ->
|
||||
onMenuClick(item)
|
||||
true
|
||||
}
|
||||
setOnMenuItemClickListener(this@HomeFragment)
|
||||
}
|
||||
|
||||
binding.homePager.apply {
|
||||
|
@ -103,7 +100,7 @@ class HomeFragment : ViewBindingFragment<FragmentHomeBinding>() {
|
|||
|
||||
// --- VIEWMODEL SETUP ---
|
||||
|
||||
homeModel.fastScrolling.observe(viewLifecycleOwner, ::updateFastScrolling)
|
||||
homeModel.isFastScrolling.observe(viewLifecycleOwner, ::updateFastScrolling)
|
||||
homeModel.currentTab.observe(viewLifecycleOwner) { tab -> updateCurrentTab(sortItem, tab) }
|
||||
homeModel.recreateTabs.observe(viewLifecycleOwner, ::handleRecreateTabs)
|
||||
|
||||
|
@ -111,7 +108,12 @@ class HomeFragment : ViewBindingFragment<FragmentHomeBinding>() {
|
|||
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) {
|
||||
R.id.action_search -> {
|
||||
logD("Navigating to search")
|
||||
|
@ -147,6 +149,8 @@ class HomeFragment : ViewBindingFragment<FragmentHomeBinding>() {
|
|||
.assignId(item.itemId)))
|
||||
}
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
private fun updateFastScrolling(isFastScrolling: Boolean) {
|
||||
|
|
|
@ -40,21 +40,21 @@ class HomeViewModel : ViewModel(), SettingsManager.Callback, MusicStore.Callback
|
|||
private val musicStore = MusicStore.getInstance()
|
||||
private val settingsManager = SettingsManager.getInstance()
|
||||
|
||||
private val mSongs = MutableLiveData(listOf<Song>())
|
||||
private val _songs = MutableLiveData(listOf<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>>
|
||||
get() = mAlbums
|
||||
get() = _albums
|
||||
|
||||
private val mArtists = MutableLiveData(listOf<Artist>())
|
||||
private val _artists = MutableLiveData(listOf<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>>
|
||||
get() = mGenres
|
||||
get() = _genres
|
||||
|
||||
var tabs: List<DisplayMode> = visibleTabs
|
||||
private set
|
||||
|
@ -63,18 +63,18 @@ class HomeViewModel : ViewModel(), SettingsManager.Callback, MusicStore.Callback
|
|||
private val visibleTabs: List<DisplayMode>
|
||||
get() = settingsManager.libTabs.filterIsInstance<Tab.Visible>().map { it.mode }
|
||||
|
||||
private val mCurrentTab = MutableLiveData(tabs[0])
|
||||
val currentTab: LiveData<DisplayMode> = mCurrentTab
|
||||
private val _currentTab = MutableLiveData(tabs[0])
|
||||
val currentTab: LiveData<DisplayMode> = _currentTab
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
private val mRecreateTabs = MutableLiveData(false)
|
||||
val recreateTabs: LiveData<Boolean> = mRecreateTabs
|
||||
private val _shouldRecreateTabs = MutableLiveData(false)
|
||||
val recreateTabs: LiveData<Boolean> = _shouldRecreateTabs
|
||||
|
||||
private val mFastScrolling = MutableLiveData(false)
|
||||
val fastScrolling: LiveData<Boolean> = mFastScrolling
|
||||
private val _isFastScrolling = MutableLiveData(false)
|
||||
val isFastScrolling: LiveData<Boolean> = _isFastScrolling
|
||||
|
||||
init {
|
||||
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. */
|
||||
fun updateCurrentTab(pos: Int) {
|
||||
logD("Updating current tab to ${tabs[pos]}")
|
||||
mCurrentTab.value = tabs[pos]
|
||||
_currentTab.value = tabs[pos]
|
||||
}
|
||||
|
||||
fun finishRecreateTabs() {
|
||||
mRecreateTabs.value = false
|
||||
_shouldRecreateTabs.value = false
|
||||
}
|
||||
|
||||
fun getSortForDisplay(displayMode: DisplayMode): Sort {
|
||||
|
@ -102,23 +102,23 @@ class HomeViewModel : ViewModel(), SettingsManager.Callback, MusicStore.Callback
|
|||
|
||||
/** Update the currently displayed item's [Sort]. */
|
||||
fun updateCurrentSort(sort: Sort) {
|
||||
logD("Updating ${mCurrentTab.value} sort to $sort")
|
||||
when (mCurrentTab.value) {
|
||||
logD("Updating ${_currentTab.value} sort to $sort")
|
||||
when (_currentTab.value) {
|
||||
DisplayMode.SHOW_SONGS -> {
|
||||
settingsManager.libSongSort = sort
|
||||
mSongs.value = sort.songs(unlikelyToBeNull(mSongs.value))
|
||||
_songs.value = sort.songs(unlikelyToBeNull(_songs.value))
|
||||
}
|
||||
DisplayMode.SHOW_ALBUMS -> {
|
||||
settingsManager.libAlbumSort = sort
|
||||
mAlbums.value = sort.albums(unlikelyToBeNull(mAlbums.value))
|
||||
_albums.value = sort.albums(unlikelyToBeNull(_albums.value))
|
||||
}
|
||||
DisplayMode.SHOW_ARTISTS -> {
|
||||
settingsManager.libArtistSort = sort
|
||||
mArtists.value = sort.artists(unlikelyToBeNull(mArtists.value))
|
||||
_artists.value = sort.artists(unlikelyToBeNull(_artists.value))
|
||||
}
|
||||
DisplayMode.SHOW_GENRES -> {
|
||||
settingsManager.libGenreSort = sort
|
||||
mGenres.value = sort.genres(unlikelyToBeNull(mGenres.value))
|
||||
_genres.value = sort.genres(unlikelyToBeNull(_genres.value))
|
||||
}
|
||||
else -> {}
|
||||
}
|
||||
|
@ -129,7 +129,7 @@ class HomeViewModel : ViewModel(), SettingsManager.Callback, MusicStore.Callback
|
|||
* begins to fast scroll.
|
||||
*/
|
||||
fun updateFastScrolling(scrolling: Boolean) {
|
||||
mFastScrolling.value = scrolling
|
||||
_isFastScrolling.value = scrolling
|
||||
}
|
||||
|
||||
// --- OVERRIDES ---
|
||||
|
@ -137,16 +137,16 @@ class HomeViewModel : ViewModel(), SettingsManager.Callback, MusicStore.Callback
|
|||
override fun onMusicUpdate(response: MusicStore.Response) {
|
||||
if (response is MusicStore.Response.Ok) {
|
||||
val library = response.library
|
||||
mSongs.value = settingsManager.libSongSort.songs(library.songs)
|
||||
mAlbums.value = settingsManager.libAlbumSort.albums(library.albums)
|
||||
mArtists.value = settingsManager.libArtistSort.artists(library.artists)
|
||||
mGenres.value = settingsManager.libGenreSort.genres(library.genres)
|
||||
_songs.value = settingsManager.libSongSort.songs(library.songs)
|
||||
_albums.value = settingsManager.libAlbumSort.albums(library.albums)
|
||||
_artists.value = settingsManager.libArtistSort.artists(library.artists)
|
||||
_genres.value = settingsManager.libGenreSort.genres(library.genres)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onLibTabsUpdate(libTabs: Array<Tab>) {
|
||||
tabs = visibleTabs
|
||||
mRecreateTabs.value = true
|
||||
_shouldRecreateTabs.value = true
|
||||
}
|
||||
|
||||
override fun onCleared() {
|
||||
|
|
|
@ -106,7 +106,7 @@ object Indexer {
|
|||
private val Context.contentResolverSafe: ContentResolver
|
||||
get() = applicationContext.contentResolver
|
||||
|
||||
fun run(context: Context): MusicStore.Library? {
|
||||
fun index(context: Context): MusicStore.Library? {
|
||||
val songs = loadSongs(context)
|
||||
if (songs.isEmpty()) return null
|
||||
|
||||
|
@ -116,14 +116,12 @@ object Indexer {
|
|||
|
||||
// Sanity check: Ensure that all songs are linked up to albums/artists/genres.
|
||||
for (song in songs) {
|
||||
if (song.internalIsMissingAlbum ||
|
||||
song.internalIsMissingArtist ||
|
||||
song.internalIsMissingGenre) {
|
||||
if (song._isMissingAlbum || song._isMissingArtist || song._isMissingGenre) {
|
||||
throw IllegalStateException(
|
||||
"Found malformed song: ${song.rawName} [" +
|
||||
"album: ${!song.internalIsMissingAlbum} " +
|
||||
"artist: ${!song.internalIsMissingArtist} " +
|
||||
"genre: ${!song.internalIsMissingGenre}]")
|
||||
"album: ${!song._isMissingAlbum} " +
|
||||
"artist: ${!song._isMissingArtist} " +
|
||||
"genre: ${!song._isMissingGenre}]")
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -242,9 +240,9 @@ object Indexer {
|
|||
songs
|
||||
.distinctBy {
|
||||
it.rawName to
|
||||
it.internalMediaStoreAlbumName to
|
||||
it.internalMediaStoreArtistName to
|
||||
it.internalMediaStoreAlbumArtistName to
|
||||
it._mediaStoreAlbumName to
|
||||
it._mediaStoreArtistName to
|
||||
it._mediaStoreAlbumArtistName to
|
||||
it.track to
|
||||
it.duration
|
||||
}
|
||||
|
@ -270,7 +268,7 @@ object Indexer {
|
|||
*/
|
||||
private fun buildAlbums(songs: List<Song>): List<Album> {
|
||||
val albums = mutableListOf<Album>()
|
||||
val songsByAlbum = songs.groupBy { it.internalAlbumGroupingId }
|
||||
val songsByAlbum = songs.groupBy { it._albumGroupingId }
|
||||
|
||||
for (entry in songsByAlbum) {
|
||||
val albumSongs = entry.value
|
||||
|
@ -289,13 +287,13 @@ object Indexer {
|
|||
}
|
||||
}
|
||||
|
||||
val albumName = templateSong.internalMediaStoreAlbumName
|
||||
val albumYear = templateSong.internalMediaStoreYear
|
||||
val albumName = templateSong._mediaStoreAlbumName
|
||||
val albumYear = templateSong._mediaStoreYear
|
||||
val albumCoverUri =
|
||||
ContentUris.withAppendedId(
|
||||
Uri.parse("content://media/external/audio/albumart"),
|
||||
templateSong.internalMediaStoreAlbumId)
|
||||
val artistName = templateSong.internalGroupingArtistName
|
||||
templateSong._mediaStoreAlbumId)
|
||||
val artistName = templateSong._artistGroupingName
|
||||
|
||||
albums.add(
|
||||
Album(
|
||||
|
@ -318,14 +316,14 @@ object Indexer {
|
|||
*/
|
||||
private fun buildArtists(albums: List<Album>): List<Artist> {
|
||||
val artists = mutableListOf<Artist>()
|
||||
val albumsByArtist = albums.groupBy { it.internalArtistGroupingId }
|
||||
val albumsByArtist = albums.groupBy { it._artistGroupingId }
|
||||
|
||||
for (entry in albumsByArtist) {
|
||||
val templateAlbum = entry.value[0]
|
||||
val artistName =
|
||||
when (templateAlbum.internalGroupingArtistName) {
|
||||
when (templateAlbum._artistGroupingName) {
|
||||
MediaStore.UNKNOWN_STRING -> null
|
||||
else -> templateAlbum.internalGroupingArtistName
|
||||
else -> templateAlbum._artistGroupingName
|
||||
}
|
||||
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()) {
|
||||
// Songs that don't have a genre will be thrown into an unknown genre.
|
||||
val unknownGenre = Genre(null, songsWithoutGenres)
|
||||
|
@ -398,9 +396,7 @@ object Indexer {
|
|||
|
||||
while (cursor.moveToNext()) {
|
||||
val id = cursor.getLong(idIndex)
|
||||
songs.find { it.internalMediaStoreId == id }?.let { song ->
|
||||
genreSongs.add(song)
|
||||
}
|
||||
songs.find { it._mediaStoreId == id }?.let { song -> genreSongs.add(song) }
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -24,6 +24,7 @@ import android.provider.MediaStore
|
|||
import androidx.core.text.isDigitsOnly
|
||||
import org.oxycblt.auxio.R
|
||||
import org.oxycblt.auxio.ui.Item
|
||||
import org.oxycblt.auxio.util.formatDuration
|
||||
import org.oxycblt.auxio.util.unlikelyToBeNull
|
||||
|
||||
// --- MUSIC MODELS ---
|
||||
|
@ -59,17 +60,17 @@ data class Song(
|
|||
/** The track number of this song, null if there isn't any. */
|
||||
val track: Int?,
|
||||
/** Internal field. Do not use. */
|
||||
val internalMediaStoreId: Long,
|
||||
val _mediaStoreId: Long,
|
||||
/** Internal field. Do not use. */
|
||||
val internalMediaStoreYear: Int?,
|
||||
val _mediaStoreYear: Int?,
|
||||
/** Internal field. Do not use. */
|
||||
val internalMediaStoreAlbumName: String,
|
||||
val _mediaStoreAlbumName: String,
|
||||
/** Internal field. Do not use. */
|
||||
val internalMediaStoreAlbumId: Long,
|
||||
val _mediaStoreAlbumId: Long,
|
||||
/** Internal field. Do not use. */
|
||||
val internalMediaStoreArtistName: String?,
|
||||
val _mediaStoreArtistName: String?,
|
||||
/** Internal field. Do not use. */
|
||||
val internalMediaStoreAlbumArtistName: String?,
|
||||
val _mediaStoreAlbumArtistName: String?,
|
||||
) : Music() {
|
||||
override val id: Long
|
||||
get() {
|
||||
|
@ -89,68 +90,65 @@ data class Song(
|
|||
/** The URI for this song. */
|
||||
val uri: Uri
|
||||
get() =
|
||||
ContentUris.withAppendedId(
|
||||
MediaStore.Audio.Media.EXTERNAL_CONTENT_URI, internalMediaStoreId)
|
||||
ContentUris.withAppendedId(MediaStore.Audio.Media.EXTERNAL_CONTENT_URI, _mediaStoreId)
|
||||
/** The duration of this song, in seconds (rounded down) */
|
||||
val seconds: Long
|
||||
get() = duration / 1000
|
||||
|
||||
private var mAlbum: Album? = null
|
||||
private var _album: Album? = null
|
||||
/** The album of this song. */
|
||||
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. */
|
||||
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
|
||||
* back to the album artist tag (i.e parent artist name). Null if name is unknown.
|
||||
*/
|
||||
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
|
||||
* falls back to the album artist tag (i.e parent artist name)
|
||||
*/
|
||||
fun resolveIndividualArtistName(context: Context) =
|
||||
internalMediaStoreArtistName ?: album.artist.resolveName(context)
|
||||
_mediaStoreArtistName ?: album.artist.resolveName(context)
|
||||
|
||||
/** Internal field. Do not use. */
|
||||
val internalAlbumGroupingId: Long
|
||||
val _albumGroupingId: Long
|
||||
get() {
|
||||
var result = internalGroupingArtistName.lowercase().hashCode().toLong()
|
||||
result = 31 * result + internalMediaStoreAlbumName.lowercase().hashCode()
|
||||
var result = _artistGroupingName.lowercase().hashCode().toLong()
|
||||
result = 31 * result + _mediaStoreAlbumName.lowercase().hashCode()
|
||||
return result
|
||||
}
|
||||
|
||||
/** Internal field. Do not use. */
|
||||
val internalGroupingArtistName: String
|
||||
get() =
|
||||
internalMediaStoreAlbumArtistName
|
||||
?: internalMediaStoreArtistName ?: MediaStore.UNKNOWN_STRING
|
||||
val _artistGroupingName: String
|
||||
get() = _mediaStoreAlbumArtistName ?: _mediaStoreArtistName ?: MediaStore.UNKNOWN_STRING
|
||||
|
||||
/** Internal field. Do not use. */
|
||||
val internalIsMissingAlbum: Boolean
|
||||
get() = mAlbum == null
|
||||
val _isMissingAlbum: Boolean
|
||||
get() = _album == null
|
||||
/** Internal field. Do not use. */
|
||||
val internalIsMissingArtist: Boolean
|
||||
get() = mAlbum?.internalIsMissingArtist ?: true
|
||||
val _isMissingArtist: Boolean
|
||||
get() = _album?._isMissingArtist ?: true
|
||||
/** Internal field. Do not use. */
|
||||
val internalIsMissingGenre: Boolean
|
||||
get() = mGenre == null
|
||||
val _isMissingGenre: Boolean
|
||||
get() = _genre == null
|
||||
|
||||
/** Internal method. Do not use. */
|
||||
fun internalLinkAlbum(album: Album) {
|
||||
mAlbum = album
|
||||
fun _linkAlbum(album: Album) {
|
||||
_album = album
|
||||
}
|
||||
|
||||
/** Internal method. Do not use. */
|
||||
fun internalLinkGenre(genre: Genre) {
|
||||
mGenre = genre
|
||||
fun _linkGenre(genre: Genre) {
|
||||
_genre = genre
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -164,11 +162,11 @@ data class Album(
|
|||
/** The songs of this album. */
|
||||
val songs: List<Song>,
|
||||
/** Internal field. Do not use. */
|
||||
val internalGroupingArtistName: String,
|
||||
val _artistGroupingName: String,
|
||||
) : MusicParent() {
|
||||
init {
|
||||
for (song in songs) {
|
||||
song.internalLinkAlbum(this)
|
||||
song._linkAlbum(this)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -187,24 +185,24 @@ data class Album(
|
|||
|
||||
/** The formatted total duration of this album */
|
||||
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. */
|
||||
val artist: Artist
|
||||
get() = unlikelyToBeNull(mArtist)
|
||||
get() = unlikelyToBeNull(_artist)
|
||||
|
||||
/** Internal field. Do not use. */
|
||||
val internalArtistGroupingId: Long
|
||||
get() = internalGroupingArtistName.lowercase().hashCode().toLong()
|
||||
val _artistGroupingId: Long
|
||||
get() = _artistGroupingName.lowercase().hashCode().toLong()
|
||||
|
||||
/** Internal field. Do not use. */
|
||||
val internalIsMissingArtist: Boolean
|
||||
get() = mArtist == null
|
||||
val _isMissingArtist: Boolean
|
||||
get() = _artist == null
|
||||
|
||||
/** Internal method. Do not use. */
|
||||
fun internalLinkArtist(artist: Artist) {
|
||||
mArtist = artist
|
||||
fun _linkArtist(artist: Artist) {
|
||||
_artist = artist
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -219,7 +217,7 @@ data class Artist(
|
|||
) : MusicParent() {
|
||||
init {
|
||||
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() {
|
||||
init {
|
||||
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 */
|
||||
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. */
|
||||
suspend fun index(context: Context): Response {
|
||||
suspend fun load(context: Context): Response {
|
||||
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) {
|
||||
callback.onMusicUpdate(newResponse)
|
||||
}
|
||||
return newResponse
|
||||
}
|
||||
|
||||
private fun indexImpl(context: Context): Response {
|
||||
private fun loadImpl(context: Context): Response {
|
||||
val notGranted =
|
||||
ContextCompat.checkSelfPermission(context, Manifest.permission.READ_EXTERNAL_STORAGE) ==
|
||||
PackageManager.PERMISSION_DENIED
|
||||
|
@ -81,7 +81,7 @@ class MusicStore private constructor() {
|
|||
val response =
|
||||
try {
|
||||
val start = System.currentTimeMillis()
|
||||
val library = Indexer.run(context)
|
||||
val library = Indexer.index(context)
|
||||
if (library != null) {
|
||||
logD(
|
||||
"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
|
||||
* rust.
|
||||
*
|
||||
* TODO: Add the exception to the "FAILED" ErrorKind
|
||||
*/
|
||||
sealed class Response {
|
||||
class Ok(val library: Library) : Response()
|
||||
|
|
|
@ -17,28 +17,9 @@
|
|||
|
||||
package org.oxycblt.auxio.music
|
||||
|
||||
import android.text.format.DateUtils
|
||||
import org.oxycblt.auxio.util.logD
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
// --- 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 {
|
||||
private val musicStore = MusicStore.getInstance()
|
||||
|
||||
private val mLoaderResponse = MutableLiveData<MusicStore.Response?>(null)
|
||||
val loaderResponse: LiveData<MusicStore.Response?> = mLoaderResponse
|
||||
private val _loaderResponse = MutableLiveData<MusicStore.Response?>(null)
|
||||
val loaderResponse: LiveData<MusicStore.Response?> = _loaderResponse
|
||||
|
||||
private var isBusy = false
|
||||
|
||||
|
@ -42,29 +42,29 @@ class MusicViewModel : ViewModel(), MusicStore.Callback {
|
|||
* navigated to and because SnackBars will have the best UX here.
|
||||
*/
|
||||
fun loadMusic(context: Context) {
|
||||
if (mLoaderResponse.value != null || isBusy) {
|
||||
if (_loaderResponse.value != null || isBusy) {
|
||||
logD("Loader is busy/already completed, not reloading")
|
||||
return
|
||||
}
|
||||
|
||||
isBusy = true
|
||||
mLoaderResponse.value = null
|
||||
_loaderResponse.value = null
|
||||
|
||||
viewModelScope.launch {
|
||||
val result = musicStore.index(context)
|
||||
mLoaderResponse.value = result
|
||||
val result = musicStore.load(context)
|
||||
_loaderResponse.value = result
|
||||
isBusy = false
|
||||
}
|
||||
}
|
||||
|
||||
fun reloadMusic(context: Context) {
|
||||
logD("Reloading music library")
|
||||
mLoaderResponse.value = null
|
||||
_loaderResponse.value = null
|
||||
loadMusic(context)
|
||||
}
|
||||
|
||||
override fun onMusicUpdate(response: MusicStore.Response) {
|
||||
mLoaderResponse.value = response
|
||||
_loaderResponse.value = response
|
||||
}
|
||||
|
||||
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
|
||||
* 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
|
||||
*/
|
||||
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
|
||||
*/
|
||||
class ExcludedViewModel(private val excludedDatabase: ExcludedDatabase) : ViewModel() {
|
||||
private val mPaths = MutableLiveData(mutableListOf<String>())
|
||||
private val _paths = MutableLiveData(mutableListOf<String>())
|
||||
val paths: LiveData<MutableList<String>>
|
||||
get() = mPaths
|
||||
get() = _paths
|
||||
|
||||
var isModified: Boolean = false
|
||||
private set
|
||||
|
@ -53,10 +53,10 @@ class ExcludedViewModel(private val excludedDatabase: ExcludedDatabase) : ViewMo
|
|||
* called.
|
||||
*/
|
||||
fun addPath(path: String) {
|
||||
val paths = unlikelyToBeNull(mPaths.value)
|
||||
val paths = unlikelyToBeNull(_paths.value)
|
||||
if (!paths.contains(path)) {
|
||||
paths.add(path)
|
||||
mPaths.value = mPaths.value
|
||||
_paths.value = _paths.value
|
||||
isModified = true
|
||||
}
|
||||
}
|
||||
|
@ -66,8 +66,8 @@ class ExcludedViewModel(private val excludedDatabase: ExcludedDatabase) : ViewMo
|
|||
* [save] is called.
|
||||
*/
|
||||
fun removePath(path: String) {
|
||||
unlikelyToBeNull(mPaths.value).remove(path)
|
||||
mPaths.value = mPaths.value
|
||||
unlikelyToBeNull(_paths.value).remove(path)
|
||||
_paths.value = _paths.value
|
||||
isModified = true
|
||||
}
|
||||
|
||||
|
@ -75,7 +75,7 @@ class ExcludedViewModel(private val excludedDatabase: ExcludedDatabase) : ViewMo
|
|||
fun save(onDone: () -> Unit) {
|
||||
viewModelScope.launch(Dispatchers.IO) {
|
||||
val start = System.currentTimeMillis()
|
||||
excludedDatabase.writePaths(unlikelyToBeNull(mPaths.value))
|
||||
excludedDatabase.writePaths(unlikelyToBeNull(_paths.value))
|
||||
isModified = false
|
||||
onDone()
|
||||
this@ExcludedViewModel.logD(
|
||||
|
@ -90,7 +90,7 @@ class ExcludedViewModel(private val excludedDatabase: ExcludedDatabase) : ViewMo
|
|||
isModified = false
|
||||
|
||||
val dbPaths = excludedDatabase.readPaths()
|
||||
withContext(Dispatchers.Main) { mPaths.value = dbPaths.toMutableList() }
|
||||
withContext(Dispatchers.Main) { _paths.value = dbPaths.toMutableList() }
|
||||
|
||||
this@ExcludedViewModel.logD(
|
||||
"Path load completed successfully in ${System.currentTimeMillis() - start}ms")
|
||||
|
|
|
@ -20,6 +20,7 @@ package org.oxycblt.auxio.playback
|
|||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import android.view.MenuItem
|
||||
import androidx.appcompat.widget.Toolbar
|
||||
import androidx.core.view.updatePadding
|
||||
import androidx.fragment.app.Fragment
|
||||
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.music.MusicParent
|
||||
import org.oxycblt.auxio.music.Song
|
||||
import org.oxycblt.auxio.music.toDuration
|
||||
import org.oxycblt.auxio.playback.state.RepeatMode
|
||||
import org.oxycblt.auxio.ui.MainNavigationAction
|
||||
import org.oxycblt.auxio.ui.NavigationViewModel
|
||||
import org.oxycblt.auxio.ui.ViewBindingFragment
|
||||
import org.oxycblt.auxio.util.clamp
|
||||
import org.oxycblt.auxio.util.formatDuration
|
||||
import org.oxycblt.auxio.util.getAttrColorSafe
|
||||
import org.oxycblt.auxio.util.logD
|
||||
import org.oxycblt.auxio.util.stateList
|
||||
|
@ -55,7 +56,8 @@ import org.oxycblt.auxio.util.textSafe
|
|||
class PlaybackPanelFragment :
|
||||
ViewBindingFragment<FragmentPlaybackPanelBinding>(),
|
||||
Slider.OnChangeListener,
|
||||
Slider.OnSliderTouchListener {
|
||||
Slider.OnSliderTouchListener,
|
||||
Toolbar.OnMenuItemClickListener {
|
||||
private val playbackModel: PlaybackViewModel by activityViewModels()
|
||||
private val navModel: NavigationViewModel by activityViewModels()
|
||||
|
||||
|
@ -77,16 +79,7 @@ class PlaybackPanelFragment :
|
|||
|
||||
binding.playbackToolbar.apply {
|
||||
setNavigationOnClickListener { navModel.mainNavigateTo(MainNavigationAction.COLLAPSE) }
|
||||
|
||||
setOnMenuItemClickListener { item ->
|
||||
if (item.itemId == R.id.action_queue) {
|
||||
navModel.mainNavigateTo(MainNavigationAction.QUEUE)
|
||||
true
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
setOnMenuItemClickListener(this@PlaybackPanelFragment)
|
||||
queueItem = menu.findItem(R.id.action_queue)
|
||||
}
|
||||
|
||||
|
@ -127,6 +120,8 @@ class PlaybackPanelFragment :
|
|||
binding.playbackSkipNext.setOnClickListener { playbackModel.skipNext() }
|
||||
binding.playbackShuffle.setOnClickListener { playbackModel.invertShuffled() }
|
||||
|
||||
binding.playbackSeekBar.apply {}
|
||||
|
||||
// --- VIEWMODEL SETUP --
|
||||
|
||||
playbackModel.song.observe(viewLifecycleOwner, ::updateSong)
|
||||
|
@ -146,11 +141,22 @@ class PlaybackPanelFragment :
|
|||
}
|
||||
|
||||
override fun onDestroyBinding(binding: FragmentPlaybackPanelBinding) {
|
||||
binding.playbackToolbar.setOnMenuItemClickListener(null)
|
||||
binding.playbackSong.isSelected = false
|
||||
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) {
|
||||
requireBinding().playbackPosition.isActivated = true
|
||||
}
|
||||
|
@ -162,7 +168,7 @@ class PlaybackPanelFragment :
|
|||
|
||||
override fun onValueChange(slider: Slider, value: Float, fromUser: Boolean) {
|
||||
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
|
||||
val seconds = song.seconds
|
||||
binding.playbackDuration.textSafe = seconds.toDuration(false)
|
||||
binding.playbackDuration.textSafe = seconds.formatDuration(false)
|
||||
binding.playbackSeekBar.apply {
|
||||
isEnabled = seconds > 0L
|
||||
valueTo = max(seconds, 1L).toFloat()
|
||||
|
@ -197,7 +203,7 @@ class PlaybackPanelFragment :
|
|||
val binding = requireBinding()
|
||||
if (!binding.playbackPosition.isActivated) {
|
||||
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 playbackManager = PlaybackStateManager.getInstance()
|
||||
|
||||
// Playback
|
||||
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 var intentUri: Uri? = null
|
||||
|
||||
private val _song = MutableLiveData<Song?>()
|
||||
/** The current 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] */
|
||||
val parent: LiveData<MusicParent?>
|
||||
get() = mParent
|
||||
|
||||
get() = _parent
|
||||
private val _isPlaying = MutableLiveData(false)
|
||||
val isPlaying: LiveData<Boolean>
|
||||
get() = mIsPlaying
|
||||
get() = _isPlaying
|
||||
private val _positionSecs = MutableLiveData(0L)
|
||||
/** The current playback position, in seconds */
|
||||
val positionSecs: LiveData<Long>
|
||||
get() = mPositionSecs
|
||||
get() = _positionSecs
|
||||
private val _repeatMode = MutableLiveData(RepeatMode.NONE)
|
||||
/** The current repeat mode, see [RepeatMode] for more information */
|
||||
val repeatMode: LiveData<RepeatMode>
|
||||
get() = mRepeatMode
|
||||
get() = _repeatMode
|
||||
private val _isShuffled = MutableLiveData(false)
|
||||
val isShuffled: LiveData<Boolean>
|
||||
get() = mIsShuffled
|
||||
get() = _isShuffled
|
||||
|
||||
private val _nextUp = MutableLiveData(listOf<Song>())
|
||||
/** The queue, without the previous items. */
|
||||
val nextUp: LiveData<List<Song>>
|
||||
get() = mNextUp
|
||||
get() = _nextUp
|
||||
|
||||
init {
|
||||
playbackManager.addCallback(this)
|
||||
|
@ -167,7 +159,7 @@ class PlaybackViewModel : ViewModel(), PlaybackStateManager.Callback {
|
|||
} else {
|
||||
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) {
|
||||
val index =
|
||||
adapterIndex + (playbackManager.queue.size - unlikelyToBeNull(mNextUp.value).size)
|
||||
adapterIndex + (playbackManager.queue.size - unlikelyToBeNull(_nextUp.value).size)
|
||||
if (index in playbackManager.queue.indices) {
|
||||
apply()
|
||||
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.
|
||||
*/
|
||||
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 to = adapterTo + delta
|
||||
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.
|
||||
*/
|
||||
fun setupPlayback(context: Context) {
|
||||
val intentUri = mIntentUri
|
||||
val intentUri = intentUri
|
||||
|
||||
if (intentUri != null) {
|
||||
playWithUriInternal(intentUri, context)
|
||||
// Remove the uri after finishing the calls so that this does not fire again.
|
||||
mIntentUri = null
|
||||
this.intentUri = null
|
||||
} else if (!playbackManager.isInitialized) {
|
||||
// Otherwise just restore
|
||||
viewModelScope.launch { playbackManager.restoreState(context) }
|
||||
|
@ -319,33 +311,33 @@ class PlaybackViewModel : ViewModel(), PlaybackStateManager.Callback {
|
|||
}
|
||||
|
||||
override fun onIndexMoved(index: Int) {
|
||||
mSong.value = playbackManager.song
|
||||
mNextUp.value = playbackManager.queue.slice(index.inc() until playbackManager.queue.size)
|
||||
_song.value = playbackManager.song
|
||||
_nextUp.value = playbackManager.queue.slice(index + 1 until playbackManager.queue.size)
|
||||
}
|
||||
|
||||
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?) {
|
||||
mParent.value = playbackManager.parent
|
||||
mSong.value = playbackManager.song
|
||||
mNextUp.value = queue.slice(index.inc() until queue.size)
|
||||
_parent.value = playbackManager.parent
|
||||
_song.value = playbackManager.song
|
||||
_nextUp.value = queue.slice(index + 1 until queue.size)
|
||||
}
|
||||
|
||||
override fun onPositionChanged(positionMs: Long) {
|
||||
mPositionSecs.value = positionMs / 1000
|
||||
_positionSecs.value = positionMs / 1000
|
||||
}
|
||||
|
||||
override fun onPlayingChanged(isPlaying: Boolean) {
|
||||
mIsPlaying.value = isPlaying
|
||||
_isPlaying.value = isPlaying
|
||||
}
|
||||
|
||||
override fun onShuffledChanged(isShuffled: Boolean) {
|
||||
mIsShuffled.value = isShuffled
|
||||
_isShuffled.value = isShuffled
|
||||
}
|
||||
|
||||
override fun onRepeatChanged(repeatMode: RepeatMode) {
|
||||
mRepeatMode.value = repeatMode
|
||||
_repeatMode.value = repeatMode
|
||||
}
|
||||
}
|
||||
|
|
|
@ -118,31 +118,31 @@ class HybridBackingData<T>(
|
|||
private val adapter: RecyclerView.Adapter<*>,
|
||||
diffCallback: DiffUtil.ItemCallback<T>
|
||||
) : BackingData<T>() {
|
||||
private var mCurrentList = mutableListOf<T>()
|
||||
private var _currentList = mutableListOf<T>()
|
||||
val currentList: List<T>
|
||||
get() = mCurrentList
|
||||
get() = _currentList
|
||||
|
||||
private val differ = AsyncListDiffer(adapter, diffCallback)
|
||||
|
||||
override fun getItem(position: Int): T = mCurrentList[position]
|
||||
override fun getItemCount(): Int = mCurrentList.size
|
||||
override fun getItem(position: Int): T = _currentList[position]
|
||||
override fun getItemCount(): Int = _currentList.size
|
||||
|
||||
fun submitList(newData: List<T>, onDone: () -> Unit = {}) {
|
||||
if (newData != mCurrentList) {
|
||||
mCurrentList = newData.toMutableList()
|
||||
if (newData != _currentList) {
|
||||
_currentList = newData.toMutableList()
|
||||
differ.submitList(newData, onDone)
|
||||
}
|
||||
}
|
||||
|
||||
fun moveItems(from: Int, to: Int) {
|
||||
mCurrentList.add(to, mCurrentList.removeAt(from))
|
||||
differ.rewriteListUnsafe(mCurrentList)
|
||||
_currentList.add(to, _currentList.removeAt(from))
|
||||
differ.rewriteListUnsafe(_currentList)
|
||||
adapter.notifyItemMoved(from, to)
|
||||
}
|
||||
|
||||
fun removeItem(at: Int) {
|
||||
mCurrentList.removeAt(at)
|
||||
differ.rewriteListUnsafe(mCurrentList)
|
||||
_currentList.removeAt(at)
|
||||
differ.rewriteListUnsafe(_currentList)
|
||||
adapter.notifyItemRemoved(at)
|
||||
}
|
||||
|
||||
|
@ -152,7 +152,7 @@ class HybridBackingData<T>(
|
|||
* can do to marry the adapter primitives with DiffUtil.
|
||||
*/
|
||||
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())
|
||||
differImmutableListField.set(this, newList)
|
||||
}
|
||||
|
|
|
@ -41,23 +41,24 @@ import org.oxycblt.auxio.util.logD
|
|||
*
|
||||
* All access should be done with [PlaybackStateManager.getInstance].
|
||||
* @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() {
|
||||
private val musicStore = MusicStore.getInstance()
|
||||
private val settingsManager = SettingsManager.getInstance()
|
||||
|
||||
// Playback
|
||||
private var mutableQueue = mutableListOf<Song>()
|
||||
|
||||
/** The currently playing song. Null if there isn't one */
|
||||
val song
|
||||
get() = queue.getOrNull(index)
|
||||
/** The parent the queue is based on, null if all songs */
|
||||
var parent: MusicParent? = null
|
||||
private set
|
||||
private var _queue = mutableListOf<Song>()
|
||||
/** The current queue determined by [parent] */
|
||||
val queue
|
||||
get() = mutableQueue
|
||||
get() = _queue
|
||||
/** The current position in the queue */
|
||||
var index = -1
|
||||
private set
|
||||
|
@ -160,8 +161,8 @@ class PlaybackStateManager private constructor() {
|
|||
fun next() {
|
||||
// Increment the index, if it cannot be incremented any further, then
|
||||
// repeat and pause/resume playback depending on the setting
|
||||
if (index < mutableQueue.lastIndex) {
|
||||
goto(index.inc(), true)
|
||||
if (index < _queue.lastIndex) {
|
||||
goto(index + 1, true)
|
||||
} else {
|
||||
goto(0, repeatMode == RepeatMode.ALL)
|
||||
}
|
||||
|
@ -174,7 +175,7 @@ class PlaybackStateManager private constructor() {
|
|||
rewind()
|
||||
isPlaying = true
|
||||
} 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. */
|
||||
fun playNext(song: Song) {
|
||||
mutableQueue.add(index.inc(), song)
|
||||
_queue.add(index + 1, song)
|
||||
notifyQueueChanged()
|
||||
}
|
||||
|
||||
/** Add a list of [songs] to the top of the queue. */
|
||||
fun playNext(songs: List<Song>) {
|
||||
mutableQueue.addAll(index.inc(), songs)
|
||||
_queue.addAll(index + 1, songs)
|
||||
notifyQueueChanged()
|
||||
}
|
||||
|
||||
/** Add a [song] to the end of the queue. */
|
||||
fun addToQueue(song: Song) {
|
||||
mutableQueue.add(song)
|
||||
_queue.add(song)
|
||||
notifyQueueChanged()
|
||||
}
|
||||
|
||||
/** Add a list of [songs] to the end of the queue. */
|
||||
fun addToQueue(songs: List<Song>) {
|
||||
mutableQueue.addAll(songs)
|
||||
_queue.addAll(songs)
|
||||
notifyQueueChanged()
|
||||
}
|
||||
|
||||
/** Move a queue item at [from] to a position at [to]. Will ignore invalid indexes. */
|
||||
fun moveQueueItem(from: Int, to: Int) {
|
||||
logD("Moving item $from to position $to")
|
||||
mutableQueue.add(to, mutableQueue.removeAt(from))
|
||||
_queue.add(to, _queue.removeAt(from))
|
||||
notifyQueueChanged()
|
||||
}
|
||||
|
||||
/** Remove a queue item at [index]. Will ignore invalid indexes. */
|
||||
fun removeQueueItem(index: Int) {
|
||||
logD("Removing item ${mutableQueue[index].rawName}")
|
||||
mutableQueue.removeAt(index)
|
||||
logD("Removing item ${_queue[index].rawName}")
|
||||
_queue.removeAt(index)
|
||||
notifyQueueChanged()
|
||||
}
|
||||
|
||||
|
@ -240,7 +241,7 @@ class PlaybackStateManager private constructor() {
|
|||
) {
|
||||
if (shuffled) {
|
||||
if (regenShuffledQueue) {
|
||||
mutableQueue =
|
||||
_queue =
|
||||
parent
|
||||
.let { parent ->
|
||||
when (parent) {
|
||||
|
@ -253,15 +254,15 @@ class PlaybackStateManager private constructor() {
|
|||
.toMutableList()
|
||||
}
|
||||
|
||||
mutableQueue.shuffle()
|
||||
_queue.shuffle()
|
||||
|
||||
if (keep != null) {
|
||||
mutableQueue.add(0, mutableQueue.removeAt(mutableQueue.indexOf(keep)))
|
||||
_queue.add(0, _queue.removeAt(_queue.indexOf(keep)))
|
||||
}
|
||||
|
||||
index = 0
|
||||
} else {
|
||||
mutableQueue =
|
||||
_queue =
|
||||
parent
|
||||
.let { parent ->
|
||||
when (parent) {
|
||||
|
@ -343,7 +344,7 @@ class PlaybackStateManager private constructor() {
|
|||
if (state != null) {
|
||||
index = state.index
|
||||
parent = state.parent
|
||||
mutableQueue = state.queue.toMutableList()
|
||||
_queue = state.queue.toMutableList()
|
||||
repeatMode = state.repeatMode
|
||||
isShuffled = state.isShuffled
|
||||
|
||||
|
@ -372,7 +373,7 @@ class PlaybackStateManager private constructor() {
|
|||
PlaybackStateDatabase.SavedState(
|
||||
index = index,
|
||||
parent = parent,
|
||||
queue = mutableQueue,
|
||||
queue = _queue,
|
||||
positionMs = positionMs,
|
||||
isShuffled = isShuffled,
|
||||
repeatMode = repeatMode))
|
||||
|
|
|
@ -19,8 +19,10 @@ package org.oxycblt.auxio.search
|
|||
|
||||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import android.view.MenuItem
|
||||
import android.view.View
|
||||
import android.view.inputmethod.InputMethodManager
|
||||
import androidx.appcompat.widget.Toolbar
|
||||
import androidx.core.view.isInvisible
|
||||
import androidx.core.view.postDelayed
|
||||
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.
|
||||
* @author OxygenCobalt
|
||||
*/
|
||||
class SearchFragment : ViewBindingFragment<FragmentSearchBinding>(), MenuItemListener {
|
||||
class SearchFragment :
|
||||
ViewBindingFragment<FragmentSearchBinding>(),
|
||||
MenuItemListener,
|
||||
Toolbar.OnMenuItemClickListener {
|
||||
// SearchViewModel is only scoped to this Fragment
|
||||
private val searchModel: SearchViewModel by viewModels()
|
||||
private val playbackModel: PlaybackViewModel by activityViewModels()
|
||||
|
@ -75,15 +80,7 @@ class SearchFragment : ViewBindingFragment<FragmentSearchBinding>(), MenuItemLis
|
|||
findNavController().navigateUp()
|
||||
}
|
||||
|
||||
setOnMenuItemClickListener { item ->
|
||||
if (item.itemId != R.id.submenu_filtering) {
|
||||
searchModel.updateFilterModeWithId(context, item.itemId)
|
||||
item.isChecked = true
|
||||
true
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
setOnMenuItemClickListener(this@SearchFragment)
|
||||
}
|
||||
|
||||
binding.searchEditText.apply {
|
||||
|
@ -116,11 +113,25 @@ class SearchFragment : ViewBindingFragment<FragmentSearchBinding>(), MenuItemLis
|
|||
}
|
||||
|
||||
override fun onDestroyBinding(binding: FragmentSearchBinding) {
|
||||
super.onDestroyBinding(binding)
|
||||
binding.searchToolbar.setOnMenuItemClickListener(null)
|
||||
binding.searchRecycler.adapter = 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) {
|
||||
when (item) {
|
||||
is Song -> playbackModel.playSong(item)
|
||||
|
|
|
@ -43,30 +43,30 @@ class SearchViewModel : ViewModel() {
|
|||
private val musicStore = MusicStore.getInstance()
|
||||
private val settingsManager = SettingsManager.getInstance()
|
||||
|
||||
private val mSearchResults = MutableLiveData(listOf<Item>())
|
||||
private var mFilterMode: DisplayMode? = null
|
||||
private var mLastQuery: String? = null
|
||||
private val _searchResults = MutableLiveData(listOf<Item>())
|
||||
private var _filterMode: DisplayMode? = null
|
||||
private var lastQuery: String? = null
|
||||
|
||||
/** Current search results from the last [search] call. */
|
||||
val searchResults: LiveData<List<Item>>
|
||||
get() = mSearchResults
|
||||
get() = _searchResults
|
||||
val filterMode: DisplayMode?
|
||||
get() = mFilterMode
|
||||
get() = _filterMode
|
||||
|
||||
init {
|
||||
mFilterMode = settingsManager.searchFilterMode
|
||||
_filterMode = settingsManager.searchFilterMode
|
||||
}
|
||||
|
||||
/**
|
||||
* Use [query] to perform a search of the music library. Will push results to [searchResults].
|
||||
*/
|
||||
fun search(context: Context, query: String?) {
|
||||
mLastQuery = query
|
||||
lastQuery = query
|
||||
|
||||
val library = musicStore.library
|
||||
if (query.isNullOrEmpty() || library == null) {
|
||||
logD("No music/query, ignoring search")
|
||||
mSearchResults.value = listOf()
|
||||
_searchResults.value = listOf()
|
||||
return
|
||||
}
|
||||
|
||||
|
@ -79,48 +79,48 @@ class SearchViewModel : ViewModel() {
|
|||
|
||||
// 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 ->
|
||||
results.add(Header(-1, R.string.lbl_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 ->
|
||||
results.add(Header(-2, R.string.lbl_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 ->
|
||||
results.add(Header(-3, R.string.lbl_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 ->
|
||||
results.add(Header(-4, R.string.lbl_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]. */
|
||||
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].
|
||||
*/
|
||||
fun updateFilterModeWithId(context: Context, @IdRes id: Int) {
|
||||
mFilterMode =
|
||||
_filterMode =
|
||||
when (id) {
|
||||
R.id.option_filter_songs -> DisplayMode.SHOW_SONGS
|
||||
R.id.option_filter_albums -> DisplayMode.SHOW_ALBUMS
|
||||
|
@ -129,9 +129,9 @@ class SearchViewModel : ViewModel() {
|
|||
else -> null
|
||||
}
|
||||
|
||||
logD("Updating filter mode to $mFilterMode")
|
||||
logD("Updating filter mode to $_filterMode")
|
||||
|
||||
settingsManager.searchFilterMode = mFilterMode
|
||||
settingsManager.searchFilterMode = _filterMode
|
||||
|
||||
refresh(context)
|
||||
}
|
||||
|
|
|
@ -32,8 +32,8 @@ import org.oxycblt.auxio.BuildConfig
|
|||
import org.oxycblt.auxio.R
|
||||
import org.oxycblt.auxio.databinding.FragmentAboutBinding
|
||||
import org.oxycblt.auxio.home.HomeViewModel
|
||||
import org.oxycblt.auxio.music.toDuration
|
||||
import org.oxycblt.auxio.ui.ViewBindingFragment
|
||||
import org.oxycblt.auxio.util.formatDuration
|
||||
import org.oxycblt.auxio.util.logD
|
||||
import org.oxycblt.auxio.util.showToast
|
||||
import org.oxycblt.auxio.util.systemBarInsetsCompat
|
||||
|
@ -64,7 +64,8 @@ class AboutFragment : ViewBindingFragment<FragmentAboutBinding>() {
|
|||
homeModel.songs.observe(viewLifecycleOwner) { songs ->
|
||||
binding.aboutSongCount.textSafe = getString(R.string.fmt_songs_loaded, songs.size)
|
||||
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 ->
|
||||
|
|
|
@ -24,36 +24,39 @@ import org.oxycblt.auxio.music.Music
|
|||
|
||||
/** A ViewModel that handles complicated navigation situations. */
|
||||
class NavigationViewModel : ViewModel() {
|
||||
private val mMainNavigationAction = MutableLiveData<MainNavigationAction?>()
|
||||
private val _mainNavigationAction = MutableLiveData<MainNavigationAction?>()
|
||||
/** Flag for main fragment navigation. Intended for MainFragment use only. */
|
||||
val mainNavigationAction: LiveData<MainNavigationAction?>
|
||||
get() = mMainNavigationAction
|
||||
get() = _mainNavigationAction
|
||||
|
||||
private val mExploreNavigationItem = MutableLiveData<Music?>()
|
||||
/** Flag for unified navigation. Observe this to coordinate navigation to an item's UI. */
|
||||
private val _exploreNavigationItem = MutableLiveData<Music?>()
|
||||
/**
|
||||
* Flag for navigation within the explore fragments. Observe this to coordinate navigation to an
|
||||
* item's UI.
|
||||
*/
|
||||
val exploreNavigationItem: LiveData<Music?>
|
||||
get() = mExploreNavigationItem
|
||||
get() = _exploreNavigationItem
|
||||
|
||||
/** Notify MainFragment to navigate to the location outlined in [MainNavigationAction]. */
|
||||
fun mainNavigateTo(action: MainNavigationAction) {
|
||||
if (mMainNavigationAction.value != null) return
|
||||
mMainNavigationAction.value = action
|
||||
if (_mainNavigationAction.value != null) return
|
||||
_mainNavigationAction.value = action
|
||||
}
|
||||
|
||||
/** Mark that the main navigation process is done. */
|
||||
fun finishMainNavigation() {
|
||||
mMainNavigationAction.value = null
|
||||
_mainNavigationAction.value = null
|
||||
}
|
||||
|
||||
/** Navigate to an item's detail menu, whether a song/album/artist */
|
||||
fun exploreNavigateTo(item: Music) {
|
||||
if (mExploreNavigationItem.value != null) return
|
||||
mExploreNavigationItem.value = item
|
||||
if (_exploreNavigationItem.value != null) return
|
||||
_exploreNavigationItem.value = item
|
||||
}
|
||||
|
||||
/** Mark that the item navigation process is done. */
|
||||
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.
|
||||
*/
|
||||
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. */
|
||||
val currentList: List<T>
|
||||
get() = mCurrentList
|
||||
get() = _currentList
|
||||
|
||||
override fun getItem(position: Int): T = mCurrentList[position]
|
||||
override fun getItemCount(): Int = mCurrentList.size
|
||||
override fun getItem(position: Int): T = _currentList[position]
|
||||
override fun getItemCount(): Int = _currentList.size
|
||||
|
||||
/**
|
||||
* 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")
|
||||
fun submitList(newList: List<T>) {
|
||||
mCurrentList = newList.toMutableList()
|
||||
_currentList = newList.toMutableList()
|
||||
adapter.notifyDataSetChanged()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -276,10 +276,10 @@ sealed class Sort(open val isAscending: Boolean) {
|
|||
* a non-equal result being propagated upwards.
|
||||
*/
|
||||
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 {
|
||||
for (comparator in mComparators) {
|
||||
for (comparator in _comparators) {
|
||||
val result = comparator.compare(a, b)
|
||||
if (result != 0) {
|
||||
return result
|
||||
|
|
|
@ -29,7 +29,7 @@ import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
|||
import org.oxycblt.auxio.util.logD
|
||||
|
||||
abstract class ViewBindingDialogFragment<T : ViewBinding> : DialogFragment() {
|
||||
private var mBinding: T? = null
|
||||
private var _binding: T? = null
|
||||
|
||||
protected abstract fun onCreateBinding(inflater: LayoutInflater): T
|
||||
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 val binding: T?
|
||||
get() = mBinding
|
||||
get() = _binding
|
||||
|
||||
protected fun requireBinding(): T {
|
||||
return requireNotNull(mBinding) {
|
||||
return requireNotNull(_binding) {
|
||||
"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,
|
||||
container: ViewGroup?,
|
||||
savedInstanceState: Bundle?
|
||||
): View = onCreateBinding(inflater).also { mBinding = it }.root
|
||||
): View = onCreateBinding(inflater).also { _binding = it }.root
|
||||
|
||||
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
|
||||
return MaterialAlertDialogBuilder(requireActivity(), theme).run {
|
||||
|
@ -68,6 +68,6 @@ abstract class ViewBindingDialogFragment<T : ViewBinding> : DialogFragment() {
|
|||
override fun onDestroyView() {
|
||||
super.onDestroyView()
|
||||
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. */
|
||||
abstract class ViewBindingFragment<T : ViewBinding> : Fragment() {
|
||||
private var mBinding: T? = null
|
||||
private var _binding: T? = null
|
||||
|
||||
protected abstract fun onCreateBinding(inflater: LayoutInflater): T
|
||||
protected open fun onBindingCreated(binding: T, savedInstanceState: Bundle?) {}
|
||||
protected open fun onDestroyBinding(binding: T) {}
|
||||
|
||||
protected val binding: T?
|
||||
get() = mBinding
|
||||
get() = _binding
|
||||
|
||||
protected fun requireBinding(): T {
|
||||
return requireNotNull(mBinding) {
|
||||
return requireNotNull(_binding) {
|
||||
"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,
|
||||
container: ViewGroup?,
|
||||
savedInstanceState: Bundle?
|
||||
): View = onCreateBinding(inflater).also { mBinding = it }.root
|
||||
): View = onCreateBinding(inflater).also { _binding = it }.root
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
|
@ -57,6 +57,6 @@ abstract class ViewBindingFragment<T : ViewBinding> : Fragment() {
|
|||
override fun onDestroyView() {
|
||||
super.onDestroyView()
|
||||
onDestroyBinding(requireBinding())
|
||||
mBinding = null
|
||||
_binding = null
|
||||
}
|
||||
}
|
||||
|
|
|
@ -18,6 +18,7 @@
|
|||
package org.oxycblt.auxio.util
|
||||
|
||||
import android.os.Looper
|
||||
import android.text.format.DateUtils
|
||||
import androidx.core.math.MathUtils
|
||||
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] */
|
||||
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"?>
|
||||
<resources>
|
||||
<!-- Note: Not actually transparent, making it transparent would actually make it translucent -->
|
||||
<color name="chrome_transparent">#01151515</color>
|
||||
|
||||
<color name="red_primary">#FFB4A8</color>
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<!-- Note: Not actually transparent, making it transparent would actually make it translucent -->
|
||||
<color name="chrome_transparent">#01fafafa</color>
|
||||
<color name="chrome_translucent">#80000000</color>
|
||||
|
||||
|
|
|
@ -9,7 +9,7 @@ buildscript {
|
|||
}
|
||||
|
||||
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 "androidx.navigation:navigation-safe-args-gradle-plugin:$navigation_version"
|
||||
classpath "com.diffplug.spotless:spotless-plugin-gradle:6.3.0"
|
||||
|
|
Loading…
Reference in a new issue